ThinkPHP 框架SQL注入技术分析

2021-12-17 12:52:44 浏览数 (1)

简要描述

ThinkPHP是一个免费开源的,快速、简单的面向对象的轻量级PHP开发框架,是为了敏捷WEB应用开发和简化企 业应用开发而诞生的。ThinkPHP从诞生的12年间一直秉承简洁实用的设计原则,在保持出色的性能和至简的代码 的同时,也注重易用性。目前ThinkPHP框架是国内使用量最大的框架之一,国内用户量众多。近日,360企业安全 集团代码卫士团队安全研究人员发现该框架V5.1.7-V5.1.8 版本在底层数据处理驱动解析数据的时候存在缺陷,一 定场景下,攻击者可以通过构造恶意数据包利用SQL注入的方式获取用户数据库内容。360企业安全集团代码卫士 团队已第一时间和ThinkPHP团队进行沟通修复,建议相关用户及时更新官方发布的新版本。

漏洞分析:

注:改漏洞ThinkPHP官方团队在报送当天(2018-04-06)紧急进行了修复处理,详细请参考:

代码语言:javascript复制
https://github.com/top-think/framework/commit/39bb0fe6d50ee77e0779f646b10bce08c442a5e3

以下漏洞分析基于ThinkPHP V5.1.8(2018-04-05未更新版)

这里我们主要跟进分析执行update操作的过程。为了方便理解,先直接放出函数的调用栈。

代码语言:javascript复制
Mysql.php:200, thinkdbbuilderMysql->parseArrayData()

Builder.php:147, thinkdbBuilder->parseData()

Builder.php:1139, thinkdbBuilder->update()

Connection.php:1149, thinkdbConnection->update()

Query.php:2571, thinkdbQuery->update()

Index.php:18, appindexcontrollerIndex->testsql()

Container.php:285, ReflectionMethod->invokeArgs()

Container.php:285, thinkContainer->invokeReflectMethod()

Module.php:139, thinkroutedispatchModule->run()

Url.php:31, thinkroutedispatchUrl->run()

App.php:378, thinkApp->think{closure}()

Middleware.php:119, call_user_func_array:

{C:wamp64wwwthink518thinkphplibrarythinkMiddleware.php:119}()

Middleware.php:119, thinkMiddleware->think{closure}()

Middleware.php:74, call_user_func:

{C:wamp64wwwthink518thinkphplibrarythinkMiddleware.php:74}()

Middleware.php:74, thinkMiddleware->dispatch()

App.php:399, thinkApp->run()

index.php:21, {main}()

缺陷关键点为thinkphp解析用户传递过来的Data可控,且可以绕过安全检查。

根据文件 Connection.php:1149, thinkdbConnection->update() 第1102行update函数分析,这个函数的主要功 能是用于执行update SQL语句。

代码语言:javascript复制
 //Connection.php:1149, thinkdbConnection->update()

public function update(Query $query)

    {

        $options = $query->getOptions();

        if (isset($options['cache']) && is_string($options['cache']['key'])) {

            $key = $options['cache']['key'];

        }

        $pk   = $query->getPk($options);

        $data = $options['data'];

if (empty($options['where'])) {

// 如果存在主键数据 则自动作为更新条件

if (is_string($pk) && isset($data[$pk])) {

                $where[$pk] = [$pk, '=', $data[$pk]];

                if (!isset($key)) {

                    $key = $this->getCacheKey($query, $data[$pk]);

                }

unset($data[$pk]);

} elseif (is_array($pk)) { // 增加复合主键支持

                foreach ($pk as $field) {

                    if (isset($data[$field])) {

                        $where[$field] = [$field, '=', $data[$field]];

                    } else {

// 如果缺少复合主键数据则不执行

                        throw new Exception('miss complex primary data');

                    }

                    unset($data[$field]);

                }

}

if (!isset($where)) {

// 如果没有任何更新条件则不执行

throw new Exception('miss update condition');

            } else {

                $options['where']['AND'] = $where;

                $query->setOption('where', ['AND' => $where]);

            }

        } elseif (!isset($key) && is_string($pk) && isset($options['where']['AND'][$pk])) {

            $key = $this->getCacheKey($query, $options['where']['AND'][$pk]);

        }

// 更新数据 $query->setOption('data', $data);

// 生成UPDATE SQL语句

$sql = $this->builder->update($query); $bind = $query->getBind();

if (!empty($options['fetch_sql'])) { // 获取实际执行的SQL语句

            return $this->getRealSql($sql, $bind);

    }

// 检测缓存

$cache = Container::get('cache');

if (isset($key) && $cache->get($key)) { // 删除缓存

        $cache->rm($key);

    } elseif (!empty($options['cache']['tag'])) {

        $cache->clear($options['cache']['tag']);

    }

// 执行操作

$result = '' == $sql ? 0 : $this->execute($sql, $bind);

    if ($result) {

        if (is_string($pk) && isset($where[$pk])) {

            $data[$pk] = $where[$pk];

        } elseif (is_string($pk) && isset($key) && strpos($key, '|')) {

            list($a, $val) = explode('|', $key);

            $data[$pk]     = $val;

        }

        $query->setOption('data', $data);

        $query->trigger('after_update');

    }

return result; 刚刚我们将用户可控的 data set到 query['options'] 中,这里我们先获取 query'options' 内容到 options 中,然后对Data进行解析 data = this->parseData(

第1146行, query->setOption(‘data’, $``$data set到 query 变量中,为下一步的生 成 UPDATE SQL语句做准备,执行 sql = this->builder->update(

代码语言:javascript复制
Builder.php:1139, thinkdbBuilder->update() 函数

//Builder.php:1139, thinkdbBuilder->update()

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;

        }

        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);

刚刚我们将用户可控的 data set到 query'options' 中,这里我们先获取 query['options'] 内容到 options 中,然后对Data进行解析 data = this->parseData(query, options'data');

代码语言:javascript复制
//Builder.php:147, thinkdbBuilder->parseData()

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;

    }

  //Mysql.php:200, thinkdbbuilderMysql->parseArrayData()

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';

//在第115行,通过 foreach ($data as $key => $val) 处理 $data ,然后解析 $key 保存到 $item 变量中去,之后 执行下面的判断逻辑,想要合理地进入各个判断分支,就要巧妙的构造 $key 和 $value 也就是 $data 的值。紧接 着我们进入漏洞触发点 $value = $this->parseArrayData($query, $val); ,跟进函数 $value = $this- >parseArrayData($query, $val);

//Mysql.php:200, thinkdbbuilderMysql->parseArrayData()    

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 . ')')';//需要简单的构造一下sql语

    break;

default:

}

这里 type 、 value 和 data 均为可控值,那么函数返回的 result 也就是可控的。回到上一个 Builder.php 文件中,将返回的结果赋值到 result[ item] = $value; 中,之后的生成SQL语句和常见的流程没有任何差别不在 展开具体分析。

版权属于:逍遥子大表哥

本文链接:https://cloud.tencent.com/developer/article/1920491

按照知识共享署名-非商业性使用 4.0 国际协议进行许可,转载引用文章应遵循相同协议。

0 人点赞