浅谈PHP反序列化漏洞

摘要:PHP反序列化

PHP序列化

序列化是将变量转换为可保存或传输的字符串的过程。

函数 serialize(),可将变量转换为字符串并且在转换中保存当前变量的值。
序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字。php允许保存一个对象方便以后重用,这个过程被称为序列化。
我们先建一个Test对象,存入信息,然后直接输出对象

1
2
3
4
5
6
7
8
9
<?php
class Test{
public $id=1;
public $name="admin";
public $password="admins";
}
$m=new Test();
print_r($m);
?>

运行结果:

1
2
3
4
5
6
Test Object
(
[id] => 1
[name] => admin
[password] => admins
)

先new一个实例$m,再用serialize()函数将这个对象进行序列化成字符串,然后输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class Test
{
public $id=1;
public $name ="admin";
public $password="admins";
}
$m=new Test();
$m->id=2;
$m->name="root";
$m->password="roots";

echo serialize($m);
?>

序列化后的结果:

1
O:4:"Test":3:{s:2:"id";i:2;s:4:"name";s:4:"root";s:8:"password";s:5:"roots";}

分析一下输出结果:
在这里插入图片描述

黑色箭头,字母O代表Object对象;如果是A,则代表Array数组。
黄色箭头,数字4,代表对象名称Test占4个字符。
橙色箭头,数字3,代表对象里面有3个变量。
红色箭头,字母s代表string类型,i代表int类型。
紫色箭头,数字2,代表变量名占2个字符。

PHP反序列化

反序列化是在适当的时候把这个字符串再转化成原来的变量使用。

函数unserialize ,把serialize序列化后的字符串变成一个对象。可以从已存储的表示中创建PHP的值。恢复原先被序列化的变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
class Test
{
public $id=1;
public $name ="admin";
public $password="admins";
}
$m=new Test();
$m->id=2;
$m->name="root";
$m->password="roots";

$val = serialize($m);
$Nm=unserialize($val);
echo $Nm->name.'<br />';
echo $Nm->id;
?>

输出结果:
在这里插入图片描述
php在线反序列化工具
在这里插入图片描述

魔术函数

PHP面向对象变成中,有一类函数叫做魔术函数。这些函数是以__开头的,依照某些规则实例化类或者调用某些函数的时候会自动调用这些magic函数,

函数 描述
__wakeup() 触发unserialize()方法时会被调用
__sleep() 触发serialize()方法时会被调用
__construct() 创建一个对象时会被调用
__destruct() 销毁一个对象时会被调用
__get() 调出不可访问(private,protect等修饰)属性时会被调用。
__set() 修改或写入不可访问(private,protect等修饰)属性时会被调用。
__toString() 类对象被当作一个字符串使用时会被调用。
__isset() 对不可访问(private,protect等修饰)属性使用empty()或isset()方法时会被调用。
__unset() 对不可访问(private,protect等修饰)属性使用unset()方法时会被调用。
__invoke() 将实例化对象当作方法使用时会被调用。

PHP反序列化漏洞

__wakeup()和__sleep()

PHP __wakeup()函数漏洞
在程序执行前,serialize() 函数会首先检查是否存在一个魔术方法 __sleep。如果存在,__sleep()方法会先被调用, 然后才执行串行化(序列化)操作。
这个功能可以用于清理对象,并返回一个包含对象中所有变量名称的数组。如果该方法不返回任何内容,则NULL被序列化,导致一个E_NOTICE错误。与之相反,unserialize()会检查是否存在一个__wakeup方法。
如果存在,则会先调用__wakeup方法,预先准备对象数据。但是这个wakeup()是可以被绕过的__wakeup 触发于 unserilize() 调用之前, 当反序列化时的字符串所对应的对象的数目被修改,__wake 的函数就不会被调用. 并且不会重建为对象, 但是会触发其他的魔术方法比如__destruct。

来一道题

[X-CTF]unserialize3
题目源码:

1
2
3
4
5
6
class xctf{
public $flag = '111';
public function __wakeup(){
exit('bad requests');
}
?code=

打开题目,进行代码审计,可以看到xctf类只拥有一个public的flag变量,值为111。对xctf类进行序列化

1
2
3
4
5
6
7
8
9
10
<?php
class xctf{
public $flag = '111';
public function __wakeup(){
exit('bad requests');
}
}
$test=new xctf();
echo(serialize($test));
?>

序列化后的结果

1
O:4:"xctf":1:{s:4:"flag";s:3:"111";}

直接传参给code的结果
在这里插入图片描述
我们要绕过__wakeup这个魔术函数,利用反序列化漏洞,当序列化字符串中表示对象属性个数的值大于真实的属性个数时会绕过__wakeup的执行

将上面的序列化后字符串,对象属性个数由真实值1修改为2。

1
O:4:"xctf":2:{s:4:"flag";s:3:"111";}

在这里插入图片描述

序列化参数

序列化参数有三种,分别是public、protected和private,三者在序列化时有明显的区别。

public

1
2
3
4
5
6
<?php
class test{
public $test2="hello";
}
$test = new test();
echo serialize($test);

运行结果:
在这里插入图片描述
而在网页中运行的结果:

1
O:4:"test":1:{s:5:"test1";s:5:"hello";}

public序列化后的参数变成 test1

private

private 声明的字段为私有字段,只在所声明的类中可见,在该类的子类和该类的对象实例中均不可见。因此私有字段的字段名在序列化时,类名和字段名前面都会加上\0的前缀。字符串长度也包括所加前缀的长度。其中 \0 字符也是计算长度的。

1
2
3
4
5
6
<?php
class test{
private $test1="hello";
}
$test = new test();
echo serialize($test);

运行结果:
在这里插入图片描述
而在网页中运行的结果:

1
O:4:"test":1:{s:11:"\00test\00test2";s:5:"hello";}

private序列化后的参数被反序列化后变成 \00test\00test2

protected

protected 声明的字段为保护字段,在所声明的类和该类的子类中可见,但在该类的对象实例中不可见。因此保护字段的字段名在序列化时,字段名前面会加上\0\0的前缀。这里的 \0 表示 ASCII 码为 0 的字符(不可见字符),而不是 \0组合。这也许解释了,为什么如果直接在网址上,传递\0\0username会报错,因为实际上并不是\0,只是用它来代替ASCII值为0的字符。必须用python传值才可以。

1
2
3
4
5
6
<?php
class test{
protected $test3="hello";
}
$test = new test();
echo serialize($test);

运行结果:
在这里插入图片描述

而在网页中运行的结果:

1
O:4:"test":1:{s:8:"\00*\00test3";s:5:"hello";}

protected序列化后的参数变成 \00*\00test3


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!