contents [06]
Em agosto, reportei uma falha no GitHub Enterprise Server (GHES) que começava numa checagem de hardware acessível sem autenticação. Essa checagem fazia chamadas internas aos nós do cluster. Consegui redirecionar uma delas e fazer o GHES devolver uma credencial da API de gerenciamento como mensagem de erro.
Com essa credencial, cadastrei uma chave SSH de admin pela API, o que me deu controle sobre o servidor.
A API de gerenciamento
O GHES é a versão do GitHub que uma organização instala na própria infraestrutura. Pela Manage API, dá para consultar configurações, acompanhar serviços e gerenciar o acesso ao GHES.
Num cluster, os serviços se distribuem entre vários nós. O gateway da Manage API recebe o pedido do cliente e consulta por RPC os agentes nessas máquinas. Para verificar o hardware, ele chama CheckSystemRequirements e reúne os resultados, incluindo possíveis mensagens de erro dos nós.
O fluxo da falha
A checagem aceitava, sem exigir autenticação, uma configuração de cluster com os endereços dos nós. O gateway enviava chamadas a CheckSystemRequirements para esses endereços. Com meu servidor nessa lista, a chamada chegava até ele com a autenticação interna usada pelo GHES para falar com os agentes.
Meu servidor respondia com um redirecionamento para GetSecrets, a operação interna do GHES que consultava credenciais. O cliente RPC seguia o redirecionamento com a mesma autenticação, que também era aceita em GetSecrets. A resposta dessa operação incluía uma chave com acesso administrativo à Manage API.
No retorno, o gateway tratava a resposta de GetSecrets como se viesse de CheckSystemRequirements. A chave era lida como uma mensagem de erro de um nó e devolvida a quem havia feito a requisição inicial. Na figura, A representa CheckSystemRequirements e B, GetSecrets.

O HMAC não vinculava a operação
O gateway autenticava as chamadas aos agentes com um HMAC calculado apenas sobre o timestamp. Como o cálculo não incluía a operação nem o corpo da requisição, redirecionar a chamada para outra operação não invalidava a autenticação.

Os mesmos bytes, outro significado
As mensagens internas usavam Protobuf. Na representação binária, cada campo leva um número e um wire type, que indica como ler o valor. Para strings, por exemplo, o decoder lê primeiro o tamanho e depois os bytes do texto.
Os bytes não incluem os nomes dos campos ou da mensagem, então o decoder depende do schema para interpretá-los. No exemplo fictício da figura, o campo 7 é uma string tanto em ToyA.text quanto em ToyB.label. Assim, os mesmos bytes de ready podem preencher qualquer um dos dois.

No GHES, a resposta de GetSecrets guardava a chave num campo com o mesmo número e tipo do campo de erro da resposta de CheckSystemRequirements. Ambos aceitavam strings, então o decoder lia a chave como uma mensagem de erro.
A compatibilidade entre os schemas explicava por que o decoder aceitava a resposta da operação errada sem falhar.
PoC
Criei um script de PoC para automatizar todo o processo, incluindo o envio da configuração de cluster, o SSRF via redirect interno, a extração da chave da Manage API vazada no campo de erro do Protobuf e, por fim, o cadastro de uma chave SSH para admin sob meu controle. Reproduzi essa cadeia no GHES 3.21.3, 3.21.4 e 3.21.5.
Timeline
| Data | Evento |
|---|---|
| 02/08/2026 | Enviei o report ao GitHub. |
| 14/08/2026 | O GitHub encerrou o report como duplicado de um caso anterior. |
| 01/09/2026 | Publicação da CVE-2026-18730, associada ao report original. |
| 26/09/2026 | Revalidei na 3.21.6 e confirmei que a cadeia já não reproduzia o resultado anterior. |
