
PoC de exploit en Python para CVE-2026-85706, una lectura arbitraria de archivos no autenticada en GitLab CE/EE mediante el bypass de codificación de rutas de Workhorse, con writeup y variantes de bypass.
Lectura arbitraria de archivos locales sin autenticación en GitLab CE/EE. Afecta a 18.7–19.1.7, 19.2.0–19.2.5, 19.3.0–19.3.1. Corregido en 19.1.8 / 19.2.6 / 19.3.2 (2026-09-10). CVSS 10.0, presuntamente explotado en la naturaleza. Informe original de s3ntago y este repositorio es solo mi writeup + PoC.
Esta PoC se publica únicamente con fines educativos y de investigación defensiva, para ayudar a administradores e investigadores a comprender y probar la vulnerabilidad. Ejecútala exclusivamente contra sistemas que poseas o para los que tengas autorización escrita explícita de prueba. Si tu instancia está dentro del rango afectado, deja de leer y aplica el parche a 19.1.8 / 19.2.6 / 19.3.2 primero.
Tres endpoints de repositorio (POST :id/repository/commits, POST/PUT :id/repository/files/:file_path) están detrás del requestBodyUploader de Workhorse. El handler de Rails lee la ruta en disco directamente del campo crudo file.path de la petición y hace File.open sobre ella antes de cualquier autenticación. no sirve como autenticación porque el signing round-tripper de Workhorse adjunta un JWT válido a cada petición que proxya, así que cualquier cosa que llegue al proxy genérico de la API pasa esa comprobación.
require_gitlab_workhorse!Gitlab-Workhorse-Api-RequestLa única razón por la que esto no es un LFI instantáneo para todo el mundo es que se supone que Workhorse reescribe la petición primero. Pero su regex de rutas coincide contra la ruta escapada (EscapedPath() más un clon con path.Clean que nunca decodifica %XX), mientras que Puma decodifica %XX antes del enrutado de Grape. Así que percent-encodea cualquier carácter de un segmento estático (%63ommits, %72epository, %66iles), añade una barra final, o pega .json que la regex de Workhorse no detecta, y Rails sigue enrutando al handler vulnerable. Ese desajuste de codificación es donde reside el bypass. (Variantes como //, /./, %2F, ; no funcionan porque path.Clean normaliza las dos primeras y Puma rechaza %2F.)
Luego solo hay que enviar metadatos de subida falsificados sin firmar como parámetros de query:
POST /api/v4/projects/1/repository/%63ommits?file=&file.path=<ABSOLUTE_PATH>&file.size=1&Content-Type=application/x-www-form-urlencoded
file= en blanco satisface la validación requires :file, WorkhorseFile (en blanco coacciona a nil). La lectura se dispara antes de la autenticación. Recuperar los bytes de vuelta es la parte divertida: en la rama urlencoded el helper ejecuta Rack::Utils.parse_nested_query(File.read(path)) e interpola los errores del parser en el cuerpo del 400. Cualquier % no seguido de dos dígitos hexadecimales lanza InvalidParameterError: invalid %-encoding (<raw file bytes>) y el contenido del archivo vuelve dentro del mensaje de error. La rama JSON (Oj) no filtra nada, por eso el content type urlencoded importa aquí.
El fix (master 0d9ce3e7, backports 1fe30154 / b43c8b26 / 0ff7b6b2) añade authenticate! a los tres endpoints más el pre-paso /authorize, confía solo en el UploadedFile producido por el middleware para ruta/tamaño, y deja de hacer eco de los errores del parser. El bug se introdujo en diciembre de 2025, por eso el rango afectado empieza en 18.7.
./exploit.py --url http://localhost:8080 --file /etc/hostname
./exploit.py --url http://localhost:8080 --file /opt/gitlab/embedded/service/gitlab-rails/config/gitlab.yml
El script recorre todas las formas de bypass verificadas (segmentos codificados, barra final, .json; endpoints commits + files, POST y PUT) y clasifica cada respuesta para que puedas saber en qué punto de la cadena murió una prueba. Solo objetivos autorizados, obviamente.
% no seguido de dos caracteres hexadecimales.(.*) hasta el último ) en el cuerpo). Bien contra el error JSON estándar, sobrecapturará si algo delante envuelve la respuesta en HTML. El fix adecuado es parsear el campo message del JSON.before da 404 en caso contrario).