freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

ThinkPHP 框架SQL注入技术分析
360企业安全 2018-04-19 15:00:54 546868

4月12号,ThinkPHP官方团队发布“ThinkPHP5.0.17&5.1.9版本发布——包含安全更新”通知,提醒用户第一时间更 新框架版本,在这次更新中,包含了由360企业安全集团代码卫士团队报送的一个高危安全漏洞。本文针对该漏洞 的技术细节进行分析。

简要描述

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

漏洞分析:

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

https://github.com/top-think/framework/commit/39bb0fe6d50ee77e0779f646b10bce08c442a5e3

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

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

Mysql.php:200, think\db\builder\Mysql->parseArrayData()

Builder.php:147, think\db\Builder->parseData()

Builder.php:1139, think\db\Builder->update()

Connection.php:1149, think\db\Connection->update()

Query.php:2571, think\db\Query->update()

Index.php:18, app\index\controller\Index->testsql()

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

Container.php:285, think\Container->invokeReflectMethod()

Module.php:139, think\route\dispatch\Module->run()

Url.php:31, think\route\dispatch\Url->run()

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

Middleware.php:119, call_user_func_array:

{C:\wamp64\www\think518\thinkphp\library\think\Middleware.php:119}()

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

Middleware.php:74, call_user_func:

{C:\wamp64\www\think518\thinkphp\library\think\Middleware.php:74}()

Middleware.php:74, think\Middleware->dispatch()

App.php:399, think\App->run()

index.php:21, {main}()

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

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

 //Connection.php:1149, think\db\Connection->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($query, $options['data']);

第1146行, $query->setOption('data', $data); 这里将用户传递的 $data set到 $query 变量中,为下一步的生 成 UPDATE SQL语句做准备,执行 $sql = $this->builder->update($query); 语句,重点马上要来了,跟进

Builder.php:1139, think\db\Builder->update() 函数

//Builder.php:1139, think\db\Builder->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']);        

//Builder.php:147, think\db\Builder->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, think\db\builder\Mysql->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, think\db\builder\Mysql->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语句和常见的流程没有任何差别不在 展开具体分析。

验证截图

屏幕快照 2018-04-18 下午6.24.47.png

修复建议

更新受影响ThinkPHP版本到5.1.8版本以上

关于我们

360代码安全实验室是360企业安全集团旗下专门从事源代码、二进制漏洞挖掘和分析的研究团队,主要研究方向 包括:Windows/Linux/MacOS操作系统、应用软件、开源软件、网络设备、IoT设备等。团队成员既有二进制漏洞 挖掘高手,微软全球TOP100贡献白帽子,Pwn2own2017冠军队员,又有开源软件安全大拿,人工智能安全专 家。实验室安全团队的研究成果获得微软、Adobe、各种开源组织等的50多次致谢。

参考

ThinkPHP5.0.17&5.1.9版本发布——包含安全更新

*本文作者:360企业安全,转载请注明来自FreeBuf.COM

# SQL注入 # ThinkPHP
免责声明
1.一般免责声明:本文所提供的技术信息仅供参考,不构成任何专业建议。读者应根据自身情况谨慎使用且应遵守《中华人民共和国网络安全法》,作者及发布平台不对因使用本文信息而导致的任何直接或间接责任或损失负责。
2. 适用性声明:文中技术内容可能不适用于所有情况或系统,在实际应用前请充分测试和评估。若因使用不当造成的任何问题,相关方不承担责任。
3. 更新声明:技术发展迅速,文章内容可能存在滞后性。读者需自行判断信息的时效性,因依据过时内容产生的后果,作者及发布平台不承担责任。
本文为 360企业安全 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
360企业安全 LV.4
360企业安全集团官方帐号
  • 12 文章数
  • 1 关注者
委员交作业 | 政协委员周鸿祎的3个网络安全提案
2019-03-04
B轮融资9亿元, 360企业安全集团融资能力出众
2019-01-07
360企业安全上海周末沙龙登陆魔都
2018-07-19
文章目录