序列化负责把内存中的对象或数据结构转换成可存储、可传输的格式;反序列化则把这种格式还原成程序可以使用的数据或对象。

这套机制本身没有问题。真正的风险出现在应用把不可信输入直接交给反序列化器,并且运行环境中还存在能够被串联起来的对象方法时。

一个典型的危险条件组合是:

可控输入
    + 不安全反序列化
    + 可利用的类或对象链
    + 足够的进程权限
    = 可能造成命令执行、文件访问或服务异常

本文讨论识别和防御,不提供针对真实系统的对象链或利用代码。

一、序列化解决什么问题

对象通常存在于程序内存中,而内存状态不能直接跨网络传输,也不能长期保存。序列化把这些状态编码成字符串、字节流或其他格式,使它能够:

  • 写入缓存。
  • 保存到文件。
  • 在服务之间传输。
  • 进入消息队列。
  • 作为会话数据保存。
  • 在进程重启后被恢复。

常见应用场景包括:

  • Java 的 Serializable 对象。
  • PHP 的 serialize() 与 unserialize()。
  • Python 的 pickle。
  • .NET 的二进制序列化。
  • 各种 RPC 框架的自定义编码。

不同格式的风险并不相同。JSON、Protocol Buffers 等数据格式主要描述普通值,通常不会自动实例化任意类;PHP 对象序列化和 Java 原生序列化则可能带有对象类型和调用语义。

二、理解序列化格式

2.1 常见类型标记

PHP 序列化字符串使用类型标记和长度描述数据。一个简化的数组示例如下:

a:3:{i:0;s:5:"hello";i:1;i:42;i:2;b:1;}
标记含义
a数组
i整数
s字符串
d浮点数
b布尔值
Nnull
O对象

理解格式有助于分析数据,但格式本身并不构成漏洞。关键在于接收方如何使用这些内容。

2.2 对象序列化

对象序列化通常除了属性值,还会保留类名和字段信息。概念上可以写成:

Object
  class: Example
  property: enabled
  property: label

当反序列化器读到对象类型时,可能尝试加载对应类并恢复属性。不同访问级别和语言实现还会加入额外元数据,例如私有属性标记或版本信息。

这意味着反序列化输入不只是“数据”,它也可能影响程序创建什么类型的对象。

三、漏洞形成的条件

反序列化漏洞通常不是单一函数造成的,需要同时满足几个条件。

3.1 输入可控

用户能够影响传入反序列化器的完整内容,或者能够改变关键字段、类型和长度。

常见入口包括:

  • Cookie 或 Session 数据。
  • 隐藏表单字段。
  • 请求体中的序列化字符串。
  • 缓存、消息队列和内部接口。
  • 文件导入功能。
  • 第三方系统回调。

3.2 没有可信边界

应用把来自客户端、外部系统或文件的内容当作可信对象处理。即使数据经过 Base64 或其他编码,也不代表它已经受到保护。

3.3 类中存在危险方法

反序列化过程中可能触发一些生命周期方法。PHP 中常见的魔术方法包括:

  • __construct():对象创建时执行。
  • __destruct():对象销毁时执行。
  • __wakeup():对象反序列化恢复时可能执行。
  • __toString():对象被转换成字符串时执行。
  • __call():调用不存在的方法时可能执行。
  • __invoke():对象被像函数一样调用时执行。

这些方法有正常业务用途,但如果它们继续调用文件、命令、数据库或网络能力,就可能形成危险链路。

3.4 存在可利用的对象链

多个类的方法可以首尾相接时,可能形成对象链:

反序列化入口
    -> 恢复对象 A
    -> 触发某个方法
    -> 调用对象 B
    -> 到达文件或命令能力

攻击者不需要真的提交“代码”,只需要构造一组能够影响程序控制流的属性。

四、不同技术栈的关注点

4.1 PHP

PHP 中应重点检查所有把请求数据交给 unserialize() 的位置。还要注意:

  • 框架的反序列化封装。
  • Session 处理器。
  • 缓存读写。
  • 任务队列中的对象消息。
  • 第三方库的魔术方法。

即使业务代码没有直接调用 unserialize(),框架或依赖也可能在背后使用。

4.2 Java 与 Spring

Java 原生序列化在 ObjectInputStream 等入口使用。相关风险可能来自:

  • 接受序列化字节流的 RMI、JMS 或缓存组件。
  • 第三方反序列化库。
  • 能解析动态类型的表达式工具。
  • 日志、模板和依赖库中的方法链。

Spring 应用通常还需要关注:

  • 反序列化数据的来源和认证。
  • 对外接口是否允许提交具体类名。
  • 日志配置是否会把外部输入当作表达式。
  • 依赖版本是否存在公开的对象链问题。

