DVWA盲注实战:布尔盲注与时间盲注从入门到精通

发布时间:2026/9/16 2:31:08
DVWA盲注实战:布尔盲注与时间盲注从入门到精通 很多人练SQL注入最喜欢挑有回显的模块一提交页面就哗啦哗啦把数据吐出来成就感拉满。但一到DVWA靶场的SQL InjectionBlind模块很容易卡住——页面翻来覆去只有两句话User ID exists in the database. 或者 User ID is MISSING from the database. 没有报错明细没有数据表格连输出内容都看不到变化。其实这正是真实业务里最常见的状态绝大多数Web应用不会把SQL报错原样甩给用户攻击者只能靠页面行为差异一点一点把数据库信息盲猜出来。我当年在这个模块上磨了整整两天从完全懵圈到能徒手拖出整个库结构踩了不少坑也把布尔盲注和时间盲注彻底吃透了。这篇文章把我完整的练习链路和当时的判断思路都整理出来适合刚过完入门注入、想系统啃下盲注的同学参考。1. 为什么拿DVWA练盲注以及搭环境时更容易踩的两个坑1.1 Blind模块在DVWA里的独特价值DVWA一共十个漏洞模块SQL Injection和SQL InjectionBlind看起来很像实际训练目标完全不同。前者练的是如何利用回显直接提取数据后者练的是如何在没有回显的情况下逐位还原数据。很多教程把Blind模块一笔带过说用sqlmap一把梭就行但我的体会是手工过一遍Blind对SQL语句结构、查询执行顺序、数据库元数据表的敏感度提升非常明显。你会在一次次AND判断中真正理解WHERE子句里的条件是如何影响结果集的。另外DVWA把同一漏洞分成Low、Medium、High三个安全等级这能让你直观看到同一段代码在不同防护强度下注入难度是怎么逐级提升的。Low基本是裸奔Medium加了转义High换了输入点这恰好对应了实际开发中常见的三种历史遗留场景。1.2 Docker方式搭建比手动配PHP环境省心得多如果你还没搭好DVWA我强烈建议用Docker而不是手动去配PHPMySQL。手动搭建我要多说一句PHP版本和MySQL驱动很容易出兼容问题尤其在你本机已经装了其他版本PHP的情况下改配置时会牵连别的项目。用Docker的步骤很直接docker pull vulnerables/web-dvwa docker run -d -p 8080:80 vulnerables/web-dvwa启动后浏览器访问http://127.0.0.1:8080默认账号admin、密码password。登录后先点左侧的Setup/Reset Database初始化数据库再重新登录一次。1.3 一次真实的启动失败排查我第一次在Windows上用Docker跑DVWA时遇到过容器起来但页面一直转圈的情况。用docker logs看了下发现MySQL没有正常初始化原因是内存分配不足。解决方案是给Docker Desktop多分配一些内存比如4GB以上或者改用下面的命令限制容器内存占用docker run -d -p 8080:80 --memory1g vulnerables/web-dvwa还有个很常见的坑是端口冲突。如果8080被占用容器会启动失败这时候换个端口映射即可不用纠结非要用8080不可。注意DVWA自带弱口令和多个已知漏洞建议只在本地虚拟机或隔离环境里练习。尽量不要把它部署到公网或公司内网段靶场的目的就是用来打坏主意的别让坏主意打到别人身上。2. 盲注的本质数据库是如何被问出答案的2.1 布尔盲注把数据库当成一台是/否问答机我先说结论无论盲注形式怎么变核心思路都是构造一个条件表达式让它因为你的输入而变成True或False再观察页面结果有没有变化。拿Low级别举例页面存在与不存在分别输出两句话存在User ID exists in the database.不存在User ID is MISSING from the database.当你提交1 AND 11 -- -SQL里实际执行的是SELECT first_name, last_name FROM users WHERE user_id 1 AND 11 -- - LIMIT 1;AND 11恒为真结果集和只查user_id1一样页面显示存在。当你提交1 AND 12 -- -变成SELECT first_name, last_name FROM users WHERE user_id 1 AND 12 -- - LIMIT 1;AND 12恒为假结果集为空页面显示不存在。这就是布尔盲注的原始信号**条件真页面一种表现条件假页面另一种表现。**数据库不会直接告诉你答案但它会用结果集是否为空这种物理行为来回答你的是/否提问。2.2 时间盲注当是/否没有区别时用延迟来回答布尔盲注有个前提页面必须有可观察的差异。如果开发者做了统一处理比如不管查没查到都返回一张空表或者统一返回没有数据那True和False在页面上长得一模一样布尔盲注就失效了。这时候可以换成时间盲注。思路很简单让条件成立时执行sleep()条件不成立时不执行然后用响应时间作为判断信号。典型Payload长这样1 AND IF(SUBSTRING(DATABASE(),1,1)d,SLEEP(3),0) -- -如果第一秒页面没出来等了3秒才返回说明DATABASE()第一位确实是d如果秒回说明第一位猜错了。数据库本质上变成了一个会睡午觉的问答机。2.3 字符串比较与子串函数的选择无论布尔还是时间盲注提取数据时都离不开逐字符比较。MySQL里最常用的组合是SUBSTRING()配合等值比较。这里有几个细节要注意SUBSTRING(str, pos, len)的索引从1开始不是0。比较字符时用a没问题但在Medium级别如果单引号被转义就需要用十六进制0x61或CHAR(97)来代替。注释符--后面必须跟一个空格否则注释不生效。我在URL里习惯写成-- -末尾那个减号是给空格占位的。也有人用#但在URL里要编码成%23。理解这些之后你可以把盲注看作是一种协议请求→条件判断→响应差异→推断位值。每一步都非常机械所以后面完全可以脚本化。3. Low级别通关纯手工布尔盲注的完整链路3.1 第一步确认注入点类型访问/vulnerabilities/sqli_blind/提交id1页面显示存在。先测单引号提交1如果页面变成MISSING或报错说明单引号干扰了SQL语法。再测1 AND 11 -- -和1 AND 12 -- -观察到存在/不存在两种结果这就确认了注入类型是字符型因为需要用引号闭合前边的user_id $id。页面有布尔差异可以用布尔盲注。3.2 第二步猜库名长度和内容拿到布尔通道后第一个目标是确认当前数据库名。先用二分法判断长度。比如DVWA的库名长度是4我可以这样测1 AND LENGTH(DATABASE())4 -- -返回存在则长度为4如果返回不存在就继续试5、6、7。接下来逐字符猜解库名。先猜第一位1 AND SUBSTRING(DATABASE(),1,1)d -- -存在则第一位是d否则换字母继续试。完整的库名是dvwa四轮下来就能确定。这个操作看起来简单但你要理解背后的含义**我们并没有把数据直接读出来而是通过一次次的等值判断把未知字符缩小到一个确定值。**这个过程和人类猜密码的逻辑本质上是一样的只是猜得更慢、更蠢。3.3 第三步从表名到字段名的推理路径库名确定后继续查表名。表名存在information_schema.tables里标准Payload是1 AND SUBSTRING((SELECT table_name FROM information_schema.tables WHERE table_schemadvwa LIMIT 0,1),1,1)g -- -这里嵌套子查询的作用是先从元数据表里取第一张表名再逐字符取出表名每一位进行比较。LIMIT 0,1表示取第一行猜完第一张表所有字符后改LIMIT 1,1取第二张表。DVWA里通常能猜出users表。然后查字段名1 AND SUBSTRING((SELECT column_name FROM information_schema.columns WHERE table_nameusers LIMIT 0,1),1,1)u -- -依次能得到user_id、first_name、last_name、user和password等字段。3.4 手工太慢用Burp Intruder加速字符枚举手工慢慢换字母能练耐心但效率实在太低。第二次过的时候我改用Burp Suite的Intruder模块把整个过程缩短到几分钟思路如下把请求发送到Intruder。将Payload里变化的部分比如SUBSTRING(DATABASE(),1,1)d中的字符d标记为变量。使用Brute Force攻击类型字符集选abcdefghijklmnopqrstuvwxyz0123456789_。跑完后看响应中哪个包返回exists那个字符就是当前位。每轮Intruder只解决一位字符。想更高效可以把位置也变量化一次跑完所有位置和字符但需要处理响应判断逻辑。我建议刚开始还是手工一轮一轮来能更直观地理解盲注的每一步。3.5 低级别源码回顾Low级别的PHP代码大致是这样的$id $_GET[id]; $getid SELECT first_name, last_name FROM users WHERE user_id $id;;用户输入直接进了SQL字符串完全没有任何过滤。这个等级存在的意义就是让我们理解漏洞的原始形态什么防护都没有时注入可以畅通到何种程度。4. Medium级别转义来了但数字型注入留了个口子4.1 单引号被转义后字符串注入为什么失效Medium级别的源码核心变化是$id mysqli_real_escape_string($GLOBALS[___mysqli_ston], $id); $getid SELECT first_name, last_name FROM users WHERE user_id $id;;mysqli_real_escape_string()会对、、\等字符做转义。你提交1 AND 11 -- -时单引号会变成\SQL语句变成了WHERE user_id 1\ AND 11 -- -转义后的单引号不再闭合字符串反而变成了字符串内容的一部分语法直接报错。这就是为什么Low级别那套字符型Payload在这里全军覆没。4.2 数字型盲注不碰引号也能判断注意查询语句里user_id $id这个$id没有用引号包起来。这意味着输入会被当作数字处理我们根本不需要闭合单引号只需要注入数字运算逻辑。判断注入是否可用1 AND 11 1 AND 12第一句返回存在第二句返回不存在。原理和Low一模一样只是少了引号闭合这一环。提取库名时使用十六进制绕过单引号1 AND SUBSTRING(DATABASE(),1,1)0x640x64就是字符d的十六进制表示MySQL会自动把它转成字符进行比较完全不依赖单引号。如果觉得十六进制记起来麻烦也可以用CHAR(100)效果相同。4.3 Medium级别的另一种玩法UPDATE二次盲注Medium源码里除了SELECT还有一个UPDATE操作。很多新手只盯着查询注入忽略了更新语句的利用价值。页面代码会先根据id查询用户然后尝试更新first_name字段。这意味着可以构造类似下面的Payload把布尔盲注转化为数据更新1 AND UPDATE(test, user_id, first_name) ...实际DVWA里的UPDATE语句是UPDATE users SET first_name ... WHERE user_id $id;但因为转义和引号的问题直接利用并不容易。我的建议是**既然SELECT路径已经能通就不必在这里死磕UPDATE。**知道有这条路径就够了它更大的价值在于启发思路——同一段业务逻辑里可能存在多个SQL入口注入点排查必须把所有访问数据库的位置都过一遍。4.4 Medium级别的核心总结Medium级别看起来只加了一个转义函数实际上把所有字符串型注入都堵死了。但开发者犯了一个更隐蔽的错误把输入拼接到数字上下文里转义函数对数字上下文无能为力。**过滤函数永远只能处理已知的坏字符而SQL注入的本质是结构问题不是字符问题。**这句话我在之后的所有渗透测试里都反复验证过。5. High级别藏在Cookie里的时间盲注5.1 为什么URL里怎么改都没反应到了High级别很多人的第一反应是继续在URL参数里试Payload结果发现怎么改页面都一样。原因在于这个等级已经不再从$_GET[id]取值了而是读取Cookie中的id字段。我以自己练手的DVWA 1.9版本为例源码大致是这样的$id $_COOKIE[id]; $getid SELECT first_name, last_name FROM users WHERE user_id $id LIMIT 1;;浏览器第一次访问时其实并没有id这个Cookie所以后端拿到的可能是空值页面行为很不直观。你需要自己往请求里加一个idCookiePayload就放进这个Cookie里。5.2 用Burp Suite修改Cookie打时间盲注这个等级我推荐直接上时间盲注因为Cookie改动不像URL参数那么直观用布尔盲注来回切换看页面字体大小会很累。用时间盲注就舒服很多。用Burp Suite抓一个普通的POST/GET请求在请求头中手工加入Cookie: PHPSESSID你的会话; securityhigh; id1 AND IF(SUBSTRING(DATABASE(),1,1)CHAR(100),SLEEP(3),0) -- -然后发送。如果等待3秒后才有响应说明当前第一位是d如果立刻返回说明猜错了。为什么用CHAR(100)而不是d因为Cookie值同样会经过字符串拼接使用十六进制和CHAR()可以避免各种编码干扰也让Payload在各种版本下更稳定。5.3 完整提取一次库名用时间盲注逐位提取DATABASE()的过程是这样的id1 AND IF(SUBSTRING(DATABASE(),1,1)CHAR(100),SLEEP(3),0) -- - id1 AND IF(SUBSTRING(DATABASE(),2,1)CHAR(118),SLEEP(3),0) -- - id1 AND IF(SUBSTRING(DATABASE(),3,1)CHAR(119),SLEEP(3),0) -- - id1 AND IF(SUBSTRING(DATABASE(),4,1)CHAR(97),SLEEP(3),0) -- -四个字符分别对应d v w a时间盲注也能还原出完整库名。整个过程比布尔盲注慢但它在页面表现完全没有差异的场景里是唯一选择。5.4 用sqlmap偷懒之前先搞懂它在做什么很多人到这里就喊sqlmap一把梭我反而觉得High级别是手工练习时间盲注最好的机会。但如果着急通关sqlmap用法也很简单sqlmap -u http://127.0.0.1:8080/vulnerabilities/sqli_blind/?SubmitSubmit --cookiePHPSESSIDxxx; securityhigh; id1 -p id --techniqueT --dbs注意-p id指定Cookie里的参数--techniqueT只启用时间盲注。如果你不想让sqlmap扫一堆无关的东西加--level2让它检测Cookie参数。但我的建议仍然是**先用Burp手工跑通一遍时间盲注再用sqlmap验证结果。**盲注的Payload看上去很简单但判断逻辑、响应时间阈值、字符集枚举这些经验只有自己写出来、跑过才能真正形成肌肉记忆。工具能帮你出结果但帮不了你理解漏洞。5.5 为什么High级别还要防一道High级别的防护其实也不强只是把输入源从GET挪到了Cookie。这给我们的启示是**开发人员常常只防看得见的入口却忽略了Cookie、Header、Referer这些看不见的入口。**很多真实系统里参数校验只做在Form参数上但后端读取的数据来源五花八门。安全测试如果只盯着URL参数很容易漏掉更大的攻击面。6. 通关之后必须落实的防御自检6.1 参数化查询才是治本方案练完三个等级你会发现所有注入问题都出在用户输入直接拼接进了SQL语句结构。要根治不是写更严密的过滤函数而是让SQL语句的结构和用户输入彻底分离这就是参数化查询Prepared Statement做的事情。以PHP PDO为例$stmt $pdo-prepare(SELECT first_name, last_name FROM users WHERE user_id :id); $stmt-bindValue(:id, $id, PDO::PARAM_INT); $stmt-execute();用户输入只作为参数值传给数据库永远不可能变成SQL结构的一部分。这就像你让一个快递员送包裹他只能送到你写的地址但没法改变快递单上的路线图。6.2 黑名单过滤为什么不靠谱有些开发者喜欢用黑名单过滤危险关键字比如把、AND、SELECT、UNION都替换掉。问题是黑名单永远列不全。你不拦CHAR(100)它就绕过你不拦%0a它也能换行继续。我见过最离谱的一次过滤了空格对方用/**/注释当空格绕过去了。过滤只能增加攻击成本不能消灭漏洞。6.3 回到真实业务的自检清单玩完DVWA的Blind模块后我养成了一个习惯每次代码里出现数据库操作都会下意识问自己几个问题这条SQL语句是不是参数化查询如果不是改成参数化。有没有直接从Cookie、Header、Referer里取值拼SQL有的话先洗手。数据库账号是否用了最小权限应用账号能不能DROP表SQL日志有没有开启即使被注入了能不能从日志里还原攻击路径这些问题看着基础但真实系统里因为赶上线、图省事而埋下的注入点远比想象中多。最后分享一个我自己的感受盲注练完最大的收获不是会背几个Payload而是重新理解了一个老道理——所有攻击手法本质上都是对系统逻辑的试探和利用。SQL注入如此其他漏洞也一样。你能把这种试探逻辑的思维刻进脑子里才算真正跨过了靶场到实战的那道坎。