
GitLab CE/EE의 Workhorse 경로 인코딩 우회를 통한 인증되지 않은 임의 파일 읽기 취약점인 CVE-2026-85706에 대한 Python PoC 익스플로잇으로, 분석 글과 우회 변형을 포함합니다.
GitLab CE/EE의 인증되지 않은 임의 로컬 파일 읽기 취약점. 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). CVSS 10.0, 실제 공격에서 악용된 것으로 보고됨. 최초 보고는 s3ntago가 했으며 이 저장소는 단지 나의 분석 글 + PoC일 뿐이다.
이 PoC는 교육 및 방어적 연구 목적으로만 공개되었으며, 관리자와 연구자가 취약점을 이해하고 테스트하는 데 도움을 주기 위한 것이다. 소유한 시스템 또는 테스트에 대한 명시적 서면 승인을 받은 시스템에만 실행하라. 당신의 인스턴스가 영향을 받는 범위에 해당한다면 읽기를 멈추고 먼저 19.1.8 / 19.2.6 / 19.3.2로 패치하라.
세 개의 저장소 엔드포인트(POST :id/repository/commits, POST/PUT :id/repository/files/:file_path)가 Workhorse의 requestBodyUploader 뒤에 위치한다. Rails 핸들러는 원시 file.path 요청 필드에서 디스크 상의 경로를 그대로 읽어들이고, 인증이 이루어지기 전에 File.open을 호출한다. require_gitlab_workhorse!는 인증 역할을 하지 못하는데, Workhorse의 서명 라운드트리퍼가 프록시하는 모든 요청에 유효한 JWT를 첨부하기 때문이며, 따라서 일반 API 프록시로 흘러 들어가는 모든 것은 이 검사를 통과한다.
Gitlab-Workhorse-Api-Request이것이 모든 사람에게 즉각적인 LFI가 되지 않는 유일한 이유는 Workhorse가 먼저 요청을 재작성해야 하기 때문이다. 그러나 Workhorse의 라우트 정규식은 이스케이프된 경로(EscapedPath()와 결코 %XX를 디코딩하지 않는 path.Clean 복제본)에 대해 매칭되는 반면, Puma는 Grape 라우팅 전에 %XX를 디코딩한다. 따라서 정적 세그먼트의 아무 문자나 퍼센트 인코딩하거나(%63ommits, %72epository, %66iles), 후행 슬래시를 추가하거나, .json을 붙이면 Workhorse의 정규식이 놓치지만 Rails는 여전히 취약한 핸들러로 라우팅한다. 이 인코딩 불일치가 바로 우회의 핵심이다. (//, /./, %2F, ; 같은 변형은 작동하지 않는데, path.Clean이 처음 두 개를 정규화하고 Puma가 %2F를 거부하기 때문이다.)
그런 다음 위조된 서명 없는 업로드 메타데이터를 쿼리 파라미터로 전송하면 된다:
POST /api/v4/projects/1/repository/%63ommits?file=&file.path=<ABSOLUTE_PATH>&file.size=1&Content-Type=application/x-www-form-urlencoded
file=을 비워두면 requires :file, WorkhorseFile 검증을 만족시킨다(빈 값은 nil로 강제 변환됨). 읽기는 인증 전에 발생한다. 바이트를 되돌려 받는 것이 재미있는 부분이다: urlencoded 분기에서 헬퍼는 Rack::Utils.parse_nested_query(File.read(path))를 실행하고 파서 오류를 400 응답 본문에 보간한다. 두 개의 16진수 숫자가 뒤따르지 않는 %는 InvalidParameterError: invalid %-encoding (<raw file bytes>)를 발생시키며, 파일 내용이 오류 메시지 안에 담겨 돌아온다. JSON 분기(Oj)는 아무것도 유출하지 않으며, 이것이 여기서 urlencoded 콘텐츠 타입이 중요한 이유이다.
수정 사항(master 0d9ce3e7, 백포트 1fe30154 / b43c8b26 / 0ff7b6b2)은 세 엔드포인트 모두에 authenticate!를 추가하고 /authorize 사전 단계를 더하며, 경로/크기에 대해 미들웨어가 생성한 UploadedFile만 신뢰하고, 파서 오류를 에코하는 것을 중단한다. 버그는 2025년 12월에 도입되었으며, 이것이 영향을 받는 범위가 18.7에서 시작하는 이유이다.
./exploit.py --url http://localhost:8080 --file /etc/hostname
./exploit.py --url http://localhost:8080 --file /opt/gitlab/embedded/service/gitlab-rails/config/gitlab.yml
이 스크립트는 검증된 모든 우회 형태(인코딩된 세그먼트, 후행 슬래시, .json; commits + files 엔드포인트, POST 및 PUT)를 순환하며 각 응답을 분류하여 체인의 어느 지점에서 프로브가 실패했는지 알 수 있게 해준다. 물론 승인된 대상에만 사용하라.
%가 포함된 경우에만 발생한다.)까지 (.*)). 기본 JSON 오류에 대해서는 문제없지만, 앞단의 무언가가 응답을 HTML로 감싸면 과도하게 캡처할 것이다. 올바른 수정은 JSON 메시지 필드를 파싱하는 것이다.before 블록이 404를 반환한다).