我被勒索了

重要提示:

背景:

我被勒索了,而且是最常见的比特币勒索。

事情是这样的,我有一台服务器运行着DNF服务,这个服务可以让我和我的朋友们在我的服务器上一起畅游马拉德大陆。

有一点很奇葩,就是连接DNF服务器数据库的MySQL,先不说默认端口3306,账号密码不是root+root,就是game+123456

不要说为什么不改一个复杂一点的密码,不是因为我懒。这可不是改密码这么简单,密码改完之后,很多地方的源码都需要修改,工作量太惊人。

秉着又不是不能玩的心态,马拉德勇士也正常征战了好几天呢。直到有一天:

我一开始以为是账号密码输错了,输了好几遍,发现还是不对,于是去检查数据库,发现了一行小字:

All your data is backed up. You must pay 0.0111 BTC to BitcoinAccountXXXXXXXXXXXXXXXXXX In 48 hours, your data will be publicly disclosed and deleted. (more information: go to http://xxxxxxxxx)

For users in China, Bitcoin can be purchased with Alipay from: CoinCola: https://www.coincola.com/?lang=zh-HK BitValve: https://www.bitvalve.com/buy-bitcoin/alipay

0.0111比特币大概是五千块人民币,为了方便我支付,贴心的老外还为中国用户提供了Alipay的服务!

教训

这次黑客事件给了我2个教训:

  1. 数据安全不可忽视!
  2. 数据备份非常重要!

关键词

数据库被黑的原因分析

  • 首先,MySQL使用默认端口3306不可取,Nmap一扫一个准。
  • 数据库的账户密码root+root不可取,但是这个目前无法规避,需要一个强而有力的workaround。
  • MySQL的数据库公网暴露问题,规避公网暴露,如果一定要暴露的话,需要添加IP白名单。
  • MySQL没有设置IP白名单,这样才让那些老外有机可乘,得改。
  • 数据库没有备份机制。

通过和ChatGPT的轮询沟通,一套完美的解决方案应运而生。

端口问题

规避使用MySQL默认端口3306,这个解决起来很简单,修改MySQL的配置文件:

shell
sudo nano /etc/my.cnf

找到[mysqld]部分,修改port=33066

shell
[mysqld]
port = 33066

为了加固端口访问,我们会再通过OpenVPN创建一条连接majun.funDNF服务器(10.8.0.11)的虚拟专线,然后转发一个高段的随机端口给mysql。例如:

shell
sudo iptables -t nat -A PREROUTING -p tcp -i enp0 --dport 33066 -j DNAT --to-destination 10.8.0.11:33066

sudo iptables -A FORWARD -p tcp -d 10.8.0.11 --dport 33066 -m state --state NEW,ESTABLISHED,RELATED -j ACCEPT

这可太安全了!

MySQL公网暴露+IP限制问题

如之前分析的,简单账号密码root+root无法规避,需要一个强有力的workaround。

公网暴露

我一开始是计划通过数据库的存储过程来实现的,但是GPT告诉我说通常的做法是通过一个web service提供的API来发起SQL请求。

这个方法可太好了呀,不仅仅可以避免数据库暴露问题,而且由于API的封装,用户在前端根本就不知道后端的执行情况。

基于之前的Express项目经验:Express从0到1,我们写一个简单的Express服务就可以了。

不仅可以解决公网暴露的问题,还可以添加一个IP限制的功能。

IP限制

MySQL数据库可以通过网络访问控制列表(ACL)来限制IP访问,我原本计划限制所有的国外IP,让那些老外访问不了我。

但是在获取中国GEO IP的数据上遇到了很多问题,经过我的后台工程师Bruce的点拨,为啥不直接添加一个白名单,按需访问,按需添加。

迎刃而解。

Express核心代码展示:

  • 前端
javascript
<script>
        async function fetchAndDisplay() {
            const iporigin = document.getElementById('ipInput').value;
            //验证用户输入是否是IP地址
            const ip = iporigin.replace(/\s/g, '');
            const ipPattern = /^(\d{1,3}\.){3}\d{1,3}$/;
            if (ipPattern.test(ip)) {
                //alert('IP 地址格式正确');
                
            console.log(JSON.stringify({ ip: ip }));
            const apiUrl = 'http://localhost:32068/updateSQL'; // 后台 API 地址

            try {
                const response = await fetch(apiUrl, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: JSON.stringify({ ip: ip }),
                });

                const data = await response.json();

                // 将返回的数据展示在 iframe 中
                const ipFrame = document.getElementById('ipFrame');
                const ipDocument = ipFrame.contentWindow.document;
                ipDocument.body.innerHTML = `<p style="font-size:20px;color:red;text-align:center">${JSON.stringify(data, null, 2)}</p>`;
            } catch (error) {
                console.error('Error:', error);
                const ipFrame = document.getElementById('ipFrame');
                ipFrame.contentWindow.document.body.innerHTML = '<p style="font-size:30px;color:red;text-align:center">赛利亚现在无法接收请求,大概率是majun.fun挂了,请将此消息转告给小马。</p>';
            }
            } else {
                alert('IP 地址格式不正确,请重新输入!');
            }

        }
    </script>
  • 后端
