安全性测试入门 (五):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自动化中遇到验证码怎么办?

其实很简单,与开发协商将测试环境调为测试模式,即关闭验证码功能便可。验证码功能能够单独予以测试。

相关文章
相关标签/搜索