跳转到帖子

背景

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 05

AC 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 原生序列化在网络边界和跨系统传输中的使用,把对象类型、输入长度、协议格式都显式约束起来。这样即使后续依赖里出现新的利用链,也不会轻易被外部请求触达。

Java 服务调用链与安全审计示意图
Java 依赖、调用链和反序列化风险分析示意

0篇意见

推荐意见

没有意见。

游客
抱歉,你的帖子内容包括我们不允许的字词。请编辑你的帖子,删除下面高亮的屏蔽字。
添加意见…