1. 从"验证码拦路"说起:为什么暴力破解值得单独拎出来练
很多人第一次接触 pikachu 靶场,都是冲着 SQL 注入和 XSS 去的,暴力破解这一关往往被当成"送分题"草草跳过。但真到了实际项目里,你会发现登录接口才是攻击面最集中的地方——验证码、Token、登录失败锁定、频率限制,一层套一层。pikachu 的暴力破解模块之所以经典,就是因为它把这几层防护拆成了递进的几个关卡,让你能一层一层地理解"防护是怎么被绕过的",而不是一上来就面对一个综合防御体系。
这篇文章面向的是刚打完 pikachu 基础关卡、想真正搞懂"验证码和 Token 到底防住了什么、又漏在哪"的安全学习者。我会从最裸奔的登录表单开始,一路讲到带 Token 防护的场景,把 Burp Suite 的 Intruder 模块、验证码的几种处理思路、Token 的提取与回填逻辑全部拆开讲。中间会穿插我自己踩过的坑,比如 Intruder 跑了几千次请求结果全是 200 但没一个登录成功、比如 Token 每次请求都变导致 payload 对不上——这些问题在教程里通常一笔带过,但实际操作时能耗掉你一下午。
需要先明确一点:本文所有操作都在本地搭建的 pikachu 靶场环境中进行,目的是理解防护机制的薄弱点,从而在开发时写出更靠谱的登录逻辑。任何针对非授权系统的测试都是违规的,这个边界必须守住。
pikachu 的暴力破解关卡通常分为三到四个递进难度:无任何防护的纯表单、带验证码的表单、带 Token 的表单、以及 Token 加验证码的组合。每一关的绕过思路完全不同,下面逐个拆。
2. 第一关:裸奔登录表单的爆破节奏控制
2.1 先看清请求长什么样
打开 pikachu 暴力破解的第一关,页面就是一个普通的用户名密码登录框。这时候别急着开 Burp,先做一件事:手动提交一次错误的账号密码,然后在 Burp 的 Proxy History 里找到这个 POST 请求。
典型的请求体长这样:
username=admin&password=123456&submit=Login响应里通常会有一句"username or password is not exists"之类的提示。这句话就是爆破的"信号灯"——只要响应内容和成功登录时不一样,Intruder 就能靠它来判断哪次尝试成功了。
这里有个新手常犯的错误:不手动提交就直接开 Intruder,结果连请求里有哪些参数都没搞清楚,payload 位置设错了,跑半天全是无效请求。先手动走一遍完整流程,再上自动化工具,这个习惯能帮你省掉大量排查时间。
2.2 Intruder 的四种攻击类型怎么选
Burp Intruder 有四种攻击类型,很多人只知道 Sniper,其实选对类型能大幅提升效率:
| 攻击类型 | 适用场景 | pikachu 暴力破解中的用法 |
|---|---|---|
| Sniper | 单个参数位置,逐个替换 | 已知用户名,只爆破密码 |
| Battering ram | 多个位置用同一个 payload | 用户名密码相同的情况,很少用 |
| Pitchfork | 多个位置各自独立字典,一一对应 | 用户名和密码有对应关系时用 |
| Cluster bomb | 多个位置字典交叉组合 | 用户名和密码都未知,全组合爆破 |
pikachu 第一关通常是已知用户名 admin、未知密码,所以用Sniper就够了。把 payload 位置设在 password 的值上,加载一个常见密码字典(比如 rockyou.txt 的前几千条,或者自己整理的弱口令列表),线程数设成 1 到 5 之间。
2.3 线程数不是越大越好
这里要重点说一下线程数。很多人觉得线程拉满跑得快,但在本地靶场环境下,线程数过高会导致请求丢失、响应错乱,甚至把靶场的 Web 服务打挂。我一般设单线程或 3 线程,配合 Grep-Match 功能标记响应中的关键字。
具体操作:在 Intruder 的 Options 标签里找到 Grep-Match,添加"login success"或者成功后才出现的字符串。这样跑完之后,结果列表里命中关键字的请求会被高亮,一眼就能看出哪条成功了。
提示:如果响应长度差异明显,也可以用 Grep-Extract 提取响应长度,按长度排序找异常值。但关键字匹配更直观,优先用这个。
2.4 字典的选择比工具更重要
工具再顺手,字典不行也是白搭。pikachu 这种教学靶场,密码通常就在常见弱口令列表里,比如 123456、password、admin888 这类。但实际场景中,字典要结合目标的信息来定制——比如目标公司名、域名、常见年份组合。
我自己的习惯是准备三层字典:第一层是几十条的超弱口令,快速试;第二层是几千条的常见密码;第三层是结合目标信息生成的定制字典。pikachu 第一关用第一层就够了,几秒钟出结果。
3. 第二关:验证码不是万能盾,先搞懂它防的是什么
3.1 验证码的三种失效姿势
到了第二关,登录框多了一个图形验证码。很多人第一反应是"完了,爆破不了了"。但验证码的防护效果取决于它的实现方式,常见的失效姿势有三种:
第一种是验证码不刷新。你提交一次错误密码后,验证码图片没变,还是原来那个。这种情况下,你只要识别一次验证码,然后固定这个值反复提交就行。
第二种是验证码可复用。服务端校验完验证码后没有立即销毁 session 里的值,导致同一个验证码能通过多次校验。这种在早期系统里很常见。
第三种是验证码与请求分离。验证码的校验逻辑和登录逻辑不在同一个请求里处理,导致你可以先获取一个验证码,然后用它去跑密码字典。
pikachu 第二关通常是第一种或第二种,具体要看版本。判断方法很简单:手动提交两次错误密码,观察验证码图片是否变化。如果两次的验证码一样,那就是不刷新;如果变了,但你能用同一个验证码连续提交多次成功,那就是可复用。
3.2 Burp 里的验证码处理实操
假设验证码不刷新,操作流程是这样的:
- 在浏览器里打开登录页,让验证码显示出来,但不要提交。
- 在 Burp 里拦截这个 GET 请求(获取验证码图片的请求),记下 session 对应的验证码值。如果靶场把验证码明文存在 cookie 或隐藏字段里,直接读出来就行。
- 手动识别验证码图片上的字符(pikachu 的验证码通常很简单,四位数字或字母)。
- 构造 POST 请求,把验证码字段固定成识别出的值,密码字段设为 payload 位置。
- 在 Intruder 里跑字典。
如果验证码是明文存在响应里的(有些靶场为了教学方便会这么做),那就更简单了——用 Grep-Extract 把验证码提取出来,然后在 Payload 里用递归提取的方式动态填充。不过 pikachu 一般不会这么"送",通常需要你手动识别一次。
3.3 验证码识别的几条路
如果验证码每次请求都变,那就需要"识别"这一步。常见思路有:
- 手动识别:适合验证码数量少的情况。用 Burp 的 Intruder 配合宏(Macro),每次请求前先获取新验证码,人工识别后填入。效率低但准确率高。
- OCR 自动识别:用 Python 的 ddddocr 库,对简单图形验证码的识别率相当高。流程是:请求验证码图片 → 保存到本地 → ddddocr 识别 → 把结果填入登录请求。
- 打码平台:实际项目中会用,但学习和靶场环境没必要,成本高且涉及外部服务。
对于 pikachu 靶场,我推荐用ddddocr写个小脚本,因为它的验证码足够简单,识别率接近 100%。脚本逻辑大致是:
import ddddocr import requests ocr = ddddocr.DdddOcr() session = requests.Session() # 获取验证码 img_resp = session.get("http://靶场地址/pikachu/vul/burteforce/bf_form.php") with open("captcha.png", "wb") as f: f.write(img_resp.content) # 识别 with open("captcha.png", "rb") as f: code = ocr.classification(f.read()) # 提交登录 data = {"username": "admin", "password": "123456", "vcode": code, "submit": "Login"} resp = session.post("http://靶场地址/pikachu/vul/burteforce/bf_form.php", data=data) print(resp.text)这个脚本跑通之后,把它嵌到爆破循环里,每次请求前重新获取并识别验证码,就能绕过"验证码每次刷新"的防护。
注意:ddddocr 的安装依赖 onnxruntime,Windows 下直接 pip install ddddocr 即可,Linux 下可能需要额外装一些系统库。如果识别率不理想,可以先用 PIL 对图片做二值化、去噪处理,再喂给 OCR。
3.4 一个容易忽略的点:验证码的 session 绑定
有些实现会把验证码和当前 session 绑定,你换了 session 验证码就失效。这时候用 requests.Session() 保持会话就很重要。如果你用 Burp 的 Intruder,要确保请求里的 Cookie 和获取验证码时的 Cookie 一致,否则服务端会认为验证码无效。
我在这一关卡过一次:用脚本获取验证码时用的是 session A,提交登录时不小心用了新的 session B,结果验证码永远校验失败。排查了半天才发现是 Cookie 没对上。验证码和 session 的绑定关系,是排查验证码类问题的第一检查项。
4. 第三关:Token 防护的提取与回填逻辑
4.1 Token 到底防住了什么
Token 防护的核心思路是:每次请求都带一个服务端生成的、一次性的令牌,服务端校验这个令牌是否有效且未被使用过。这样即使攻击者截获了请求,也无法重放,因为令牌已经失效了。
在 pikachu 的 Token 关卡里,登录表单里会多一个隐藏字段,比如:
<input type="hidden" name="token" value="a1b2c3d4e5f6">这个 token 每次刷新页面都会变。如果你直接拿一个固定的 token 去跑字典,第一次请求之后 token 就失效了,后续请求全部失败。
4.2 用 Burp 宏自动提取 Token
Burp 提供了 Macro(宏)功能,可以在每次请求前自动执行一系列操作,包括获取新 token 并提取出来。配置步骤如下:
- 在 Burp 的 Settings → Sessions → Macros 里新建一个宏。
- 宏的步骤设为:请求登录页面 → 从响应中提取 token 值。
- 在提取规则里,用正则匹配
name="token" value="([^"]+)",把捕获组设为变量csrf_token。 - 回到 Intruder 的 Options → Sessions,启用宏,并设置"每次请求前运行宏"。
- 在请求的 token 字段值里,用
%csrf_token%引用宏提取的变量。
这样每次 Intruder 发请求前,都会先跑一遍宏拿到新 token,再填入登录请求。配置正确的话,爆破就能正常进行。
这里有个细节:宏提取 token 的请求和登录请求必须是同一个 session。Burp 的 Session Handling Rules 里要确保 Cookie 一致,否则服务端返回的 token 和登录时用的 session 对不上,校验照样失败。
4.3 脚本方案:requests 里的 Token 处理
如果不想折腾 Burp 的宏,用 Python 脚本更直观:
import requests import re session = requests.Session() url = "http://靶场地址/pikachu/vul/burteforce/bf_token.php" # 先获取页面,提取 token resp = session.get(url) token = re.search(r'name="token" value="([^"]+)"', resp.text).group(1) # 用提取的 token 提交登录 data = {"username": "admin", "password": "123456", "token": token, "submit": "Login"} resp = session.post(url, data=data) print(resp.text)把这个逻辑放进循环里,每次请求前重新获取 token,就能绕过 Token 防护。关键点是每次都要重新 GET 页面拿新 token,不能复用。
4.4 Token 和验证码同时存在怎么办
pikachu 的高难度关卡会把 Token 和验证码叠在一起。这时候你的脚本需要同时处理两件事:获取新 token、识别新验证码。流程变成:
- GET 登录页 → 提取 token、下载验证码图片。
- OCR 识别验证码。
- POST 登录请求,带上 token 和验证码。
- 判断响应,如果失败则回到第 1 步。
这个循环里,任何一步出错都会导致失败。常见的坑包括:token 提取的正则写错、验证码识别错误、session 中途丢失。建议在脚本里加日志,把每次的 token 和验证码值打印出来,方便排查。
5. 那些教程不会告诉你的排查经验
5.1 响应全是 200 但没成功,先看这三处
爆破跑完,结果列表里所有请求的响应码都是 200,但没有任何一条显示登录成功。这种情况我遇到过好几次,排查下来通常是三个原因:
第一,payload 位置设错了。比如把 payload 设在了 username 上而不是 password 上,或者设在了 submit 按钮的值上。检查方法是看 Intruder 的请求预览,确认§符号包住的是你要爆破的字段。
第二,Grep-Match 关键字写错了。成功登录后的响应里可能包含"welcome"或"login success",但你匹配的是"success"而实际响应里是"successful",差一个字母就匹配不上。解决办法是手动登录一次,把成功响应的原文复制出来,从中提取准确的关键字。
第三,请求缺少必要字段。有些表单除了 username、password、token 之外,还有隐藏的 submit 字段或其他参数,漏掉任何一个都可能导致服务端拒绝处理。对比手动提交的请求和 Intruder 里的请求,逐字段核对。
5.2 线程数和请求间隔的平衡
本地靶场环境下,我建议线程数不超过 5,请求间隔设 0 到 100 毫秒。线程太高会导致两个问题:一是靶场的 PHP 服务处理不过来,返回 502 或超时;二是某些防护逻辑会检测高频请求,直接封 IP 或返回假响应。
如果你发现跑了一部分请求后突然全部失败,先检查是不是被限流了。把线程降到 1,间隔加到 500 毫秒,重新跑一遍看看。
5.3 字典编码和特殊字符的坑
密码字典里如果包含特殊字符(比如&、=、%),在 URL 编码或表单提交时可能被转义,导致实际提交的密码和你字典里的不一致。Burp 的 Intruder 默认会对 payload 做 URL 编码,但有些情况下需要手动调整。
比如密码是pass&word,如果不编码直接提交,服务端可能把&当成参数分隔符,实际收到的密码是pass。解决办法是在 Payloads 设置里勾选"URL-encode these characters",把&、=、%等字符加进去。
5.4 成功之后的验证不能省
Intruder 标记出某条请求"成功"了,别急着下结论。手动用那组账号密码登录一次,确认真的能进。有时候 Grep-Match 匹配到的只是响应里的巧合字符串,实际并没有登录成功。自动化工具的判断结果,永远要用人工验证兜底。
6. 从靶场到开发:这些防护该怎么写才对
打完 pikachu 的暴力破解关卡,最有价值的不是"我学会了爆破",而是"我知道这些防护为什么会被绕过,所以我开发时不会这么写"。
验证码方面,正确的做法是:每次校验后立即销毁 session 里的验证码值,无论校验成功还是失败;验证码与 session 严格绑定;对同一 session 的验证码错误次数做限制,超过阈值就强制刷新或锁定。
Token 方面,正确的做法是:token 一次性使用,校验后立即失效;token 与用户 session 绑定;token 生成用足够随机的源,避免可预测。
频率限制方面,正确的做法是:对同一 IP、同一账号的登录失败次数做计数,超过阈值后增加延迟或临时锁定;锁定策略要有梯度,不能简单粗暴地永久封禁,否则容易被用来做拒绝服务。
这些防护单独用都有绕过方法,但组合起来、再加上合理的阈值和监控,就能把暴力破解的成本拉到很高。pikachu 靶场的价值就在于让你亲手体验"单层防护有多脆弱",从而理解纵深防御的必要性。
我在实际开发中还会加一条:登录接口的响应时间做归一化处理。如果用户名不存在时立即返回、用户名存在但密码错误时延迟 500 毫秒返回,攻击者就能通过响应时间枚举出有效用户名。统一响应时间能堵住这个侧信道。
最后分享一个我在靶场练习时的小习惯:每打完一关,把完整的请求响应、payload 配置、排查过程记到一个 Markdown 文件里。过一段时间回头看,你会发现当时卡住的地方,恰恰是你理解最深刻的地方。pikachu 的暴力破解模块不大,但把这几关吃透,登录接口的安全设计思路基本就通了。