PHP 反序列化 POP 链学习:从 wakeup 绕过到伪协议读文件
一、前言
PHP 反序列化是 Web CTF 中非常常见的考点,也是真实代码审计中需要重点关注的风险点。它的核心不是“背 payload”,而是理解对象在反序列化后的生命周期,以及魔术方法之间如何形成调用链。
本文结合几道典型题目,总结 PHP 反序列化中常见的三个方向:
__wakeup()绕过- private/protected 属性序列化格式
- POP 链构造和伪协议读文件
内容仅用于靶场学习和授权环境复现。
二、PHP 序列化基础
一个普通对象序列化后大概长这样:
1 | O:4:"User":2:{s:4:"name";s:5:"admin";s:4:"role";s:4:"root";} |
含义如下:
| 片段 | 含义 |
|---|---|
O |
object,对象 |
4 |
类名长度 |
"User" |
类名 |
2 |
属性数量 |
s:4:"name" |
字符串类型属性名 |
s:5:"admin" |
字符串类型属性值 |
常见类型标识:
1 | s string |
三、属性可见性
PHP 中 public、protected、private 属性在序列化后的格式不同。
1. public
1 | public $name = "admin"; |
序列化后:
1 | s:4:"name";s:5:"admin"; |
2. protected
1 | protected $name = "admin"; |
序列化后属性名前会带有:
1 | \0*\0 |
URL 编码后表现为:
1 | %00%2A%00name |
3. private
1 | private $name = "admin"; |
序列化后属性名前会带有:
1 | \0类名\0 |
URL 编码后类似:
1 | %00User%00name |
这也是很多题目中 payload 需要 URL 编码的原因。
四、魔术方法触发点
PHP 反序列化题常见魔术方法:
| 方法 | 触发场景 |
|---|---|
__wakeup() |
unserialize() 时触发 |
__destruct() |
对象销毁时触发 |
__toString() |
对象被当作字符串使用时触发 |
__get() |
读取不可访问属性时触发 |
__set() |
写入不可访问属性时触发 |
__call() |
调用不可访问方法时触发 |
__invoke() |
对象被当作函数调用时触发 |
构造 POP 链时,一般要先找“链尾”,也就是最终能产生敏感行为的位置,例如:
file_get_contents()include()system()eval()call_user_func()
然后再从触发方法一步步往前推。
五、案例一:wakeup 绕过读取 flag
某题源码逻辑如下:
1 | class Name { |
目标:
1 | username === admin |
但 __wakeup() 会把 username 改成 guest,所以需要绕过 __wakeup()。
老版本 PHP 中存在一种常见绕过思路:当序列化字符串中声明的属性数量大于实际属性数量时,可能跳过 __wakeup()。
构造重点:
1 | O:4:"Name":3:{...} |
实际只有两个属性,但数量写成 3。
由于属性是 private,需要使用 %00Name%00username 和 %00Name%00password:
1 | O:4:"Name":3:{s:14:"%00Name%00username";s:5:"admin";s:14:"%00Name%00password";i:100;} |
这个案例的重点:
- private 属性需要带类名前缀。
__wakeup()会影响反序列化后的属性。- 属性数量异常可用于特定版本下绕过
__wakeup()。
六、案例二:AreUSerialz 强弱类型绕过
题目核心代码:
1 | class FileHandler { |
目标是进入:
1 | else if ($this->op == "2") |
从而调用 read() 读取文件。
但是析构方法中有:
1 | if ($this->op === "2") |
这里一个是弱类型比较:
1 | $this->op == "2" |
另一个是强类型比较:
1 | $this->op === "2" |
因此可以设置:
1 | $op = 2; |
这样:
1 | 2 === "2" 不成立 |
最终绕过析构方法中的强类型判断,同时进入 process() 中的读文件分支。
读取 flag.php 可使用 PHP 伪协议:
1 | php://filter/read=convert.base64-encode/resource=flag.php |
构造示例:
1 |
|
这个案例的重点:
- 找到最终敏感函数
file_get_contents()。 - 利用
php://filter读取源码。 - 利用强弱类型比较差异绕过逻辑限制。
- 在部分题目中可将 protected 属性改为 public 来绕过不可见字符限制。
七、案例三:典型 POP 链构造
某题中存在三个类:Show、Test、Modifier。关键触发点类似:
1 | class Modifier { |
这类题通常不是一个方法直接读 flag,而是通过多个魔术方法串起来:
1 | unserialize() |
构造 POP 链的步骤:
- 先找链尾,也就是最终的文件包含或文件读取。
- 再找哪个魔术方法能触发链尾。
- 继续往前找哪个属性或方法能触发这个魔术方法。
- 最后把对象属性按链路连接起来。
示例构造思路:
1 |
|
这里的重点不是死记这个 payload,而是理解对象之间的指向关系:
1 | $b->source -> $a |
这就是 POP 链中“把多个对象拼成一条执行路径”的过程。
八、POP 链分析方法
做 PHP 反序列化题时,可以按这个顺序分析:
1. 找入口
搜索:
1 | unserialize($_GET['x']) |
确认用户是否可控。
2. 找链尾
优先找危险函数:
1 | system |
3. 找触发器
看危险函数在哪个魔术方法中,或者能不能被魔术方法间接触发。
4. 连对象关系
根据属性访问和方法调用,把不同类串起来。
5. 处理过滤
常见过滤绕过:
- URL 编码空字节。
- 使用
php://filter。 - 强弱类型比较差异。
__wakeup()属性数量绕过。- 使用对象触发
__toString()或__get()。
九、防护建议
真实业务中应避免以下写法:
1 | unserialize($_GET['data']); |
加固建议:
- 不对用户可控数据使用
unserialize()。 - 使用 JSON 传递普通数据。
- 如果必须反序列化,使用
allowed_classes限制可反序列化类。 - 对序列化数据做签名校验。
- 避免在魔术方法中执行敏感操作。
- 不在析构、字符串转换、属性读取等魔术方法中调用危险函数。
- 对文件读取路径做白名单限制。
示例:
1 | $data = unserialize($input, ["allowed_classes" => false]); |
这可以避免反序列化成对象,但仍建议优先使用 JSON。
十、总结
PHP 反序列化的核心是对象生命周期和魔术方法触发。做题时不要一上来就拼 payload,而是先画出调用链:
1 | 入口 -> 触发方法 -> 中间对象 -> 链尾函数 -> 目标效果 |
本文总结的三个点比较常见:
__wakeup()绕过适合理解对象恢复过程。- 强弱类型比较适合理解 PHP 类型转换风险。
- POP 链适合训练从链尾反推触发路径的能力。
理解这些之后,再遇到 PHP 反序列化题,就可以从“看源码找链”开始,而不是盲目套 payload。