
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: PHPSESSIDxxx; securitylow Content-Type: application/x-www-form-urlencoded usernameadminpassword1234LoginLogin把这个请求送到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, paramsparams, cookiescookies, timeout10) 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(targetworker) t.daemon True t.start() q.join()这个脚本开了10个线程每个线程独立发请求。因为服务端的sleep是在失败分支里多个请求可以并行地“睡”总耗时就被摊薄了。实测下来10个线程跑Medium级别速度大概能恢复到Low级别的三分之一左右。第三个思路是利用响应差异做时间盲判。这个在Medium级别其实用不上因为成功和失败的响应内容本身就有差异。但到了更复杂的场景如果响应内容被统一了你就只能靠响应时间来区分——成功的请求立即返回失败的请求延迟2秒。这时候你需要记录每次请求的耗时而不是看响应体。3.3 一个容易被忽略的细节GET还是POSTDVWA的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, cookiescookies) match re.search(rnameuser_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, datadata, cookiescookies) 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及以上请求令牌防重放和CSRFHigh及以上失败次数限制限制尝试次数Impossible账户锁定阻断持续攻击Impossible日志记录事后追溯建议补充这里我想特别强调失败次数限制和账户锁定的区别。失败次数限制是“超过N次后拒绝”账户锁定是“超过N次后锁定一段时间”。前者可能被绕过比如换IP后者对特定账户的防护更彻底。但账户锁定也有副作用攻击者可以故意输错密码来锁定正常用户的账户造成拒绝服务。所以真实业务里通常会结合IP限制、验证码、风险评分等多种手段而不是单一依赖账户锁定。5.3 一个常见的误解Impossible级别真的“不可破”吗严格来说Impossible级别并不是“不可破”而是“在给定条件下攻击成本远高于收益”。如果攻击者能拿到session、能绕过token校验、能分布式发起请求理论上仍然存在突破的可能。但在DVWA这个封闭环境里这些条件都不具备所以它表现为“不可破”。这个区别很重要。很多初学者以为加了账户锁定就万事大吉但实际上如果锁定策略设计不当比如锁定时间太短、锁定阈值太高、没有IP维度限制攻击者仍然可以通过慢速攻击或者分布式攻击来绕过。DVWA的Impossible级别是一个理想化的教学模型真实业务要比它复杂得多。6. 把四个级别串起来一套可复用的测试思路6.1 先判断防护级别再选工具和策略我在实际测试一个登录接口时不会一上来就跑字典。我会先做几件事第一看响应差异。成功和失败的响应在状态码、长度、内容上有没有区别如果没有区别就要考虑用时间盲判。第二测频率限制。连续发10次失败请求看服务端有没有延迟、有没有锁定、有没有验证码。第三看请求方法。是GET还是POST有没有tokentoken是怎么生成的第四看错误信息。失败时返回的是“用户名或密码错误”还是“用户名不存在”后者可以用来枚举有效用户名。这四步做完基本就能判断出防护级别然后决定用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 methodPOST input nameusername input namepassword typepassword input typehidden nametoken value{{ token }} button typesubmitLogin/button /form , tokentoken) 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这个练习环境真正想教给你的东西。工具和字典只是手段理解防护原理和攻击成本的博弈才是核心。