漏洞/实验概述

这是一次真实的服务器加固记录,不是一份“照着敲就完事”的教程。起点是一台香港免备案 VPS:原 IP 被墙,商家把 IP 换成了 206.187.208.43,系统是 Ubuntu 22.04.4 LTS,内核 5.15.0-30-generic,我以 root 身份登录。

换 IP 只解决了“能不能连上”,没有解决“谁能连上”。真正让我下决心改认证方式的是登录日志里的一串记录:来源 193.32.162.15 对 root 有 500 余次失败尝试,目标集中在 /etc/passwd。这不是有人在针对我,而是公网上的自动化扫描已经把我的 SSH 端口当成了日常目标。

项目内容
服务器香港免备案 VPS,2 核 2G
系统Ubuntu 22.04.4 LTS,内核 5.15.0-30-generic
公网 IP206.187.208.43
登录方式(加固前)root + 密码
主要风险公网自动化爆破、密码猜测、弱口令
加固结果关闭密码与键盘交互认证,仅保留公钥认证

我当时的真实状态是“知道要关密码登录,但说不清为什么”。所以这篇文章按“我在做什么、它为什么有用、不做会怎样”来写,把中间的错误也保留下来。

漏洞成因 / 原理

为什么密码登录会成为突破口

SSH 暴露在公网时,任何人扫描到 22 端口都可以尝试登录。密码认证的弱点在于:

  • 验证成本低:服务器对每次尝试的计算开销很小,攻击者可以高频尝试;
  • 密码空间有限:弱口令、默认口令、复用口令让猜测空间进一步缩小;
  • 默认没有封禁:没有 fail2ban 之类的限制时,失败多少次都可以继续;
  • 成功后权限大:直接以 root 登录,攻击者不需要再做提权。

日志里 500 余次失败尝试说明的正是这一点:对方在批量试密码,而不是在利用某个漏洞。只要密码足够弱,或者哪天被泄露,失败次数就会变成成功次数。

公钥认证到底在做什么

公钥认证使用一对数学上关联的密钥:

  • 私钥只保存在我的 Windows 电脑上(C:\Users\Hawk\.ssh\id_ed25519),从不发送给服务器;
  • 公钥可以公开,被放进服务器的 /root/.ssh/authorized_keys。

登录时并不是“把私钥发过去让服务器比对”,而是:

  1. 客户端告诉服务器“我想用这把公钥登录”;
  2. 服务器生成一段随机挑战,用公钥加密后发回;
  3. 客户端用私钥完成签名/解密运算,把结果送回;
  4. 服务器用公钥验证结果是否成立。

私钥始终没有离开本地,服务器只验证“持有私钥的人才能算出的答案”。想冒充登录,就必须拿到私钥本身;靠猜出对应的私钥在计算上不可行。这就是它比密码强的根本原因。

root 与 PermitRootLogin

OpenSSH 的 PermitRootLogin 默认值是 prohibit-password:允许 root 用公钥登录,但禁止用密码和键盘交互方式登录。也就是说,即使不改这一项,关掉密码认证后 root 也已经不能靠密码进来了。

我一开始以为“必须把 root 登录整个禁掉”,后来才理解:prohibit-password 已经挡住了密码通道,真正要确认的是公钥通道是否可靠。对单人维护的服务器,保留 root 公钥登录是可用性与安全性的折中。

攻击链路分析

以下只描述这类事件的通用链路,不涉及任何具体工具或参数。

  1. 资产发现:自动化扫描器在公网批量探测开放的 SSH 端口,记录 IP、端口和 banner;
  2. 身份枚举:尝试常见用户名,root 是最常见的目标之一;
  3. 口令猜测:用字典、常见弱口令或泄露口令高频尝试,失败就换下一个;
  4. 登录成功后的动作:一旦命中,攻击者通常先确认权限与系统信息,再考虑写入公钥、安装持久化程序或横向探测;
  5. 长期驻留:在自己的公钥写进 authorized_keys 后,即使管理员改了密码,攻击者仍能凭公钥回来。

这条链路里没有“高深漏洞”,全部是配置与习惯问题。对应的防线也不是某个神奇工具,而是把认证方式换成不可猜测的公钥,并把可尝试的通道逐个关掉。

攻击痕迹与日志特征

我确认风险时看的是失败登录记录。lastb 读取的是失败登录日志(/var/log/btmp),它和记录成功登录的 last 是两份不同的数据:

