跳转到帖子

背景Java Web 反序列化问题这几年一直没有消失,原因很现实:不少业务系统仍然依赖历史框架、缓存组件、消息中间件或远程调用协议,其中一部分组件会在不可信边界上处理序列化数据。单看漏洞公告往往只能知道“某组件存在反序列化风险”,但落到实际审计时,更关键的是确认数据入口、反序列化调用点、类加载环境以及可利用链是否真的成立。这篇文章整理一次相对通用的复现与审计流程,重点放在如何判断风险、如何搭建验证环境、如何做代码层面的定位和修复。文中示例均以本地靶场和无害验证为前提,不涉及对真实目标的攻击操作。问题场景假设某 Java Web 系统具备以下特征:使用 Spring MVC 或传统 Servlet 架构;项目中存在对象缓存、会话同步、RPC、消息消费等功能;依赖中包含 commons-collections、groovy、spring-core、fastjson、xstream、hessian 等序列化相关库;某些接口接收 Base64、二进制流、加密后字符串或自定义协议数据;历史代码中存在 ObjectInputStream、readObject、XMLDecoder、XStream、HessianInput 等调用。在真实审计中,不应只看到依赖中有高危库就直接下结论。更稳妥的判断路径是:入口是否可控,数据是否会被反序列化,反序列化前是否有签名校验或类型限制,运行时 classpath 是否满足危险 gadget 条件。技术分析Java 原生反序列化的典型危险点来自 ObjectInputStream.readObject()。当不可信数据被传入该方法时,JVM 会根据序列化流中声明的类名加载类并恢复对象状态。如果相关类实现了特殊方法,例如 readObject、readResolve、finalize 或某些集合类的比较、转换逻辑,就可能触发非预期代码路径。漏洞是否可利用,通常由四个条件共同决定:条件说明审计关注点数据入口攻击者或外部用户能够控制序列化数据HTTP 参数、请求体、Cookie、Header、MQ 消息、Redis 值反序列化调用服务端对该数据执行反序列化ObjectInputStream、HessianInput、XStream、JSON.parseObject类路径环境运行环境中存在可触发链相关类Maven 依赖、容器公共 lib、插件目录防护缺失缺少白名单、签名、类型约束或隔离JEP 290 过滤器、业务签名校验、封装安全反序列化工具很多误报来自第三个条件和第四个条件没有确认。例如依赖树里出现了 commons-collections,并不代表一定可触发;某接口存在 Base64 参数,也不代表一定进入了反序列化流程。本地复现环境建议先在本地构造一个最小化示例,便于理解调用链和日志表现。下面是一个仅用于说明风险点的示例 Servlet 逻辑,重点是展示不安全的反序列化调用位置。protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
byte[] data = req.getInputStream().readAllBytes();

