碎碎念
2026 年 8 月,我的一台 Ubuntu 家庭服务器突然出现严重卡顿:SSH 难以连接、Nginx 长时间阻塞、Swap 被完全占满、系统不断触发 OOM,最终甚至出现 RCU stall 和 systemd-journald watchdog timeout。
最初我怀疑是磁盘、内存不足、Docker 服务异常,甚至一度怀疑大量 SSH 扫描是服务器失联的直接原因。
随着调查深入,最终发现真正的根因是:
公网暴露的 Gitea 1.22.0 已被利用 Git
diffpatch/post-index-change相关远程代码执行漏洞入侵。攻击者在 Gitea Docker 容器中部署了至少两套不同的 XMR 挖矿组件,其中第二套甚至包含主动清除竞争矿工的逻辑。
第一套恶意程序伪装成:
kmod-cache-update
实际为 XMRig 6.26.0,连接 MoneroOcean。
第二套恶意程序:
ssl-compet
连接 C3Pool,并通过 mnmonitor.sh 搜索、杀死包括 MoneroOcean 在内的竞争矿工。

与此同时,还发现一个名为:
memfd:a
的可疑内存执行进程尝试连接 104.168.19.222:9000,但由于没有完成同等深度的样本分析,其具体用途仍无法最终确认。
整个事件中最关键的一条证据链是:
Gitea
→ 自动注册攻击账户
→ 创建仓库
→ POST /diffpatch
→ hooks/post-index-change
→ Git Hook 执行
→ Shell RCE
→ 矿工 / Watchdog
→ CPU、RAM、Swap 资源耗尽
→ OOM / RCU stall
→ 整机服务异常
现场日志中不仅直接记录了:
Applied patch to 'hooks/post-index-change' cleanly.
甚至出现:
User-Agent: gitea-rce-poc/2.0
结合运行版本、HTTP 请求序列、Git Hook、进程父子关系以及矿工启动时间,现场证据与 CVE-2026-60004 / GHSA-rcr6-4jqh-j84m 的利用机制形成了极高置信度的闭环。原始复盘中记录的 Gitea 版本为 1.22.0,且 /diffpatch 与 post-index-change 均有直接现场证据。
本文完整记录这次事件从:
“服务器为什么突然卡死?”
一路追查到:
“是谁启动了矿工、矿工从哪个容器出来、Gitea 是怎样执行恶意 Git Hook 的、为什么两套矿工会互相攻击,以及宿主机到底有没有被进一步突破。”
1. 事故发生:一台几乎无法使用的家庭服务器
服务器当时运行:
Ubuntu 24.04.2 LTS
4 核 CPU
约 16 GiB RAM
4 GiB Swap
Docker
Nginx
MySQL
Gitea
以及多个自托管服务
它并没有突然重启。
磁盘使用同样正常:
系统盘约 17%
数据盘约 45%
因此一开始就基本可以排除:
磁盘满
inode 耗尽
文件系统空间不足
真正异常的是:
CPU idle ≈ 0%
Load Average ≈ 7~9
Swap 4 GiB 几乎全部占满
对一台 4 核机器而言,Load 长时间处于 7~9 已经意味着大量任务处于运行或不可中断等待状态。
更危险的是:
CPU 没有空闲,同时 Swap 被完全吃满。
这通常意味着系统已经不仅仅是“内存用得多”,而是在不停进行内存回收、Swap I/O 和进程调度竞争。

