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语句