diff --git a/docs/vorfall-2026-08-12-gitea.md b/docs/vorfall-2026-08-12-gitea.md new file mode 100644 index 0000000..930c86d --- /dev/null +++ b/docs/vorfall-2026-08-12-gitea.md @@ -0,0 +1,128 @@ +# Einbruch in den Gitea, 12.08.2026 + +Kein Ausfall, kein Datenverlust, kein laufender Schadcode — aber ein echter +Schreibzugriff von außen. Das hier ist der Hergang, so wie er **gemessen** +wurde, und was daraus geändert wurde. + +## Was passiert ist + +``` +21:13:56 POST /api/internal/manager/add-logger -> 200 OK +21:13:57 POST /api/internal/manager/remove-logger/... -> 200 OK +21:13:57 /data/gitea/home/.gitconfig neu geschrieben +``` + +Der Angreifer hat über Giteas **interne Manager-API** einen Datei-Logger auf +die `.gitconfig` gelegt, seine Nutzlast als Logzeile hineinschreiben lassen und +den Logger eine Sekunde später wieder entfernt. Der Vorgang räumt hinter sich +auf; ohne den Nebeneffekt wäre er unbemerkt geblieben. + +Eingeschleust wurde ein `packObjectsHook` — ein Kommando, das Git bei jedem +`upload-pack` (also bei jedem Klonen und Ziehen) ausführt. Dekodiert: + +- lädt `xmrig` von `111.90.158.58` bzw. `111.90.139.202` +- tarnt ihn als `/var/tmp/.gt_sys_health` mit Konfiguration daneben +- schürft gegen `gulf.moneroocean.stream:443`/`:80`, Worker-ID `gitea`, + Wallet `89wM69z64AMhAKAY…` +- verankert sich per `crontab` (`*/3 * * * *` und `@reboot`) und `/etc/cron.d/gt_sys` +- killt konkurrierende Miner und eigene Altfassungen + +## Warum trotzdem nichts lief + +**Die Nutzlast hat sich selbst zerschossen.** Die eingeschleuste Zeile beginnt +mit `[uploadpack]\n` — wobei das `\n` als **Text** in der Datei steht und nicht +als Zeilenumbruch. Git kann die Datei damit nicht lesen, bricht mit +`fatal: bad config line 23` ab, und der Hook wurde nie gültig. + +Nachgemessen, im Container wie auf dem Wirt: + +| | Befund | +|---|---| +| Miner-Prozess | keiner | +| `/var/tmp/.gt_*`, `/tmp/.gt_*` | nichts | +| crontab, `/etc/cron.d` | unverändert | +| Nutzlast in Repos | keine | +| Docker-Socket im Container | **nicht gemountet** — kein Ausbruch auf den Wirt | +| Gitea-Konten | eines, das eigene | +| Fremde Token oder SSH-Schlüssel | keine | + +Der einzige Schaden war, dass **alle Git-Operationen ausfielen** — inklusive +der Pushes aus diesem Projekt. Der Angriff ist nur daran aufgefallen. + +## Der Weg hinein + +Gitea lag mit `0.0.0.0:3000` **direkt im Internet**, am Reverse-Proxy vorbei — +`http://88.218.224.10:3000/` antwortete mit 200. Damit war auch +`/api/internal/*` von außen erreichbar; geschützt nur durch das +`INTERNAL_TOKEN`. + +**Woher der Angreifer das Token hatte, ließ sich nicht klären.** Es steht in +keinem Repo, und RDP war es nicht (siehe unten). Bleibt eine Lücke in Gitea +1.26.4 oder ein Leck, das hier nicht sichtbar ist. Genau deshalb ist die +Antwort nicht „Token tauschen", sondern **den Weg zumachen**. + +### RDP: beschossen, nicht geknackt + +Cloudflare meldete offene RDP-Server. Nachgesehen im Sicherheitsprotokoll, +14 Tage: + +``` +2756 fehlgeschlagene Anmeldungen, verteilt auf viele IPs + 33 erfolgreiche von aussen -- alle von EINER IP, dem Anschluss des Betreibers +``` + +RDP war also **nicht** der Einbruchsweg. NLA ist aktiv. Trotzdem gehört der +Zugang eingegrenzt — 2756 Versuche sind ein Dauerbeschuss. + +## Was geändert wurde + +1. **`.gitconfig` gesäubert** — die zwei vergifteten Zeilen raus, 22 Zeilen + übrig. Git funktioniert wieder beim ersten Versuch. +2. **Port 3000 ist weg.** Nicht auf localhost gebunden, sondern **gar nicht + mehr veröffentlicht**. Von außen: keine Antwort. +3. **Der Proxy geht jetzt durchs interne Netz.** NPM hängt zusätzlich in + `gitea_default` und spricht `gitea:3000` an. Dafür musste der nginx-Resolver + auf Dockers eigenen DNS (`127.0.0.11`) umgestellt werden — der alte stammte + noch aus der Zeit, als NPM nur im `bridge`-Netz hing. +4. **`INTERNAL_TOKEN` und `SECRET_KEY` getauscht.** Gefahrlos hier, weil weder + 2FA noch externe Anmeldequellen eingerichtet sind (beides nachgezählt: 0). +5. **`/api/internal/` gibt am Proxy jetzt 404.** Dritte Sperre: selbst wenn das + Token erneut leckt, ist der Weg von außen zu. Gitea ruft diese Pfade nur + containerintern über localhost. +6. **Frisches Abbild.** Der Container lief elf Tage mit dem Stand von damals — + `gitea/gitea:latest` heißt nicht, dass das Laufende aktuell ist. + +Gegengeprüft, beides: + +``` +Port 3000 von aussen -> 000 (zu) +/api/internal/manager/add-logger -> 404 (gesperrt) +https://git.d4rkst3r.de/ -> 200 +https://git.d4rkst3r.de/api/v1/version -> 200 +grosse Uploads (LFS): 3 von 3 Direktiven noch da +Push aus diesem Projekt: geht, beim ersten Versuch +Repos gegen die lokalen Klone: identisch +``` + +## Was noch offen ist + +- **Alle API-Token und Webhook-Geheimnisse als geleakt behandeln** und + tauschen: `GITEA_TOKEN`, das Token `Claude`, Devlog- und Gitea-Webhooks. + Nicht weil ein Diebstahl belegt ist, sondern weil er nicht ausgeschlossen + werden kann. +- **Admin-Passwort ändern.** +- **RDP eingrenzen** — siehe unten. +- **Beweise** liegen unter `C:\backup\vorfall-2026-08-12\`: die vergiftete + `.gitconfig`, die ursprüngliche `app.ini`, das vollständige Gitea-Log + (24 MB) und die alte Compose-Datei. + +## Die Lehre, die über diesen Fall hinausgeht + +Der offene Port war **nie nötig**. Der Proxy hätte von Anfang an über das +interne Netz gehen können; die Veröffentlichung war Bequemlichkeit beim +Einrichten. Sie stand elf Tage, und in dieser Zeit hat jemand sie gefunden. + +Und: aufgefallen ist es nicht, weil etwas überwacht wurde, sondern weil der +Angreifer geschludert hat. **Hätte er sein `\n` richtig gesetzt, liefe der +Miner noch** — und die einzige Spur wäre eine erhöhte CPU-Last gewesen, die +niemandem auffällt.