2. 从 OOM 到 RCU stall:系统已经进入资源饥饿
系统日志很快给出了更多线索。
首先出现:
rtkit-daemon:
The canary thread is apparently starving
随后开始触发 OOM。
包括:
pipewire
wireplumber
dbus
portal
gnome-keyring
在内的一系列用户态服务被 OOM Killer 清理。
继续向后看,异常已经不仅仅是内存不足:
nginx blocked > 197 sec
qbittorrent-nox blocked > 197 sec
openssl blocked > 197 sec
内核开始报告:
RCU stall
rcu_preempt kthread starved
甚至 systemd-journald 自己也出现:
watchdog timeout
SIGKILL
journal 异常关闭/损坏
重新建立 journal
这解释了一个重要现象:
为什么服务器最严重的那段时间日志反而不完整?
因为:
系统已经卡到连负责记录日志的 journald 自己都无法正常得到调度。
因此,这次故障本质上并不是:
某一个 Docker 容器崩了
也不是:
某一个 Web 服务挂了
而是整台机器进入:
CPU 饥饿
+
内存回收
+
Swap 抖动
+
I/O 等待
+
内核调度延迟
形成的系统级资源雪崩。
3. 第一条错误线索:大量 SSH 扫描
排查期间还发现 SSH 日志存在明显异常。
例如:
MaxStartups throttling
短时间内:
约 45 秒丢弃 455 个连接
累计丢弃约 888 个连接
来自大量公网地址。
很容易自然地产生一个判断:
“是不是有人一直暴力破解 SSH,最后把服务器打挂了?”
但是继续调查后,没有找到这些公网地址对应的成功登录证据。
更关键的是,后面找到的真正高 CPU 恶意进程全部属于:
Gitea Docker cgroup
所以最终结论是:
SSH 扫描真实存在,但它只是同时发生的互联网背景攻击,并不是这次服务器资源耗尽的核心原因。
这是整次调查的第一个重要经验:
最显眼的攻击,不一定是真正的攻击入口。
4. 真正突破口:kmod-cache-update
最终在进程列表中发现:
PID 3866911
程序路径:
/data/gitea/home/.cache/kmod-cache/kmod-cache-update
完整启动参数里出现:
--url=gulf.moneroocean.stream:10128
--user=4AjneeUV2MmW8XTusJCditAFk9PQtHm1gV3ZD6RvfHzBf2yd5zZ6Zt27fNDmMhXBCN5NY1EDgKiqoVj25wLK2uwpMb1n3hu
--pass=5ffd2589d210
--algo=rx/0
--threads=3
--background
这已经足以确认它不是普通系统程序。
其中:
rx/0
对应 RandomX。
而:
gulf.moneroocean.stream
则是 MoneroOcean。
继续查看矿工日志后直接看到:
XMRig/6.26.0
以及:
POOL #1
gulf.moneroocean.stream:10128
连接目标:
139.99.69.109:10128
随后持续收到:
new job
并提交算力。
因此可以确认:
kmod-cache-update
↓
XMRig 6.26.0
↓
RandomX
↓
MoneroOcean
↓
139.99.69.109:10128
这里还有一个非常有意思的细节。
矿工参数:
--pass=5ffd2589d210
恰好是 Gitea Docker Container ID:
5ffd2589d210...
的前 12 位。
这很可能意味着攻击脚本自动把当前容器 ID 当成了矿池 worker 标识。
5. 它不是一次性矿工:攻击者还部署了 Watchdog
继续调查后又发现三个:
miner-watchdog.sh
启动时间分别为:
2026-08-07 00:07:43
2026-08-07 00:08:07
2026-08-07 00:08:28
路径:
/data/gitea/home/.cache/kmod-cache/miner-watchdog.sh
宿主机通过 bind mount 可以看到:
/opt/gitea/data/gitea/home/.cache/kmod-cache/miner-watchdog.sh
它的主要逻辑非常简单:
每隔 60 秒检查一次矿工
如果矿工存在:
sleep
如果矿工不存在:
重新执行 kmod-cache-update
使用的:
POOL
WALLET
THREADS
与前面发现的 XMRig 完全一致。
也就是说:
杀掉一次矿工是不够的。
攻击者已经建立了一个:
矿工被杀
↓
Watchdog 发现
↓
重新启动
的自动恢复机制。
6. 为什么恶意进程显示成了我的用户名?
一个很容易引起误解的地方是:
ps
显示这些矿工属于:
yanchang
第一感觉很可能是:
“攻击者是不是已经登录了我的宿主机账户?”
但查看 /proc/<PID>/cgroup 后发现:
/system.slice/docker-5ffd2589d210...scope
进程父子关系同样是:
systemd
└─ containerd-shim
└─ s6-svscan
└─ miner/watchdog
说明这些恶意进程实际属于 Gitea Docker 容器。
原因只是 UID。
容器里的:
git
使用:
UID 1000
宿主机:
yanchang
也是:
UID 1000
所以 Linux 在宿主机 ps 中将 UID 1000 翻译成了:
yanchang
而不是说攻击者真的使用了宿主机的 yanchang 登录会话。
7. 锁定 Gitea:恶意进程来自哪个容器?
对应容器信息为:
Container:
5ffd2589d2106e17cfc4841ab57dae70d045a19b42794cc1f1df4a4d84e913ab
Name:
gitea
Image:
gitea/gitea:1.22.0
容器不是:
Privileged=true
也没有发现:
/var/run/docker.sock
被挂入容器。
主要的宿主机可写映射为:
/opt/gitea/data:/data
于是调查方向变成:
攻击者究竟是通过什么方式,在 Gitea 内部获得代码执行的?
8. 攻击入口:/diffpatch
扩展日志时间范围以后,关键记录开始出现。
2026-08-07:
00:06:57
POST /api/v1/user/repos
201 Created
来源:43.174.51.188
说明一个账户创建了 repository。
5 秒之后:
00:07:02
POST /api/v1/repos/poco1gccf/poc-bvzbrw/diffpatch
201 Created
接下来:
00:07:06
POST .../diffpatch
00:07:14
POST .../diffpatch
00:07:15
POST .../diffpatch
00:07:18
POST .../diffpatch
00:07:28
POST .../diffpatch
00:07:30
POST .../diffpatch
如果单看这些请求,只知道有人在大量调用 Gitea:
/diffpatch
接口。
真正决定性的证据随后出现。
9. Applied patch to 'hooks/post-index-change' cleanly.
Gitea 日志中直接记录:
Applied patch to 'hooks/post-index-change' cleanly.
至少出现在:
2026-08-07 00:09:20
2026-08-07 00:10:24
2026-08-07 00:10:47
这正好对应了:
CVE-2026-60004
GHSA-rcr6-4jqh-j84m
所涉及的 Gitea diffpatch Git Hook 代码执行链。
可以将核心机制简化为:
flowchart TD
A[攻击者拥有仓库写权限] --> B[调用 /diffpatch]
B --> C[构造恶意 executable patch]
C --> D[Git apply 进入 three-way fallback]
D --> E[创建 temporary bare repository]
E --> F[写入 hooks/post-index-change]
F --> G[Git 自动执行 Hook]
G --> H[以 Gitea 服务用户执行任意命令]
这里最关键的并不是“理论上这个漏洞能这样利用”。
而是现场真实出现了:
/diffpatch
以及:
hooks/post-index-change
两者同时出现。
这已经让漏洞利用归因达到非常高的可信度。
10. 更直接的证据:gitea-rce-poc/2.0
Nginx 日志随后还记录:
User-Agent: gitea-rce-poc/2.0
例如:
223.109.1.187
用户:sec_test_a2fde3
POST /api/v1/user/repos
User-Agent: gitea-rce-poc/2.0
随后:
POST /api/v1/repos/sec_test_a2fde3/test-repo-659df9901b/diffpatch
User-Agent: gitea-rce-poc/2.0
这里已经能看出明显的自动化攻击特征:
生成随机用户名
↓
创建随机 repository
↓
调用 /diffpatch
↓
重复执行
因此这次事件并不像:
某个人专门盯着这台家庭服务器进行了定向入侵。
而更像:
漏洞公开后,自动化扫描器在整个互联网中寻找暴露的 Gitea,并对满足条件的目标批量执行 PoC。
11. 为什么攻击者不需要管理员密码?
检查 Gitea 配置:
DISABLE_REGISTRATION = false
REQUIRE_SIGNIN_VIEW = false
REGISTER_EMAIL_CONFIRM = false
这意味着:
允许公开注册
+
无需邮件确认
于是攻击路径变成:
flowchart LR
A[发现公网 Gitea] --> B[自动注册普通账户]
B --> C[创建自己的 Repository]
C --> D[获得 Repository 写权限]
D --> E[调用 /diffpatch]
E --> F[Git Hook RCE]
也就是说,目前没有必要假设攻击者:
破解了管理员密码
或者:
偷到了已有用户 Token
正常的业务功能已经允许攻击者自己获得漏洞所需要的仓库权限。
这是一个非常重要的安全问题:
低权限业务账号 + 应用 RCE,依然可以构成完整的服务器入侵链。
12. 第一套攻击:时间线精确到秒
将不同日志统一以后,第一套攻击链基本可以恢复:
这意味着从第一批漏洞利用到矿工真正连上矿池:
不到两分钟。
这显然不是:
人进入 Shell
→ 手动研究系统
→ 慢慢下载 miner
→ 手动配置
更符合:
自动扫描
→ 自动注册
→ 自动利用
→ 自动下载
→ 自动启动
的一体化攻击脚本。
13. 8 月 9 日再次利用:8 秒之后矿工启动
另一组时间关系更加直接。
2026-08-09 08:19:33
创建 repository
08:19:35
POST /diffpatch
08:19:40
POST /diffpatch
08:19:45
POST /diffpatch
随后:
08:19:53
kmod-cache-update 启动
也就是:
08:19:45
最后一次 /diffpatch
↓
8 秒
↓
08:19:53
XMRig 启动
对于第一套矿工而言,这已经构成非常强的:
HTTP exploit
→ Shell execution
→ miner
时间关联。
14. 第二套感染出现:ssl-compet
事情到这里原本已经足够复杂。
但随后又发现:
PID 794672
启动时间:
2026-08-10 06:03:59
表面进程名:
sslmon
实际 ELF:
/tmp/.xmon-1000/ssl-compet
但真正决定它来源的是父进程树:
systemd
└─ containerd-shim
└─ s6-svscan
└─ post-index-change
└─ post-index-change
└─ bash
└─ ssl-compet
环境变量中则有:
GIT_DIR=.
USER=git
PWD=/data/gitea/tmp/local-repo/upload.git2316476783
因此这件事可以明确确认:
第二套矿工同样是从 Gitea 临时 repository 的
post-index-changeGit Hook 中被执行出来的。
但这里需要维持取证上的严谨。
已确认
第二套矿工:
Gitea temporary Git repo
→ post-index-change
→ bash
→ ssl-compet
高度可能,但尚未完成最终 HTTP 闭环
其最初 HTTP 请求同样来自:
CVE-2026-60004 /diffpatch
因为没有完成 06:03 前后 HTTP 请求与进程启动的逐秒对应,所以不能把这一点写成百分之百确认。
15. 第二套矿工连接的是另一家矿池
第二套:
/tmp/.xmon-1000/config.json
记录:
url:
auto.c3pool.org:19999
钱包:
457KQKqFtyg9WtcHQdvok4N6uRZstLmPzFrPv2ewNjHeSRP1MyFFw2zHHjCpm7JoYgMGSpwCwfK9PXXd4nsXQ7Hm9AXofmi
实际网络连接:
ssl-compet
172.27.0.2
↓
47.86.46.179:19999
ESTABLISHED
所以两套感染分别是:
感染 A
XMRig
→ MoneroOcean
→ Wallet A
和:
感染 B
ssl-compet
→ C3Pool
→ Wallet B
如果仅仅是不同矿池、不同钱包,还不能证明它们一定是不同攻击活动。
但随后发现的 mnmonitor.sh 基本改变了判断。
16. 最荒诞的一幕:矿工之间还会互相攻击
第二套目录中存在:
mnmonitor.sh
脚本注释明确写道:
Kills rival miners / deployment scripts / malicious connections.
更关键的是它包含:
COMPETITOR_POOL_PATTERNS
其中明确列出:
moneroocean.stream
gulf.moneroocean
而:
gulf.moneroocean.stream
恰好就是第一套矿工连接的矿池。
换句话说,第二套感染逻辑实际上包含:
flowchart TD
A[扫描当前运行进程和网络连接] --> B{发现竞争矿工?}
B -- 否 --> C[保持自己的 miner 运行]
B -- 是 --> D[识别矿池/钱包/进程]
D --> E[杀死竞争矿工]
E --> C
实际效果可能是:
攻击活动 A:
启动 MoneroOcean miner
攻击活动 B:
发现 MoneroOcean
→ 杀 A
→ 启动自己的 C3Pool miner
攻击活动 A Watchdog:
发现矿工没了
→ 再启动 A
于是,在服务器主人甚至还不知道发生了什么的时候:
两套恶意程序已经开始争夺这台机器的 CPU。
是否一定对应两个不同“人”,无法证明。
但从:
不同钱包
不同矿池
不同恶意框架
主动识别并清除竞争矿工
来看:
至少存在两套具有明显竞争关系的恶意感染活动。
17. 还有第三个东西:memfd:a
网络检查过程中还发现:
memfd:a
PID 820963
尝试连接:
104.168.19.222:9000
状态:
SYN-SENT
memfd 本身并不能直接证明恶意。
但恶意程序经常使用 Linux:
memfd_create()
实现:
下载 payload
→ 写入匿名内存文件
→ 直接执行
→ 减少普通磁盘文件痕迹
遗憾的是,这个进程没有完成和两套矿工同等程度的深入取证。
所以:
104.168.19.222:9000
只能记录为:
恶意环境中发现的高度可疑 IOC。
不能进一步武断定义为:
C2
或者:
Payload Server
它也可能是其他功能组件。
18. 为什么服务器最终会“假死”?
第一套 XMRig 使用:
RandomX
3 threads
RandomX 本身属于典型的内存密集型 PoW。
矿工日志中单次初始化就记录:
allocated 2336 MB
与此同时还有:
第一套 miner
第一套 watchdog
第二套 miner
第二套 monitor
memfd 可疑进程
Gitea
MySQL
Nginx
其他自托管服务
Linux 文件缓存
桌面和用户服务
于是整个故障链变成:
flowchart TD
A[矿工长期占用 CPU] --> B[RandomX 占用大量 RAM]
B --> C[系统开始大量回收内存]
C --> D[冷页面进入 Swap]
D --> E[4 GiB Swap 被耗尽]
E --> F[大量 Swap I/O / Page Reclaim]
F --> G[普通服务无法及时调度]
G --> H[OOM]
H --> I[RCU Stall]
I --> J[journald / nginx / SSH 等服务异常]
所以:
服务器没有真正 Kernel Panic,也没有突然断电。
它更像是在恶意计算造成的巨大资源压力下:
逐渐失去正常调度能力。
最终从用户视角看起来就像:
服务器“死机”了
19. 是否已经 Docker Escape?
这是整个调查中必须保持谨慎的一点。
Gitea 容器配置显示:
Privileged=false
没有发现:
docker.sock
也没有发现:
host PID namespace
以及明显特殊设备映射。
主要可写宿主机路径:
/opt/gitea/data:/data
因此恶意程序在容器中写:
/data/gitea/...
宿主机看到:
/opt/gitea/data/gitea/...
属于正常 bind mount 行为。
这本身不等于 Docker Escape。
后续又检查:
/etc/systemd/system
/etc/cron*
/var/spool/cron
~/.config/systemd
~/.bashrc
~/.profile
以及:
kmod-cache
miner-watchdog
ssl-compet
sslmon
mnmonitor
xmon
MoneroOcean
C3Pool
相关 IOC IP
未发现对应的宿主机持久化。
root 没有 crontab。
yanchang 的 crontab 文件存在,但只包含 2026 年 2 月写入的 Vixie Cron 注释,没有任何实际定时任务。
近期 systemd 文件检查同样只看到符合 Snap 正常更新模式的 mount unit。
因此最终结论必须精确表述为:
截至现有在线调查,没有发现明确的 Docker Escape 或宿主机持久化证据。
而不能说:
“能够证明攻击者绝对没有碰过宿主机。”
如果这是一台极高价值设备,要达到更高置信度,需要:
离线磁盘镜像
内存取证
可信基线文件 Hash 比对
EDR 时间线
甚至直接从可信介质重装
20. 完整攻击链
最终可以将此次事件总结为:
flowchart TD
Internet[Internet 自动扫描器]
Internet --> Gitea[公网 Gitea 1.22.0]
Gitea --> Registration[开放注册<br/>无需邮箱确认]
Registration --> Account[自动注册账户]
Account --> Repo[创建 PoC Repository]
Repo --> Diffpatch[POST /api/v1/.../diffpatch]
Diffpatch --> CVE[CVE-2026-60004]
CVE --> BareRepo[Temporary Bare Git Repository]
BareRepo --> Hook[hooks/post-index-change]
Hook --> RCE[Shell RCE]
RCE --> A[感染链 A]
RCE --> B[感染链 B]
A --> Watchdog[miner-watchdog.sh]
Watchdog --> MinerA[kmod-cache-update / XMRig]
MinerA --> Ocean[MoneroOcean]
Ocean --> IPA[139.99.69.109:10128]
B --> Monitor[mnmonitor.sh]
Monitor --> MinerB[ssl-compet]
MinerB --> C3[C3Pool]
C3 --> IPB[47.86.46.179:19999]
Monitor -. 清除竞争矿工 .-> MinerA
RCE -. 未完全确认关联 .-> Memfd[memfd:a]
Memfd --> IPC[104.168.19.222:9000]
MinerA --> Resource[CPU / RAM 被抢占]
MinerB --> Resource
Resource --> Swap[Swap 4 GiB 耗尽]
Swap --> OOM[OOM / RCU Stall]
OOM --> Outage[nginx / journald / SSH 等异常]
21. 关键事件时间线
2026-08-07
2026-08-09
即:
最后一次 /diffpatch
↓
8 秒
↓
矿工启动
2026-08-10
22. IOC 附录
第一套矿工
第二套矿工
未完全分类组件
Gitea 攻击特征
/api/v1/user/repos
/api/v1/repos/<owner>/<repo>/diffpatch
hooks/post-index-change
User-Agent:
gitea-rce-poc/2.0
23. 关键排查命令
这次事件里最有价值的并不是某一条“神器命令”,而是从:
资源
→ 进程
→ 父进程
→ 容器
→ 文件
→ 网络
→ 应用日志
→ 时间线
逐层往上追。
查看资源
top
free -h
uptime
vmstat 1
查找异常进程
ps aux --sort=-%cpu | head -30
ps aux --sort=-%mem | head -30
追进程父链
pstree -aps <PID>
/proc 取证
readlink -f /proc/<PID>/exe
readlink -f /proc/<PID>/cwd
cat /proc/<PID>/comm
tr '\0' ' ' < /proc/<PID>/cmdline
tr '\0' '\n' < /proc/<PID>/environ
cat /proc/<PID>/cgroup
这一步在本次事件中尤其关键,因为遇到过:
cmdline:
[kworker/0:1]
但真实:
exe:
/usr/bin/bash
comm:
bash
的用户态伪装进程。
查看网络
sudo ss -ntp
或:
sudo lsof -i -nP
Docker 归属
docker ps -a
docker inspect <container>
docker logs <container>
docker diff <container>
Gitea/Nginx 日志
grep -Ri "diffpatch" /path/to/logs
grep -Ri "post-index-change" /path/to/logs
grep -Ri "gitea-rce-poc" /path/to/logs
检查已知 IOC
ps aux | grep -Ei \
'gitea|kmod-cache|miner-watchdog|ssl-compet|sslmon|mnmonitor|xmon|memfd:a|moneroocean|c3pool' \
| grep -v grep
sudo ss -ntp | grep -E \
'139\.99\.69\.109|47\.86\.46\.179|104\.168\.19\.222|:10128|:19999|:9000'
检查宿主机持久化
sudo grep -RniEi \
'kmod-cache|miner-watchdog|ssl-compet|sslmon|mnmonitor|xmon|moneroocean|c3pool' \
/etc/systemd/system \
/etc/cron.d \
/etc/cron.daily \
/etc/cron.hourly \
/etc/cron.weekly \
/etc/cron.monthly \
/var/spool/cron \
2>/dev/null
24. 最终处置
最终我选择的并不是:
清理矿工,然后升级 Gitea 继续运行。
而是:
彻底退役这套 Gitea。
已经确认完成的清理包括:
✓ Gitea Docker 容器停止并删除
✓ gitea.service 删除
✓ systemd enable symlink 删除
✓ systemd daemon-reload
✓ Nginx Gitea 配置删除
✓ nginx -t 验证成功
✓ Gitea Docker network 删除
✓ gitea/gitea:1.22.0 image 删除
✓ 未发现 Gitea named volume
✓ 已知恶意进程消失
✓ 已知恶意网络连接消失
✓ systemd 已知 IOC 搜索无结果
✓ root cron 无任务
✓ 用户 cron 无实际任务
对应清理记录显示 Docker 容器、网络和镜像均已成功删除。
这里有一个需要特别说明的取证细节:
在目前保存的最后一份命令记录中:
/opt/gitea
仍然存在,其中包括:
docker-compose.yml
data/
gitea.db
当时已经明确计划执行:
sudo rm -rf --one-file-system /opt/gitea
但当前保存日志中没有出现执行该删除命令后的最终确认输出。
因此,严谨起见,本篇不把:
/opt/gitea已经删除
列入“日志已证明完成”的处置项。
这也是事故报告中一个很重要的原则:
计划执行过什么,和日志证明已经执行完成什么,必须区分。
25. 清理后的验证
最终再次搜索:
kmod-cache
miner-watchdog
ssl-compet
sslmon
mnmonitor
xmon
memfd:a
moneroocean
c3pool
无进程。
网络检查:
139.99.69.109
47.86.46.179
104.168.19.222
10128
19999
9000
同样没有连接。
这是一个非常重要的旁证:
随着 Gitea 容器被停止和删除,已知恶意进程全部一起消失。
这进一步支持:
已知矿工执行范围
主要位于 Gitea 容器
而不是宿主机上存在独立运行的另一套相同恶意服务。
26. 哪些结论已经确认?
事故复盘中,我认为必须明确区分“证据”和“猜测”。
27. 这次事件真正的根因
回头看,并不是一个单独配置造成了事故。
而是三个条件叠加。
27.1 公网服务版本严重滞后
运行版本:
Gitea 1.22.0
而现场使用的 RCE 攻击链针对的是一个后续修复的高危漏洞。
公网服务如果长期不升级,实际上相当于:
漏洞公开
↓
PoC 发布
↓
扫描器自动化
↓
等待某一天扫到自己
27.2 服务直接暴露在整个互联网
一个经常出现的误区是:
“我的服务器只是家庭服务器,谁会攻击我?”
但自动扫描根本不关心:
你是谁
服务器里有什么重要数据
这是不是企业机器
它只关心:
这个 IP 开了什么端口?
这个服务是什么?
这个版本可能存在什么漏洞?
PoC 能不能打通?
所以:
低价值目标并不等于低攻击概率。
27.3 开放注册降低了攻击条件
当:
DISABLE_REGISTRATION=false
REGISTER_EMAIL_CONFIRM=false
时,攻击者无需已有账户。
可以:
自己注册
→ 自己创建 repository
→ 自己获得 repository 写权限
→ 利用应用漏洞
从正常业务角度看:
普通用户只能管理自己的仓库
权限并不高。
但当应用存在 RCE:
普通账户
+
Repository 写权限
+
危险漏洞
=
代码执行
28. 这次事件带给我的几个教训
28.1 “服务还在正常打开”不代表没有被入侵
很多挖矿攻击不会:
删除数据
勒索
修改首页
主动告诉你服务器被黑
相反,它希望:
服务器尽可能长时间保持在线。
因为在线时间就是算力。
所以有时候最先暴露恶意活动的不是业务异常,而是:
CPU
内存
Swap
网络连接
28.2 Docker 确实有价值,但不是安全免疫层
这次容器隔离很可能降低了进一步宿主机感染的风险。
但攻击者仍然能够读取和修改:
/data
而它映射的是:
/opt/gitea/data
这意味着:
Git repository
Gitea DB
配置
Secret
附件
用户数据
理论上都可能处于应用 RCE 可访问范围。
所以:
Docker 能限制部分爆炸半径,但无法修复应用本身的 RCE。
28.3 Kill Miner 不等于 Incident Response
如果当时只做:
kill <miner>
几分钟后:
miner-watchdog.sh
完全可能再次将它拉起来。
真正需要追查的是:
这个程序是谁启动的?
父进程是什么?
文件在哪里?
持久化机制是什么?
入口在哪里?
28.4 父进程树比进程名更可信
攻击者可以把进程名伪装成:
sslmon
甚至伪装成:
[kworker/0:1]
但是:
/proc/PID/exe
/proc/PID/cgroup
/proc/PID/environ
pstree
往往会讲出另一套故事。
这一次真正把第二套矿工和 Gitea 连起来的,不是它叫什么。
而是:
post-index-change
→ bash
→ ssl-compet
28.5 时间线是入侵取证最强的工具之一
单独看到:
/diffpatch
不能证明它启动了矿工。
单独看到:
XMRig
也不能说明入口是 Gitea。
但:
08:19:45
最后一次 /diffpatch
08:19:53
XMRig 启动
只有 8 秒的时间差,再结合:
post-index-change
父进程链,证据强度就完全不同。
29. 如果重新部署自托管服务,我会怎么做?
这次事件以后,我会把公网自托管服务默认当作:
持续受到自动扫描的互联网资产。
至少会执行以下原则。
只暴露真正需要公网访问的服务
管理后台优先使用:
VPN
WireGuard
Tailscale
Zero Trust
IP Allowlist
而不是直接暴露整个互联网。
建立更新机制
定期关注:
Security Advisory
CVE
Release Notes
Container Image Update
不能只做到:
部署成功
还要考虑:
六个月以后
一年以后
这个版本是否还安全?
不需要注册就关闭注册
私人使用的 Git 服务:
开放注册
通常没有多少实际收益。
建立最低限度监控
至少监控:
CPU
Load
Memory
Swap
Disk
Docker restart
异常网络连接
尤其是:
Swap 突然从几乎不用变成长期 100% 使用
本身就值得立刻告警。
重要服务定期检查公网暴露面
例如:
我究竟有哪些端口直接面向 Internet?
往往比:
我记得我只开放了几个端口
更加重要。
30. 最后一个有趣但危险的观察:漏洞公开后的时间窗口已经非常短
此次第一批明确攻击发生在:
2026-08-07
从攻击行为看,利用脚本已经高度自动化。
一个新的目标可能经历:
扫描到服务
↓
注册账号
↓
建立仓库
↓
利用 RCE
↓
投放 Miner
整个过程可以压缩到几分钟。
这说明今天真正的风险已经不再是:
“漏洞公开后我这个月有空再更新。”
而是:
PoC 能不能在你更新之前被自动化扫描器部署。
结语:从“服务器为什么这么卡”追到了真正的入口
这次排查一开始只是:
“为什么服务器今天突然这么卡?”
一路调查下来却变成:
服务器卡顿
↓
CPU 100%
↓
Swap 满
↓
OOM
↓
RCU stall
↓
SSH 大量扫描
↓
发现 XMRig
↓
发现 Watchdog
↓
确定 Docker cgroup
↓
锁定 Gitea
↓
发现 /diffpatch
↓
发现 hooks/post-index-change
↓
还原 Gitea RCE
↓
发现公开 PoC 指纹
↓
发现第二套竞争矿工
↓
发现 memfd 可疑组件
↓
删除整个 Gitea 服务
↓
检查宿主机持久化
最后真正得到的并不只是:
“矿工杀掉了”
而是能够回答整个事件中最关键的问题:
服务器为什么会卡死?
恶意挖矿导致 CPU、RandomX 内存以及 Swap 长期处于高压力状态,引发 OOM、调度延迟和 RCU stall。
恶意程序在哪里运行?
Gitea Docker 容器。
是怎么进去的?
现场证据高度吻合 Gitea diffpatch / post-index-change RCE。
有没有管理员密码泄露证据?
没有必要假设存在。开放注册已经能够提供漏洞利用需要的仓库权限。
为什么杀掉矿工以后还会回来?
存在 miner-watchdog.sh 自动恢复矿工。
为什么有两个矿工?
至少存在两套不同恶意感染链,而且第二套主动清理第一套使用的矿池和程序,具有明显竞争关系。
有没有 Docker Escape?
截至当前在线调查:
没有发现明确证据。
宿主机有没有 systemd / cron 后门?
当前检查:
没有发现。
已知恶意进程是否已经消失?
最终 IOC 验证中:
已全部消失。
这次事件最后给我留下最深的一句话是:
公网服务并不需要“被人盯上”才会遭到攻击。只要漏洞存在、服务能够被扫描到,它就已经在攻击者的目标列表里。
对于互联网自动化攻击而言:
你是谁
并不重要。
重要的只是:
你的端口开着
服务能识别
版本存在漏洞
PoC 可以执行
当这些条件同时成立时,从:
“一台正常运行的家庭服务器”
到:
“一台正在给陌生钱包挖 Monero 的机器”
可能只需要不到两分钟。
而真正有效的事件响应,也不应该停在:
把那个高 CPU 进程杀掉。
更重要的是继续追问:
它为什么会出现在这里?
只有追到这个问题的答案,一次入侵排查才算真正完成。