4.3 Python 与其他语言

Python 的 pickle 可以表达对象重建逻辑。不要把不可信的 pickle 数据当作普通配置文件或缓存值处理。

.NET、Ruby、Node.js 生态中的某些序列化扩展也可能允许类型元数据,使用前需要确认默认安全边界。

五、常见错误认知

5.1 Base64 不是安全控制

Base64 只是编码,不是加密,也不提供完整性保护。任何人都可以解码并重新构造内容。

5.2 删除几个字符不能阻止反序列化

对序列化字符串做简单的字符串替换,容易破坏格式,也不能可靠限制对象类型。真正需要控制的是反序列化器允许创建哪些类。

5.3 WAF 不是根本修复

多态序列化格式可以改变字段顺序、类型和编码方式。仅靠请求内容黑名单很难稳定判断。

5.4 “只在内部网络”不是充分条件

内部接口仍然可能被 SSRF、横向移动或供应链问题触达。内部流量同样需要认证、授权和输入验证。

六、安全设计

6.1 优先使用纯数据格式

如果业务只需要传递普通值,优先使用 JSON 或其他明确的数据格式,而不是能够表达类和方法的格式。

在 JSON 场景中也要防范:

  • 超深嵌套。
  • 超大数组和对象。
  • 类型混淆。
  • 反序列化到危险类型。
  • 过度宽松的对象映射库。

6.2 不反序列化不可信输入

最有效的修复通常是去掉不必要的反序列化入口。

例如,会话只保存随机的服务端 Session ID,而不是把整个用户对象交给客户端保存;缓存只保存普通 JSON 数据;内部消息使用明确的协议和模式。

6.3 限制允许的类

如果必须使用 PHP 反序列化,应限制允许创建的对象类型。安全示例:

$data = '...';
$result = unserialize($data, [
    'allowed_classes' => false,
]);

关闭所有类恢复后,数据只能作为普通值处理。如果业务确实需要对象,应提供最小的类允许列表,而不是允许全部类。

6.4 签名与完整性校验

当序列化数据需要经过不可信链路时,可以使用 HMAC 或数字签名验证完整性。

安全流程应满足:

  1. 先验证签名。
  2. 再检查时间窗口和用途。
  3. 最后才进入反序列化器。
  4. 使用独立密钥。
  5. 密钥泄露后支持轮换。

仅加密而不校验完整性也不行。攻击者可能无法读取内容,却仍然篡改密文。

6.5 最小权限与沙箱

即使应用发生反序列化问题,以下限制也能减少影响:

  • 服务以低权限账户运行。
  • 只允许读取必要目录。
  • 禁止不必要的系统命令和网络出口。
  • 使用容器或沙箱隔离高风险解析任务。
  • 限制内存、CPU 和请求时间。
  • 对解析服务设置单独的访问控制。

6.6 依赖与版本管理

对象链往往来自框架或第三方库。应:

  • 维护软件物料清单。
  • 关注依赖安全公告。
  • 定期更新高风险组件。
  • 为关键应用建立回归测试。
  • 不直接暴露不必要的序列化接口。

七、检测思路

反序列化流量可能表现为:

  • Cookie 或字段值包含类型标记和长度信息。
  • 请求体由可识别的序列化前缀开始。
  • 同一接口出现大量相似但长度不同的请求。
  • 响应错误中包含反序列化异常。
  • 请求后出现文件访问、网络连接或子进程。

检测应结合:

网络流量
    + 应用异常日志
    + 对象类型白名单
    + 进程行为
    + 文件与网络访问

不要只依赖固定字节前缀,因为框架封装、压缩和加密会改变外部格式。

八、修复清单

  • 盘点所有反序列化入口。
  • 区分可信和不可信数据。
  • 优先移除不必要的对象反序列化。
  • 默认使用 JSON 等纯数据格式。
  • 限制允许反序列化的类。
  • 对跨边界数据使用签名校验。
  • 不把 Session、Cookie 和缓存当成可信对象。
  • 检查框架、日志和第三方依赖中的隐式反序列化。
  • 使用最小权限和网络隔离。
  • 对反序列化异常建立监控和告警。

九、小结

反序列化漏洞的核心不是“某个字符串格式有风险”,而是程序把外部输入恢复成了可信对象,并允许对象方法继续影响系统行为。

修复时应优先减少反序列化入口,使用纯数据格式;不得不使用时,再叠加允许类、签名、最小权限和运行时隔离。只有同时收紧数据和对象边界,才能降低对象链带来的风险。