Fastjson 只负责把 JSON 推进类型选择、对象创建和属性赋值。后续是否调用 JNDI、是否解析出可执行对象、是否成功修改 Tomcat 请求链,是三个独立阶段。
1 | Fastjson 类型恢复成功 |
历史实验经常只记录“Fastjson 版本”,导致同一个 Payload 在不同环境表现完全不同。这篇补充只解决兼容性定位,不提供远程利用服务和攻击载荷。
一、先拆开四层兼容条件
| 层级 | 必须固定的变量 | 常见失败结果 |
|---|---|---|
| JSON 解析 | Fastjson 精确版本、调用 API、Feature、AutoType、SafeMode | 类型未选择、直接拒绝、只生成 JSONObject |
| 对象行为 | 目标类、构造方法、Setter、传递依赖 | 对象创建失败、属性方法未调用 |
| JNDI/JDK | JDK 更新号、Provider、协议、过滤器、信任属性 | lookup 未发生、Reference 或对象被拒绝 |
| 容器注册 | Tomcat 版本、javax/jakarta、ClassLoader、内部 API | 植入代码执行,但请求链没有变化 |
版本号只能解释第一层的一部分。完整实验记录至少包括:
1 | Fastjson 依赖坐标和 JAR 哈希 |
javax.servlet.* 与 jakarta.servlet.* 由 Servlet 代际和中间件决定,不由 JDK 版本直接决定。Tomcat 9 的 Filter 字节码不能直接注册进 Tomcat 10,反过来也一样。
二、用 ProbeBean 只验证类型恢复
不要一上来就连接 JNDI 或修改容器。先用没有任何副作用的 Bean,确认解析器到底调用了什么。下面固定使用 Fastjson 1.2.83 的 1.x API,仅用于独立实验进程:
1 | <dependency> |
1 | package lab.fastjson; |
在隔离测试进程中,只允许实验包前缀:
1 | package lab.fastjson; |
预期输出顺序:
1 | [probe] constructor |
这个实验只证明三件事:@type 被接受、对象被创建、Setter 被调用。它没有网络访问、JNDI lookup、动态类定义和容器注册。
不要在生产应用中为了复现而开启全局 AutoType。实验必须使用独立进程、固定依赖、仅允许测试包,并阻断外部网络;结束后销毁环境。
三、沿数据流定位失败阶段
当历史样本或告警无法复现时,按顺序加观测点:
1 | 1. 原始 JSON 是否到达目标解析 API |
常见现象与定位:
| 现象 | 优先检查 |
|---|---|
| 结果只是 JSONObject | 调用 API、目标类型、AutoType 与 SafeMode |
| 构造执行但 Setter 未调用 | 属性名、可见性、Bean 元数据和 Feature |
| Setter 执行但无网络 | 参数值、业务分支、JNDI Provider 与出站策略 |
| 有 LDAP/RMI 连接但无新类 | JDK 更新、对象工厂过滤、远程 Codebase 限制 |
| 类已加载但 Filter 不生效 | javax/jakarta、Tomcat API、FilterDef/Map/Config |
| 重启后再次出现 | 漏洞入口、制品、配置、启动项或 Agent 仍存在 |
不要把 HTTP 500 当成最终证据。应保存完整异常链,并为每个阶段设置独立标记。只有看到 Tomcat、Spring 或其他中间件的运行时注册表发生变化,才能确认链条已经进入驻留阶段。
四、实验记录与生产处置分开
安全实验记录应形成一张矩阵:
| 项目 | 记录内容 |
|---|---|
| 解析器 | Fastjson 版本、API、ParserConfig、Feature |
| JDK | 完整更新号、模块、反序列化与 JNDI 属性 |
| 类加载 | Class 主版本、ClassLoader、CodeSource |
| 容器 | Tomcat 版本、Servlet API、Context 获取路径 |
| 结果 | 构造、Setter、lookup、网络、类加载、注册状态 |
生产告警的处置顺序则不同:
1 | 保存原始请求和解析异常 |
仅升级 Fastjson 不能清除已经注册的 Filter,单独移除 Filter 也不能修复仍可触发的解析入口。补丁、入口、运行时对象和复活机制必须分别验证,最后再用同一套 Probe 和运行时清单确认恢复结果。