一、PHP 序列化与反序列化基础原理

1. 通俗理解

  • serialize () 序列化:把数组、对象等复杂数据拆成标准化字符串,用于存储、网络传输,类似家具拆板打包快递。
  • unserialize () 反序列化:将序列化字符串还原为原始对象 / 数组,类似收货后组装家具。

2. 核心应用场景

Session 会话存储、Redis/Memcached 缓存、跨服务 API 数据传输、数据库对象存储、消息队列异步任务。

3. serialize () 序列化函数

  1. 语法:string serialize(mixed $value),仅不支持 resource 资源类型。

  2. 基础类型序列化格式对照表 | 标识 | 类型 | 格式示例 | | ---- | ---- | ---- | |N|null|N;| |b | 布尔 | b:1;/b:0;| |i | 整型 | i:123;| |d | 浮点 | d:3.14;| |s | 普通字符串 | s:5:"hello";| |S | 转义字符串 | S:5:"he\6c\6co";| |a | 数组 | a: 元素数量:{键值对}| |O | 对象 | O: 类名字符长度:"类名": 属性数量:{属性序列化内容}| |R/r | 引用 | R: 编号;|

  3. 对象序列化特殊规则

  • 序列化前优先执行__sleep(),仅序列化该方法返回的属性;
  • 属性访问修饰符会改变序列化字符串:
    • public:直接写属性名 s:4:"name"
    • protected:前缀\0*\0 s:6:"*pro"
    • private:前缀\0类名\0 s:9:"Testpri"

4. unserialize () 反序列化函数

  1. 语法:mixed unserialize(string $data, array $options = []),PHP7 + 支持allowed_classes限制可反序列化类。
  2. 失败场景:序列化字符串格式错误、字符串长度不匹配、对应类未加载、语法缺失分号 / 括号。
  3. 漏洞核心成因:用户可控输入直接传入unserialize(),可构造恶意字符串触发魔术方法执行危险操作。

5. 入门靶场例题(基础数组 / 对象序列化)

  1. 第一题:直接反序列化 POST 数组,构造指定 username、password 数组序列化串传入即可读取 flag;
  2. 第二题:反序列化自定义 Person 对象,实例化对象后序列化提交;
  3. 第三题:包含 protected、private 私有属性,序列化后必须 URL 编码提交,否则空字符丢失导致解析失败。

二、PHP 魔术方法(反序列化漏洞核心利用点)

魔术方法以双下划线__开头,满足特定条件自动调用,是连接恶意输入与危险代码的桥梁。

1. 生命周期类方法

  1. __construct():实例化 new 对象时触发,反序列化不会调用
  2. __destruct():对象销毁、脚本执行结束自动触发,反序列化后必触发,最常用漏洞入口,可执行文件删除、命令执行;
  3. __sleep():序列化 serialize () 前执行,筛选需要持久化的属性;
  4. __wakeup():unserialize 反序列化完成后立刻触发,常用来做安全校验,存在经典绕过漏洞 CVE-2016-7124;
  5. PHP7.4 新增__serialize()/__unserialize(),优先级高于 sleep/wakeup。

2. POP 链跳板常用魔术方法

  1. __toString():对象被当作字符串使用(echo、字符串拼接、print)触发,多用于文件读取;
  2. __invoke():对象像函数一样$obj()调用时触发,可执行回调命令;
  3. __call():调用对象不存在的实例方法触发;__callStatic():调用不存在静态方法触发;
  4. __get():读取私有 / 不存在属性触发;__set():写入不可访问属性触发;
  5. __isset()/__unset():对不可访问属性执行 isset、unset 操作触发。

3. 魔术方法触发总表

表格

魔术方法 反序列化时是否自动触发 利用场景
__construct
__destruct 是(脚本结束) 漏洞入口,命令 / 文件操作
__wakeup 是(反序列化完成) 安全校验绕过
__toString 需主动字符串操作 POP 链中间跳板
__call/__get/__invoke 需对应操作 POP 链串联链路

4. 魔术方法靶场例题

  1. 第四题:__destruct()动态调用函数,private 属性 name 赋值 system,age 赋值命令,对象销毁时执行system(命令)
  2. 第五题:同时存在__wakeup()重置 age、__toString()读取文件,echo 输出对象触发文件读取;
  3. 第六题 CVE-2016-7124 __wakeup 绕过:PHP5.6.25/7.0.10 以下版本,修改序列化字符串中对象属性数量,大于实际属性数即可跳过__wakeup 执行;
  4. 第七题 引用 & 绕过相等校验:利用序列化 R 引用标记,让两个私有属性指向同一内存,绕过$name===$age全等判断。

三、Phar 反序列化(无 unserialize 也能触发)

1. 原理

Phar 是 PHP 归档文件,文件元数据自动以序列化存储;使用phar://协议访问文件时,PHP 会自动解析元数据并执行反序列化,无需代码中出现 unserialize 函数,大幅扩大攻击面。

2. Phar 文件四部分

Stub(存根,必须以__HALT_COMPILER();结尾,可伪装 GIF/JPG 图片)+ Manifest(序列化元数据)+ 文件内容 + 签名。

3. 生成恶意 phar 文件条件

  1. php.ini 配置phar.readonly = Off
  2. 代码创建 Phar 对象,设置恶意类实例为元数据;
  3. 至少添加一个文件内容,可修改 Stub 伪装图片后缀绕过上传检测。

4. 可触发 phar 反序列化的函数

文件检测(file_exists)、文件读取(file_get_contents)、文件信息(filesize)、图像处理(getimagesize)、哈希函数(md5_file)、目录操作、复制重命名等绝大多数文件操作函数。

5. phar:// 过滤绕过方案

大小写混淆Phar://、URL 双重编码、协议嵌套php://filter/resource=phar://、文件后缀伪装图片、协议拼接绕过字符串匹配。

四、反序列化漏洞形成三大必要条件

  1. 入口可控:用户可控数据直接传入 unserialize (),如 POST/GET 参数、Cookie、上传文件内容、数据库缓存;
  2. 存在利用 Gadget 类:代码内包含魔术方法,内部存在 system、file_get_contents、include 等危险操作;
  3. 类可加载:反序列化时目标类已定义或自动加载可用。

五、POP 链构造(复杂反序列化核心考点)

1. POP 链概念

单一类无法直达命令执行时,串联多个类的魔术方法形成完整调用链路:

  • 入口点:__wakeup() / __destruct()(反序列化最先触发)
  • 跳板 Gadget:__toString/__call/__get等中转魔术方法
  • 终点 Sink:危险函数 system、eval、文件读取 / 包含

2. 两种分析思路

  1. 正向分析:从反序列化入口出发,顺着魔术方法调用链往下追踪至危险函数;
  2. 逆向分析:先找到代码内危险函数,反向寻找能触发该代码的魔术方法,回溯到入口类。

3. 常用 POP 链串联模式

__destruct() -> echo输出对象 -> __toString() -> 调用不存在方法 -> __call() -> 属性读取__get() -> system/文件读取

六、防御措施总结

  1. 严格限制 unserialize 输入,禁止用户可控数据直接反序列化;
  2. PHP7 + 开启allowed_classes白名单,只允许安全类反序列化;
  3. 类中__wakeup()做安全校验,重置危险可控属性;
  4. 关闭 phar.readonly,限制 phar 协议使用;
  5. 过滤文件操作函数中的 phar://、伪协议;
  6. 代码审计避免魔术方法内直接拼接、执行用户可控参数。

更多推荐