☰
DVWA中级SQL注入绕过思路与实操指南
2026/10/10 3:10:40 网站建设 项目流程

2. 写在DVWA这面靶墙前:中级SQL注入到底在考什么

我接触过不少刚开始练Web安全的朋友,他们做完DVWA的低级SQL注入之后,会有一种“不过如此”的感觉——输入一个' or 1=1 --就能把用户表全拉出来,防护形同虚设。但真到了中级难度,卡住的人不在少数。原因很简单:你之前那把万能钥匙,被服务端做了一些处理之后,直接失效了。

DVWA的中级SQL注入,本质上是在模拟一种非常常见的开发失误——程序员知道要防注入,于是给输入加了过滤,但过滤本身写得不够严谨,留下了可绕过的空间。这种“知道要防,但没防到位”的场景,在真实业务里比比皆是。所以中级这关不是让你停留在“哇可以注入”的兴奋感里,而是逼着你思考:当条件变得苛刻,注入该怎么调整思路、怎么构造Payload、怎么判断你猜的逻辑对不对。

这篇文章我就以DVWA靶场的中级SQL注入模块为例,从原理、绕过的完整思路、手工注入步骤到SQLMap的使用,把整个链路捋一遍。无论你是刚接触注入的小白,还是想补一补绕过思路的初学者,这篇都能给你一个可以直接照着操作的路径。

注意:本文所有演示只针对DVWA这类本地靶场环境。未授权测试真实系统是违法行为,这点务必记住。

3. 搭建环境:把靶场跑起来再谈注入

3.1 DVWA是什么,以及为什么选它

DVWA(Damn Vulnerable Web Application)是一个基于PHP+MySQL的Web应用靶场,专门用来练习常见Web漏洞的利用与防御。它内置了SQL注入、XSS、CSRF、文件上传等多个漏洞模块,而且每个漏洞都设了 Low、Medium、High、Impossible 四个难度等级,方便你循序渐进地感受“同一种漏洞在不同防护下的表现差异”。

当前DVWA版本基于PHP环境运行,官方主要在Linux/Windows下的Apache/Nginx环境测试,实战中最常见的方式是用PHPStudy、XAMPP等集成环境或直接用Docker部署。我自己的习惯是用Docker,干净、可重复、出问题直接删掉重建,比在宿主机手动配环境省心太多。

3.2 快速部署步骤

如果你本机已经装了Docker,几分钟就能把DVWA跑起来:

docker run --rm -it -p 8080:80 vulnerables/web-dvwa

这里把容器的80端口映射到宿主机的8080。启动后浏览器访问http://localhost:8080,首次进入会要求初始化数据库,点底部的“Create / Reset Database”按钮,然后默认账号admin、密码password登录即可。

如果你用的是Windows且不想折腾Docker,用PHPStudy建一个网站目录,把DVWA源码丢进去,然后把config/config.inc.php.dist复制为config/config.inc.php并填上数据库账号密码,同样能跑。

3.3 把难度调到Medium并进入SQL注入模块

登录后,在DVWA左侧菜单找到“DVWA Security”,把安全等级选择为 Medium,并点击 Submit 提交。这时再进入左侧的“SQL Injection”模块,就是你这次要打的目标。

记住,中级关卡不会像低级那样把源码直接摆在页面上,但你完全可以去阅读源码。DVWA本来就是个教学工具,阅读源码恰恰是理解防护逻辑的最佳方式,比黑盒盲测高效得多。

4. 中级注入的防护逻辑:它到底做了什么拦截动作

4.1 和低级相比,中级多出来的是什么

拆开DVWA中级的源码,核心逻辑其实就两处变化。第一处是接收参数的方式变了——低级直接取$_GET['id'],中级改成了$_POST['id'],这意味着你之前直接在URL里修改?id=...的方式行不通了,参数得放在请求体里提交。第二处也是关键的一处,代码对输入值调用了一个函数做过滤。

这个函数是 PHP 的mysqli_real_escape_string()。它会把输入中的单引号'、双引号"、反斜杠\等特殊字符前面加上一个\转义,从而让这些字符失去原本在SQL语句中的语义。用通俗的方式解释:你提交一个1' OR '1'='1,经过转义后,单引号全部变成了\',原本可以闭合SQL语句中的引号并注入额外逻辑的地方,直接被“打上了封印”,低级注入那套直接在单引号处闭合的思路自然就断了。

