一, 背景介绍
ThinkPHP 是一个快速, 简单的基于 MVC 和面向对象的轻量级 PHP 开发框架, 遵循 Apache2 开源协议发布. ThinkPHP 从诞生以来一直秉承简洁实用的设计原则, 在保持出色的性能和至简的代码的同时, 也注重开发体验和易用性, 为 web 应用和 API 开发提供了强有力的支持.
在近期, ThinkPHP 框架被曝出存在 SQL 注入漏洞. 由于 SQL 注入漏洞的危害性以及该框架应用十分广泛. 对此, 天融信阿尔法实验室以静态和动态两种方式对该漏洞进行了深入分析.
1.1 漏洞描述
在 ThinkPHP5.1.23 之前的版本中存在 SQL 注入漏洞, 该漏洞是由于程序在处理 order by 后的参数时, 未正确过滤处理数组的 key 值所造成. 如果该参数用户可控, 且当传递的数据为数组时, 会导致漏洞的产生.
1.2 受影响的系统版本
ThinkPHP <5.1.23
1.3 漏洞编号
CVE-2018-16385
二, 环境搭建
1. 下载安装 thinkphp5.1.x
对于 thinkphp5.1.x 完整版, 目前官方没有直接下载的链接. GitHub 上只是放出核心版. 该版本需要以 Composer 或 Git 方式进行安装.
这里以 Composer 安装方式说明.
在 Linux 和 Mac OS X 中可以运行如下命令:
- curl -sS https://getcomposer.org/installer | PHP
- mv Composer.phar/usr/local/bin/Composer
在 Windows 中, 你需要下载并运行 Composer-Setup.exe .
安装好之后, 切换路径到 Web 目录下运行:
composercreate-project topthink/think=5.1.1 tp5.1 --prefer-dist
然后会生成一个名为 tp5.1 的文件夹. 到此 think5.1.1 下载成功.
2. 然后在浏览器中访问
如果出现该页面, 则证明安装成功.
3.Demo 示例
编写 Demo 文件, 并将文件命名为 Test.PHP, 然后放在 / tp5.1/application/index/controller / 目录下.
4. 数据库
与 Demo 文件匹配, 需要创建一个 user 表, 然后设一个字段 (id).
三, 漏洞细节
在 / ThinkPHP/library/think/db/Builder.PHP parseOrder() 的函数中:
通过 Demo 传入 order 参数内容, 当传入的 $order 是一个数组时, foreach 函数将 $order 数组分为 key 和 value 形式.
根据漏洞修复补丁, 知道漏洞发生在 parseOrderField() 函数中.
当 $val 为数组时, 会进入 parseOrderField() 函数.
跟踪 parseOrderField() 函数
getOptions() 函数是获取了当前要查询的参数, getFieldsBind() 函数是获取数据表绑定信息, foreach 循环是对 $val 值进行了处理, 这里其实不是重点, 就提一下.$val 值是什么不用管, 因为注入点在 $key 上, 而 $val 拼接在 $key 后面, 可以在构造 $key 加 #注释掉 $val.
重点是 parseKey() 函数, 跟踪 parseKey() 函数.
这里对传入的 $key 进行多重判断以及处理.
1. is_numeric 判断, 如果是数字, 则返回, 不是的话继续向下执行.
2. 判断 $key 是否属于 Expression 类.
- strpos($key, '->') && false ===strpos($key, '(') .
- ('*' != $key && ($strict ||!preg_match('/[,\'\"\*\(\)`.\s]/', $key))).
因为 $key 是我们的 sql 注入语句, 所以 1.2.3 肯定不满足, 而 4 满足.
所以此时的 $key 会在左右两侧加个 ` 号.
$table 不存在, 不会对 $key 修改, 所以加 ` 号后会返回 $key, 然后和 $val 以及 field 字符串进行拼接, 再然后 return 赋值给 array 变量, 紧接着, array 与 order by 字符串进行拼接形成 order by 查询语句, 最终系统调用 query() 函数进行数据库查询, 触发漏洞.
着重说明一下, 这里由于 field 函数, 漏洞利用有两个关键点:
首先解释下 field() 函数: MySQL 中的 field() 函数作用是对 SQL 中查询结果集进行指定顺序排序. 一般与 order by 一起使用.
关键点 1:
field() 函数必须指定大于等于两个字段才可以正常运行, 否则就会报错.
当表中只有一个字段时, 我们可以随意指定一个数字或字符串的参数.
关键点 2:
当 field 中的参数不是字符串或数字时, 指定的参数必须是正确的表字段, 否则程序就会报错. 这里由于程序会在第一个字段中加 " 限制 , 所以必须指定正确的字段名称. 第二个字段没有限制, 可以指定字符串或数字.
所以, 我们要利用该漏洞, 第一我们至少需要知道表中的一个字段名称, 第二向 field() 函数中中传入两个字段. 第二个字段不需要知道字段名, 用数字或字符串绕过即可.
Payload 构造
根据以上分析, 构造 payload 需要满足以下条件:
1. 传入的 $order 需要是一个数组.
2.$val 必须也是数组.
3. 至少知道数据库表中的一个字段名称, 并且传入两个参数.
4. 闭合 ` .
最终 Payload 构造如下:
http://127.0.0.1/tp5.1/public/index/test/index?order[id`,111)|updatexml(1,concat(0x3a,user()),1)#][]=1
或
http://127.0.0.1/tp5.1/public/index/test/index?order[id`,'aaa')| updatexml(1,concat(0x3a,user()),1)#][]=1
四, 动态调试分析
有时候单单静态分析, 很难知道某些函数做了些什么, 而对于程序运行过程, 也很难理解透彻. 而利用动态分析, 一步一步 debug, 就很容易理解清楚.
这里我们利用上面构造的 payload 进行 debug.
首先下断点:
$val 是个数组, 进入 parseOrderField() 函数. F7 下一步 (我这里用的是 phpstorm,F7 是单步调试的意思).
foreach 循环后, 可以看到只是处理了 $val, 并没有涉及 $key(我们的关注点在 $key).$val 的值是在 $key 的基础上加了个: data__前缀, 后面加了个 0.
继续 F7, 进入了 parseKey() 函数.
到这里, 看到 $key 满足 if 条件, 然后两边加了个 ` 号.
最后返回的 $key. 其实就是两边加了个 ` 号 .
最后, 返回给 array 变量的值为 file 字符串和 $key 以及 $val 的拼接.
继续 F7, 看看接下来程序怎么走.
进行了 limit 分析, union 分析等多个分析处理, 最终来到了 removeoption() 函数.
可以看到这里的 $sql, 已经可以触发注入漏洞.
然后经过了中间的几个过程, 对比上图, 并没有改变 sql 语句内容. 最后 sql 语句, 进入 query() 函数执行了 SQL 语句, 成功触发注入漏洞.
五, 修复建议
官方补丁
目前官方已经更新补丁, 请受影响的用户尽快升级到 ThinkPHP5.1.24 版本.
手工修复
根据官方给出的方案进行代码修改.
来源: http://www.tuicool.com/articles/BR3YFnj