安全性測試入門 (五):Insecure CAPTCHA 驗證碼繞過


本篇繼續對於安全性測試話題,結合DVWA進行研習。

Insecure Captcha不安全驗證碼

1. 驗證碼到底是怎么一回事

這個Captcha狹義而言就是谷歌提供的一種用戶驗證服務,全稱為:Completely Automated Public Turing Test to Tell Computers and Humans Apart (全自動區分計算機和人類的圖靈測試)。

很巧妙的是,Captcha單獨成詞的意思就是,抓到你了喲_

Captcha在各種海外網站被廣泛用於用戶驗證。而在國內,由於眾所周知的原因,我們不用谷歌的服務,很多接口平台都可以提供類似服務。

比如apishop的這個四位驗證碼服務接口:

那么驗證碼到底在用戶驗證的過程中起到什么樣的作用呢?

驗證碼最大的作用就是防止攻擊者使用工具或者軟件自動調用系統功能

就如Captcha的全稱所示,他就是用來區分人類和計算機的一種圖靈測試,這種做法可以很有效的防止惡意軟件、機器人大量調用系統功能:比如注冊、登錄功能。

我們前面講到的Brute Force字典式暴力破解,就必須要使用工具大量嘗試登錄。如果這個時候系統有個嚴密的驗證碼機制,此類攻擊就無計可施了。

其工作流程如下所示:

2. 驗證碼繞過

為什么前文要在驗證碼機制前面黑體強調他要是嚴密的,那當然是如果驗證碼機制設計不得當,繞過它也只是分分鍾的事情。。。

DVWA提供的試驗模塊長這個樣子:

我們將其安全級別調到最低,使用ZAP做為代理進行抓包,填入任意密碼觸發請求,看到請求內容如下:

這是個啥子意思呢,我們參考一下DVWA的后台邏輯:

<?php

if( isset( $_POST[ 'Change' ] ) && ( $_POST[ 'step' ] == '1' ) ) {
	// Hide the CAPTCHA form
	$hide_form = true;

	// Get input
	$pass_new  = $_POST[ 'password_new' ];
	$pass_conf = $_POST[ 'password_conf' ];

	// Check CAPTCHA from 3rd party
	$resp = recaptcha_check_answer(
		$_DVWA[ 'recaptcha_private_key'],
		$_POST['g-recaptcha-response']
	);

	// Did the CAPTCHA fail?
	if( !$resp ) {
		// What happens when the CAPTCHA was entered incorrectly
		$html     .= "<pre><br />The CAPTCHA was incorrect. Please try again.</pre>";
		$hide_form = false;
		return;
	}
	else {
		// CAPTCHA was correct. Do both new passwords match?
		if( $pass_new == $pass_conf ) {
			// Show next stage for the user
			$html .= "
				<pre><br />You passed the CAPTCHA! Click the button to confirm your changes.<br /></pre>
				<form action=\"#\" method=\"POST\">
					<input type=\"hidden\" name=\"step\" value=\"2\" />
					<input type=\"hidden\" name=\"password_new\" value=\"{$pass_new}\" />
					<input type=\"hidden\" name=\"password_conf\" value=\"{$pass_conf}\" />
					<input type=\"submit\" name=\"Change\" value=\"Change\" />
				</form>";
		}
		else {
			// Both new passwords do not match.
			$html     .= "<pre>Both passwords must match.</pre>";
			$hide_form = false;
		}
	}
}

