安全性测试入门 (五):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自动化有没有必要去引入验证码破解机制予以破解?
个人认为,没有这个必要。
- 如果你使用自动化的手段破解了验证码,那么只能说明你们系统的验证码是废物!要更新升级!直到你破不掉为止。
- 破解验证码需要额外的代码量和技术手段,费力不讨好
- 图形验证码也许现在的技术手段可以破解,但是类似谷歌的第三代captcha验证,现在还没有完美的破解手段。
所以,如果UI自动化中遇到验证码怎么办?
其实很简单,与开发协商将测试环境调为测试模式,即关闭验证码功能即可。验证码功能可以单独予以测试。
安全性测试入门 (五):Insecure CAPTCHA 验证码绕过的更多相关文章
- 安全性测试入门:DVWA系列研究(一):Brute Force暴力破解攻击和防御
写在篇头: 随着国内的互联网产业日臻成熟,软件质量的要求越来越高,对测试团队和测试工程师提出了种种新的挑战. 传统的行业现象是90%的测试工程师被堆积在基本的功能.系统.黑盒测试,但是随着软件测试整体 ...
- 安全性测试入门:DVWA系列研究(二):Command Injection命令行注入攻击和防御
本篇继续对于安全性测试话题,结合DVWA进行研习. Command Injection:命令注入攻击. 1. Command Injection命令注入 命令注入是通过在应用中执行宿主操作系统的命令, ...
- 安全性测试入门 (四):Session Hijacking 用户会话劫持的攻击和防御
本篇继续对于安全性测试话题,结合DVWA进行研习. Session Hijacking用户会话劫持 1. Session和Cookies 这篇严格来说是用户会话劫持诸多情况中的一种,通过会话标识规则来 ...
- 安全性测试入门 (三):CSRF 跨站请求伪造攻击和防御
本篇继续对于安全性测试话题,结合DVWA进行研习. CSRF(Cross-site request forgery):跨站请求伪造 1. 跨站请求伪造攻击 CSRF则通过伪装成受信任用户的请求来利用受 ...
- DVWA-全等级验证码Insecure CAPTCHA
DVWA简介 DVWA(Damn Vulnerable Web Application)是一个用来进行安全脆弱性鉴定的PHP/MySQL Web应用,旨在为安全专业人员测试自己的专业技能和工具提供合法 ...
- DVWA 黑客攻防演练(六)不安全的验证码 Insecure CAPTCHA
之前在 CSRF 攻击 的那篇文章的最后,我觉得可以用验证码提高攻击的难度. 若有验证码的话,就比较难被攻击者利用 XSS 漏洞进行的 CSRF 攻击了,因为要识别验证码起码要调用api,跨域会被浏览 ...
- DVWA全级别之Insecure CAPTCHA(不安全的验证码)
Insecure CAPTCHA Insecure CAPTCHA,意思是不安全的验证码,CAPTCHA是Completely Automated Public Turing Test to Tell ...
- Insecure CAPTCHA (不安全的验证码)
dvwa不能正常显示,需要在配置文件中加入谷歌的密钥: $_DVWA[ 'recaptcha_public_key' ] = '6LfX8tQUAAAAAOqhpvS7-b4RQ_9GVQIh48dR ...
- 安全性测试:OWASP ZAP使用入门指南
免责声明: 本文意在讨论使用工具来应对软件研发领域中,日益增长的安全性质量测试需求.本文涉及到的工具不可被用于攻击目的. 1. 安全性测试 前些天,一则12306用户账号泄露的新闻迅速发酵,引起了购票 ...
随机推荐
- 杂项-Log:NLog
ylbtech-杂项-Log:NLog NLog是一个基于.NET平台编写的类库,我们可以使用NLog在应用程序中添加极为完善的跟踪调试代码. NLog是一个简单灵活的.NET日志记录类库.通过使用N ...
- linux命令-vim编辑模式
按 i 键 进去编辑模式 左下角显示 插入 按 I 键 进入编辑模式 光标到行首 按 a 键 在光标的后一位 按A 键 光标在行尾 按 o 键 在光标下面另起一行 按O 键 在光标上面另起一行 ...
- maven spring3.2.5
出现的情形: 开发环境: spring3.2.5 + springmvc +spirngDATA +maven 一. 偶然的spring Junit4测试 加载applicationContext.x ...
- Loadrunner 监控 Linux (centos6.5)服务器系统资源
Loadrunner 监控 Linux 服务器系统资源,需要在被监控的服务器上启用 rstatd 进程但尝试启动时,爆炸了: [root@test1 rpc.rstatd-4.0.1]# rpc.rs ...
- Tomcat+Nginx实现动静分离
Tomcat是我们经常用的服务器,轻便快捷,但是数据量大的时候,会影响访问.响应速度,这时Nginx就出现了. Nginx可做反向代理.负载均衡.动态与静态资源的分离的工作,这里我们就用它来做动静分离 ...
- 树莓派 Learning 001 装机 ---之 1 安装NOOBS系统
树莓派安装NOOBS系统 (使用的树莓派板卡型号:Raspberry Pi 2 Model B V1.1)(板卡的型号在板子正面的丝印层上印着,你可以看到.) RASPBERRY PI 2 MODEL ...
- 怎么触发gridview 的SelectedIndexChanged事件?
<asp:GridView onclick="javascript:SelectedIndexChanged()" ID="GridView1" runa ...
- nkv客户端性能调优
此文已由作者张洪箫授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 问题描述 随着考拉业务的增长和规模的扩大,很多的应用都开始重度依赖缓存服务,也就是杭研的nkv.但是在使用过 ...
- JSONCPP开发环境搭建
环境设置 项目地址 https://github.com/open-source-parsers/jsoncpp.git 操作系统 64位 Fedora 24 安装jsoncpp $ git clon ...
- Python中的循环语句
Python中有while循环和for循环 下面以一个小例子来说明一下用法,用户输入一些数字,输出这些数字中的最大值和最小值 array = [5,4,3,1] for i in array: pri ...