Thinkphp 5.0.x SQL注入-select方法
漏洞概述
官方不认为是漏洞,因在特定模式 exp
下执行 SQL 语句,未经任何过滤处理导致注入
影响版本
ThinkPHP5 全版本
需要开启 Debug 页面
环境搭建
拉取项目
1 | composer create-project --prefer-dist topthink/think=5.0.10 thinkphp_5.0.10 |
修改 composer.json
文件的 require
字段设置成如下,然后再执行 composer update
1 | "require": { |
修改 application/index/controller/Index.php
文件内容
1 |
|
创建 users
表
1 | create database tpdemo; |
根据相关内容修改 database.php
文件
在 config.php
文件中开启 app_debug
和 app_app_trace
漏洞复现
1 | http://127.0.0.1/thinkphp/public/index.php/index/index/index?username=)%20union%20select%20updatexml(1,concat(0x7e,user(),0x7e),1)%23 |
漏洞分析
打断点开始调试,首先进入 get()
方法,在 thinkphp/library/think/Request.php
中
get()
方法中对传入的 $name
进行判断,如果是数组则在第 673 行处合并后输出,进入 input()
方法
input()
方法对参数 $name
分为 /
和 .
两种情况进行处理
当判断参数 $data
不是数组时,进入 filterValue()
方法
继续跟进,进入 filterExp()
方法
使用正则过滤查询关键字,然后在参数 $value
后加上空格,最后再返回数据回到 application/index/controller/Index.php
文件中
先进入 where()
方法,在 thinkphp/library/think/db/Query.php
中
where 部分经过 parseWhereExp()
方法处理后返回,继续调用 select()
方法
在 select()
方法中,重点在第 2306 行处生成 SQL 查询语句, $options
参数的生成在第 2286 行,通过 parseExpress()
函数分析表达式获取到表名信息、where 查询后的内容
继续跟进 ,调用 BUilder
类中 select()
方法,文件在 thinkphp/library/think/db/Builder.php
在 select()
方法中,程序会对 SQL 语句模板用变量填充,其中用来填充 %WHERE%
的变量中存在用户输入的数据,跟进 parseWhere()
方法
使用 buildWhere()
方法生成查询条件 SQL 语句,继续跟进该方法
跟进 parseWhereItem()
方法查看 where子单元分析
当操作符为 exp
时,会将来自用户的数据直接拼接进为 SQL 语句,导致产生 SQL注入漏洞,所以此时的语句为
1 | ( `username` ) union select updatexml(1,concat(0x7e,user(),0x7e),1)# ) |
回到 Builder
类中的 select()
方法,组合成完整的 SQL 语句
1 | SELECT * FROM `users` WHERE ( `username` ) union select updatexml(1,concat(0x7e,user(),0x7e),1)# ) |
最后返回到 Query
类中 select()
方法调用 query()
执行查询
Thinkphp 5.0.10 SQL注入-select方法
漏洞概述
漏洞存在于 Mysql 类的 parseWhereItem 方法中,由于程序没有对数据进行很好的过滤,直接将数据拼接进 SQL 语句。Request 类的 filterExp 方法漏过滤 NOT LIKE 关键字,最终导致 SQL注入漏洞 的产生。该注入影响小于 5.0.10 版本,高版本已经将 NOT LIKE
添加到 filterExp()
方法中进行过滤。
影响版本
- ThinkPHP <= 5.0.10
环境搭建
拉取项目
1 | composer create-project --prefer-dist topthink/think=5.0.10 thinkphp_5.0.10 |
修改 composer.json
文件的 require
字段设置成如下,然后再执行 composer update
1 | "require": { |
修改 application/index/controller/Index.php
文件内容
1 |
|
创建 users
表
1 | create database tpdemo; |
根据相关内容修改 database.php
文件
漏洞复现
1 | http://127.0.0.1/thinkphp/public/index.php/index/index/index?username[0]=not%20like&username[1][0]=%%&username[1][1]=aa&username[2]=)%20union%20select%201,user()%23 |
漏洞分析
打断点开始调试,先进入到 thinkphp/library/think/Request.php
中的 get()
方法中
判断 $name
参数类型,继续进入 input()
方法中
在 input()
方法中,因为这次是数组数据,判断 $data
类型后进入 if
语句中
使用 array_walk_recursive()
函数对 $data
中内容进行过滤,进入 filterValue()
方法
继续进入 filterExp()
中,查看有哪些过滤字符
使用正则过滤,有对 NOTLIKE
进行过滤,但是忽略了 NOT LIKE
,回到 application/index/controller/Index.php
文件
先调用 Query
类中的 where()
方法,然后通过 parseWhereExp()
分析查询表达式
最后返回 index.php
并继续调用 select()
方法构建 SQL 语句
继续跟进 ,调用 BUilder
类中 select()
方法,文件在 thinkphp/library/think/db/Builder.php
在 select()
方法中,程序会对 SQL 语句模板用变量填充,其中用来填充 %WHERE%
的变量存在用户输入的数据,跟进 parseWhere()
方法
数据传入 buildWhere()
方法,继续跟进,生成查询表达式 SQL 语句
跟进 parseWhereItem()
方法,对 where子单元分析
第 305 行使用 list()
获取 $val
中的值,分别赋值给 $exp = $val[0] = NOT LIKE
和 $value = $val[1] = array('%%', 'aa')
,当程序走到第 357 行判断 $exp
的值是否为 NOT LIKE
。由于在 Request
类中的 filterExp()
方法忽略掉对 NOT LIKE
的过滤,导致用户输入的内容可控,进入该 elseif()
分支中
$value
为数组,进入 if
分支,在第 363 行判断是否存在 $val[2]
,如果存在则赋值给 $logic
,即
1 | $logic = ") union select 1,user()#" |
然后在 364 行处使用 implode()
将 $array
和 $logic
组合为字符串返回给 $whereStr
1 | $whereStr = "(`username` NOT LIKE '%%' ) UNION SELECT 1,USER()# `username` NOT LIKE 'aa')" |
最终 Builder
类中 select()
方法生成的完整查询 SQL 语句为
1 | $sql = "SELECT * FROM `users` WHERE (`username` NOT LIKE '%%' ) UNION SELECT 1,USER()# `username` NOT LIKE 'aa') " |
最后返回到 Query
类中 select()
方法调用 query()
执行查询
补充说明
拉取 ThinkPHP 5.0.9
版本,复现该漏洞,漏洞仍存在
在 thinkphp/library/think/db/Builder.php#324
处使用 in_array()
函数判断 exp
数组中是否存在 not like
实际上$exp
是检查数组的值而不是键名
所以小于 5.0.10 版本同样存在漏洞
Thinkphp 5.x SQL注入-insert方法
漏洞概述
漏洞存在于 Builder 类的 parseData 方法中。由于程序没有对数据进行很好的过滤,将数据拼接进 SQL 语句,导致 SQL注入漏洞 的产生。
影响版本
- 5.0.13 <= ThinkPHP <= 5.0.15
- 5.1.0 <= ThinkPHP <= 5.1.5
- 需要开启 Debug 页面
环境搭建
拉取项目
1 | composer create-project --prefer-dist topthink/think=5.0.15 thinkphp_5.0.15 |
修改 composer.json
文件的 require
字段设置成如下,然后再执行 composer update
1 | "require": { |
修改 application/index/controller/Index.php
文件内容
1 |
|
创建 users
表
1 | create database tpdemo; |
根据相关内容修改 database.php
文件,在 config.php
文件中开启 app_debug
和 app_app_trace
漏洞复现
1 | http://127.0.0.1/thinkphp/public/index.php/index/index/index?username[0]=inc&username[1]=updatexml(1,concat(0x7e,user(),0x7e),1)&username[2]=1 |
漏洞分析
打断点开始调试
第 18 行通过 request()
助手函数的 get()
方法获取 username
数据,直接跟进第 19 行处 insert()
方法,文件位于 thinkphp/library/think/db/Query.php
第 2082 行中 parseExpress()
方法获取配置信息,在 2085 行调用 $this->builder->insert()
方法生成 $sql
语句,继续跟进 insert()
方法,文件位于 thinkphp/library/think/db/Builder.php
在第 721 行调用 parseDate()
方法处理传入的数据,跟进查看
第 101 行遍历传入的 $data
数据,将单元的值赋值给 $val
数组,跟进到第 114 行处通过 switch
判断 $val[0]
参数,在 payload 输入为 inc
,进入 inc
分支中,,跟进 parseKey()
方法
在 parseKey()
方法中,不会对传入的内容进行任何过滤,只是用来解析处理数据,传入的 $val[1]
不受影响,回到 parseDate()
中
数据拼接完后回到 insert()
方法中,在 728-736 行中替换字符串将 $data
中的数据填充到 SQL 语句中,最后在执行时出发报错注入
1 | $sql = " INSERT INTO `users` (`username`) VALUES (updatexml(1,concat(0x7e,user(),0x7e),1)+1) " |
在分析过程中,parseDate()
方法中一共存在 3 种 case 语句
理论上也可以使用 exp
和 dec
来构造 SQL 语句,但是在实际测试中发现,exp
会在 thinkphp/library/think/Request.php#filterValue
中被处理,后面增加空格,那么到 parseDate()
中不符合 case 内容,无法注入
但 dec
不受影响,所以也可以造成注入漏洞
1 | http://tp.cn/index.php/index/index/index?username[0]=dec&username[1]=updatexml(1,concat(0x7e,user(),0x7e),1)&username[2]=1 |
Thinkphp 5.1.x SQL注入-update方法
漏洞概述
漏洞存在于 Mysql 类的 parseArrayData 方法中由于程序没有对数据进行很好的过滤,将数据拼接进 SQL 语句,导致 SQL注入漏洞 的产生
影响版本
- 5.1.6 <= ThinkPHP <= 5.1.7
- 部分 5.1.8 存在
- 需要开启 Debug 页面
环境搭建
拉取项目
1 | composer create-project --prefer-dist topthink/think thinkphp_5.1.7 |
修改 composer.json
文件的 require
字段设置成如下,然后再执行 composer update
1 | "require": { |
修改 application/index/controller/Index.php
文件内容
1 |
|
创建 users
表
1 | create database tpdemo; |
根据相关内容修改 config/database.php
文件,在 config/app.php
文件中开启 app_debug
和 app_app_trace
漏洞复现
1 | http://127.0.0.1/thinkphp/public/index.php/index/index/index?username[0]=point&username[1]=1&username[2]=updatexml(1,concat(0x7e,user(),0x7e),1)^&username[3]=0 |
漏洞分析
打断点开始调试
直接进入到 update()
方法中,位于 thinkphp/library/think/db/Query.php
继续跟进进入 update()
方法,位于 thinkphp/library/think/db/Connection.php
中
直接查看 UPDATE SQL
语句生成,在 thinkphp/library/think/db/Builder.php
中 1134 行
进入 parseData()
数据分析方法中
在第 115 行处遍历 $data
赋值 $key
和 $val
参数,到第 128 行处判断 $key
是否存在 ->
,继续走到第 138 行符合判断条件。取 $val[0]
为 point
在 switch
中判断,进入 default
条件里,跟进 parseArrayData()
函数中解析数组数据,位于 thinkphp/library/think/db/builder/Mysql.php
函数中没有任何过滤,直接拼接为sql语句
1 | $result = $fun . '(\'' . $point . '(' . $value . ')\')'; |
参数均可控,实际上语句为
1 | $result = $data[2] . '(\''. $data[3].'('.$data[1].')\')'; |
SQL语句为
1 | UPDATE `users` SET `username` = $data[2]('$data[3]($data[1])') WHERE `id` = :where_AND_id |
填充$data
最后得到完整的 sql语句