1. 项目概述与攻击面拆解
1.1 这个实战笔记想解决什么问题
最近我在梳理 Web 身份验证相关的攻防思路时,把 PortSwigger 实验室里关于认证机制的那部分案例从头到尾重新刷了一遍。从最基础的用户名枚举开始,到密码爆破、会话令牌分析,再到 2FA 绕过和通过宏(Macro)实现自动化爆破,整个链路走下来,最大的感受是:身份验证不是单点问题,而是一条可以被逐层拆解的信任链。
这条笔记适合三类人。第一类是刚接触 Web 安全、想做渗透测试入门但不知道怎么下手的新手,第二类是已经会抓包改包、但总觉得思路不成体系的初中级测试人员,第三类是在做蓝队防御、想搞清楚攻击者会从哪里突破身份验证的开发者或安全运维。不管你是哪一类,这篇笔记里都不会出现悬空的理论,所有内容都能落回到 Burp Suite 的实际操作上。
这套内容能解决什么问题?说白了就是三件事:怎么更快地发现认证机制的薄弱点,怎么在不需要复杂工具的情况下完成高成功率验证,怎么把手工测试中重复性的劳动自动化。我在刷这些案例的过程中踩了不少坑,比如刷着刷着发现会话失效了、宏配置错了导致请求链路断裂、枚举时忽略了响应长度的微小差异等等。这些坑在官方文档里往往只有一句话带过,但实际操作时真的会卡人一两个小时。
所以我想把这些经验和细节沉淀下来。这篇文章里写的所有攻击手法,都只适用于你有授权的测试环境,比如 PortSwigger 自带的靶场、自己搭建的练习系统,或者签了测试授权的目标系统。未经授权的探测和爆破在任何国家和地区都不是小事,别把练习环境里的技能带到生产环境去乱试。
1.2 认证机制的常见战场与测试路径
身份验证听起来范围很大,但真正需要我们关心的核心节点其实就这几个:
- 登录入口本身:用户名/密码的提交逻辑,包括参数格式、加密方式、请求频率限制。
- 会话生命周期:登录成功后服务端发放的 Session ID、Token,它们怎么生成、怎么校验、怎么失效。
- 多因素认证:除了密码之外的验证层,常见的有短信验证码、邮件验证码、TOTP 动态口令。
- 密码找回与账户恢复:这类辅助流程往往更容易被忽视,也经常存在逻辑漏洞。
- 登录态下的敏感操作复核:比如修改密码、绑定手机号时是否需要重新验证身份。
PortSwigger 的认证实验室覆盖了上面几乎所有方面。我自己的测试路径一般是这样:先用枚举确认目标系统里存在哪些有效用户名,再针对这些用户名尝试密码爆破,同时观察系统是否有时序锁定、IP 锁定、CSRF Token 刷新等防御机制。如果爆破被卡住,就转向分析会话管理或者看 2FA 的验证逻辑是否可以被绕过。
这条路径看起来简单,但每一步都有细节。比如很多人一上来就直接挂字典跑爆破,结果跑了半天发现用户名全是无效的,白费力气。又比如有些人明明拿到了一个有效会话,却在测试 2FA 时把会话搞丢了,导致后面所有验证都失败。下面我会按这条路径,把每个环节怎么做、为什么这么做、有哪些坑,逐一展开。
2. 先从枚举探测开始:把立足点找到
2.1 为什么要先枚举用户名,而不是直接爆破
很多初学者有个误区:觉得爆破就是拿一个大字典对着登录接口猛跑。但实际上,如果连有效用户名都不知道,爆破的效率和准确率都会大打折扣。
从攻击者的角度看,用户名枚举的价值在于缩小爆破范围。一个典型的业务系统里,用户名往往是邮箱或者手机号,它们的格式规则相对固定。如果你能确认一批有效用户名,后面爆破密码时,不需要在字典里混入“无效用户名”干扰结果,每条请求的有效性都会更高。
从测试方法论的角度看,枚举本身也是在探路。你通过枚举能发现系统在身份验证上存在的问题——比如错误提示过于精确、响应内容可区分、时序存在差异等等。这些信息本身就是漏洞证据,可以在测试报告里作为中低危问题提交,也可以作为进一步攻击的前置条件。
在实际操作中,我一般会同时使用两种策略:一种是用小字典快速探测常见用户名,比如 admin、test、root、info、support 这类;另一种是结合目标系统的业务场景,去收集真实的用户标识。PortSwigger 实验室里通常会直接给出一些线索,比如注册页面返回的提示语、已经暴露的用户邮箱前缀等。要把这些信息利用起来,而不是死板地跑字典。
2.2 响应差异分析:从状态码到响应体
用户名枚举最常见的判断依据,就是系统对“用户名不存在”和“密码错误”这两种情况给出的响应不同。这个差异可能出现在三个层面。
第一是状态码。某些系统会用 200 表示登录失败,但用 404 甚至 302 表示资源不存在,或者反过来。理论上正规系统应该统一返回 401 或者 200,但很多老旧系统、外包项目就是不规范。PortSwigger 的一些入门级实验室就故意设计了这种问题。测试时先手工提交两个不同的请求,对比状态码差异,如果有差异,直接就能确认枚举漏洞。
第二是响应体的内容和长度。比如用户名不存在时提示“Invalid username”,密码错误时提示“Incorrect password”,这属于最直白的差异。还有一些系统虽然用了相同的提示文案,但响应长度不一样——多了一个空格、多了个小写字母或者 HTML 注释,肉眼看不出来,但 Intruder 的对比功能会很明显地展示出来。
第三是响应头或行为差异。例如某些系统在用户名不存在时不会设置 Cookie,而在密码错误时设置了 Cookie;或者用户名不存在时请求时间明显缩短,因为系统直接短路了查询逻辑。在测试时把响应包完整展开看,不要只盯返回体,Header 里的 Set-Cookie、Content-Type、Server 信息有时候都会泄露线索。
我习惯在 Burp Suite 的 Intruder 里把匹配项同时设置为状态码、响应长度和关键字,一次性做对比。如果某个用户名对应的响应长度和其他样本有明显差异,就标记出来手动复核。
2.3 利用错误消息、时间差异与登录响应细节
除了上面提到的直观差异,还有一些更隐蔽的枚举技巧。
时序攻击是很有意思的一种。某些系统在用户名不存在时会先查用户名表,没查到就直接返回;而在用户名存在时,会继续查密码哈希、做比对,耗时明显更长。如果这个时间差足够稳定且可测量,即使响应内容完全相同,也能通过 Intruder 里的响应时间列判断哪些用户名是有效的。有个实验室的案例就是这样,密码哈希算法相对复杂,存在用户时响应时间会慢几百毫秒,用响应时间排序后结果一目了然。
登录响应细节也值得深挖。比如第一次用不存在的用户名登录,服务器返回的某个 Cookie 值保持不变;换成存在的用户名后,Cookie 值会变长或者追加了一段新内容——这往往说明后端逻辑走了不同的分支。还有一种情况是,系统会返回“密码错误”和“账户已锁定”两种提示,前者意味着用户名有效且未锁定,后者说明账号存在但被保护了。这些都是枚举阶段可以利用的分支信号。
实际操作时,我会先把同一请求重放两三次,确认响应差异不是随机的。因为有些应用会在响应头里注入随机值,或者负载均衡导致多个后端响应不一致。只有当差异稳定可复现时,才能作为判断依据。
提示:PortSwigger 的实验室里,枚举部分通常不会设置请求频率限制,但真实系统里会。所以我在做枚举时都会控制线程数,先用单个线程确认规则,再逐步提高并发,避免触发封禁导致结果失真。
3. 登录爆破中的细节博弈
3.1 请求结构分析与令牌处理
拿到有效用户名之后,下一步是尝试密码爆破。但这一步远不是把字典灌进 Intruder 那么简单。爆破的第一步是搞清楚登录请求的完整结构。
先看请求行的路径和 HTTP 方法。有些系统登录入口是 POST /login,有些则是 POST /api/auth,还有的是用 XMLHttpRequest 异步提交。请求路径不同,对应的防护策略也可能不同——比如后端可能只对页面登录接口做了频率限制,但 API 接口没做任何防护。这往往是爆破突破口。
再看请求体里的参数。一个典型的登录请求长这样:
POST /login HTTP/1.1 Host: target.example.com Content-Type: application/json {"username":"admin","password":"123456"}注意 Content-Type 是 JSON 还是表单格式。如果是 JSON,Intruder 里对参数做爆破时需要选择合适的位置标记,并且要注意转义问题。如果是表单格式,CSRF Token 通常也是请求体的一部分。Burp 的 Intruder 有两种处理方式:用宏自动刷新 Token,或者用手工提取 Token 后单独标记为不参与爆破的资源。后面我会详细说宏的配置。
还要关注 Cookie。很多系统在登录页面会预置一个 session 或 csrfToken 的 Cookie,爆破时如果 Cookie 过期或需要更新,要继续使用旧的 Cookie 会导致请求全部失败。这种问题我在测试时遇到过不止一次,后面会在排查章节展开。
3.2 绕过频率限制的几种思路
真实系统里,爆破最大的拦路虎通常是频率限制和账户锁定。理解这些限制的绕过思路,对测试深度有很大帮助。
第一种思路是IP 轮换。限制通常基于 IP 维度,比如 10 次失败后锁定该 IP 15 分钟。如果你能控制代理池或者使用 IPv6 地址池,就能轻松绕过这种限制。但这个方法依赖基础设施,不是所有测试场景都适用。
第二种思路是分布式爆破。不是换 IP,而是换入口——同一套凭据,在登录接口失败一定次数后,转去密码重置接口试试,或者去移动端 API 接口试试。很多系统的频率限制只做了 Web 端,API 端完全裸奔。这种思路要求你先做接口资产梳理,把同一个认证功能的不同入口都找出来。
第三种思路是请求混淆。添加 X-Forwarded-For 头部、X-Real-IP 头部,值为任意 IP,有些系统真的会信任这些头部而绕过频率限制。虽然这是很老套的手法,但我在不少真实项目里仍然见到过。
第四种思路是利用时间窗口。有些系统的锁定逻辑是“连续失败 N 次锁定 M 分钟”,但如果你在每个锁定周期内只试 N-1 次,隔一段时间再试,就能把爆破周期拉长,用时间换空间。这种方式慢,但胜在稳妥。PortSwigger 的 2FA 宏爆破实验室里,也涉及到了类似思路的不同变体。
注意:这里说的所有绕过思路,都只是为了让你在有授权的测试中验证防御有效性。别拿这些方法去对付你不拥有的系统。
3.3 用户名密码字典的精细化构造
爆破的效果很大程度上取决于字典质量。PortSwigger 的实验室里通常会给出一个候选密码列表,但真实项目中不可能这么轻松,所以我会结合目标情况手动构造字典。
首先,从目标系统收集信息。注册页面的密码规则会告诉你最小长度、是否要求特殊字符;找回密码页面的提示语可能泄露密码策略的细节。这些信息能帮你过滤掉字典里大量不合规的词条,让每个密码尝试都更接近真实。
其次,基于目标语言习惯定制。如果是国内系统,用拼音名字加生日、公司名加年份这类组合命中率很高;如果是英文系统,天气、球队、宠物名这类词汇命中率也比较可观。把弱密码字典和这些自定义规则合并,能明显提高爆破成功率。
最后,要注意大小写和变形。很多系统要求密码包含大小写字母,所以爆破时不能只试纯小写。我一般在 Intruder 里同时使用字典中原词、首字母大写、全大写、末尾加数字等变体。这些变体会成倍增加请求数量,但对应的是真实场景里的用户习惯。
有了好的字典,爆破的命中率才会上去。否则即使绕过了一切限制,纯靠暴力碰撞的效率也低得可怜。
4. 2FA 绕过与宏爆破
4.1 2FA 机制中容易被忽视的逻辑缝隙
多因素认证在很多人眼里是“不可攻破”的,但实际测试里我见过太多 2FA 的落地缺陷。PortSwigger 最经典的一个实验室,核心漏洞就是2FA 校验与会话状态脱节。
具体来说,正常的 2FA 流程是:用户提交用户名密码后,系统设置一个未完全验证的会话 Cookie,同时发送验证码到用户手机或邮箱。用户提交验证码后,系统校验通过,把会话标记为已验证。这个流程本身没问题,但某些实现里存在两种情况可以绕过。
第一种情况:验证码不是与账号绑定的。系统的验证码校验接口只校验验证码本身是否正确,而不校验这个验证码是发给谁的。攻击者可以用自己的账号接收验证码,然后把这个验证码用在受害者的账号上。这种漏洞在短信验证码的电商类系统里并不少见。
第二种情况:验证步骤可以通过修改请求跳过。比如登录后跳转到 2FA 页面,系统通过请求中的某个参数判断用户是否已通过 2FA,比如"verify": "false"、"step": "2fa"。如果直接把这个参数改成"verify": "true"或者访问一个未校验权限的后续接口,就能绕过验证步骤。
PortSwigger 的某个实验室就是利用第二种思路。在登录接口成功之后,把请求路径从/login改成直接访问/my-account,发现系统没有强制跳转到 2FA 页面,而是直接返回了账户信息。原因很简单:后端只在执行 2FA 流程的接口上做了校验,但没有在资源访问的必经链路上统一校验会话的“已验证”状态。
这个案例给我最大的启发是:2FA 不是一个“功能”,而是一个“状态”。如果这个状态没有渗透到所有敏感操作里,那它就只是摆在那里的一层装饰。
4.2 2FA 验证码爆破的痛点和宏的引入
当 2FA 逻辑本身没有明显的绕过漏洞时,另一个思路是直接爆破验证码。这个思路成立的唯一前提是:验证码位数足够短(常见 4 位或 6 位数字),且系统没有严格的尝试次数限制。
4 位数字只有 10000 种组合,6 位数字也只有 100 万种,听起来好像不少,但对于自动化工具来说完全可控。问题是,爆破过程中需要处理的往往不是那 10000 次请求本身,而是每 次 请 求 都 要 带 上 一 个 新 的 CSRF Token 或 新 的 会 话 Cookie。有些系统每刷新一次验证码页面,就会废弃旧的 Token,并且把旧的会话标记为无效。
如果只是手工抓一个 Token 然后循环替换验证码字段,第二次请求就会失败,因为 Token 已过期。必须做到“每次爆破尝试前,自动获取一个新的 Token,并携带这个新 Token 发起验证码提交请求”。这就是宏技术的用武之地。
宏(Macro)是 Burp Suite 里一项非常实用的功能,它能让你在每次请求前自动执行一组预设的子请求,从子请求的响应里提取动态值,再把提取到的值注入到后续的主请求中。这样就能在 Intruder 跑爆破的同时维持会话的新鲜度。
4.3 宏配置实操:从零到一发入魂
我以 6 位数字验证码的爆破为例,完整演示一遍宏的配置过程。
第一步:录制宏。在 Burp Suite 的 Project options -> Sessions -> Macros 里点击 Add,然后选择一个 GET /2fa 页面这样的请求作为宏的子请求。Burp 会记录这个请求的完整链路,包括响应包。
第二步:提取动态值。在宏录制完成后,配置一个 Cookie Jar 更新规则。关键是告诉 Burp,从宏的响应中提取 Set-Cookie 里的某个 Cookie 值,更新到全局 Cookie Jar 中。同时,如果验证码页面 HTML 里有隐藏的 CSRF Token,需要在宏的响应里添加一个提取规则,把 Token 值提取出来。
第三步:创建 Session Handling 规则。在 Session Handling Rules 里新建规则,作用域限定在爆破验证码的这个 Intruder 请求上。规则的动作选择“Run a Macro”,选中刚才录制好的宏,然后在参数更新设置里,把提取出来的 Token 值替换到主请求的csrf参数位置。
第四步:配置 Intruder。把验证码参数标记为爆破位置,Payload 类型选择 Numbers,设置为从 000000 到 999999,步长 1。线程数不要太高,因为每次都跑一次宏,如果线程太高,宏的执行顺序会混乱,导致 Token 和 Cookie 对不上号。我个人习惯用单线程或双线程跑这种爆破,速度虽然慢一点,但准确率高。
整个流程跑起来后,你可以在 Intruder 的结果列表里看到每次请求前 Burp 会自动执行宏,把新的 Token 和 Cookie 注入到请求里。当某个请求返回的状态码或响应长度明显异于其他样本时,那条往往就是验证码命中后的成功响应。
提示:宏配置中最容易出错的地方是步骤顺序。宏必须先刷新页面获取 Token,然后拿这个 Token 才能提交验证码。如果顺序反了,Token 就是上一次的,注定失败。配置完宏后,一定要先在 Repeater 里手动触发一次宏,验证 Token 提取和注入是否正常,再启动 Intruder。
4.4 宏和 Intruder 配合的进阶技巧
上面的流程是基础版,实际测试中你会遇到更多复杂情况。这里分享几个我实测有效的小技巧。
技巧一:多级宏。有些系统的 2FA 页面加载时不止一个请求,而是先请求页面框架,再异步加载 Token。如果单纯录制一个 GET 请求不够,可以把整个请求链录制成一个宏——Burp 允许在一个宏里包含多个请求,第一个请求的响应里提取到的值,可以作为第二个请求的参数。这种多级宏在面对 SPA(单页应用)时几乎是必须的。
技巧二:响应条件判断。Burp 的 Session Handling Rule 可以对宏执行后的结果做条件判断。比如宏执行后检查响应值是否发生了变化,如果发现新的 Cookie 值没有被更新,就认为整个会话已失效,直接终止爆破。这在应对会话随机失效的系统时非常有用,可以避免大量无效请求浪费时间。
技巧三:验证码长度判断。4 位和 6 位验证码的爆破时间完全不同。4 位验证码从 0000 到 9999,单线程最多 1 万次请求,几乎瞬间完成;6 位验证码单线程 100 万次请求就要跑很久。这时候需要考虑用分布式或者先缩小范围(比如确认验证码是纯数字还是包含字母)再做爆破。PortSwigger 实验室里常用 4 位数字验证码,那是教育环境的特殊性,真实系统里 6 位数字加次数限制已经很普遍了。
这些技巧不是炫技,而是从实际踩坑中总结出来的。宏这个功能在遇到动态 Token、动态 Cookie、多请求链路的场景时,能把原本只能手工完成的重复劳动解放出来,非常值得投入时间熟练掌握。
5. 常见问题与排查技巧实录
5.1 爆破过程中的典型故障与排查思路
理论讲完了,接下来是实操中大家最容易翻车的环节。我把这一路上撞过的坑按出现频率排序,整理成速查表。
| 现象 | 可能原因 | 排查思路 |
|---|---|---|
| 所有爆破请求返回 401/403 | CSRF Token 未更新或格式错误 | 检查宏是否提取到了正确的 Token 字段,对比原始请求中的参数名和位置 |
| 前几次请求成功,之后全部失败 | 会话被服务器主动失效或 Cookie 未同步更新 | 检查 Cookie Jar 是否开启,查看宏是否每次都会更新 Cookie,必要时在宏里强制刷新会话 |
| 爆破结果里响应长度全部相同 | 验证码在服务端被静默丢弃或请求体格式不对 | 用 Repeater 手工重放一次成功的请求,确认请求格式完全一致后再跑 Intruder |
| 宏执行超时或链路中断 | 录制宏时包含了非必需请求,导致单次执行耗时太长 | 精简宏的请求链,只保留真正承载动态值的请求,去掉静态资源加载请求 |
| Token 提取规则匹配到多个值 | 页面里有隐藏 Token 和可见 Token 同时存在 | 细化提取规则,使用正则匹配具体字段属性,把提取范围限定到表单内的 Token |
| 爆破速度太慢 | 每次请求都会执行一次宏,宏本身有开销 | 适当调高线程数,同时注意宏执行顺序的稳定性,可用单线程先跑少量样本确认准确率 |
5.2 容易被忽略的错误配置细节
除了上面表格里列的硬性问题,还有不少隐蔽细节会导致结论误判。
响应长度排序的陷阱。在爆破验证码时,如果系统对每个错误验证码返回的内容长度都一样,只有正确验证码返回不同长度,那排序法很有效。但只要系统在响应头里加入了时间戳、随机数或者动态广告内容,长度排序就会产生很多噪声。这种情况要改用关键字匹配,把登录成功页面里的特征字符串作为匹配项。
Cookie 的作用域问题。Burp 的 Cookie Jar 是按域名维护 Cookie 的。如果主请求和宏的子请求域名不同,比如一个走www.example.com另一个走api.example.com,Cookie Jar 不会自动共享。需要在 Cookie Jar 规则里把两个域名都加上,或者改用自定义参数提取的方式手动传递 Cookie 值。
爆破完没有二次验证。当 Intruder 结果显示某条请求与众不同时,不要急着下结论。把这条请求放到 Repeater 里重放,确认是否真的拿到了已认证的会话;再把该会话对应的 Cookie 放进浏览器里模拟登录后的操作,确认能访问到账户数据。只有完成了二次验证,才能确认验证码爆破成功。我有一次就是因为直接看响应长度差异就下结论,结果后来发现那个响应只是系统的一个通用报错页,长度恰好相同。
宏失败的灰度处理。如果宏在爆破过程中偶发失败,Intruder 会直接跳过该次请求,导致部分组合没有被测试到。所以在暴破结束后,要对 Intruder 的状态列做检查,必要时补跑失败区间的组合。这个问题在设置过低的超时时间时尤其容易发生。
5.3 关于枚举与爆破的防御视角
聊了这么多攻击手法,最后我想换到蓝队视角,聊聊怎么防。
身份验证的防御不是单纯加一个验证码或者限制 IP 就能解决的。从我测试的经验来看,真正有效的防御体系至少要包含:
- 统一错误消息,不区分“用户名不存在”和“密码错误”;
- 在服务端做请求频率限制,不信任任何客户端传来的 IP 头;
- 对验证码等动态凭据设置严格的尝试次数和有效时间窗口;
- 为敏感操作强制要求输入当前密码,防止会话劫持后的越权操作;
- 在关键路径上统一检验会话的 2FA 状态,而不是只在某个页面做校验。
这些措施的每一条都对应着上面分析和测试过的攻击手段。安全测试的本质不是找漏洞本身,而是理解漏洞背后的设计缺陷,并把这种理解用于反向加固。这是我在刷完 PortSwigger 身份验证系列之后最大的感触。
6. 个人实操心得与后续扩展
这个系列刷下来,我自己最大的变化是:不再把“爆破”当成一个无脑撞库的动作,而是当成一个需要设计的流程。设计的前提是理解目标系统怎么区分“你是谁”和“你输入了什么”——这两者之间存在的每个环节,都可能是一个值得测试的点。
如果后续想继续深挖,我会重点看两个方向。一是 OAuth 2.0 和 JWT 这类现代认证协议中的逻辑漏洞,它们比传统表单认证更复杂,也更难防御;二是服务端模板注入与身份验证结合的利用链,比如一旦拿到了模板注入点,身份验证的绕过往往就变得微不足道。你可以把 PortSwigger 的认证专题当成一个入口,刷完之后顺着他推荐的进阶专题继续往前走,方向基本不会错。
最后再分享一个小技巧:每次完成一个实验室,不要急着关掉 Burp 就完事。把当时的 Intruder 配置、宏定义和整个过程截图保存下来,整理成一份自己的笔记。Repeater 里那些失败尝试的请求也别删,回头翻看的时候真的能帮你少走很多弯路。我自己整理这些素材时,经常能发现当时忽略了但后来很有用的信息。