try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data))) {
Object obj = ois.readObject();
resp.getWriter().println(\"ok: \" + obj.getClass().getName());
} catch (Exception e) {
resp.setStatus(400);
resp.getWriter().println(\"deserialize error\");
}
}这类写法的问题在于,它没有限制可反序列化的类型,也没有确认数据来源的可信性。即使当前业务只期望接收某个 DTO,对端实际可以提交任意序列化对象流,后续风险取决于 classpath 和对象恢复过程中触发的代码路径。更合理的做法是避免在外部接口直接使用 Java 原生序列化。如果确实是内部历史协议无法短期替换,至少应增加类型过滤和数据完整性校验。代码审计关键步骤1. 从危险 API 做全局检索第一步可以从反序列化相关 API 入手。常见关键词如下:ObjectInputStream
readObject(
readUnshared(
XMLDecoder
XStream
fromXML
HessianInput
BurlapInput
Kryo
FSTObjectInput
JSON.parseObject
JSON.parse
readValue
enableDefaultTyping如果项目较大,建议结合调用链分析,而不是停留在字符串命中。重点追踪数据从 Controller、Filter、Servlet、MQ Listener、定时任务、RPC Provider 到危险 API 的路径。2. 确认数据是否来自不可信边界以下位置需要特别关注:HTTP 请求体直接读取为 byte[] 后处理;Cookie 或 Header 中存储序列化对象;Base64 解码、AES 解密、GZIP 解压之后进入对象恢复;消息队列消费者反序列化消息体;Redis、Memcached 中读取对象后直接反序列化;文件上传后解析对象内容。实际审计中经常出现“外层有加密所以认为安全”的情况。加密只能保护传输或存储内容,不等于数据可信。如果密钥在客户端、历史版本泄露、签名缺失或多个系统共用密钥,仍然可能形成攻击面。3. 检查依赖树和运行时类路径Maven 项目可以先输出依赖树,确认是否存在高风险序列化组件或历史版本库。mvn dependency:tree -Dverbose
mvn dependency:tree | grep -E \"commons-collections|groovy|xstream|fastjson|hessian|kryo\"需要注意,运行时 classpath 不一定完全等于 pom.xml。某些老项目会把 jar 放在 WEB-INF/lib、Tomcat lib、应用插件目录或私有仓库打包目录中。排查线上风险时,应以实际部署包和容器目录为准。4. 做无害化验证验证反序列化漏洞时,不建议直接使用具备系统命令执行效果的 payload。更稳妥的方式是构造只触发日志、DNS 测试域名、异常栈或自定义类标记的样本。在本地环境中,可以通过自定义类的 readObject 方法观察是否被调用。public class AuditMarker implements Serializable {
private String message;

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
System.out.println(\"[deserialize-marker] \" + message);
}
}如果服务端日志出现对应标记,说明外部输入确实进入了反序列化流程。这个结论通常已经足够支撑风险确认,后续不应继续向危险利用方向推进。5. 关注异常信息和日志特征反序列化失败时,日志里常见以下异常:java.io.StreamCorruptedException: invalid stream header
java.io.InvalidClassException
java.lang.ClassNotFoundException
java.io.OptionalDataException
com.thoughtworks.xstream.security.ForbiddenClassException
java.io.InvalidObjectException: filter status: REJECTED这些异常能帮助判断当前处理链路。例如 invalid stream header 通常说明数据不是 Java 原生序列化格式,ClassNotFoundException 说明服务端尝试加载流中声明的类但失败,filter status: REJECTED 则说明已有 JEP 290 过滤策略生效。修复与加固建议优先级最高的修复,是避免在不可信边界使用通用反序列化。可以将外部协议替换为 JSON、Protobuf 等明确 schema 的格式,并在业务层做字段校验。对于无法立即替换的历史系统,可以分层加固。1. 使用 JEP 290 对类型做白名单过滤Java 9 之后提供了全局或局部的对象输入过滤机制,Java 8 的部分更新版本也支持。示例:ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
\"com.example.dto.*;java.base/*;!*\");

try (ObjectInputStream ois = new ObjectInputStream(inputStream)) {
ois.setObjectInputFilter(filter);
Object obj = ois.readObject();
}过滤规则建议采用白名单思路,只允许明确的 DTO 包和基础类型。不要简单写成允许全部再排除少数类,这种黑名单方式很容易被新的 gadget 或依赖变化绕开。2. 反序列化前做完整性校验如果是系统间内部协议,应在反序列化前验证 HMAC 签名,且密钥只保存在服务端可信环境中。校验必须发生在 readObject() 之前。if (!verifyHmac(body, signature, secretKey)) {
throw new SecurityException(\"invalid signature\");
}

try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(body))) {
Object obj = ois.readObject();
}签名校验不能替代类型过滤,但可以有效降低伪造数据进入反序列化流程的概率。3. 收敛依赖和升级组件清理不再使用的高风险依赖,尤其是历史版本 commons-collections、xstream、fastjson、groovy 等。升级时不要只看直接依赖,还要检查传递依赖。对于 fat jar 或传统 war 包,要确认旧 jar 没有残留在部署目录。4. 增加运行时监控可以对关键异常和敏感调用建立日志告警,例如短时间内大量 InvalidClassException、StreamCorruptedException、ForbiddenClassException,往往意味着有人在探测序列化入口。对于核心系统,也可以在 RASP、EDR 或应用审计日志中关注反序列化相关调用栈。注意点不要把“依赖存在”等同于“漏洞可利用”,需要确认完整调用链。不要把“接口需要登录”当作漏洞不存在,内部低权限用户、被盗会话和服务间调用同样可能成为入口。不要在生产环境做危险 payload 验证,使用日志标记、类型探测、异常行为即可确认风险。黑名单修复不可靠,新增依赖或版本变化可能引入新的触发链。修复后要补充回归测试,确保过滤策略不会影响合法业务对象。一个简单的审计清单检查项结论记录是否存在反序列化 API 调用记录类名、方法名、调用位置输入是否外部可控记录入口参数、请求路径、消息 topic是否有签名或权限校验确认校验发生在反序列化之前是否有类型白名单确认策略为默认拒绝依赖中是否存在高风险组件记录版本和来源是否有异常日志或探测痕迹关联访问日志和应用日志修复后是否回归验证验证合法对象和非法对象处理结果总结Java 反序列化漏洞的审计重点,不是记住某条利用链,而是建立稳定的判断方法:入口是否可控、是否进入反序列化、类型是否受限、运行环境是否满足触发条件。复现时应坚持无害化验证,避免把风险确认变成攻击演示。从长期维护角度看,最推荐的治理方式是逐步淘汰不可信边界上的原生序列化协议,配合白名单过滤、签名校验、依赖收敛和日志监控。这样即使未来出现新的 gadget 或组件漏洞,也能把影响面控制在较小范围内。

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

0篇意见

推荐意见

没有意见。

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