
Este é um POC que escrevi para a CVE-2024-6387
Qualys Security Advisory
regreSSHion: RCE no servidor do OpenSSH, em sistemas Linux baseados em glibc (CVE-2024-6387)
Resumo SSH-2.0-OpenSSH_3.4p1 Debian 1:3.4p1-1.woody.3 (Debian 3.0r6, de 2005)
Tudo o que é preciso é um salto de fé
-- The Interrupters, "Leap of Faith"
Nota preliminar: o OpenSSH é um dos softwares mais seguros do mundo; esta vulnerabilidade é um deslize em uma implementação que, fora isso, é quase impecável. Seu design e código de defesa em profundidade são um modelo e uma inspiração, e agradecemos aos desenvolvedores do OpenSSH por seu trabalho exemplar.
Descobrimos uma vulnerabilidade (uma condição de corrida no manipulador de sinais) no servidor do OpenSSH (sshd): se um cliente não autenticar dentro de LoginGraceTime segundos (120 por padrão, 600 em versões antigas do OpenSSH), o manipulador SIGALRM do sshd é chamado assincronamente, mas esse manipulador de sinais chama várias funções que não são seguras para sinais assíncronos (por exemplo, syslog()). Essa condição de corrida afeta o sshd em sua configuração padrão.
Ao investigar, percebemos que essa vulnerabilidade é, na verdade, uma regressão do CVE-2006-5051 ("Condição de corrida no manipulador de sinais no OpenSSH anterior à versão 4.4 permite que atacantes remotos causem negação de serviço (queda) e, possivelmente, executem código arbitrário"), que foi relatado em 2006 por Mark Dowd.
Essa regressão foi introduzida em outubro de 2020 (OpenSSH 8.5p1) pelo commit 752250c ("infraestrutura de log revisada para OpenSSH"), que removeu acidentalmente um "#ifdef DO_LOG_SAFE_IN_SIGHAND" de sigdie(), uma função que é chamada diretamente pelo manipulador SIGALRM do sshd. Em outras palavras:
OpenSSH < 4.4p1 é vulnerável a essa condição de corrida no manipulador de sinais, se não tiver recebido backport de correção contra CVE-2006-5051, ou não tiver sido corrigido contra CVE-2008-4109, que foi uma correção incorreta para CVE-2006-5051;
4.4p1 <= OpenSSH < 8.5p1 não é vulnerável a essa condição de corrida no manipulador de sinais (porque o "#ifdef DO_LOG_SAFE_IN_SIGHAND" que foi adicionado a sigdie() pelo patch para CVE-2006-5051 transformou essa função insegura em uma chamada segura _exit(1));
8.5p1 <= OpenSSH < 9.8p1 é vulnerável novamente a essa condição de corrida no manipulador de sinais (porque o "#ifdef DO_LOG_SAFE_IN_SIGHAND" foi removido acidentalmente de sigdie()).
Essa vulnerabilidade é explorável remotamente em sistemas Linux baseados em glibc, onde syslog() chama funções não seguras para sinais assíncronos (por exemplo, malloc() e free()): uma execução remota de código não autenticada como root, porque afeta o código privilegiado do sshd, que não é isolado em sandbox e executa com privilégios totais. Não investigamos nenhuma outra libc ou sistema operacional; mas o OpenBSD notadamente não é vulnerável, porque seu manipulador SIGALRM chama syslog_r(), uma versão de syslog() mais segura para sinais assíncronos, inventada pelo OpenBSD em 2001.
Para explorar essa vulnerabilidade remotamente (até onde sabemos, o CVE-2006-5051 nunca foi explorado com sucesso antes), nos inspiramos em um artigo visionário, "Delivering Signals for Fun and Profit", publicado em 2001 por Michal Zalewski:
https://lcamtuf.coredump.cx/signals.txt
No entanto, enfrentamos imediatamente três grandes problemas:
Do ponto de vista teórico, precisamos encontrar um caminho de código útil que, se interrompido no momento certo pelo SIGALRM, deixe o sshd em um estado inconsistente, e devemos então explorar esse estado inconsistente dentro do manipulador SIGALRM.
Do ponto de vista prático, precisamos encontrar uma maneira de alcançar esse caminho de código útil no sshd e maximizar nossas chances de interrompê-lo no momento certo.
Do ponto de vista de temporização, precisamos encontrar uma maneira de aumentar ainda mais nossas chances de interromper esse caminho de código útil no momento certo, remotamente.
Para focar nesses três problemas sem ter que enfrentar imediatamente todas as proteções modernas dos sistemas operacionais (em particular, ASLR e NX), decidimos explorar primeiro versões antigas do OpenSSH, em i386, e depois, com base nessa experiência, versões recentes:
Primeiro, "SSH-2.0-OpenSSH_3.4p1 Debian 1:3.4p1-1.woody.3", de "debian-30r6-dvd-i386-binary-1_NONUS.iso": esta é a primeira versão do Debian que tem separação de privilégios habilitada por padrão e que foi corrigida contra todas as vulnerabilidades críticas daquela época (em particular, CVE-2003-0693 e CVE-2002-0640).
Para explorar esta versão remotamente, interrompemos uma chamada a free() com SIGALRM (dentro do código de análise de chave pública do sshd), deixamos o heap em um estado inconsistente e exploramos esse estado inconsistente durante outra chamada a free(), dentro do manipulador SIGALRM.
Em nossos experimentos, leva em média ~10.000 tentativas para vencer essa condição de corrida; ou seja, com 10 conexões (MaxStartups) aceitas a cada 600 segundos (LoginGraceTime), leva em média ~1 semana para obter um shell root remoto.
Segundo, "SSH-2.0-OpenSSH_4.2p1 Debian-7ubuntu3", de "ubuntu-6.06.1-server-i386.iso": esta é a última versão do Ubuntu que ainda é vulnerável ao CVE-2006-5051 ("Condição de corrida no manipulador de sinais no OpenSSH anterior à versão 4.4").
Para explorar esta versão remotamente, interrompemos uma chamada a pam_start() com SIGALRM, deixamos uma das estruturas do PAM em um estado inconsistente e exploramos esse estado inconsistente durante uma chamada a pam_end(), dentro do manipulador SIGALRM.
Em nossos experimentos, leva em média ~10.000 tentativas para vencer essa condição de corrida; ou seja, com 10 conexões (MaxStartups) aceitas a cada 120 segundos (LoginGraceTime), leva em média ~1-2 dias para obter um shell root remoto.
Por fim, "SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u2", de "debian-12.5.0-i386-DVD-1.iso": esta é a versão estável atual do Debian, e ela é vulnerável à regressão do CVE-2006-5051.
Para explorar esta versão remotamente, interrompemos uma chamada a malloc() com SIGALRM (dentro do código de análise de chave pública do sshd), deixamos o heap em um estado inconsistente e exploramos esse estado inconsistente durante outra chamada a malloc(), dentro do manipulador SIGALRM (mais precisamente, dentro de syslog()).
Em nossos experimentos, leva em média ~10.000 tentativas para vencer essa condição de corrida, portanto ~3-4 horas com 100 conexões (MaxStartups) aceitas a cada 120 segundos (LoginGraceTime). Em última análise, leva em média ~6-8 horas para obter um shell root remoto, porque só conseguimos adivinhar corretamente o endereço da glibc metade das vezes (devido ao ASLR).
Esta pesquisa ainda está em andamento:
temos como alvo apenas máquinas virtuais, não servidores bare-metal, em um enlace de rede majoritariamente estável (~10ms de jitter de pacotes);
estamos convencidos de que vários aspectos de nossos exploits podem ser muito melhorados;
começamos a trabalhar em um exploit para amd64, que é muito mais difícil por causa do ASLR mais forte.
Poucos dias depois de começarmos nosso trabalho em amd64, notamos o seguinte relatório de bug (no Bugzilla público do OpenSSH), sobre um deadlock no manipulador SIGALRM do sshd: