我们的游戏服务器被黑了

/ dousha

虽然是定番,但还是要强调:不该暴露的服务不暴露,不该做出的假设不做出。先把安全做好,再上业务逻辑。

2025-05-17 22 时左右,我们的团队发现游戏后台在报数据库写入失败的错误。我看了之后发现一个恐怖的事情:

无法写入数据库 prod: 数据库不存在

当我登上 MySQL 控制台查看的时候:

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| RECOVER_YOUR_DATA  |
| information_schema |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
5 rows in set (0.00 sec)

我们的数据库被骇客勒索了

这是一台新的裸金服务器,我们才刚将生产环境迁移过来不到十小时,许多安全加固都还没有实装。没想到这么快就吃了开门红。

从攻击手法上,不难看出这是自动化脚本攻击。互联网每时每刻都有闲着蛋疼不断地扫各种漏洞、爆破弱口令的脚本在跑。如果配置错误,那么你离中招也许只有五分钟的距离。

有意思的是:这个傻逼居然开口要价 0.0082 BTC, 折合超过十万美刀,这也太看得起我们了。俺们这种为爱发电的是真掏不起。

虽然我们配置了异地冷备,但冷备毕竟是冷备——它每 24 小时产生一次。所以我们丢失了今天产生的数据。得,那只能认栽。该恢复恢复,该给玩家赔偿赔偿。

根本原因:数据库弱口令

最开始这个服务器配置的时候,开发人员为了省事,不仅使用了 root 敢死队,还配置了一个弱口令。就是 6 个数字的那种弱口令。后来由于使用数据库的程序越来越多,等我开始执行安全加固的时候已经积重难返,所以敢死队和弱口令就一直延续地用了下去。

这一遭之后,我们换了强口令,然后给各个程序重新配置了数据库访问凭证。人教人教不会,事教人一遍通,大概就是这么个道理。

直接原因:防火墙未到位

这里有三个层面的防火墙均未到位。三层防火墙中如果有任何一个到位,那么它就可以抵御这样的防御。

第一层防火墙是安全组。在之前,我们的服务器是在一层 NAT 之后的。如果要放行端口,则必须通过厂商的控制面板配置映射来放行。这实际上是一层防火墙——虽然难用了一点,但它可以说是抵御绝大部分自动攻击的最外围的防线。切到裸金服务器之后,我们实际上就没有了厂商安全组设置了。

第二层防火墙是系统防火墙。新系统并没有默认启用防火墙,而我也没有去主动配置防火墙。这就导致了当自动攻击脚本来扫我们的服务器的时候,服务器处在一个大门敞开的状态。

第三层防火墙是应用级访问控制。root 账号本来应该是只能通过本地访问的,但是由于我们的程序运行在主机内,但数据库用了 Docker, 所以 root 账号的默认策略变成了允许任何 IP 地址登录(毕竟从容器里来看,从主机中的登录就是来自于一个外部网络)。这使得自动攻击脚本得以对我们的数据库执行爆破攻击。

除此之外,我们还可以配置的:

总之,这个锅确实在我。

ufw 和 Docker

即使配置了防火墙,也万不可掉以轻心。尤其是如果你的系统使用的是 ufw 的情况下,错误的 Docker 配置仍然会将你的服务暴露于风险之中。

考虑这样一个配置:

services:
  db:
    image: postgres:16-alpine
    ports:
      - 5432:5432
    # 余下从略

如果你使用 ufw, 那么这么写会直接将端口 5432 暴露于公网访问之中,即使你从未主动开放过这个端口:

# ufw status

Status: active

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW       Anywhere
80/tcp                     ALLOW       Anywhere
22/tcp (v6)                ALLOW       Anywhere
80/tcp (v6)                ALLOW       Anywhere

# _

正确的写法是:

services:
  db:
    image: postgres:16-alpine
    ports:
      - "127.0.0.1:5432:5432"
    # 余下从略

这样可以保证它只接受来自本地的连接。

我实在是懒得上 ufw-docker 之类的东西了。最理想的情况是 Docker 完全不需要暴露端口,而是将所有的程序都塞到 Docker 里,然后直接通过 Docker 的内部网络实现通联:

# in db/docker-compose.yml
services:
  db:
    image: postgres:16-alpine
    networks:
      - db
    # 余下从略
networks:
  db:
    name: db
    external: true

# in app/docker-compose.yml
services:
  app:
    image: prod.image.my.company.arpa/game-group/the-game:1
    ports:
      - 23333:23333 # 这个是刻意暴露给外部的端口
    networks:
      - app
      - db
    # 余下从略
networks:
  db:
    name: db
    external: true
  app:
    name: app
    external: true

这样即使我家大门常打开,那最多就是进一个空无一物的院子里看看。各个房间的门是直接焊死的。

正在加载评论……

发表评论

您的评论将由管理员审核后方可公开显示。

Your comments will be submitted to a human moderator and will only be shown publicly after approval.