php的序列化
序列化函数:serialize,反序列化函数:unserialize,例:
<?php
class Test{
public $a = 'ThisA';
protected $b = 'ThisB';
private $c = 'ThisC';
public function test1(){
return "this is test1" ;
}
}
$test = new Test();
var_dump(serialize($test));
输出如下:
string(84) "O:4:"Test":3:{s:1:"a";s:5:"ThisA";s:4:" * b";s:5:"ThisB";s:7:" Test c";s:5:"ThisC";}"
其中:
O
:代表数据类型是对象Object,4
:代表该对象名有4个字符,Test
:表示对象名称,3
:表示对象里有3个成员。后面花括号里分三组看,第一组s:1:"a";s:5:"ThisA";
,分别是对变量名和变量值的序列化表示。
可以看到,虽然Test类中有个test1方法,但是序列化后的字符串并没有包含这部分,因此得知序列化不保存方法。
这里几个变量序列化后的变量名都各不相同,因为分别是public, protected, private, php为了区分这些属性添加了一些修饰。
php反序列化魔术方法
void __wakeup ( void ):
unserialize( )会检查是否存在一个_wakeup( ) 方法。如果存在,则会先调用_wakeup 方法,预先准备对象需要的资源。
void __construct ([ mixed $args [, $… ]]):
具有构造函数的类会在每次创建新对象时先调用此方法。
void __destruct ( void ):
析构函数会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行。
public string __toString ( void ):
__toString( ) 方法用于一个类被当成字符串时应怎样回应。例如 echo $obj;应该显示些什么。
此方法必须返回一个字符串,否则将发出一条 E_RECOVERABLE_ERROR 级别的致命错误。
这里举个例子来说明各个魔术方法自动执行的场景
(参考:https://www.freebuf.com/articles/web/209975.html)
<?php
class Demo{
public function __construct(){
echo "construct run\n";
}
public function __destruct(){
echo "destruct run\n";
}
public function __toString(){
echo "toString run\n";
return "toString run\n";
}
public function __sleep(){
echo "sleep run\n";
return array();
}
public function __wakeup(){
echo "wakeup run\n";
}
}
/**/
echo "-------new了一个对象,对象被创建,执行__construct-------\n";
$test = new Demo();
/**/
echo "-------serialize了一个对象,对象被序列化,先执行__sleep,再序列化-------\n";
$sTest = serialize($test);
/**/
echo "-------unserialize了一个序列化字符串,对象被反序列化,先反序列化,再执行__wakeup-------\n";
$usTest = unserialize($sTest);
/**/
echo "-------把Test这个对象当做字符串使用了,执行__toString-------\n";
$string = 'hello class ' . $test;
/**/
echo "-------程序运行完毕,对象自动销毁,执行__destruct-------\n";
输出:
-------new了一个对象,对象被创建,执行__construct-------
construct run
-------serialize了一个对象,对象被序列化,先执行__sleep,再序列化-------
sleep run
-------unserialize了一个序列化字符串,对象被反序列化,先反序列化,再执行__wakeup-------
wakeup run
-------把Test这个对象当做字符串使用了,执行__toString-------
toString run
-------程序运行完毕,对象自动销毁,执行__destruct-------
destruct run
destruct run
绕过__wakeup()函数
CVE-2016-7124:
反序列化时,如果表示对象属性个数的值大于真实的属性个数时就会跳过__wakeup( )的执行。
影响版本:
- PHP before 5.6.25
- 7.x before 7.0.10
绕过一些正则
以攻防世界的Web_php_unserialize这道题为例,源码如下:
<?php
class Demo {
private $file = 'index.php';
public function __construct($file) {
$this->file = $file;
}
function __destruct() {
echo @highlight_file($this->file, true);
}
function __wakeup() {
if ($this->file != 'index.php') {
//the secret is in the fl4g.php
$this->file = 'index.php';
}
}
}
if (isset($_GET['var'])) {
$var = base64_decode($_GET['var']);
if (preg_match('/[oc]:\d+:/i', $var)) {
die('stop hacking!');
} else {
@unserialize($var);
}
} else {
highlight_file("index.php");
}
?>
这里可以看出有两个需要绕过的点:
function __wakeup()
preg_match('/[oc]:\d+:/i', $var)
绕过__wakeup()的方式上面已经讲了,这里重点说一下绕过preg_match这一块,这里参考一个大佬的文章:https://www.guildhab.top/?p=990
先说结论吧:
这里可以将序列化字符串O:4:"Demo":1:{s:10:" Demo file";s:8:"fl4g.php";}
改为O:+4:"Demo":1:{s:10:" Demo file";s:8:"fl4g.php";}
来绕过正则检测。因为在php5中,对序列化字符串的解析过程中,若O:+4
:
后面出现+
,会跳过它直接检查下一位是否是数字,若是数字则继续解析下去。
接下来看大佬的分析过程,这里下载了一份php5的源码来解析:
-
先看 var_unserializer.c 文件 441 行
序列化字符串的第一位为O
,因此这里跳转到yy13
。
注意这里区分大小写 ! -
yy13
yy13
会判断下一位的字符是否为:
, 若是就跳转到yy17
,若不是就跳转到yy3
,这里会跳转到yy17
。 -
yy17
yy17
会判断下一位是否为数字, 若为数字就跳转到yy20
,若为+
就跳转到yy19
。 -
yy19
yy19
会判断下一位是否为数字,若为数字就跳转到yy20
,否则跳转到yy18
。
问题来了!当反序列化操作读取到 :
号时 , 下面不管是数字
还是+数字
,都会跳转到yy20
,而正则匹配的规则能过滤O:4
,却不会过滤O:+4
因此,我们可以利用O:+4
这样的写法来绕过正则过滤。
这里大佬的分析让看到了另一条研究底层的思路,直接下载php本身的源码进行解析。