验证码绕过漏洞剖析:从res_code参数篡改看服务端校验缺失

验证码绕过漏洞剖析:从res_code参数篡改看服务端校验缺失 1. 项目概述验证码绕过的攻防本质在Web应用安全测试中验证码CAPTCHA一直被视为一道重要的自动化防护屏障用于区分人类用户和机器脚本。然而这道屏障并非固若金汤。今天要聊的这个场景——“通过修改数据包中res_code的值实现验证码绕过”——就是一个非常典型且在实际渗透测试中高频出现的业务逻辑漏洞。它暴露的不仅仅是代码层面的疏忽更是开发人员在设计“状态验证”这一核心逻辑时的思维盲区。简单来说当你在一个登录或注册页面输入正确的验证码后前端通常会向服务器发送一个包含验证码校验结果的请求。如果服务器端天真地相信前端传来的“校验结果”比如一个名为res_code的参数值为success或200而不是在服务端重新、独立地比对用户输入的验证码与Session中存储的真实值那么攻击者就可以通过抓包工具拦截这个请求并将res_code的值手动修改为“成功”的状态码从而欺骗服务器实现绕过验证码校验的目的。这听起来似乎很简单甚至有些“低级”但恰恰是这种对客户端数据无条件的信任构成了大量中低风险业务安全漏洞的源头。对于安全测试人员而言理解并掌握这类漏洞的挖掘、验证与修复思路是构建完整业务安全测试能力拼图的关键一块。它不仅适用于验证码场景其背后“服务端状态校验缺失”的核心思想可以延伸到短信轰炸、订单金额篡改、权限越权等众多业务环节。2. 漏洞原理深度剖析信任的边界在哪里要理解这个漏洞我们必须深入到一次完整的验证码校验交互流程中去看。一个健壮的校验流程通常包含以下服务器端逻辑生成与存储用户请求验证码图片时服务器生成一个随机字符串或算式结果将其存入服务器端的Session或Redis等缓存中并将对应的图片返回给前端。用户提交用户填写验证码连同其他表单数据如用户名、密码一并提交。服务端校验服务器接收到请求后执行两个关键动作动作A关键从当前用户的Session中取出之前存储的正确的验证码值。动作B关键比对用户提交的验证码值与Session中取出的值是否一致。返回结果根据比对结果返回成功或失败的响应。漏洞就出现在第3步的“返回结果”之前或者更准确地说出现在对“校验”这个动作的抽象上。有漏洞的程序逻辑错误地将“校验”这个动作及其结果“外包”给了前端或者在前端与后端之间引入了一个不可靠的“中间状态”。具体表现为错误模式一前端计算后端信任这是最直接的漏洞形式。前端JavaScript在用户输入后本地计算输入是否与某个值匹配这个值可能被硬编码在前端或由上一个响应返回然后将计算结果如res_code200发送给后端。后端看到200就直接认为验证通过完全跳过了与服务端Session比对的环节。错误模式二冗余的状态参数这是本次案例的典型场景。后端虽然进行了实际的校验但在返回给前端的响应数据中包含了一个明确的校验状态码例如{“status”: “success”, “res_code”: 200}。随后前端在发起下一个正式业务请求如登录请求时错误地将这个“结果状态”作为参数之一如res_code200又传回了后端。后端在处理这个新请求时没有再次校验验证码而是直接读取了res_code参数的值来判断之前的验证码是否成功。攻击者只需在第一次收到响应后在后续的请求包里篡改res_code为成功值即可。错误模式三校验与业务逻辑分离验证码校验是一个独立的API接口如/api/verify_captcha返回校验令牌token。业务接口如/api/login需要携带这个token。漏洞在于业务接口在验证token时仅仅检查了token是否存在或格式是否正确却没有去校验这个token在服务器端的有效性、是否已被使用、是否与当前会话绑定。攻击者可以截获一个合法的token重复使用或篡改后用于其他请求。核心要点安全的基石在于“服务端状态不可被客户端预言或篡改”。任何最终决定业务能否进行的判断依据必须是服务器端基于其自身存储的、可信的状态信息实时计算得出的绝不能依赖于客户端传来的、表示“过去某个动作结果”的参数。2.1 关键参数res_code的常见形态与含义在实际测试中像res_code这样的参数可能以各种名称出现其值也多种多样。理解这些变体有助于我们更快地识别漏洞点。参数常见名称可能取值成功状态可能取值失败状态说明res_code200,0,success,true,1500,-1,fail,false,0直接表示操作结果代码是最常见的命名。statussuccess,ok,1,trueerror,fail,0,false表示状态与res_code语义类似。code200,1000,0400,500,1001HTTP状态码的扩展或自定义业务码。verify/checkedtrue,1,yesfalse,0,no明确表示“是否已验证”的布尔型字段。token/captcha_token一串加密字符串如JWTnull, 空值令牌本身可能有效但漏洞在于服务端未校验其使用状态或绑定关系。is_correcttrue,1false,0直白地表示“是否正确”。攻击者的目标就是在向服务器发送的、触发核心业务逻辑的请求中找到这类参数并将其值修改为“成功”或“已通过”的形态。3. 实战测试流程与工具使用详解理论清晰后我们进入实战环节。整个测试过程可以概括为观察 - 拦截 - 修改 - 重放。下面以一次模拟的“用户登录绕过图形验证码”测试为例详细拆解每个步骤。3.1 环境准备与工具选型测试环境我们需要一个目标Web应用。可以是自行搭建的带有漏洞的演示平台如DVWA、bWAPP、Pikachu漏洞练习平台或者在授权范围内进行测试的真实业务。这里假设我们有一个目标登录页http://target.com/login该页面在密码输入框上方有一个图形验证码。核心工具代理抓包工具这是我们的“手术刀”。必备选择是Burp Suite社区版或专业版。它是Web安全测试的瑞士军刀拦截、修改、重放功能齐全。替代品有OWASP ZAP免费开源功能类似。浏览器任何现代浏览器均可配合代理工具使用。通常使用Chrome或Firefox因为它们对开发者工具支持完善便于前期分析。可选辅助工具drissionpage是一个较新的Python库它融合了浏览器自动化和网络请求操控。在热词中它被提及用于“监听数据包”这意味着你可以编写脚本用drissionpage驱动浏览器并直接监听、修改其网络请求适合自动化测试场景。但对于手动深入测试和学习原理Burp Suite的交互式操作更直观。为什么选择Burp Suite因为它提供了完整的闭环测试环境Proxy拦截所有流量Repeater允许我们对单个请求进行反复修改和重放Intruder可以对参数进行暴力破解Scanner能进行主动漏洞扫描。对于验证码绕过这种需要精细修改参数的测试Burp Repeater是绝佳场所。3.2 第一步正常业务流程抓取与分析在开始“攻击”前必须先了解“正常”的流程。配置代理打开Burp Suite在Proxy - Options中确保代理监听端口默认8080是开启的。在浏览器中设置网络代理为127.0.0.1:8080端口8080。安装并信任Burp Suite的CA证书在http://burp可下载以便拦截HTTPS流量。访问登录页浏览器访问http://target.com/login。此时Burp Suite的Proxy - Intercept应该是“Intercept is on”状态但我们可以先把它关掉到HTTP history中查看记录。获取验证码通常验证码图片是由一个独立接口加载的例如GET /api/captcha/image?t123456789。在Burp的HTTP history中你会看到这个请求和响应。关键点服务器在这个响应里可能会在Cookie中设置一个Session ID用于后续关联验证码文本。验证码文本本身不应该在响应体中返回。完成一次正常登录在浏览器中输入用户名、密码、正确的验证码点击登录。此时打开Burp Proxy的拦截Intercept is on让请求被暂停在Burp中。你会看到一个POST请求可能发往/api/login或/login。仔细分析这个请求的各个部分Headers关注Cookie里面包含了你的会话标识。Params/Body这里是我们关注的重点。通常你会看到usernameadminpassword123456captchaAB12这样的表单数据。现在你要寻找除了captcha之外是否还有另一个表示“验证码校验结果”的参数比如res_code、verify_status等。在正常正确的请求中这个参数可能不存在或者其值就是正确的如res_code200。我们的任务是找到它并尝试修改它。3.3 第二步定位与修改可疑参数假设在正常请求中我们发现了如下请求体usernametestpasswordtest123captchaAB12res_code200这非常可疑为什么在提交验证码的同时还需要一个res_code来告诉服务器验证码对不对这很可能就是漏洞点。测试操作发送到Repeater在Burp Proxy的拦截界面右键点击该请求选择 “Send to Repeater”。这样我们可以在Repeater标签页里随意修改并重复发送这个请求而不会影响浏览器会话。构造错误验证码请求首先我们验证一下服务端正常的校验逻辑。将captcha参数的值改为一个明显错误的比如captchaWRONG保持res_code200不变。点击“Send”。分析响应情况A存在漏洞服务器返回了登录成功的响应如跳转主页、返回{“code”:200, “msg”:”登录成功”}。这说明服务器完全依赖res_code参数根本没有检查captcha参数。漏洞实锤。情况B可能存在漏洞服务器返回验证码错误如{“code”:400, “msg”:”验证码错误”}。这还不能断定需要进一步测试。深入测试针对情况B将captcha参数改回正确的AB12但把res_code修改为失败的值比如res_code400或res_codeerror。再次发送请求。结果B1服务器返回登录成功。这说明服务器虽然校验了验证码但在最终决策时res_code参数权重更高或者流程存在逻辑错误仍然可被绕过。结果B2服务器返回验证码错误或参数错误。这说明服务器可能同时校验了这两个参数且逻辑为“与”关系。此时可以尝试直接删除captcha参数只保留username,password,res_code200进行测试。如果成功说明验证码校验环节可以被整个跳过。3.4 第三步绕过验证码实现未授权操作一旦确认修改res_code可以导致验证码校验失效攻击路径就清晰了。固定攻击请求在Burp Repeater中构造一个稳定的攻击请求。例如POST /api/login HTTP/1.1 Host: target.com Cookie: sessionidxyz123abc Content-Type: application/x-www-form-urlencoded usernameattackerpasswordguess123res_code200注意这里我们完全移除了captcha参数或者将其置空只保留被我们篡改为成功值的res_code参数。重放与验证多次点击“Send”。观察响应是否始终为登录成功。如果成功意味着攻击者可以在不知道验证码的情况下对任意用户名进行密码爆破如果密码较弱或者直接登录已知账号。扩展到其他场景同样的思路可以测试其他业务点注册注册时发送短信或邮箱验证码提交时是否有一个sms_code_verifiedtrue的参数密码重置重置密码时验证身份后提交新密码的请求是否携带identity_verified1支付确认订单时是否有一个price_checkedtrue的参数修改后可以绕过金额校验这属于业务逻辑漏洞的另一个大类篡改实操心得在测试时不要只盯着res_code。用Burp的“查找”功能CtrlF在整个请求原始报文中搜索success、true、200、ok等关键词可能会发现隐藏在HTTP头部、Cookie甚至JSON结构其他字段中的类似状态标识。我曾在一个项目的Authorization头里发现过一个X-Verify-Status: passed的自定义头修改它直接绕过了两步验证。4. 漏洞挖掘的进阶技巧与自动化思路手动测试能解决单点问题但面对大型应用我们需要更高效的方法。4.1 使用Burp Suite插件辅助探测Burp Scanner主动扫描引擎在审计时有时能发现这类逻辑漏洞但其主要擅长于注入、跨站等对业务逻辑漏洞的检测能力有限。Autorize这是一个强大的授权测试插件。虽然它主要用于越权测试但其原理是修改请求中的身份标识如用户ID。我们可以借鉴其思想配置一个“请求修改”规则自动为每一个经过Burp的请求添加或修改一个res_code200的参数然后观察应用响应是否发生变化。这能帮助我们进行批量、被动的漏洞探测。Custom Plugin如果你熟悉Java或Python通过Burp Extender API可以编写一个简单的插件自动检测请求参数中是否包含code、status、verify等关键字并在Repeater中高亮提示提升测试效率。4.2 结合drissionpage进行自动化监听与篡改热词中提到了drissionpage这是一个有趣的工具。它允许你用Python像Selenium一样控制浏览器但同时又能像Requests库一样直接操作网络请求。对于验证码绕过测试可以设计如下自动化脚本思路from drissionpage import ChromiumPage, ChromiumOptions from urllib.parse import urlparse, parse_qs, urlencode import re # 创建浏览器页面对象 co ChromiumOptions().headless(False) # 非无头模式方便观察 page ChromiumPage(addr_or_optsco) # 监听网络请求 page.listen.start(api/login) # 监听登录接口 # 访问目标登录页 page.get(http://target.com/login) # 手动或自动识别验证码此处需接入OCR或手动输入演示用假设 captcha_text input(请查看浏览器并输入验证码: ) # 填写表单并提交假设元素id已知 page.ele(#username).input(test_user) page.ele(#password).input(test_pass) page.ele(#captcha).input(captcha_text) page.ele(#submit-btn).click() # 等待并捕获到登录请求 packet page.listen.wait() request packet.request # 获取请求对象 # 分析请求体寻找并修改 res_code body request.body # 可能是字符串或字典 if isinstance(body, str): # 如果是表单字符串解析并修改 params parse_qs(body) params[res_code] [200] # 修改或添加 res_code new_body urlencode(params, doseqTrue) elif isinstance(body, dict): # 如果是JSON直接修改字典 body[res_code] 200 new_body body else: new_body body # 使用修改后的请求体重新发起请求绕过浏览器直接发包 response page.request.修改请求方法(request.url, datanew_body, headersrequest.headers) # 分析响应判断是否绕过成功 if 登录成功 in response.text or response.status_code 302: print([] 验证码绕过成功) else: print([-] 绕过失败。)这个脚本模拟了用户行为但在最后一刻截获请求并修改了关键参数。这种方法将浏览器自动化与网络层攻击结合非常适合测试需要前端交互触发、但漏洞点在后端逻辑的复杂场景。4.3 流量对比分析正确 vs 错误这是一种非常有效的手动分析技术。在Burp Suite中分别完成一次成功的登录输入正确验证码和一次失败的登录输入错误验证码。在Proxy history中找到这两个登录请求。使用Burp的“Compare”功能选中两个请求右键 - Compare进行请求差异对比。Burp会高亮显示两个请求之间的不同之处。除了captcha参数值不同你是否能看到其他参数如res_code,token,_csrf等也存在差异这些差异点就是潜在的校验状态参数。尝试在错误请求中将这些参数修改为成功请求中的值然后重放。5. 防御方案设计与开发指南作为测试人员我们不仅要会挖洞更要能提出切实可行的修复方案。针对“修改状态参数绕过”这类漏洞核心防御思想是在服务端关键业务逻辑执行点进行独立的、基于服务器可信数据的校验绝不信任客户端传来的任何“结果声明”。5.1 安全的验证码校验架构以下是一个推荐的安全校验流程伪代码以登录为例# 1. 生成验证码接口 (GET /api/captcha) def generate_captcha(request): session_id request.session.session_key captcha_text random_string(4) # 生成4位随机码 # 将验证码文本与session_id绑定存入缓存设置短有效期如60秒 cache.set(fcaptcha:{session_id}, captcha_text, timeout60) # 生成图片返回给前端。文本绝不返回。 image create_image(captcha_text) return HttpResponse(image, content_typeimage/png) # 2. 登录接口 (POST /api/login) def login(request): username request.POST.get(username) password request.POST.get(password) user_input_captcha request.POST.get(captcha) # 只接收用户输入的原始验证码 # 注意这里没有 res_code 参数 # 第一步独立校验验证码 session_id request.session.session_key real_captcha cache.get(fcaptcha:{session_id}) if not real_captcha: return JsonResponse({code: 400, msg: 验证码已过期}) if user_input_captcha.lower() ! real_captcha.lower(): # 忽略大小写 # 可选校验失败后立即清除缓存防止暴力破解 cache.delete(fcaptcha:{session_id}) return JsonResponse({code: 400, msg: 验证码错误}) # 第二步验证码校验通过后立即清除本次使用的验证码确保一次性使用 cache.delete(fcaptcha:{session_id}) # 第三步执行后续业务逻辑如密码校验 user authenticate(usernameusername, passwordpassword) if user is not None: login(request, user) return JsonResponse({code: 200, msg: 登录成功}) else: return JsonResponse({code: 400, msg: 用户名或密码错误})关键防御点状态服务器存储验证码真实值存储在服务器缓存Redis/Memcached中键名与用户会话强绑定如使用session id。客户端只传原始输入前端只提交用户肉眼识别后输入的验证码字符串不提交任何关于“对错”的衍生状态。服务端实时比对在业务逻辑入口处服务器从自己的缓存中取出真值与客户端输入进行比对。这个比对动作是实时的、不可绕过的。一次一用立即失效无论校验成功与否只要参与了比对就应该立即使该验证码失效删除缓存防止重放攻击。5.2 针对“校验令牌Token”模式的安全加固如果业务设计必须使用前端先校验、后传token的模式例如为了减轻服务器压力或实现异步验证则必须保证token的不可伪造性和状态性。# 1. 专用验证码校验接口 (POST /api/verify_captcha) def verify_captcha(request): user_input request.POST.get(captcha) session_id request.session.session_key real_captcha cache.get(fcaptcha:{session_id}) if user_input and real_captcha and user_input.lower() real_captcha.lower(): # 生成一个随机的、唯一的、与当前会话和时间绑定的令牌 verify_token generate_secure_token(session_id, timestamptime.time()) # 将令牌存入缓存并标记为“已验证-未使用”状态值可以是 verified_unused cache.set(fverify_token:{verify_token}, {session_id: session_id, status: verified_unused}, timeout300) # 清除原始验证码缓存 cache.delete(fcaptcha:{session_id}) return JsonResponse({code: 200, token: verify_token}) else: return JsonResponse({code: 400, msg: 验证码错误}) # 2. 业务接口 (POST /api/login) def login(request): verify_token request.POST.get(verify_token) # 前端传递上一步获得的token # 校验token token_data cache.get(fverify_token:{verify_token}) if not token_data: return JsonResponse({code: 400, msg: 无效或过期的验证令牌}) if token_data[status] ! verified_unused: return JsonResponse({code: 400, msg: 验证令牌已被使用}) if token_data[session_id] ! request.session.session_key: return JsonResponse({code: 400, msg: 会话不匹配}) # 防止token被跨会话使用 # token校验通过立即将其标记为已使用或删除 cache.delete(fverify_token:{verify_token}) # 或修改状态为 used # 然后继续执行用户名密码校验等业务逻辑...这种模式通过一个具有状态未使用/已使用且与会话绑定的临时令牌将“校验动作”和“使用结果”解耦同时保证了后端对最终状态的绝对控制。5.3 补充性防御措施请求完整性校验对关键请求参数计算签名HMAC签名密钥仅服务器知晓。客户端提交参数和签名服务器端重新计算并比对。这样任何对参数的篡改都会导致签名无效。但这会增加前后端复杂度。时间戳与随机数防重放在请求中加入时间戳和随机数Nonce服务器校验请求是否在有效时间窗口内并检查随机数是否已被使用可以有效防御重放攻击但同样无法防御单次请求内的参数篡改。安全开发意识培训这是根本。让开发人员理解“永不信任客户端输入”这一基本原则在代码审查中特别关注所有来自客户端的、表示“状态”、“结果”、“是否”的参数。6. 测试中的常见陷阱与排查实录在实际测试中事情往往不会像示例一样顺利。以下是一些常见的坑和排查思路。问题1修改了res_code但服务器依然返回验证码错误。排查检查Cookie/Session验证码很可能与当前会话Session绑定。你抓包修改请求时使用的Cookie是否依然是获取验证码时那个会话的Cookie如果会话已过期或不匹配服务器自然校验失败。确保从获取验证码到发起登录请求整个流程在同一个浏览器会话或Burp Suite维护的同一个会话中完成。检查其他隐藏参数除了res_code请求中是否还有captcha_id,timestamp,nonce,sign等参数这些可能是验证码的关联ID或防篡改签名。你需要一并修改或理解其生成规则。用对比法正确 vs 错误请求找出所有差异点。验证码可能是一次性的有些系统在验证码校验成功后无论对错都会立即使该验证码失效。你第一次用正确验证码测试后该验证码就已作废。第二次测试时即使修改res_code服务器发现验证码ID无效也会返回错误。测试时确保每次使用全新的验证码。问题2没有找到明显的res_code参数。排查查看响应而非请求漏洞可能不在登录请求里而在获取验证码的响应里。服务器可能把校验结果放在获取验证码接口的响应头或响应体中如X-Captcha-Valid: true然后前端在下次请求时原样带回。你需要检查获取验证码图片的那个HTTP响应。检查JSON结构如果请求体是JSON格式仔细查看JSON的每一个字段。可能不叫res_code而是verification.status、result.code等嵌套字段。检查HTTP头部自定义头部也是藏匿信息的好地方如X-Verification: passed。可能不是此类型漏洞如果无论如何都找不到可能目标系统采用了相对安全的服务端校验。此时应转向测试验证码的其他弱点如识别OCR、重复使用、时间窗口、逻辑缺陷如验证码与手机号不绑定等。问题3使用自动化脚本如drissionpage时请求发送后浏览器页面状态异常。排查上下文隔离drissionpage直接发送的请求可能不会更新浏览器页面的内部状态如JavaScript变量、Cookie。虽然服务器端登录成功了但浏览器页面并不知道。后续需要浏览器状态的操作会失败。对于需要保持状态流的测试最好让浏览器正常完成跳转。请求头完整性确保你重放的请求包含了所有必要的头信息特别是Origin,Referer,X-Requested-With等有些服务器会校验这些头。HTTPS与证书确保你的脚本能正确处理HTTPS证书。drissionpage和requests库可能需要设置verifyFalse但在生产环境测试中需谨慎可能触发安全告警。问题4在测试环境中成功但在类似的生产环境中失败。排查架构差异生产环境可能有前置的WAFWeb应用防火墙或API网关它们会过滤或规范化请求参数可能丢弃了你的res_code参数或者检测到参数异常而拦截请求。代码版本生产环境和测试环境的代码版本可能不同漏洞可能已在生产环境修复。监控与告警你的测试流量可能触发了安全监控系统的异常行为告警。在授权测试中这是正常现象但也意味着你的测试方法已被记录。最后我想强调的是验证码绕过测试只是业务安全测试的冰山一角。它像一把钥匙打开的是“客户端可控输入影响服务器端业务决策”这扇大门。掌握这种测试思维你可以将其应用于投票刷票、优惠券重复领取、积分无限增加等无数业务场景。每一次测试不仅是寻找漏洞更是在理解开发者的思维模式与信任边界。保持好奇多问一句“如果这个参数是我来定会怎样”你会发现更多有趣的问题。