☰
DVWA靶场SQL注入通关手册:从Low到Impossible全程解析
2026/10/22 19:45:57 网站建设 项目流程

本地搭一个DVWA靶场,把SQL Injection那一列挨个刷过去,是很多Web安全入门的人都绕不开的第一课。DVWA本身就是故意做成漏洞百出的训练环境,SQL Injection又是其中最典型、最能锻炼思路的一个模块。这篇我直接按一位实际练习者的视角,把Low、Medium、High、Impossible四个难度从探测到通关讲清楚,顺便把"为什么要这么打"也说透。

DVWA靶场通关——SQL Injection篇

1. 先把DVWA跑起来,这一步别偷懒

1.1 为什么要拿DVWA练SQL注入

DVWA全称是Damn Vulnerable Web Application,翻译过来就是"一个故意设计得很脆弱的Web应用"。它把SQL注入、XSS、文件包含、命令注入这些经典漏洞做成了独立模块,并且每个模块都人为设置了多个难度,方便练习者一层层体会漏洞是怎么产生的、防御代码又是怎么升级的。

SQL Injection模块最适合做入门练习,因为它的逻辑非常单纯:一个ID输入框,一段查询代码,漏洞成因就摆在那里。很多初学者一上来就急着上工具扫描,结果工具跑了一堆数据,自己却说不清楚"为什么这里能注入"。DVWA要的就是让你先把原理看懂,再用手工方式把整个注入过程走一遍。

1.2 三个关键点:环境、权限、安全等级

很多人在登录DVWA之前就被环境卡住了。我建议先确认三件事:Web服务有没有跑起来、数据库密码配置对不对、数据库表有没有初始化。

先说环境。本地练习最稳妥的方式是装一套PHP加MySQL加Apache的组合环境,把DVWA解压后放进Web根目录,然后访问/vulnerabilities/sqli/就能看到对应的模块页面。如果不想在自己的电脑里塞一堆运行时,用现成的Docker镜像也可以,但需要注意端口映射和MySQL的版本兼容问题。

然后是配置文件。DVWA目录下的config/config.inc.php里面默认写着一组数据库连接参数,如果你本机MySQL的root密码不是默认值,就必须改这里的密码字段。这一行错了,后面setup页面会直接报数据库连接失败,很多人以为是靶场坏了,其实只是配置没对上。

最后是安全等级。登录DVWA后,进入DVWA Security页面,把等级切到Low,再回到SQL Injection模块。DVWA的每个模块都是按当前全局安全等级走,等级设置错了,练习的难度也会跟着变。建议刚开始先固定在Low,把整套流程跑通后再往上加难度。

还需要注意一个细节:DVWA在旧PHP版本上跑得最顺,新版PHP下有些旧模块会暴露废弃函数警告,甚至直接白屏。真遇到这种情况,优先看页面右上角的View Source还能不能打开,如果源码能看但页面报错,多数是PHP版本导致的函数差异,换一个兼容版本基本能解决。

1.3 跑通前的快速自检

在进入SQL Injection关卡之前,用浏览器打开http://127.0.0.1/login.php,用默认账号登录,确认能进入主菜单,再单独访问一次SQL Injection模块,随便提交一个数字ID,看页面是否有用户数据返回。这一套都通了,说明环境没问题。

2. SQL注入的原理,一句话讲清楚

2.1 漏洞的本质是拼接

SQL注入听起来玄,本质就两个字:拼接。后端程序拿到用户输入后,没有做任何隔离,直接把它拼进SQL语句里,导致输入里的内容被当成了SQL代码执行。

比如DVWA低级别的查询代码,逻辑上约等于:

$id = $_GET['id']; $query = "SELECT first_name, last_name FROM users WHERE user_id = $id;";

正常情况下,用户传id=1,SQL语句就是:

SELECT first_name, last_name FROM users WHERE user_id = 1;

这个查询完全正常。但用户如果传的不是数字,而是1' OR 1=1 -- -,拼接后就变成了:

SELECT first_name, last_name FROM users WHERE user_id = 1' OR 1=1 -- -;

原来的参数在这里已经不是"一个值",而是SQL语法的一部分。单引号把字符串闭合,OR 1=1让条件永远为真,注释符把后面多余的语句全部注释掉,整条查询直接变成了"返回全部用户"。这就是注入的第一现场。

2.2 判断注入点的两条铁律

