
1. 從一次登錄框測試說起暴力破解到底在破什么很多人第一次接觸DVWA的暴力破解模塊注意力都放在“怎么把密碼跑出來”上結果跑通一次之后腦子里留下的只有Burp的Intruder界面和一堆字典文件換個環(huán)境就不知道從哪下手了。我自己早期也是這樣直到有一次在模擬環(huán)境里對著一個登錄接口折騰了大半天才發(fā)現(xiàn)真正值得琢磨的不是“跑字典”這個動作而是這個登錄框在什么條件下會泄露信息、在什么條件下會攔截你、以及不同防護級別下攻擊者的策略要怎么調整。DVWA的Brute Force模塊就是為這件事準備的。它本質上是一個極簡的登錄表單只有username和password兩個字段背后連著一張用戶表。但DVWA把它做成了四個難度級別——Low、Medium、High、Impossible每個級別對應一種典型的防護思路。這四個級別串起來看幾乎就是一部“登錄接口防護演進史”從毫無防護到參數(shù)過濾到延遲響應再到令牌加鎖定。你把這四個級別吃透再回頭看真實業(yè)務里的登錄邏輯會發(fā)現(xiàn)大部分設計都能在里面找到影子。這篇文章適合兩類人一類是剛學Web安全、想搞明白暴力破解原理和工具用法的初學者另一類是做開發(fā)或測試、想搞清楚“登錄接口到底該怎么防”的從業(yè)者。我會按級別逐個拆每個級別講清楚三件事——服務端實際做了什么、攻擊者會怎么應對、這個設計為什么擋不住或者能擋住。工具上主要用Burp Suite的Intruder也會提到用Python腳本做自動化因為有些級別用腳本比用圖形化工具更直觀。需要提前說明的是DVWA本身是一個刻意設計成有漏洞的練習環(huán)境所有操作都應該在本地搭建的靶機里進行。下面涉及的所有請求、參數(shù)、腳本都只針對DVWA這個模擬系統(tǒng)目的是理解防護原理不要往任何真實系統(tǒng)上套。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注入也能直接繞過登錄。第二它沒有對失敗次數(shù)做任何限制你提交一萬次請求它就老老實實查一萬次數(shù)據(jù)庫不會攔截、不會延遲、不會記錄。從攻擊者視角看這就是一個“無限次嘗試且每次響應都即時返回”的理想靶子。這里有個細節(jié)值得注意登錄成功和失敗的響應是有差異的。成功時會返回一段包含歡迎信息的頁面失敗時返回的是“Username and/or password incorrect.”。這個差異就是攻擊者判斷密碼是否正確的依據(jù)。在Burp里你可以通過響應長度、狀態(tài)碼或者關鍵字來區(qū)分。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級別下成功的那條請求響應長度會明顯不同而且響應體里會出現(xiàn)“Welcome to the password protected area”這樣的字樣。你按長度排個序或者用Grep Match功能匹配這個關鍵字密碼就出來了。實測下來DVWA默認的admin密碼是password如果你字典里包含這個詞通常幾十條請求內就能命中。但這里我想強調的是字典的選擇比工具本身更重要。很多人跑不出結果不是工具用錯了是字典太離譜。針對DVWA這種練習環(huán)境用rockyou的前幾千條或者一個精簡的常見密碼列表就夠了。2.3 為什么這個級別“沒有防護”反而是最好的教材Low級別的價值在于它讓你看清暴力破解的最小必要條件一個可枚舉的輸入點、一個可區(qū)分的響應、以及沒有頻率限制。這三者缺一不可。如果響應不可區(qū)分你就不知道哪次猜對了如果有頻率限制你就沒法快速嘗試。我在帶新人的時候會讓他們先在Low級別把整個流程走通然后問一個問題如果只允許你改服務端的一行代碼來增加攻擊成本你改哪里大部分人的第一反應是“加個驗證碼”但其實更直接的是加一個sleep(1)。這就是Medium級別要做的事情。3. Medium級別延遲響應與參數(shù)過濾帶來的新問題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秒延遲確實很煩。但攻擊者有幾個應對思路。第一個思路是多線程并發(fā)。Burp Intruder本身支持設置線程數(shù)默認是1你可以調到10甚至更高。但這里有個坑DVWA跑在本地或者低配虛擬機上時并發(fā)太高會導致請求超時或者服務端響應混亂反而影響結果判斷。我一般把線程設在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個線程每個線程獨立發(fā)請求。因為服務端的sleep是在失敗分支里多個請求可以并行地“睡”總耗時就被攤薄了。實測下來10個線程跑Medium級別速度大概能恢復到Low級別的三分之一左右。第三個思路是利用響應差異做時間盲判。這個在Medium級別其實用不上因為成功和失敗的響應內容本身就有差異。但到了更復雜的場景如果響應內容被統(tǒng)一了你就只能靠響應時間來區(qū)分——成功的請求立即返回失敗的請求延遲2秒。這時候你需要記錄每次請求的耗時而不是看響應體。3.3 一個容易被忽略的細節(jié)GET還是POSTDVWA的Brute Force模塊在Low和Medium級別用的是GET請求參數(shù)直接拼在URL里。到了High級別會變成POST。這個變化看起來小但對工具配置有影響。用Burp的時候GET請求的參數(shù)在URL里POST請求的參數(shù)在body里Intruder里設置payload位置的方式不一樣。很多人從Low切到High之后跑不出結果就是因為沒注意到請求方法變了。另外GET請求的參數(shù)會出現(xiàn)在服務端訪問日志里這本身也是一個信息泄露點。真實業(yè)務里登錄請求應該用POST而且敏感參數(shù)不應該出現(xiàn)在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不匹配直接返回 } // 繼續(xù)驗證用戶名密碼 }這個機制的本意是防CSRF但它順帶增加了暴力破解的難度你不能簡單地重復提交同一個請求了因為每次請求都需要一個有效的、與當前session綁定的token。4.2 攻擊者怎么拿到并復用token面對token機制攻擊者的核心問題是token能不能自動獲取并復用。答案取決于token的生成和校驗邏輯。DVWA的High級別里token是每次頁面加載時重新生成的但同一個session內舊token在頁面刷新后會被新token覆蓋。這意味著你不能一次性抓一堆token然后慢慢用必須“取一個token發(fā)一次登錄請求再取下一個token”。用Burp Intruder做這件事比較麻煩因為Intruder本身不擅長處理“先請求A頁面提取token再請求B頁面提交”這種有狀態(tài)流程。這時候用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沒有加失敗次數(shù)限制所以用上面的腳本依然可以跑。但相比Low和Medium攻擊成本已經(jīng)高了不少每次嘗試需要兩次HTTP請求一次取token一次提交而且不能并發(fā)太高否則token和session的對應關系容易亂。這里有個實操心得用腳本跑High級別時不要開多線程。因為token是和session綁定的多個線程同時取token會互相覆蓋導致校驗失敗。老老實實單線程跑雖然慢但結果可靠。如果非要提速可以給每個線程分配獨立的session但DVWA默認只允許一個session同時存在所以這條路也走不通。5. Impossible級別當暴力破解在理論上變得不可行5.1 三層防護疊加后的效果Impossible級別是DVWA給出的“正確答案”。它同時做了三件事第一賬戶鎖定。服務端記錄每個用戶的失敗嘗試次數(shù)超過3次就鎖定賬戶一段時間。這意味著你最多只能試3個密碼第4次開始即使密碼正確也會被拒絕。第二強制延遲。失敗時隨機sleep 2到4秒進一步拉高時間成本。第三token校驗 參數(shù)化查詢。token防重放參數(shù)化查詢防注入把其他攻擊路徑也堵死。if ($failed_attempts 3) { // 賬戶鎖定 echo Account locked.; exit; }這段邏輯意味著攻擊者面對的不再是一個“可以無限嘗試”的接口而是一個“試錯次數(shù)有限且每次試錯成本很高”的接口。從數(shù)學上看如果密碼空間是10的6次方每次嘗試平均耗時3秒且只能試3次那么成功概率幾乎可以忽略不計。5.2 從防御者視角看哪些設計是必要的把四個級別串起來看一個健壯的登錄接口至少需要以下幾層防護防護措施作用DVWA對應級別參數(shù)化查詢防SQL注入Medium及以上失敗延遲增加時間成本Medium及以上請求令牌防重放和CSRFHigh及以上失敗次數(shù)限制限制嘗試次數(shù)Impossible賬戶鎖定阻斷持續(xù)攻擊Impossible日志記錄事后追溯建議補充這里我想特別強調失敗次數(shù)限制和賬戶鎖定的區(qū)別。失敗次數(shù)限制是“超過N次后拒絕”賬戶鎖定是“超過N次后鎖定一段時間”。前者可能被繞過比如換IP后者對特定賬戶的防護更徹底。但賬戶鎖定也有副作用攻擊者可以故意輸錯密碼來鎖定正常用戶的賬戶造成拒絕服務。所以真實業(yè)務里通常會結合IP限制、驗證碼、風險評分等多種手段而不是單一依賴賬戶鎖定。5.3 一個常見的誤解Impossible級別真的“不可破”嗎嚴格來說Impossible級別并不是“不可破”而是“在給定條件下攻擊成本遠高于收益”。如果攻擊者能拿到session、能繞過token校驗、能分布式發(fā)起請求理論上仍然存在突破的可能。但在DVWA這個封閉環(huán)境里這些條件都不具備所以它表現(xiàn)為“不可破”。這個區(qū)別很重要。很多初學者以為加了賬戶鎖定就萬事大吉但實際上如果鎖定策略設計不當比如鎖定時間太短、鎖定閾值太高、沒有IP維度限制攻擊者仍然可以通過慢速攻擊或者分布式攻擊來繞過。DVWA的Impossible級別是一個理想化的教學模型真實業(yè)務要比它復雜得多。6. 把四個級別串起來一套可復用的測試思路6.1 先判斷防護級別再選工具和策略我在實際測試一個登錄接口時不會一上來就跑字典。我會先做幾件事第一看響應差異。成功和失敗的響應在狀態(tài)碼、長度、內容上有沒有區(qū)別如果沒有區(qū)別就要考慮用時間盲判。第二測頻率限制。連續(xù)發(fā)10次失敗請求看服務端有沒有延遲、有沒有鎖定、有沒有驗證碼。第三看請求方法。是GET還是POST有沒有tokentoken是怎么生成的第四看錯誤信息。失敗時返回的是“用戶名或密碼錯誤”還是“用戶名不存在”后者可以用來枚舉有效用戶名。這四步做完基本就能判斷出防護級別然后決定用Burp還是腳本、用單線程還是多線程、用固定字典還是動態(tài)生成。6.2 字典策略不是越大越好很多人迷信大字典動輒幾個G的密碼表。但在DVWA這種環(huán)境里大字典反而浪費時間。我的做法是分層第一層Top 100常見密碼快速試探。第二層Top 1000覆蓋大部分弱口令。第三層針對目標信息的定制字典比如用戶名、項目名、日期等組合。在Low和Medium級別第一層往往就能命中。在High級別因為每次嘗試成本高更應該優(yōu)先用高質量的小字典而不是無腦跑大字典。6.3 工具選擇Burp和腳本各有所長Burp Intruder適合快速驗證和可視化分析尤其是Low和Medium級別配置簡單、結果直觀。但到了High級別因為token的存在Burp的配置會變得很繁瑣這時候Python腳本更靈活。我通常的做法是用Burp做初步探測用腳本做深度測試。先用Burp跑幾十條請求看看響應特征和防護行為然后根據(jù)情況決定要不要寫腳本。6.4 一個完整的測試流程示例假設我要測試一個未知的登錄接口我會按這個順序走用Burp抓一次正常登錄請求觀察請求方法、參數(shù)、cookie、token。手動提交幾次錯誤密碼觀察響應差異和延遲。如果無延遲無鎖定直接用Intruder跑Top 100字典。如果有延遲改用多線程腳本線程數(shù)從5開始逐步增加。如果有token改用帶token獲取邏輯的腳本單線程跑。如果有賬戶鎖定停止暴力破解轉向其他攻擊面如密碼重置、會話固定等。這個流程不是固定的但核心思路是一致的先摸清防護再決定策略不要拿著錘子找釘子。7. 幾個我在實操中踩過的坑第一個坑是session過期。用腳本跑High級別時如果跑的時間太長session可能會過期導致token校驗失敗。表現(xiàn)是腳本突然開始全部返回失敗但密碼明明是對的。解決辦法是定期檢查session有效性或者在腳本里加一個“重新登錄”的邏輯。第二個坑是并發(fā)導致的結果錯亂。用多線程跑Medium級別時如果線程數(shù)太高DVWA的響應可能會串。比如A線程的請求返回了B線程的結果。這在本地低配環(huán)境里尤其常見。我的經(jīng)驗是線程數(shù)不要超過10并且每次請求都帶上獨立的標識方便排查。第三個坑是字典編碼問題。有些字典文件包含特殊字符或者非UTF-8編碼直接加載到Burp里會亂碼。解決辦法是先用文本編輯器轉成UTF-8或者用腳本讀取時指定編碼。第四個坑是忽略了GET和POST的差異。從Low切到High時請求方法從GET變成POST如果Intruder的payload位置沒跟著改跑出來的結果全是失敗。這個坑我踩過不止一次后來養(yǎng)成了習慣每次切換級別后先手動提交一次確認請求格式。第五個坑是把DVWA的防護當成真實防護。DVWA的Impossible級別是一個教學模型它的賬戶鎖定是硬編碼的、沒有考慮分布式場景、沒有驗證碼、沒有風險評分。真實業(yè)務里的防護要復雜得多。所以學完DVWA之后一定要找機會看看真實系統(tǒng)的登錄邏輯或者自己動手寫一個帶完整防護的登錄模塊才能真正理解這些機制在實際場景中的表現(xiàn)。8. 從攻擊回到防御寫一個帶防護的登錄模塊學完DVWA的四個級別最有價值的練習不是繼續(xù)跑字典而是自己寫一個登錄模塊把學到的防護手段用上去。下面是一個簡化的Python Flask示例展示了如何組合token、延遲、失敗計數(shù)和賬戶鎖定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校驗、失敗計數(shù)、賬戶鎖定和延遲響應。你可以拿它和DVWA的四個級別對比看看每個防護手段對應哪個級別、解決了什么問題、還有什么不足。比如這個模塊的賬戶鎖定是基于用戶名的攻擊者可以故意輸錯密碼來鎖定admin賬戶。要解決這個問題可以引入IP維度的限制、驗證碼、或者風險評分機制。再比如延遲是固定2秒攻擊者可以用多線程繞過要解決這個問題可以改成隨機延遲或者基于失敗次數(shù)的遞增延遲。這些思考才是DVWA這個練習環(huán)境真正想教給你的東西。工具和字典只是手段理解防護原理和攻擊成本的博弈才是核心。