目錄
一、查看源碼
二、功能分析
三、SQL注入分析
1、使用PDO預處理語句和參數綁定
2、mysqli_real_escape_string轉義
3、stripslashes去除反斜杠
四、暴力破解分析
1、token防止暴力破解機制?
2、登錄失敗隨機延遲機制
3、登陸失敗報錯信息相同
4、登陸失敗的賬戶鎖定機制
本系列為通過《DVWA靶場通關筆記》的暴力破解關卡(low,medium,high,impossible共4關)滲透集合,通過對相應關卡源碼的代碼審計找到講解滲透原理并進行滲透實踐,本文為暴力破解impossible關卡的原理分析部分,講解相對于low、medium和high級別,為何對其進行滲透測試是Impossible的。
一、查看源碼
進入DVWA靶場源目錄,找到impossible.php源碼,分析其為何能讓這一關卡名為不可能實現暴力破解。
打開impossible.php文件,對其進行代碼審計,詳細注釋的impossible.php源碼如下所示。
<?php// 檢查是否通過 POST 方法提交了名為 'Login' 的表單數據,并且用戶名和密碼字段均已設置
if( isset( $_POST[ 'Login' ] ) && isset ($_POST['username']) && isset ($_POST['password']) ) {// 檢查反跨站請求偽造(CSRF)令牌// $_REQUEST['user_token'] 是從請求中獲取的用戶令牌// $_SESSION['session_token'] 是存儲在會話中的令牌// 'index.php' 是如果令牌驗證失敗將跳轉的頁面checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );// 從 POST 請求中獲取用戶輸入的用戶名$user = $_POST[ 'username' ];// 去除用戶名中的反斜杠(如果有的話)$user = stripslashes( $user );// 使用 mysqli_real_escape_string 函數對用戶名進行轉義處理,防止 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! This code does not work.", E_USER_ERROR)) ? "" : ""));// 從 POST 請求中獲取用戶輸入的密碼$pass = $_POST[ 'password' ];// 去除密碼中的反斜杠(如果有的話)$pass = stripslashes( $pass );// 使用 mysqli_real_escape_string 函數對密碼進行轉義處理,防止 SQL 注入$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! This code does not work.", E_USER_ERROR)) ? "" : ""));// 使用 md5 函數對密碼進行加密$pass = md5( $pass );// 設置默認值// 允許的最大失敗登錄次數$total_failed_login = 3;// 賬戶鎖定時間(分鐘)$lockout_time = 15;// 標記賬戶是否被鎖定$account_locked = false;// 從數據庫中查詢用戶的失敗登錄次數和最后登錄時間$data = $db->prepare( 'SELECT failed_login, last_login FROM users WHERE user = (:user) LIMIT 1;' );// 綁定用戶名參數$data->bindParam( ':user', $user, PDO::PARAM_STR );// 執行查詢$data->execute();// 獲取查詢結果的一行$row = $data->fetch();// 檢查用戶是否因多次失敗登錄而被鎖定if( ( $data->rowCount() == 1 ) && ( $row[ 'failed_login' ] >= $total_failed_login ) ) {// 計算用戶可以再次登錄的時間$last_login = strtotime( $row[ 'last_login' ] );$timeout = $last_login + ($lockout_time * 60);$timenow = time();// 檢查是否已經過了鎖定時間,如果未過則鎖定賬戶if( $timenow < $timeout ) {$account_locked = true;}}// 從數據庫中查詢用戶名和密碼匹配的記錄$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 ) && ( $account_locked == false ) ) {// 獲取用戶的頭像、失敗登錄次數和最后登錄時間$avatar = $row[ 'avatar' ];$failed_login = $row[ 'failed_login' ];$last_login = $row[ 'last_login' ];// 登錄成功,顯示歡迎信息和用戶頭像$html .= "<p>Welcome to the password protected area <em>{$user}</em></p>";$html .= "<img src=\"{$avatar}\" />";// 如果賬戶之前被鎖定過,給出警告信息if( $failed_login >= $total_failed_login ) {$html .= "<p><em>Warning</em>: Someone might of been brute forcing your account.</p>";$html .= "<p>Number of login attempts: <em>{$failed_login}</em>.<br />Last login attempt was at: <em>${last_login}</em>.</p>";}// 重置失敗登錄次數$data = $db->prepare( 'UPDATE users SET failed_login = "0" WHERE user = (:user) LIMIT 1;' );$data->bindParam( ':user', $user, PDO::PARAM_STR );$data->execute();} else {// 登錄失敗,腳本暫停 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>";// 更新失敗登錄次數$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();}// 設置用戶的最后登錄時間$data = $db->prepare( 'UPDATE users SET last_login = now() WHERE user = (:user) LIMIT 1;' );$data->bindParam( ':user', $user, PDO::PARAM_STR );$data->execute();
}// 生成反跨站請求偽造(CSRF)令牌
generateSessionToken();?>
二、功能分析
此 PHP 代碼實現了一個具備反 CSRF 保護、防暴力破解機制的用戶登錄驗證系統。當用戶通過 POST 方法提交登錄表單時,代碼會先檢查 CSRF 令牌的有效性,接著對用戶名和密碼進行處理,包括去除反斜杠和轉義處理,再對密碼進行 MD5 加密。之后會從數據庫中查詢用戶的失敗登錄次數和最后登錄時間,判斷賬戶是否被鎖定。若賬戶未鎖定且用戶名和密碼匹配,則登錄成功,顯示歡迎信息和用戶頭像,同時重置失敗登錄次數;若登錄失敗,會暫停一段時間并給出反饋信息,同時更新失敗登錄次數。無論登錄結果如何,都會更新用戶的最后登錄時間。最后,會生成一個新的反 CSRF 令牌。
三、SQL注入分析
代碼中使用了如下三種方法進行防御,具體如下所示。
- 使用PDO預處理語句和參數綁定
- 對用戶名和密碼輸入進行了mysqli_real_escape_string轉義
- 使用了stripslashes去除反斜杠
1、使用PDO預處理語句和參數綁定
PDO預處理語句是SQL注入防護的黃金標準。通過參數綁定,用戶輸入的數據不會被解釋為SQL代碼的一部分,而是作為純數據處理。這種方式完全隔離了代碼和數據,即使輸入中包含SQL特殊字符(如單引號、分號等),也不會改變SQL語句的結構。這是最有效的SQL注入防護措施,從根本上消除了SQL注入風險的可能性。
$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();
2、mysqli_real_escape_string轉義
mysqli_real_escape_string
對特殊字符進行轉義,特別是在字符串上下文中。它會轉義單引號(')、雙引號(")、反斜杠()和NULL字符等,防止這些字符破壞SQL語句結構。雖然這在PDO預處理的基礎上是冗余的,但提供了額外的防御層。這種深度防御策略確保了即使預處理機制出現問題,轉義功能仍然能提供保護。
$user = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $user ) : "");
$pass = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass ) : "");
3、stripslashes去除反斜杠
stripslashes
函數去除輸入中的反斜杠,主要用于處理Magic Quotes功能開啟時自動添加的轉義字符。雖然現代PHP版本默認關閉Magic Quotes,但這個處理確保了向后兼容性。它防止了雙重轉義問題,確保數據在進入數據庫前保持正確的格式。這是防御深度策略的一部分,處理各種可能的輸入場景。
$user = stripslashes( $user );
$pass = stripslashes( $pass );
綜上,代碼在處理用戶輸入時,使用了stripslashes和mysqli_real_escape_string對用戶名和密碼進行處理,并且在后續的數據庫查詢中使用了 PDO 預處理語句。PDO 預處理語句本身能夠有效防止 SQL 注入,因為它將 SQL 語句和用戶輸入分開處理,數據庫會自動對輸入進行正確的轉義。但stripslashes的使用可能會破壞mysqli_real_escape_string的轉義效果,不過由于后續使用了 PDO 預處理語句,整體上 SQL 注入的風險相對較低。
四、暴力破解分析
代碼中使用了如下三種方法進行防御暴力破解,具體如下所示。
- 基于token防暴力破解驗證機制
- 失敗登錄后有隨機延遲(2-4秒)
- 所有登錄失敗都打印同一錯誤信息,不利于攻擊者枚舉有效用戶名
- 登陸失敗后的賬戶鎖定機制
1、token防止暴力破解機制?
CSRF令牌驗證有效防止自動化暴力破解。攻擊者必須首先獲取有效的user_token才能提交登錄請求,這增加了攻擊復雜度。每個會話的令牌唯一且有時效性,阻止了重放攻擊和批量自動化請求。Token機制確保了每個登錄請求都是來自合法的用戶會話,顯著提高了暴力破解的門檻,迫使攻擊者需要先破解CSRF保護才能進行密碼猜測。
// 檢查反跨站請求偽造(CSRF)令牌
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
2、登錄失敗隨機延遲機制
隨機2-4秒延遲極大降低了暴力破解的速度。傳統攻擊每秒可嘗試數十次,現在降至每分鐘僅15-30次。對于萬級密碼字典,攻擊時間從幾分鐘延長到數小時。隨機性防止攻擊者通過固定時間模式優化攻擊節奏,增加了不確定性。這種時間成本策略有效消耗攻擊者資源,使得大規模暴力破解在經濟和時間上變得不可行,顯著提升防御效果。
sleep( rand( 2, 4 ) );
3、登陸失敗報錯信息相同
統一錯誤信息有效防止用戶名枚舉攻擊。攻擊者無法通過錯誤消息區分"用戶名不存在"和"密碼錯誤",必須同時猜測正確的用戶名和密碼組合。
$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>";
這使得攻擊復雜度從O(n+m)增加到O(n×m),顯著擴大搜索空間。雖然當前實現仍透露了鎖定信息,但基本防止了通過錯誤信息差異進行用戶枚舉,迫使攻擊者采用更耗時耗力的組合攻擊方式。如下所示,如果登錄錯誤,報錯信息均如下所示,并不會出現只是用戶名錯誤,密碼錯誤,使攻擊者不清楚到底是輸入參數中哪個信息是不正確的。
4、登陸失敗的賬戶鎖定機制
記錄了失敗登錄次數和最后登錄時間,有賬戶鎖定機制(3次失敗后鎖定15分鐘=15*60秒)。
if( ( $data->rowCount() == 1 ) && ( $row[ 'failed_login' ] >= $total_failed_login ) ) {$last_login = strtotime( $row[ 'last_login' ] );$timeout = $last_login + ($lockout_time * 60);if( $timenow < $timeout ) {$account_locked = true;}
}
賬戶鎖定機制針對性地保護每個用戶賬戶。在3次失敗登錄后鎖定15分鐘,有效阻止針對特定用戶的持續暴力破解。時間基于最后登錄時間計算,確保鎖定期的準確性。這種機制保護了用戶賬戶免受定向攻擊,迫使攻擊者要么等待鎖定解除,要么轉向其他目標,分散了攻擊壓力。結合全局延遲和令牌驗證,形成了多層次的有效防護體系。
綜上,代碼采取了多種措施來防范暴力破解。包括token防暴力破解,設置了最大失敗登錄次數限制(3次),當失敗次數達到上限時,賬戶會被鎖定一段時間(3次失敗后鎖定15分鐘);登錄失敗時,腳本會暫停 2 到 4 秒之間的隨機時間,增加每次嘗試的時間成本。但是本關卡仍然存在一些小問題:
- 鎖定是基于用戶名而非IP,允許攻擊者針對不同用戶嘗試
- 鎖定時間較短(僅15分鐘)
- 隨機延遲不夠長(2-4秒不足以阻止自動化攻擊)