☰
从无防护到Token校验:暴力破解密码猜解实战全流程解析
2026/10/19 9:22:04 网站建设 项目流程

1. 从一次内部演练说起:为什么暴力破解值得单独拎出来记录

前段时间帮一个刚入行的朋友复盘他的一次内部安全演练,他卡在某个登录框上整整两天,字典跑了几万条,返回全是失败。他一度以为是字典不够大,换了好几套字典,结果依然一样。后来我让他把请求抓出来看一眼,问题瞬间就清楚了——每次提交表单时,请求里都带了一个动态变化的 token 字段,他用的工具每次都在重放同一个旧 token,服务端校验直接判无效,压根没走到密码比对那一步。

这个场景在实战和演练里太常见了。暴力破解(Brute Force)听起来是最没技术含量的攻击方式,无非就是拿字典一条条试。但真正动手做过的人都知道,它反而是最考验细节的一类活儿:有没有验证码、有没有登录失败锁定、有没有动态 token、token 是一次性还是可复用、失败提示是否统一、响应时间有没有差异……每一个细节都可能让你的字典白跑。

这篇记录就以一个本地搭建的 pikachu 演练环境为背景,把暴力破解从最基础的“无防护猜解”到“带 token 的密码猜解”完整走一遍。pikachu 是一个专门用来做安全学习的开源演练平台,里面内置了各种典型漏洞场景,暴力破解是其中入门级但非常经典的一类。我会把每一步的请求长什么样、为什么这么构造、工具怎么配、踩过哪些坑,全部摊开讲清楚。

适合谁看?如果你刚接触 Web 安全,想搞明白“暴力破解到底怎么落地”,或者你已经会跑字典但总在带 token 的场景里翻车,那这篇内容应该能帮你把思路理顺。全程只针对自己本地搭建的演练环境,所有操作都在授权范围内进行,这一点必须先说清楚——未经授权的测试是违法的,我们讨论的一切都建立在合法合规的演练前提上。

2. 暴力破解的核心逻辑与场景拆解

2.1 暴力破解到底在“破”什么

很多人对暴力破解的理解停留在“用字典撞密码”,这个理解不算错,但太浅了。暴力破解的本质,是在认证环节寻找一个可以自动化、可重复、且服务端不会有效阻断的提交通道,然后通过大量尝试来命中正确的凭据组合。

这里有两个关键词:可自动化和不被有效阻断。前者决定了你能不能写脚本或配工具批量跑,后者决定了你跑的量能不能真正生效。很多新手只关注字典质量,却忽略了这两个前提,结果就是字典再大也没用。

从认证要素来看,暴力破解通常针对以下几类目标:

  • 用户名已知、密码未知:最常见,比如已经知道管理员账号是 admin,只需要猜密码。
  • 用户名和密码都未知:需要同时枚举两个字段,难度和请求量呈平方级增长,一般会先通过其他手段(如错误提示差异、注册接口)确定用户名。
  • 验证码、token 等附加校验:这是进阶场景,也是本文重点,附加校验的存在会直接决定你的请求构造方式。

pikachu 的暴力破解模块把这几类场景拆得很清楚,基本覆盖了从“裸奔”到“带防护”的完整梯度。理解这个梯度,比单纯会用一个工具重要得多。

2.2 为什么 token 会让普通字典攻击失效

先解释一下 token 在这里的作用。服务端在返回登录页面时,会生成一个随机字符串塞进表单的隐藏字段里,同时把这个字符串存到服务端的 session 中。用户提交表单时,token 会一起提交,服务端拿提交的 token 和 session 里存的做比对,一致才继续处理登录逻辑,不一致直接拒绝。

这个机制最初是为了防跨站请求伪造(CSRF),但客观上它也提高了暴力破解的门槛。因为 token 是动态的,你没法用一个固定的请求包反复重放——每次提交前都得先 GET 一次登录页,把最新的 token 提取出来,再带着它去 POST。

这就带来几个实操上的难点:

  • token 的提取规则:它在 HTML 里的位置、字段名、有没有被 JS 动态改写,都会影响你用什么方式抓取。
  • token 是否一次性:有些实现里 token 用一次就失效,下次必须重新获取;有些则在一个 session 周期内可复用。这两种情况对请求频率和脚本结构的要求完全不同。
  • session 的绑定关系:token 通常和 session 绑定,如果你在脚本里没有正确维持 cookie,token 对不上,照样失败。

