Java 内存马(十一·补充):Fastjson 实验环境与兼容性

Fastjson 只负责把 JSON 推进类型选择、对象创建和属性赋值。后续是否调用 JNDI、是否解析出可执行对象、是否成功修改 Tomcat 请求链,是三个独立阶段。

1
2
3
4
Fastjson 类型恢复成功
≠ JNDI lookup 成功
≠ 返回对象被接受
≠ 内存马注册成功

历史实验经常只记录“Fastjson 版本”,导致同一个 Payload 在不同环境表现完全不同。这篇补充只解决兼容性定位,不提供远程利用服务和攻击载荷。

一、先拆开四层兼容条件

层级 必须固定的变量 常见失败结果
JSON 解析 Fastjson 精确版本、调用 API、Feature、AutoType、SafeMode 类型未选择、直接拒绝、只生成 JSONObject
对象行为 目标类、构造方法、Setter、传递依赖 对象创建失败、属性方法未调用
JNDI/JDK JDK 更新号、Provider、协议、过滤器、信任属性 lookup 未发生、Reference 或对象被拒绝
容器注册 Tomcat 版本、javax/jakarta、ClassLoader、内部 API 植入代码执行,但请求链没有变化

版本号只能解释第一层的一部分。完整实验记录至少包括:

1
2
3
4
5
6
7
8
Fastjson 依赖坐标和 JAR 哈希
实际调用的 parse/parseObject 重载
ParserConfig 与 Feature
JDK 完整版本和 JVM 参数
Tomcat 完整版本
Servlet API 包名
目标类的 ClassLoader 与 CodeSource
每一层的成功标记或完整异常

javax.servlet.* 与 jakarta.servlet.* 由 Servlet 代际和中间件决定,不由 JDK 版本直接决定。Tomcat 9 的 Filter 字节码不能直接注册进 Tomcat 10,反过来也一样。

二、用 ProbeBean 只验证类型恢复

不要一上来就连接 JNDI 或修改容器。先用没有任何副作用的 Bean,确认解析器到底调用了什么。下面固定使用 Fastjson 1.2.83 的 1.x API,仅用于独立实验进程:

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package lab.fastjson;

public final class ProbeBean {
private String value;

public ProbeBean() {
System.out.println("[probe] constructor");
}

public void setValue(String value) {
System.out.println("[probe] setValue=" + value);
this.value = value;
}

public String getValue() {
return value;
}
}

在隔离测试进程中,只允许实验包前缀:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package lab.fastjson;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public final class FastjsonTypeProbe {
public static void main(String[] args) {
ParserConfig config = new ParserConfig();
config.addAccept("lab.fastjson.");
config.setAutoTypeSupport(true);

String json = "{"
+ "\"@type\":\"lab.fastjson.ProbeBean\","
+ "\"value\":\"FASTJSON_LAB\""
+ "}";

Object result = JSON.parse(json, config);

System.out.println("result=" + result.getClass().getName());
System.out.println("value=" + ((ProbeBean) result).getValue());
}
}

预期输出顺序:

1
2
3
4
[probe] constructor
[probe] setValue=FASTJSON_LAB
result=lab.fastjson.ProbeBean
value=FASTJSON_LAB

这个实验只证明三件事:@type 被接受、对象被创建、Setter 被调用。它没有网络访问、JNDI lookup、动态类定义和容器注册。

不要在生产应用中为了复现而开启全局 AutoType。实验必须使用独立进程、固定依赖、仅允许测试包,并阻断外部网络;结束后销毁环境。

三、沿数据流定位失败阶段

当历史样本或告警无法复现时,按顺序加观测点:

1
2
3
4
5
6
7
8
1. 原始 JSON 是否到达目标解析 API
2. ParserConfig 是否接受目标类型
3. 构造方法和哪个 Setter 被调用
4. 是否进入 JNDI 或其他副作用方法
5. 是否产生 DNS、LDAP、RMI 或 HTTP 出站
6. 返回对象是否通过过滤与类型检查
7. 动态类是否成功加载
8. 容器注册表是否真实变化

常见现象与定位:

现象 优先检查
结果只是 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
2
3
4
5
6
7
保存原始请求和解析异常
→ 隔离节点并限制异常出站
→ 确认是否发生对象创建与 JNDI 调用
→ 枚举最终请求链和 JVM 修改
→ 修复不安全解析入口
→ 清除驻留与复活点
→ 使用可信制品重建

仅升级 Fastjson 不能清除已经注册的 Filter,单独移除 Filter 也不能修复仍可触发的解析入口。补丁、入口、运行时对象和复活机制必须分别验证,最后再用同一套 Probe 和运行时清单确认恢复结果。

上一篇
下一篇