拿到任何一个输入点,先不要急着拖数据,先判断它到底能不能注入。判断方法无非两条:看报错、看响应差异。

第一条,输入一个单引号。如果页面报出SQL语法错误,说明输入被直接拼进了SQL语句。DVWA低级别在id框里输1',页面会吐出来一段包含You have an error in your SQL syntax的报错,这就是最直接的证据。

第二条,输入1 AND 1=1和1 AND 1=2看返回结果差异。前者为真,正常返回数据;后者为假,查询结果为空。如果两条返回结果不一样,说明我们可以控制WHERE条件的一部分,即使没有报错,也大概率存在可布尔判断的注入点。

这里有一条很重要的经验:真正干活的时候,报错不是坏事,反而是最明显的信号灯。很多刚入门的人看到报错就慌,觉得把自己环境搞坏了,其实报错信息里往往直接写明了表名、字段名和拼接结构。

2.3 先分清数字型和字符型

SQL注入里有一个特别容易忽略的分界,就是参数在被拼接时有没有引号包裹。

如果SQL语句是WHERE user_id = $id,这叫数字型注入点,用户输入不需要用引号闭合,传什么都直接在数字位置上生效。如果SQL语句是WHERE username = '$name',这叫字符型注入点,需要先输入一个单引号把字符串闭合,让后面的内容逃逸到SQL语法层面。

DVWA的低级别两个模块正好各占一种:SQL Injection的ID查询是数字型,后续盲注模块在某些版本里则是字符型或者兼容两种。分清类型的意义在于,构造payload的时候能少做很多无用功。比如数字型注入点,你拼1' union select ...反而会因为多出来的单引号把语句破坏掉;字符型注入点如果不闭合引号,union关键字又永远只会被当成字符串,根本不生效。

所以拿到输入点,先通过报错判断类型,再决定payload开头的写法,这是我一直坚持的流程。

3. Low难度:联合查询,手工注入的标准起手式

3.1 从报错里确认参数入口

把DVWA安全等级切到Low,打开SQL Injection模块,id框直接输入:

1'

提交后页面会报SQL语法错误。从报错中可以看到,输入被原封不动拼进了SQL语句。截取到的关键信息基本长这样:

