Fastjson 是 Java 生态中广泛使用的 JSON 解析库,其反序列化漏洞历经多年迭代仍未彻底根治。本文从防御视角出发,系统梳理 Fastjson 各版本的安全机制演进、机制失效的根本原因、Gadget 类型分类及防御建议,旨在为安全审计与架构设计提供全面参考。
一、 核心防御演进与机制失效路径
Fastjson 的安全演进史,本质上是一场“动态类型推导”与“攻击者 Gadget 挖掘”的博弈过程。
1. 1.2.24 以前:无 AutoType 阶段(完全反射驱动)
核心机制
早期版本采用纯反射驱动的反序列化模型。当使用JSON.parseObject(json, Target.class)或通用的JSON.parse(json)时,Fastjson 会根据 JSON 中的@type键动态加载类。
JSON 字符串 (含 @type) └──> DefaultJSONParser.parse() └──> JavaBeanDeserializer.deserializer() └──> MethodInvoker / FieldDeserializer └──> 反射调用 target.setXxx() / getXxx()风险本质与防御缺陷
无边界信任:
@type字段可指定任意类,完全信任输入内容,无类白名单机制。副作用方法触发:反序列化时会自动触发符合条件的
Setter、特定的Getter以及无参构造函数,从而触发“副作用方法”构造利用链。
2. 1.2.25 - 1.2.46:AutoType 初代(黑名单机制)
防御引入
引入checkAutoType(String typeName, Class<?> expectClass, int features)函数,默认关闭AutoType支持。若开启,则配合内置的denyList(黑名单)和acceptList(白名单)实现基础过滤。
架构缺陷一:静态黑名单的天然滞后性
黑名单机制属于消极防御。Java 生态中的第三方组件(如 Spring, Tomcat, Commons-Collections)极其庞大,黑名单永远无法全覆盖。
架构缺陷二:Class 缓存污染(Mapping Cache 绕过)
这是该阶段最经典的架构漏洞。Fastjson 为了提升性能,设计了类加载缓存机制(TypeUtils.mappings)。
[输入类名] ──> checkAutoType() 检查 ──> (不在黑名单) ──> Class.forName() 加载 │ [后续输入] <── 从 mappings 缓存直接返回 <── 注入 mappings 缓存 <──┘绕过原理:攻击者通过特定的基础类(如
java.lang.Class)触发解析,诱导 Fastjson 将恶意类加载并写入mappings缓存。在随后的解析中,恶意类直接跳过checkAutoType检查,从缓存中被直接返回。
3. 1.2.48 - 1.2.68:ExpectClass(期望类型)引入与类型收窄
防御升级
修复了缓存污染漏洞,并引入expectClass参数,实施类型子树收窄约束:
$$\text{if } (\text{clazz} \not\subseteq \text{expectClass}) \longrightarrow \text{REJECT}$$
绕过思路:利用“广泛继承根”(合法接口族)
当业务代码要求反序列化某个接口或基类时(例如JSON.parseObject(json, Serializable.class)),expectClass赋值为该接口。攻击者转向利用实现了这些广泛接口的“合规 Gadget”:
java.io.Serializablejava.lang.AutoCloseablejava.lang.Throwable
失效根本原因:安全边界 $\neq$ 类型边界。
expectClass允许的子树范围过大,恶意 Gadget 依然隐藏在合法的子树内部。
4. 1.2.69 - 1.2.80:安全模型收紧与基础设施类滥用
防御演进
不断扩充黑名单,强化expectClass校验,逐步推出SafeMode雏形。
攻击面转向
攻击者不再局限于常规的第三方组件漏洞,转而滥用 JDK 或常用框架的“基础设施类”:
特殊集合类:
Collection/Queue/Map的特殊实现。内部工具类:JDK 内部负责代理、动态绑定或特殊上下文切换的类。
共性利用模型:
$$\text{JSON Input} \longrightarrow \text{Object Graph Construction} \longrightarrow \text{Setter/Getter Chain} \longrightarrow \text{Indirect Sink Invocation}$$
5. 1.2.83+ & Fastjson v2:SafeMode 与架构重构
SafeMode 防御原理
全面关闭AutoType功能,不解析任何包含@type的动态类,强制执行严格 Schema 绑定(Strict Schema Mapping)。
Fastjson v2 架构变革
流式解析器(Streaming Parser):重写底层架构,极大降低对反射的依赖。
显式多态注册:废弃全局任意类反序列化,多态类必须通过
@JSONType(seeAlso = {...})显式声明。
二、 Gadget 类型分类(防御与审计视角)
从防御视角来看,不论 Gadget 如何变化,其落脚点(Sink)主要分为以下三类:
1. JNDI / 远程资源加载类(远程 Sink)
特征:通过触发
Context.lookup()或远程 URL 加载。典型代表:
com.sun.rowset.JdbcRowSetImpl。触发链:
setDataSourceName()$\rightarrow$setAutoCommit()$\rightarrow$InitialContext.lookup(dataSourceName)。
2. 本地命令执行型(OS 命令 Sink)
特征:直接或间接调用
Runtime.getRuntime().exec()或ProcessBuilder。典型代表:各类利用模板类(如
TemplatesImpl)加载恶意字节码,或利用脚本引擎(ScriptEngineManager)执行内联代码。
3. 反序列化副作用链(中间桥梁 Sink)
特征:本身不执行命令,但其
Setter方法会触发其他引擎的解析。分类:
XML 解析引擎(触发 XXE / 远程对象注入)
表达式引擎(触发 SpEL、EL、OGNL 表达式注入)
模板引擎(触发 Velocity、FreeMarker 模版注入)
三、 环境对抗演进
1. JDK 基线演进对漏洞利用的影响
| JDK 版本基线 | 默认配置行为 | 攻击面转移阻尼 |
| < JDK 8u121 / 7u131 | com.sun.jndi.rmi.object.trustURLCodebase=true | 黄金时代:可通过 RMI 远程加载任意恶意类。 |
| < JDK 8u191 / 7u201 | com.sun.jndi.ldap.object.trustURLCodebase=true | LDAP 时代:RMI 受阻,转而利用 LDAP 协议绕过。 |
| $\ge$ JDK 8u191 | 远程 Codebase 默认全部关闭 | 本地 Gadget 时代:被迫转向纯本地 Gadget 链组合(如利用BeanFactory触发本地命令执行)。 |
2. WAF 检测对抗(绕过特征分析)
WAF 通常基于规则匹配,攻击者常用以下机制实施绕过:
编码混淆:利用 Fastjson 支持的字符集,使用 Unicode 编码(如
\u0040\u0074\u0079\u0070\u0065代替@type)或十六进制/常规转义。结构扰动:改变 JSON Key 的顺序,或者插入大量无关的嵌套多层对象包装,冲刷 WAF 的深度扫描限制。
语法兼容路径:利用 Fastjson 独有的解析特性,如用
,、/、{}畸形闭合等非标准 JSON 语法绕过规范的 WAF 解析器。
四、 终极防御建议
依赖黑名单补丁无法从根本上解决 Fastjson 的安全问题,架构设计应转入深度防御模式:
1. 强制启用静态 Schema 绑定
拒绝使用通用的
JSON.parse(String text)。必须指定明确的 DTO 类:
JSON.parseObject(text, UserDTO.class)。禁止在 DTO 中使用
Object、Serializable等宽泛类型作为属性。
2. 开启 SafeMode / 升级 v2
Fastjson 1.x:在 JVM 启动参数中显式添加
-Dfastjson.parser.safeMode=true,或在代码初始化时执行ParserConfig.getGlobalInstance().setSafeMode(true)。Fastjson 2.x:全面向 v2 版本迁移,不开启兼容 1.x 的
@type解析模式。
3. 语义级输入规范化与 WAF 联动
解析前规范化(Canonicalization):在 WAF 或网关层对 JSON 进行统一的反转义与解码。
AST 语义检测:WAF 层的检测应基于 JSON AST(抽象语法树)结构检查是否存在非预期的键(如
@type),而非单纯的字符串正则表达式匹配。
总结:演进历程一览表
| 阶段 | 核心特征 | 核心缺陷机制 | 根本解决方向 |
| 无约束反射时代(< 1.2.24) | 完全信任输入,由@type反射驱动。 | 无边界约束,任意反序列化。 | 转向静态绑定:放弃“允许动态类型”的架构设计,全面转向强类型 Schema 绑定与SafeMode,方能从根源阻断反序列化攻击面。 |
| 黑名单补丁时代(1.2.25 - 1.2.46) | 引入checkAutoType,依赖静态黑名单 + 缓存。 | 黑名单不完备,缓存污染(Mapping 绕过)。 | ~ |
| 类型约束与安全模式时代(1.2.48+) | 引入expectClass限制类型树,逐步推行SafeMode。 | 类型约束过宽(合法接口族绕过),残留语法兼容路径。 | ~ |