记一次服务器被入侵的经历
广州的十一月依然带着些许黏腻的温热。大四的课表已经很空了,室友们大多在忙着找工作或者实习,宿舍里常常只有我一个人。昨晚大概十一点多,我像往常一样坐在电脑前,准备给我的个人博客增加一个小功能。打开终端,习惯性地通过 SSH 连上那台每个月几十块钱的云服务器,打算先看一眼最近的运行日志。
一切看起来都很平静,风扇在底座下发出微弱的嗡嗡声。直到我连上 MongoDB,敲下 show dbs 的那一刻。
终端的黑色背景上,原本应该列出我熟悉的几个数据库名字的地方,赫然出现了一个冗长且刺眼的名称:READ__ME_TO_RECOVER_YOUR_DATA。
我停下了敲击键盘的手,盯着这行字看了几秒钟。没有什么心跳加速的恐慌,也没有电影里那种警报大作的戏剧感,只有一种靴子落地的无奈。这就好像你出门倒垃圾,以为只是去去就回便没有锁门,结果回来发现客厅里坐着个陌生人。
我切进这个数据库,里面只有孤零零的一条记录。内容很简短,大意是我的数据已经被他们下载并加密删除了,如果想要恢复,需要往某个地址汇入 0.01 个比特币。我顺手查了一下昨天的币价,0.01 个 BTC 大概折合人民币几千块。对一个还在广州商学院念书的学生来说,这绝对不是一笔小数目。
是的,我的数据库被勒索了。
冷静下来后,我开始回溯整个事件的起因。作为软件工程专业的学生,排查问题是一种本能。我翻看了服务器的安全日志和 MongoDB 的访问记录,入侵的路径简单得有些可笑,甚至可以说是对我敞开大门的一种嘲讽。
原因归结起来只有致命的三点。首先是 MongoDB 的配置文件里,bindIp 被设置成了 0.0.0.0。这是很多中间件的默认配置,意味着它在监听所有网络接口,直接将自己暴露在了公网之下。其次,我没有为数据库设置任何访问认证。最后,云服务器的安全组规则里,我为了当初本地调试方便,直接放行了 27017 端口,并且一直忘了关。
这就像是一个人在繁华的大街上建了一个透明的玻璃房,里面放着自己的日记本,不仅没有锁门,还挂了个牌子写着“欢迎翻阅”。
攻击者根本不需要什么高深的技术。公网上无时无刻不在运行着大量的自动化扫描脚本,它们像是不知疲倦的幽灵,在 IP 地址段里盲目地游荡。当脚本扫描到我的服务器,发现 27017 端口不仅开着,而且处于毫无防备的裸奔状态时,剩下的事情就顺理成章了。连上数据库,执行 drop 命令清空数据,然后写入那条勒索信息。整个过程可能只需要几毫秒,甚至没有任何人类参与。
看着那些日志,我心里其实有些五味杂陈。大学这几年,我一直是个标准意义上的好学生。在这个双非民办三本的学校里,我保持着 4.02 的绩点,拿过两次国家奖学金。在课堂上,我能把操作系统、计算机网络的理论倒背如流,能写出时间复杂度最优的算法。但在真实的网络世界里,这些光环毫无意义。公网是一片黑暗森林,那些自动化的扫描机器人不会在乎你是名校的高材生,还是某个三本院校的普通学生。它们只看端口,只看漏洞。书本上的满分,和工程实践中的及格线之间,原来隔着这么真实的一道鸿沟。
好在,我并没有真的失去这些数据。
这得益于我之前养成的一个微小习惯。几个月前折腾服务器的时候,我随手写了一个 Shell 脚本,通过 crontab 每天凌晨两点把重要的数据打包成压缩文件,存放在服务器的一个隐藏目录里。当初只是觉得好玩,没想到现在成了救命稻草。
我没有理会那条勒索信息,直接把它 drop 掉了。然后找到了昨晚的备份文件。解压,执行恢复命令:
mongorestore --db mx-space ./backup/mx-space/
终端屏幕上开始快速滚动起恢复的日志。看着那些熟悉的数据集合一个个被重新写入,心情就像是看着枯萎的植物重新吸水挺立起来。大概过了两分钟,命令执行完毕。我重新连上数据库,检查了文章、评论和配置信息,一切都在,连一个标点符号都没少。
数据虽然找回来了,但这扇破损的门必须马上修补。亡羊补牢的过程其实并不复杂,但每敲下一行命令,我都觉得是在给过去的傲慢和懒惰还债。
第一步是修改监听地址。我打开了 /etc/mongod.conf 文件,将 net 配置块里的 bindIp 从 0.0.0.0 改回了 127.0.0.1。这意味着从今以后,这个数据库只接受来自服务器内部的连接。
# /etc/mongod.conf
net:
bindIp: 127.0.0.1
第二步是启用认证。我重启了 MongoDB 服务,进入 admin 数据库,给自己创建了一个具有 root 权限的管理员账号。设置了一个由大小写字母、数字和特殊符号组成的随机强密码。
做完这些,我又开启了配置里的 authorization: enabled 选项。从现在起,任何想要读取或修改数据的操作,都必须经过凭证的检验。
最后一步是防火墙层面的加固。虽然已经限制了监听 IP,但我还是决定在系统的 iptables 里加上两道锁。我写了两条规则,明确规定 27017 端口只允许本地环回地址访问,丢弃所有其他来源的 TCP 请求。
做完这一切,我用自己的笔记本尝试直连服务器的 27017 端口。几秒钟后,终端返回了 Connection timed out。看着这行红色的超时报错,我反而感到一种前所未有的踏实。
夜已经很深了,我靠在椅背上,喝了一口已经变凉的水。这次事件其实没有造成什么实质性的损失,前后不过花了我半个小时的时间。但它留给我的教训,远比考卷上扣掉几分要深刻得多。
我们做开发的,总是习惯性地追求速度。为了让项目赶紧跑起来,为了早点看到页面上的效果,我们经常会选择一条最省事的路径。默认配置就是这种路径的代表。它假定你处于一个绝对安全的环境中,于是为了便捷牺牲了所有的防御。但现实是,永远不要相信默认配置。在把任何服务暴露到公网之前,审视它的安全边界是工程师的必修课。
此外,定期备份真的是这个脆弱数字世界里的唯一真理。硬件会损坏,系统会崩溃,黑客会入侵,甚至你自己也会因为疲劳敲下 rm -rf。在所有这些不确定性中,一份安安静静躺在硬盘角落里的冷备份,是你面对灾难时底气的唯一来源。
安全意识这个东西,很像我们生活中的保险。不出事的时候,你觉得那些繁琐的配置、复杂的密码、定期的备份都是在浪费时间,显得多余且累赘。可一旦厄运降临,你才会明白那道防线有多么重要。
合上电脑前,我又看了一眼窗外。广州的夜景依旧繁华,无数的数据正在看不见的光缆中穿梭。在这个庞大的网络里,我的一台小服务器微不足道。但我知道,经过今晚,它变得比昨天更坚固了一点。而我也是。