前言
之前审计都是反序列化链比较多,分析SQL注入漏洞的比较少,这里分析总结一下ThinkPHP5的SQL注入漏洞汇总,学习一下利用漏洞通告和commit修复信息进行分析的方法,顺便再熟悉一下TP5的框架。
环境搭建
- 拉tp5.0.15的库
1 | composer create-project --prefer-dist topthink/think=5.0.15 tp5 |
- 修改一下composer.json和composer.lock并
composer update
1 | # composer.json |
- 配置数据库并在config.php中开启app_debug和app_trace
- 创建如下数据库
1 | create database tpdemo; |
insert注入
在application/index/controller/Index.php加入测试页面
1 |
|
poc
1 | index?username[0]=inc&username[1]=updatexml(1,concat(0x7e,database(),0x7e),1)&username[2]=1 |
影响
- 5.0.13 <= TP <= 5.0.15 、 5.1.0 <= TP <= 5.1.5
- 调用
insert()
函数,并传入可控数组参数 - 开启
app_debug
可以报错注入
分析
可拼接在insert语句的field(字段值)中,开启app_debug的话可以报错注入,没开启的话可以尝试盲注(bool、报错、时间)等
update注入
poc
1 | index?username[0]=point&username[1]=1&username[2]=updatexml(1,concat(0x7,user(),0x7e),1)^&username[3]=0 |
影响
分析
select注入(where子句)
在application/index/controller/Index.php加入测试页面
1 |
|
poc
1 | 第一种: |
影响
- TP5全版本
- 调用
where('username','exp',$username)->select()
,where函数的第二个参数为允许的操作符(在Build
类$exp
可查看允许的操作符),并传入可控的参数
分析
有了前两个的分析经验且基本熟悉TP调用SQL语句的方式后,直接把端点打在两个地方,Query类的where
函数和select
函数,根据update注入where函数处理,这里也是一样的,将where部分经过parseWhereExp
处理后,存在$this->options['where']
中
然后便继续调用到select
函数,在继续跟进$this->builder->select($options)
,进入到Builder::select
,中间又是一长串的处理SQL语句各个部分的parseXxx
方法,然后用str_replace
来替换占位符拼接成SQL语句,所以我们跟到最后的sql语句,得到
说明其中存在语句的拼接,由于我们是在where语句的部分进行的注入,因此我们跟到$this->parseWhere
中
然后经过如下调用栈到parseWhereItem()
中
在该函数的处理过程中,当我们的操作符是exp
时,也就是index.php页面的db('users')->where('username','exp',$username)
中where
函数的第二个参数,那么就会进入到如下的拼接语句中
前面的(
我们就用)
去拼接,后面的)
就注释掉即可,最后会得到这样的SQL语句然后返回
1 | ( `username` ) union select updatexml(1,concat(0x7,user(),0x7e),1)# ) |
然后回到parseWhere
函数中,此时$whereStr
就是上面的值,最后和前导WHERE
拼在返回
最终得到的$sql
为
最后调用query
函数完成SQL语句执行
整个SQL注入产生的关键部分是parseWhereItem
函数中当操作符为EXP
时,sql语句未经任何过滤处理倒置的不当拼接,因此产生SQL注入。上述分析均为带POC的逆向分析,如果要正向去寻找SQL注入拼接的漏洞的话,需要关注不同的操作符条件带来的不同的拼接效果,抓住一些没有过滤的,直接字符串拼接的部分,可能就大概率存在漏洞。
修复
未修复。因为开发人员认为此漏洞依赖db('users')->where('username','exp',$username)->select()
这段漏洞代码的exp
操作符,但实际上实际开发情况中是有可能出现这样的写法的。
select注入(where子句)
在application/index/controller/Index.php加入测试页面
1 |
|
然后更新版本为5.0.10
poc
1 | index?username[0]=not like&username[1][0]=%%&username[1][1]=233&username[2]=) union select 1,user()# |
username[0]=not like
:设置操作符为NOT LIKE
,触发进入漏洞分支username[1][0]=%%&username[1][1]=233
:必须传入长度大于等于2的username[1]
数组username[2]
:注入拼接的SQL语句
影响
- 仅影响TP5.0.10
- 无需开启app_debug,因为不是报错注入。
分析
根据官方在安全漏洞公告前的commit,发现一处改进了Request类的filterExp方法,发现其中对于操作符的过滤新增了一个NOT LIKE
,十分可疑
这次漏洞也跟该操作符相关。
我们知道,Request类是用于处理用户在URL中提交的GET/POST参数的,其中有过滤函数filterValue
、filterExp
等,因此无论怎么样,我们提交的参数都会经过其过滤,先跟一下过滤细节,端点直接打在filterExp,可以发现和上一个select注入的处理流程是一样的,递归处理我们传入的数组参数,最后跟进到select
方法中,得到的$this->options['where']
:
和上一个select语句不同的地方就是操作符从exp
变为了not like
,所以我们的分析思路还是一样,看看对于not like操作符,其sql语句是如何处理拼接的,有了之前分析的经验,处理$this->options['where']
的参数传递关系如下
摸清参数传递后,基本上都是这个套路,然后我们直接定位到NOT LIKE
操作符的elseif
分支
这里将$value
数组的每个值进行NOT LIKE
的拼接处理,并存在$array
数组中。然后下面比较重要,当$val[2]
参数不为空时,则会赋值$val[2]
给$logic
,注意下面这段
1 | $test = implode($array, ' ' . strtoupper($logic) . ' '); //添加的测试语句 |
其使用小写后的$logic
去拼接$array
的值,当$array
数组的长度大于等于2时,才会用$logic
进行拼接,否则不会拼接,因此这也就是为什么poc中username[1]
需要一个长度为2的数组,而长度为1的数组是不行的,所以最终得到的$whereStr
的SQL语句值为
1 | (`username` NOT LIKE '%%' ) UNION SELECT 1,USER()# `username` NOT LIKE '233') |
如果长度仅为1的话,只能得到
1 | (`username` NOT LIKE '%%') |
之后就是相同的加上前导WHERE
,最终得到的SQL语句
1 | SELECT * FROM `users` WHERE (`username` NOT LIKE '%%' ) UNION SELECT 1,USER()# `username` NOT LIKE '233') |
修复
- 在5.0.10之前的版本中,未出现该漏洞的原因是在Builder类的
$exp
成员变量中就没有设置not like
为合法的操作符,注意空格
因此就无法过parseWhereItem
函数对于操作符的白名单检测
- 在新版本的修复中,官方在我们前面所说的
filterExp
函数的黑名单过滤中添加了NOT LIKE
(带空格的),匹配到则将$value
替换为空,这样就无法进入到我们想进入的拼接分支了。
order by方法注入
在application/index/controller/Index.php加入测试页面
1 |
|
然后更新版本为
1 | "require": { |
poc
1 | /index?orderby[id`|updatexml(1,concat(0x7e,user(),0x7e),1)%23]=1 |
影响
- 5.1.16 <= TP5 <= 5.1.22
分析
还是一样,Request类的filterValue等方法过滤处理传入的参数,但是实际上对我们传入的orderby
参数的键值没有任何过滤影响。
那么就直接跟到order
方法中,这里的$field
就是我们的传入的orderby参数
然后处理了一下这里的orderby
参数,并同样将其添加到$this->options['orderby']
中
继续分析find
方法,经过一段调用栈,还是来到我们最熟悉的Builder::select
函数中,该函数前面已经见过好几面了,就是将模板SQL语句和处理后的实际SQL语句的各个部分进行一一对应的替换,然后生成最终的SQL语句。这里由于我们漏洞点是处在orderby子句中,所以我们跟进parseOrder
中看一下,经过一系列if判断条件跟进到parseKey
在parseKey中又进入到如下分支,从而给整个$key
也就是我们注入的SQL语句添加了反引号包裹,所以我们的POC中前面要用一个反引号进行闭合,后面用注释。
处理完后直接拼接前导ORDER BY
以及拆分$array
数组并连接,最终处理完后的SQL语句为
1 | SELECT * FROM `users` WHERE `username` = :where_AND_username ORDER BY `id`|updatexml(1,concat(0x7e,user(),0x7e),1)#` LIMIT 1 |
由于|
拼接,在ORDER BY后面形成了报错注入但不局限于报错注入。
修复
其增加了一句判断条件,当ORDER BY后面的语句中出现#
或)
时,则不处理$array
,那么到最后拼接的$array
即为空
MySQL聚合函数注入
在application/index/controller/Index.php加入测试页面
1 |
|
并更新版本
1 | "require": { |
poc
不同版本的poc稍有调整,主要是增加了`
拼接
- 5.0.0~5.0.21 、 5.1.3~5.1.10
1 | index?options=id)+updatexml(1,concat(0x7e,user(),0x7e),1) from users# |
- 5.1.11~5.1.25 :
1 | index?options=id`)+updatexml(1,concat(0x7e,user(),0x7e),1) from users# |
影响
- 5.0.0 <= TP <= 5.0.21 、 5.1.3 <= TP5 <= 5.1.25
- 使用了MySQL的聚合函数(aggregation function),有
AVG
、MAX
、COUNT
、MIN
、SUM
分析
根据修复,我们可以发现其改进了数据库驱动,代码中多了检测特殊字符的片段,我们来具体分析一下。
还是一样的,经过一些列动态类加载(文件包含)和初始化后,直接到了Query::max
方法,然后又调用了aggregate
方法,
然后由继续进入到parseKey
中处理我们的$key
。同样的,在该版本的parseKey
中,会对我们传入$key
进行反引号拼接,
然后返回到Mysql::aggregate
函数,拼接之后的$field
即为聚合函数子句部分的SQL语句,正常情况下已经是类似MAX(字段)
,但由于我们这里注入了SQL语句,拼接语句
1 | $field = $aggregate . '(' . $this->builder->parseKey($query, $field, true) . ') AS tp_' . strtolower($aggregate); |
最后拼接成
1 | MAX(`id`)+updatexml(1,concat(0x7e,user(),0x7e),1) from users#`) AS tp_max |
继续跟进$this->value
方法,然后经过一段调用,又会到我们最最最熟悉的select
方法替换填充模板SQL语句,由于我们在之前并未调用其他SQL子句的函数,因此这部分直接可以跳过了,最后得到的SQL语句为
1 | SELECT MAX(`id`)+updatexml(1,concat(0x7e,user(),0x7e),1) from users#`) AS tp_max FROM `users` LIMIT 1 |
修复
官方加固了parseKey
方法,$strict
默认为true
,当匹配到除了字母和.*
之外的字符时,则抛出异常,因此我们注入的SQL
语句经过修复后的parseKey
处理后会报错。
总结
分析完整个TP5系列的SQL注入漏洞,学到了一些东西。SQL注入漏洞的触发主要原因还是处理不同SQL子句部分的语句过滤不够完全,导致SQL语句的拼接,加上开发人员在某种情况下的一些SQL语句的写法,配合用户可控输入,从而造成SQL注入。通篇分析下来,出现语句拼接的方法都是parseXxx
,因此在挖掘SQL注入的时候,需要关注相关类的解析SQL的方法,构造一些条件来调试跟踪在何种情况下导致进入某些分支从而拼接未过滤完全的用户可控输入
其次,根据github的漏洞补丁信息逆推调试,一样可以写出sql注入的poc。
最重要的还是对整个框架的熟悉程度吧,如果十分熟悉TP开发的话,很快速的就能根据index.php中调用的SQL方法对应到相应的处理语句,如这里的三个SQL处理类Query
、Build
、Mysql
。入口类方法基本都是Query
类,因为db('table')
返回的是Query
类的实例,从而进一步调用,若调用了Query
类的find
、select
等方法,最终都会调用而Build
类的select
方法,该方法中调用了多个parseXxx
方法来处理SQL各部分子句,然后替换SQL语句模板中相应的部分。