我见过太多人卡在这里:工具配置里明明填了“从响应中提取 token”,但因为提取的正则写错、或者 cookie 没跟着走,导致每次提交的 token 都是空的或者旧的,服务端一律拒绝,而返回的失败提示又和密码错误长得一模一样,根本分不清是 token 问题还是密码问题。这就是为什么我坚持认为,带 token 的猜解必须先把请求链路彻底搞清楚,再谈工具配置。

2.3 pikachu 演练环境的定位与价值

pikachu 这个平台的设计思路很务实,它不追求漏洞的“高深”,而是把每个漏洞的典型形态和常见变体都摆出来,让你在一个可控环境里反复练。暴力破解模块尤其如此,它把“无防护”“带 token”“带验证码”等场景分门别类,你可以逐个攻破,体会每种防护对攻击手法的影响。

用本地环境练的好处是,你可以随便折腾,看日志、改代码、调参数,不用担心影响任何真实系统。而且因为源码是开放的,你甚至可以直接去看服务端是怎么校验的,这对理解“为什么这个请求能成功、那个不能”帮助极大。我强烈建议每个学 Web 安全的人都自己搭一套,边打边看源码,比看十篇教程都管用。

3. 基础场景:无任何防护的密码猜解实操

3.1 先看清请求长什么样

动手之前,第一步永远是抓包。打开浏览器的开发者工具,切到网络面板,在登录框里随便输一个账号密码提交,观察请求。

在 pikachu 的基础暴力破解场景里,你会看到一个典型的 POST 请求,请求体大概是这样的:

username=admin&password=123456&submit=Login

响应里如果密码错误,通常会返回一段提示文字,比如“username or password is not exists”。这个提示很关键,它就是我们判断“是否命中”的依据。

这里有个细节要注意:失败提示是否统一。如果用户名错误和密码错误返回的提示不一样,那你就可以先枚举出有效用户名,再针对性地猜密码,效率会高很多。pikachu 的基础场景里提示是统一的,所以我们默认用户名已知,专注猜密码。

抓包的时候顺便把请求头也看一眼,特别是Content-Type和Cookie。基础场景一般没有复杂的 cookie 校验,但养成看全的习惯没坏处。

3.2 工具选型:为什么我优先用 Burp Suite 的 Intruder

跑字典的工具很多,命令行有 hydra、medusa,图形化有 Burp Suite 的 Intruder、OWASP ZAP 的 Fuzzer。基础场景其实用哪个都行,但我个人习惯用 Burp 的 Intruder,原因有几个:

  • 请求可视化:抓到的包直接送进 Intruder,哪些位置是变量一目了然,不用手动拼命令。
  • 结果对比方便:跑完之后可以按响应长度、状态码、关键字排序,快速定位“不一样”的那条。
  • 后续扩展顺滑:带 token 的场景需要在请求之间做提取和替换,Burp 的宏(Macro)和会话处理规则能直接搞定,不用换工具。

当然,如果你更喜欢命令行,hydra 配合一个字典也能跑,只是带 token 的场景会麻烦很多。工具没有绝对优劣,看场景和个人习惯。

3.3 配置 Intruder 的完整步骤

把抓到的登录请求右键发送到 Intruder,然后按下面的步骤配置。

第一步,切到 Positions 标签,点右边的 Clear 按钮清掉所有自动标记的变量,然后手动选中密码字段的值,点 Add 标记。这样我们就只对密码做枚举,用户名固定为已知的 admin。

第二步,切到 Payloads 标签,Payload type 选 Simple list,然后加载你的密码字典。字典的选择后面单独讲,这里先假设你已经有一个。

第三步,切到 Options 标签,在 Grep-Match 里加上失败提示的关键字,比如“not exists”。这样跑完之后,没有匹配到这个关键字的响应就是可疑的,很可能就是成功的那条。

配置好之后点 Start attack,Intruder 就会拿着字典一条条提交。跑完之后看结果列表,按 Grep-Match 那一列排序,把没匹配到失败关键字的挑出来,基本就是正确答案。

