一次 Minecraft 服务器被破坏的复盘:攻击者根本不需要密码
10 月 4 日凌晨,有人冒用一位玩家的身份进了我的服务器,用飞行外挂、TNT 和岩浆炸掉了他的基地,掏空了箱子,还留下了署名。今天(10 月 8 日)我把整件事查清楚,修好了漏洞,并把世界回档到了事发前。
这篇文章记录调查过程和最后的结论。结论有点出乎意料:攻击者从来没有拿到过密码,也不需要密码。
文中玩家名都做了处理,受害者称为"玩家 A"。
服务器背景
- Minecraft 1.21.11 Fabric,离线模式(
online-mode=false),用 EasyAuth 做登录认证:每个玩家第一次进服时/register设密码,之后用/login登录。 - 服务器在家里,通过 frp 映射到腾讯云:玩家连云主机上的 frps,流量经隧道转到家里的 frpc,再交给 MC。
发生了什么
| 时间(10-04) | 事件 |
|---|---|
| 02:47 | 玩家 A 的账号登录。没有提示输入密码,进服后立刻可以移动、破坏方块 |
| 02:49 | 被服务端以 floating too long 踢出(没开允许飞行),几秒后重连,落点在玩家 A 基地的正下方 |
| 02:52 | 杀死了另一位在线玩家 |
| 02:57 | 在聊天里发了第一次署名:Rekt by The Fifth Column ... |
| 03:04 | 重连到出生点附近继续破坏 |
| 03:27 | 第三次署名后离开,前后共 40 分钟 |
| 05:05 | 每天的定时冷备份照常执行,这份备份已经包含了破坏 |
| 19:28 | 玩家 A 上线:"我家全炸了""箱子全拆了" |
The Fifth Column 是一个专门扫描公网 Minecraft 服务器、进去搞破坏的国际团体,手法很固定:扫描、进服、TNT 加岩浆、掏空容器、留下 Discord 署名和"战绩编号"。
调查:这真的是玩家 A 本人吗?
第一反应是玩家 A 的密码泄露了。但查下来有三条证据说明不是他本人:
1. 客户端特征对不上。 玩家 A 装了 Carpet 客户端,每次进服日志里都有一条 Carpet 握手记录。攻击发生前后,他本人的十几次登录都有这条记录,只有凌晨那 3 次登录一次都没有。
2. 行为特征。 那 3 次会话里密集出现 moved too quickly,还被 floating too long 踢过,是典型的飞行外挂特征。
3. 动机不成立。 被炸的就是玩家 A 自己的基地和箱子。
调查:攻击者是怎么登录的?
既然不是本人,那攻击者是怎么通过 EasyAuth 的?我先排除了几种可能:
- 暴力破解:EasyAuth 配置了
max-login-tries=3,错 3 次就踢出。这几天的日志里一次Too many login attempts都没有,数据库里这个账号的失败次数也是 0。 - 密码数据库泄露:密码哈希只存在服务器本地的数据库里,攻击者只有游戏客户端,没有主机权限。
- 改过密码:EasyAuth 改密码需要旧密码,而且玩家 A 当天中午还能用原密码正常登录。
然后我找到了一个关键的对照:
9 月 28 日,同一个账号,必须输密码:
[06:57:46] 玩家A[/127.0.0.1:39322] logged in with entity id 751
[06:58:31] 玩家A lost connection: Too many login attempts. Please wait a few minutes and try again.
45 秒里输错 3 次被踢。这说明正常情况下,这个账号进服是要输密码的。
10 月 4 日凌晨,同一个账号,没有任何密码提示:
[02:47:39] 玩家A[/127.0.0.1:58722] logged in with entity id 649758 at (...)
[02:49:01] 玩家A moved too quickly! ...
注意两行里的来源 IP:都是 127.0.0.1。
根本原因:三个条件叠在一起
① server.properties: online-mode=false → 任何人都可以用任意名字连进来
② frp 没有传递真实 IP → 所有玩家的来源 IP 都是 127.0.0.1
③ EasyAuth: session-timeout=86400 → 同一 IP 24 小时内免密码自动登录
EasyAuth 的会话功能是这样定义的:玩家进服时,比较这次的 IP 和上次登录的 IP,如果相同并且会话没过期,就自动登录。
本意是方便玩家短时间内重连不用再输密码。但在 frp 后面,所有人的 IP 都是 127.0.0.1,"IP 相同"这个条件永远成立。于是第 ③ 条实际上变成了:任何玩家登录后的 24 小时内,他的身份对全世界开放,谁用他的名字连进来都会被直接放行。
玩家 A 在 10-03 19:11 下线,会话有效期到 10-04 19:11,攻击者正好在这个窗口里进来。前一天晚上还有一个新注册的账号进来转了 9 分钟、说了句"Hello?"就走了,现在看很可能是在踩点。
这个漏洞对数据库里全部 133 个注册账号都成立,包括 OP 账号。这次没被拿到 OP 权限,纯属运气。
这不是谁的密码被盗了,而是认证机制在这个网络架构下整体失效了。
修复
今天做的事情,按顺序:
1. 关掉会话自动登录(根因修复)
# config/EasyAuth/main.conf
- session-timeout=86400
+ session-timeout=-1
-1 是官方定义的"完全关闭会话",之后每次进服都必须 /login。代价是玩家每次都要输密码,不过 EasyAuth 支持 /l 这个简写。
2. 打开 EasyAuth 的登录和注册日志
# config/EasyAuth/extended.conf
- log-player-login=false
+ log-player-login=true
- log-player-registration=false
+ log-player-registration=true
这次调查最大的遗憾是 EasyAuth 默认什么都不记,我只能通过"9 月那次被踢、这次没被踢"的行为差异来推断,拿不到一行"该玩家通过会话自动登录"的直接记录。证据链是完整的,但本质上还是推断。
3. 开启白名单
扫描器早就收录了这个服务器。数据库里有 82 个格式相同的批量账号,还有二十多个各种扫描器的账号。开启白名单后,只有名单上的玩家能进服,扫描器连注册都做不到。
4. SSH 关闭密码登录
顺着查下去,发现服务器的 SSH 也通过 frp 暴露在公网上,一直在被暴力破解,10 月 4 日一天就有 3.5 万次尝试。同样因为 frp,sshd 看到的来源全是 127.0.0.1,fail2ban 之类按 IP 封禁的工具全部失效。所以直接关掉了密码登录,只允许公钥:
# /etc/ssh/sshd_config.d/00-hardening.conf
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin prohibit-password
文件名用 00- 开头是有讲究的:sshd 对每个配置项取最先出现的值,必须排在系统自带的 50-cloud-init.conf 前面才能生效。改完之后,Failed password 的记录立刻变成了 0。
5. 最小权限
- frpc 原来以 root 运行,实际上它只需要往外拨号、连本机端口,就改成了专用的系统用户;
- 运行 MC 的账号在
lxd组里,而lxd组的成员能把宿主机的文件系统挂进容器,等于有了不需要密码的 root。机器上根本没用 LXD,就把账号移出了这个组。万一某个 Mod 有远程代码执行漏洞,攻击者至少不能顺手拿到 root。
回档:踩到了备份脚本的坑
唯一干净的回档点是 10-03 05:06 的冷备份,因为 10-04 05:05 那份已经包含了破坏。回档会丢掉这之后的进度,好在中间只有两天的游玩,而且 10-06、10-07 没人上线。
回档前,脚本会先给当前状态打一个完整备份,所以这次回档是可以撤销的。执行过程中遇到了两个问题:
1. 回档把加固配置也一起回滚了。 冷备份里包含 config/EasyAuth/,解压后 session-timeout 等几项又变回了事发时的危险值。好在事先预料到了这一点,准备了重新应用加固的脚本,回档后马上执行了一遍。回档之后一定要检查安全配置有没有被一起覆盖掉。
2. 回档后服务器起不来。 恢复脚本解压后会删掉 .fabric 目录,但备份里根本没包含它。缺了这个目录,Fabric 的启动器会退化成安装模式,尝试从网上重新下载原版服务端,结果下载超时,Java 进程退出。更糟的是,启动脚本只是 sleep 3 之后检查进程在不在,这时候下载还没超时,进程还活着,于是报告"启动成功"。手动在前台重跑了一次启动器,.fabric 重建成功,服务器才正常启动。
还有一个意外的好处:tar -xzf 是覆盖式解压,不是同步,备份里没有的文件会保留下来。所以 10-04 之后新探索出来的区块、新玩家的角色数据都还在,比预想的损失小。只是那位新玩家的 EasyAuth 账号随数据库一起回到了注册之前,需要重新 /register 一次。离线模式下 UUID 由名字决定,重新注册后会自动关联回原来的角色数据,背包和进度都不会丢。
还没解决的
- 攻击者的真实 IP。 MC 和 EasyAuth 记录的全是
127.0.0.1,真实 IP 只可能在云主机的 frps 日志里。日志会轮转,要尽快去查。 - 让服务器重新看到真实 IP。 现在关掉会话只是绕开了问题。根治需要 frp 开启 PROXY protocol,服务端再装一个能解析它的 Mod,这样封 IP、按 IP 限流才重新有意义。
- 没有方块日志。 服务器没装 Ledger 这类记录方块改动的 Mod,所以只知道"有人进来了",不知道"他改了哪些方块",没法精确回滚,只能整体回档。
- 发现太晚。 破坏发生在凌晨 3 点,16 个小时后才被玩家发现,期间没有任何告警。
后记(10-09 补记)
第二天就把上面几件事处理了:
- 在云主机的 frps 日志里找到了攻击者的真实 IP。每次冒名登录前两三秒,frps 都正好收到一条来自同一个 IP 的连接,这个 IP 只在作案的 20 分钟里出现过。前一晚踩点的账号也对上了另一个 IP。两个 IP 都封了。
- 顺带发现云主机本身还允许 root 用密码登录,5 周里被尝试了 4 万多次,也一并关掉了。这台机器控制着 frps,一旦被攻破,所有入口都会被接管。
- frpc 开启了 PROXY protocol,服务端装了
proxy-protocol-support,只信任 frpc 专用的源地址。从此每次登录都能看到玩家的真实 IP。 - 告警和实时监控做进了后来的运维平台 mc-ops:凌晨登录、账号出现在新网段、外挂特征、冒用玩家名进服等规则,都是从这次事件里总结出来的。测试里专门有一条要求:用这次事件的原始日志回放,必须能触发告警。
总结
回头看,单独看每一条配置都很合理:离线模式配合登录插件是常见做法,frp 是常见的内网穿透方案,会话自动登录也是一个方便玩家的功能。但三者叠在一起,就产生了一个谁都没想到的漏洞。
几点教训:
- 基于 IP 的安全机制,在反向代理后面要格外小心。 只要服务端看到的 IP 不是真实 IP,所有"按 IP 判断"的功能都可能失效,或者变成反效果。
- 日志要提前开。 出事以后才发现关键环节什么都没记,只能靠推断。
- 备份要能真正恢复。 这次的回档暴露了备份范围不完整、启动判定不可靠、恢复会覆盖安全配置三个问题,都是平时没演练过才没发现的。
- 要有告警。 凌晨 3 点的破坏,16 个小时后才被发现。
评论