0x00 前言
通常来说,在进行字符型的SQL注入时,都需要先将前面的引号等(以单引号为例)进行闭合才能执行我们构造的SQL语句,那么如果单引号被过滤了,是否还能够成功的SQL注入呢?
答案是可以,当你在判断登录时使用的是如下SQL语句:
1 | select user from user where user='$_POST[username]' and password='$_POST[password]'; |
那么即使在过滤的时候将单引号过滤掉了,还是可以进行一定程度上的注入的,下面通过两道CTF题来进行介绍。
0x01 [BJDCTF 2020]简单注入
进入题目后就是一个登录框,名字也提示了要注入:
扫描目录发现hint.txt中的提示:
告诉了我们登录的sql语句:
1 | select * from users where username='$_POST["username"]' and password='$_POST["password"]'; |
稍微fuzz一下可以发现过滤了'
、"
、=
、-
、and
、select
等关键词,其中关键就是这个单引号,由hint已知这里是需要单引号进行闭合的,而单引号又被过滤了。
可以发现注释符#
和反斜杠\
并没有被过滤,既然注释符没有过滤,那么我们就可以注释掉最后一个单引号,这样就只剩下3个单引号需要处理。
而如果我们输入的用户名以反斜杠\
结尾,例如POST:username=admin\&password=123456#
,那么拼接进去后,\
就可以将第2个单引号转义掉,如下:
1 | select * from users where username='admin\' and password='123456#'; |
这样第1个单引号就会找第3个单引号进行闭合,后台接收到的username实际上是admin\' and password=
这个整体,而这个用户名显然不存在,所以还是登录失败。
但是别忘了我们还有一个password变量可控,实际上我们已经解决了单引号的闭合问题了,下面的就是常规的思路,比如我们构造password为 or 2>1#
那么拼接后的sql语句就为:
1 | select * from users where username='admin\' and password=' or 2>1#'; |
很显然上面的语句会返回为真,通过这样的思路,我们就可以进行bool盲注:
最终脚本如下:
1 | import requests |
运行脚本得到admin的密码:OhyOuFOuNdit,登录即可得到flag。
0x02 某道CTF入群题
同样也是一到登陆题,在check.php
中给出了源码如下:
1 |
|
可以看到这一题的sql语句同样是下面这样:
1 | select user from user where user='$_POST[username]' and passwd='$_POST[passwd]' |
只不过过滤了更多的东西,下面思考如果进行绕过:
or
可以用||
进行绕过。- 用
%00
绕过注释符#、--
的过滤
(虽然%00
在过滤列表中,但由于浏览器在传给php过程中会经过一次urldecode(),所以php接收到的并不是%00
) - 用
/**/
绕过空格的过滤。 - 如果我们只需要得到
user
表下password
字段的值,因为是在同一条语句中,所以select
等查询语句的过滤对我们没有影响。 - 反斜杠
\
没被过滤,思路还是利用转义单引号来构造闭合,然后再在password字段构造盲注语句进行注入。
现在就是如何构造盲注语句,常用的=
、>
、<
、like
等逻辑运算都被过滤掉了,这时候就需要用到REGEXP正则注入。
在MySQL中除了可以使用LIKE ..%
进行模糊匹配,同样也支持正则表达式的匹配,其使用REGEXP 操作符来进行正则表达式匹配。
正则表达式的规则就类似于PHP 或 Perl,下表中的正则模式可应用于 REGEXP 操作符中:
查找以Le
开头的用户名:
查找以ck
结尾的用户名:
回到题目中来,我们尝试将按照题目的sql逻辑构造如下语句:、
发现是能够进行真假判断的,如果第一个字母猜对了,我们只需要接着判断前两位的开头是否正确即可,然后以此类推,直到猜出全部字段的值:
在题目中进行一下验证,利用Intruder模块爆破一下:
可以看到在以d
和D
开头时都返回了真,是因为使用regexp正则匹配时是不区分大小写,通常来说只需要加上binary
即可解决这个问题,如||binart/**/passwd/**/regexp/**/"^a";%00
,但是这一题过滤了in
,所以目前我还没有想到好的方法进行大小写匹配。
但实际上这一题的密码都是由小写组成的,否则就只能去遍历所有情况进行爆破了。
最终的脚本如下:
1 | import requests |
运行脚本得到密码:d0itr1ght
得到密码后,后面的就比较简单了,admin
用adm/**/in
绕过,然后用Referer利用file://localhost/..
伪造本地读文件即可:
源码中得到flag: