wywwzjj's Blog

ThinkPHP 3.X / 5.X order by 注入

字数统计: 1.1k阅读时长: 5 min
2020/01/30 Share

概述

在 ThinkPHP 5.1.23 之前的版本中存在 SQL 注入漏洞,该漏洞是由于程序在处理 order by 后的参数时,未正确过滤处理数组的 key 值所造成。如果该参数用户可控,且当传递的数据为数组时,会导致漏洞的产生。

ThinkPHP 5.1 中的更新日志也可看到:V5.1.23(2018-8-23)改进order方法的数组方式解析,增强安全性。

同时受到影响的还有 3.2.3 及以下的版本。

环境搭建

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

ThinkPHP 3.2.3 版本环境建议按官方文档操作,直接下载: https://www.kancloud.cn/manual/thinkphp/1680

配好数据库后,在 index.php 中加入测试代码。

class Index {
public function index() {
$order = input('order');
$res = db('user')->order($order)->find();
dump($res);
}
}

user 是随便创建的表,看到该页面说明环境没问题。

image.png

复现

image.png

分析

TP 5.1.x

我们先来看一下正常的 SQL 查询流程。

ThinkPHP 提供了大量封装数据库操作的函数给开发者使用,但终究是要落实到生成 SQL 语句的。

Builder.php 中可看到这些实现细节,以 select 查询为例,TP 弄了一个查询模板,每次查询时替换成具体的值。

protected $selectSql = 'SELECT%DISTINCT% %FIELD% FROM %TABLE%%FORCE%%JOIN%%WHERE%%GROUP%%HAVING%%UNION%%ORDER%%LIMIT% %LOCK%%COMMENT%';

select 语句替换操作,在这里生成 SQL 语句。既然 parseOrder() 有注入,其他的同样可能出现问题。

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);
}

最终在 Connection.php 中用 PDO 执行。

$this->PDOStatement = $this->linkID->prepare($sql);
$this->PDOStatement->execute();
return $this->getResult($pdo, $procedure);

回到本文的重点,order by 处理,可看到 order by$array 拼接一下就返回了。

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)) {
// 有些分析是从这进去的,没这个必要,反而使 payload 复杂化
$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);
}

如果 $val 非 ASC、DESC,将被直接清空。继续跟进 parseKey(),这个方法会随使用的数据库驱动类型变化。

// Mysql.php
public function parseKey(Query $query, $key, $strict = false) {
if (is_numeric($key)) {
return $key;
} elseif ($key instanceof Expression) {
return $key->getValue();
}

$key = trim($key);

// ... 处理 json 字段和 table_name.filed 这种形式

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

// ...

return $key;
}

简单来说,经过这一步操作,由于$strict = true,$key 将被包一层反引号。

现在的问题就变成了:

select xxx from xxx order by `$key` limit 1;  -- limit 1 是自动拼接上的

联合注入时,经常使用 order by 4 来判断字段数,当 4 被反引号包裹时还能起到同样的效果吗?没了。

也就是说至少得知道一个字段名,否则 order by 这里就会报错了。不能堆叠注入,待继续突破!

看下 V5.1.23 的补丁,order by 后的 ) 没了,还能继续绕吗?

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

TP 3.2.3

这个版本就更简单了,相比 5 系列,连反引号都没有了。

// Driver.class.php
protected function parseOrder($order) {
if (is_array($order)) {
$array = array();
foreach ($order as $key => $val) {
if (is_numeric($key)) {
$array[] = $this->parseKey($val);
} else {
$array[] = $this->parseKey($key) . ' ' . $val;
}
}
$order = implode(',', $array);
}
return !empty($order) ? ' ORDER BY ' . $order : '';
}

组成的 SQL 语句是这样的:

select xxx from xxx order by $order limit 1;  -- limit 1 是自动拼接上的

拿出 order by 的常规套路即可,有报错就报错注入,没报错就盲注。

总结

TP 3 这个注入还是挺实用的,TP 5 还需要继续研究下,如果不能获取到列名,很难利用。

参考

https://mp.weixin.qq.com/s/jDvOif0OByWkUNLv0CAs7w

CATALOG
  1. 1. 概述
  2. 2. 环境搭建
  3. 3. 复现
  4. 4. 分析
    1. 4.1. TP 5.1.x
    2. 4.2. TP 3.2.3
  5. 5. 总结
  6. 6. 参考