从开发者视角看,这个函数是PHP里相当常规的数据库安全写法,很多项目都会用到。所以这个关卡的核心难点在于:既然单引号被转义了,注入该怎么继续?

4.2 破局点:为什么数字型注入能绕过转义

现在我们需要看清目标SQL语句到底长什么样。

DVWA中级SQL注入模块的查询语句大致是这样的:

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

注意变量$id没有加引号。这正是这道题被设计出来的核心考点——它是一个数字型参数的查询。数字型参数和字符型参数有个本质区别:数字型参数在SQL拼接时不需要用引号包裹,所以注入时也不需要闭合引号。

mysqli_real_escape_string()虽然转义了引号,但它的职责仅限于引号、双引号和反斜杠这类字符。数字型注入利用的根本不是引号,而是靠调整数字和运算符的优先级、使用 UNION、注释符等手段直接改变查询逻辑。转义函数对1 AND 1=1这种纯数字加上运算符的输入,完全无感。

你可以把它类比成一道安检门:它拦下了带刀具的人(引号),但没拦下带着扳手的人(数字、运算符、函数)。安检门确实升级了,但防的是上一关的武器,这一关换一种工具就能过。

4.3 中级和低级在判断思路上还有哪些区别

另外一个和低级不太一样的点是响应反馈。低级靶场会把查询结果直接显示在页面上,而中级虽然也显示结果,但它的响应内容在异常情况下会更“沉默”一些。比如你输入一个不存在的ID,可能页面没有明显的SQL报错提示,很多新手会因此误判“这里没有注入”,其实只是反馈机制不同而已。

判断是否存在注入不能只看报错,要看逻辑差异。比如输入1正常返回记录,输入1 AND 1=2时查询结果为空或不再显示记录,输入1 AND 1=1时又恢复显示,那基本可以确定这里存在可利用的注入点。这种基于逻辑差异的判断方式,在真实渗透中也远比依赖报错信息更可靠。

5. 手工注入:从探测到脱库的完整过程

5.1 搭好Burp Suite代理,先解决参数提交方式

在开始手工注入前,考虑到中级关卡改成了POST提交参数,我建议你在Burp Suite里操作。思路是这样:浏览器访问DVWA的SQL Injection页面,把Burp设为代理,然后随便输入一个ID点提交,Burp会抓到一条POST请求,请求体里是id=1&Submit=Submit。你之后在Burp的Repeater里修改id参数的值即可,这会比直接在浏览器里改参数方便得多,也能避开浏览器各种编码带来的干扰。

如果你刚接触Burp,只要记住一个操作:把请求发送到Repeater,然后在id=后面替换成你要测试的Payload,点Go发送,右侧看返回结果。就这么简单。

5.2 第一步:验证注入点存在

先用基本功测试——输入1,提交,页面返回User ID: 1以及对应的First name和Surname。

再输入1 AND 1=2。这里要注意,中级代码里对输入做过转义,但数字型参数不依赖引号,所以AND、=、数字这些内容都不会被过滤掉。如果页面没有返回任何数据,说明这个逻辑生效了。

然后输入1 AND 1=1,页面又恢复正常显示数据。到这里基本可以确认:后台查询语句是数字型拼接,且存在可用的注入点。

5.3 第二步:确认查询列数

注入点确认后,下一步是判断原查询的列数。列数判断常见有两种方式:ORDER BY和UNION SELECT。

ORDER BY的规则是:按第几列排序,如果指定的列数超过了查询实际返回的列数,数据库会报错。所以从1开始逐渐往上加:

1 ORDER BY 1 1 ORDER BY 2 1 ORDER BY 3

当我提交到1 ORDER BY 3时页面正常,提交1 ORDER BY 4时页面出现错误提示,说明原查询返回3列。这一步很关键,因为接下来的UNION注入要求你SELECT的列数必须与原始查询的列数一致,否则联合查询不成立。

5.4 第三步:定位可显示字段

知道列数是3之后,要摸清楚这3列里,哪些列的查询结果会显示在页面上。操作方式是用一个必定不存在的ID去触发UNION分支,让UNION SELECT后面的内容成为唯一的数据来源。

先构造:

-1 UNION SELECT 1,2,3

因为user_id正常不会是负数,所以前半段的原始查询结果为空,页面显示的就是UNION SELECT的结果。如果页面显示了First name: 2、Surname: 3,那就说明第2列和第3列的数据会直接回显到页面上。这两个位置就是你的“输出区”——后面所有想拿到的数据都放在这两个列里。

