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

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

Insecure Captcha不安全驗證碼html

1. 驗證碼究竟是怎麼一回事

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

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

Captcha在各類海外網站被普遍用於用戶驗證。而在國內,因爲衆所周知的緣由,咱們不用谷歌的服務,不少接口平臺均可以提供相似服務。編程

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

那麼驗證碼到底在用戶驗證的過程當中起到什麼樣的做用呢?安全

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

就如Captcha的全稱所示,他就是用來區分人類和計算機的一種圖靈測試,這種作法能夠頗有效的防止惡意軟件、機器人大量調用系統功能:好比註冊、登陸功能。session

咱們前面講到的Brute Force字典式暴力破解,就必需要使用工具大量嘗試登陸。若是這個時候系統有個嚴密的驗證碼機制,此類攻擊就機關用盡了。app

其工做流程以下所示:

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自動化中遇到驗證碼怎麼辦?

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

相關文章
相關標籤/搜索