1. 从一次登录框测试说起:暴力破解到底在破什么
很多人第一次接触DVWA的暴力破解模块,注意力都放在“怎么把密码跑出来”上,结果跑通一次之后,脑子里留下的只有Burp的Intruder界面和一堆字典文件,换个环境就不知道从哪下手了。我自己早期也是这样,直到有一次在模拟环境里对着一个登录接口折腾了大半天,才发现真正值得琢磨的不是“跑字典”这个动作,而是这个登录框在什么条件下会泄露信息、在什么条件下会拦截你、以及不同防护级别下攻击者的策略要怎么调整。
DVWA的Brute Force模块就是为这件事准备的。它本质上是一个极简的登录表单,只有username和password两个字段,背后连着一张用户表。但DVWA把它做成了四个难度级别——Low、Medium、High、Impossible,每个级别对应一种典型的防护思路。这四个级别串起来看,几乎就是一部“登录接口防护演进史”:从毫无防护,到参数过滤,到延迟响应,再到令牌加锁定。你把这四个级别吃透,再回头看真实业务里的登录逻辑,会发现大部分设计都能在里面找到影子。
这篇文章适合两类人:一类是刚学Web安全、想搞明白暴力破解原理和工具用法的初学者;另一类是做开发或测试、想搞清楚“登录接口到底该怎么防”的从业者。我会按级别逐个拆,每个级别讲清楚三件事——服务端实际做了什么、攻击者会怎么应对、这个设计为什么挡不住或者能挡住。工具上主要用Burp Suite的Intruder,也会提到用Python脚本做自动化,因为有些级别用脚本比用图形化工具更直观。
需要提前说明的是,DVWA本身是一个刻意设计成有漏洞的练习环境,所有操作都应该在本地搭建的靶机里进行。下面涉及的所有请求、参数、脚本,都只针对DVWA这个模拟系统,目的是理解防护原理,不要往任何真实系统上套。
2. Low级别:一个没有任何防护的登录框长什么样
2.1 服务端代码到底做了什么
Low级别的后端逻辑简单到近乎裸奔。用户提交username和password之后,服务端直接拼一条SQL去查:
$query = "SELECT * FROM users WHERE user = '$user' AND password = '$pass';"; $result = mysqli_query($connection, $query); if (mysqli_num_rows($result) == 1) { // 登录成功 }这段代码有两个致命问题。第一,它没有对输入做任何转义,所以除了暴力破解,SQL注入也能直接绕过登录。第二,它没有对失败次数做任何限制,你提交一万次请求,它就老老实实查一万次数据库,不会拦截、不会延迟、不会记录。从攻击者视角看,这就是一个“无限次尝试且每次响应都即时返回”的理想靶子。
这里有个细节值得注意:登录成功和失败的响应是有差异的。成功时会返回一段包含欢迎信息的页面,失败时返回的是“Username and/or password incorrect.”。这个差异就是攻击者判断密码是否正确的依据。在Burp里,你可以通过响应长度、状态码或者关键字来区分。
2.2 用Burp Intruder跑一遍完整流程
先做一次正常登录,用Burp抓包。请求大概长这样:
POST /dvwa/vulnerabilities/brute/ HTTP/1.1 Host: 127.0.0.1 Cookie: PHPSESSID=xxx; security=low Content-Type: application/x-www-form-urlencoded username=admin&password=1234&Login=Login把这个请求送到Intruder,攻击类型选Sniper,把password的值设成payload位置。Payload类型选Simple list,加载一个常用密码字典。然后开始攻击。
攻击结束后,看响应结果。Low级别下,成功的那条请求响应长度会明显不同,而且响应体里会出现“Welcome to the password protected area”这样的字样。你按长度排个序,或者用Grep Match功能匹配这个关键字,密码就出来了。
实测下来,DVWA默认的admin密码是password,如果你字典里包含这个词,通常几十条请求内就能命中。但这里我想强调的是字典的选择比工具本身更重要。很多人跑不出结果,不是工具用错了,是字典太离谱。针对DVWA这种练习环境,用rockyou的前几千条或者一个精简的常见密码列表就够了。
2.3 为什么这个级别“没有防护”反而是最好的教材
Low级别的价值在于它让你看清暴力破解的最小必要条件:一个可枚举的输入点、一个可区分的响应、以及没有频率限制。这三者缺一不可。如果响应不可区分,你就不知道哪次猜对了;如果有频率限制,你就没法快速尝试。
我在带新人的时候,会让他们先在Low级别把整个流程走通,然后问一个问题:如果只允许你改服务端的一行代码来增加攻击成本,你改哪里?大部分人的第一反应是“加个验证码”,但其实更直接的是加一个sleep(1)。这就是Medium级别要做的事情。
3. Medium级别:延迟响应与参数过滤带来的新问题
3.1 服务端加了两道防线
Medium级别的代码在Low的基础上做了两处改动。第一处是对username和password做了转义处理:
$user = mysqli_real_escape_string($connection, $_GET['username']); $pass = mysqli_real_escape_string($connection, $_GET['password']);这一下就把SQL注入的路堵死了,但暴力破解本身不受影响,因为转义只是让特殊字符失效,正常的用户名密码照样能查。
第二处改动才是关键:
if ($result && mysqli_num_rows($result) == 1) { // 登录成功 } else { sleep(2); // 登录失败 }失败时服务端会先睡2秒再返回。这个设计的意图很明确:把每次失败尝试的时间成本从几十毫秒拉高到2秒以上,让暴力破解变得“不划算”。假设字典有一万条,Low级别可能几分钟跑完,Medium级别就要跑五个多小时。
3.2 攻击者视角:延迟真的挡得住吗
从单线程的角度看,2秒延迟确实很烦。但攻击者有几个应对思路。
第一个思路是多线程并发。Burp Intruder本身支持设置线程数,默认是1,你可以调到10甚至更高。但这里有个坑:DVWA跑在本地或者低配虚拟机上时,并发太高会导致请求超时或者服务端响应混乱,反而影响结果判断。我一般把线程设在5到10之间,配合超时时间调大一点。
第二个思路是换工具。用Python写个多线程脚本,效果比Burp更可控。下面是一个简化版的示例:
import requests import threading from queue import Queue target = "http://127.0.0.1/dvwa/vulnerabilities/brute/" cookies = {"PHPSESSID": "your_session", "security": "medium"} usernames = ["admin"] passwords = ["123456", "password", "admin", "letmein"] def try_login(username, password): params = {"username": username, "password": password, "Login": "Login"} r = requests.get(target, params=params, cookies=cookies, timeout=10) if "Welcome to the password protected area" in r.text: print(f"[+] Found: {username}:{password}") q = Queue() for pwd in passwords: q.put(pwd) def worker(): while not q.empty(): pwd = q.get() try_login("admin", pwd) q.task_done() for i in range(10): t = threading.Thread(target=worker) t.daemon = True t.start() q.join()这个脚本开了10个线程,每个线程独立发请求。因为服务端的sleep是在失败分支里,多个请求可以并行地“睡”,总耗时就被摊薄了。实测下来,10个线程跑Medium级别,速度大概能恢复到Low级别的三分之一左右。
第三个思路是利用响应差异做时间盲判。这个在Medium级别其实用不上,因为成功和失败的响应内容本身就有差异。但到了更复杂的场景,如果响应内容被统一了,你就只能靠响应时间来区分——成功的请求立即返回,失败的请求延迟2秒。这时候你需要记录每次请求的耗时,而不是看响应体。
3.3 一个容易被忽略的细节:GET还是POST
DVWA的Brute Force模块在Low和Medium级别用的是GET请求,参数直接拼在URL里。到了High级别会变成POST。这个变化看起来小,但对工具配置有影响。用Burp的时候,GET请求的参数在URL里,POST请求的参数在body里,Intruder里设置payload位置的方式不一样。很多人从Low切到High之后跑不出结果,就是因为没注意到请求方法变了。
另外,GET请求的参数会出现在服务端访问日志里,这本身也是一个信息泄露点。真实业务里登录请求应该用POST,而且敏感参数不应该出现在URL中。DVWA在High级别改成POST,算是往正确方向走了一步。
4. High级别:令牌机制与请求方法的双重变化
4.1 反CSRF令牌是怎么加进来的
High级别的代码引入了anti-CSRF token。每次页面加载时,服务端生成一个随机token,存在session里,同时嵌入表单的隐藏字段。提交登录时,服务端会先校验token是否匹配,不匹配就直接拒绝。
if ($_SERVER['REQUEST_METHOD'] == 'POST') { $token = $_POST['user_token']; if (!hash_equals($_SESSION['session_token'], $token)) { // token不匹配,直接返回 } // 继续验证用户名密码 }这个机制的本意是防CSRF,但它顺带增加了暴力破解的难度:你不能简单地重复提交同一个请求了,因为每次请求都需要一个有效的、与当前session绑定的token。
4.2 攻击者怎么拿到并复用token
面对token机制,攻击者的核心问题是:token能不能自动获取并复用。答案取决于token的生成和校验逻辑。
DVWA的High级别里,token是每次页面加载时重新生成的,但同一个session内,旧token在页面刷新后会被新token覆盖。这意味着你不能一次性抓一堆token然后慢慢用,必须“取一个token,发一次登录请求,再取下一个token”。
用Burp Intruder做这件事比较麻烦,因为Intruder本身不擅长处理“先请求A页面提取token,再请求B页面提交”这种有状态流程。这时候用Python脚本就顺手多了:
import requests import re target = "http://127.0.0.1/dvwa/vulnerabilities/brute/" cookies = {"PHPSESSID": "your_session", "security": "high"} def get_token(): r = requests.get(target, cookies=cookies) match = re.search(r'name="user_token" value="([^"]+)"', r.text) return match.group(1) if match else None def try_login(username, password): token = get_token() if not token: return False data = { "username": username, "password": password, "user_token": token, "Login": "Login" } r = requests.post(target, data=data, cookies=cookies) return "Welcome to the password protected area" in r.text passwords = ["123456", "password", "admin", "letmein"] for pwd in passwords: if try_login("admin", pwd): print(f"[+] Found: admin:{pwd}") break这个脚本每次尝试前都先GET一次页面拿token,然后带着token POST登录请求。逻辑上完全模拟了正常用户的浏览器行为。
4.3 为什么token机制单独用不够
token机制能挡住“重放同一个请求”这种低级攻击,但它挡不住“每次重新获取token再提交”的高级攻击。因为token的生成和校验是自动化的,攻击者也可以自动化地获取和提交。真正让暴力破解变难的是token + 频率限制 + 账户锁定的组合拳。
DVWA的High级别只加了token,没有加失败次数限制,所以用上面的脚本依然可以跑。但相比Low和Medium,攻击成本已经高了不少:每次尝试需要两次HTTP请求(一次取token,一次提交),而且不能并发太高,否则token和session的对应关系容易乱。
这里有个实操心得:用脚本跑High级别时,不要开多线程。因为token是和session绑定的,多个线程同时取token会互相覆盖,导致校验失败。老老实实单线程跑,虽然慢,但结果可靠。如果非要提速,可以给每个线程分配独立的session,但DVWA默认只允许一个session同时存在,所以这条路也走不通。
5. Impossible级别:当暴力破解在理论上变得不可行
5.1 三层防护叠加后的效果
Impossible级别是DVWA给出的“正确答案”。它同时做了三件事:
第一,账户锁定。服务端记录每个用户的失败尝试次数,超过3次就锁定账户一段时间。这意味着你最多只能试3个密码,第4次开始即使密码正确也会被拒绝。
第二,强制延迟。失败时随机sleep 2到4秒,进一步拉高时间成本。
第三,token校验 + 参数化查询。token防重放,参数化查询防注入,把其他攻击路径也堵死。
if ($failed_attempts >= 3) { // 账户锁定 echo "Account locked."; exit; }这段逻辑意味着,攻击者面对的不再是一个“可以无限尝试”的接口,而是一个“试错次数有限且每次试错成本很高”的接口。从数学上看,如果密码空间是10的6次方,每次尝试平均耗时3秒,且只能试3次,那么成功概率几乎可以忽略不计。
5.2 从防御者视角看:哪些设计是必要的
把四个级别串起来看,一个健壮的登录接口至少需要以下几层防护:
| 防护措施 | 作用 | DVWA对应级别 |
|---|---|---|
| 参数化查询 | 防SQL注入 | Medium及以上 |
| 失败延迟 | 增加时间成本 | Medium及以上 |
| 请求令牌 | 防重放和CSRF | High及以上 |
| 失败次数限制 | 限制尝试次数 | Impossible |
| 账户锁定 | 阻断持续攻击 | Impossible |
| 日志记录 | 事后追溯 | 建议补充 |
这里我想特别强调失败次数限制和账户锁定的区别。失败次数限制是“超过N次后拒绝”,账户锁定是“超过N次后锁定一段时间”。前者可能被绕过(比如换IP),后者对特定账户的防护更彻底。但账户锁定也有副作用:攻击者可以故意输错密码来锁定正常用户的账户,造成拒绝服务。所以真实业务里通常会结合IP限制、验证码、风险评分等多种手段,而不是单一依赖账户锁定。
5.3 一个常见的误解:Impossible级别真的“不可破”吗
严格来说,Impossible级别并不是“不可破”,而是“在给定条件下攻击成本远高于收益”。如果攻击者能拿到session、能绕过token校验、能分布式发起请求,理论上仍然存在突破的可能。但在DVWA这个封闭环境里,这些条件都不具备,所以它表现为“不可破”。
这个区别很重要。很多初学者以为加了账户锁定就万事大吉,但实际上,如果锁定策略设计不当(比如锁定时间太短、锁定阈值太高、没有IP维度限制),攻击者仍然可以通过慢速攻击或者分布式攻击来绕过。DVWA的Impossible级别是一个理想化的教学模型,真实业务要比它复杂得多。
6. 把四个级别串起来:一套可复用的测试思路
6.1 先判断防护级别,再选工具和策略
我在实际测试一个登录接口时,不会一上来就跑字典。我会先做几件事:
第一,看响应差异。成功和失败的响应在状态码、长度、内容上有没有区别?如果没有区别,就要考虑用时间盲判。
第二,测频率限制。连续发10次失败请求,看服务端有没有延迟、有没有锁定、有没有验证码。
第三,看请求方法。是GET还是POST?有没有token?token是怎么生成的?
第四,看错误信息。失败时返回的是“用户名或密码错误”还是“用户名不存在”?后者可以用来枚举有效用户名。
这四步做完,基本就能判断出防护级别,然后决定用Burp还是脚本、用单线程还是多线程、用固定字典还是动态生成。
6.2 字典策略:不是越大越好
很多人迷信大字典,动辄几个G的密码表。但在DVWA这种环境里,大字典反而浪费时间。我的做法是分层:
- 第一层:Top 100常见密码,快速试探。
- 第二层:Top 1000,覆盖大部分弱口令。
- 第三层:针对目标信息的定制字典,比如用户名、项目名、日期等组合。
在Low和Medium级别,第一层往往就能命中。在High级别,因为每次尝试成本高,更应该优先用高质量的小字典,而不是无脑跑大字典。
6.3 工具选择:Burp和脚本各有所长
Burp Intruder适合快速验证和可视化分析,尤其是Low和Medium级别,配置简单、结果直观。但到了High级别,因为token的存在,Burp的配置会变得很繁琐,这时候Python脚本更灵活。
我通常的做法是:用Burp做初步探测,用脚本做深度测试。先用Burp跑几十条请求,看看响应特征和防护行为,然后根据情况决定要不要写脚本。
6.4 一个完整的测试流程示例
假设我要测试一个未知的登录接口,我会按这个顺序走:
- 用Burp抓一次正常登录请求,观察请求方法、参数、cookie、token。
- 手动提交几次错误密码,观察响应差异和延迟。
- 如果无延迟无锁定,直接用Intruder跑Top 100字典。
- 如果有延迟,改用多线程脚本,线程数从5开始逐步增加。
- 如果有token,改用带token获取逻辑的脚本,单线程跑。
- 如果有账户锁定,停止暴力破解,转向其他攻击面(如密码重置、会话固定等)。
这个流程不是固定的,但核心思路是一致的:先摸清防护,再决定策略,不要拿着锤子找钉子。
7. 几个我在实操中踩过的坑
第一个坑是session过期。用脚本跑High级别时,如果跑的时间太长,session可能会过期,导致token校验失败。表现是脚本突然开始全部返回失败,但密码明明是对的。解决办法是定期检查session有效性,或者在脚本里加一个“重新登录”的逻辑。
第二个坑是并发导致的结果错乱。用多线程跑Medium级别时,如果线程数太高,DVWA的响应可能会串。比如A线程的请求返回了B线程的结果。这在本地低配环境里尤其常见。我的经验是线程数不要超过10,并且每次请求都带上独立的标识,方便排查。
第三个坑是字典编码问题。有些字典文件包含特殊字符或者非UTF-8编码,直接加载到Burp里会乱码。解决办法是先用文本编辑器转成UTF-8,或者用脚本读取时指定编码。
第四个坑是忽略了GET和POST的差异。从Low切到High时,请求方法从GET变成POST,如果Intruder的payload位置没跟着改,跑出来的结果全是失败。这个坑我踩过不止一次,后来养成了习惯:每次切换级别后,先手动提交一次,确认请求格式。
第五个坑是把DVWA的防护当成真实防护。DVWA的Impossible级别是一个教学模型,它的账户锁定是硬编码的、没有考虑分布式场景、没有验证码、没有风险评分。真实业务里的防护要复杂得多。所以学完DVWA之后,一定要找机会看看真实系统的登录逻辑,或者自己动手写一个带完整防护的登录模块,才能真正理解这些机制在实际场景中的表现。
8. 从攻击回到防御:写一个带防护的登录模块
学完DVWA的四个级别,最有价值的练习不是继续跑字典,而是自己写一个登录模块,把学到的防护手段用上去。下面是一个简化的Python Flask示例,展示了如何组合token、延迟、失败计数和账户锁定:
from flask import Flask, request, session, render_template_string import time import secrets app = Flask(__name__) app.secret_key = "your_secret_key" failed_attempts = {} lockout_time = {} @app.route("/login", methods=["GET", "POST"]) def login(): if request.method == "GET": token = secrets.token_hex(16) session["token"] = token return render_template_string(''' <form method="POST"> <input name="username"> <input name="password" type="password"> <input type="hidden" name="token" value="{{ token }}"> <button type="submit">Login</button> </form> ''', token=token) username = request.form.get("username") password = request.form.get("password") token = request.form.get("token") if token != session.get("token"): return "Invalid token", 403 if username in lockout_time and time.time() < lockout_time[username]: return "Account locked", 403 if username == "admin" and password == "correct_password": failed_attempts[username] = 0 return "Welcome" failed_attempts[username] = failed_attempts.get(username, 0) + 1 if failed_attempts[username] >= 3: lockout_time[username] = time.time() + 300 return "Account locked for 5 minutes", 403 time.sleep(2) return "Invalid credentials", 401这个模块虽然简单,但包含了token校验、失败计数、账户锁定和延迟响应。你可以拿它和DVWA的四个级别对比,看看每个防护手段对应哪个级别、解决了什么问题、还有什么不足。
比如,这个模块的账户锁定是基于用户名的,攻击者可以故意输错密码来锁定admin账户。要解决这个问题,可以引入IP维度的限制、验证码、或者风险评分机制。再比如,延迟是固定2秒,攻击者可以用多线程绕过,要解决这个问题,可以改成随机延迟或者基于失败次数的递增延迟。
这些思考才是DVWA这个练习环境真正想教给你的东西。工具和字典只是手段,理解防护原理和攻击成本的博弈,才是核心。