if( isset( $_POST[ 'Change' ] ) && ( $_POST[ 'step' ] == '2' ) ) {
	// Hide the CAPTCHA form
	$hide_form = true;

	// Get input
	$pass_new  = $_POST[ 'password_new' ];
	$pass_conf = $_POST[ 'password_conf' ];

	// Check to see if both password match
	if( $pass_new == $pass_conf ) {
		// They do!
		$pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
		$pass_new = md5( $pass_new );

		// Update database
		$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
		$result = mysqli_query($GLOBALS["___mysqli_ston"],  $insert ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

		// Feedback for the end user
		$html .= "<pre>Password Changed.</pre>";
	}
	else {
		// Issue with the passwords matching
		$html .= "<pre>Passwords did not match.</pre>";
		$hide_form = false;
	}

	((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

?>

代碼有點長,但是可以明顯看出來,這個機制是很簡單的。整個驗證邏輯將驗證分為兩步:Step1,Step2,而對於captcha驗證的邏輯只存在於Step1中的小小一段:

既然所有驗證邏輯都只存在於Step1,那么如果我直接繞過它,有可能嗎?

其實非常簡單,這里我們要用到抓包-改包-重發的方法,ZAP已經給我們提供了“請求斷點”功能。

點擊上圖中綠色斷點按鈕,ZAP就進入請求斷點狀態,在此狀態下ZAP不再簡單的將客戶端和服務器之間的請求交互轉發,而是像其他編程工具的斷點功能一樣,讓請求反饋變為單步執行。那么我們就可以在請求發出,尚未傳遞至服務器之前,對請求內容進行篡改:

改包重發的結果:

密碼修改成功,而這整個過程中我們完全沒有去處理captcha的驗證碼,也就是說這個驗證碼被完全繞過了!

3. DVWA的驗證碼機制完善防御

既然驗證碼邏輯是有可能被繞過,接下來我們來研究一下,如何建立更完善的機制呢。

Medium級別

<?php

if( isset( $_POST[ 'Change' ] ) && ( $_POST[ 'step' ] == '1' ) ) {
	// Hide the CAPTCHA form
	$hide_form = true;

	// Get input
	$pass_new  = $_POST[ 'password_new' ];
	$pass_conf = $_POST[ 'password_conf' ];

	// Check CAPTCHA from 3rd party
	$resp = recaptcha_check_answer(
		$_DVWA[ 'recaptcha_private_key' ],
		$_POST['g-recaptcha-response']
	);

	// Did the CAPTCHA fail?
	if( !$resp ) {
		// What happens when the CAPTCHA was entered incorrectly
		$html     .= "<pre><br />The CAPTCHA was incorrect. Please try again.</pre>";
		$hide_form = false;
		return;
	}
	else {
		// CAPTCHA was correct. Do both new passwords match?
		if( $pass_new == $pass_conf ) {
			// Show next stage for the user
			$html .= "
				<pre><br />You passed the CAPTCHA! Click the button to confirm your changes.<br /></pre>
				<form action=\"#\" method=\"POST\">
					<input type=\"hidden\" name=\"step\" value=\"2\" />
					<input type=\"hidden\" name=\"password_new\" value=\"{$pass_new}\" />
					<input type=\"hidden\" name=\"password_conf\" value=\"{$pass_conf}\" />
					<input type=\"hidden\" name=\"passed_captcha\" value=\"true\" />
					<input type=\"submit\" name=\"Change\" value=\"Change\" />
				</form>";
		}
		else {
			// Both new passwords do not match.
			$html     .= "<pre>Both passwords must match.</pre>";
			$hide_form = false;
		}
	}
}

if( isset( $_POST[ 'Change' ] ) && ( $_POST[ 'step' ] == '2' ) ) {
	// Hide the CAPTCHA form
	$hide_form = true;

	// Get input
	$pass_new  = $_POST[ 'password_new' ];
	$pass_conf = $_POST[ 'password_conf' ];

	// Check to see if they did stage 1
	if( !$_POST[ 'passed_captcha' ] ) {
		$html     .= "<pre><br />You have not passed the CAPTCHA.</pre>";
		$hide_form = false;
		return;
	}

	// Check to see if both password match
	if( $pass_new == $pass_conf ) {
		// They do!
		$pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
		$pass_new = md5( $pass_new );

		// Update database
		$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
		$result = mysqli_query($GLOBALS["___mysqli_ston"],  $insert ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

		// Feedback for the end user
		$html .= "<pre>Password Changed.</pre>";
	}
	else {
		// Issue with the passwords matching
		$html .= "<pre>Passwords did not match.</pre>";
		$hide_form = false;
	}

	((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

?>

主要的機制在這里:

加入了驗證第一步是否通過的判斷。

唔。。。其實沒什么變化,同樣改包重發一鍵搞定,無非是多加了一個參數:

High級別

<?php

if( isset( $_POST[ 'Change' ] ) ) {
	// Hide the CAPTCHA form
	$hide_form = true;

	// Get input
	$pass_new  = $_POST[ 'password_new' ];
	$pass_conf = $_POST[ 'password_conf' ];

	// Check CAPTCHA from 3rd party
	$resp = recaptcha_check_answer(
		$_DVWA[ 'recaptcha_private_key' ],
		$_POST['g-recaptcha-response']
	);

	if (
		$resp || 
		(
			$_POST[ 'g-recaptcha-response' ] == 'hidd3n_valu3'
			&& $_SERVER[ 'HTTP_USER_AGENT' ] == 'reCAPTCHA'
		)
	){
		// CAPTCHA was correct. Do both new passwords match?
		if ($pass_new == $pass_conf) {
			$pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
			$pass_new = md5( $pass_new );

			// Update database
			$insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "' LIMIT 1;";
			$result = mysqli_query($GLOBALS["___mysqli_ston"],  $insert ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

			// Feedback for user
			$html .= "<pre>Password Changed.</pre>";

		} else {
			// Ops. Password mismatch
			$html     .= "<pre>Both passwords must match.</pre>";
			$hide_form = false;
		}

	} else {
		// What happens when the CAPTCHA was entered incorrectly
		$html     .= "<pre><br />The CAPTCHA was incorrect. Please try again.</pre>";
		$hide_form = false;
		return;
	}

	((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

// Generate Anti-CSRF token
generateSessionToken();

?>

High級別的變動還比較多:

  • 驗證改成了單步

  • 加入了另一個參數'g-recaptcha-response'

  • 加入驗證user-agent

  • 加入Anti-CSRF-Token(本文雖未提及,但其實前面兩個級別通過CSRF攻擊也可以實現攻擊,可以參考上一篇中的方法)

通過前兩兩個級別的攻破,我們應該知道,增加的這個參數根本沒啥用;而user-agent也是完全可以改包的。

改包如下即可繞過:

Impossible

<?php

if( isset( $_POST[ 'Change' ] ) ) {
	// Check Anti-CSRF token
	checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );

	// Hide the CAPTCHA form
	$hide_form = true;

	// Get input
	$pass_new  = $_POST[ 'password_new' ];
	$pass_new  = stripslashes( $pass_new );
	$pass_new  = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
	$pass_new  = md5( $pass_new );

	$pass_conf = $_POST[ 'password_conf' ];
	$pass_conf = stripslashes( $pass_conf );
	$pass_conf = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $pass_conf ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
	$pass_conf = md5( $pass_conf );

	$pass_curr = $_POST[ 'password_current' ];
	$pass_curr = stripslashes( $pass_curr );
	$pass_curr = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $pass_curr ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
	$pass_curr = md5( $pass_curr );

	// Check CAPTCHA from 3rd party
	$resp = recaptcha_check_answer(
		$_DVWA[ 'recaptcha_private_key' ],
		$_POST['g-recaptcha-response']
	);

	// Did the CAPTCHA fail?
	if( !$resp ) {
		// What happens when the CAPTCHA was entered incorrectly
		$html .= "<pre><br />The CAPTCHA was incorrect. Please try again.</pre>";
		$hide_form = false;
		return;
	}
	else {
		// Check that the current password is correct
		$data = $db->prepare( 'SELECT password FROM users WHERE user = (:user) AND password = (:password) LIMIT 1;' );
		$data->bindParam( ':user', dvwaCurrentUser(), PDO::PARAM_STR );
		$data->bindParam( ':password', $pass_curr, PDO::PARAM_STR );
		$data->execute();

		// Do both new password match and was the current password correct?
		if( ( $pass_new == $pass_conf) && ( $data->rowCount() == 1 ) ) {
			// Update the database
			$data = $db->prepare( 'UPDATE users SET password = (:password) WHERE user = (:user);' );
			$data->bindParam( ':password', $pass_new, PDO::PARAM_STR );
			$data->bindParam( ':user', dvwaCurrentUser(), PDO::PARAM_STR );
			$data->execute();

			// Feedback for the end user - success!
			$html .= "<pre>Password Changed.</pre>";
		}
		else {
			// Feedback for the end user - failed!
			$html .= "<pre>Either your current password is incorrect or the new passwords did not match.<br />Please try again.</pre>";
			$hide_form = false;
		}
	}
}

// Generate Anti-CSRF token
generateSessionToken();

?>

Impossible級別主要的改動在於,移除了High級別中多余的g-recaptcha-response參數判斷,而只采用CAPTCHA本身的驗證結果進行判斷,並且要求輸入原始密碼。這樣要繞過驗證碼就基本不可能了。

3. 驗證碼機制測試

話題再回到測試,如本文開頭所說,驗證碼的主要作用就是防止所謂的"機器人" - 即計算機自動程序。

在驗證碼機制推行起來之前,許多知名網站都經受過“機器人”注冊的攻擊。由於“機器人”可能在短時間內大量調用系統功能,因此經常導致服務器宕機以及垃圾數據。

我們之前提到過的字典式破解等攻擊方式,也可以通過驗證碼進行防御。

不過現在隨着人工智能的發展,驗證碼的破解,圖形解析的技術門檻越來越低,圖形類驗證碼的破解已經不是很難的事情了。

而且通過此文,我們也應該知道,現在各大系統的驗證碼一般通過接口調用實現,而一般來說驗證碼的處理邏輯則是獨立的。而這個處理邏輯則是安全驗證和測試的主要要點,如果邏輯設計不合理,驗證碼就會變成徒勞。如本文所示,其實我們完全沒有去處理驗證碼破解的問題。

題外話

對於UI自動化而言,我們也會遇到驗證碼的問題。那么UI自動化中應該如何處理驗證碼呢。

要知道做為一種圖靈測試機制,驗證碼防御的就是類似selenium這樣的計算機自動化程序,即“機器人”。我們做UI自動化有沒有必要去引入驗證碼破解機制予以破解?

個人認為,沒有這個必要。

    1. 如果你使用自動化的手段破解了驗證碼,那么只能說明你們系統的驗證碼是廢物!要更新升級!直到你破不掉為止。
    1. 破解驗證碼需要額外的代碼量和技術手段,費力不討好
    1. 圖形驗證碼也許現在的技術手段可以破解,但是類似谷歌的第三代captcha驗證,現在還沒有完美的破解手段。

所以,如果UI自動化中遇到驗證碼怎么辦?

其實很簡單,與開發協商將測試環境調為測試模式,即關閉驗證碼功能即可。驗證碼功能可以單獨予以測試。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM