测试环境弱口令实战复盘:从SSH暴力破解到自动化口令治理

发布时间:2026/5/25 14:14:01

测试环境弱口令实战复盘:从SSH暴力破解到自动化口令治理 1. 这不是黑客电影是测试环境里真实发生的“开门揖盗”上周五下午四点十七分我正准备下班钉钉弹出一条告警测试服务器 CPU 使用率持续 98% 超过 5 分钟。这台机器平时负载不到 5%连 Docker 容器都只跑着两个 Spring Boot 的 demo 接口。我顺手 ssh 连上去看一眼输入密码后——卡住了。三秒后终端返回Connection closed by remote host。再试一次还是断开。我换了个终端重连这次直接提示Permission denied, please try again.。不是我记错了密码是密码已经失效了。查日志发现/var/log/auth.log里密密麻麻全是Failed password for root from 192.168.3.11 port 54212 ssh2这类记录时间戳从凌晨 2:13 开始每秒 3–5 条持续了近 4 小时。更关键的是在凌晨 3:47:22出现了一行Accepted password for root from 192.168.3.11 port 54212 ssh2——攻击者用暴力猜解的方式撞开了 root 账户。而那个密码是admin123。没错就是你猜的那个 admin123没改过、没加 salt、没启双因素就静静躺在/etc/shadow里像一把没上锁的门把手。这不是渗透测试团队干的是我们自己运维同学在搭建测试环境时随手设的临时密码。它本该在部署完成当天就被替换结果被遗忘在 Jira 待办列表第 7 页的某个子任务里。这件事让我意识到弱口令从来不是“理论风险”而是测试环境里最常见、最廉价、也最致命的单点故障入口。它不依赖零日漏洞不挑战内核机制只靠字典耐心自动化脚本就能让一台本该隔离的测试机变成跳板、矿机、勒索中转站甚至横向移动的起点。本文不讲高深的加密原理也不堆砌 Kali 工具链而是以这次真实沦陷为切片完整还原一次弱口令引发的 SSH 暴力破解全过程从攻击者视角看字典怎么选、速率怎么控、如何绕过基础防护从防御者视角看日志怎么读、阈值怎么设、配置怎么改才真正有效最后给出一套可落地的、非口号式的测试环境口令治理 checklist。适合 DevOps、测试工程师、初级安全同学以及所有会配 Linux 服务器但未必懂“为什么这样配”的人。2. 攻击链路拆解暴力不是蛮干是带节奏的精准打击很多人以为暴力破解就是拿个字典狂轰滥炸其实不然。现代 SSH 暴力攻击早已脱离“穷举”阶段演变为一套节奏清晰、规避意识强、成本极低的标准化流程。这次攻击所用的工具链非常典型hydra 自定义字典 IP 轮换代理池虽未启用但日志显示其具备该能力。我们来逐层剥开它的动作逻辑。2.1 字典选择不是越大越好而是“够用且高效”攻击者使用的字典并非网上下载的 10GB 通用密码库而是高度聚焦的“测试环境专用词表”。我从攻击 IP 的 HTTP 请求头中反向追踪到其 C2 服务器一个托管在境外的静态页面扒下了它加载的字典片段。核心结构如下类型示例条目占比设计意图默认凭证组合root:admin123,admin:password,test:test12332%针对开发/测试人员习惯性复用默认账号密码环境特征词根数字变体devops:devops2023,jenkins:jenkins1,gitlab:gitlab828%利用服务名、主机名、部署时间等公开信息生成弱口令高频模式admin123,123456,password,qwe12325%覆盖 NIST SP 800-63B 明确禁止的 Top 100 弱口令大小写符号扰动Admin123!,ADMIN123,admin12315%应对部分系统强制的“复杂度策略”表面合规这个字典共 12,847 行远小于常见的 rockyou.txt1430 万行但命中率极高。原因在于它不做全量覆盖而是做“上下文精准打击”。比如目标服务器 hostname 是test-jenkins-01攻击者会优先尝试jenkins:jenkins2023、test:test123、01:012345这类组合而非盲目扫user:123456。我在本地用hydra -L users.txt -P dict.txt -t 4 ssh://192.168.3.11复现时仅用 17 分钟就爆破成功——而用完整 rockyou.txt耗时超过 6 小时且无结果。提示很多团队花大力气部署 WAF 或 IDS却对字典本身毫无认知。记住防御的第一道防线永远是你对攻击者思维的理解深度。建议运维团队定期用自身环境信息主机名、服务名、部署日期生成一份“反向字典”然后用它测试自己的服务器比任何扫描报告都真实。2.2 速率控制与连接策略慢即是快的艺术攻击日志显示峰值请求速率为 4.2 次/秒但整体呈现明显的“脉冲式”分布每 90 秒集中发起 200–300 次尝试随后静默 60 秒。这种节奏绝非随机而是精心设计的规避策略绕过 fail2ban 默认阈值fail2ban 默认bantime 60010 分钟、findtime 60010 分钟内触发 5 次失败即封禁。攻击者将单次脉冲控制在 300 次内间隔大于 10 分钟确保每次触发都在新周期内永远卡在封禁阈值之下。降低服务器负载感知持续高并发会触发 CPU 告警或连接数限制如MaxStartups 10:30:60。脉冲式攻击让平均负载维持在 0.3–0.5远低于监控告警线通常设为 2.0避免引起人工关注。适配网络抖动SSH 连接建立涉及 TCP 三次握手、密钥交换、认证协商。攻击者设置timeout 10超时 10 秒并内置重试逻辑最多 2 次确保单次失败不影响整体节奏。我在测试环境中用tc模拟 100ms 网络延迟后发现其成功率下降 37%但攻击者通过延长脉冲间隔至 150 秒、减少单次请求数至 150 条迅速恢复了 92% 的成功率。这说明真正的攻击者不是在和你的防火墙对抗而是在和你的监控粒度、响应速度、运维习惯博弈。2.3 认证成功后的“静默期”比入侵更危险的是不被发现从Accepted password for root日志出现到 CPU 告警触发中间隔了 1 小时 24 分钟。这段时间里攻击者做了三件事环境测绘执行uname -a; cat /etc/os-release; df -h; ps aux --forest确认系统版本、磁盘空间、进程树结构权限维持创建隐藏用户xadmUID 0在/etc/passwd中添加xadm:x:0:0:root:/root:/bin/bash并修改/root/.bash_history清除操作痕迹横向探针扫描内网192.168.3.0/24段的 22 端口发现另一台 Jenkins 服务器192.168.3.12同样使用jenkins:jenkins1凭证立即建立 SSH 隧道进行跳转。关键点在于所有操作均通过screen会话后台运行命令前缀统一加nohup输出重定向至/dev/null且全程未调用curl、wget等易被审计的外连工具。它甚至没下载任何恶意软件只是利用已有的gcc编译了一个内存驻留的挖矿程序/tmp/.sysupdate并通过crontab -e添加每 5 分钟执行一次的持久化任务。注意很多团队认为“只要没下载木马就算没被黑”这是巨大误区。现代攻击的终点不是植入后门而是达成业务目标挖矿、勒索、数据窃取。而达成目标所需的全部能力Linux 系统自带工具链已足够完备。这次攻击中gcc、ps、netstat、crontab全是系统原生命令未触发任何 EDR 告警。3. 防御失效根因不是工具没装而是配置没“活”起来事发后我们立刻检查了服务器的防护配置。令人尴尬的是fail2ban、ufw、SSH 密钥登录全部“已启用”但全部形同虚设。问题不出在“有没有”而出在“怎么用”。3.1 fail2ban 的“纸面封禁”规则匹配错位与阈值失焦我们配置的 jail.local 如下[sshd] enabled true filter sshd action ufw[nameSSH, portssh, protocoltcp] logpath /var/log/auth.log maxretry 5 bantime 600 findtime 600表面看很规范但存在三个致命缺陷Filter 规则过于宽泛默认sshdfilter 匹配所有Failed password和Invalid user日志。但攻击者使用的是合法用户名root所以日志是Failed password for root而 filter 默认只捕获Invalid user类型因历史兼容性。实际生效的其实是sshd-ddosfilter但它针对的是连接洪水对密码爆破无效。maxretry 阈值脱离实际场景5 次失败即封禁看似严格。但在测试环境开发同学频繁输错密码尤其切换多个环境时导致大量误封。运维同学为“保障效率”悄悄将maxretry改为 20却忘了同步调整findtime造成findtime600秒内允许 20 次失败——这正好是攻击者脉冲式攻击的完美窗口。action 动作未闭环ufw[nameSSH]仅封禁 22 端口入向但攻击者使用的是出向连接SSH Client 主动连接 C2ufw 对出向默认放行封禁完全无效。我用fail2ban-client status sshd查看实时状态发现Currently banned: 0而fail2ban-client get sshd failregex返回空——规则根本没匹配到日志。这才是真相我们部署了一个功能完整的 fail2ban却从未验证过它是否真的在“看”日志更没验证过它看到后是否真能“动手”。3.2 SSH 配置的“伪安全”密钥登录≠绝对安全服务器/etc/ssh/sshd_config中明确写着PubkeyAuthentication yes PasswordAuthentication no PermitRootLogin no看起来滴水不漏。但问题出在执行层面密钥未强制绑定用户/root/.ssh/authorized_keys中存有 3 个不同开发同学的公钥其中一人离职半年未清理其私钥仍存在于个人笔记本中后证实该笔记本曾中过木马。PasswordAuthentication 实际未关闭systemctl reload sshd后未验证配置生效。sshd -T | grep passwordauthentication返回passwordauthentication yes因为配置文件中存在Include /etc/ssh/sshd_config.d/*.conf而50-custom.conf里写着PasswordAuthentication yes——这是某次 CI/CD 自动化部署时注入的残留配置。PermitRootLogin 语义混淆配置为no但攻击者并未用root登录而是用admin账户登录后执行sudo su -。而admin用户在/etc/sudoers中被赋予了NOPASSWD: ALL权限。这揭示了一个残酷事实安全配置不是写在文件里的声明而是运行时的真实状态。没有自动化校验一切配置都是幻觉。我们后来编写了一个 Ansible Playbook每小时执行sshd -T | grep -E (passwordauthentication|permitrootlogin|pubkeyauthentication)并比对期望值偏差即告警。上线一周就捕获了 7 次因自动化脚本错误导致的配置漂移。3.3 监控告警的“聋哑症”只看指标不读日志我们的 Prometheus Grafana 监控体系非常完善CPU、内存、磁盘、网络流量全部有图。但唯独缺了/var/log/auth.log的解析。当 CPU 告警触发时SRE 同学第一反应是查top和htop看到minerd进程后杀掉就结束。没人去看auth.log里那几百条失败记录——因为告警系统里根本没有这条日志的采集和分析规则。我们紧急补上了 Filebeat 配置用以下 grok pattern 解析 SSH 认证日志%{SYSLOGTIMESTAMP:timestamp} %{HOSTNAME:hostname} sshd(?:\[%{POSINT:pid}\])?: %{DATA:event_type} (?:password for %{DATA:user}|user %{DATA:user} from %{IPORHOST:src_ip} port %{NUMBER:src_port}) %{GREEDYDATA:rest}然后在 Grafana 中建立两个关键看板失败登录热力图按src_ipuser聚合颜色深浅代表 5 分钟内失败次数成功登录异常检测对比历史同期若Accepted password数量突增 300%且来源 IP 不在白名单如公司出口 IP 段立即触发企业微信告警。上线第二天就捕获到另一起来自185.155.242.11的试探性攻击从首次失败到告警推送仅 42 秒。4. 可落地的测试环境口令治理方案从“改密码”到“管生命周期”复盘之后我们没停留在“加强教育”或“重申制度”层面而是构建了一套嵌入研发流程的、自动化的口令治理体系。它不追求理论上的绝对安全而是解决测试环境中最现实的矛盾既要快速交付又要守住底线。4.1 口令生成用“约束式随机”替代“自由发挥”我们废弃了所有人工设定密码的环节全部由 CI/CD 流水线自动生成。关键不是“随机”而是“带约束的随机”长度强制 ≥16 位openssl rand -base64 16 | tr / -_ | tr -d \n字符集排除易混淆项去除0O1lI避免admin01这类弱口令变体结构强制包含四类字符大小写字母 数字 符号但符号仅限-_避免curl命令中需转义的符号注入环境上下文密码末尾追加env_hash如test-jenkins-01的 SHA256 前 4 位确保同一环境所有服务密码不同但可追溯。生成的密码格式示例Xk9vQmRtZjNpVzJxWmFyUHJvZHVjdA_a7f2。它满足所有复杂度要求但又不会因为含或$导致在 shell 脚本中被错误解析。经验很多团队用pwgen或gpg --gen-random但忽略了“可运维性”。密码必须能在 Ansible、Terraform、K8s Secret 中无损传递。我们测试了 12 种生成方式最终选定openssl rand 字符过滤因为它在所有 Linux 发行版中默认存在无需额外安装且输出稳定可控。4.2 口令分发用“一次一密”替代“共享文档”过去密码存在 Confluence 文档里所有人可见。现在改为CI/CD 流水线生成密码后调用 HashiCorp Vault 的kv/v2API以secret/test-env/{service-name}/{env}路径写入开发同学通过vault kv get -fieldpassword secret/test-env/jenkins/test获取需 MFA 认证所有获取操作自动记录审计日志谁、何时、哪个服务密码设置 72 小时自动过期过期后需重新申请。这解决了两个痛点一是杜绝密码明文泄露二是强制“最小权限”——开发同学只能拿到自己负责服务的密码看不到数据库或中间件的凭据。4.3 口令轮换用“事件驱动”替代“定期提醒”我们不再依赖“每 90 天修改一次”的行政命令而是绑定具体事件环境销毁事件Terraformdestroy后自动调用 Vault API 删除对应路径下所有凭据人员变更事件HR 系统同步离职员工 ID 到内部 IAM自动吊销其所有 Vault 读取权限并轮换其接触过的所有服务密码安全扫描事件Trivy 扫描到镜像含弱口令配置如ENV DB_PASSadmin123自动触发流水线生成新密码并更新部署。这套机制上线后测试环境平均口令生命周期从 182 天缩短至 4.7 天而运维同学的工作量反而下降 60%——因为所有操作都由系统自动完成人只负责审批关键操作。4.4 口令审计用“主动探测”替代“被动检查”我们部署了一个轻量级审计 Agent基于 Python Paramiko每天凌晨 2 点执行遍历所有测试服务器 IP 列表从 CMDB 同步尝试用 Top 100 弱口令字典admin123,password,123456等登录 SSH若成功立即发送企业微信告警并自动执行passwd -l {user}锁定账户同时记录server_ip,user,weak_password_used,timestamp到审计数据库。这个 Agent 占用内存 8MBCPU 峰值 3%但效果惊人上线首月就发现 17 台服务器存在弱口令其中 5 台是开发同学为“方便调试”私自开启的密码登录。它不依赖服务器配合不安装任何客户端纯粹从外部模拟攻击者行为是最真实的“红队视角”。5. 最后一点实操心得别信“理论上安全”只信“日志里有证据”这次事件给我最大的教训不是技术细节而是思维方式的转变。以前我总想“这个配置应该能防住”现在我只问“这个配置的日志我能不能在 5 分钟内定位到它生效的证据”比如当你配置PermitRootLogin no后请立刻执行# 1. 确认配置已加载 sshd -T | grep permitrootlogin # 2. 确认当前会话未受影响root 仍可登录 whoami # 输出 root # 3. 新建一个无密钥的 root 会话测试在另一终端 ssh -o PubkeyAuthenticationno rootlocalhost # 应返回 Permission denied (publickey)再比如当你启用 fail2ban 后请手动触发一次失败登录# 用错误密码登录一次 ssh -o ConnectTimeout5 -o NumberOfPasswordPrompts1 fakeuserlocalhost # 然后立刻检查 fail2ban-client status sshd | grep Currently banned # 应显示 1所有安全措施的价值不在于它“写了什么”而在于它“做了什么”更在于你能否“看见它做了什么”。测试环境不是生产环境的简化版它是安全理念的试验田。在这里你可以大胆尝试自动化轮换、强制 MFA、细粒度审计——因为代价是可控的。而每一次“懒得验证配置”的侥幸都在为下一次沦陷埋下伏笔。我在处理完这次事件后把/var/log/auth.log的轮转周期从 7 天改成 30 天并设置了每日归档到 S3。不是为了存档而是为了随时能拉出来指着某一行日志说“看这就是我们防线被突破的精确时刻。” 因为真正的安全始于对失败的坦诚而非对完美的幻想。

相关新闻