Snyk Fetch the Flag CTF 2023: solução do desafio Off the SETUID
Carlos Polop
Yago Gutiérrez
30 de novembro de 2023
0 minutos de leituraObrigado por jogar Fetch com a gente! Parabéns aos milhares de participantes do Fetch the Flag CTF. Se você participou do Fetch the Flag 2023 da Snyk e está procurando a resposta para o desafio Off the SETUID, chegou ao lugar certo. Vamos conferir a solução juntos!
Você está em um ambiente desconhecido. Será que consegue se virar com o que tem à disposição? Ou será que dá para simplesmente...
Recuperar a flag do diretório pessoal do usuário root.
Que desafio estranho. É um desafio web ou um desafio de exploração do kernel?
Temos um script do qemu, um kernel compilado (com seu respectivo .diff) e uma imagem initramfs (além do código do programa init). Para começar, podemos descompactar o initramfs e dar uma olhada.
Usar uma distro ou não usar uma distro
Certo, uma imagem distroless. Sem shell, sem utilitários comuns, sem nada. O programa init obviamente foi compilado (não há shell, então não pode ser um script), mas também temos o código-fonte:
Ele faz o que um script init normalmente faz: inicializa alguns dispositivos, monta procfs e sysfs (com noexec), remonta o rootfs como somente leitura e inicializa a rede. Em seguida, inicia um servidor HTTP na porta 8080, com o diretório raiz em /var/run e executado como usuário com uid=100.
Em /var/run, só há um index.php:
Recapitulando: temos um servidor HTTP claramente vulnerável a injeção de código PHP. Podemos usar algo como o exemplo abaixo para obter um shell PHP interativo reverso:
Mas não podemos ler a flag, pois ela só pode ser lida pelo root. Portanto, precisamos encontrar uma maneira de escalar privilégios. Entra em cena o fun_setuid.diff.
fun_setuid()
No arquivo .diff, vemos que um novo syscall foi adicionado ao kernel:
Em resumo, esse syscall permite que um processo altere seu uid para outro valor, desde que ele seja maior que o atual (ou que o processo tenha a capacidade CAP_SETUID).
O syscall usa uma função auxiliar chamada prepare_user_creds(), que por sua vez usa prepare_cred() para alocar um novo struct cred, modificá-lo para se tornar o usuário desejado e retorná-lo. Se ocorrer um erro, ela retorna o valor negativo de um código de erro. Por isso, a função syscall verifica se o valor retornado não é negativo antes de confirmar as credenciais (veja a Observação de projeto abaixo). Ela também verifica se o uid solicitado é válido (ou seja, diferente de -1); caso contrário, retorna NULL.
Observação de projeto
Esse projeto também tem dois problemas sérios:
Comparar um endereço com zero em C não funciona. Simplesmente não funciona. Endereços são tratados como tipos sem sinal, então não faz sentido verificar se um deles é negativo. Por isso, o compilador remove completamente o if.
Mesmo que o compilador não o removesse, o código ainda não funcionaria porque, no Linux em x86, os endereços do kernel são sempre negativos. Assim, mesmo quando
prep_user_creds()não encontra nenhum problema, o ponteirostruct cred*retornado por ela seria interpretado como negativo e não seria confirmado.
Credencialismo
O problema é que a função syscall não verifica essa última possibilidade e, por isso, pode confirmar um ponteiro NULL como ponteiro para nossa estrutura de credenciais. Depois de modificar o initramfs para adicionar o busybox,, podemos executar sysctl vm.mmap_min_addr para ver o endereço mínimo que um processo pode mapear neste sistema. Se precisarmos depurar mais, também temos /proc/config.gz disponível.
Vemos que mmap_min_addr=0 e que o script run.sh não habilita SMAP na VM... ótimo! Podemos mapear o endereço NULL, colocar ali uma estrutura de credenciais de root e chamar fun_setuid(-1) para fazer nosso ponteiro de credenciais apontar para NULL, ou seja, para nossas credenciais falsas.
Não precisamos realmente alterar as capacidades, mas já que estamos aqui, podemos fazer isso para ganhar pontos extras. Mas atenção: é muito importante definir cred->usage=1 (que funciona como um contador de referências), ou o kernel vai considerar que se trata de um bug de UAF (BUG_ON(atomic_read(&new->usage) < 1)).
Também não podemos deixar esse processo terminar, senão o kernel tentará liberar essa estrutura, o que não seria nada bom. Por isso, não podemos iniciar um shell (chamar execve() acaba encerrando o processo atual); daí o pause() no final.
Mas ainda temos um problema: não podemos enviar o exploit para lugar nenhum para executá-lo. Podemos converter o exploit em shellcode e usar código PHP para gravar no arquivo mem do procfs do processo PHP, obtendo execução arbitrária de código nativo. A partir daí, podemos executar mmap() em NULL, colocar algumas credenciais falsas e chamar fun_setuid(-1), já que não podemos fazer nada disso com um script PHP…
Mas isso não é legal o bastante.
Memexec
Há alguns meses, fizemos uma palestra na DEFCON 31 sobre como contornar ambientes sem distro, em determinadas circunstâncias. Para isso, eu (Yago Gutiérrez) desenvolvi uma ferramenta chamada memexec, que permite executar qualquer programa que você quiser no PHP, sem gravá-lo em um arquivo.
Basta colar esta sessão interativa de PHP. A partir daí, você terá uma função memexec() que aceita dois argumentos: uma URI para um binário e um array com os argumentos do programa. Por exemplo, podemos configurar um servidor HTTP com busybox e executar comandos no sistema:
Importante: não há bibliotecas SSL na VM, então não use HTTPS.
Muito bem, vamos explorar esse kernel de uma vez por todas.
Obrigado por fazer o Fetch acontecer!
Um enorme agradecimento a todas as equipes do Fetch the Flag 2023! Foi ótimo encontrar vocês por lá. Sempre dá para falar com a gente em @Arget (no GH, @arget13) e @carlospolop (também no GH, @carlospop).
Confira as soluções dos outros desafios de 2023. Mergulhe de cabeça!
