背景
Java Web 系统里的反序列化问题并不新,但在实际排查中,它经常不是以“漏洞利用成功”的形式出现,而是先表现为网关异常、WAF 告警、日志里出现可疑类名、接口返回 500 或 JVM 负载短时间升高。很多团队在处理这类问题时容易走两个极端:要么只看单条告警,封一个 IP 就结束;要么直接全站停机排查,影响业务。
这篇文章整理一套相对稳妥的排查思路,重点放在识别风险入口、定位反序列化链路、验证影响范围和加固措施上。内容只讨论防御和代码审计视角,不涉及攻击载荷构造和未授权利用。
典型场景
某 Java Web 服务前面有 Nginx 和应用网关,后端是 Spring MVC 老项目,依赖里包含若干历史包。某天网关出现如下特征:
- 部分 POST 请求体为二进制或 Base64 形态,接口路径并非上传接口。
- 日志中出现
java.io.ObjectInputStream、readObject、InvalidClassException等异常。 - 应用进程短时间 CPU 升高,但没有明显业务峰值。
- WAF 告警命中“Java 反序列化探测”规则。
这里不能简单判断为“已经被攻陷”,但至少说明有人在探测可反序列化入口,或者系统内部确实存在把外部输入直接交给反序列化 API 的代码路径。
技术分析
Java 原生反序列化的核心风险在于:如果服务端对不可信数据调用 ObjectInputStream#readObject(),攻击者可控的数据会参与对象恢复过程。对象恢复期间可能触发类的 readObject、readResolve、compareTo、hashCode 等方法。如果项目依赖中存在危险调用链,就可能形成远程代码执行、文件操作、网络访问等风险。
实际项目里,入口不一定是显式的 ObjectInputStream。常见风险来源包括:
- 历史 RPC 或自研协议使用 Java 原生序列化。
- 缓存、消息队列、任务调度系统传输对象时使用默认序列化。
- Controller 接收 Base64 字符串后解码并反序列化。
- 老版本框架或组件暴露了不安全的反序列化端点。
- 会话、RememberMe、分布式 Session 使用了不安全的序列化策略。
排查时建议把“入口”和“危险依赖”分开看。入口决定外部输入能否进入反序列化流程,危险依赖决定进入后是否可能造成高危后果。没有入口,依赖风险通常只是潜在问题;没有危险链,入口仍然可能导致拒绝服务、类型混淆或业务异常。
关键排查步骤
1. 从日志确认是否存在反序列化异常
先检索应用日志、网关日志、容器日志,关注与对象流相关的异常。可以从以下关键词开始:
ObjectInputStream
readObject
InvalidClassException
StreamCorruptedException
OptionalDataException
ClassNotFoundException
serialVersionUID
AC ED 00 05AC ED 00 05 是 Java 序列化流常见头部的十六进制表示。如果请求体、参数或日志中出现该特征,需要重点看对应接口。
Linux 环境下可做初步检索:
grep -RniE "ObjectInputStream|readObject|InvalidClassException|StreamCorruptedException|AC ED 00 05" /data/logs/app/如果日志中包含请求 ID,建议串联网关访问日志和应用异常日志,确认异常对应的 URL、方法、Content-Type、请求大小、来源 IP、User-Agent。
2. 代码层面定位反序列化入口
代码审计时优先搜以下 API 和关键类:
ObjectInputStream
readObject(
XMLDecoder
XStream
Kryo
HessianInput
BurlapInput
SerializationUtils.deserialize
DefaultSerializer
RedisSerializer
readValue
parseObject需要注意,readValue、parseObject 本身不等同于 Java 原生反序列化,但如果启用了多态类型、自动类型识别或不安全配置,也可能引入类似风险。审计时要结合配置和入参来源判断。
一个典型的不安全写法如下:
public Object decode(String data) throws Exception {
byte[] bytes = Base64.getDecoder().decode(data);
try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes))) {
return ois.readObject();
}
}这里最大的问题不是 Base64,而是外部可控数据直接进入 readObject(),且没有类型白名单、长度限制和来源校验。
3. 梳理依赖中的高风险组件
反序列化问题经常与依赖版本有关。建议导出依赖树,检查是否存在历史高风险组件或过旧版本:
mvn dependency:tree -DoutputFile=dependency-tree.txt
gradle dependencies > dependency-tree.txt重点关注以下几类:
- 老版本 commons-collections、commons-beanutils、groovy、spring-core、c3p0、rome、aspectjweaver 等通用库。
- 历史版本 Shiro、WebLogic、JBoss、Dubbo、Hessian 等曾多次出现反序列化相关问题的组件。
- 业务自研 jar 中实现了
Serializable且包含敏感操作的方法。
不要只看“有没有某个库”,还要看它是否在可达路径上。对于后端服务,很多依赖是传递引入的,但实际没有被加载或没有与入口形成调用链。治理时可以按“外部可达入口优先、历史高危组件优先、可移除依赖优先”的顺序推进。
4. 使用安全方式验证影响范围
线上验证必须谨慎,不建议直接投放未知 payload。更稳妥的做法是构造无害的序列化对象,只验证系统是否会尝试反序列化,并观察日志行为。例如内部测试环境可以序列化一个普通 DTO,确认接口是否接受对象流。
public class SafeProbe implements Serializable {
private static final long serialVersionUID = 1L;
private String marker = "deserialization-check";
}验证目标应该是:
- 该接口是否会读取 Java 序列化流。
- 是否存在类型限制。
- 异常是否会暴露类名、路径、堆栈信息。
- 请求大小和频率是否有限制。
- 是否能在网关或应用层拦截非预期 Content-Type。
如果生产环境已经出现异常流量,建议先在网关层做临时收敛,例如限制可疑接口的请求方法、Content-Type、请求体大小和来源范围,再进入代码修复。
加固方案
1. 避免对不可信输入使用 Java 原生反序列化
最直接的修复是改协议,使用 JSON、Protobuf、Avro 等更可控的格式,并显式绑定 DTO 类型。即便使用 JSON,也要关闭不必要的多态类型能力,避免把“任意类实例化”换成另一种形式。
对于必须保留兼容的历史接口,应至少增加类型白名单。JDK 9 以后可以使用 ObjectInputFilter 做过滤:
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"com.example.dto.*;java.base/*;!*"
);
try (ObjectInputStream ois = new ObjectInputStream(inputStream)) {
ois.setObjectInputFilter(filter);
Object obj = ois.readObject();
}过滤规则要尽量收窄,只允许业务 DTO 和必要基础类型,不要为了省事写成大范围包名。
2. 限制输入大小和对象复杂度
反序列化不仅有执行风险,也可能带来内存和 CPU 消耗问题。建议在网关和应用层同时限制:
- 请求体最大长度。
- 接口访问频率。
- 对象图深度、数组长度、引用数量。
- 非预期 Content-Type 的请求。
如果使用全局过滤器,可以设置更明确的上限:
-Djdk.serialFilter=maxdepth=20;maxrefs=1000;maxbytes=1048576;com.example.dto.*;java.base/*;!*这个参数适合做基础防线,但不应替代代码层面的白名单。
3. 清理危险依赖和升级组件
依赖治理建议分三步:
- 确认是否直接依赖,不需要的直接移除。
- 传递依赖通过 exclusion 排除,避免被历史包带入。
- 必须保留的组件升级到官方修复版本,并验证兼容性。
Maven 中可以排除不需要的传递依赖:
<dependency>
<groupId>com.example</groupId>
<artifactId>legacy-sdk</artifactId>
<version>1.0.0</version>
<exclusions>
<exclusion>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
</exclusion>
</exclusions>
</dependency>依赖升级后要跑回归测试,尤其是序列化格式、缓存对象、消息消费端和定时任务。
4. 监控与告警规则
反序列化风险适合做多层监控:
- 网关层:识别异常二进制请求、非常规 Content-Type、大请求体。
- 应用层:监控反序列化异常、类加载异常、对象过滤器拒绝日志。
- 主机层:关注 Java 进程异常子进程、异常网络连接、临时目录异常文件。
- 依赖层:持续扫描高危组件版本。
应用日志建议保留必要上下文,但不要把完整请求体写入日志,避免敏感数据泄露和日志放大。
事件处置注意点
- 不要只封 IP。探测来源经常变化,关键是确认入口是否存在。
- 不要直接删除日志。先做备份,保留时间线、请求 ID、异常栈和访问来源。
- 不要在生产环境执行未知验证样本。验证应放在隔离测试环境,并使用无害对象。
- 不要只升级一个库就结束。入口、依赖、配置、日志都要闭环。
- 如果发现异常进程、异常计划任务或可疑外连,应按入侵事件处理,而不只是漏洞修复。
一个简化的排查清单
| 阶段 | 检查项 | 目标 |
| 日志排查 | ObjectInputStream、readObject、AC ED 00 05、异常堆栈 | 确认是否存在探测或触发 |
| 入口定位 | Controller、RPC、消息队列、缓存、Session | 找出外部输入进入点 |
| 依赖分析 | dependency tree、历史组件版本 | 判断潜在危险链范围 |
| 安全验证 | 无害 DTO、测试环境、日志观察 | 确认可达性和过滤效果 |
| 加固闭环 | 白名单、大小限制、组件升级、监控规则 | 降低复现和变种风险 |
总结
Java 反序列化排查的重点不是复现某条利用链,而是确认“不可信输入是否能进入反序列化流程”以及“依赖和配置是否会放大风险”。实际处置中,建议先从日志和入口收敛影响面,再做代码修复、依赖治理和监控补强。
长期看,最可靠的做法是减少 Java 原生序列化在网络边界和跨系统传输中的使用,把对象类型、输入长度、协议格式都显式约束起来。这样即使后续依赖里出现新的利用链,也不会轻易被外部请求触达。
推荐意见