认证机制攻防学习|PortSwigger Authentication 靶场复现总结(二):用户名枚举进阶

前言

上一篇我们靠"响应内容不同"枚举用户名。但如果开发把错误信息统一了呢?是不是就没辙了?这一篇打两个更隐蔽的信号信道 :响应时间 和账户锁定行为------服务器嘴上不说话,但"想多久"和"什么时候发火"会把它卖了。读者可以自行去打靶场,把我的博客当游戏攻略;虽然靶场自带解法,但不直观,而且我对底层逻辑也有一些自己的思考,如果对你有用,希望可以点一个赞。

本篇靶场

靶场 难度 攻击手法
Username enumeration via response timing PRACTITIONER 从业者 响应时间侧信道
Username enumeration via account lock PRACTITIONER 从业者 账户锁定行为差异

靶场链接:https://portswigger.net/web-security/authentication/password-based

建议先读系列第一篇《用户名枚举入门》,Intruder 的基础操作(四种攻击模式、Positions / Payloads 配置)在那里讲过,这一篇不再重复。

前置知识:X-Forwarded-For 是什么

这两个靶场都会撞上 IP 封锁,绕过要靠伪造 IP,先把原理讲清楚:

X-Forwarded-For(XFF):代理追加的请求头,用来告诉后端原始访问者 IP,可伪造。

复制代码
用户 → 代理服务器 → 后端网站

网站直接看到的 IP 是代理的 IP ,看不到用户真实 IP。于是代理会额外加一个 HTTP 头 X-Forwarded-For,写上用户真实 IP,传给后端。

⚠️ 重点 :这个头不是 HTTP 原生自带 。如果没有代理,用户自己就能随便构造这个头来造假。所以不能直接信这个头里的 IP,必须由可信代理来填充它。

这个"过度信任代理头"的问题,正是本系列后面密码重置投毒 (X-Forwarded-Host)的同一个病根。


实战一:Username enumeration via response timing

实验目标:在错误信息完全统一的情况下,通过响应时间差异枚举出有效用户名,并爆破其密码登录。

原理:时间差从哪来

登录校验一般是这样的:

java 复制代码
// 伪代码
User u = db.findByUsername(username);
if (u == null) {
    return "Invalid username or password.";   // 分支 1:查不到,直接返回
}
boolean ok = hash(password) == u.passwordHash; // 分支 2:算哈希!耗时操作
return ok ? "success" : "Invalid username or password.";

用户名存在时,服务器才会去做密码哈希比对(bcrypt / PBKDF2 这类慢哈希动辄几十到几百毫秒)。用户名不存在就直接返回了。

所以:响应时间 ≈ 用户名是否存在的信号。

怎么放大这个信号?用一个超长密码。密码越长,哈希运算越久,时间差越明显(从几十毫秒放大到几百毫秒甚至秒级)。

第一步:手工验证时间差

在登录界面分别输入正确和错误的用户名,Burp 的 HTTP history 里能看到每个请求的响应时间。

错误的(有点随机,但总体在 700ms 到 200ms 之间注意观察右下角):

正确的(用已知账号 wiener,明显有个递增的趋势):

这里没截全,他到达了2000millis

坑一:撞上防护机制,时间差不准确

⚠️ 踩坑:反复发包后撞上了防护机制,导致响应时间数据失真。

解决:在请求里加上 X-Forwarded-For 头,每次换一个 IP,绕过 IP 封锁。

第二步:用 Pitchfork 模式并发枚举

攻击类型选择 Pitchfork(草叉)------两个位置一一对应:

  • payload 1 = X-Forwarded-For 的 IP(每次不同,绕过封锁)
  • payload 2 = 候选用户名(每次重复同一批,配合超长密码)

替换位置如下:

第一个 payload(IP 列表):

第二个 payload(用户名列表):

⚠️ 踩坑 :我一开始把 IP 范围设成 105~200,结果不够用。set 1 的数量必须大于等于 set 2 的数量,否则后面的用户名取不到对应的 IP,又会被封。

坑二:并行发包把时间差"抹平"了

⚠️ 踩坑 :跑完后发现并没有明显高出的一行------因为线程池是并行的,请求互相抢资源、排队,响应时间就不再反映服务器真实的计算耗时。

解决:在 Resource pool 里把 Maximum concurrent requests 设为 1 ,也就是单线程串行发包 ;同时把已知有效账号 wiener 加进列表当参考系(对照组)。

📌 方法论 :做时序攻击一定要设对照组 。没有 wiener 这个已知有效账号做基准,你根本不知道"多少毫秒算高"。

第三步:排除网络抖动,逐个验证

由于是海外靶场,网络波动导致出现好几个都上 1000ms 的,无法直接判定。于是决定把 1000ms 以上的候选逐个手工验证:

Alaska 排除(确认是网络抖动,不是有效账号):

最终锁定 arlington(状态码是302):

登录成功:


实战二:Username enumeration via account lock

实验目标:利用"账户锁定"这一防护机制本身的逻辑缺陷,反向枚举出有效用户名,并爆破其密码。

原理:防护为什么会"泄密"

账户锁定是防护,但它自带信息泄露:

java 复制代码
// 伪代码
if (!userExists(username)) {
    return "Invalid username or password.";  // 不存在的账号:永远不锁
}
if (failCount(username) >= 3) {
    return "Account locked";                 // 存在的账号:错 3 次就锁
}

