在 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 是随便创建的表,看到该页面说明环境没问题。
我们先来看一下正常的 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;
}
这个版本就更简单了,相比 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 还需要继续研究下,如果不能获取到列名,很难利用。