概述
在 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 是随便创建的表,看到该页面说明环境没问题。
复现
分析
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)) { $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()
,这个方法会随使用的数据库驱动类型变化。
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 ('*' != $key && ($strict || !preg_match('/[,\'\"\*\(\)`.\s]/', $key))) { $key = '`' . $key . '`'; } return $key; }
|
简单来说,经过这一步操作,由于$strict = true,$key 将被包一层反引号。
现在的问题就变成了:
select xxx from xxx order by `$key` 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 系列,连反引号都没有了。
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;
|
拿出 order by 的常规套路即可,有报错就报错注入,没报错就盲注。
总结
TP 3 这个注入还是挺实用的,TP 5 还需要继续研究下,如果不能获取到列名,很难利用。
参考
https://mp.weixin.qq.com/s/jDvOif0OByWkUNLv0CAs7w