# 查看最近的失败登录记录
lastb | head -n 20

输出里能直接看到来源 IP、尝试的用户名和时间。本次的特征很明确:

观察点本次记录说明
来源 IP193.32.162.15单一来源高频尝试
目标账户root攻击者优先尝试最高权限账户
失败次数500 余次典型的自动化爆破,不是人工操作
目标路径/etc/passwd试探系统账户信息,属于登录后的侦察目标

也可以用 journalctl 查 sshd 的认证日志:

# 只看 sshd 的失败认证记录
journalctl -u ssh --no-pager | grep -i "failed"

journalctl -u ssh 的作用是筛选 ssh 服务单元的日志;--no-pager 避免输出被分页器截断。日志里的失败记录会随尝试次数持续增长,这正是判断“是否正在被爆破”的直接证据。

需要说明的是:看到失败记录不等于已经被攻破。它说明的是“门正在被反复敲”,而门锁是否可靠,取决于认证方式和密码强度。

缓解、检测与防御方案

第一步:改密码,但不要以为改完就安全

我先改了 root 密码。改密码能提高单次猜测的成本,但解决不了两个问题:一是只要密码认证还开着,爆破就会继续;二是密码可能通过其他渠道泄露。改密码是必要的,但它只是临时降低风险,不是认证方式的升级。

真正要做的,是把“猜密码”这条路彻底关掉,换成公钥认证。

第二步:生成本地密钥对

我在 Windows 本地生成密钥,使用 ed25519 算法:

ssh-keygen -t ed25519 -C "hokachi2023@163.com"
  • -t ed25519 指定密钥算法,ed25519 比传统的 RSA 更短、更快,安全性也足够;
  • -C 是给公钥加的注释,方便以后在服务器上认出这把钥匙属于谁;
  • 命令会生成两个文件:私钥 id_ed25519 和公钥 id_ed25519.pub。

私钥必须留在本地,不能上传、不能贴到聊天窗口、不能提交到 Git。公钥才可以放到服务器上。

第三步:踩坑——终端粘贴把公钥折断了

我一开始的做法是打开公钥文件,复制内容,粘贴到服务器的 authorized_keys。结果 authorized_keys 出现了 3 行,但只有 1 把钥匙是有效的。

原因是终端或 SSH 会话会对过长的行做视觉折行,复制时把一整行公钥拆成了多行。公钥是一段连续的 base64 文本,中间插入换行后就不再是合法公钥,sshd 会把它当成几条格式错误的记录。

正确做法是用 scp 把 .pub 文件原样传到服务器,再在服务器上追加,避免经过终端剪贴板:

# 在本地把公钥文件传到服务器
scp C:\Users\Hawk\.ssh\id_ed25519.pub root@206.187.208.43:/tmp/t.pub

scp 走的是 SSH 通道,按字节传输文件,不会因为终端宽度而改变内容。这一步看起来只是“换个传文件方式”,实际上是在消除“文本被中间环节改写”的风险。

第四步:踩坑——Windows 的 \r 让公钥格式错误

文件传上去后,sshd 仍然认为公钥行格式错误。原因是 Windows 文本使用 CRLF 换行,行尾带有 \r;而 sshd 按 Unix 的 LF 解析,\r 会被当成公钥内容的一部分。

处理方式是先把行尾的 \r 去掉:

sed -i 's/\r$//' /tmp/t.pub

sed -i 表示直接修改文件;s/\r$// 表示把每行末尾的 \r 替换为空。改完后可以用 cat -A 之类的命令确认行尾不再有 ^M。

这个坑的教训是:跨平台传文本文件时,行尾格式也是内容的一部分。

第五步:踩坑——按行号删掉了正确的公钥

清理测试用的临时钥匙时,我用了:

sed -i '$ d' /root/.ssh/authorized_keys

$ d 的意思是删除最后一行。我的假设是“临时钥匙肯定在最后一行”,但实际上最后一行恰好是正确的公钥。结果就是文件里还有内容,登录却一直失败。

问题出在“按位置删除”依赖一个并不可靠的假设。正确做法是按内容匹配删除,只删掉明确不要的那一行:

# 按公钥内容或注释匹配删除,而不是按行号
sed -i '/hokachi2023@163.com/d' /root/.ssh/authorized_keys

更稳妥的方式是先 cat 看清内容、确认要删的那一行,再执行删除;删除后重新检查文件行数与指纹。

第六步:用指纹判断公钥是否配对

