安全性测试⼊门(五):InsecureCAPTCHA验证码绕过
本篇继续对于安全性测试话题,结合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;
}
html怎么实现登录验证功能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( $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)) // 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( $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)) // 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 $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)) // 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( $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( $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() $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. 如果你使⽤⾃动化的⼿段破解了验证码,那么只能说明你们系统的验证码是废物!要更新升级!直到你破不掉为⽌。
2. 破解验证码需要额外的代码量和技术⼿段,费⼒不讨好
3. 图形验证码也许现在的技术⼿段可以破解,但是类似⾕歌的第三代captcha验证,现在还没有完美的破解⼿段。
所以,如果UI⾃动化中遇到验证码怎么办?
其实很简单,与开发协商将测试环境调为测试模式,即关闭验证码功能即可。验证码功能可以单独予以测试。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论