
做安全测试的人基本都会接触靶场DVWA 差不多是入门阶段绕不开的一个。它把常见的 Web 漏洞做成了一个个独立关卡其中 SQL Injection 是最容易出效果、也最适合用来理解注入原理的部分。这篇就把我在本地环境里过 DVWA SQL Injection 全流程的笔记整理出来从环境准备、Low 等级的手工注入一直写到 Medium 和 High 等级的防御思路比对最后聊点真实修复经验。适合刚开始学 Web 安全的同学也适合已经知道点概念但没完整跑过一遍注入流程的人参考。1. 靶场部署与关卡定位1.1 DVWA 为什么适合练注入DVWA 全称是 Damn Vulnerable Web Application翻译过来就是“故意做得漏洞百出的 Web 应用”。它本身是一个跑在 PHP 和 MySQL 上的网站内置 SQL Injection、XSS、CSRF、文件上传这些常见漏洞类型。每一关都提供 Low、Medium、High、Impossible 四种安全等级等级变化模拟了不同强度的开发防护让你从最粗暴的注入一路看到参数化查询等修复方案。选它练 SQL 注入有个很实际的好处你能同时看到漏洞代码和正确的代码写法而且是同一套业务逻辑的对比。比如同样是根据用户 ID 查资料Low 等级直接拼接字符串High 等级就走预编译两种代码放在一个页面里对照比单纯看文档理解得深得多。1.2 本地环境怎么搭我当时的搭建方式是 PHP 环境加 MySQL把 DVWA 源码解压放到本地 Web 目录然后配置config/config.inc.php里的数据库连接信息。DVWA 依赖的 PHP 环境比较常规一般集成环境就能满足MySQL 负责存取数据。启动之后访问首页底下会先做环境检查数据库、PHP 扩展这些没问题就能点 “Create/Reset Database” 初始化。初始化会生成几个用户默认账号密码是 admin/password。登录后左侧菜单就是各个漏洞关卡我们这次只盯 SQL Injection。右侧有个 Security Level 下拉框我把等级先切到 Low准备从最简单的状态开始测。顺便说一句DVWA 每关页面下面都有 “View Source” 按钮点开能看到对应等级的实际 PHP 代码这对理解注入点是极好的辅助。1.3 第一眼的信息收集进入 SQL Injection 页面界面很简单就是一个输入框加一个提交按钮提示 “User ID”。随便填一个数字 1 提交页面会返回这个用户的姓和名。这说明后台有一条按用户ID查资料的查询返回结果直接显示在页面上。此时要做的不是急着上 payload而是先想清楚两个事输入框的提交方式是 GET 还是 POST影响拼 payload 的位置。返回结果里哪些内容是我们可控位置对应的回显比如用户名、姓、名。Low 等级下这个输入框是 GET 参数提交后 URL 里会带上id1之类的参数。后台代码大概是这样的逻辑$id $_GET[id]; $query SELECT first_name, last_name FROM users WHERE user_id $id;;这是最经典的字符串拼接写法。用户输入被直接塞进单引号包裹的 SQL 语句中这就是后面所有注入操作的入口。2. Low 等级手工注入的完整链路2.1 从单引号报错到布尔判断在 ID 输入框中输入1并提交页面大概率会报 SQL 语法错误或者出现类似 “You have an error in your SQL syntax” 的信息。这个报错很有价值它说明我输入的单引号确实参与了解析打破了原有 SQL 的结构也就是说输入点没有经过有效过滤和转义。接着可以用布尔条件的思路验证。输入1 AND 11页面正常返回用户 1 的信息因为整个条件为真再输入1 AND 12页面没有返回结果因为条件为假。同一个参数改了判断条件就影响返回结果这进一步确认了后台的 SQL 语句里正在执行我拼接进去的逻辑。这一步里有个容易忽略的细节为什么要用AND 11而不是直接在后面加OR 11因为原 SQL 里user_id $id的最后一个单引号必须被合理闭合。我输入1 AND 11后整个语句变成SELECT first_name, last_name FROM users WHERE user_id 1 AND 11;注意我输入的1闭合了原始的第一个单引号然后用AND 11补了一个永远为真的条件表达式。理解这个“闭合”概念后面做 UNION 注入就顺理成章了。2.2 用 ORDER BY 数出字段数量注入攻击要高效回显数据最常用的是 UNION SELECT但 UNION 的两边字段数量必须一致。所以先要确定原始查询到底查了几个字段。方法是输入1 ORDER BY 1-- #如果正常返回说明原始查询至少可以按第 1 列排序。然后把数字 1 改成 2、3、4依次尝试。当尝试某个编号时页面报错就说明原始查询只有那么多字段。这里-- #是注释符作用是把原始 SQL 语句后面的内容全部注释掉避免原有的单引号干扰我们构造的语句。我实测时在 Low 等级下这条链路走得很顺字段数量是 2说明原始查询为两个字段正好对应输入框下方显示的 first_name 和 last_name。2.3 UNION SELECT 探测回显位置知道了字段数量下一步是用 UNION SELECT 构造一个“假查询”把想要的数据填进字段位。先输入1 UNION SELECT 1,2-- #如果页面返回First name: 1和Surname: 2说明第一个字段和第二个字段都被原样回显我们可以直接把真实数据放到某个字段位上。我习惯先放两个数字进去试试好处是能明显看出渲染位置避免把一个字段误当成另一个字段。确认回显位置后把数字替换成要读取的内容即可。比如要读用户名和密码可以输入1 UNION SELECT user, password FROM users-- #这样后台执行的结果会同时包含原查询返回的第一条记录和 users 表中的所有账号记录页面会把它们全部列出来密码字段是经过哈希处理的一串值。到这里Low 等级的核心流程就跑通了。3. 利用信息模式扩展读取范围3.1 先知道当前在哪个库Low 等级可以直接UNION SELECT database(), 1把当前数据库名称显示出来。我这里的库名是dvwa之类这会成为后面查询表结构的关键条件。题目环境里有一个非常实用的特性information_schema数据库MySQL 默认自带它存了所有数据库的表、字段、索引等元数据。我们常用的表是information_schema.tables和information_schema.columns。3.2 metadata 查询流程先查当前库里有哪些表1 UNION SELECT table_name, table_schema FROM information_schema.tables WHERE table_schemadvwa-- #这样能看到多个表名。因为字段只有两个我尝试把多个表名合并显示比如1 UNION SELECT table_schema, group_concat(table_name) FROM information_schema.tables WHERE table_schemadvwa GROUP BY table_schema-- #group_concat会把查出的多行拼成一整行显示方便在一个回显位置里读完多个数据。接着还要确定某个表中的字段名对 users 表执行1 UNION SELECT column_name, 1 FROM information_schema.columns WHERE table_nameusers-- #这里只能查看当表格有多少列——实际上受限显示。重要的是把这个思路记下来注入的本质就是把我们想要查询的 SQL 片段拼到正常查询中information_schema提供了拿到“数据库结构”的路径先表名、再字段名、最后取数据是一个递进过程。我用这种递进方式拿到了 user_id、first_name、last_name、user、password 等字段名这一步只依靠页面回显不需要任何图形化工具。3.3 密码哈希的处理读取 password 字段时显示的不是明文而是一串 MD5 哈希值。这是因为 DVWA 的 users 表里默认用 MD5 存密码哈希这本身就是一个值得警惕的安全教训即使防住了注入如果数据库被拖走弱哈希密码也经不起离线破解。得到哈希后可以用在线破解站或本地的哈希破解工具撞库还原。比如21232f297a57a5a743894a0e4a801fc3是 admin 对应 MD5 值这类目录里通常几秒就能查出明文。这里我能成功破解是因为 DVWA 使用了弱哈希且密码是常见的弱密码。这个流程提醒我们正是这种弱哈希型存储配合注入读取才形成了完整的攻击链。4. Medium 与 High防护升级后的实测对比4.1 Medium 的入门式防御与绕过测试把 Security Level 切到 Medium 再回到 SQL Injection 页页面的提交方式通常转为 POST从 URL 参数变成了请求体参数。后台增加了类似mysqli_real_escape_string的转义逻辑对输入中的单引号、双引号、反斜杠做处理看起来像是过滤注入。但实测时会发现转义不等于安全。因为我注入的 payload 不一定非要用单引号。当原始 SQL 对user_id的处理是$query SELECT first_name, last_name FROM users WHERE user_id $id;;输入1 OR 11直接就能让条件恒真因为这里没有单引号包裹数字属于裸拼接入mysql_real_escape_string对普通数字字符毫无拦截力。即使原始 SQL 有单引号也可以用其他编码技巧尝试绕过。Medium 的设计目标就是让你明白转义需要全局统一地做一旦处理不彻底绕过只是时间问题。我做了一个小实验在 Medium 等级下输入1 AND 11能返回用户数据输入1 AND 12则无结果说明布尔类的注入依然可行。这个现象很关键因为它点出了数字型参数在转义防护下的天然脆弱性。4.2 High 等级的参数化查询防线把等级切到 High再看后台代码会发现查询方式变成了参数绑定$query SELECT first_name, last_name FROM users WHERE user_id ?;这类预编译语句把 SQL 结构和数据分离输入值无论包含什么特殊字符都会被当作纯数据处理而不是 SQL 指令来执行。此时再往输入框填1或1 OR 11页面都是正常的通配处理不会出现报错也不会产生额外的查询结果。从我实测的角度看High 等级下基本没法再用前面 Low 的那套注入手段因为根本没有拼接可借力。数据库访问参数化后作为上文中的拼接型攻击会瞬间失效这是从源头解决 SQL 注入的惯用手段也是现实中大多数正规业务系统的推荐写法。High 里还有一道 Impossible 等级它在参数化的基础上加上更多安全头部和逻辑校验属于锦上添花的纵深防御。4.3 三档防御的对照把这个过程整理成一张对照表能直观看出不同防御思路的效果差异安全等级后台处理方式实际效果Low直接字符串拼接 SQL无任何过滤标准注入流程可完整走通Medium对输入做特殊字符转义看似有防护数字型注入、部分闭合绕过仍能成功High参数绑定 / 预编译语句拼接类注入基本失效数据与代码分离Impossible参数绑定加二次校验和白名单注入面被压缩到极小几乎无利用可能对比完你会发现SQL 注入的修复核心不在“用户输入是否被过滤”而在“SQL 结构是否和数据隔离”。转义容易有漏洞参数化查询才是更可靠的根基。5. 从靶场回归真实开发修复5.1 修复示例把拼接改成参数绑定前面提到 High 等级用的是参数绑定这里给出一个真实开发中常见的 PHP 修复示例。原来的高危写法是$query SELECT first_name, last_name FROM users WHERE user_id . $_GET[id] . ;;修复后应改成$stmt $pdo-prepare(SELECT first_name, last_name FROM users WHERE user_id ?); $stmt-execute([$_GET[id]]); $rows $stmt-fetchAll();用 PDO 的预处理机制把用户 ID 作为参数传给数据库引擎引擎在执行前就确定了 SQL 语义无论传什么值都只影响数据不改变查询结构。这条修改简单到一句话能说完但实际项目里很多遗留代码还停在地着字符串拼接的阶段尤其是旧系统改造时很容易遗漏。5.2 别的几条防御层次除了参数化查询我还会建议在后端做几层加固这些是我从靶场练习里得出的实际操作感受为数据库账号设置最小权限。应用账号只允许查询必要的表和字段无权限就不该让应用获得它写死之外的读表能力这样即便出现注入能读取的数据面也会被限制在很小的范围内。对输入做严格类型校验。比如用户 ID 既然是数字就先执行(int)或正则只允许数字大多数注入 payload 在类型校验这一层就被拦掉了。不要在生产环境保留前端调试信息。Low 等级能看到完整 SQL 报错真实业务一旦报错页面泄露语法细节攻击者的每一步试探就有了重要反馈。关闭调试把错误信息统一记录到日志。5.3 边界意识与适用范围必须强调这些注入和绕过技巧只能在你自己搭建的实验环境或者有明确授权的安全测试项目中使用。DVWA 本身就是教学靶场用它练习的目地是理解攻击原理反过来才能知道防御时该防在哪里。我在跑完整个流程后的最大体会是SQL 注入之所以常年排在漏洞榜单前几位不是因为它高深而是因为很多系统里确实存在低级的拼接代码。靶场里的 Low 等级正是对这种高危写法的放大演习。能从回显、报错、布尔差异里读出后台 SQL 结构和数据的关联性才算真正学懂注入的原理。最后分享个小技巧自己在做这种靶场练习时尽量别一上来就搭自动化工具先手工把 Single Quote、ORDER BY、UNION SELECT 这套流程走一遍。工具能帮你拿结果但手工过程能帮你建立对查询结构和执行逻辑的感觉。这种感觉在后续排查真实业务漏洞时会派上大用场。