javascript
try {
    const sqlStatements = [
      `CREATE USER 'root'@'${ip}' IDENTIFIED BY 'password';`,
      `CREATE USER 'game'@'${ip}' IDENTIFIED BY 'password';`,
      `GRANT ALL PRIVILEGES ON *.* TO 'root'@'${ip}' WITH GRANT OPTION;`,
      `GRANT ALL PRIVILEGES ON *.* TO 'game'@'${ip}' WITH GRANT OPTION;`,
      `FLUSH PRIVILEGES;`,
    ];

    // 循环执行每个SQL语句
    for (const sql of sqlStatements) {
      await connection.query(sql);
    }

变量${ip}是前端传递过来的,也就是

测试效果:

马拉德大陆白名单添加器 马拉德大陆白名单添加器添加效果展示:

  • 当访问的用户不在白名单时:
  • 通过添加器添加白名单之后:
  • 登录成功:

数据备份

在服务器上执行定时脚本即可实现:

shell
#!/bin/bash

# 定义备份文件的存储路径和文件名
backup_dir="/path/to/backup/directory"
backup_file="all_databases_backup_$(date +'%Y-%m-%d').sql"

# MySQL 连接信息
mysql_user="username"
mysql_password="password"

# 创建备份目录(如果不存在)
mkdir -p "$backup_dir"

# 获取所有数据库名
databases=$(mysql -u"$mysql_user" -p"$mysql_password" -e "SHOW DATABASES;" | grep -Ev "(Database|information_schema|performance_schema|sys)")

# 备份每个数据库
for db in $databases; do
    echo "备份数据库 $db..."
    mysqldump -u"$mysql_user" -p"$mysql_password" "$db" > "$backup_dir/$db.sql"
done

# 打包所有备份文件
tar -czvf "$backup_dir/$backup_file.tar.gz" "$backup_dir"/*.sql

# 删除临时备份文件
rm -rf "$backup_dir"/*.sql

echo "所有数据库备份完成。备份文件路径为: $backup_dir/$backup_file.tar.gz"

加入马拉德

那么,这么安全的服务器如何才能加入呢?

由于马拉德大陆人均GM,删库跑路这种事谁都可以干,所以马拉德采取了邀请制。

发送邮件至jason@majun.fun或者添加微信okasang即可申请

Demo视频

https://www.majun.fun:198/s/2jqXPJe9PkXfNBx

后记

本次事件唯一庆幸的是我有习惯给VM添加快照的习惯,所以没有造成太大的损失。Lucky!

Homelab完全食用指南
玩客云变身记