ThinkPHP5 最新一处注入分析

0x01 前言

听说thinkphp又出事了,之前看过一次tp5的源码,不过只看了查询(select)的过程,这次问题出在update和insert中,但是归根结底还是fileExp()这个函数出了问题,分析过程有部分重复,建议配合前文食用:)

0x02 准备工作

根据payload

name[0]=dec&name[1]=1 and (extractvalue(1,concat(1,(user()))))#&name[2]=1

dec是thinkphp实现的一个sql的表达式,和之前的not like性质类似。

dec这个表达式是在5.0.13新实现的,该漏洞在5.0.16被修复,所以影响范围也在这之间。

测试代码:

applicationindexcontrollerIndex.php

<?php
namespace app\index\controller;

class Index
{
    public function index()
    {
        $username = input('post.name/a');
        db('user')->where(['id'=> 1])->update(['username'=>$username]);
    }
}

0x03 分析

3.1 input()

这个函数可以看之前的文章Thinkphp5实现安全数据库操作以及部分运行流程分析的3.1部分,流程是相同的。

3.2 update()

直接定位到这个漏洞关键点,thinkphp/library/think/db/Builder.php的update函数。

具体定位过程可以看之前文章的3.2部分。

/**
     * 生成update SQL
     * @access public
     * @param array     $data 数据
     * @param array     $options 表达式
     * @return string
     */
    public function update($data, $options)
    {
        $table = $this->parseTable($options['table'], $options);
        $data  = $this->parseData($data, $options);
        if (empty($data)) {
            return '';
        }
        foreach ($data as $key => $val) {
            $set[] = $key . '=' . $val;
        }

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

        return $sql;
    }

问题出在更新数据处,跟进parseData函数。

3.2.1 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)) {
                switch ($val[0]) {
                    case 'exp':
                        $result[$item] = $val[1];
                        break;
                    case 'inc':
                        $result[$item] = $this->parseKey($val[1]) . '+' . $val[2];
                        break;
                    case 'dec':
                        $result[$item] = $this->parseKey($val[1]) . '-' . $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;
    }

可以看到当$val是数组时,会将其第一个元素取出来作为表达式。这里有exp、des、inc三个表达式,但是exp已经被过滤了(filterExp里的第一个。。。)。第二个元素进入了parseKey函数,第三个元素没有任何操作直接拼接。跟进parseKey()看一下:

3.2.2 parseKey()
/**
     * 字段名分析
     * @access protected
     * @param string $key
     * @param array  $options
     * @return string
     */
    protected function parseKey($key, $options = [])
    {
        return $key;
    }

emmmm,不要慌。thinkphp/library/think/db/Builder.php只是一个抽象类,具体实现在这里

thinkphp/library/think/db/builder/Mysql.php

/**
     * 字段和表名处理
     * @access protected
     * @param string $key
     * @param array  $options
     * @return string
     */
    protected function parseKey($key, $options = [])
    {
        $key = trim($key);
        if (strpos($key, '$.') && false === strpos($key, '(')) {
            // JSON字段支持
            list($field, $name) = explode('$.', $key);
            $key                = 'json_extract(' . $field . ', \'$.' . $name . '\')';
        } elseif (strpos($key, '.') && !preg_match('/[,\'\"\(\)`\s]/', $key)) {
            list($table, $key) = explode('.', $key, 2);
            if ('__TABLE__' == $table) {
                $table = $this->query->getTable();
            }
            if (isset($options['alias'][$table])) {
                $table = $options['alias'][$table];
            }
        }
        if (!preg_match('/[,\'\"\*\(\)`.\s]/', $key)) {
            $key = '`' . $key . '`';
        }
        if (isset($table)) {
            if (strpos($table, '.')) {
                $table = str_replace('.', '`.`', $table);
            }
            $key = '`' . $table . '`.' . $key;
        }
        return $key;
    }

并没有什么过滤,不过在某些情况下会用反引号把字段的值包起来,可能会影响payload的构造。

经过这么一系列操作构造出来的sql语句像下面这样:

update `user` set username=1 #name[1] 可控
                         - #name[0] dec => -
                         1 #name[2] 可控

有两处可控,约等于为所欲为。

mark

3.3 insert()

thinkphp/library/think/db/Builder.php构造insert语句的时候同样使用了parseData()函数,也存在问题。

/**
     * 生成insert SQL
     * @access public
     * @param array     $data 数据
     * @param array     $options 表达式
     * @param bool      $replace 是否replace
     * @return string
     */
    public function insert(array $data, $options = [], $replace = false)
    {
        // 分析并处理数据
        $data = $this->parseData($data, $options);
    }

0x04 防御

thinkphp在5.0.16版本修复了这个漏洞,并没有在filterExp函数中增加过滤,补丁如下:

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

加了一个判断,保证$val[1]为对应的字段名,并且把$val[2]进行了强制类型转换。

最后说一句,漏洞的利用是建立在thinkphp接受数组形式的参数的基础上,虽然这种写法在查询中非常少见,但是在更新、插入的时候还是有相应的需求的。总而言之,一切用户输入都是有害的,还是那句话,框架只是简化开发的一种工具,并不能把应用安全全部交给框架来处理。

标签: thinkphp
添加新评论