Thinkphp3.2.3 sql注入
前言
最近投了星盟网安团队,面试的时候y2shin大师傅拷打我thinkphp框架是否在本地搭环境调试过,说实话之前一直是看着其他师傅的博客来学习,尽管最后如愿进入了预备队,但觉得还是有必要本地调试一下来提升代码审计能力,于是打算利用国庆假期把thinkphp系列的漏洞都debug完
环境搭建
Github获取:https://github.com/liu21st/thinkphp ,把文件解压在apache的web根目录即可
在浏览器访问此文件,如:http://localhost:8888/thinkphp-master/
出现如下界面即代表成功
环境搭建这里真的踩了好多坑,师傅们的博客都说的不够详细
我们得先访问根目录中的thinkphp文件,整个框架会被初始化,自动生成模块,不然接下来的控制器配置会非常无厘头,我就是在这里墨迹了巨长他妈长的时间,一个劲在子目录找不存在的文件
数据库配置:打开ThinkPHP/Conf/convention.php
1 | /* 数据库设置 */ |
再向我们的mysql写入这个库,接着用Navicat连接
where注入
控制器配置
修改Application/Home/Controller/IndexController.class.php
的index方法
1 | public function index() |
传参得到我们的查询结果:
payload
?id[where]=1 and 1=updatexml(1,concat(0x7e,(select password from users limit 1),0x7e),1)%23
Debug调试
在$data
处下个断点,先试试普通的sql注入会发生什么,在浏览器传入id=1'
这个M方法处理的是users,并不是我们传入的值,是用来建立模型的,不用管,直接步出,进入到I()
方法,I()
函数中获取参数,会经过ThinkPHP/Common/functions.php:391
htmlspecialchars()
进行处理
总之,I
方法用来获取我们通过GET传入的id值,然后传入find方法,跟进find方法,经过赋值后的where变量是一个array(借用pazuris大师傅的图)
find函数最关键的部分如下,这个select就是正式进行查询,我们后面会讲到:
跟进_parseOptions()
方法_parseOptions()
对表达式进行分析(也就是处理我们的查询语句,options是一个array,里面有where,而where也是array,里面放了id="1'"
)
_parseOptions()
前面都在自动获取表名,指定表之类的操作,字段类型验证在后面:
可以看到这个字段类型验证的条件是where是一个array,后续我们sql注入做文章的地方就是这里,先看看如何进行字段验证,关键函数是parseType,跟进看看:
简单来说就是根据表字段的类型来将传入的值强转,可以看到我们传入的1'
被intval转成了1(id字段的类型是int),sql注入失效
可以通过将id字段的类型设置成varchar看看会怎么处理字符串,是否存在sql注入。
改了之后可以重新断点跳到这个地方,可以看到是成功跳过了所有的if,回到了parseOptions再回到find函数中,来到find中第二个关键函数select
select中的关键是buildSelectSql,就是这个函数利用我们传入的options生成了sql查询语句,跟进:
parsesql对我们的options进行了处理,跟进
看到了一堆函数,用来处理sql语句的不同逻辑
一通跳转到了处理我们传入的值的函数,parseValue,注意看此时传入前的value仍然是1'
这行代码调用了escapeString来处理我们value,跟进:
这个addslashes直接宣告了常规sql注入的终结,因为会转义单引号,可以看到我们的value变成了1\
,失去效果。
既然传字符串不行,可以考虑传入一个数组,而联想到刚刚的判断,可以传入?id[where]=1
看看,因为可以直接跳过强转部分,理由稍后阐明(不需要再使得字段是varchar,是int也行)
从头开始一路跟进,回调think_filter
函数进行过滤
跟进到find方法中会发现这个options里的where不再是一个数组array,而是一个字符串。
继续跟进,既然where不再是array,这个parseOptions里的字段类型验证直接失效,更不会到parseType中发生强转,成功绕过第一层,
回到find中,到第二个关键函数parseWhere处理了where(因为我们现在相当于传入的是where,处理我们传入的就只有parseWhere,而不会像上次一样传入parseValue处理,自然也就绕过了addslashes),并且直接把where字符串内容返回
最后一通跳转返回parsesql函数,返回了sql语句
我配置的环境是已经修复过的版本,pazuris师傅的未修复的版本是这样的,sql这里比我多了个WHERE 1
这个where后面的值就是我们传入的1,从而可以随意控制值来进行sql注入,相当于没有了任何验证,传入?id[where]=id=-1 union select 1,group_concat(flag4s),3,4 from flags
(后面一整个字符串直接拼到sql语句中)得到这一题的flag。
总结:
- 传入id值时正常逻辑,
find()->_parseOptions()->_parseTypes()->select()->parseSql()->parseValue()
,其中_parseTypes()
是第一层需要绕过的点,parseValue
是第二层需要绕过的点 - 传入
?id[where]=1
,绕过上述两层,find()->_parseOptions()->select()->parseSql()->parseWhere()
,直接将内容拼接到sql语句中。
修复后:
可以对比下前后两个版本(白色背景的是pazuris大师傅的)
可知重要的改动是把options变成了this->options,并且不再直接将options传入parseOptions,this->options初始为空,需要赋值,而如果传数组,就没法赋值where给this->Options,所以相当于到parseOptions中我们根本就没有where(直接传数组相当于啥也没传),自然绕不过第二层,不再可以注入。
exp注入
控制器配置
继续修改index方法
1 | public function index() |
为什么用GET获取而不用I方法获取,因为I方法内部有过滤,不允许传入参数值以exp开头(后面分析payload就知道为什么要用exp开头了)。直接调用where再调用find主要是为了保证where是我们传入的array
payload
http://localhost:8888/thinkphp-master/index.php?username[0]=exp&username[1]==-1 union select 1,2,3
Debug调试
打上断点,按下F5,浏览器传入payload,又是沟槽的debug的一天
先跟进where方法,一句话:把我们传入的这个数组array('username' => $_GET['username'])
传给了$this->options['where']
继续跟进
跟进到parseOptions中,和之前一样,进行字段验证,这个val变量是一个数组,而is_scalar方法判断一个变量是否是标量(存储单个值的数据类型),这里if判断失败,自然实现了绕过。
进入我们的老朋友select方法
进入buildSelectSql方法,然后跳到parseSql一大堆函数,前面的验证都和之前一样,直接跳到不一样的地方,parseWhere,由于我们现在的where是一个二维数组(应该可以这么说),自然处理方式和之前不同
可以看到whereStr现在是空的,也就是说他的值即为parseWhereItem处理的结果
跟进到parseWhereItem
1 | elseif ('bind' == $exp) { |
这个exp变量获取的是var[0]
的值,也就是我们传入的第一个值,就是exp,可以看到这时候whereStr就是key拼上var[1]
,也就是查询的username拼上我们传入第二个值。细心的同学会发现还有一个bind判断,不急,这是下文的主角
如此看来,whereStr的值这不就是联合注入吗
然后这个语句就毫无过滤的传回去拼到where查询了,实现了sql注入。
总结:
往where方法传入数组,第一个值为exp,第二个值为=-1 union ….,就可以在parseWhere构建whereStr,从而实现sql注入。比如传?username[0]=exp
效果如下:
1 | select * from `users` where `username` $val[1] limit 1 |
bind注入
控制器配置
继续修改index
1 | public function index() |
payload
?id[0]=bind&id[1]=0 and updatexml(1,concat(0x7e,user(),0x7e),1)&passwd=1
Debug调试
打好断点,跟进和上文如出一辙的where方法,进入save方法,save先使用facade方法来处理我们的data
跟进facade方法,这个方法是检查数据字段并进行过滤,看到了经典的检查和强转,不过我们也不是用data进行注入
然后进入update方法
跟进parseSet
很抱歉的通知您,由于Saofe1a师傅手贱,不小心把框架中的代码误删且无法恢复,不愿意再重新配环境了,于是接下来会借用其他师傅的图
跟进bindParam看看:
从而得到bind的值:0 = 1
,后面就是常规的bind注入和上面的exp注入一样,会产生whereStr
返回str构成的sql语句:
UPDATE users
SET password
=:0 WHERE id
= :0 and updatexml(1,concat(0x7e,user(),0x7e),1)
进入最终的excute方法:
重点在最下面那个最终的queryStr生成,简单来说strtr就是把所有的:0都替换成了1(运用了之前那个bind规则,当我们的id[1]
传入的是0时,刚好拼出一个:0,从而实现替换,使得sql报错)
替换完得到最终的语句,成功sql注入:
UPDATE users
SET password
=’1’ WHERE id
= ‘1’ and updatexml(1,concat(0x7e,user(),0x7e),1)
总结:
首先第一个id[0]
=bind传入是为了在parseWhere中按bind处理,拼接返回whereString,
然后第二个id[1]
=0 and updatexml(1,concat(0x7e,user(),0x7e),1)是用来报错注入的语句(因为是save操作,并不会有回显,只能用报错注入),这个0是强制的,因为可以替换的仅仅为:0
,如果不是,最后就不会替换,从而报错(但是没有注入)。
最后第三个passwd的值就是希望:0
替换成的值,由于这里是id,随便输个数字就行。
- Title: Thinkphp3.2.3 sql注入
- Author: Saofe1a
- Created at : 2024-10-01 23:49:09
- Updated at : 2024-10-01 23:49:04
- Link: https://saofeia.github.io/2024/10/01/Thinkphp3.2.3 sql注入/
- License: This work is licensed under CC BY-NC-SA 4.0.