3.4 字典怎么选:别迷信“大而全”

说到字典,新手最容易犯的错就是去下载一个几十 G 的“超级字典”,然后跑半天跑不完,还占满磁盘。字典的核心不是大,而是贴合目标。

对于演练环境,其实用一个小字典就够了,比如常见的弱口令 top 1000。真实场景里,字典的构造要结合目标的信息:如果目标是一个企业系统,可以加入公司名、域名、常见年份组合;如果是个人站点,可以加入站长常用的昵称、生日等。这些信息可以通过公开渠道收集,但前提是必须在授权范围内。

我自己的习惯是准备几套字典分层使用:先跑 top 100 的极弱口令,几秒钟就能出结果;没中的话再上 top 1000;再不行才考虑更大的字典或者定制规则。这样能在最短时间内覆盖最可能命中的情况。

提示:字典里的密码要注意编码问题。如果目标系统对特殊字符做了处理,某些密码可能提交上去就变形了,导致明明字典里有却跑不中。遇到这种情况,检查一下请求的编码方式。

3.5 结果验证与常见误判

跑出结果后别急着下结论,一定要手动验证。把可疑的那条密码拿出来,在浏览器里正常登录一次,确认能进。我遇到过好几次“看起来成功”其实是误判的情况,比如:

  • 失败提示关键字写错了,导致所有响应都没匹配上,看起来全是“成功”。
  • 服务端在某些情况下返回了不同的页面(比如跳转到错误页),响应长度和成功页接近,被误认为成功。
  • 网络抖动导致某条请求超时,返回了空响应,也被当成异常。

所以验证这一步不能省。确认成功后,把正确的凭据记下来,这个场景就算通关了。

4. 进阶场景:带 token 的密码猜解全流程

4.1 token 机制在请求链路中的位置

进入带 token 的场景,第一件事还是抓包,但这次要抓两个包:一个是 GET 登录页的请求,一个是 POST 提交的请求。

GET 请求的响应 HTML 里,你会看到类似这样的隐藏字段:

<input type="hidden" name="token" value="a1b2c3d4e5f6..." />

这个 value 就是本次会话的 token。POST 请求的请求体里,除了 username 和 password,还会多一个 token 字段,值就是刚才那个。

关键点来了:这个 token 和你的 cookie 是绑定的。服务端在生成 token 时,会把它存进当前 session,而 session 是通过 cookie 识别的。所以你的 POST 请求必须带着和 GET 请求相同的 cookie,token 才能校验通过。

这就意味着,你不能简单地“抓一个 POST 包然后反复重放”,因为 token 会变(或者用一次就失效),你必须模拟“GET 取 token → POST 带 token 提交”这个完整循环。

4.2 为什么不能直接重放:一次失败实验的复盘

我一开始也不信邪,想着“token 就算变,我抓的那个说不定还能用几次”,于是直接拿一个抓到的 POST 包在 Intruder 里跑。结果你也猜到了,全部失败。

为了搞清楚原因,我把服务端的校验逻辑捋了一遍(演练环境可以看源码):每次 POST 过来,服务端先比对 token,不一致就直接返回失败,根本不会去查数据库里的密码。而且这个 token 在每次 GET 登录页时都会重新生成,旧的虽然不一定立即失效,但和当前 session 里存的对不上,照样拒绝。

更坑的是,token 校验失败返回的提示和密码错误返回的提示是一模一样的。这就导致你从响应上完全看不出是 token 的问题,只会以为字典不对。我那朋友卡两天,就是卡在这里。

所以结论很明确:带 token 的场景,必须让工具在每次提交前自动去获取最新 token,并维持同一个 session。

4.3 用 Burp 宏(Macro)自动获取并替换 token

Burp 解决这个问题的方案是“宏 + 会话处理规则”。听起来有点绕,但配一次就懂了。

先配宏。在 Burp 的 Settings 里找到 Sessions 相关配置,新建一个宏,让它执行“GET 登录页”这个请求。宏的作用就是让 Burp 在需要的时候自动发一个 GET,拿到响应。

然后配会话处理规则。新建一条规则,作用范围选 Intruder,规则类型选“在请求中替换参数”。具体配置是:从宏的响应里,用正则提取 token 的值,然后把它替换到 POST 请求的 token 字段里。

