【安全攻防】序列化与反序列,你了解多少?
1.序列化与反序列化
首先要了解序列化与反序列化的定义,以及序列化反序列化所用到的基本函数。
序列化:把对象转换为字节序列的过程称为对象的序列化,相当于游戏中的存档。
PHP中的序列化函数serialize()
serialize()函数用于序列化对象
或数组
,并返回一个字符串。serialize()函数序列化对象后,可以很方便的将它传递给其他需要它的地方,且其类型和结构不会改变。
语法 | string serialize ( mixed $value ) |
---|---|
参数说明 | $value: 要序列化的对象或数组。 |
返回值 | 返回一个字符串。 |
示例:
<?php
highlight_file(__FILE__);
$sites=array('I', 'Like', 'PHP');
echo'<br/>';
var_dump(serialize($sites)); //把这个对象进行序列化
echo'<br/>';
classman{
public$name="xiaocui";
public$sex="man";
private$age=26;
}
$M=newman();//创建一个对象
var_dump(serialize($M)); //把这个对象进行序列化
?>
输出结果为:
string(47) "a:3:{i:0;s:1:"I";i:1;s:4:"Like";i:2;s:3:"PHP";}"
string(79) "O:3:"man":3:{s:4:"name";s:7:"xiaocui";s:3:"sex";s:3:"man";s:8:"manage";i:26;}"
参数说明:
数组的序列化:
a 代表一数组
3 代表数组中有3个元素
i 代表数组的下标
0 代表I元素的下标值
s 代表元素I的数据类型为字符型
1 代表元素I的长度为1
对象的序列化:
O 代表是一个对象
3 代表类名man的长度
3 代表类中的字段数
s 代表属性name的类型为字符型
4 代表属性name的长度
//后面的以此类推,序列化字符串中字段内容以{开始,;}结束
反序列化:把字节序列恢复为对象的过程称为对象的反序列化,相当于游戏中的读档。
【一一帮助安全学习,所有资源获取处一一】
①网络安全学习路线
②20份渗透测试电子书
③安全攻防357页笔记
④50份安全攻防面试指南
⑤安全红队渗透工具包
⑥信息收集80条搜索语法
⑦100个漏洞实战案例
⑧安全大厂内部视频资源
⑨历年CTF夺旗赛题解析
PHP中的反序列化函数unserialize()
unserialize()函数用于将序列化后的字符串在还原为原来的数组或对象
的过程。unserialize()函数可以快速将序列化的字符串还原出来,用来完成它本来的功能。
语法 | mixed unserialize ( string $str ) |
---|---|
参数说明 | $str: 序列化后的字符串。 |
返回值 | 返回的是转换之后的值,可为 integer、float、string、array 或 object。不可反序列化时返回FALSE,并抛出提醒。 |
示例代码:
<?php
highlight_file(__FILE__);
$sites=array('I', 'Like', 'PHP');
echo'<br/>';
echo$ser=serialize($sites).'<br/>'; //把这个对象进行序列化
var_dump(unserialize($ser)); //把序列化的字符串进行反序列化
echo'<br/>';
classman{
public$name="xiaocui";
public$sex="man";
private$age=26;
}
$M=newman();//创建一个对象
echo$ser=serialize($M).'<br/>'; //把这个对象进行序列化
var_dump(unserialize($ser)); //把序列化的字符串进行反序列化
?>
输出结果为:
a:3:{i:0;s:1:"I";i:1;s:4:"Like";i:2;s:3:"PHP";}
array(3) { [0]=>string(1) "I"[1]=>string(4) "Like"[2]=>string(3) "PHP"}
O:3:"man":3:{s:4:"name";s:7:"xiaocui";s:3:"sex";s:3:"man";s:8:"manage";i:26;}
object(man)#2 (3) { ["name"]=> string(7) "xiaocui" ["sex"]=> string(3) "man" ["age":"man":private]=> int(26) }
可以看出上述反序列化后的数组或对象,与原数据没有任何变化。
序列化与反序列化在系统中的作用
①把对象的字节序列永久放在磁盘中,需要时可以随时调用,大大节省磁盘占用空间。
②在传输过程中可以直接传输字节序列,而不是对象,这可以大大提高传输速率。
在业务系统中,需要对一些对象进行序列化存储,让他们离开内存空间,存放在一个文件中,以便持久化保存。例如:在用户注册并登录系统时,会将用户信息如用户名,密码,cookie等信息先通过序列化存储起来,当用户再次登录时将序列化后的字节序列通过反序列化成原对象到内存进行使用,可以大大节省内存开销。
2.魔术方法
PHP将所有以__
(两个下划线)开头的类方法保留为魔术方法。所以在定义类方法时,除了上述魔术方法,建议不要以__
为前缀。
PHP中常见魔术方法
__construct()
具有 __construct函数的类会在每次创建新对象时先调用此方法,适合在使用对象之前做一些初始化工作。
代码示例:
<?php
highlight_file(__FILE__);
classdemo{
public$name="xiaocui";
public$sex="man";
private$age=26;
publicfunction__construct()
{
echo"<br/>"."类被实例化时调用我!";
}
}
$D=newdemo(); //实例化对象
?>
输出结果:
类被实例化时调用我!
__destruct()
该函数会在到某个对象的所有引用都被删除或者当对象被销毁时执行
代码示例:
<?php
highlight_file(__FILE__);
class demo{
public $name="xiaocui";
public $sex="man";
private $age=26;
public function __construct()
{
echo "<br/>"."类被实例化时调用我!"."<br/>";
}
public function num($a,$b){
echo $c = $a + $b.'<br/>';
return $c;
}
public function __destruct(){
echo "当类中的所有方法都被销毁时调用我!";
}
public function person($per){
echo "We are $per !!!".'<br/>';
}
}
$D=new demo(); //实例化对象
$D->num(5,6); //调用num()方法
$D->person(man); //调用person()方法
?>
输出结果:
类被实例化时调用我!
11
Weareman!!!
当类中的所有方法都被销毁时调用我!
上述输出结果可以很直观的看到代码的执行顺序:__construct()
=>num(5,6)
=>person(nanren)
=>__destruct()
实例化对象时首先要执行构造方法__construct()
,然后接着执行类中的实例num()
、person()
,当所有方法都执行完成销毁时,最后调用析构方法__destruct()
。
__wakeup()
在使用unserialize()
时,会检查是否存在一个__wakeup()
魔术方法。如果存在,则该方法会先被调用,预先准备对象需要的资源。
代码示例:
<?php
highlight_file(__FILE__);
classdemo{
public$name="xiaocui";
protected$sex="man";
private$age=26;
publicfunction__construct()
{
echo"<br/>"."类被实例化时调用我!"."<br/>";
}
publicfunction__destruct(){
echo"<br/>"."当类中的所有方法都被销毁时调用我!"."<br/>";
}
publicfunction__wakeup()
{
echo"<br/>"."当反序列时首先调用我 !".'<br/>';
}
}
$D=newdemo(); //实例化对象
echo$ser=serialize($D); //序列化对象$D
var_dump(unserialize($ser)); //反序列化字符串$ser
?>
输出结果:
由于$age
为私有属性,在序列化时会在前后加空白字符%00,原本的字符长度为7,而在两侧加入空白字符则字符长度就会变为9
类被实例化时调用我!
O:4:"demo":3:{s:4:"name";s:7:"xiaocui";s:6:"*sex";s:3:"man";s:9:"demoage";i:26;}
当反序列时调用我 !
object(demo)#2
(3) { ["name"]=> string(7) "xiaocui" ["sex":protected]=>
string(3) "man" ["age":"demo":private]=> int(26) }
当类中的所有方法都被销毁时调用我!
当类中的所有方法都被销毁时调用我!
上述输出结果可以很直观的看到代码的执行顺序:__construct()
=>serialize($D)
=>__wakeup()
=>unserialize($ser)
=>__destruct()
=>__destruct()
实例化对象时首先要执行构造方法__construct()
,然后执行序列化serialize($D)
,然后再反序列化之前要执行__wakeup()
方法,然后执行反序列化unserialize($ser)
,当所有方法都执行完成销毁时最后执行__destruct()
析构方法,由于反序列化后将原来的序列化字符串还原又执行一次__destruct()
。
__toString()
__toString()
方法用于定义一个类被当成字符串时该如何处理。
示例代码:
<?php
highlight_file(__FILE__);
class demo{
public $name="xiaocui";
protected $sex="man";
private $age=26;
public function __construct()
{
echo "<br/>"."类被实例化时调用我!"."<br/>";
}
public function __destruct(){
echo "<br/>"."当类中的所有方法都被销毁时调用我!"."<br/>";
}
public function __wakeup()
{
echo "<br/>"."当反序列时调用我 !".'<br/>';
}
public function __toString(){
return "<br/>"."类被当成字符串处理时调用我!"."<br/>";
}
}
$D=new demo(); //实例化对象
echo $D; //类被当成字符串输出
?>
输出结果:
类被实例化时调用我!
类被当成字符串处理时调用我!
当类中的所有方法都被销毁时调用我!
上述输出结果可以看到,当类被当成字符串echo
时,__toString()
方法被调用并执行。
如果该类中没有__toString()
方法,进行echo输出时,PHP会抛出致命性错误,错误如下:
Catchablefatalerror: ObjectofclassdemocouldnotbeconvertedtostringinD:\XXXX\phpstudy_pro\WWW\two\demo.phponline30
__sleep()
在使用serialize()
函数时,程序会检查类中是否存在一个__sleep()
魔术方法。如果存在,则该方法会先被调用,然后再执行序列化操作。
__sleep()
方法常用于提交未提交的数据,或类似的清理操作。同时,如果有一些很大的对象,但不需要全部保存,该函数就起到了很好的清理作用。
示例代码:
<?php
highlight_file(__FILE__);
class demo{
public $name="xiaocui";
protected $sex="man";
private $age=26;
public function __construct()
{
echo "<br/>"."类被实例化时调用我!"."<br/>";
}
public function __destruct(){
echo "<br/>"."当类中的所有方法都被销毁时调用我!"."<br/>";
}
public function __wakeup()
{
echo "<br/>"."当反序列时调用我 !".'<br/>';
}
public function __sleep(){
echo "<br/>"."当序列时调用我 !".'<br/>';
return array("name","sex","age"); //这里必须返回一个数值,里边的元素表示返回的属性名称
}
}
$D=new demo(); //实例化对象
echo $ser = serialize($D); //序列化对象
?>
输出结果:
类被实例化时调用我!
当序列时调用我!
O:4:"demo":3:{s:4:"name";s:7:"xiaocui";s:6:"*sex";s:3:"man";s:9:"demoage";i:26;}
当类中的所有方法都被销毁时调用我!
上述输出结果可以看到,当类被序列化时首先调用了__sleep()
方法,该函数必须返回一个数值。如果该函数没有返回属性的话,序列化时会将属性清空。
__invoke()
当尝试以调用函数的方式调用一个对象时,__invoke
方法会被自动调用。(本特性只在 PHP 5.3.0 及以上版本有效。)
代码示例:
<?php
highlight_file(__FILE__);
class demo{
public $name="xiaocui";
protected $sex="man";
private $age=26;
public function __construct()
{
echo "<br/>"."类被实例化时调用我!"."<br/>";
}
public function __destruct(){
echo "<br/>"."当类中的所有方法都被销毁时调用我!"."<br/>";
}
public function __wakeup()
{
echo "<br/>"."当反序列化时调用我 !".'<br/>';
}
public function __sleep(){
echo "<br/>"."当序列化时调用我 !".'<br/>';
return array("name","sex","age");
}
public function __invoke()
{
echo "<br/>"."当以函数的方式调用对象时会调用我!".'<br/>';
}
}
$D=new demo(); //实例化对象
$D(); //以函数的方式调用对象
?>
结果输出:
类被实例化时调用我!
当以函数的方式调用对象时会调用我!
当类中的所有方法都被销毁时调用我!
上述结果可以看到当使用函数的方法调用对象时,就会调用__invoke()
方法.
如果没有类中没有该方法,那么PHP会报出致命性错误,如下:
Fatalerror:
UncaughtError:
FunctionnamemustbeastringinD:\xxxxx\phpstudy_pro\WWW\two\demo.php:42Stacktrace:
#0 {main} thrown in D:\xxxxx\phpstudy_pro\WWW\two\demo.php on line 42
__call()
在对象中调用一个不存在或者不可访问方法时,__call
会被调用。
代码示例:
<?php
highlight_file(__FILE__);
class demo{
public $name="xiaocui";
protected $sex="man";
private $age=26;
public function __construct()
{
echo "<br/>"."类被实例化时调用我!"."<br/>";
}
public function num($a,$b){
echo "<br/>".$c = $a + $b.'<br/>';
return $c;
}
public function __destruct(){
echo "<br/>"."当类中的所有方法都被销毁时调用我!"."<br/>";
}
public function person($per){
echo "<br/>"."We are $per !!!".'<br/>';
}
public function __wakeup()
{
echo "<br/>"."当反序列化时调用我 !".'<br/>';
}
public function __sleep(){
echo "<br/>"."当序列时调用我 !".'<br/>';
return array("name","sex","age");
}
public function __call($arg1,$arg2){
echo "<br/>"."当对象调用一个不存在或者不可访问的方法时调用我!".'<br/>';
}
}
$D=new demo(); //实例化对象
$D->num1(1,2); //调用一个不存在的方法
?>
结果输出:
类被实例化时调用我!
当对象调用一个不存在或者不可访问的方法时调用我!
当类中的所有方法都被销毁时调用我!
__set()
给不可访问属性赋值时,__set
会被调用。
代码示例:
<?php
highlight_file(__FILE__);
class demo extends demo1{
public $name="xiaocui";
protected $sex="man";
private $age=26;
public function __construct()
{
echo "<br/>"."类被实例化时调用我!"."<br/>";
}
public function __destruct(){
echo "<br/>"."当类中的所有方法都被销毁时调用我!"."<br/>";
}
public function person($per){
echo "<br/>"."We are $per !!!".'<br/>';
}
public function __set($arg1,$arg2){
echo "<br/>"."当给不存在或者不可访问的属性赋值时调用我!"."<br/>";
}
}
class demo1{
private $weight;
public $height;
public function people(){
echo $this->weight;
echo $this->height;
}
}
$D=new demo(); //实例化对象
$D->weight=74; //给不可访问的属性赋值
?>
输出结果:
类被实例化时调用我!
当给不存在或者不可访问的属性赋值时调用我!
当类中的所有方法都被销毁时调用我!
__isset()
对不可访问属性调用isset()
或empty()
时,__iset()
会被调用。
__unset()
对不可访问属性调用unset()
时,__unset()
会被调用。
__get()
读取不可访问属性的值时,__get
会被调用。
代码示例:
<?php
highlight_file(__FILE__);
class demo extends demo1{
public $name="xiaocui";
protected $sex="man";
private $age=26;
public function __construct()
{
echo "<br/>"."类被实例化时调用我!"."<br/>";
}
public function __destruct(){
echo "<br/>"."当类中的所有方法都被销毁时调用我!"."<br/>";
}
public function __get($arg1){
echo "<br/>"."读取不存在或不可访问的属性时调用我!";
}
}
class demo1{
private $weight = 0;
public $height;
public function people(){
echo $this->weight;
echo $this->height;
}
}
$D=new demo(); //实例化对象
$D->weight; //读取父类中不可访问的属性
?>
输出结果:
类被实例化时调用我!
读取不存在或不可访问的属性时调用我!
当类中的所有方法都被销毁时调用我!
3.反序列化漏洞
3.1 反序列化漏洞利用条件
① unserialize()函数中参数可控
② 存在可利用的类,且类中有魔术方法
代码示例1:
<?php
highlight_file(__FILE__);
class demo
{
public $arg1 = "0";
public function __destruct()
{
echo $this->arg1; //输出用户传递的arg1值
}
}
$a=$_GET['arg']; //接收前端传递的arg1变量
$unser = unserialize($a); //反序列化传递的arg1
?>
上述的代码满足反序列化漏洞的两个条件,arg
参数可控,也就是unserialize()
函数的参数可控;存在可利用的类demo
,且demo类中有可利用的魔术方法__destruct()
,然而__destruct()
魔术方法中使用echo输出变量了arg
。
通过arg
参数,构造序列化字符串,通过unserialize()
函数进行反序列化,最后通过__destruct()
魔术方法输出变量,造成XSS
。
示例代码2
<?php
highlight_file(__FILE__);
class demo
{
public $arg1 = "0";
public function __destruct()
{
eval($this->arg1); //eval()去执行用户传递的arg1值
}
}
$a=$_GET['arg']; //接收前端传递的arg1变量
$unser = unserialize($a); //反序列化传递的arg1
var_dump($unser);
?>
如果魔术方法中存在eval()
,且参数可控,那么通过构造序列化字符串,通过反序列化漏洞造成RCE
。
3.2 __wakeup()函数绕过
在序列化时传递对象的属性大于实际对象的属性时,__wakeup()
魔术方法将不会被执行,从而导致绕过。
PHP版本<=5.6.25或者PHP版本<=7.0.11
示例代码:
<?php
highlight_file(__FILE__);
class demo
{
public $arg1 = "0";
public function __destruct()
{
eval($this->arg1); //eval()去执行用户传递的arg1值
}
public function __wakeup(){
foreach(get_object_vars($this) as $k => $v) {
$this->$k = ''; //将传入的参数遍历,全部赋值为空
}
}
}
$a=$_GET['arg']; //接收前端传递的arg1变量
$unser = unserialize($a); //反序列化传递的arg1
var_dump($unser);
?>
如果属性值与对象实际属性值相同,则会在反序列化时执行__wakeup()
函数将传入的变量值替换为空。
如果属性值设置大于实际对象属性值则会绕过__wakeup()
函数。