
1. 项目概述一次典型的漏洞复现踩坑实录最近在整理内部安全团队的技能矩阵时我决定带新人复现一些经典的、有教学意义的漏洞。CVE-2018-15473这个OpenSSH的用户名枚举漏洞自然就进入了我的视野。它不像那些能直接获取权限的漏洞那么“刺激”但它在信息收集阶段的价值以及在渗透测试中体现的“优雅”和“隐蔽性”恰恰是安全从业者需要深刻理解的。我选择了在Vulhub这个优秀的漏洞靶场集成环境中进行复现本以为是个“开箱即用”的简单演示结果却意外地踩了一连串的坑。从环境启动报错、到POC脚本运行失败再到对漏洞原理的模糊地带产生疑问整个过程几乎把新手可能遇到的问题都经历了一遍。这篇文章就是我这次复现过程的完整记录和问题解决手册希望能帮你绕过这些弯路不仅成功复现漏洞更能透彻理解其背后的每一个技术细节。2. 漏洞原理深度解析为什么格式错误的数据包能泄露信息在动手之前我们必须先搞清楚这个漏洞到底是怎么一回事。很多复现文章只告诉你怎么运行脚本但如果不明白原理一旦脚本报错你根本无从下手调试。2.1 核心漏洞机制连接过程与异常处理的差异OpenSSH在用户认证阶段遵循一个标准的流程客户端先发起连接服务端返回支持的认证方法如publickey, password。然后客户端会发送一个认证请求包这个包里包含了要尝试认证的用户名和具体的认证方法比如“password”。CVE-2018-15473的根源在于OpenSSH服务端7.7及之前版本在处理认证请求包的用户名字段时存在逻辑缺陷。具体来说正常流程当客户端发送一个格式完全正确的SSH认证请求包SSH_MSG_USERAUTH_REQUEST时无论用户名是否存在服务端都会进入后续的认证逻辑。如果用户名不存在服务端最终会回复认证失败但这个失败是发生在认证方法检查之后并且返回的是SSH_MSG_USERAUTH_FAILURE数据包。漏洞触发流程如果客户端发送的认证请求包在用户名字段的格式上是错误的例如用户名字段被故意构造为包含非法字符或格式不完整导致sshpkt_get_cstring()函数在解析时失败服务端的处理逻辑就出现了分支。用户名存在服务端在解析到错误的用户名格式时会立即中断连接并返回一个SSH2_DISCONNECT_PROTOCOL_ERROR类型的断开连接消息同时关闭TCP连接。用户名不存在服务端在解析到错误的用户名格式时则不会立即断开连接而是会返回一个SSH_MSG_USERAUTH_FAILURE数据包表示认证失败但连接保持。这种差异就是漏洞的本质通过观察服务端对畸形认证请求的响应方式是立即断连还是返回认证失败攻击者可以推断出用户名是否存在。注意这里有一个极其关键的细节也是很多文章语焉不详的地方。漏洞利用的不是“用户名不存在会返回不同错误码”而是“用户名存在时对畸形数据的处理会触发一个协议级错误并导致连接关闭用户名不存在时则走正常的、温和的认证失败流程”。响应时间上也可能有细微差别但最可靠的判断标志是TCP连接是否被服务端主动关闭。2.2 漏洞影响与修复这个漏洞的危害在于它允许攻击者在无需有效凭证的情况下枚举系统上的有效用户名。这对于后续的密码爆破、社会工程学攻击或针对特定用户的漏洞利用至关重要。它破坏了认证过程的机密性。官方修复方案OpenSSH 7.7及以上版本也很直接统一了错误处理路径。无论用户名是否存在当收到格式错误的认证请求包时服务端都采用相同的处理方式——返回SSH_MSG_USERAUTH_FAILURE并保持连接从而消除了可观测的差异。3. 环境搭建与初始配置Vulhub的“陷阱”与解决我选择Vulhub是因为它提供了容器化的、一键式的漏洞环境理论上省去了自己编译旧版本OpenSSH的麻烦。但正是这种“便利”隐藏了一些环境依赖的坑。3.1 Vulhub环境启动与常见报错按照Vulhub官方文档进入/openssh/CVE-2018-15473目录执行docker-compose up -d。然而问题很快就出现了。问题一docker-compose命令未找到或版本过低。这通常是因为系统只安装了Docker Engine但没有安装Docker Compose插件新版本或独立工具旧版本。解决方案对于较新系统Docker Engine 24Docker Compose已作为插件集成。你需要安装的是docker-compose-plugin。# Ubuntu/Debian sudo apt-get update sudo apt-get install docker-compose-plugin # 验证安装 docker compose version注意命令是docker compose没有横杠。对于旧系统或习惯独立工具可以安装Python pip版本的docker-compose。sudo pip3 install docker-compose # 验证安装 docker-compose version问题二启动过程中镜像拉取失败或容器启动后立即退出。这可能是网络问题也可能是Vulhub的Dockerfile中某些基础镜像的链接失效了。解决方案检查Docker服务状态sudo systemctl status docker。手动拉取基础镜像查看Dockerfile看它基于哪个镜像如ubuntu:16.04。尝试手动拉取docker pull ubuntu:16.04。查看详细日志使用docker-compose logs或docker logs container_id查看容器启动失败的具体原因。我遇到的一次是容器内apt-get update源失效需要修改Dockerfile中的源地址为可用的归档镜像源。直接使用预构建镜像如果可用有时Vulhub项目在Docker Hub上有预构建的镜像可以直接修改docker-compose.yml中的image字段指向它而不是本地构建。3.2 确认环境状态环境成功启动后你需要确认两件事容器运行状态执行docker ps应该能看到一个运行中的容器映射了主机的22端口或其他端口如2222到容器的22端口。SSH服务版本通过docker exec进入容器或者直接从主机连接检查SSH版本。# 从主机连接假设映射端口是2222 ssh -p 2222 rootlocalhost -o UserKnownHostsFile/dev/null -o StrictHostKeyCheckingno # 连接后或进入容器查看版本 ssh -V确认版本是低于7.7的例如Vulhub环境通常是OpenSSH_7.5p1。4. 漏洞复现实操与POC脚本分析环境就绪后就到了最关键的利用环节。网络上流传的POC脚本很多我用的是一个基于Python的经典脚本。但直接运行大概率会出错。4.1 原始POC脚本运行与报错一个典型的POC脚本会利用Python的socket和paramiko或其底层传输机制来构造并发送畸形的认证包。当你运行类似python3 ssh_username_enum.py 192.168.1.10 2222 userlist.txt的命令时可能会遇到ModuleNotFoundError: No module named ‘paramiko’这是最常见的问题脚本依赖paramiko库。解决pip3 install paramiko。建议使用虚拟环境。Authentication failed.对所有用户脚本没有正确识别出有效用户。这通常不是环境问题而是POC脚本本身与目标SSH服务版本或配置存在兼容性问题或者脚本的逻辑判断条件不够健壮。连接超时或连接被拒绝检查目标IP和端口是否正确防火墙是否放行容器是否正常运行。脚本报错SSHException或socket.error可能是脚本中的网络通信代码不够健壮或者目标服务对快速连接尝试进行了限制。4.2 POC脚本核心逻辑拆解与改造我们不能只做一个脚本的搬运工。我们来拆解一个POC的核心部分并说明如何让它更健壮。以下是一个简化但关键逻辑完整的示例import socket import sys import time import logging # 配置日志便于调试 logging.basicConfig(levellogging.INFO) def check_username(host, port, username): 检查用户名是否存在。 返回 True 如果用户名可能存在连接被立即关闭 返回 False 如果用户名可能不存在收到认证失败包。 sock None try: # 1. 建立TCP连接 sock socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(5) # 设置超时非常重要 sock.connect((host, port)) # 2. 接收服务端初始横幅 (Banner) banner sock.recv(1024).decode(utf-8, errorsignore) logging.debug(fBanner: {banner}) # 3. 发送客户端标识 (这里简化实际SSH协议需要交换版本号) # 我们直接跳到关键部分构造并发送畸形的 SSH_MSG_USERAUTH_REQUEST 包 # 这里省略了完整的SSH协议握手密钥交换等这是很多简单POC不稳定的原因。 # 一个更稳定的方法是用paramiko的Transport类来建立连接然后注入畸形包。 # 4. 发送畸形认证请求包 # 构造一个用户名字段格式错误的数据包。 # 一种常见手法是发送一个包含 \n (换行符) 的用户名字符串因为sshpkt_get_cstring() 以null结尾遇到\n可能解析异常。 # 实际构造需要精确的二进制格式。以下是概念性代码 # packet construct_malformed_auth_packet(username) # sock.send(packet) # 5. 观察响应 # 设置一个很短的超时来读取响应 sock.settimeout(1.0) try: response sock.recv(1024) # 如果收到了数据很可能是 SSH_MSG_USERAUTH_FAILURE说明用户名可能不存在 logging.debug(fReceived response: {response.hex()[:50]}...) return False # 用户名可能不存在 except socket.timeout: # 超时了没有收到任何数据这需要结合连接状态判断 pass # 关键检查连接是否还被对端保持着 # 发送一个微小探测例如尝试再读一次或者检查socket错误 sock.settimeout(0.5) try: sock.recv(1, socket.MSG_PEEK) # 窥探一个字节不消费 # 如果成功说明还有数据可读或连接正常这可能意味着之前收到了失败包但没读干净或者情况复杂 # 更可靠的判断是在发送畸形包后如果连接立刻被对端关闭recv会返回空字节串 # 我们需要重构上面的逻辑 except (socket.timeout, ConnectionResetError, BrokenPipeError): # 连接已被对端重置或断开 logging.debug(Connection was closed by peer.) return True # 用户名可能存在触发了协议错误导致断开 except Exception as e: logging.error(fError checking {username}: {e}) return None # 不确定 finally: if sock: sock.close() return False # 默认返回不存在 # 实际利用中强烈建议使用成熟的库如paramiko monkey-patch或经过验证的POC。从上面的代码你可以看到自己从头实现一个稳定的利用脚本非常复杂需要深入理解SSH二进制协议。因此我强烈建议使用社区维护的、经过验证的工具例如ssh-username-enum这个Python工具。即使使用这些工具也可能需要调整。4.3 使用成熟工具进行复现以ssh-username-enum为例安装工具git clone https://github.com/offensive-security/exploitdb.git # 在exploitdb的存档中找到对应的脚本或者搜索独立的项目。 # 实际上一个更知名的专门工具是来自Rhino Security Labs的脚本。 # 假设我们找到了一个可靠的版本ssh_username_enum.py准备用户名字典创建一个users.txt文件每行一个用户名例如root,admin,test,ubuntu。运行枚举python3 ssh_username_enum.py --port 2222 127.0.0.1 users.txt常见问题工具可能因为Python版本、paramiko版本或网络延迟而误判。解决技巧增加延迟在工具的参数中或代码里在每次探测之间添加time.sleep(0.5)避免被服务端限速或误判。多次验证对“可能存在”的用户名单独运行2-3次确认减少误报。查看调试输出如果工具支持-v或--debug参数打开它观察每次交互的详细日志理解其判断依据。5. 复现过程中的疑难杂症与解决方案即使使用了成熟工具我在复现时依然遇到了几个棘手问题以下是排查和解决过程。5.1 问题工具运行无结果或全部返回“无效”可能原因1目标SSH版本已修复。排查再次确认容器内OpenSSH版本是否为7.7之前。docker exec container_id sshd -V。解决确保Vulhub环境构建正确没有意外升级了openssh-server包。可能原因2容器或主机防火墙干扰。排查在容器内执行netstat -tlnp | grep :22确认sshd进程在监听。从主机尝试telnet localhost 2222映射端口看是否能连通。解决检查Docker的防火墙规则如firewalld, ufw和主机的安全组如果是在云服务器上确保端口映射无误。可能原因3POC脚本的逻辑与特定服务端实现不匹配。排查这是最复杂的情况。需要打开调试模式对比脚本发送的数据包和正常客户端如OpenSSH客户端发送的数据包有何不同。可以使用Wireshark在主机上抓取port 2222的流量进行分析。解决尝试换用另一个POC脚本。有时不同脚本构造畸形包的方式略有差异可能其中一个恰好对目标有效。5.2 问题误报率很高不存在的用户也被报告为存在可能原因1网络延迟或抖动导致连接超时被误判为“断开”。解决显著增加脚本中的socket超时时间例如从2秒增加到5秒或更长。在探测之间加入更长的睡眠时间如time.sleep(1)。可能原因2服务端配置了MaxAuthTries等限制。排查查看容器内/etc/ssh/sshd_config是否有MaxAuthTries 3之类的配置。这会导致在多次失败认证后服务端主动断开连接模拟了漏洞触发的行为。解决修改sshd配置将其注释或设为一个很大的值然后重启sshd服务service ssh restart。注意在生产环境中绝不能这样做这仅用于实验环境。可能原因3脚本的判定逻辑有缺陷。解决如前所述依赖TCP连接断开与否是最可靠的指标。检查脚本是否错误地将“读取超时”等同于“连接断开”。一个更健壮的方法是在发送畸形包后尝试发送一个小的、无害的协议数据如果连接还存活或者直接捕获ConnectionResetError异常。5.3 问题复现成功但如何验证枚举出一些用户名如root,ubuntu后如何确认这真的是漏洞利用的结果而不是巧合正向验证尝试用枚举出的“存在”的用户名和任意密码或空密码进行正常的SSH连接尝试。虽然会认证失败但错误信息通常是“Permission denied”而不是“Invalid user”。你可以用OpenSSH客户端观察ssh -p 2222 nonexistentuserlocalhost # 输出可能包含Permission denied (publickey,password). 或者 Invalid user... ssh -p 2222 rootlocalhost # 输出可能包含Permission denied (publickey,password). 注意这里没有“Invalid user”。注意有些SSH配置会隐藏用户是否存在的信息统一返回“Permission denied”。反向验证修改POC脚本让它对已知不存在的用户如randomstring12345进行测试观察其返回结果是否为“不存在”即连接未断开。同时对已知存在的用户如root测试结果应为“存在”连接断开。这能验证你的利用链在特定环境下是有效的。6. 从复现到理解漏洞挖掘与防御的启示成功复现漏洞只是第一步更重要的是从中提炼出安全研究和防御的思路。6.1 漏洞挖掘视角关注异常处理路径CVE-2018-15473给我们的经典启示是安全研究员需要特别关注软件在不同逻辑路径下的异常处理行为是否一致。无论是错误信息、返回时间、还是资源状态如连接是否保持任何差异都可能成为信息泄露的渠道。在代码审计时可以刻意构造各种边界和异常输入观察程序的行为是否有可区分的差异。6.2 防御加固建议对于系统管理员和安全工程师这个漏洞的防御措施是明确的及时升级将OpenSSH升级至7.7或更高版本。这是最根本的解决方案。网络层面限制如果因特殊原因无法立即升级可以考虑使用网络防火墙或入侵防御系统IPS来检测和阻止频繁的、针对SSH的用户名枚举行为。例如对短时间内来自同一源IP的大量SSH认证失败尝试进行告警或阻断。修改配置缓解虽然不能完全消除漏洞但可以修改sshd_config来增加攻击者的成本MaxAuthTries 3限制单次连接的最大认证尝试次数。LoginGraceTime 30设置登录宽限期超时后断开连接。使用AllowUsers或AllowGroups白名单明确指定允许登录的用户即使攻击者枚举出其他系统用户也无法登录。监控与审计加强SSH日志的监控/var/log/auth.log或/var/log/secure关注异常认证模式及时发现扫描行为。6.3 对安全测试的思考在渗透测试中用户名枚举是信息收集的关键一步。即使目标系统已修复此特定漏洞测试人员也应掌握多种枚举方法例如通过SMTP服务的VRFY、EXPN命令。通过SNMP服务查询。通过Web应用的用户注册、登录、密码找回等功能的反馈信息差异。通过Kerberos等单点登录系统的错误信息。理解CVE-2018-15473的原理能帮助你更好地理解“信息泄露”类漏洞的普遍模式从而在测试中更具洞察力。复现漏洞不只是为了运行一个脚本而是为了将那个“Aha!”时刻——理解攻击者如何从细微差异中提取信息——转化为自己评估系统安全性的能力。