提取 token 的正则大概长这样:

name="token" value="([^"]+)"

这个正则的意思是:匹配name="token" value="后面的一串非引号字符,用括号捕获出来。配置的时候把捕获组的值作为替换源。

配好之后,Intruder 每发一次 POST,都会先跑一遍宏拿到新 token,替换进去再提交。这样 token 就永远是新鲜的,校验能过,密码比对才能真正进行。

注意:宏里的 GET 请求也要带着正确的 cookie。如果宏请求和 Intruder 请求用的 session 不一致,token 照样对不上。Burp 默认会处理 cookie 的同步,但你要确认一下会话处理规则里没有把 cookie 排除掉。

4.4 不用 Burp 的替代方案:脚本化思路

如果你不想用 Burp,或者想更灵活地控制流程,用 Python 写个脚本也是很好的选择。核心逻辑就是用一个 session 对象维持 cookie,每次提交前先 GET 拿 token,解析出来,再带着 token 和密码 POST。

用 requests 库的话,大致结构是这样的:

import requests import re s = requests.Session() url = "http://本地演练地址/vul/burteforce/bf_token.php" with open("passwords.txt") as f: for pwd in f: pwd = pwd.strip() # 先 GET 拿 token r1 = s.get(url) token = re.search(r'name="token" value="([^"]+)"', r1.text).group(1) # 再 POST 提交 data = {"username": "admin", "password": pwd, "token": token, "submit": "Login"} r2 = s.post(url, data=data) if "not exists" not in r2.text: print("命中:", pwd) break

这段代码的关键点有三个:用Session()维持 cookie、每次循环重新 GET 拿 token、用正则从 HTML 里提取 token。跑起来虽然比 Burp 慢一点,但逻辑透明,出问题好排查。

实测下来,这种脚本方式在 token 是一次性失效的场景里反而更稳,因为你对“取 token”和“用 token”的时机控制得更精确。

4.5 token 一次性失效场景的处理

有些实现里,token 用一次就作废,下次必须重新获取。这种情况下,你的脚本或工具必须保证“一次 GET 配一次 POST”,不能缓存 token 复用。

用 Burp 宏的话,要确认宏是每次请求都执行,而不是只执行一次。用脚本的话,就像上面那样把 GET 放在循环内部,天然满足这个要求。

还有一种更隐蔽的情况:token 在页面加载后会被 JavaScript 改写。也就是说,你从原始 HTML 里提取的 token 和最终提交的 token 可能不一样。遇到这种情况,要么用能执行 JS 的工具(比如带浏览器的自动化框架),要么直接分析 JS 逻辑,看它把 token 改成了什么规则。演练环境里这种情况不多,但真实场景里不少见,值得留意。

5. 常见问题与排查技巧实录

5.1 排查思路:先分清是 token 问题还是密码问题

带 token 场景最让人头疼的就是失败提示不区分原因。我的排查套路是这样的:

先手动构造一次请求,用浏览器正常登录,确认账号密码正确时能成功。然后故意改错 token,看返回什么。再故意改错密码但 token 正确,看返回什么。如果两者返回一样,那就说明服务端不区分,你只能靠“流程是否正确”来判断。

接着检查你的工具或脚本:token 有没有取到?取到的值对不对?cookie 有没有跟着走?把这几步的中间结果打印出来,比盲目跑字典有效得多。

5.2 常见问题速查表

问题现象可能原因排查方法
所有请求都失败,提示统一token 未正确获取或替换打印每次提交的 token,和 GET 响应里的对比
偶尔成功偶尔失败token 一次性失效,存在复用确认每次 POST 前都重新 GET
脚本报错取不到 token正则不匹配或字段名不同查看实际 HTML,调整正则
浏览器能登录,脚本不能cookie 未维持检查 session 对象是否贯穿全程
响应长度都一样失败提示被统一处理尝试用响应时间或状态码区分
跑很久没结果字典不含正确密码换小字典先验证流程,再扩大范围

5.3 几个容易忽略的细节

