
1. 项目概述从“万能密码”到真实攻击的认知升级如果你在网上搜索“SQL注入”大概率会看到一堆关于“万能密码”的段子比如在登录框输入 or 11就能绕过验证。这些例子虽然经典但过于简化容易让人产生一种错觉SQL注入就是输入一个神奇的字符串然后系统就“啪”地一下把门打开了。这种认知是危险的它让你只知其“形”不知其“神”更无法理解攻击者是如何一步步从外部渗透最终掌控整个数据库的。今天我们就彻底告别这种“玩具级”的认知。我将带你使用DVWADamn Vulnerable Web Application靶场的Low安全级别完整地重现一次真实的、有逻辑的SQL注入攻击过程。DVWA是一个专门为安全学习而设计的Web应用内置了从低到高的安全漏洞。选择Low级别是因为它完全模拟了早期或安全意识薄弱的开发者编写的代码对用户输入没有任何过滤和检查。这就像一扇完全没有锁的门我们可以清晰地看到攻击的每一个步骤和原理而不会被各种防御机制干扰。通过这次实操你将不再仅仅记住一个“万能密码”而是掌握一套完整的“黑客”思维如何探测漏洞、如何判断注入类型、如何一步步获取数据库结构、最终如何窃取核心数据。这对于开发者而言是理解漏洞成因、写出安全代码的最佳途径对于安全爱好者而言是入门Web安全的绝佳实战。整个过程我们将在本地搭建的DVWA环境中进行绝对安全、合法目的只有一个在攻击中学习防御。2. 环境准备与靶场搭建在开始“攻击”之前我们需要一个安全的实验环境。DVWA通常部署在PHPMySQL的环境中为了最大程度还原真实开发场景并简化搭建过程我推荐使用XAMPP或PHPStudy这类集成环境。这里以Windows系统下的PHPStudy为例它的优势在于一键启停自带PHP、MySQL和Apache/Nginx非常适合本地测试。2.1 基础服务部署首先从PHPStudy官网下载最新版本并安装。安装完成后启动PHPStudy确保Apache和MySQL服务都显示为绿色“运行中”状态。默认情况下你的Web根目录位于PHPStudy安装目录下的WWW文件夹。接下来我们需要获取DVWA的源码。你可以从其官方GitHub仓库下载ZIP包解压后将整个dvwa文件夹复制到WWW目录下。此时通过浏览器访问http://localhost/dvwa就应该能看到DVWA的安装引导页面了。注意很多新手在这一步会遇到访问权限或端口冲突问题。如果无法访问请首先检查PHPStudy的端口配置默认Apache是80MySQL是3306确保没有其他程序如IIS、Skype占用。如果80端口被占可以在PHPStudy的设置中修改Apache端口为8080然后通过http://localhost:8080/dvwa访问。2.2 DVWA核心配置访问DVWA页面后你会看到一个红色的提示要求你配置config/config.inc.php文件。我们找到dvwa/config目录会发现一个config.inc.php.dist文件这是配置文件模板。将其复制一份重命名为config.inc.php然后用文本编辑器打开。关键的配置项是数据库连接信息$_DVWA[ db_server ] 127.0.0.1; $_DVWA[ db_database ] dvwa; $_DVWA[ db_user ] root; $_DVWA[ db_password ] pssw0rd;这里需要根据你的PHPStudy的MySQL设置进行调整。PHPStudy的MySQL默认密码通常是root。你需要将$_DVWA[ db_password ]的值改为root。同时确保$_DVWA[ db_user ]是root。修改保存后刷新DVWA的安装页面。此时点击页面下方的“Create/Reset Database”按钮。这个操作会执行一个SQL脚本在MySQL中创建名为dvwa的数据库并插入必要的测试数据包括我们等下要攻击的users表。如果一切顺利页面会提示数据库创建成功。然后使用默认凭证登录用户名admin密码password。2.3 关键安全等级设置登录成功后在左侧菜单找到“DVWA Security”选项。这是DVWA的核心特性之一它允许你动态调整整个应用的安全级别从而模拟不同强度的防御措施。我们将安全级别设置为“Low”。这个级别对应的源码文件如vulnerabilities/sqli/source/low.php完全没有任何输入过滤是学习注入原理最纯净的“标本”。实操心得在实验过程中我强烈建议你保持浏览器开发者工具F12打开的网络Network标签页处于开启状态。这样你提交的每一个请求、携带的每一个参数都能被清晰地记录下来。这对于理解HTTP请求的构造和后续的漏洞利用至关重要。同时准备一个简单的文本编辑器如VS Code、Notepad来临时记录你构造的Payload攻击载荷会大大提高效率。3. SQL注入攻击原理深度拆解在动手之前我们必须把原理吃透。SQL注入之所以发生其根源在于“将用户输入的数据直接拼接到了SQL查询语句中并且没有对输入进行任何合法性校验或转义处理”。3.1 从一段危险代码说起让我们直接看DVWA Low级别下SQL注入模块的源代码核心片段位于vulnerabilities/sqli/source/low.php$id $_REQUEST[ id ]; $query SELECT first_name, last_name FROM users WHERE user_id $id;;这段代码的逻辑非常简单从用户请求中获取一个名为id的参数然后将其直接放入SQL查询语句的字符串中。假设用户正常输入1那么最终执行的SQL语句是SELECT first_name, last_name FROM users WHERE user_id 1;这没有任何问题。但是如果用户输入的不是一个数字而是一段精心构造的字符串呢比如输入1 or 11。我们把它代入到查询语句中看看SELECT first_name, last_name FROM users WHERE user_id 1 or 11;原本的查询意图是“查找user_id等于某个值的用户”。现在WHERE子句变成了user_id 1 or 11。在SQL逻辑中or表示“或”只要两边有一个条件为真整个条件就为真。11是一个恒为真的表达式。因此这个查询的条件变成了“user_id等于1或者1等于1”。由于“1等于1”永远成立这个查询将返回users表中的所有记录。这就是“万能密码”或“万能查询”的原理。3.2 注入类型的判断字符型 vs 数字型理解注入类型是构造有效Payload的第一步。核心判断方法是看用户输入的值在SQL语句中是否被引号包裹。字符型注入如果源码中查询语句像WHERE user_id $id参数被单引号包裹那么就是字符型注入。我们的Payload必须考虑如何“闭合”这个引号。DVWA Low级别的SQL注入就是典型的字符型注入。数字型注入如果查询语句像WHERE user_id $id参数没有被引号包裹则是数字型注入。Payload构造相对简单通常不需要处理引号。在实战中如果看不到源码如何判断一个简单的方法是输入一个单引号。如果页面返回了SQL语法错误如You have an error in your SQL syntax...这强烈暗示存在字符型注入因为我们的单引号破坏了原语句的引号闭合。如果输入单引号后页面正常但输入and 12这类逻辑假条件时页面内容消失或异常则可能是数字型注入。3.3 攻击者的核心目标与步骤一次完整的SQL注入攻击远不止返回所有数据那么简单。一个有经验攻击者的思路是系统性的其目标通常是逐步获取整个数据库的控制权。这个过程可以抽象为以下几个步骤探测与确认确认存在注入点并判断注入类型。信息搜集获取数据库的字段数、当前数据库名、数据库版本等信息。结构探查列出数据库中的所有表然后找出感兴趣的表如users,admin的字段结构。数据窃取从目标表中提取敏感数据如用户名、密码哈希、个人信息等。权限提升与后续操作在更高阶的攻击中尝试写入文件获取Webshell、执行系统命令等。下面我们就严格按照这个思路在DVWA Low级别上走一遍完整的流程。4. 实战逐步攻破DVWA Low级别SQL注入确保你已经登录DVWA并将安全级别设置为Low然后导航到“SQL Injection”模块。你会看到一个简单的输入框提示你输入User ID。4.1 第一步探测漏洞与确认注入点首先我们进行最基本的探测。在输入框中提交一个单引号然后点击“Submit”。预期结果页面应该会显示一个SQL语法错误。在DVWA中错误信息可能直接回显在页面上例如包含You have an error in your SQL syntax...的字样。这说明我们输入的单引号破坏了SQL语句的语法应用程序没有处理这个错误直接将其展示了出来。这不仅是漏洞存在的确凿证据也告诉我们这是一个“错误回显型”注入点这非常有利于我们后续的利用因为错误信息本身可能泄露数据库结构。注意事项在实际的渗透测试中很多网站会关闭错误回显这时页面可能只是空白、跳转或返回一个通用的错误页。这就需要使用“盲注”技术通过观察页面返回内容的真假、时间延迟等差异来判断注入是否成功。DVWA的Low级别给了我们最理想的学习环境。4.2 第二步判断字段数量与回显位置确认注入点后我们需要知道当前查询的SQL语句大概查询了多少个字段列以及哪个字段的内容会显示在页面上。这需要使用ORDER BY和UNION SELECT语句。1. 使用ORDER BY猜解字段数ORDER BY子句用于根据指定列排序结果。如果ORDER BY 5表示按第5列排序但如果查询结果只有3列数据库就会报错。我们可以利用这个特性来猜解字段数。 在输入框中依次尝试1 order by 1 # 1 order by 2 # 1 order by 3 ##在MySQL中是注释符它会注释掉原SQL语句中我们输入后面的单引号以及后续内容确保语句不会因为引号不闭合而报错。当你尝试1 order by 3 #时页面很可能会报错。而1 order by 2 #时页面正常显示可能显示ID为1的用户信息也可能因为1条件不匹配而显示空但只要不报错就行。这说明原查询语句只查询了2个字段。2. 使用UNION SELECT确定回显点UNION操作符用于合并两个SELECT语句的结果集。要成功使用UNION前后两个SELECT语句必须拥有相同数量的列。现在我们知道了是2列。 输入1 union select 1,2 #这个Payload的意思是先执行原查询SELECT first_name, last_name FROM users WHERE user_id 1然后联合查询我们构造的SELECT 1,2。如果注入成功且页面正常显示我们就能在页面上看到数字“1”和“2”出现的位置。这两个数字就是“回显点”意味着我们可以将想要查询的数据替换到这两个位置上并显示在页面上。在DVWA中执行上述语句后你可能会发现页面只显示了ID为1的用户信息并没有显示“1”和“2”。这是因为原查询WHERE user_id 1成功查询到了一条记录admin用户而UNION查询的结果被放在了后面。数据库默认只返回了第一行结果。为了让我们的UNION SELECT结果显示出来我们需要让原查询的结果为空。修改Payload为999 union select 1,2 #user_id999在数据库中大概率不存在因此原查询部分返回空结果集。这样页面就会显示我们UNION查询的结果“1”和“2”。记下它们显示的位置比如“First name”处显示了“1”“Surname”处显示了“2”。4.3 第三步获取数据库信息现在我们有了回显点位置1和2就可以开始提取数据库的元信息了。数据库的元信息如库名、表名、列名通常存储在名为information_schema的默认数据库中。1. 获取当前数据库名使用database()函数。999 union select database(),2 #将database()放在第一个回显点替换之前的“1”执行后在“First name”位置就会显示当前连接使用的数据库名称应该是dvwa。2. 获取数据库版本和用户这有助于判断数据库类型和权限。999 union select version(),user() #version()返回MySQL版本user()返回当前数据库用户。你可能会看到类似5.7.36和rootlocalhost的信息。root用户意味着拥有最高权限这在实际攻击中是非常危险的信号。4.4 第四步探查数据库表结构知道了数据库名dvwa接下来我们要看看里面有哪些表特别是存放用户凭证的表。1. 列出所有表名information_schema.tables表存储了所有表的信息。999 union select 1,group_concat(table_name) from information_schema.tables where table_schemadatabase() #这里用到了group_concat()函数它可以将多行结果合并成一个字符串方便我们一次性查看所有表名。执行后你会在回显点看到类似guestbook,users的结果。显然users表就是我们的主要目标。2. 列出users表的所有列名information_schema.columns表存储了所有列的信息。999 union select 1,group_concat(column_name) from information_schema.columns where table_nameusers #执行后你会得到一个包含多个列名的字符串例如user_id,first_name,last_name,user,password,avatar,last_login,failed_login。其中user或可能是username和password列就是我们最关心的敏感信息列。4.5 第五步窃取核心数据用户密码现在表名、列名都知道了我们可以直接查询数据了。1. 一次性提取所有用户和密码999 union select group_concat(user), group_concat(password) from users #这个语句从users表中查询所有行的user和password列并用group_concat()合并显示。点击提交后你会在页面上看到所有用户名和对应的密码哈希值一串由MD5加密的32位字符串。在DVWA的默认数据中你会看到admin对应的密码哈希是5f4dcc3b5aa765d61d8327deb882cf99。你可以用任何在线的MD5解密网站尝试破解或者你知道的这个哈希值对应的明文就是password。这直观地展示了攻击的后果攻击者无需知道你的明文密码只要获取了密码哈希就可以尝试离线破解对于弱密码非常有效或者在另一些允许传递哈希值进行认证的系统中直接使用Pass-the-Hash攻击。2. 更精细的数据提取你也可以不一次性提取所有数据而是进行条件查询例如只查询admin用户999 union select user, password from users where useradmin #或者将多个字段更清晰地组合显示999 union select concat(user_id, : , first_name, , last_name), password from users #至此我们已经完成了一次完整的、从信息探测到数据窃取的SQL注入攻击。你不再是只会输入 or 11的新手而是一个理解了攻击链每一步原理的实践者。5. 漏洞根源分析与防御思想在成功“攻击”之后我们必须回过头来分析漏洞产生的根本原因并思考如何防御。这才是我们进行靶场练习的终极目的。5.1 Low级别漏洞代码审计我们再回顾一下Low级别的关键代码$id $_REQUEST[ id ]; // 直接接收用户输入无任何过滤 $query SELECT first_name, last_name FROM users WHERE user_id $id;; // 直接拼接字符串问题一目了然未验证输入程序完全信任用户输入的id值没有检查它是否为预期的数字或格式。直接字符串拼接将用户输入直接嵌入到SQL字符串中这是万恶之源。错误信息回显将数据库的详细错误信息直接展示给用户die( pre . mysqli_error(...) . /pre )这为攻击者提供了极大的便利。5.2 层层递进的防御方案防御SQL注入核心原则就是“永不信任用户输入严格区分代码与数据”。1. 初级防御输入验证与转义白名单验证如果id必须是数字那么在拼接前就用is_numeric()函数检查非数字直接拒绝。这是最有效的方法之一。转义特殊字符使用mysqli_real_escape_string()函数对输入中的特殊字符如单引号、双引号、反斜杠进行转义。这样用户输入的单引号会被转义为\从而成为字符串的一部分而不会破坏SQL语法。DVWA的Medium级别就采用了这种方法。但这种方法并非绝对安全在某些特定编码或二次注入场景下可能被绕过。2. 终极防御使用参数化查询预编译语句这是目前公认最有效、最根本的防御手段。它的原理是将SQL语句的结构代码和传递的数据分开处理。使用PDOPHP Data Objects$stmt $pdo-prepare(SELECT first_name, last_name FROM users WHERE user_id :id); $stmt-execute([id $id]); $results $stmt-fetchAll();使用MySQLi$stmt $conn-prepare(SELECT first_name, last_name FROM users WHERE user_id ?); $stmt-bind_param(i, $id); // i 表示参数是整数类型 $stmt-execute(); $result $stmt-get_result();在预编译过程中数据库会先解析SELECT ... WHERE user_id ?这个语句模板确定其结构。之后无论$id传入什么值哪怕是1 or 11数据库都只会将其视为一个纯粹的数据而不会将其作为代码的一部分来解析。这就彻底切断了注入的可能性。DVWA的Impossible级别就采用了PDO预编译语句并结合了CSRF Token、严格的输入类型检查intval和结果数量检查构成了铜墙铁壁。3. 最小权限原则 为Web应用连接数据库的账户分配最小的必要权限。例如只授予其SELECT权限不授予DROP、UPDATE、INSERT或FILE等权限。这样即使发生注入危害也能被限制在有限范围内。4. 关闭错误回显 在生产环境中务必关闭PHP向浏览器显示数据库错误信息的设置如display_errors Off改为将错误记录到日志文件中避免向攻击者泄露敏感信息。6. 常见问题与排查技巧实录在实战演练中你可能会遇到一些意想不到的问题。这里我记录了几个常见的“坑”及其解决方法。问题1输入Payload后页面空白或没有任何变化。可能原因1Payload语法错误。仔细检查你的SQL语句特别是引号闭合和注释符。在Low级别中字符型注入必须用单引号闭合并用#或--注意后面有个空格注释掉后续部分。确保没有拼写错误。可能原因2安全级别未设置为Low。这是最常见的原因请务必再次确认DVWA Security级别是“Low”。Medium和High级别引入了过滤和转义Low级别的Payload会失效。排查技巧打开浏览器开发者工具的“网络(Network)”标签查看提交请求后服务器的响应。如果响应是302重定向到了登录页可能是你的DVWA会话过期了需要重新登录。问题2使用union select时页面只显示了数据库错误没有显示我们想要的数据。可能原因UNION前后查询的列数不一致。你必须确保UNION SELECT后面的列数与原查询的列数完全相同。这就是为什么我们要先用ORDER BY来猜解字段数。如果你猜的字段数是3但实际是2就会报错。排查技巧从order by 1开始逐步增加数字测试直到页面报错那么字段数就是上一个成功的数字。问题3知道字段数是2但union select 1,2执行后页面上看不到“1”和“2”。可能原因原查询条件WHERE user_id ...匹配到了数据数据库返回了第一行结果原查询结果而UNION查询的结果在第二行之后没有被显示出来。解决方案让原查询条件不返回任何结果。就像我们之前做的使用一个数据库中不存在的ID例如999 union select 1,2 #。问题4在获取表名或列名时group_concat返回的结果不完整或被截断。可能原因group_concat()函数有一个默认的最大长度限制group_concat_max_len变量。如果结果超过这个长度就会被截断。解决方案可以尝试在查询前先修改这个会话变量999 union select 1,group_concat_max_len #查看当前长度然后用999; SET SESSION group_concat_max_len 10240; --这样的语句来临时增大它注意这里用了分号执行多条语句需要数据库支持且未禁用。更简单的方法是在Payload中不用group_concat而是直接查询limit 0,1来一条条查看。问题5所有步骤都正确但就是无法注入成功。终极排查流程确认环境PHPMySQL服务是否正常运行DVWA数据库是否重置成功确认级别DVWA安全级别是否为Low确认注入点输入单引号是否报错这是最基本的探测。查看请求使用Burp Suite或开发者工具确认你提交的Payload确实被原封不动地作为id参数发送到了服务器。有时前端JavaScript可能会进行一些初步的校验或编码。模拟请求你可以尝试直接在URL中构造Payload进行GET请求因为Low级别同时支持GET和POST例如访问http://your-dvwa-site/vulnerabilities/sqli/?id1SubmitSubmit这样更直观。7. 从Low到High安全升级的对抗演进在DVWA中将安全级别从Low调到Medium和High你可以直观地看到防御措施如何层层加码以及攻击者如何调整策略进行绕过。这就像一场精彩的攻防博弈。Medium级别前端将输入框改成了下拉菜单但更重要的是后端代码使用了mysqli_real_escape_string()对输入进行了转义。这防御了Low级别的简单字符注入。但攻击者可以通过抓包工具如Burp Suite修改POST请求绕过前端限制。并且由于转义了引号数字型注入可能依然有效如果参数是数字型且未强制类型转换。此外攻击者还可以利用十六进制编码如将users编码为0x7573657273来绕过对引号的过滤。High级别引入了LIMIT 1和会话管理。注入点移到了一个单独的弹出页面并且每次查询都限制只返回一行结果。这增加了利用难度但通过使用UNION SELECT并结合#注释掉LIMIT 1攻击仍然可以进行。这说明了单一防御措施的局限性。Impossible级别这里展示了最佳实践。它综合运用了参数化查询PDO从根本上杜绝注入。严格的输入类型检查is_numeric()和intval()确保输入只能是数字。CSRF Token防止攻击通过伪造请求发起。结果数量检查只当查询结果恰好为1条时才输出防止数据批量泄露。通过对比这四个级别你能清晰地建立起一个认知安全是一个体系而不是一个单点功能。从开发的第一行代码开始就需要将安全思维融入其中。对于开发者最应该养成的习惯就是只要涉及数据库操作无脑使用参数化查询预编译语句。这是成本最低、效果最好的安全投资。我个人在多年的开发和安全评估中最大的体会是绝大多数SQL注入漏洞并非源于高深的技术而是源于最基本的习惯缺失。当你亲手在DVWA上从注入探测一路走到数据窃取你就能切身体会到一个看似微小的输入框如果处理不当背后可能连接着整个数据库的“宝藏”。这种震撼远比阅读十篇理论文章来得深刻。希望这次手把手的实战能让你彻底告别对“万能密码”的肤浅理解真正踏入Web安全实战的大门。下次当你编写一个SQL查询时你一定会下意识地想起今天这个靶场然后默默地写下prepare和execute。