5.5 第四步:拿到当前数据库名

信息收集的顺序可以按“库名→表名→字段名→数据”来走。先测当前使用的数据库,用database()函数:

-1 UNION SELECT 1,database(),3

提交后,页面Surname位置直接回显dvwa。当前库名到手。

5.6 第五步:查表名

知道库名之后,从information_schema.tables里把dvwa库的所有表名都拖出来。information_schema是MySQL自带的元数据数据库,里面存了所有库、表、字段的注册信息,SQL注入拿表名基本都靠它。

-1 UNION SELECT 1,table_name,3 FROM information_schema.tables WHERE table_schema='dvwa'

中级关卡这时候有一个坑:table_schema='dvwa'带了单引号,之前说转义函数会处理单引号,这里不就撞枪口上了吗?

对,确实会。所以得换个思路:绕过引号限制的方式之一,是让被转义的单引号在数据库里重新“凑成”一个完整的字符串。我们可以把查询写成:

-1 UNION SELECT 1,table_name,3 FROM information_schema.tables WHERE table_schema=0x64767761

0x64767761是字符串dvwa的十六进制表示。MySQL里字符串和十六进制字面量是可以等价比较的,所以table_schema=0x64767761和table_schema='dvwa'效果完全相同,但前者完全没有用到引号。

提交后,页面上会列出dvwa库里的所有表。重点找users,因为这是存账号密码的表。

5.7 第六步:查字段名

表名拿到后,去information_schema.columns里找users表的字段:

-1 UNION SELECT 1,column_name,3 FROM information_schema.columns WHERE table_schema=0x64767761 AND table_name=0x7573657273

其中0x7573657273是users的十六进制表示。提交后,能看到user_id、first_name、last_name、user、avatar、password等字段。

这里就有一个非常经典的实操细节了:password字段里存的是MD5哈希,不是明文。早期DVWA的默认测试账号,密码password对应的MD5值是5f4dcc3b5aa765d61d8327deb882cf99,这个值在MD5在线查询库里是秒出的。你拿到哈希之后,可以在本地用哈希工具或在线库反查,就能还原出明文。

5.8 第七步:拖出账密数据

字段名到位,直接查数据:

-1 UNION SELECT 1,user,password FROM users

页面会把所有用户的用户名和密码哈希逐一显示出来。至此,第一次完整的中级SQL注入已经跑通。从判断注入点到脱库,每一步都有明确的目标和对应的验证方法,这就是手工注入需要掌握的基本功。

实操提示:中级靶场的输入框长度有限制,URL里如果Payload太长会被截断,但在Burp里修改POST参数则没有这个限制,建议全程以Burp为主操作环境。

6. 工具配合:用SQLMap快速打穿中级靶场

6.1 为什么手工一遍之后还要用工具

手工注入能让你理解漏洞本质,但真实场景下,数据量大了之后,手工操作的效率会很低。

举个例子,手工查表名、字段名,一步一行地敲Payload,遇到几百张表几百个字段的库,你要花大量时间才能把所有数据抓完。SQLMap的优势在于自动化:你只要给它一个疑似注入的请求,它会自动探测注入类型、识别数据库指纹、枚举库表字段,然后一键导出数据。它是业内使用频率最高的自动化注入工具,没有之一。

但我不建议新手一上来就跑SQLMap。原因很简单:SQLMap跑出来的注入类型和Payload,如果你完全看不懂背后的原理,实战中遇到一点偏差就抓瞎。手工打一遍,再用工具,工具输出的内容你才看得明白,排查问题也有了底子。

6.2 实际操作:抓包、存文件、跑命令

SQLMap可以直接接URL参数,但考虑到中级是POST请求,我推荐先把Burp里抓到的请求保存为文件,再用-r参数加载请求文件。这样做的好处是干净、可控,也方便复用。

在Burp的Repeater里,右键请求内容,选择 Copy to file,保存成dvwa.txt。文件内容大概是这样的:

