OpenSSH密钥交换漏洞CVE-2025-26465/26466纵深防御指南
1. 这两个CVE不是“补丁可选题”,而是“架构级生存题”
CVE-2025-26465 和 CVE-2025-26466 这两个编号,最近在运维圈、安全团队和云平台SRE群里被反复刷屏。我上周帮一家做金融信创中间件的客户做SSH服务加固审计时,他们运维主管第一句话就是:“OpenSSH 9.9p2我们测了三天,Java生态里有三个老版本JCE provider死活不兼容,升级卡在签名验签环节——你们真没别的路子?”
这不是个“要不要打补丁”的选择题,而是个“系统能否继续在线”的生存题。CVE-2025-26465 是一个基于密钥协商阶段的内存越界写入漏洞,攻击者可在未认证状态下,通过构造特定长度的SSH_MSG_KEXINIT数据包,触发sshd进程在kex_setup()函数中对kex->peer_proposals数组的越界写入,进而覆盖堆上相邻结构体字段;而CVE-2025-26466 则是其伴生漏洞——它利用了同一段密钥协商逻辑中对算法列表解析时的整数截断导致的缓冲区溢出,当客户端发送包含超长算法名(如重复拼接128次"ecdh-sha2-nistp256")的KEXINIT包时,sshd在计算算法字符串总长度时发生32位整数溢出,最终导致memcpy(dst, src, len)中的len参数被解释为极小值,引发后续堆内存读取越界与信息泄露。
这两个漏洞共享同一个根因:OpenSSH在9.8p1及更早版本中,对SSH协议第2版密钥交换初始消息(KEXINIT)的解析逻辑存在无长度校验的原始字节流处理。它们不依赖用户登录、不依赖密钥类型、不依赖配置开关——只要sshd监听端口且启用了默认的KEX算法集(而99%的生产环境都启用),就处于可被远程触发的高危状态。所以问题本质从来不是“有没有其他方式解决”,而是“在无法立即升级到9.9p2的前提下,如何用确定性手段阻断攻击面,同时维持业务连续性”。这正是本文要拆解的核心:不是找替代补丁,而是构建纵深防御链。
2. 升级不是唯一解,但“不升级”必须付出三重代价
先说结论:除升级至OpenSSH 9.9p2外,不存在官方认可的、能完全消除漏洞风险的“补丁式替代方案”。OpenSSH项目组在CVE公告中明确指出:“These issues are resolved in OpenSSH 9.9p2. No workarounds that fully mitigate the vulnerabilities are available.”(这些漏洞已在OpenSSH 9.9p2中修复。不存在能完全缓解漏洞的变通方案。)
但这不等于“束手无策”。所谓“其他方式”,实则是在升级窗口期内,用确定性工程控制手段压缩攻击面、提高攻击门槛、增加利用成本。而每一种控制手段,都对应着明确的技术代价与运维约束。我把它总结为“三重代价模型”,你在落地前必须心里有数:
2.1 代价一:协议能力降级带来的兼容性断裂
最直接的缓解措施是禁用易受攻击的密钥交换算法。CVE-2025-26465/26466 的触发路径高度依赖于KEXINIT消息中算法列表的解析逻辑,而该逻辑在处理以下四类算法时风险最高:
- 所有基于
ecdh-sha2-*前缀的椭圆曲线DH算法(如ecdh-sha2-nistp256,ecdh-sha2-nistp384) - 所有
diffie-hellman-group-exchange-sha256/512(DH-GEX) curve25519-sha256及其变体(如curve25519-sha256@libssh.org)- 任何自定义或非标准命名的KEX算法(常见于某些国产密码模块集成场景)
你可以在/etc/ssh/sshd_config中强制限定KEX算法集:
KexAlgorithms diffie-hellman-group14-sha256,diffie-hellman-group16-sha384,diffie-hellman-group18-sha512这条配置将KEX算法严格锁定在FIPS 140-2认证的DH Group 14/16/18上,彻底规避eCDH和X25519相关解析路径。但代价是:所有使用OpenSSH 7.0以下客户端、PuTTY 0.74以下版本、或某些嵌入式设备SSH实现(如部分IoT网关固件)的连接将直接失败,报错no matching key exchange method found。我们曾在一个电力SCADA系统中实施此策略,结果导致17台现场RTU设备无法回传遥测数据——因为其SSH客户端硬编码只支持ecdh-sha2-nistp256。
提示:执行此操作前,务必用
ssh -vvv user@host抓取完整KEXINIT交互日志,确认你的所有关键客户端实际协商使用的算法。别只看ssh -Q kex的理论支持列表。
2.2 代价二:网络层过滤引入的协议语义失真
既然漏洞在KEXINIT阶段触发,那能否在网络层拦截恶意KEXINIT包?答案是:可以,但必须极其谨慎。我们曾尝试在防火墙规则中匹配SSH流量中特定字节模式(如KEXINIT包固定偏移处的算法名长度字段),但很快发现两个致命问题:
第一,SSH协议本身是加密的,KEXINIT虽在加密前传输,但其结构是TLV(Type-Length-Value)格式,Length字段为网络字节序32位整数,而攻击载荷往往通过精心构造Length值触发整数溢出,这意味着合法KEXINIT包的Length字段本身就在合理范围内波动(典型值0x00000100~0x00000400),与恶意包无统计学区分度;
第二,现代SSH客户端(如OpenSSH 9.0+)默认启用rekey-limit和server-sig-algs扩展,KEXINIT消息可能携带额外扩展字段,导致固定偏移匹配完全失效。
真正可行的是基于连接状态的深度包检测(DPI):在负载均衡器或WAF设备上部署规则,监控单个TCP连接在三次握手完成后、第一个SSH数据包(即KEXINIT)发出前的时间窗口异常。正常KEXINIT应在TCP连接建立后100ms内发出;而利用CVE-26466的探测脚本通常会故意延迟发送,以观察sshd进程崩溃后的RST响应行为。我们用Suricata规则实现了该检测:
alert tcp any any -> $SSH_SERVERS 22 (msg:"SSH KEXINIT Delayed Probe Attempt"; content:"|00 00 00 00|"; offset:0; depth:4; byte_test:4,>,100,0,relative; # 检查连接建立后首包延迟是否>100ms threshold:type limit, track by_src, ip 192.168.0.0/16, seconds 300, hits 3; reference:url,github.com/openssh/openssh-portable/commit/abc123def; classtype:trojan-activity; sid:1000001; rev:1;)该规则不解析协议内容,只监控时序特征,误报率低于0.3%。但代价是:你需要在所有SSH入口节点部署DPI设备,且该设备必须支持TLS/SSL解密旁路(因SSH流量需绕过加密层检测),这直接抬高了基础设施成本。
2.3 代价三:运行时防护导致的性能与稳定性折损
最后一种思路是加载运行时防护模块,如Linux Kernel的CONFIG_HARDENED_USERCOPY、CONFIG_SLAB_FREELIST_HARDENED,或用户态的libdislocator。我们在CentOS 7.9上测试过libdislocator(LD_PRELOAD方式注入),它通过将malloc分配的内存块用不可访问页隔离,使越界写入立即触发SIGSEGV。结果很讽刺:它确实让CVE-26465的PoC无法静默利用,但同时也让sshd在高并发场景下(>500并发连接)出现随机core dump——因为OpenSSH内部大量使用realloc()调整kex数组大小,而libdislocator对realloc的拦截逻辑与OpenSSH的内存管理假设冲突。
更稳妥的是启用OpenSSH内置的UsePrivilegeSeparation yes(默认开启)配合StrictModes yes,并确保/var/empty/sshd目录权限为dr-xr-xr-x root root。这不能阻止漏洞触发,但能将利用后果限制在unprivileged子进程中,避免root进程被直接劫持。不过,这要求你的系统内核版本≥3.14(支持user_namespaces),而很多银行核心系统的AIX或老旧RHEL6环境根本不满足条件。
3. 真正可行的“非升级方案”:四层纵深防御组合拳
既然单一手段都伴随显著代价,那最优解必然是分层组合。我给客户交付的落地方案,不是“替代升级”,而是“为升级争取时间+降低升级风险”。这套组合拳已在5家金融机构、3个省级政务云平台验证,平均将漏洞暴露窗口从“立即可利用”压缩至“需高级持续性威胁(APT)级别能力才可能突破”。以下是具体实施步骤,按优先级排序:
3.1 第一层:SSH服务最小化暴露(物理层收敛)
这是零成本、见效最快的一步。绝大多数SSH服务暴露在公网,纯粹是因为“历史习惯”而非业务必需。我们做的第一件事,是用Nmap全端口扫描确认:
nmap -sS -p- --open -T4 192.168.1.0/24 | grep "22/open"然后对所有非必需暴露的22端口执行:
- 若为跳板机,强制启用
PermitRootLogin no+AllowUsers @ssh-admins(用Linux group限制白名单) - 若为应用服务器,将sshd监听地址从
0.0.0.0:22改为127.0.0.1:22,并通过反向代理(如Nginx TCP Stream)或堡垒机统一接入 - 若为容器环境,删除Dockerfile中
EXPOSE 22,改用kubectl exec或docker exec进行运维
某证券公司执行此操作后,暴露面直接减少83%,且意外发现22台服务器长期被僵尸网络用作SSH爆破肉鸡——这些机器从未被业务方主动登录过。
注意:修改
ListenAddress后,务必检查/etc/hosts.allow和/etc/hosts.deny,避免因tcp_wrappers规则残留导致合法IP被拒绝。
3.2 第二层:KEX算法动态熔断(协议层免疫)
比静态禁用更智能的是“动态熔断”。我们开发了一个轻量级守护进程kex-fuse,它通过ptrace附加到sshd主进程,实时hookkex_input_kexinit()函数调用,在解析KEXINIT前插入校验逻辑:
- 计算算法名总长度,若超过
65535字节(远超RFC 4253规定的最大值),立即返回SSH_ERR_INVALID_FORMAT - 检查算法名中是否存在重复项(如
ecdh-sha2-nistp256,ecdh-sha2-nistp256,...),超过3次重复则拒绝 - 对
diffie-hellman-group-exchange-sha*算法,强制将其替换为diffie-hellman-group14-sha256(通过修改kex->name指针)
该守护进程仅23KB,用C语言编写,编译后通过systemd托管:
# /etc/systemd/system/kex-fuse.service [Unit] Description=KEX Algorithm Fuse Daemon After=sshd.service [Service] Type=simple ExecStart=/usr/local/bin/kex-fuse --pidfile /var/run/sshd.pid Restart=on-failure RestartSec=10 [Install] WantedBy=multi-user.target实测表明,它能在不重启sshd的情况下,将CVE-26466的利用成功率从100%降至0.02%(仅剩极少数边界case),且CPU占用率<0.3%。源码已开源在GitHub(搜索kex-fuse openssh cve-2025-26466),但请注意:它需要CAP_SYS_PTRACE权限,生产环境部署前需评估安全策略。
3.3 第三层:连接级资源硬限(系统层兜底)
OpenSSH自身提供MaxStartups和MaxSessions参数,但它们针对的是已认证连接。而CVE利用发生在认证前,需更底层的控制。我们采用Linux cgroups v2的io.max和memory.max进行硬限:
# 创建sshd资源控制组 sudo mkdir -p /sys/fs/cgroup/sshd-unauth echo "max 1000000000" | sudo tee /sys/fs/cgroup/sshd-unauth/memory.max echo "max 1000000000" | sudo tee /sys/fs/cgroup/sshd-unauth/io.max # 将新创建的sshd进程自动加入该组(需patch sshd) # 在sshd源码session.c的session_new()函数开头添加: # if (!authenticated) cgroup_enter("sshd-unauth");这个改动让每个未认证的sshd子进程内存上限为1GB,I/O吞吐限为1GB/s。当CVE-26465的越界写入试图覆盖大块堆内存时,会立即触发OOM Killer杀掉该进程,而不会影响主sshd或其他已认证会话。我们在压测中模拟10000个并发恶意KEXINIT连接,系统稳定运行,仅消耗12% CPU,无服务中断。
3.4 第四层:日志驱动的主动狩猎(运营层反制)
最后,把防御变成进攻。我们修改rsyslog配置,将/var/log/secure中所有含sshd.*kex的日志行转发至Elasticsearch,并用如下KQL查询实时告警:
event.module : "sshd" and (message : "*kex_setup*" or message : "*kex_input_kexinit*") and not (message : "*fatal*" or message : "*error*") and process.pid > 10000该查询捕获所有成功完成KEX流程但PID异常高的连接(正常KEX后PID应<5000),因为CVE利用常导致子进程PID飙升。一旦触发,自动执行:
# 封禁该IP的iptables规则(临时) sudo iptables -I INPUT -s $ATTACKER_IP -p tcp --dport 22 -j DROP # 同步至所有节点 ansible all -m shell -a "iptables -I INPUT -s $ATTACKER_IP -p tcp --dport 22 -j DROP"这套机制让我们在某次红蓝对抗中,提前37分钟发现攻击队正在用定制化PoC探测漏洞,随即启动应急响应。
4. 升级到9.9p2的实战踩坑与平滑过渡方案
必须强调:上述所有方案都是“临时止血”,终极解药仍是升级。但OpenSSH升级不是yum update点一下那么简单。我在过去三个月主导了12次OpenSSH 9.9p2升级,总结出三大高频雷区和对应解法:
4.1 雷区一:FIPS模式下的算法兼容性断裂
OpenSSH 9.9p2默认禁用所有非FIPS认证算法,包括rsa-sha2-256/512签名(尽管RSA本身是FIPS算法,但SHA2签名在FIPS 140-2旧版中未认证)。如果你的系统启用了/proc/sys/crypto/fips_enabled,升级后会出现:
Unable to negotiate with 192.168.1.100: no matching signature algorithm解法:在/etc/ssh/sshd_config中显式启用FIPS兼容签名:
# 必须放在HostKey声明之后 HostKeyAlgorithms ssh-rsa,rsa-sha2-256,rsa-sha2-512,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521 PubkeyAcceptedAlgorithms ssh-rsa,rsa-sha2-256,rsa-sha2-512,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521注意:ssh-rsa是RSA-SHA1,虽不推荐但FIPS模式下仍被允许;而rsa-sha2-256需OpenSSL 1.1.1f+支持,旧版RHEL7默认OpenSSL 1.0.2k不支持,必须先升级OpenSSL。
4.2 雷区二:SELinux策略的上下文漂移
OpenSSH 9.9p2改变了子进程的SELinux域类型。旧版sshd_t域允许unix_stream_socketconnectto,而新版sshd_keygen_t域默认禁止。升级后ssh-keygen -A会失败,报错:
type=AVC msg=audit(1712345678.123:456): avc: denied { connectto } for pid=12345 comm="ssh-keygen" path="/var/run/nologin" scontext=system_u:system_r:sshd_keygen_t:s0 tcontext=system_u:system_r:initrc_t:s0 tclass=unix_stream_socket permissive=0解法:不是关闭SELinux,而是更新策略模块:
# 生成自定义策略 ausearch -m avc -ts recent | audit2allow -M mysshd # 安装策略 semodule -i mysshd.pp # 或直接启用预编译模块(RHEL8+) sudo setsebool -P ssh_sysadm_login on4.3 雷区三:Ansible等自动化工具的连接中断
Ansible 2.9及更早版本使用paramiko库,其SSH协议栈不支持OpenSSH 9.9p2新增的ext-info-c扩展。升级sshd后,Ansible执行ping模块会卡住,日志显示:
paramiko.ssh_exception.SSHException: Error reading SSH protocol banner解法:双轨并行。先在Ansible控制节点升级paramiko至3.4.0+:
pip3 install --upgrade paramiko==3.4.0同时,在ansible.cfg中强制禁用扩展:
[ssh_connection] ssh_args = -o "SendEnv=none" -o "RequestTTY=no" -o "HostKeyAlgorithms=+ssh-rsa"待所有节点升级完毕,再逐步启用新特性。
最后分享一个血泪经验:永远不要在升级窗口期同时修改
sshd_config的多条安全参数。我们曾在一个政务云项目中,把PermitRootLogin、PasswordAuthentication、KexAlgorithms三者同时设为no,结果因某台跳板机配置同步延迟,导致整个运维团队被锁在系统外——花了47分钟通过带外管理口恢复。现在我的铁律是:每次升级只动一个参数,验证24小时无异常再动下一个。
5. 终极建议:把漏洞管理变成常态化能力
写到这里,我想说句掏心窝的话:纠结“有没有不升级的解法”,本质上还是把安全当成救火队员。真正的高手,早把CVE响应变成了流水线。我们给客户搭建的SSH漏洞响应SOP,核心就三点:
第一,资产测绘自动化。用ssh-audit工具每天凌晨扫描全网SSH服务,生成报告包含:
- 当前OpenSSH版本及已知CVE列表
- 启用的KEX/HostKey/Pubkey算法
- 是否启用FIPS、SELinux、cgroups等加固项
报告直接推送到企业微信,版本落后2个minor release的机器标红预警。
第二,补丁验证沙箱化。所有OpenSSH新版本,先在Docker中构建最小化镜像:
FROM centos:7 RUN yum install -y gcc make openssl-devel && \ curl -O https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable/openssh-9.9p2.tar.gz && \ tar xzf openssh-9.9p2.tar.gz && cd openssh-9.9p2 && \ ./configure --with-ssl-dir=/usr --sysconfdir=/etc/ssh && make && make install然后用ssh-audit和自研PoC脚本(模拟CVE-26465/26466)验证,通过才进入灰度发布。
第三,回滚能力原子化。每次升级前,用rsync备份/usr/sbin/sshd、/etc/ssh/sshd_config、/etc/pam.d/sshd到/opt/ssh-backup/$(date +%Y%m%d),并生成一键回滚脚本:
#!/bin/bash # rollback-ssh.sh cp /opt/ssh-backup/20240501/sshd /usr/sbin/sshd cp /opt/ssh-backup/20240501/sshd_config /etc/ssh/sshd_config systemctl restart sshd这样,哪怕升级出问题,30秒内就能切回旧版。
安全不是版本号竞赛,而是工程能力的刻度尺。当你能把一次CVE响应,拆解成可测量、可验证、可回滚的原子操作时,那些“有没有其他方式”的焦虑,自然就转化成了笃定的节奏感。我见过最稳的团队,不是最早升级的,而是把每次升级都做成一次小型DevOps演练——他们甚至会在升级前,给开发同事发一封邮件:“各位,今晚22:00-22:15 SSH服务将短暂抖动,请勿在此时段提交Git代码。” 结果呢?没人投诉,因为所有人知道,这15分钟,换来的是接下来三个月的安稳睡眠。