You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''1''' at line 1

这个报错已经告诉我们两件事:后端用的是MySQL,并且查询语句中用户ID的位置没有引号包裹。因为我们在数字参数后加了一个单引号,导致SQL里出现了一个多余的引号,语法因此断裂。

3.2 用order by判断列数

确认注入点存在后,第一件事不是去拖数据,而是搞清楚查询结果有几列。判断列数最稳的方式是order by加上数字排序:

1' ORDER BY 1 -- - 1' ORDER BY 2 -- - 1' ORDER BY 3 -- -

依次递增数字,直到某一列序号合法但下一列序号报错为止。比如ORDER BY 2正常,ORDER BY 3报错,说明当前查询只返回2列。这里要注意,我是用1'开头来闭合数字位置,再用注释符把原SQL后面可能存在的部分吃掉。如果在有些版本里注释符被过滤,也可以考虑用-- %20或#的变体,但DVWA的低级别直接用经典写法就行。

注释符后面的空格是个坑。很多浏览器会把--后面的空格删掉,导致注释失效。常见做法是把空格写成URL编码+,也就是--+,或者确保SQL注释符后面至少跟一个空格。

3.3 union select定位回显位

知道了列数是2,下一步用union把两条查询的结果合并。前提是union前的查询和union后的查询列数一致:

1' UNION SELECT 1, 2 -- -

正常提交后,页面上原来的用户信息区域会多出一行,里面显示出数字1和2。这两个数字出现在页面的不同位置,说明这两个位置就是查询结果的回显位,后面的拖数据操作就是往这两个位置上填数据。

3.4 库名、表名、字段名、数据一步步拖出来

有了回显位,先拿数据库的基本信息。提交:

1' UNION SELECT database(), user() -- -

页面第一列回显当前数据库名,第二列回显数据库账号。DVWA的默认数据库通常叫dvwa,了解一下即可。

接着爆表名。我们知道MySQL的元数据存在information_schema数据库里,所以可以通过查询table_name拿到所有表:

1' UNION SELECT table_name, table_schema FROM information_schema.tables WHERE table_schema=database() -- -

如果页面上多张表名被列出来,找到users相关的表,一般是users或者user表。然后再查字段名:

1' UNION SELECT column_name, column_type FROM information_schema.columns WHERE table_name='users' AND table_schema=database() -- -

这一步会列出该表所有字段,比如user_id、first_name、last_name、user、password。等你确认了字段名,最后一步就是拖数据:

1' UNION SELECT user, password FROM users -- -

页面上会显示用户名和一串MD5密文。DVWA里密码通常是MD5运算后的值,可以拿在线工具或hashcat解出明文。到这一步,低级别整条链路就闭环了。

4. Medium难度:转义过滤不是万能盾牌

4.1 先看源码,Medium到底过滤了什么

把安全等级切到Medium,回到SQL Injection模块,打开View Source,代码里多了一行关键防御:

$id = mysqli_real_escape_string($conn, $_GET['id']);

mysqli_real_escape_string做的事情,是把用户输入中的单引号、双引号、反斜杠等字符进行转义。比如你输入1',到SQL语句里会变成1\',单引号失去了闭合字符串的能力。在字符型注入点里,这种防护确实能挡住经典payload。

但别忘了DVWA是故意设计成有漏洞的,Medium的坑就在查询语句本身:SQL里还是直接拼接变量,而且变量位置没有用户ID的值周围没有引号。准确说,查询仍然是:

SELECT first_name, last_name FROM users WHERE user_id = $id;

也就是说,这是一个数字型注入点。转义函数对引号的保护,在"根本不需要引号"的场景里等于白做。

4.2 绕过思路:别急着闭合引号

很多人在Medium关卡会习惯性套用Low的payload,输入1' UNION SELECT ...,结果发现页面报错或者直接没反应。原因就是单引号被转义成了\',整个SQL语句语法被破坏了。

正确的思路是把引号忘掉。既然查询不需要引号闭合,那么直接让整数参与逻辑运算即可。先试一条最基础的:

1 OR 1=1

提交后页面返回的不再是一条记录,而是多条用户记录。这说明我们的输入影响到了WHERE条件,注入点仍然存在。

想通过union拖数据也一样,开头不要写引号,直接写数字:

1 UNION SELECT user, password FROM users -- -

注意这里union前是WHERE user_id=1,结果有一行,union后是整张用户表,两边列数相同,数据自然就出来了。页面回显里能直接看到用户名和密码密文。

在Medium级别还有一个常见姿势是利用运算符优先级。比如输入1 OR 1=1 -- -,虽然也能让条件恒真,但往往会把查询结果散成多行;用union直接并列查询,显得节奏更清晰,也更符合"拿数据"的目标。

4.3 Medium档完整通关演示

我用一组连贯的请求把Medium流程走一遍,方便照着复现:

第一次提交,测试转义是否生效:

输入:1' 结果:SQL报错,提示语法错误,或页面无返回

这里的报错本身反而能用来确认"转义后引号导致SQL结构破坏",证明参数确实是数字型拼接。当然,更简单的是看源码。

第二次提交,验证逻辑注入点:

输入:1 OR 1=1 结果:页面返回多行

第三次提交,确认列数:

输入:1 ORDER BY 2 -- - 输入:1 ORDER BY 3 -- -

ORDER BY 3报错,说明还是2列。

第四次提交,直接拖数据:

输入:1 UNION SELECT user, password FROM users -- - 结果:页面显示出所有用户名和密码

这里有一个细节值得单独说:因为SQL语句里没有引号包裹,ORDER BY后面的数字、UNION SELECT后面的字符串都不需要闭合引号,但要确保前面的1能正常命中一条记录,否则union前的结果集为空,整条查询虽然语法正确,也不会返回union后的数据。

5. High难度:当常规注入失效,试试盲注

5.1 High级别的白名单把普通注入堵死了

切到High级别,再回到SQL Injection的ID输入框,你会发现原来好用的payload全部失效了。原因是High级别对id参数做了白名单校验,只允许id等于1到5中的固定值,其他值一律被过滤或者替换成0。

这个时候,"手动注入"的主模块基本已经闭环,焦点头马上转到另一个子模块:菜单里的SQL Injection (Blind)。盲注模块是High级别真正的考点,它的特点是你看不到查询结果,页面只告诉你"存在"或者"不存在"。

这种场景下,数据不是直接列出来的,而是靠一次一次地"提问"猜出来的。SQL注入变成了一个判断题,我们问数据库"某个字符是否大于某个值",页面用真假回答我们。

5.2 盲注核心:把数据库问成判断题

盲注的原理其实还是布尔逻辑。如果注入点是数字型,输入1 AND 1=1时,条件为真,页面返回"存在";输入1 AND 1=2时,条件为假,页面返回"不存在"。

那么我们就可以把需要获取的信息拆成一个一个的布尔问题。比如要知道当前数据库名的第一个字符是不是a,可以构造:

1 AND SUBSTRING(DATABASE(),1,1)='a'

如果页面返回"存在",说明数据库名第一个字母是a;返回"不存在",就说明不是a。接着猜第二位,第三位,直到把整个字符串猜出来。

效率问题当然是存在的。英文加数字每个字符最多几十种可能,手工一个个试会非常慢,所以盲注强推配合脚本。脚本的逻辑很简单:循环截取字符串每个位置,逐个比较字符大小,用二分法或者穷举法跑完。写Python脚本的时候,注意读取响应文本里的关键区分词,比如exists和missing,不要判断整页HTML,否则任何微小格式变化都会误判。

5.3 布尔盲注手工实操

在DVWA盲注模块里,先用一条最简单的payload验证布尔差异:

1 AND 1=1

页面显示"User ID exists"。接着:

1 AND 1=2

页面显示"User ID missing"。

确认布尔差异存在后,就可以开始猜数据库名了。手工版本先猜一下数量级,比如问第一个字符是否大于t:

1 AND ASCII(SUBSTRING(DATABASE(),1,1)) > ASCII('t')

返回exists,说明字符排序比t大,再问是否大于v,最终锁定字符。整串数据库名可以通过逐位重复这个过程得到。

这里的关键点是,盲注的payload要尽量贴合注入点的类型。如果是字符型且带引号,payload开头要闭合引号;如果是数字型,直接写数字逻辑。DVWA盲注模块在Medium和高等级别甚至会通过Cookie传入参数,所以先打开源码看入口是最稳的。

5.4 时间盲注作为无回显兜底

布尔盲注要求页面有无穷的真假差异。但有些场景下页面不管SQL真假,输出的HTML完全一样,这时候就需要时间盲注。

时间盲注的核心函数是sleep,比如:

1 AND IF(1=1, SLEEP(2), 0)

如果条件为真,数据库会傻等2秒才返回结果;如果条件为假,立刻返回。这样"页面内容差异"就变成了"响应时间差异",只要用秒表或者脚本统计请求耗时,就能拼出真值。

时间盲注是盲注的最后一张底牌,因为它的速度比布尔盲注还慢。在DVWA里如果有布尔差异,优先用布尔;布尔失效再用时间。实际练习时,时间盲注的payload配合脚本使用,否则一个一个手工判断响应时间会做到怀疑人生。

5.5 Cookie入口的盲注姿势

DVWA盲注模块在高级别版本里,经常会从Cookie里读取ID参数。表单里输入的id可能只是传给服务端写入Cookie,后续查询全部从Cookie取值。这意味着你想直接改URL参数是没用的,必须用浏览器开发者工具或者抓包工具修改Cookie。

以开发者工具为例,找到对应Cookie里的id值,改成:

id=1 AND 1=1

重新请求页面,如果返回exists,说明Cookie入口同样存在SQL注入。后面构造盲注payload时,只要把URL参数里的那套搬到Cookie值里就行。

Cookie注入算是一个小升级版考点,原理没有变化,只是入口从GET参数变成了Cookie。这也是为什么我一直推荐练习者养成"先看源码再动手"的习惯,很多防御升级之后,入口可能变了,但拼接漏洞的本质并没有变。

6. Impossible难度:代码级修复

6.1 参数化查询到底改了什么

DVWA的Impossible级别几乎是最正确的防御示范,SQL Injection模块在最高难度下不再拼接字符串,而是使用预处理语句。

$stmt = $conn->prepare("SELECT first_name, last_name FROM users WHERE user_id = ?"); $stmt->bind_param("i", $id); $stmt->execute();

这里和最初那段拼接代码的区别特别明显:SQL语句先准备好,固定好结构,留下一个占位符?,再用bind_param把用户输入当作纯数据绑定进去。数据库在执行时,会把用户输入当成一个值,而不是SQL语法的一部分,所以无论输入多少次1' OR 1=1,它都只是一个字符串,不会改变查询结构。

这就解释了为什么参数化查询被称为SQL注入的"根治方案":它从语法层面隔离了数据和代码。

6.2 从靶场到真实项目的防御习惯

在DVWA里通关Impossible只是第一步,更重要的是把防御思路迁移到真实项目。给数据库账号最小权限是基本盘,应用账号不该有drop、delete、file权限,就不能给;所有动态SQL必须走参数化查询,哪怕是自己内部管理系统也一视同仁;输入校验遵循白名单原则,明确"允许什么"而不是"禁止什么"。

我见过不少开发同学平时写代码很规范,一到报表导出、排序字段这种动态拼接场景就图省事直接把参数拼进SQL,排序字段还不好参数化,只能用白名单校验。这种零散的"图省事"恰恰是注入点的温床。DVWA的Impossible级别就是为这种场景准备的教案,它告诉你,再复杂的查询,也能通过参数化和白名单两层机制把风险压下去。

7. 常见问题速查:几个踩过的坑

7.1 现象、原因、解决办法

下面这些坑是我在DVWA练习时遇到的高频问题,整理成速查表更方便对照。

现象常见原因解决办法
setup页面提示数据库连接失败config.inc.php里的数据库密码没改对确认MySQL账号和密码,并确认数据库服务已启动
页面空白或HTTP 500PHP版本和DVWA旧代码不兼容换兼容性更好的PHP版本,或者改用Docker镜像
1' UNION SELECT 1,2 -- -返回正常但没有数据注释符后面的空格被浏览器删掉了使用--+或-- %20,也可以改用#
Medium级别输入单引号payload全部失效单引号被mysqli_real_escape_string转义数字型注入点不再使用引号,直接写数字逻辑
Blind模块页面永远显示同一个内容判断语句写错,或入口在Cookie而非URL参数先看源代码确认参数来源,再调整payload
sqlmap跑不出数据可没有带上登录后的会话Cookie用--cookie="PHPSESSID=..."指定合法会话
使用order by探测时列数一直报错前端把输入做了字符限制,或者后断存在干扰抓包直接修改请求参数,不要只看页面输入框

第二行的版本兼容问题值得单独展开一下。DVWA的很多旧版源码带了一些已被废弃的PHP函数,到了新版PHP里会直接报致命错误。解决办法不是硬着头皮改代码,而是看自己装的DVWA版本要求什么PHP版本,或者直接用官方新版。这样能省掉一晚上的排查时间。

7.2 工具不是主角,但能帮你节约时间

手工流程走顺之后,再用sqlmap这类工具会理解得更深。基础命令可以直接在本地靶场上跑:

sqlmap -u "http://127.0.0.1/dvwa/vulnerabilities/sqli/?id=1&Submit=Submit" --cookie="PHPSESSID=xxxxxx; security_level=low" --batch --dbms=mysql --dbs

这里的要点是三块:目标URL要带参数;cookie要带登录后的会话ID;batch参数让工具自动选择默认选项。如果你只给URL不给cookie,sqlmap会一直在登录页面打转,这也是新手最常见的困惑。

工具跑出来的结果,建议只当参考,不直接信结论。它的数据字典探测方式、注入技术选择,都可能漏掉某些特殊场景。手工的判断力才是真正能带走的资产,工具只是把这个判断过程自动化了。

8. 最后分享几个个人习惯

练SQL Injection这一关,我最大的体会是:不要追求"最快通关",要追求"每一个payload都能解释清楚为什么有效"。比如同样一条1' UNION SELECT 1,2 -- -,你能说出单引号的作用、union的前置条件、注释符的意义、1和2对应的是哪个回显位置,才叫真的会了。

还有一个习惯帮助很大:每换一个难度,先打开View Source,把防御代码和上一级别做一次对比。看到mysqli_real_escape_string,你要想到它防的是引号闭合,测数字型注入点就不用带引号;看到switch白名单,你要想到常规注入被封死,就把注意力转去盲注模块或者Cookie入口。这种"防御代码反推攻击手法"的思路,比背一百个payload都管用。

练习完毕之后,记得把本地环境里的数据重置一下,不要长期用test库乱造数据。真想验证某个技巧,开一个干净的靶场镜像练一遍,速度更快,状态也更可控。SQL注入这门手艺,关键不是会打DVWA,而是离开靶场后仍能保持对每一段SQL拼接的警觉。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询