POST /dvwa/vulnerabilities/sqli/ HTTP/1.1 Host: localhost:8080 User-Agent: Mozilla/5.0 Accept: */* Cookie: PHPSESSID=你的会话ID; security=medium Content-Type: application/x-www-form-urlencoded Content-Length: ... id=1&Submit=Submit

然后用SQLMap加载这个文件:

sqlmap -r dvwa.txt -p id --dbms=mysql --level=2 --risk=2

这里挨个解释一下参数的含义。-r指定请求文件;-p id指定要测试的参数,因为请求里可能有多个参数,让它测id就够了;--dbms=mysql表示指定目标数据库类型,可以加快探测速度,避免无意义的试探;--level=2 --risk=2表示提高测试深度和风险级别,中级靶场的过滤逻辑比较简单,但这个参数组合能覆盖更多Payload变体,对于理解工具行为也有帮助。

跑完探测后,依次枚举:

sqlmap -r dvwa.txt -p id --dbms=mysql --current-db sqlmap -r dvwa.txt -p id --dbms=mysql -D dvwa --tables sqlmap -r dvwa.txt -p id --dbms=mysql -D dvwa -T users --columns sqlmap -r dvwa.txt -p id --dbms=mysql -D dvwa -T users -C user,password --dump

--dump会自动把整个表的数据导出,并且如果列里有哈希值,它会调用破解模块进行自动匹配。实测下来,DVWA里那几个经典的MD5值是能直接解出来的。

一个容易踩坑的点:SQLMap在跑的时候,如果DVWA开启了安全等级变化(比如Cookie里security=medium,但页面实际等级被重置),可能导致Payload始终不成功。遇到这种情况,先确认浏览器的securityCookie值是否真的是medium。Cookie不对的话,可以直接在请求文件里改,或者用--cookie参数显式指定:

sqlmap -r dvwa.txt -p id --dbms=mysql --cookie="PHPSESSID=xxxx; security=medium"

6.3 工具和手工观察结果的配合

SQLMap的结果不会每次都和手工测试完全一致。比如它可能识别出多个注入类型,工具自动选取效率最高的。你如果手工已经确认了UNION注入可用,也可以强制SQLMap用UNION注入技术:

sqlmap -r dvwa.txt -p id --dbms=mysql --technique=U

--technique=U表示只使用UNION查询注入。这样做一方面能加快速度,另一方面也方便你观察工具的行为,读懂工具在当前注入点上是如何构造Payload的。

7. 中级关卡的常见问题与排查实录

7.1 为什么输入1 AND 1=2页面还是有数据

这种情况我见过不少次,多半是因为参数没有真正传到后台。中级关卡的参数是POST方式提交的,如果你直接改URL里的?id=,后台根本收不到,查的还是默认ID的记录。解决办法就是回到Burp,确认你修改的是POST请求体里的id参数。

另外检查一下浏览器的插件或代理,有些本地代理工具会拦截POST请求并缓存响应,导致你看到的永远是最早的返回结果。遇到这种情况,关掉代理直连再测一次,或者换无痕窗口。

7.2 输入带单引号的Payload,页面既不报错也不回显

这是转义函数生效的正常结果。当输入1'时,单引号被转义为\',在SQL语句里它就不再是引号,而是一个普通字符。所以语句不会报错,但也不会闭合出注入点。这时候应该放弃字符型注入的思路,转向数字型运算符和逻辑表达式。

还有一种情况:你的Payload里带了空格,而某些中间环节对空格做了URL编码处理,编码之后到达后端可能解析异常。在Burp里操作时,可以手动把无效Payload清空重写,而不是在旧Payload后面直接修改。

7.3 用ORDER BY测试列数时报错信息不明确

中级靶场默认情况下报错不会直接显示在页面上,需要目测响应长度的变化。你可以在Burp里看返回包Content-Length,或者手动对比页面是否出现错误提示,两种方式都可行。如果数据库返回的错误信息被页面吞掉了,试着在注入点后面加AND 1=1和AND 1=2对比响应差异,用逻辑变化代替报错信息来辅助判断。

7.4 SQLMap跑不出结果

首先检查--dbms是否指定正确。如果目标环境是MySQL但你忽略了指定,工具可能会在探测阶段浪费大量时间去尝试其他数据库的语法,而且对DVWA这种小靶场来说反而容易误判。其次检查Cookie会话是否过期,DVWA登录状态一般持续一段时间,如果PHPSESSID过期,工具发出去的请求会被重定向到登录页,它的检测就全部失效了。

另外要注意DVWA的Token机制。如果页面里带了user_token字段并且参与请求校验,SQLMap在重放请求时可能因为Token过期导致检测失败。DVWA中级的SQL注入通常没有启用Token,但如果你修改了安全等级或后续版本有变化,就要留意。

出现这个情况的排查思路:先拿出最原始的Burp请求文件,用浏览器手动提交一次确保请求本身是通的,确认请求文件里的Cookie还是有效的,再跑SQLMap。

7.5 复现时的常见误区汇总

我把中级SQL注入练习中容易踩的坑整理成一个速查表,方便你对照自检:

现象常见原因处理方法
低级的Payload失效中级使用POST提交+转义函数改用Burp提交POST参数,放弃引号闭合思路
输入带引号不报错引号被转义为普通字符改用数字型注入语法,如AND 1=1
十六进制比较无效大小写或编码错误先本地确认十六进制字符串正确,再替换
SQLMap无结果Cookie过期或请求文件不完整重新抓包,确认Cookie有效,显式指定参数
页面无数据但无报错查询结果为空但不回显错误用真假条件对比响应差异来辅助判断

8. 打完靶场之后:如何从注入走向防御

8.1 从注入点反推开发者该怎么修

在一个真实项目里,如果代码写成“接收用户输入、字符串拼接SQL、查询数据库”,那不管你是新手还是老手,都应该一眼看出问题。中级DVWA的代码就是典型反例。它虽然加了转义函数,但依然没有摆脱“外部输入直接进入SQL语句结构”这一根本问题。

正确的修复方式有几种,从最有效到次之排列:

第一是预处理语句,这是目前最推荐的方案。在PHP里用PDO或MySQLi的预处理机制,将SQL语句结构先发送给数据库服务器,之后再把参数作为纯数据传过去。数据库在编译阶段就已经确定了语句结构,无论参数里包含什么内容,它都只会被当作字符串字面量处理,不会参与SQL语法解析。

第二是使用参数化查询或存储过程,效果和预处理类似,核心思路都是实现“代码与数据分离”。第三才是输入校验和白名单过滤,比如数字型参数强制转为整数,去处所有非数字字符。这种方式虽然有效,但如果依赖它作为唯一防线,仍然存在被绕过的风险。

8.2 为什么转义函数不是银弹

mysqli_real_escape_string这类函数确实能拦掉一部分攻击,但它本质上是基于“黑名单”思维的防御,依赖的是对特殊字符的穷举。真实场景中,如果参数编码方式、数据库字符集、查询语法存在不一致,转义就有被绕过的可能。

更稳妥的思路是默认不信任任何用户输入,从架构层面让输入永远无法成为SQL语法的一部分。你可以把SQL注入类比成“把用户说的话当成命令执行”,转义函数是在命令里加引号让危险词失效,而预处理是直接把用户的输入提到“参数”专用通道里,和命令彻底隔离。哪个更安全,一目了然。

8.3 这块知识后面能延展到哪些方向

DVWA中级SQL注入只是Web安全众多练习路线中的一关。你把这个思路吃透之后,后面High级难度的挑战是它会强制使用cookie方式传递参数且不允许同时获得多个数据,需要你利用盲注技巧逐步逐字符地“猜”数据;Impossible级则完全使用预处理语句,从根本阻断注入。这个由易到难的设置,配合DVWA源码阅读,对理解“同一漏洞在不同防御下的攻防关系”价值非常大。

更往后,你可以把注入能力迁移到其他数据库类型上,比如Oracle、SQL Server、PostgreSQL,它们各自的元数据表结构、语法特性、注入技巧都不同。真实业务环境里数据库类型多种多样,掌握一种不代表掌握全部。

9. 一点实际操作的体会

这篇文章里的每一步,基本都有对应的踩坑时刻。我自己印象最深的一次,是在中级关卡的十六进制绕过那里卡了很久。当时只想着用单引号去闭合条件,完全没想到转义函数这回事,后来读源码才发现查询语句是数字型拼接,那一刻突然意识到:注入的关键不是“找特殊字符”,而是“读懂后台的SQL逻辑,再顺着它的逻辑设计Payload”。

按照这篇的步骤走一遍,你会得到一个很重要的手感:做注入测试,先别急着上工具,先在脑子里把后台SQL语句长什么样推断出来,再去决定Payload怎么写。工具能帮你省时间,但它不会替你想问题。

另外一个建议是,打DVWA的时候多对照源码。DVWA整个项目本身就是一本活教材,每一关都展示了一种具体的代码写法,以及这种写法对应的漏洞形态。把源码、页面表现、Payload三者对照着看,比闷头刷题高效得多。你把这个中级关卡彻底打通,再回头看那些“又被转义挡住了”的困惑,基本就都能解释清楚了。

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

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

立即咨询