My Homelab Got Hacked - A Postmortem
My Homelab Got Hacked - A Postmortem
My Homelab Got Hacked - A Postmortem 10 Aug, 2026
Welp. My Forgejo instance got popped with an RCE two days ago by CVE-2026-60004. Luckily, I noticed the following morning and had the day free to figure out what happened. Let’s dive in!
哎呀。两天前,我的 Forgejo 实例因为 CVE-2026-60004 漏洞被远程代码执行(RCE)攻击了。幸运的是,我在第二天早上就注意到了,并且那天正好有空来排查到底发生了什么。让我们深入了解一下!
The Incident
I woke up on Saturday morning to several Pushover notifications from Komodo warning me that my server’s CPU usage was quickly spiking to 95%, then dropping back to 80%. That machine has an i5-10600K that normally idles at ~4% usage. I checked Komodo and found that it’d been at a sustained 50% CPU usage since 9:00AM the day before; about 24 hours.
事件经过
周六早上醒来时,我收到了几条来自 Komodo 的 Pushover 通知,警告我的服务器 CPU 使用率迅速飙升至 95%,然后回落到 80%。那台机器搭载的是 i5-10600K,通常空闲时的使用率约为 4%。我检查了 Komodo,发现从前一天早上 9 点开始,CPU 就一直维持在 50% 的使用率,持续了大约 24 小时。
I unfortunately did not get a screenshot, but the CPU usage of my Forgejo Docker container made it the obvious culprit. I logged in as the admin and quickly noticed a new user: testpoc26188. I thought I had disabled open sign-ups, but apparently I had not. This was my first mistake. Looking at this user, they only had a single repo, poc-78614, with two files: a README that just said poc-78614, and a folder called hooks containing a shell script called post-index-change.
很遗憾我没能截屏,但 Forgejo Docker 容器的 CPU 使用率显然是罪魁祸首。我以管理员身份登录,很快发现了一个新用户:testpoc26188。我本以为自己禁用了开放注册,但显然并没有。这是我犯的第一个错误。查看这个用户,他们只有一个名为 poc-78614 的仓库,里面有两个文件:一个写着 poc-78614 的 README 文件,以及一个名为 hooks 的文件夹,其中包含一个名为 post-index-change 的 Shell 脚本。
At this point, I shut down Forgejo’s Docker container, though I wish I had kept it paused for later investigation. Based on the contents of post-index-change, I quickly found CVE-2026-60004, a recently-disclosed RCE vulnerability in Gitea, upon which Forgejo is based. It exploits a bug in Gitea’s (and thus, Forgejo’s) diffpatch endpoint, enabling the attacker to execute a malicious Git hook to obtain RCE. The CVE had already been patched in the latest releases of v15 LTS and v16, so why was I still vulnerable?
此时,我关闭了 Forgejo 的 Docker 容器,虽然我真希望当时能把它暂停以便后续调查。根据 post-index-change 的内容,我很快查到了 CVE-2026-60004,这是 Gitea(Forgejo 基于此构建)最近披露的一个 RCE 漏洞。它利用了 Gitea(因此也是 Forgejo)diffpatch 端点中的一个 Bug,使攻击者能够执行恶意的 Git Hook 来实现 RCE。该 CVE 已经在 v15 LTS 和 v16 的最新版本中修复了,那么为什么我仍然受到影响呢?
Now for my second mistake. For most containers, I use latest image tags which lets Komodo check for and notify me of available updates every night. Its yellow “update available” icons give my ADHD-ass an out of place reminder to read release notes and manually decide when to update. Unfortunately, Forgejo only offers versioned image tags, meaning no latest tag. I still had mine pinned to v13 which reached EOL 6 months ago in January, 2026. It would be nice to have a latest tag, but I understand why the Forgejo team would be hesitant to offer one. No hate of course, I still have mad respect for y’all. 🫶
现在来说我的第二个错误。对于大多数容器,我使用 latest 镜像标签,这让 Komodo 能够每晚检查并通知我是否有可用更新。它那黄色的“有更新可用”图标给了我这个 ADHD 患者一个及时的提醒,让我去阅读发行说明并手动决定何时更新。不幸的是,Forgejo 只提供带版本号的镜像标签,这意味着没有 latest 标签。我的版本还停留在 v13,该版本在 6 个月前的 2026 年 1 月就已经停止维护(EOL)了。如果能有一个 latest 标签会很好,但我理解 Forgejo 团队为什么不愿提供。当然,我没有任何恶意,我依然非常尊重你们。🫶
The Nerdy Bits!
I ran build_hook("COMMAND", "LEAK_REF") from Gitea’s published POC and diff’d it against the exploit on my server.
技术细节!
我运行了 Gitea 公布的 POC 中的 build_hook("COMMAND", "LEAK_REF"),并将其与我服务器上的漏洞利用脚本进行了对比。
(Diff output omitted for brevity)
They were nearly identical. The only differences were the details in the Git commit and tree variables(foreshadowing…), my placeholder COMMAND and LEAK_REF values, and the attacker’s added curl back to their server’s IP. Speaking of their server’s IP, it looks to be a server on RackNerd’s infra. I curled the malicious IP and grabbed the payload: another shell script. This one was a heavily minified, lightly obfuscated 6-liner, with the longest line being 1063 characters. After formatting it with shfmt, it was still gross, but much more readable at 57 lines.
它们几乎完全相同。唯一的区别在于 Git commit 和 tree 变量中的细节(伏笔……)、我占位符 COMMAND 和 LEAK_REF 的值,以及攻击者添加的指向其服务器 IP 的 curl 命令。说到他们的服务器 IP,看起来是 RackNerd 基础设施上的服务器。我 curl 了那个恶意 IP 并获取了载荷:另一个 Shell 脚本。这是一个经过高度压缩、轻微混淆的 6 行脚本,最长的一行有 1063 个字符。在使用 shfmt 格式化后,它依然很乱,但 57 行的代码已经可读多了。
This bit immediately stood out to me:
[ "$__a" = "x86_64" ] && __u="http://172.245.159.216/1"
[ "$__a" = "aarch64" ] && __u="http://172.245.159.216/2"
[ "$__a" = "amd64" ] && __u="http://172.245.159.216/3"
Smells like platform-specific binaries; these must be the real payloads. The second-stage script was still pretty unreadable overall though, so I had an LLM add comments throughout to explain what each chunk did. Note: I know the LLM-use will (understandably) piss some people off. However, I would not have otherwise been able to understand this nigh-unreadable shell script. Rest-assured that AI was not used elsewhere in the writing of this post, and that I am not an AI-bro.
这一部分立刻引起了我的注意: (代码块同上) 这看起来像是针对特定平台的二进制文件;这些一定是真正的载荷。不过,第二阶段的脚本整体上仍然很难读,所以我让 LLM 在各处添加了注释,解释每一块代码的作用。注意:我知道使用 LLM 会(可以理解地)让一些人感到不爽。然而,否则我将无法理解这个几乎无法阅读的 Shell 脚本。请放心,本文的其他部分没有使用 AI,我也不是什么“AI 吹”。
In short, the second-stage script:
- Attempts to kill processes with “guard” in the name (antivirus?).
- Kills processes using more than 80% of the CPU and disables their systemd entries.
- Determines a suitable download location between /tmp, HOME, and PWD.
- Determines the host’s CPU architecture.
- Attempts to download the corresponding binary, with fallback methods including curl, wget, Python urllib, Perl HTTP:Tiny, a raw Perl HTTP request, and, when all else fails, a plain-old HTTP/1.0 GET request.
- Executes, then deletes the binary from the disk once it’s safely running in memory.
简而言之,第二阶段的脚本:
- 尝试杀死名称中带有“guard”的进程(杀毒软件?)。
- 杀死 CPU 使用率超过 80% 的进程并禁用它们的 systemd 条目。
- 在 /tmp、HOME 和 PWD 之间确定一个合适的下载位置。
- 确定主机的 CPU 架构。
- 尝试下载相应的二进制文件,并提供多种回退方法,包括 curl、wget、Python urllib、Perl HTTP:Tiny、原始 Perl HTTP 请求,如果所有方法都失败,则使用最原始的 HTTP/1.0 GET 请求。
- 执行二进制文件,并在其安全运行于内存中后将其从磁盘删除。
Not bad! So, what about those binaries? What exactly is the goal of this whole thing? To find out, I threw them into VirusTotal. Shocker: they are all known crypto-miners. For those interested, here are the SHA-256 hashes of each binary.
还不错!那么,那些二进制文件呢?这一切的目标到底是什么?为了找出答案,我把它们扔进了 VirusTotal。不出所料:它们都是已知的加密货币挖矿程序。对于感兴趣的人,这里是每个二进制文件的 SHA-256 哈希值。
(Hashes omitted)
I couldn’t find a wallet address for the miner to send crypto to in either of the shell scripts, so I assume it’s hard-coded into the binaries. It’s possible that the binaries coordinate with the attacker’s server to get or create an address, bu
我在两个 Shell 脚本中都没找到挖矿程序发送加密货币的钱包地址,所以我推测它被硬编码在二进制文件中了。也有可能这些二进制文件会与攻击者的服务器协调以获取或创建地址,但……