相对于网上其他关于Serialize的讲解,不下万篇,学习笔记,稍微总结一番。本篇文章按照浅谈php反序列化漏洞,进行学习指导,基础内容略过。
举个栗子
先举个例子说明serialize()
与unserialize()
显而易见,两个函数的例子所表达的形式就是这么简单。
再捡个栗子
接下来看看下面的例子:
1 |
|
针对其中的代码分析,可以看到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);
)。 下图呈现两种函数执行形式:
可以看到serialize
序列化对象,得到的是对象引用类的字符串输出,unserialize
得到的结果与之相反,将对象引用类的字符串输出转化为类中引用值的呈现模式。
深入栗子
在php官方的简述中,有以下红标记的描述,提到了__sleep()
和__wakeup()
,只是啥呢,以第一个栗子引申出来剖析
还是一样的代码,不过类中我们加了一些东西
1 |
|
__wakeup()
、__construct()
、__destruct()
这三个函数是干嘛的呢
- 构造函数
__construct()
:当对象创建(new)时会自动调用。但在unserialize()
时是不会自动调用的。 - 析构函数
__destruct()
:当对象被销毁时会自动调用。 - 魔法函数
__wakeup()
:unserialize()
时会自动调用。
我们先来看看运行结果:
可以看到,在创建a
对象时,自动调用了__construct()
;serialize($a)
时调用__sleep
函数;unserialize($a_value)
时调用了__wakeup()
。
而在其中,不知你是否发现其中__sleep()
我是这样写的
1 | function __sleep() |
相对于其他函数,多了个return array("name");
,为什么呢?
在php
文档中,针对这个原因,是这样描述的
其中,不知你看到这句没
我们试试没有return
返回值的情况
不加这句的后果,如下图:
那如果,我返回的值设置不是name
时,即这里改为return array("namef");
,以下是报错结果
这样的描述,是否能更好解决你的疑问呢?
构造栗子
现在我们看看下面这段代码,尝试加在刚刚的代码后
1 | $Str_1 = 'O:1:"A":1:{s:4:"name";s:4:"Anny";}'; |
也许你注意到了$Str_1
的值,没错,这次我们通过自行构造一个name
为Anny
,看看反序列化后的值。
这里就需要我们想想,反序列化的漏洞是如何体现的呢?本质原因就是我们可以构造一串伪造的字符串,通过unserialize
函数,覆盖掉原本类成员变量的默认值。这一点就像C语言中的构造函数,传入的值会取代原本其成员变量的默认值。
有兴趣的同学可以试试下面的$Str_1的替换,看看会产生什么结果?
1 | $Str_1 = 'O:14:"Test_serialize":1:{s:4:"name";s:4:"test";}'; |
你能直观的看到,应是下面的内容
再对比下图
发现了没,析构函数多了一个?__wakeup
函数也多了?这是不是意味着什么呢?
说了这么多,烦了嘛?接下来,我们来模拟下漏洞场景吧!
在前面我们提到过,unserialize()
时会自动调用魔法函数__wakeup()
。这个漏洞的关键点就在于如何通过__wakeup()
中的执行语句,执行危险命令,下面是个简单模拟,在__wakeup()
中我们只是输出引用的name
的值(那假如__wakeup()
中存在其他的**命令呢?)
再挑白点,看看下面的例子
如果我将Anny
再改成其他,后果可想而知。
再构栗子
两个不同类,在一个类中调用另一个类的对象
1 | <?php |
通过username
传入构造好的序列化字符串后,进行反序列化时自动调用 __wakeup()
函数,从而在Object_serialize
会自动调用对象test
的类Object_Example
中的__construct()
方法,从而写入到东西到文件中。
炭烤栗子
某次比赛test.php
源码(2018fj
)
1 |
|
看看上面的代码,首先执行就两句话
1 | $a=@$_POST['a']; |
传入post
的数据,然后反序列化该值。我们知道,unserialize()
函数执行后,会自动执行__wakeup()
预备执行对象的资源,进行初始化。而当__wakeup()
函数执行完后,会调用析构函数__destruct()
释放资源,仔细观察 __wakeup()
1 | function __wakeup(){ |
发现反序列化的值会被其中waf
()进行空格过滤,如下图
再接着调用了__destruct()
,方便观察我把每个函数执行的过程呈现出来
其中语句call_user_func_array(array($this, $this->method), $this->args);
,指的是回调当前类$this
的$this->method
方法,$this->args
为传入的$this->method
方法的参数值。在本题中就是往test
类的ping()
中传入了一个$host
为类变量$args
的值,随后通过构造好的反序列化的字符串,我们拿到了想要的东西。
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 | <?php |
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";}}
关于序列化的问题,发现并不是很难,只要代码看的懂,执行步骤明白,知道漏洞的原理,没有想象的那么难,这篇还没完,持续更新,关于反序列化这只是冰山一角。青山不改,绿水长流。