判断服务器上的公钥和本地私钥是否配对,唯一可信的依据是指纹比对,而不是“文件里有几行”或“看起来像不像”。

# 查看本地公钥的指纹
ssh-keygen -lf C:\Users\Hawk\.ssh\id_ed25519.pub

# 查看服务器 authorized_keys 中每把钥匙的指纹
ssh-keygen -lf /root/.ssh/authorized_keys

ssh-keygen -lf 会对每个公钥计算 SHA256 指纹并打印。本次本地公钥的指纹是:

SHA256:hdIwiAzL5UOPrsmVstoWHjEJUZY74CJacm+LKn9a4w0  hokachi2023@163.com (ED25519)

服务器 authorized_keys 最终只有 1 行,指纹与上面完全一致。到这一步,我才敢确认“这把钥匙真的对上了”。

第七步:改 sshd_config,关掉密码通道

需要改的有三处:

配置项值作用
PasswordAuthenticationno关闭密码认证
KbdInteractiveAuthenticationno关闭键盘交互认证,堵住另一条“类密码”通道
PubkeyAuthenticationyes明确启用公钥认证

我是在 sshd_config 第 23 行就地修改 PasswordAuthentication no,KbdInteractiveAuthentication no 采用追加方式,PubkeyAuthentication yes 则放在文件最前面。

这里有一个容易忽略的规则:sshd_config 中同一个配置项出现多次时,首次出现的值生效。所以如果文件后面还有旧的 PasswordAuthentication yes,只改后面那行是没有用的;把 PubkeyAuthentication yes 放在最前面,也是为了避免被后面的旧值覆盖。

PermitRootLogin 保持默认的 prohibit-password,含义是 root 可以凭公钥登录,但不能凭密码或键盘交互登录。

第八步:先验语法,再重启,并留好退路

改完配置不能直接假设它是对的,先做语法检查:

sshd -t

sshd -t 只检查配置文件的语法和参数是否合法,不会加载新配置。所以即使它通过了,当前正在运行的 sshd 仍然是旧配置。

必须重启服务才会生效:

systemctl restart ssh
systemctl status ssh

systemctl restart ssh 会停止并重新启动 ssh 服务;systemctl status ssh 用来确认服务处于 active (running)。

重启前我做了一件很重要的事:不关闭当前已经登录的会话。如果新配置有问题,新连接会全部失败,而这个旧会话就是唯一的退路,可以用来改回配置。这是这次加固里最值得记住的操作习惯。

第九步:双向验证,两条都成立才算完成

只看“我还能登进去”是不完整的,因为旧会话可能还在用旧配置。我做了两个方向的验证:

# 验证一:强制走密码认证,应该被拒绝
ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no root@206.187.208.43

结果是:

Permission denied (publickey).

PreferredAuthentications=password 强制客户端只尝试密码;PubkeyAuthentication=no 禁用公钥。服务器返回 Permission denied (publickey),说明密码通道确实已经关闭,而且服务器只接受公钥。

# 验证二:指定私钥登录,应该直接进入
ssh -i C:\Users\Hawk\.ssh\id_ed25519 root@206.187.208.43

结果是直接登录成功,没有任何密码提示。

两条同时成立,才能说明:密码进不来、公钥进得来。只做其中一条,都可能得到错误的安全感。

第十步:收尾与备份

测试用的临时密钥 /tmp/t 和 /tmp/t.pub 已经从 authorized_keys 移除并删除。这里要注意,删除文件不等于删除授权:如果只 rm 了 /tmp/t.pub,但 authorized_keys 里还留着对应公钥,那把钥匙依然能登录。必须两边都清理。

最后一步是私钥备份。密码通道关闭后,私钥就是唯一的登录凭据;一旦本地磁盘损坏或误删,就只能通过商家面板的 VNC 救援控制台进入服务器。本次私钥已备份到 U 盘。

日常检测建议

加固完成后仍然需要持续观察:

  • 定期查看 lastb 与 journalctl -u ssh,确认失败尝试的来源和频率;
  • 关注 authorized_keys 是否出现非本人添加的公钥;
  • 对 SSH 登录失败做阈值告警,后续可结合 fail2ban 自动封禁;
  • 保留一个可用的救援通道(商家 VNC),并定期确认它可用。

实验声明:本文章所有相关实验,均在本人拥有完全授权的自建隔离靶场中完成,仅用于网络安全技术学习、防御方案研究。未经授权对任何信息系统进行测试、攻击属于违法行为。