只有真实存在的账号才有"失败计数器"。所以:

  • 提交不存在的账号 → 不管试多少次,永远返回普通错误
  • 提交存在的账号 → 试到第 4 次,返回"已锁定"

"会不会被锁"本身就是一道是非题 ,答案就是"这个账号存不存在"。这就是所谓的防护机制的元信息泄露。

第一步:在 Repeater 里摸清触发阈值

在 Repeater 里反复提交同一个用户名,观察第几次触发锁定。

先用假设的账号试了几次都没锁定------说明这个账号不存在;同时可以确认这条路子走得通:

第二步:用 Intruder 找出会"发火"的那个用户名

给每个候选用户名连续发 5 次请求(5 > 阈值,确保触发),观察哪一组的响应 Length 不一样。

结果找到用户名 ansible:

第三步:爆破密码,注意锁定的窗口期

标记 password 位置构造 payload:

⚠️ 踩坑 :这一关我试着加 X-Forwarded-For 头,发现好像没什么用 ------换了 IP 照样触发锁定。说明这里锁的是账户维度,不是 IP 维度。

但试的过程中发现一个规律:连续失败锁定后,等一会儿又解锁了,可以接着猜。

经过在 Repeater 里的反复实验,确认是连续 3 次失败触发锁定。

坑三:答案其实一直躺在响应长度里

⚠️ 踩坑 :绕了一大圈,最后才发现答案早就出来了------前面我没仔细看 Length:

Length 含义
3266 什么都没显示 → 这就是正确答案
3344 密码错误,且账号未锁定
3396 密码错误,且账号已锁定

所以直接看 Length = 3266 的那一行,用户名 ansible、密码 joshua 就出来了。

📌 心得 :先穷举信号,再动工具。状态码、Length、响应时间、报错文案------把每条信道都扫一遍再设计攻击,往往能省掉 80% 的弯路。我这一关就是先入为主认定"要用 XFF 绕过",反而忽略了最明显的 Length 信号。
每人的靶场实例不同,用户名 / 密码 / 域名请替换成你自己的。


原理对比:两种隐藏信道

维度 响应时间枚举 账户锁定枚举
信号来源 是否执行密码哈希运算 是否存在失败计数器
观测指标 Response completed 耗时 响应长度 / 文案变化
放大手段 超长密码拉长哈希耗时 连续失败触发阈值
噪声来源 网络抖动、并发排队 锁定窗口期、解锁时间
关键配置 Resource pool 单线程 锁定阈值 + 解锁周期
防御难度 高(需要恒定时间比较) 中(需要统一锁定表现)

两者的共同点:开发者只统一了"说了什么",没统一"做了什么"。


防守

  1. 恒定时间比较(Constant-time compare)
    • 用户名不存在时,也执行一次等价的密码哈希运算(用一个假哈希比对),抹平时序差异
    • 密码比对用 hash_equals() / MessageDigest.isEqual() 这类恒定时间函数,避免因字符逐位比较而提前返回
  2. 锁定行为对无效用户名同样生效
    • 对不存在的用户名也维护一个"影子计数器",返回一致的响应,让"会不会被锁"不再是判断题
  3. 统一错误文案与响应长度
    • 文案、标点、长度、状态码全部一致(回看系列第一篇)
  4. 不要过度信任代理头
    • X-Forwarded-For 必须由可信代理填充,后端不应接受客户端直传(回看本篇前置知识)
  5. 限速 + 告警
    • 登录接口按 IP + 账号双维度限速,短时间大量失败触发安全告警

小结

  • 攻击手法一句话 :当响应内容被统一时,改用"行为"做信号------要么看响应时间 (存在才做哈希运算),要么看账户锁定(存在才有失败计数器),两条信道都能反推出有效用户名。
  • 这篇学到 :X-Forwarded-For 的原理与绕过用法、Resource pool 单线程对时序攻击的必要性、设对照组 (wiener)的实验方法,以及最重要的一条教训------先穷举信号,再动工具。
  • 下篇预告 :防护再升级------如果 IP 会被直接封锁怎么办?下一篇打 Broken brute-force protection, IP block,用"每隔几次插一次正确登录"的手法重置计数器。
相关推荐
海绵宝宝转agent2 小时前
Leetcode二叉树算法个人理解
笔记·学习·算法·leetcode·深度优先
小oo呆2 小时前
【学习心得】MCP工具开发学习笔记
笔记·学习
网络小江2 小时前
组网第41课:ACL 门禁——标准/扩展、规则顺序、time-range、in/out 方向
运维·网络安全·acl·访问列表·in/out
kyrie_sakura2 小时前
大模型学习19 大模型学习 - LangChain2_Retrieval(RAG-文档处理-向量处理检索-Milvus)
学习·langchain·milvus
女神下凡3 小时前
芯参谋(79):嵌入式学习进阶_(硬件与软件)深入实践指南
arm开发·单片机·嵌入式硬件·学习·fpga开发·设计规范
星恒随风3 小时前
Linux Socket TCP编程(二):逐文件重构多进程、多线程与线程池服务器,深入fork、pthread_create、popen和std::bind
linux·服务器·笔记·学习·tcp/ip
wq8973873 小时前
[AI问答] VFIO操作的相关函数学习
学习
迪康coolmu3 小时前
金融客户数据合规落地:基于迪康端点安全一体化管理系统的四道防线实践
运维·服务器·数据库·经验分享·安全·网络安全·金融
Century_Dragon4 小时前
把一辆智能汽车“搬“进教室—智能网联汽车自动驾驶虚拟教学实训
学习