yanchang
yanchang
发布于 2026-08-11 / 0 阅读
0
0

记录一次Gitea漏洞攻击导致的植入挖矿程序(AI总结)

碎碎念

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,且 /diffpatchpost-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. 第一套攻击:时间线精确到秒

将不同日志统一以后,第一套攻击链基本可以恢复:

时间

事件

2026-08-07 00:06:57

poco1gccf 创建 repository

00:07:02

第一次 POST /diffpatch

00:07:06

再次 POST /diffpatch

00:07:43

第一个 miner-watchdog.sh 启动

00:07:44

Watchdog 文件创建

00:08:07

第二个 Watchdog 启动

00:08:28

第三个 Watchdog 启动

00:08:50

XMRig 连接 MoneroOcean

后续

持续获得 mining job 并提交算力

这意味着从第一批漏洞利用到矿工真正连上矿池:

不到两分钟。

这显然不是:

人进入 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-change Git 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

时间

事件

可信度

00:06:57

攻击账号创建 repository

已确认

00:07:02

第一次 /diffpatch

已确认

00:07:06

第二次 /diffpatch

已确认

00:07:43

miner-watchdog.sh 启动

已确认

00:07:44

Watchdog 文件创建

已确认

00:08:07

第二个 Watchdog

已确认

00:08:28

第三个 Watchdog

已确认

00:08:50

XMRig 连接 MoneroOcean

已确认

00:09:20 等

Gitea 写入 hooks/post-index-change

已确认

2026-08-09

时间

事件

08:19:33

新攻击账号/仓库创建

08:19:35

/diffpatch

08:19:40

/diffpatch

08:19:45

/diffpatch

08:19:53

kmod-cache-update 启动

即:

最后一次 /diffpatch
        ↓
8 秒
        ↓
矿工启动

2026-08-10

时间

事件

06:03:59

第二套 ssl-compet 启动

后续调查

父链确认来自 post-index-change

后续调查

发现 mnmonitor.sh 竞争矿工清除逻辑

后续调查

发现 memfd:a → 104.168.19.222:9000

事件响应

停止并删除 Gitea Docker

事件响应

删除 Gitea systemd / Nginx / Docker 网络 / 镜像

事件响应

检查宿主机 cron/systemd 持久化

最终检查

已知矿工与已知网络 IOC 均消失


22. IOC 附录

第一套矿工

类型

IOC

文件

/data/gitea/home/.cache/kmod-cache/kmod-cache-update

Watchdog

miner-watchdog.sh

算法

RandomX / rx/0

软件

XMRig 6.26.0

Pool

gulf.moneroocean.stream:10128

IP

139.99.69.109:10128

Wallet

4AjneeUV2MmW8XTusJCditAFk9PQtHm1gV3ZD6RvfHzBf2yd5zZ6Zt27fNDmMhXBCN5NY1EDgKiqoVj25wLK2uwpMb1n3hu

第二套矿工

类型

IOC

ELF

/tmp/.xmon-1000/ssl-compet

假进程名

sslmon

Monitor

mnmonitor.sh

Pool

auto.c3pool.org:19999

IP

47.86.46.179:19999

Wallet

457KQKqFtyg9WtcHQdvok4N6uRZstLmPzFrPv2ewNjHeSRP1MyFFw2zHHjCpm7JoYgMGSpwCwfK9PXXd4nsXQ7Hm9AXofmi

未完全分类组件

类型

IOC

进程

memfd:a

PID(调查时)

820963

外联

104.168.19.222:9000

分类

高度可疑,但具体用途未确认

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. 哪些结论已经确认?

事故复盘中,我认为必须明确区分“证据”和“猜测”。

项目

结论

Gitea 发生真实入侵

确认

Gitea 版本为 1.22.0

确认

出现大量 /diffpatch 请求

确认

hooks/post-index-change 被 patch

确认

CVE-2026-60004 与现场攻击链一致

极高置信度

第一套矿工来自 Gitea RCE

确认

第一套是 XMRig

确认

第一套连接 MoneroOcean

确认

第一套存在 Watchdog

确认

第二套 ssl-compet 是矿工

确认

第二套由 Gitea post-index-change 拉起

确认

第二套连接 C3Pool

确认

两套属于竞争性感染活动

高度可能

两套一定来自两个不同自然人

无法确认

第二套最初 HTTP 请求一定也是 CVE-2026-60004

高度可能,未完成 HTTP 闭环

104.168.19.222:9000 一定是 C2

无法确认

SSH 暴力破解是此次入侵入口

没有证据

已发生 Docker Escape

没有证据

宿主机存在 systemd 后门

未发现

宿主机存在 cron 后门

未发现

已知矿工清理后仍存活


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 进程杀掉。

更重要的是继续追问:

它为什么会出现在这里?

只有追到这个问题的答案,一次入侵排查才算真正完成。


评论