CVE-2022-2992
Crítica
| Produto | GitLab CE/EE (< 15.3.3, 15.2.5, 15.1.6) |
| Classe | Injeção de comando no Redis via desserialização (Sawyer) |
| Vetor | Rede · autenticado (importação de repositório) |
| Correção | Atualizar para 15.3.3 / 15.2.5 / 15.1.6 |
o bug
Acompanhando as publicações de vulnerabilidades na plataforma de bug bounty HackerOne, encontramos a vulnerabilidade 1679624, reportada pelo usuário vakzz, e que se tornou pública dia 6 de Outubro de 2022.
Relatório original de vakzz no HackerOne.
Como de constume, o relatório do vakzz fornece uma visão completa do bug, entretanto algo que chamou bastante atenção foi o fato da equipe de segurança do Gitlab não fornecer o ID do problema, dificultando o rastreamento da vulnerabilidade.
Trecho do relatório que chamou nossa atenção.
Após algum tempo investigando o repositório do Gitlab, finalmente encontramos a issue 371884 com o titulo Remote code execution via Github import, o que parecia bastante promissor. A issue, de acordo com um comentário, havia sido corrigida na versão 15.3.2.
A issue relacionada no repositório do GitLab.
O próximo passo foi olhar para os commits dessa versão, onde identificamos um commit interessante, provavelmente relacionado ao problema:
No overriding methods for Sawyer class
Busca pelos commits que corrigiram o problema.
Após ler o commit, percebemos que o problema era um pouco mais complexo do que imaginávamos.
A Causa
O Sawyer é uma biblioteca que converte um hash em um objeto cujos métodos são baseados nas chaves do hash.
O exemplo de código abaixo mostra como o Sawyer converte um hash em um objeto:
Como o Sawyer converte um hash em um objeto.
O problema é que a gem redis usa to_s e bytesize para gerar o comando RESP (protocolo de serialização do Redis).
https://github.com/redis/redis-rb/blob/v4.4.0/lib/redis/connection/command_helper.rb#L19-L21
def build_command(args)
command = [nil]
args.each do |i|
if i.is_a? Array
i.each do |j|
j = j.to_s
command << "$#{j.bytesize}"
command << j
end
else
i = i.to_s
command << "$#{i.bytesize}"
command << i
end
end
command[0] = "*#{(command.length - 1) / 2}"
command << ""
command.join(COMMAND_DELIMITER)
end
A gem redis usa to_s e bytesize para montar o comando RESP (build_command em redis-rb).
Portanto, se o Sawyer::Resource for controlável por um adversário, ele pode ser usado para sobrescrever os metódos .to_s e .bytesize que posteriormente serão usado para "montar" o comando comando RESP e executar comandos no Redis.
Os dois trechos de código subsequentes nos ajudam a exemplificar como o Redis se comporta quando adicionamos uma hash e um objeto Sawyer e depois recuperamos esse valor. O resultado será o seguinte:
Hash
Comportamento do Redis ao receber um Hash.
Sawyer::Resource
Comportamento do Redis ao receber um Sawyer::Resource.
No Gitlab o método import_repository recebe o parâmetro default_branch que vem do repositório do cliente (que é um Sawyer::Resource aninhado de dados), e o parâmetro é passado para change_head, que então chama branch_exists? e branch_names_include? e por fim, passa o valor para o redis.
https://gitlab.com/gitlab-org/gitlab/-/blob/v15.3.1-ee/lib/gitlab/repositorycacheadapter.rb#L71
O parâmetro default_branch chega ao Redis via import_repository.
Ou seja, o "default_branch" é um valor controlado pelo atacante, e é passado para o Redis.O cenário de exploração consiste em criar um repositório no GitHub com um branch chamado to_s e outro chamado bytesize, e então importar esse repositório para o GitLab.
A Correção
Como podemos ver no commit a classe Sawyer::Resource é "patcheada" para evitar que métodos já existentes sejam sobrescritos. Isso significa que o método to_s não será sobrescrito pela classe Sawyer::Resource e, portanto, nenhum dado malicioso será passado para a gem redis.
O commit de correção.
Ambiente de testes
Após entender qual versão do GitLab estava vulnerável, criamos um ambiente de testes para reproduzirmos o problema.
Provavelmente a forma mais fácil de criar um ambiente de testes é usando o GitLab Omnibus Docker, utilizando a seguinte imagem: gitlab/gitlab-ce:15.1.0-ce.0
Esses são os detalhes do ambiente de testes:
Detalhes do ambiente de testes.
A Exploração
Resumidamente a exploração dessa vulnerabilidade consiste em:
1. Criar um payload
O payload deve ser no formato RESP (protocolo de serialização Redis) que executa o comando escolhido. Esse payload é gerado usando uma chave de sessão aleatória do GitLab, que é usada posteriormente para forçar o GitLab a executar o payload.
Isso pode ser feito usando o script disponível aqui.
2. Criar um grupo no GitLab
O grupo criado será usado para importar o repositório falso.
Criação do grupo no GitLab.
3. Criar um repositório falso no GitHub
Servir um repositório falso no GitHub que responde a solicitações GET em /api/v3/repos/\w+/\w+ com um default_branch que substitui to_s e bytesize, por exemplo:
{
"default_branch": {
"to_s": {
"to_s": "ggg\r\nPAYLOAD",
"bytesize": 3
}
}
}
4. Importar o repositório falso no GitLab.
Importação do repositório falso no GitLab.
Podemos confirmar que o payload foi injetado no Redis usando o seguinte comando:
Confirmação de que o payload foi injetado no Redis.
5. Acionar o payload
Para acionar o payload é preciso acessar o GitLab com o cookie de sessão utilizado para gerar o payload, para forçar a execução do payload:
Requisição que aciona o payload.
❕ As seguintes premissas precisam ser atendidas:
- Possuir um token de acesso pessoal do GitLab com permissões para criar grupos e importar repositórios. Tal token dever estar definido na variável de ambiente API_TOKEN;
- A variável de ambiente
GROUP_NAME deve estar definida com o nome do grupo que será criado;
- O Ngrok deve estar rodando e apontando para o servidor falso. A variável de ambiente
NGROK_URL é definida com o endereço do Ngrok.
esse passo é importante para garantir que o servidor falso seja acessível a partir do servidor do GitLab e tenha um TLD válido com um certificado SSL válido [1].
O Exploit
Criamos uma prova de conceito para explorar o problema, que pode ser encontrada no nosso repositório. Para a utilização da prova de conceito, será preciso uma credencial de acesso e a execução dos seguintes comandos:
Execução da prova de conceito.
Também submetemos um módulo de exploração para o Metasploit Framework, que pode ser encontrado aqui
Notas extras
Na sua configuração padrão, o GitLab não permite importar de uma rede local, deve ser um TLD válido. Essa é a razão pela qual o Ngrok é usado para criar um servidor falso com um TLD válido e um certificado SSL válido.
Na configuração padrão, o GitLab não importa de uma rede local.
References