第一个细节是请求间隔。有些服务端对高频请求有隐式限制,虽然不返回锁定提示,但会悄悄丢弃部分请求。如果你发现命中率异常低,可以适当加一点延时,比如每次请求间隔 0.1 到 0.5 秒。演练环境一般不需要,但养成这个意识有好处。

第二个细节是编码。密码里如果有特殊字符,比如&、=、+,在表单提交时需要正确编码,否则会被解析成参数分隔符,导致密码变形。用 requests 的data参数一般会自动处理,但手动拼请求体时要注意。

第三个细节是响应中的隐藏信息。有些系统在登录失败时,虽然提示文字一样,但响应头里的Content-Length或Set-Cookie会有细微差异。跑字典时把这些也纳入对比维度,能提高判断准确率。

5.4 我踩过的坑与经验总结

最大的坑就是前面说的“以为 token 可以复用”。这个坑的本质是没搞清楚 token 的生成和校验机制,想当然地认为它和普通参数一样。后来我养成了一个习惯:遇到任何动态参数,先手动验证它的生命周期——用同一个值提交两次,看第二次是否还认。这个动作花不了几分钟,但能省下大量瞎跑的时间。

第二个坑是正则写得太宽松或太严格。太宽松会匹配到错误的字段,太严格又匹配不到。我的做法是先把响应 HTML 复制出来,在本地用正则测试工具调好,再填进工具里。

第三个坑是忽略了 cookie 的同步。Burp 的宏和 Intruder 默认共享 cookie jar,但如果你手动改过会话处理规则,可能会破坏这个同步。脚本里则要确保 GET 和 POST 用的是同一个 session 对象。

这些经验说起来简单,但都是在实际动手时一条条撞出来的。演练环境的好处就是撞了也不心疼,撞多了自然就记住了。

6. 从演练到实战的思维迁移

6.1 防护手段与攻击手法的对抗关系

把基础场景和带 token 场景放在一起看,你会发现一个清晰的对抗逻辑:每一种防护手段,都是在提高攻击的成本。

无防护时,攻击成本极低,一个请求包加字典就能跑。加了 token 后,攻击者必须多一步获取 token 的操作,脚本复杂度上升,请求量翻倍(每次都要 GET)。如果再加验证码,自动化就更难了,可能需要引入图像识别。如果再加登录失败锁定,那字典还没跑完账号就被锁了,攻击直接中断。

理解这个对抗关系,比记住某个具体操作更有价值。因为真实环境里的防护往往是组合的,你需要根据观察到的现象,判断当前有哪些防护,再决定用什么手法应对。比如看到有 token,就先解决 token 的自动获取;看到有锁定,就考虑降低频率或者换用分布式的方式(当然,这必须在授权范围内)。

6.2 演练环境与真实环境的差异

必须强调一点:演练环境是为了教学而设计的,它的防护往往是“单一且明确”的。真实环境里,你可能同时面对 token、验证码、频率限制、IP 封禁、行为分析等多层防护,而且失败提示可能被刻意模糊化,让你根本分不清卡在哪一步。

所以从演练到实战,最重要的迁移不是工具操作,而是分析问题的框架:先抓包看请求结构,再判断有哪些校验环节,然后逐个验证每个环节的行为,最后才设计自动化方案。这个框架在演练里练熟了,面对真实环境才不会慌。

6.3 合法合规的边界意识

最后必须再强调一次边界。暴力破解这类技术,只有在明确授权的演练环境或自己的系统上才能使用。未经授权对他人系统进行密码猜解,无论是否成功,都是违法行为,后果很严重。

我分享这些内容的目的是帮助大家理解防护机制、提升安全意识。你在学习过程中,所有的实操都应该在本地搭建的演练环境里进行。如果工作中需要对某个系统做安全评估,也一定要拿到书面授权,明确测试范围和时间窗口。这个边界意识,比任何技术都重要。

我自己在实际操作中的体会是,把每一次演练都当成一次完整的“分析-验证-复现”训练,而不是单纯地跑工具出结果。工具谁都会用,但能说清楚“为什么这个请求能成功、那个不能”的人,才是真正理解了原理。带 token 的密码猜解之所以值得单独记录,就是因为它逼着你去关注请求链路的每一个细节,而这种关注细节的习惯,会迁移到你做任何安全相关工作的过程中。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询