PHP反序列化漏洞
PHP7:反序列化漏洞案例及分析
1、漏洞历史
对于黑客来说,如果能够利用服务器端错误,那简直相当于中了头彩。因为用户倾向于将他们的数据保存在服务器中,如果黑客能够利用这个错误,就能针对某一个目标,从而获取更大的收益。PHP脚本语言是时下最流行的web服务器端语言之一。为了消除PHP开发过程中不同类型的漏洞,人们采用了多种安全编码方案。
然而,安全编码方案不能掩盖语言本身的缺陷。PHP是用相对低级的语言写成的,其中常见的漏洞有内存损坏漏洞,而use-after-free漏洞最为普遍。
这些年来,PHP语言得到了不断的改进,在2015年12月,一个重要的新版本——PHP7被公布出来。这个版本的内部结构与PHP5有很大不同,分配器已经发生了改变,而变量的内部表示(zvals)也完全不同了。
通过一个反序列化漏洞,Check Point研究小组成功演示了一项对PHP7的利用。在这篇报告中,我们将会一步步解释这是如何完成的。
2、技术背景
为了更好地解释这项利用,我们首先要回顾一些关键的技术细节。
(1)值和对象
在PHP-7中,用来保存值的结构与php-5有所不同。
在内部保存值的结构是zval(_zval_struct)。这个结构的第一个字段是zend_value,其中包含指向PHP基本类型的指针和结构,而主要类型有Boolean、integer、double、string、object和array 等。
我们需要关注的类型是String、Object和Array,它们在内部中被表示为zend_string、zend_object和zend_array结构。
zend_string是用于保存字符串的结构。当引擎创建了一个新的字符串后,它会分配足够的字节给zend_string结构,对字符串的大小进行扩充。然后,它会用字符串的数据填补这个结构的字段,并在结构的末尾添加上字符串的内容。因此,字符串创建为我们提供了一种在不同的尺寸中进行分配的方法:sizeof(zend_string)+ strlen(str)= 16 + strlen(str)。这样,我们就没法再伪造一个字符串zval,并让它指向我们想要的地方了,这和使用PHP-5时有所不同。
zend_object用来表示对象的基本结构。它通常被嵌入在一个代表着不同类型对象的结构中。当zval保存了一个对象时,它的value 字段是一个指向zend_object的指针。
zend_array(又名HashTable)是保存键值存储的结构。这是一个对哈希表数据结构的直接应用,其中的arData字段指向Bucket结构内的一个数组。
总体来说,我们可以看到,PHP-7值系统更倾向于嵌入结构(PHP-5相比)。这种改变可以提高代码的效率(减少分配),让我们难以利用与内存相关的bug(更少的引用)。
(2)PHP-7内存分配器
在PHP-7中,内存分配器的工作原理不同于PHP-5。小的分配(slot)由一个free list完成。每个分配大小都有一个对应的free list。free list通过一个或多个连续页(bin)进行初始化,而free list的初始化使得每一个slot指向下一个slot。一旦free list耗尽,一个新的bin会被分配出来。
重点:
一个slot的元数据是基于所在页面进行检索的。(地址对齐到最近的chunk)
下一个分配的位置可能是当前分配的位置+分配的大小。例如,如果分配器以0
x28的大小返回到地址0xf7e10000,那么下一个大小为0 x28的分配就位于0 xf7e10028。为了简单起见,我们假定这是真实的。注意,在最后一个primitive(下文Writing Memory / 64中会提到),我们设计了一个不依赖这一假设,但仍能触发错误的方法。
分配大小被四舍五入成了某个预定义的大小。
(3)反序列化
unserialize函数被用于将格式化字符串内的对象进行实例化,在反序列化期间,每个解析元素都有一个索引号,号码从1开始。
在内部,每个解析值都被放到了php_unserialize_data_t的两个数组中。第一个数组是values-array,第二个是destructor-array。在反序列化期间,值可以重新定义,即在stdClass(最基本的PHP的对象——一个键值存储)中,同一个key可以用不同的值反序列化两次。如果是这样的话,第一个定义会被覆盖,引用也会从数值数组中被移除。然而引用会被保存在destructor-array中。当反序列化结束时,destructor-array中每个值的引用数都会被减少,如果减少到零,它就会被释放。
所以请记住,在反序列化过程中,值不能被释放,只有最后的过程中才可以。
3、BUG (# 71311)
这里的bug是一个Use-After-Free bug,培训存在于标准php库内ArrayObject的反序列化函数中。
ArrayObject是一个SPL对象,它允许对象以数组的形式工作。在内部,它被表示为spl_array_object。这是该对象的序列化形式:
spacer.gif
C:11:"ArrayObject":37:{x:i:0;a:2:{i:0;i:0;i:1;i:1;};m:a:0:{}}
37是括号内的字符数
x:i:0;对应于结构中的nr_flags字段
a:2:{i:0;i:0;i:1;i:1;}对应于结构中的数组字段(从这个角度,它被称为internal数组以区别于对象本身)
m:a:0:{}对应于zend_object std字段内的properties字段(从这个角度,称为members数组)。
当对ArrayObject进行反序列化时,引擎首先会将一个默认的、拥有内部数组的ArrayObject实例化,然后解析ArrayObject的字段。当它解析到与内部数组相关的部分时,会释放初始的内部数组,然后通过引用,调用php_var_unserialize,并指向内部数组,目的是想让函数将它变成已经解析过的内部数组。内部数组可以是一个已经解析的数组的引用,在这种情况下,内部数组被修改为指向引用的数组,同时引用计数会有所增加。
在内部数组对自身进行引用时,错误出现了。这导致内部指针被分配给自己(即无操作),并指向释放了的数组,然后,数组的引用计数会增加。
4、有漏洞的代码
我们利用的代码常被用于反序列化开发。我们建立了一个运行以下PHP脚本的apache服务器:
这个脚本给了我们一个反馈。尽管我们对远程可利用性的要求有所降低,但在每一个情境中,反映到客户端的反序列化数据都是适合的。
我们通过向data参数内的脚本发送字符串进行了利用。在利用过程中,我们从返回的序列化字符串中推断出了一些内部信息。
PHP反序列化漏洞
0x00 序列化的作用
(反)序列化给我们传递对象提供了一种简单的方法。
serialize()将一个对象转换成一个字符串
unserialize()将字符串还原为一个对象
反序列化的数据本质上来说是没有危害的
用户可控数据进行反序列化是存在危害的
可以看到,反序列化的危害,关键还是在于可控或不可控。
0x01 PHP序列化格式
1. 基础格式
boolean
1 2 3 |
|
integer
1 2 3 |
|
double
1 2 |
|
NULL
1 |
|
string
1 2 |
|
array
1 2 |
|
2. 序列化举例
test.php
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
我们来生成一下它的序列化字符串:
serialize.php
1 2 3 4 5 6 |
|
代码不难懂,我们通过生成的序列化字符串,来细致的分析一下序列化的格式:
1 2 |
|
3. 注意
这里有一个需要注意的地方,testflag明明是长度为8的字符串,为什么在序列化中显示其长度为10?
翻阅php官方文档我们可以找到答案:
对象的私有成员具有加入成员名称的类名称;受保护的成员在成员名前面加上'*'。这些前缀值在任一侧都有空字节。
所以说,在我们需要传入该序列化字符串时,需要补齐两个空字节:
1 |
|
4. 反序列化示例
unserialize.php
1 2 3 4 5 |
|
0x02 PHP(反)序列化有关的魔法函数
construct(), destruct()
构造函数与析构函数
call(), callStatic()
方法重载的两个函数
__call()是在对象上下文中调用不可访问的方法时触发
__callStatic()是在静态上下文中调用不可访问的方法时触发。
get(), set()
__get()用于从不可访问的属性读取数据。
__set()用于将数据写入不可访问的属性。
isset(), unset()
__isset()在不可访问的属性上调用isset()或empty()触发。
__unset()在不可访问的属性上使用unset()时触发。
sleep(), wakeup()
serialize()检查您的类是否具有魔术名sleep()的函数。如果是这样,该函数在任何序列化之前执行。它可以清理对象,并且应该返回一个数组,其中应该被序列化的对象的所有变量的名称。如果该方法不返回任何内容,则将NULL序列化并发出E_NOTICE。sleep()的预期用途是提交挂起的数据或执行类似的清理任务。此外,如果您有非常大的对象,不需要完全保存,该功能将非常有用。
unserialize()使用魔术名wakeup()检查函数的存在。如果存在,该功能可以重构对象可能具有的任何资源。wakeup()的预期用途是重新建立在序列化期间可能已丢失的任何数据库连接,并执行其他重新初始化任务。
__toString()
__toString()方法允许一个类决定如何处理像一个字符串时它将如何反应。
__invoke()
当脚本尝试将对象调用为函数时,调用__invoke()方法。
__set_state()
__clone()
__debugInfo()
0x03 PHP反序列化与POP链
就如前文所说,当反序列化参数可控时,可能会产生严重的安全威胁。
面向对象编程从一定程度上来说,就是完成类与类之间的调用。就像ROP一样,POP链起于一些小的“组件”,这些小“组件”可以调用其他的“组件”。在PHP中,“组件”就是这些魔术方法(__wakeup()或__destruct)。
一些对我们来说有用的POP链方法:
命令执行:
1 2 3 4 |
|
文件操作:
1 2 3 |
|
2. POP链demo
popdemo.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
上面的代码即完成了一个简单的POP链,若传入一个构造好的序列化字符串,则会完成写文件操作。
poc.php
1 2 3 4 5 |
|
1 2 3 |
|
表面看上去,我们完美的执行了代码的功能,那么我们改一下序列化代码,看一看效果:
改为:
1 2 |
|
便执行了我们想要执行的效果:
3. Autoloading与(反)序列化威胁
PHP只能unserialize()那些定义了的类
传统的PHP要求应用程序导入每个类中的所有类文件,这样就意味着每个PHP文件需要一列长长的include或require方法,而在当前主流的PHP框架中,都采用了Autoloading自动加载类来完成这样繁重的工作。
在完善简化了类之间调用的功能的同时,也为序列化漏洞造成了便捷。
举个例子:
目录结构为下:
index.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
test1.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
其余的test2和test3和test1的内容类似。
运行一下index.php:
可以看到已经自动加载类会自动寻找已经注册在其队列中的类,并在其被实例化的时候,执行相关的操作。
若想了解更多关于自动加载类的资料,请查阅spl_autoload_register
4. Composer与Autoloading
说到了Autoloader自动加载类,就不得不说一下Composer这个东西了。Composer是PHP用来管理依赖(dependency)关系的工具。你可以在自己的项目中声明所依赖的外部工具库(libraries),Composer 会帮你安装这些依赖的库文件。
经常搭建框架环境的同学应该对这个非常熟悉了,无论是搭建一个新的Laravel还是一个新的Symfony,安装步骤中总有一步是通过Composer来进行安装。
比如在安装Laravel的时候,执行composer global require "laravel/installer"就可以搭建成以下目录结构的环境:
其中已经将环境所需的依赖库文件配置完毕,正是因为Composer与Autuoloading的有效结合,才构成了完整的POP数据流。
0x04 反序列化漏洞的挖掘
1. 概述
通过上面对Composer的介绍,我们可以看出,Composer所拉取的依赖库文件是一个框架的基础。
而Composer默认是从Packagist来下载依赖库的。
所以我们挖掘漏洞的思路就可以从依赖库文件入手。
目前总结出来两种大的趋势,还有一种猜想:
1.从可能存在漏洞的依赖库文件入手
2.从应用的代码框架的逻辑上入手
3.从PHP语言本身漏洞入手
接下来逐个的介绍一下。
2. 依赖库
以下这些依赖库,准确来说并不能说是依赖库的问题,只能说这些依赖库存在我们想要的文件读写或者代码执行的功能。而引用这些依赖库的应用在引用时并没有完善的过滤,从而产生漏洞。
cartalyst/sentry
cartalyst/sentinel
寻找依赖库漏洞的方法,可以说是简单粗暴:
首先在依赖库中使用RIPS或grep全局搜索__wakeup()和__destruct()
从最流行的库开始,跟进每个类,查看是否存在我们可以利用的组件(可被漏洞利用的操作)
手动验证,并构建POP链
利用易受攻击的方式部署应用程序和POP组件,通过自动加载类来生成poc及测试漏洞。
以下为一些存在可利用组件的依赖库:
任意写
monolog/monolog(<1.11.0)
guzzlehttp/guzzle
guzzle/guzzle
任意删除
swiftmailer/swiftmailer
拒绝式服务(proc_terminate())
symfony/process
下面来举一个老外已经说过的经典例子,来具体的说一下过程。
例子
1. 寻找可能存在漏洞的应用
存在漏洞的应用:cartalyst/sentry
漏洞存在于:/src/Cartalyst/Sentry/Cookies/NativeCookie.php
1 2 3 4 5 6 7 8 |
|
应用使用的库中的可利用的POP组件:guzzlehttp/guzzle
寻找POP组件的最好方式,就是直接看composer.json文件,该文件中写明了应用需要使用的库。
1 2 3 4 5 6 7 8 |
|
2. 寻找可以利用的POP组件
我们下载guzzlehttp/guzzle这个依赖库,并使用grep来搜索一下__destruct()和__wakeup()
逐个看一下,在/guzzle/src/Cookie/FileCookieJar.php发现可利用的POP组件:
跟进看一下save方法:
存在一下代码,造成任意文件写操作:
1 |
|
注意到现在$filename可控,也就是文件名可控。同时看到$jsonStr为上层循环来得到的数组经过json编码后得到的,且数组内容为$cookie->toArray(),也就是说如果我们可控$cookie->toArray()的值,我们就能控制文件内容。
如何找到$cookie呢?注意到前面
跟进父类,看到父类implements了CookieJarInterface
还有其中的toArray方法
很明显调用了其中的SetCookie的接口:
看一下目录结构:
所以定位到SetCookie.php:
可以看到,这里只是简单的返回了data数组的特定键值。
3. 手动验证,并构建POP链
首先我们先在vm中写一个composer.json文件:
1 2 3 4 5 |
|
接下来安装Composer:
1 |
|
然后根据composer.json来安装依赖库:
1 |
|
接下来,我们根据上面的分析,来构造payload:
payload.php
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
我们执行完该脚本,看一下生成的脚本的内容:
我们再写一个反序列化的demo脚本:
1 2 3 |
|
运行后,完成任意文件写操作。至此,我们可以利用生成的序列化攻击向量来进行测试。
3. PHP语言本身漏洞
提到这一点就不得不说去年的CVE-2016-7124,同时具有代表性的漏洞即为SugarCRM v6.5.23 PHP反序列化对象注入。
在这里我们就不多赘述SugarCRM的这个漏洞,我们来聊一聊CVE-2016-7124这个漏洞。
触发该漏洞的PHP版本为PHP5小于5.6.25或PHP7小于7.0.10。
漏洞可以简要的概括为:当序列化字符串中表示对象个数的值大于真实的属性个数时会跳过__wakeup()的执行。
我们用一个demo来解释一下。
例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
|
代码很简单,但是关键就是需要再反序列化的时候绕过__wakeup以达到写文件的操作。
根据cve-2016-7124我们可以构造一下我们的poc:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
运行该脚本,我们就获得了我们poc
通上文所说道的,在这里需要改两个地方:
将1改为大于1的任何整数
将Testpoc改为%00Test%00poc
传入修改后的poc,即可看到:
写文件操作执行成功。
0x05 拓展思路
1. 抛砖引玉——魔法函数可能造成的威胁
刚刚想到这一点的时候准备好好研究一下,没想到p师傅第二天小密圈就放出来这个话题了。接下来顺着这个思路,我们向下深挖一下。
__toString()
经过上面的总结,我们不难看出,PHP中反序列化导致的漏洞中,除了利用PHP本身的漏洞以外,我们通常会寻找__destruct、__wakeup、__toString等方法,看看这些方法中是否有可利用的代码。
而由于惯性思维,__toString常常被漏洞挖掘者忽略。其实,当反序列化后的对象被输出在模板中的时候(转换成字符串的时候),就可以触发相应的漏洞。
__toString触发条件:
echo ($obj) / print($obj) 打印时会触发
字符串连接时
格式化字符串时
与字符串进行==比较时(PHP进行==比较的时候会转换参数类型)
格式化SQL语句,绑定参数时
数组中有字符串时
我们来写一个demo看一下
toString_demo.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
|
执行结果为下:
通过上面的测试,可以总结以下几点:
echo ($obj) / print($obj) 打印时会触发
__wakeup的优先级>__toString>__destruct
每执行完一个魔法函数,
接下来从两个方面继续来深入:
字符串操作
魔术函数的优先级可能造成的变量覆盖
字符串操作
字符串拼接:
在字符串与反序列化后的对象与字符串进行字符串拼接时,会触发__toString方法。
字符串函数:
经过测试,当反序列化后的最想在经过php字符串函数时,都会执行__toString方法,从这一点我们就可以看出,__toString所可能造成的安全隐患。
下面举几个常见的函数作为例子(所使用的类还是上面给出的toString_demo类):
数组操作
将反序列化后的对象加入到数组中,并不会触发__toString方法:
但是在in_array()方法中,在数组中有__toString返回的字符串的时候__toString会被调用:
class_exists
从in_array()方法中,我们又有了拓展性的想法。我们都知道,在php底层,类似于in_array()这类函数,都属于先执行,之后返回判断结果。那么顺着这个想法,我想到了去年的IPS Community Suite <= 4.1.12.3 Autoloaded PHP远程代码执行漏洞,这个漏洞中有一个非常有意思的触发点,就是通过class_exists造成相关类的调用,从而触发漏洞。
通过测试,我们发现了,如果将反序列化后的对象带入class_exists()方法中,同样会造成__toString的执行:
2. 猜想——对象处理过程可能出现的威胁
通过class_exists可能触发的危险操作,继续向下想一下,是否在对象处理过程中也有可能存在漏洞呢?
还记的去年爆出了一个PHP GC算法和反序列化机制释放后重用漏洞,是垃圾回收机制本身所出现的问题,在释放与重用的过程中存在的问题。
顺着这个思路,大家可以继续在对象创建、对象执行、对象销毁方面进行深入的研究。
0x06 PHPggc
在0x04的第二节中,我们提到了cms在引用某些依赖库时,可能存在(反)序列化漏洞。那么是否有工具可以生成这些通用型漏洞的测试向量呢?
当然是存在的。在github上我们找到了PHPggc这个工具,它可以快速的生成主流框架的序列化测试向量。
关于该测试框架的一点简单的分析
1. 目录结构
目录结构为下:
1 2 3 4 5 |
|
2. 框架运行流程
首先,入口文件为phpggc,直接跟进lib/PHPGGC.php框架核心文件。
在__construct中完成了当前文件完整路径的获取,以及定义自动加载函数,以实现对于下面的类的实例化操作。
关键的操作为:
1 |
|
可以跟进代码看一看,其完成了对于所有payload的加载及保存,将所有的payload进行实例化,并保存在一个全局数组中,以方便调用。
可以动态跟进,看一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
跟进include_gadget_chains方法中看一下:
1 2 3 4 5 6 7 8 9 |
|
在这边首先获取到当前路径,之后从根目录将其下子目录中的所有chain.php遍历一下,将其路劲存储到$files数组中。接着将数组中的所有chain.php包含一遍,保证之后的调用。
回到get_gadget_chains接着向下看,将返回所有已定义类的名字所组成的数组,将其定义为$classes,接着将是PHPGGC\GadgetChain子类的类,全部筛选出来(也就是将所有的payload筛选出来),并将其实例化,在其完成格式化后,返回一个由其名与实例化后的类所组成的键值数组。
到此,完成了最基本框架加载与类的实例化准备。
跟着运行流程,看到generate方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
代码很简单,一步一步跟着看,首先parse_cmdline完成了对于所选模块及附加参数的解析。
接下来array_shift完成的操作就是将我们所选的模块从数组中抛出来。
举个例子,比如我们输入如下:
1 |
|
当前的$class为monolog/rce1,看到接下来进入了get_gadget_chain方法中,带着我们参数跟进去看。
1 2 3 4 5 6 7 8 9 |
|
现在的$full为gadgetchain/monolog/rce1,ok,看一下我们全局存储的具有payload的数组:
可以很清楚的看到,返回了一个已经实例化完成的GadgetChain\Monolog\RCE1的类。对应的目录则为/gadgetchains/Monolog/RCE/1/chain.php
继续向下,看到将类与参数传入了get_type_parameters,跟进:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
其完成的操作对你想要执行或者写入的代码进行装配,即code标志位与你输入的RCE代码进行键值匹配。若未填写代码,则返回错误,成功则返回相应的数组以便进行payload的序列化。
看完了这个模块后,再看我们最后的一个模块:将RCE代码进行序列化,完成payload的生成:
1 2 3 4 5 6 7 8 9 10 11 |
|
▼ 点击阅读原文,查看更多精彩文章。