从一道CTF题来理解PHP反序列化中的字符逃逸漏洞
一、前言
首先来看一下题目源码:
<?php
error_reporting(0);
class a
{
public $uname;
public $password;
public function __construct($uname,$password)
{
$this->uname=$uname;
$this->password=$password;
}
public function __wakeup()
{
if($this->password==='yu22x')
{
include('flag.php');
echo $flag;
}
else
{
echo 'wrong password';
}
}
}
function filter($string){
return str_replace('Firebasky','Firebaskyup',$string);
}
$uname=$_GET[1];
$password=1;
$ser=filter(serialize(new a($uname,$password)));
$test=unserialize($ser);
?>
从源码来看,我们需要使$password
为'yu22x'
才能成功得到 flag,但问题在于,$password
不是我们可控的,它被写死为 1,那如何才能让$password
可控呢?这就是本文要探讨的主题:字符逃逸漏洞。
二、漏洞原理
字符逃逸漏洞属于PHP反序列化漏洞中的其中一种,漏洞原因是在反序列化某字符属性时,由于长度异常而导致后面注入字符串被正常解析,导致我们构造的恶意字符串逃逸出正常的属性值中,最终在反序列化后恶意修改了类属性。
针对上面的题目,如果我们提交以下字符串,就能成功得到 flag:
FirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebaskyFirebasky";s:8:"password";s:5:"yu22x";}
这是$uname
的值,里面一共15个Firebasky
,加上后面的";s:8:"password";s:5:"yu22x";}
(30个字符),一共是165个字符,所以在serialize()
后,序列化的字符串中,类属性$uname
的长度也是165。
接下来,filter()
方法会把序列化字符串里的Firebasky
都替换为Firebaskyup
,多了2个字符,总增加 2 x 15 = 30 个字符,因此$uname
的实际长度其实已经达到了 165 + 30 = 195 个,但是在序列化字符串中,$uname
的长度还是被定义为 165。
关键来了,反序列化时unserialize()
是根据序列化字符串中定义的长度来获取数据的,这个长度之后的会被当作一个新的合法属性解析,所以会把以下当作$uname
的数据:
FirebaskyupFirebaskyupFirebaskyupFirebaskyupFirebaskyupFirebaskyupFirebaskyupFirebaskyupFirebaskyupFirebaskyupFirebaskyupFirebaskyupFirebaskyupFirebaskyupFirebaskyup
而后面紧跟的这个字符串将被当作正常属性解析:
s:8:"password";s:5:"yu22x";}
而因为PHP以左边数起第一个}
符号来做反序列化终点,所以后面原本的s:8:"password";s:5:"1";}
就被抛弃了,而此时,$password
的值也成功被修改为了'yu22x'
,成功得到 flag。
三、总结
这类漏洞一般只能在代码审计时检测出来,利用的关键是序列化后到反序列化前的这段时间中某些关键的属性长度发生改变。这种与原序列化时的长度不一致的情况,给攻击者提供了恶意注入数据来修改类属性的机会。