:双因素认证的流程绕过与身份绑定缺陷)
双因素认证Two-Factor Authentication2FA的目标是在密码之外增加一个独立的认证因素。即使攻击者已经获得用户密码只要无法取得第二个认证因素仍然不能完成登录。但“登录页面中出现了验证码”并不代表应用真正实现了安全的双因素认证。2FA 本质上是一个连续的认证流程服务器必须记录用户完成了哪些认证步骤并确保密码验证、验证码生成、验证码校验和最终登录始终对应同一个用户。如果应用只在页面流程上增加验证码却没有在服务端正确维护认证状态就可能出现两类典型漏洞第二因素没有被真正强制执行攻击者可以跳过验证码页面不同认证阶段没有绑定同一个用户攻击者可以把自己的认证流程切换到受害者账户。本章结合 PortSwigger Web Security Academy 中的两个实验说明这两类漏洞的原理、攻击过程以及修复方法。一、双因素认证保护的到底是什么身份认证通常依赖三类因素知识因素用户知道的信息例如密码、安全问题答案持有因素用户拥有的设备例如手机、认证器或硬件令牌固有因素用户自身的特征例如指纹、面部或行为模式。大多数网站难以直接验证生物特征因此常见的双因素认证通常组合以下两种因素用户知道的密码 用户设备上生成或接收的临时验证码常见的第二因素包括专用硬件令牌、Google Authenticator 等认证器应用以及短信验证码。专用令牌和认证器应用通常直接在设备本地生成验证码。其中认证器应用一般根据预先共享的密钥和当前时间计算 TOTP 验证码不需要通过短信网络传输。短信验证码虽然同样试图证明用户持有某个手机号码但存在短信拦截、SIM Swap 等风险。攻击者一旦接管受害者手机号就可能接收到发往该号码的验证码。双因素认证的安全收益来自两个认证因素之间的独立性。如果两个步骤实际依赖的是同一种因素那么认证强度就会降低。例如网站将验证码发送到用户邮箱但攻击者只要掌握邮箱密码就能读取验证码。在这种情况下网站密码和邮箱验证码可能都依赖“用户知道某个密码”并没有形成完全独立的两个因素。当然如果邮箱本身还受到独立设备或强认证机制保护邮箱验证码仍然可以提供额外安全性但其防护能力会依赖邮箱账户本身。因此2FA 的核心不是“让用户验证两次”而是要求用户证明两个相对独立的身份因素。二、正确的双因素认证是一套状态机安全的双因素认证不能只有“未登录”和“已登录”两个状态而应至少区分以下三个阶段未认证 ↓ 提交正确的用户名和密码 第一因素已通过等待第二因素 ↓ 提交正确的验证码 完全认证可以进一步表示为ANONYMOUS → PASSWORD_VERIFIED_PENDING_2FA → FULLY_AUTHENTICATED密码验证成功只说明第一因素已经通过。此时建立的 Session 应当是一种权限受限的临时认证状态只能用于访问验证码输入页面获取或重新发送验证码提交验证码取消当前认证流程。账户中心、用户数据、管理功能以及其他受保护接口都必须要求 Session 已经达到FULLY_AUTHENTICATED状态。服务器不能只判断 Session 中是否存在用户名因为“知道当前正在验证哪个用户”不等于“该用户已经完成全部认证”。完整的权限判断至少需要同时回答两个问题当前用户是谁 当前用户已经完成了哪些认证步骤两个实验中的漏洞正是应用没有正确处理这两个问题。三、漏洞一第二因素没有被真正强制执行第一个实验是2FA simple bypass。实验场景中攻击者已经获得 Carlos 的用户名和密码但无法获得 Carlos 的验证码。正常情况下攻击者只能通过密码验证然后停留在等待第二因素的状态。然而该应用在密码验证成功后就已经创建了能够识别 Carlos 身份的 Session。浏览器虽然随后跳转到了验证码页面但账户页面没有继续检查当前 Session 是否已经完成第二因素验证。于是攻击过程可以抽象为使用 Carlos 的密码完成第一阶段登录 → 服务器建立包含 Carlos 身份的待验证 Session → 浏览器被引导到验证码页面 → 攻击者不提交验证码直接请求 Carlos 的账户页面 → 账户页面只识别 Session 中的用户身份 → 没有检查第二因素是否完成 → 攻击者进入 Carlos 的账户这个漏洞不是验证码被破解也不是验证码系统本身失效而是验证码页面可以被绕过。1. 漏洞产生的错误假设应用错误地认为用户只要进入了验证码页面就只能提交验证码后继续访问但 HTTP 请求并不受页面顺序限制。攻击者可以忽略浏览器提供的正常跳转流程直接请求任意已知路径。因此浏览器显示了验证码页面不能证明服务器在所有受保护资源上强制检查了验证码页面跳转只是正常用户看到的交互流程真正的安全边界必须存在于服务器端。2. 这类漏洞的一般模型这类漏洞可以概括为第一因素验证成功 → 待验证 Session 被错误地当成完整登录 Session → 某个受保护页面只检查用户是否存在 → 没有检查用户的认证等级 → 第二因素被直接绕过在实际测试中进入验证码页面后不能只研究验证码本身还应检查在验证码尚未通过时能否直接访问账户主页修改资料接口订单、文件或消息接口后台管理页面原本登录后才能调用的 API。只要其中某个资源仅检查“是否存在登录 Session”而不检查“是否完成第二因素”2FA 就可能被绕过。四、漏洞二不同认证阶段的用户身份没有绑定第二个实验是2FA broken logic。这一次应用确实要求提交正确的验证码但密码验证阶段和验证码验证阶段之间的用户身份没有被可靠绑定。实验中的第二因素请求携带类似以下 CookieCookie: verifywiener; session...session用于表示当前会话而verify用于告诉服务器当前正在验证哪个用户。问题在于verify位于客户端可控的 Cookie 中。攻击者可以使用自己的账户完成密码验证获得合法 Session然后将verifywiener修改为verifycarlos如果服务器直接信任该字段就会开始为 Carlos 生成和校验验证码却没有确认 Carlos 是否就是第一阶段通过密码验证的用户。1. 正确的身份绑定关系一个安全的多阶段认证流程必须满足密码验证对应的用户 验证码生成对应的用户 验证码校验对应的用户 最终登录 Session 中的用户这个用户身份应由服务器根据第一阶段认证结果保存在 Session 或一次性认证事务中不应在第二阶段重新相信客户端提交的用户名。2. 攻击利用过程该漏洞的利用过程可以抽象为攻击者使用自己的账号通过密码验证 → 服务器创建一个合法的待验证 Session → 攻击者把第二阶段的用户标识修改为 Carlos → 服务器为 Carlos 创建验证码挑战 → 攻击者尝试 Carlos 当前的验证码 → 验证成功后服务器把 Session 提升为 Carlos 的完整登录状态这里没有绕过验证码校验。攻击者真正获得的是“替换第二阶段认证主体”的能力。3. 为什么 Wiener 的验证码不能用于 Carlos修改verify只会改变服务器当前准备验证的账户并不会让不同用户共享验证码。服务器中的对应关系仍然类似Wiener → 验证码 A Carlos → 验证码 B当请求中设置verifycarlos服务器会验证 Carlos 的验证码 B。因此攻击者不能直接提交自己邮箱中收到的 Wiener 验证码 A。身份绑定漏洞解决的是“可以验证谁”的问题而不是“任何验证码都能验证成功”的问题。攻击者仍然需要获得或猜中 Carlos 当前有效的验证码。这也是本实验需要进一步爆破四位验证码的原因。五、理解 GET 生成验证码与 POST 校验验证码这个实验中最关键的技术细节之一是/login2路径根据 HTTP 方法执行了不同操作。根据实验现象可以把应用流程抽象为POST /login → 校验用户名和密码 GET /login2 → 根据待验证用户创建验证码 → 保存并发送验证码 → 返回验证码输入页面 POST /login2 → 接收用户提交的验证码 → 与服务器保存的验证码比较 → 验证成功后完成登录因此在将待验证用户修改为 Carlos 后需要先请求GET /login2 Cookie: verifycarlos; session...这一步的作用不是验证验证码而是让服务器为 Carlos 建立当前有效的验证码挑战。随后才使用POST /login2 Cookie: verifycarlos; session... mfa-code1234尝试验证 Carlos 的验证码。如果没有先触发验证码生成就直接对POST /login2遍历0000-9999服务器可能根本没有为 Carlos 保存可供比较的有效验证码。此时所有候选值都会失败。为什么浏览器历史中主要看到 POST验证码表单提交时使用的是 POST所以 Burp HTTP history 中容易首先注意到POST /login2。但验证码输入页面在显示之前通常还需要由浏览器加载这个页面加载请求就是 GET。同一个 URL 使用不同 HTTP 方法可以对应不同的服务端处理逻辑GET /login2创建或显示验证码挑战 POST /login2消费并验证验证码挑战这不是 HTTP 协议自动赋予 GET 和 POST 的功能而是应用自身的路由和业务代码决定的。从 HTTP 语义来看GET 原则上应当是安全的读取操作不应产生发送验证码之类的状态变化。更合理的实现通常会使用受保护的 POST 请求创建或重新发送验证码并配合 CSRF 防护。不过本实验的主要漏洞并不是服务器使用 GET 生成验证码而是它根据客户端可修改的verify值决定为哪个用户生成和验证验证码。六、验证码爆破为什么能够成为第二条利用条件四位数字验证码只有0000-9999共 10,000 种可能。六位数字验证码也只有 1,000,000 种可能。与正常密码相比这种搜索空间很小。如果服务器缺少可靠的速率限制和失败次数控制攻击者就可能在验证码有效期内遍历全部候选值。第二个实验中的完整攻击链实际上组合了两个问题身份绑定缺陷 → 攻击者可以把第二阶段切换到 Carlos 验证码防爆破不足 → 攻击者可以尝试 Carlos 的全部四位验证码 两个条件组合 → 攻击者在不知道 Carlos 密码和验证码的情况下完成登录如果只有身份绑定漏洞但验证码具有严格的尝试次数限制攻击者仍然难以猜中验证码。反过来如果验证码可以无限尝试但攻击者无法把认证流程切换到其他用户那么他通常只能爆破自己账户的验证码。高影响攻击往往不是依赖一个孤立错误而是多个认证逻辑缺陷组合后的结果。七、为什么“输错几次就注销 Session”仍可能被绕过一些网站会在验证码连续错误多次后注销当前用户希望以此阻止爆破。这种防护是否有效取决于失败次数记录在哪里。如果失败计数只绑定当前 Session那么攻击者可能通过重新登录获得新的 Session同时让计数重新归零完成第一阶段登录 → 尝试少量验证码 → 达到限制后被注销 → 重新提交用户名和密码 → 获得新的 Session → 继续尝试下一批验证码这样虽然每个 Session 只能尝试有限次数但攻击者可以不断创建新的 Session最终仍然遍历整个验证码空间。这类多步骤爆破可以通过 Burp Macro 自动化。Macro 负责重复执行登录、获取新 Session、进入验证码阶段等前置流程Session handling rule 则把新的 Cookie 传递给后续 Intruder 请求。Turbo Intruder 也可以通过脚本实现相同的多阶段请求控制。工具本身不会自动产生漏洞它们只是将攻击流程自动化。能否继续爆破取决于服务器如何管理验证码和失败计数。如果每次重新登录都会生成新验证码并立即废除旧验证码或者失败次数持续绑定到用户账户和当前验证码挑战那么简单地重建 Session 就无法绕过限制。因此验证码防爆破机制不应只绑定 Session而应至少绑定用户账户 当前验证码挑战 认证用途重新登录、清理 Cookie 或更换 Session都不应清除同一个认证挑战已经发生的失败记录。八、两类漏洞的统一攻击逻辑两个实验表现不同但都源于应用没有把 2FA 当成一套完整的服务端认证协议。第一类漏洞破坏的是认证等级密码通过 → Session 被过早赋予完整权限 → 直接访问受保护资源 → 绕过第二因素第二类漏洞破坏的是认证主体自己的密码通过 → 获得合法待验证 Session → 修改第二阶段用户标识 → 为受害者创建并验证验证码 → 登录受害者账户可以将多因素认证中的通用漏洞模型概括为正常要求 同一用户依次完成第一因素和第二因素服务器才授予完整权限 错误实现 服务器依赖页面顺序、客户端用户标识或临时 Session 维持流程 攻击者行为 跳过中间页面、修改认证主体或重建 Session 最终结果 第二因素被绕过或者攻击者完成其他用户的第二因素认证因此测试 2FA 不能只检查验证码是否随机还必须追踪三个核心对象认证状态当前完成了哪一步 认证主体当前验证的是谁 验证码挑战当前验证码属于哪个用户和哪次登录九、修复建议1. 在服务端维护明确的认证等级密码验证成功后只能创建等待第二因素验证的受限状态PASSWORD_VERIFIED_PENDING_2FA只有验证码验证成功后才能将其升级为FULLY_AUTHENTICATED所有账户页面、敏感功能和 API 都必须检查完整认证状态不能只检查 Session 中是否存在用户名。2. 在第一阶段固定认证主体第一阶段验证密码后服务器应在 Session 或一次性认证事务中保存用户 ID。后续验证码生成和校验都必须使用这个服务端保存的用户 ID。不能允许 Cookie、URL、隐藏表单字段或请求体中的用户名重新决定第二阶段验证对象。3. 将验证码绑定到完整认证上下文验证码应绑定用户账户当前认证事务具体用途生成时间有效期已使用状态。用于登录的验证码不能被用于修改密码或关闭 2FA也不能在另一条认证流程中复用。4. 限制验证码尝试次数失败次数应绑定用户账户和验证码挑战而不是只绑定 Session。达到阈值后应使当前验证码失效并要求重新发起受控的认证流程。重新登录或更换 Session 不应重置原有挑战的失败次数。5. 限制验证码生成和重发除了校验接口验证码生成和重发接口也需要速率限制。否则攻击者可能利用重发功能进行骚扰、资源消耗或者不断改变验证码状态。6. 使用短期、一次性验证码验证码应当具有较短有效期验证成功后立即失效达到失败阈值后失效生成新验证码时废除旧验证码使用安全随机数生成机制。7. 完成认证后轮换 Session第二因素验证成功后应重新生成 Session ID清除临时认证状态并在新的 Session 中记录完整认证等级避免待验证 Session 被继续复用。8. 优先采用独立性更强的第二因素认证器应用、硬件令牌和安全密钥通常比短信验证码具有更强的抗截获能力。无论使用哪种形式都必须保证第二因素与第一因素之间具有足够独立性。总结双因素认证漏洞的本质通常不是验证码算法本身出现问题而是服务器没有正确管理验证码所在的认证流程。第一个实验说明如果受保护资源只检查用户身份却不检查用户是否完成第二因素那么攻击者可以直接绕过验证码页面。第二个实验说明如果密码验证和验证码验证之间没有固定同一个用户攻击者就可能使用自己的密码进入认证流程再把第二阶段切换到受害者账户。配合缺少防护的短验证码爆破最终可以完成对受害者账户的登录。分析一套 2FA 机制时应始终追踪以下关系第一因素验证的是谁 → 验证码为谁生成 → 提交的验证码在验证谁 → 最终 Session 登录成了谁同时还要确认密码通过后获得了什么权限 → 哪个请求创建验证码 → 哪个请求验证验证码 → 验证码能够尝试多少次 → 更换 Session 后限制是否仍然有效安全的双因素认证不是两个相互独立的页面而是一条由服务器强制维护的认证状态链。只有认证等级、用户主体和验证码挑战在整个流程中保持一致绑定第二因素才能真正构成账户安全边界。