序列化负责把内存中的对象或数据结构转换成可存储、可传输的格式;反序列化则把这种格式还原成程序可以使用的数据或对象。
这套机制本身没有问题。真正的风险出现在应用把不可信输入直接交给反序列化器,并且运行环境中还存在能够被串联起来的对象方法时。
一个典型的危险条件组合是:
可控输入
+ 不安全反序列化
+ 可利用的类或对象链
+ 足够的进程权限
= 可能造成命令执行、文件访问或服务异常
本文讨论识别和防御,不提供针对真实系统的对象链或利用代码。
一、序列化解决什么问题
对象通常存在于程序内存中,而内存状态不能直接跨网络传输,也不能长期保存。序列化把这些状态编码成字符串、字节流或其他格式,使它能够:
- 写入缓存。
- 保存到文件。
- 在服务之间传输。
- 进入消息队列。
- 作为会话数据保存。
- 在进程重启后被恢复。
常见应用场景包括:
- 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 | 布尔值 |
N | null |
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 或数字签名验证完整性。
安全流程应满足:
- 先验证签名。
- 再检查时间窗口和用途。
- 最后才进入反序列化器。
- 使用独立密钥。
- 密钥泄露后支持轮换。
仅加密而不校验完整性也不行。攻击者可能无法读取内容,却仍然篡改密文。
6.5 最小权限与沙箱
即使应用发生反序列化问题,以下限制也能减少影响:
- 服务以低权限账户运行。
- 只允许读取必要目录。
- 禁止不必要的系统命令和网络出口。
- 使用容器或沙箱隔离高风险解析任务。
- 限制内存、CPU 和请求时间。
- 对解析服务设置单独的访问控制。
6.6 依赖与版本管理
对象链往往来自框架或第三方库。应:
- 维护软件物料清单。
- 关注依赖安全公告。
- 定期更新高风险组件。
- 为关键应用建立回归测试。
- 不直接暴露不必要的序列化接口。
七、检测思路
反序列化流量可能表现为:
- Cookie 或字段值包含类型标记和长度信息。
- 请求体由可识别的序列化前缀开始。
- 同一接口出现大量相似但长度不同的请求。
- 响应错误中包含反序列化异常。
- 请求后出现文件访问、网络连接或子进程。
检测应结合:
网络流量
+ 应用异常日志
+ 对象类型白名单
+ 进程行为
+ 文件与网络访问
不要只依赖固定字节前缀,因为框架封装、压缩和加密会改变外部格式。
八、修复清单
- 盘点所有反序列化入口。
- 区分可信和不可信数据。
- 优先移除不必要的对象反序列化。
- 默认使用 JSON 等纯数据格式。
- 限制允许反序列化的类。
- 对跨边界数据使用签名校验。
- 不把 Session、Cookie 和缓存当成可信对象。
- 检查框架、日志和第三方依赖中的隐式反序列化。
- 使用最小权限和网络隔离。
- 对反序列化异常建立监控和告警。
九、小结
反序列化漏洞的核心不是“某个字符串格式有风险”,而是程序把外部输入恢复成了可信对象,并允许对象方法继续影响系统行为。
修复时应优先减少反序列化入口,使用纯数据格式;不得不使用时,再叠加允许类、签名、最小权限和运行时隔离。只有同时收紧数据和对象边界,才能降低对象链带来的风险。