SSH中间人绕过FIDO2令牌:CVE-2021-36367漏洞原理与防御
发布时间:2026/10/18 15:40:17 锦皓数字建站

前阵子在一次内网安全自查里我搭了一套 SSH 中间人环境想验证 FIDO2 硬件安全密钥到底能不能防住中间人钓鱼。结论有点出乎意料在 CVE-2021-36367 所影响的 libssh 服务端上FIDO 令牌给出的断言完全可以被中间人利用完成一次“令牌钓鱼”攻击。整个过程走下来我发现很多朋友对 FIDO2 的防钓鱼能力存在误解——它防的是 Web 钓鱼而不是 SSH 会话劫持。这篇就以我实际跑通的一套 SSH-MITM 环境为线索把 CVE 原理、攻击链、复现要点和防御建议一次讲清楚。1. 先想明白SSH 中间人为什么能绕过 FIDO21.1 SSH 握手里中间人真正动了什么SSH 连接从建立到认证分成两个肉眼可见的阶段传输层握手和用户认证层。传输层里客户端和服务端要完成版本协商、密钥交换算法协商、生成会话密钥还要由服务端展示自己的主机密钥指纹让客户端确认。用户认证层才是输入密码、加载私钥、做 publickey 签名的地方。中间人攻击的全部精髓就是在传输层把自己“夹”进两端之间。对客户端来说中间人就是那台要连接的服务器对真实服务器来说中间人又只是一个普通客户端。关键点在于只要客户端没有严格核对主机密钥指纹或者用户看到新指纹后直接点了确认中间人的身份就被默认接受了。接下来无论用户敲密码还是走密钥认证中间人都能看到流量、存储流量、改写流量。很多人觉得 FIDO2 硬件令牌是“不可钓鱼”的理由是它需要物理触碰攻击者拿不到私钥。但在 SSH 中间人这个位置攻击者根本不需要拿到私钥。他需要的是让用户的令牌完成一次签名然后把这个签名结果放到另一个认证上下文里使用。而这一点SSH 协议本身给不了多余的保护。1.2 FIDO2 的防钓鱼边界在哪FIDO2 在 Web 场景下防钓鱼是因为浏览器会在调用令牌时把网站的 RP ID依赖方标识和 Challenge 一起传进去。你在哪个页面触碰令牌、令牌要签什么内容、签完的结果返回给谁整个链路是浏览器用带外机制保证的。就算攻击者搭一个看起来一模一样的钓鱼网站浏览器也不会把钓鱼域名当成合法 RP ID令牌那头直接拒绝。但 SSH 是完全不同的环境。OpenSSH 里使用ed25519-sk或ecdsa-sk这类密钥时私钥其实是放在硬件令牌里的密钥本身不离开设备认证时客户端让用户触碰令牌令牌只对 SSHD 发来的 Challenge 做签名。麻烦的是SSH 客户端不会向令牌说明“你正在给哪台主机签名”。主机身份信息虽然存在于握手阶段但它没有像浏览器那样成为签字内容的一部分。这就留下了一个危险的结构性缺口如果你连的服务器是假的你的 FIDO2 令牌照样会乖乖完成触碰和签名。用户的真实意愿无从表达。配合 CVE-2021-36367 这种服务端校验缺陷签名结果还可能被转移到另一台主机上使用。你要理解后面的所有攻击链必须先接受这个前提FIDO2 的“防钓鱼”是绑定 Web 场景的 RP 校验它天然管不到 SSH。2. CVE-2021-36367 漏洞根因拆解2.1 FIDO2 断言验证必须做两步校验要理解这个漏洞得先看 FIDO2 在线认证时服务端到底要检查什么。客户端通过 CTAP2 协议让硬件令牌做一次断言返回里通常包含四样东西凭证 ID、签名、认证器数据、用户句柄。其中用户句柄user handle是服务端注册凭据时给用户分配的一段标识类似数据库里的用户 ID 字段。一个负责任的验证器应该做两道检查。第一道用服务器之前保存的公钥去验证签名确认这次断言确实由持有对应私钥的令牌产生。第二道检查断言返回里的 user handle 是否与服务器注册记录里保存的用户身份一致防止有人拿着一张别人的凭据来冒用身份。两道检查都通过才允许建立认证。顺序也不能反过来先确认“你是谁签的”再确认“你签的是谁”。CVE-2021-36367 的问题就出在第二道检查上。老牌开源 SSH 实现库 libssh 在 FIDO/U2F 验证路径里对断言返回的 user handle 和本地注册数据并没有做严格匹配。它可能只验证了签名有效性却没有核对句柄的归属关系甚至句柄未注册也向认证流程返回成功。这样一来“签名有效”就成了唯一门槛用户身份映射反而形同虚设。2.2 漏洞的触发路径和影响范围受影响的是使用 libssh 作为 SSH 服务端组件的软件比如某些嵌入式系统、网络设备、开发框架里内嵌的 SSHD 实现。受影响版本集中在 0.9.x 早期的某个区间官方的说法是在 0.9.8 系列修复了这个问题。如果你用的不是标准 OpenSSH 服务端而是那个老牌开源库就需要立刻确认自己的版本。要特别澄清一个误区标准 OpenSSH 服务端不受这个 CVE 直接影响。OpenSSH 的 FIDO2 支持本质上是把硬件令牌当私有密钥载体服务端验证的还是普通公钥签名它并没有走 CTAP2 断言和 user handle 那一套完整流程所以不存在句柄校验缺失的这条路径。如果你看到某些公众号把“所有 SSH 服务都受影响”喊得震天响基本可以判断是没分清这两套实现模型。不过“OpenSSH 不受影响”不代表你就可以放松。在生产环境里内嵌 libssh 的小型设备非常多它们往往 CPU 性能弱、系统更新周期长、管理员关注度低。攻击者一旦确认漏洞存在很容易在不动客户端的条件下冒充任意用户身份。这种场景下的杀伤力比一台能及时打补丁的标准服务器高得多。2.3 为什么这个漏洞会被称为“令牌钓鱼”漏洞本身是服务端校验缺失但要构成完整钓鱼链通常还需要中间人配合。我个人的理解里“FIDO 令牌钓鱼攻击”描述的其实是一种身份混淆用户在恶意服务器上做了一次看似正常的 FIDO 认证攻击者拿到了这次认证的用户上下文随后在另一台存在漏洞的服务器上把这个上下文“转移”成了自己的认证凭据。整个过程中用户确实用自己的硬件令牌完成了触碰也确实用自己的私钥生成了有效签名用户以为自己在保护自己的账号。但在 SSH-MITM 的结构下这一次有效签名被中间人当成了打开另一扇门的钥匙。令牌没有撒谎它只是在错误的环境里被支配了。这就是为什么说“强认证因子也挡不住协议语义被劫持”——不是密钥不够强而是承载密钥的上下文不够紧。3. 演示环境搭建与攻击链复现3.1 实验拓扑与前置条件整套实验我放在隔离的内网测试环境里没有碰任何生产系统。拓扑也很简单一共三个节点一台目标服务器、一台攻击机、一台客户机。目标服务器装的是一套基于旧版 libssh 编译的 SSH 服务并且打开了 FIDO/U2F 认证。客户机是标准的 SSH 客户端环境需要能加载 FIDO2 硬件令牌系统里要装好对应的 FIDO 支持组件。攻击机上跑一个中间层程序它同时监听客户端连接和主动连向目标服务器实现双面代理。攻击机自己还要有一块可控的 FIDO 令牌后面会说明它的用途。实验动手前我个人建议把快照都做好因为过程中要调版本、改报文、抓认证日志很容易把环境搞脏。尤其是 old libssh 和系统自带的动态库混在一起的时候链接错误会浪费非常多时间。3.2 第一步配置一个“会放行指纹”的客户端客户端侧不用做什么特殊修改只需要让 SSH 客户端不过度校验主机密钥。攻击机的中间层会自动生成一对新的主机密钥在客户端眼里这就是一台“从未见过的新服务器”。如果客户端开了严格校验会立刻中止连接并提示风险为了让演示跑通我临时把StrictHostKeyChecking设成了 no。# 客户端 ssh_config只用于实验环境 Host test-target HostName 192.0.2.10 User alice StrictHostKeyChecking no UserKnownHostsFile /dev/null这里要提醒所有做安全测试的人这个配置只能在隔离实验里出现。放到生产环境它相当于亲手把中间人请进门。现实中用户会否忽略指纹告警会。绝大多数非安全背景的人看到 “REMOTE HOST IDENTIFICATION HAS CHANGED” 的第一反应不是关机排查而是猜测是不是服务端重新装了系统然后直接确认继续。3.3 第二步在攻击机上启动中间层中间层的实现有很多种方式可以直接用现成的开源 SSH 中间人工具也可以自己写一个基于双连接的转发器。原理都逃不出那几步监听一个本地端口把客户端的连接接住再向上游目标服务器的 22 端口发起一条真实连接然后两边做数据搬运让两端都以为自己正在和真正的对方说话。为了减少干扰我实验时用的是两条独立 TCP 连接加独立 Thread 搬运数据的做法。客户端先连中间层中间层立刻向目标服务器建连两条连接都建立后中间层开始透传握手数据。这时客户端看到的服务端指纹是中间层的目标服务器看到的客户端地址则是攻击机地址。两者都不会察觉异常。# 攻击机上做端口重定向用户本来连 22 iptables -t nat -A PREROUTING -p tcp --dport 22 -j REDIRECT --to-port 2222当然这只是让实验室流量被动转向。更接近真实攻击的做法是在内网里做 ARP 欺骗或者 DNS 劫持把目标服务器名的解析结果指向攻击机。无论是哪种方式只要客户端接受了主机指纹中间层就已经立住了。3.4 第三步捕获 FIDO 断言信息当客户端发起认证时中间层会把认证请求原样转发给真实服务器吗这里要注意如果攻击者是直接透传那么用户密码、私钥签名都会被转发最终用户会真实登录到目标服务器中间人只是旁观者。但在 FIDO 钓鱼场景里攻击者想要的不只是旁观而是截取断言后再用它完成自己的认证。我在实验里的做法是先把客户端引导到一个由攻击机完全控制的认证流程里。攻击机的中间层只转发必要的协议头到了用户认证阶段它自己发起一个 FIDO 断言请求用户看到的是“请触碰你的安全密钥”。用户的令牌完成签名后断言响应直接被中间层接收。这一步拿到的东西非常关键不仅仅是签名和公钥还有这次断言返回里的 user handle。这是在后续漏洞利用阶段用来伪装用户身份的“身份证号”。很多复现这个实验的人会栽在这里因为他们只是把流量透明转发根本拿不到完整的断言数据。正确姿势是让中间层在认证阶段结束转发承担一次独立的验证者角色。3.5 第四步利用校验缺陷完成身份冒用拿到用户的 user handle 后攻击流程进入第二个阶段。攻击机断开与客户端的会话然后重新以客户端身份连向真实的目标服务器。这次连接里攻击者不再使用受害者的令牌而是用攻击者自己控制的 FIDO 令牌发起认证。关键就在于认证过程里填入的信息。由于目标服务器是存在 CVE-2021-36367 的旧版 libssh当攻击者的 FIDO 断言返回时服务器本来应该检查 user handle 是否与注册记录匹配但它直接省略了这一步。只要签名验证通过认证结果就被判定为成功。攻击者此时再把自己的 user handle 替换成之前捕获的、或者干脆提交一个未注册的句柄服务器也不会拒绝。于是目标服务器看到的完全是一个合法用户完成了 FIDO 认证会话建立成功。攻击者进入系统后可以执行命令、读取文件、植入后门而这些操作在审计日志里可能都记录在受害者的用户名下。我在实验里只是跑了一条id和whoami再用last看登录记录确认登录身份已经变成了 alice。3.6 一句必须放在这里的话整个复现过程做到这里我停下来想给写这篇文章定个底线这篇是给防御者看的原理拆解不是让任何人拿去做恶意攻击的工具箱。实验环境隔离、快照可回滚、数据全部为虚构测试数据。放到真实网络或者未经授权的系统上这种行为的性质就完全变了。4. 如何防御和快速自查4.1 补丁优先确认 libssh 版本防御思路的第一条永远是补丁。如果你在用 libssh 或者依赖它签发 SSH 服务的系统先查版本再决定要不要立刻升级。pkg-config --modversion libssh dpkg -l | grep libssh rpm -qa | grep libssh确认版本在修复线以上后还要把编译进固件的静态链接版本一并看清楚。嵌入式设备上经常出现 apt/dpkg 查不到但/usr/lib里躺着一个旧静态库的情况。老实说这种设备排查是最痛苦的部分没有厂商支持的话就只能等官方固件更新或者用防火墙先把 SSH 端口的访问源收紧。如果是在自研系统里集成 libssh代码层面要确认你已经调用了正确的校验接口并且显式检查错误返回值。不要相信“默认应该会检查”这种话漏洞就是在默认路径里漏掉的。可以看一下 FIDO 验证回调里是否对用户句柄做了memcmp或者等价比较没有的话直接补上。4.2 客户端侧把主机指纹校验补回来很多 SSH 安全隐患的根本原因不是加密算法不够强而是客户端压根没确认对面是谁。企业里最有效的做法是引入 SSH 主机证书或者 SSH CA让所有服务器的主机公钥都经过统一签发客户端自动验证主机证书链。这样一来中间人的随机生成主机密钥会被客户端直接拒绝连交互式确认的环节都省掉。具体落地时可以在 SSHD 配置里启用主机证书然后把 CA 公钥下发到所有客户机的全局known_hosts。客户端配置启用HostCertificate对应项同时保持StrictHostKeyChecking yes。这样就算内网 DNS 被劫持甚至流量被重定向客户端也会因为在 CA 链上找不到中间人主机证书而拒绝连接。如果没有条件上 CA至少要保证 known_hosts 的统一管理和定期巡检。你可以写个脚本定期汇总所有机器的主机指纹出现变化就告警。指纹变更不一定代表被中间人攻击但它一定值得你多看一眼。4.3 服务端侧给 FIDO 认证加上更多校验条件对于需要在 SSH 上使用 FIDO2 的用户服务端要有意识地把认证选项收拢。可以限制AuthenticationMethods只允许硬件密钥加上其他条件同时达标不要让 FIDO 成为唯一因子。比如强制要求公钥认证后再走一次keyboard-interactive或 TOTP至少能把单纯靠漏洞转移会话的攻击者拦下一层。同时检查授权文件里的限制选项。使用restrict、command、from这些限定词把 FIDO 密钥的权限压到最小。安全设计里有个概念叫“最小权限原则”套到 SSH 上也一样即使攻击者伪造身份成功如果那个身份本身就只能运行一个只读命令破坏面就大幅缩小。4.4 检测这种攻击的可操作性方法单靠日志排查这种攻击并不容易但也不是完全无迹可寻。最关键的特征是 user handle 与公钥的配对关系出现异常。正常情况下服务器注册表里一个用户句柄会固定对应一张或几张公钥如果日志里出现同一个 user handle 突然换了一个你没见过的公钥签名那就要警惕。另一类特征是登录源的变化。用户账号长期从办公网 IP 登录某天突然从攻击机 IP 登录而且剩余会话里执行的命令明显异常。配合 EDR 或者堡垒机的审计录像完全可以发现后续横向移动的行为。流量侧也有可看的点在客户端到服务端的 SSH 握手过程中主机密钥突然变化或者认证阶段出现不正常的错误重试。中间层工具虽然能模拟握手但很难完全模拟真实服务器的算法组合抓包对比SSH2_MSG_KEXINIT里支持的算法列表往往能看出端倪。4.5 一张自查速查表层级检查项期望状态libssh 服务端版本是否高于修复线是libssh 服务端FIDO 回调是否校验 user handle是SSH 服务端是否限制 AuthenticationMethods是客户端StrictHostKeyChecking 是否开启是客户端是否启用 SSH CA 主机证书推荐运维流程known_hosts 指纹是否定期核对推荐5. 常见问题与避坑实录5.1 复现时遇到的三个大坑第一个坑是客户端根本弹不出令牌交互。很多发行版的 OpenSSH 虽然支持sk-ed25519但运行环境里没有安装对应的 FIDO 支持库或者没有正确配置SecurityKeyProvider。症状表现为认证时只卡住没有触碰提示日志里还会提示provider相关的错误。解决办法是在实验前先用ssh-keygen -t ed25519-sk生成并测试一把确认本机能完成完整的令牌交互。第二个坑是主机指纹校验导致中间层瞬间暴露。如果你把实验客户端默认的StrictHostKeyChecking留在 yes第一次连接就会被拒。解决方法是按实验需求明确覆盖配置但操作前一定记住这只是测试环境不是生产环境的默认姿势。第三个坑是目标服务器版本弄错。有些网上教程会把 CVE 形容成 OpenSSH 漏洞导致有人在一台完全安全的 OpenSSH 上按步骤做实验自然复现不出任何异常。请记住这个 CVE 对应的实验环境一定要落到旧版 libssh 服务端上别的都没用。5.2 对 CVE 理解的几个常见误区很多人看到漏洞编号就下意识以为这是个“拿工具一键利用”的漏洞。实际上它更像个身份校验缺失的接口层缺陷漏洞利用的成功与否极度依赖周围协议上下文。没有中间人配合时它顶多造成身份混淆有了中间人拿到用户上下文后才算真正意义上完成了令牌钓鱼。也有人把问题和 NFC 中继攻击混淆。NFC 中继是物理层攻击攻击者把用户令牌的通信信号放大转发让远处服务器以为令牌就在现场。而 CVE-2021-36367 属于逻辑校验层面的漏洞它不需要接触用户设备只需要协议报文里的字段不符合预期就会被信任。两码事。此外别以为用户句柄是机密信息。它本身可能在断言响应里返回如果调试工具把断言打印出来句柄就能被人看到。防泄漏的重点不是把句柄藏起来而是服务端必须校验句柄与公钥、与用户名的绑定关系。句柄丢了不可怕可怕的是服务器不拿它当回事。5.3 经验总结我实际测试后的体感这套环境断断续续调了一周遇到的问题比预想多得多但也让我对 SSH 认证机制有了更深一层认识。五年前我可能还会说“只要上了硬件令牌SSH 就安全了”。现在我不会这么讲因为安全是一个链路令牌只是链路上一个环节协议实现里任何一段校验缺失都可能让你的强认证因子空转。我认为这个漏洞最值得传播的价值倒不是教大家怎么攻击而是提醒所有运维和安全同学升级依赖库不是小事尤其像 libssh 这种容易被集成到边缘设备里的库它不在你的日常视野里却在你的信任边界上。部署 FIDO2 之前多问一句服务端实现是走标准 OpenSSH 还是第三方库比买多少块高级令牌都有用。最后再做一次自查如果你手里正好有可接触的测试系统先确认两件事查 libssh 版本翻 known_hosts 里有没有出现不明指纹。这两件事的成本都很低效果往往比装一堆安全软件更直接。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。