RegreSSHion: execução remota de código (RCE) em servidores OpenSSH do Linux baseados em glibc
(qualys.com)- Uma condição de corrida no manipulador de sinais do
sshd, servidor OpenSSH, pode permitir que um cliente não autenticado provoque execução remota de código em servidores com configuração padrão, sem se autenticar dentro do tempo limite pré-autenticaçãoLoginGraceTime - Essa vulnerabilidade é uma regressão do CVE-2006-5051 e reapareceu entre a versão 8.5p1 e anteriores à 9.8p1 depois que um commit do OpenSSH 8.5p1, em outubro de 2020, removeu uma proteção em
sigdie() - Em Linux baseado em glibc,
syslog()chama funções unsafe para sinais assíncronos comomalloc()efree(), o que pode levar a RCE root sem autenticação no código privilegiado e sem sandbox dosshd - Os testes foram feitos em uma máquina virtual i386 e em uma rede estável com cerca de 10 ms de jitter de pacotes; no Debian 12.5.0 com OpenSSH 9.2p1, foram necessárias em média cerca de 10.000 tentativas e, com
MaxStartups=100eLoginGraceTime=120, levou cerca de 6 a 8 horas para obter um shell root - O OpenSSH corrigiu o problema em 6 de junho de 2024 com o commit
81c1099; se atualizar ou recompilar for difícil, definirLoginGraceTime 0pode bloquear a RCE, mas o risco de DoS por exaustão de conexões MaxStartups permanece
Onde a vulnerabilidade ocorre
- O problema no
sshddo OpenSSH começa no manipulador de SIGALRM que atua antes da autenticação- Se o cliente não se autenticar dentro de
LoginGraceTime, o manipulador deSIGALRMé chamado de forma assíncrona - Esse manipulador chama funções que não são async-signal-safe, como
syslog() - O valor padrão é
LoginGraceTime=120segundos; em versões antigas do OpenSSH, era 600 segundos
- Se o cliente não se autenticar dentro de
- Essa vulnerabilidade é uma regressão do CVE-2006-5051
- O CVE-2006-5051 foi uma condição de corrida no manipulador de sinais em versões anteriores ao OpenSSH 4.4, reportada por Mark Dowd em 2006
- Em outubro de 2020, o commit
752250cdo OpenSSH 8.5p1 removeu por engano#ifdef DO_LOG_SAFE_IN_SIGHANDdesigdie()
- O impacto por versão é claramente dividido
- Antes do OpenSSH 4.4p1: vulnerável, a menos que os patches relacionados ao CVE-2006-5051 ou CVE-2008-4109 tenham sido retroportados
- Do OpenSSH 4.4p1 até antes do 8.5p1:
sigdie()foi alterado para uma chamada segura a_exit(1), então não é vulnerável a essa condição de corrida - Do OpenSSH 8.5p1 até antes do 9.8p1: voltou a ficar vulnerável após a remoção da proteção
Ambientes afetados e exceções
- O alvo da exploração remota é o Linux baseado em glibc
- O
syslog()da glibc chama internamente funções async-signal-unsafe comomalloc()efree() - O código vulnerável está no código privilegiado do
sshd, executado sem sandbox e com privilégios totais - Como resultado, é possível obter execução remota de código como root sem autenticação
- O
- Outras libc ou sistemas operacionais não foram incluídos na investigação
- O OpenBSD não é vulnerável
- O manipulador de
SIGALRMdo OpenBSD chamasyslog_r()em vez desyslog() syslog_r()é uma versão mais segura para sinais assíncronos criada pelo OpenBSD em 2001
- O manipulador de
Premissas da pesquisa de exploração remota
- Para explorar essa condição de corrida remotamente, era preciso resolver três problemas
- Encontrar um caminho de código que deixasse o
sshdem estado inconsistente quandoSIGALRMinterrompesse no momento certo - Alcançar esse caminho de código e aumentar a chance de interrupção no momento correto
- Conseguir acertar esse timing mesmo em ambiente de rede remoto
- Encontrar um caminho de código que deixasse o
- Em vez de enfrentar de imediato as proteções mais novas, a pesquisa começou em um ambiente i386 com OpenSSH antigo e depois foi expandida para versões recentes
- As condições do experimento têm limitações claras
- Foram usados apenas máquinas virtuais, não servidores bare metal
- A rede era um link relativamente estável com cerca de 10 ms de jitter de pacotes
- Várias partes do exploit ainda podem ser melhoradas
- O trabalho de exploit em amd64 já começou, mas é bem mais difícil por causa de um ASLR mais forte
Experimentos com OpenSSH antigo
-
Debian 3.0r6, OpenSSH 3.4p1
- O alvo era
SSH-2.0-OpenSSH_3.4p1 Debian 1:3.4p1-1.woody.3, um ambiente de 2005 do Debian 3.0r6 - Essa versão do Debian foi a primeira a ativar privilege separation por padrão, e os principais patches de vulnerabilidade da época já estavam aplicados
- A exploração usou a interrupção de
free()e um estado inconsistente do heap- Uma chamada a
free()no código de parsing de chave pública era interrompida porSIGALRM - Depois, esse estado inconsistente do heap era usado em outro
free()dentro depacket_close()
- Uma chamada a
- A glibc 2.2.5 não tinha reforços contra a técnica
unlink()de Solar Designer - O ataque sobrescrevia
__free_hookpara desviar o fluxo de execução para o endereço do shellcode no heap - Essa versão do Debian não tinha ASLR nem NX
- Após melhorar o timing, eram necessárias em média cerca de 10.000 tentativas
- Com
MaxStartups=10eLoginGraceTime=600, levava em média cerca de uma semana para chegar a um shell root remoto
- O alvo era
-
Ubuntu 6.06.1, OpenSSH 4.2p1
- O alvo era
SSH-2.0-OpenSSH_4.2p1 Debian-7ubuntu3, um ambiente de 2006 do Ubuntu 6.06.1 - Foi a última versão do Ubuntu ainda vulnerável ao CVE-2006-5051
- Na glibc 2.3.6, funções da família malloc pegam um lock obrigatório ao entrar, então interromper um malloc e tentar explorar outra chamada de malloc acaba em deadlock
- O caminho final de exploração usou PAM
pam_start()define o ponteiro globalsshpam_handledosshd- Se
_pam_add_handler()for interrompida, um camponextnão inicializado pode permanecer - Quando
pam_end()é chamado a partir do manipulador deSIGALRM, um ponteiro arbitrário pode ser passado afree()
- Como a técnica antiga de
unlink()da glibc já estava bloqueada, foi usada a versão fastbin de House of Mind, do Malloc Maleficarum - A fake arena era apontada para
.got.pltdosshd, e a entrada de_exit()era sobrescrita com o endereço do shellcode no heap - O heap desse Ubuntu era executável por padrão
- Em média, também eram necessárias cerca de 10.000 tentativas
- Com
MaxStartups=10eLoginGraceTime=120, levava em média cerca de 1 a 2 dias para obter um shell root remoto - Um atacante azarado podia travar em deadlock todas as 10 conexões de
MaxStartupsantes de conseguir o shell root
- O alvo era
Experimento com Debian 12.5.0 e OpenSSH 9.2p1
-
syslog()e o caminho de malloc da glibc- O alvo era
SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u2, o ambiente stable atual de 2024 do Debian 12.5.0 - Esse ambiente é vulnerável à regressão do CVE-2006-5051
- Nessa versão, o manipulador de
SIGALRMnão chamapacket_close()nempam_end(), e segue pelo caminho desyslog()grace_alarm_handler()chamasigdie()sigdie()chamasyslog()passando porsshlogv()edo_log()
- O
syslog()da glibc 2.36 do Debian chama malloc na primeira execução- No caminho de
__localtime64_r(), é chamado__tzfile_read() fopen()chamamalloc(304)para a estrutura FILE- Outro
malloc(4096)também é chamado para o buffer interno de leitura
- No caminho de
- Desde outubro de 2017, o malloc da glibc não pega lock obrigatório em cenários single-threaded
- Em processos single-threaded como o
sshd, isso abre a possibilidade de explorar disputa em malloc
- O alvo era
-
Condições de ASLR e limitações do i386
- O ambiente i386 do Debian 12.5.0 tem uma fraqueza de ASLR
- PIE do
sshd, heap, a maior parte das bibliotecas e a stack normalmente são randomizados - A própria glibc, porém, é sempre mapeada em
0xb7200000ou0xb7400000 - É possível acertar o endereço da glibc com probabilidade de 50%
- O exploit assume que a glibc está mapeada em
0xb7400000 - Esse endereço era um pouco mais comum do que
0xb7200000
-
Inconsistência de heap e exploração da estrutura FILE
- O caminho de malloc escolhido é o caminho de split que divide um grande free chunk em dois
- Isso gera o chunk retornado e o chunk remainder
- Se
SIGALRMinterromper depois que o chunk remainder entrar na unsorted list, mas antes da inicialização do campo size, surge a inconsistência no heap - O atacante controla o campo size do chunk remainder usando dados residuais de uma alocação anterior no heap
- O chunk remainder fica maior do que deveria e passa a se sobrepor a outros chunks do heap
- Quando o malloc dentro do manipulador de
SIGALRMusa esse chunk, a memória do heap é corrompida - O alvo é a estrutura FILE que
fopen()aloca no heap dentro de__tzfile_read()- Com a corrupção limitada de heap, é sobrescrito 1 byte do
_vtable_offsetda estrutura FILE - Isso faz funções libio da glibc procurarem o ponteiro de vtable em um offset diferente da posição padrão
- O atacante controla nesse local o fake vtable pointer e o ponteiro
_codecvtusando dados residuais de alocações anteriores no heap
- Com a corrupção limitada de heap, é sobrescrito 1 byte do
- Na glibc i386, essa técnica permite chamar um ponteiro de função arbitrário
__fctdurante__fread_unlocked() - A glibc amd64 aparentemente não usa
_vtable_offset
-
Layout do heap e 27 janelas de corrida
- Como uma única pequena race window não bastava para vencer a condição de corrida, foram criados 27 pares de buracos grandes e pequenos
- 28 pares ultrapassariam
PACKET_MAX_SIZEde 256 KB - O pacote final força a sequência
malloc(~4KB), malloc(304), malloc(~4KB), malloc(304) - Se o split de cada large hole for interrompido no momento certo, o
fopen()do manipulador deSIGALRMaloca a estrutura FILE no small hole correspondente - Para montar o layout do heap, foi usado o código de parsing de chave pública do
sshd- Os caminhos
cert_parse()ecert_free()executam sequências demalloc()efree()quase arbitrárias - Como não foi encontrado memory leak, chunks da tcache foram usados como barrier chunks temporários
- Os caminhos
- Foram enviados cinco tipos de pacotes de chave pública
- a/ faz com que alocações de heap não controláveis entrem em chunks da tcache
- b/ cria 27 pares de large/small hole e barrier chunks
- c/ grava fake header, fake footer, fake vtable e ponteiro
_codecvt - d/ aloca e libera uma string grande de quase 256 KB para mover os holes da unsorted list para os malloc bins apropriados
- e/ força a sequência final de malloc e abre 27 pequenas race windows
-
Estratégia de timing para versões recentes
- A estratégia de timing baseada em feedback usada em versões antigas não funcionou contra o OpenSSH 9.2p1
- O quinto parsing de chave pública levava cerca de 10 ms, deixando a large race window grande demais
user_specific_delay(), introduzido no OpenSSH 7.8p1, atrasa a resposta em até cerca de 9 ms e quebra o feedback anterior- A nova estratégia compara tempos de resposta de dois tipos de erro intencional
- É enviado um pacote que provoca erro logo antes do parsing da chave pública
- É enviado outro pacote que provoca erro logo depois do parsing da chave pública
- A diferença entre os dois tempos de resposta mede o tempo do parsing final da chave pública
- Com essa estratégia, a condição de corrida foi vencida em média após cerca de 10.000 tentativas
- Com
MaxStartups=100eLoginGraceTime=120, levava em média cerca de 3 a 4 horas para vencer a condição de corrida - Por causa do ASLR, eram necessárias em média de 6 a 8 horas para chegar ao shell root remoto
Progresso do exploit em amd64
- O alvo amd64 foi o Rocky Linux 9
- A imagem alvo foi
Rocky-9.4-x86_64-minimal.iso - O OpenSSH 8.7p1 é vulnerável a essa condição de corrida no manipulador de sinais
- Como a glibc é mapeada em múltiplos de 2 MB por causa de uma fraqueza de ASLR, partial pointer overwrite se torna mais poderoso
- A imagem alvo foi
- O
syslog()da glibc 2.34 do Rocky Linux 9 chama internamente__open_memstream()- Isso faz
malloc()de uma estrutura FILE no heap calloc(),realloc()efree()também são chamados, oferecendo mais possibilidades
- Isso faz
- Com base no primitive de corrupção de heap, nas duas estruturas FILE alocadas no heap e nos 21 bits fixos do endereço da glibc, acredita-se que a exploração também seja possível em amd64
- O tempo esperado deve ser maior que as 6 a 8 horas do i386, mas a expectativa é ficar abaixo de uma semana
- Há também uma observação separada sobre o Ubuntu 24.04
- O Ubuntu 24.04 não rerandomiza o ASLR do processo filho do
sshd; ele é randomizado só uma vez no boot - A causa foi atribuída ao
systemd-socket-activation.patch, que desativarexec_flag - Em geral isso não é uma boa escolha, mas nesta vulnerabilidade impede a exploração porque o
syslog()dentro do manipulador deSIGALRMnão é a primeira chamada asyslog(), então não chama funções malloc - Patch relacionado: https://git.launchpad.net/ubuntu/+source/…
- O Ubuntu 24.04 não rerandomiza o ASLR do processo filho do
Patch e mitigação
- O OpenSSH corrigiu essa condição de corrida em 6 de junho de 2024 com o commit
81c1099- 81c1099: adiciona ao
sshd(8)um recurso para penalizar comportamento problemático de clientes - O código async-signal-unsafe foi movido do manipulador de
SIGALRMdosshdpara o processo listener, onde é tratado de forma síncrona
- 81c1099: adiciona ao
- Essa correção depende do grande commit
81c1099e do ainda maior commit de defense-in-depth03e3de4, o que pode dificultar o backport - Se o backport for difícil, é possível remover ou comentar o código async-signal-unsafe em
sshsigdie()para que apenas_exit(1)seja chamado - Se atualizar ou recompilar não for possível, pode-se definir
LoginGraceTimecomo0no arquivo de configuração- Essa configuração bloqueia a execução remota de código descrita neste advisory
- Em compensação, continua vulnerável a DoS por exaustão de todas as conexões
MaxStartups
Cronograma de divulgação
- 2024-05-19: contato com os desenvolvedores do OpenSSH, seguido por iterações de patch e revisão
- 2024-06-20: contato com
distros@openwall - 2024-07-01: divulgação na data coordenada de release
1 comentários
Opiniões do Hacker News
Curiosamente, a correção do RCE parece ter sido “misturada” publicamente quase um mês antes
Quando PerSourcePenalties está ativado, o sshd(8) monitora o status de encerramento dos processos-filhos de sessão antes da autenticação e registra condições como falhas repetidas de autenticação ou crashes do sshd como uma penalidade temporária para o endereço do cliente
https://github.com/openssh/openssh-portable/commit/81c1099d2...
Em vez de ser um patch que possa ser analisado por engenharia reversa e diga algo ao atacante, parece bastante engenhoso, pois aparentemente altera a estrutura do binário, eliminando uma vulnerabilidade específica e, como efeito colateral, também mitigando toda essa família de exploits
A mudança acima era um recurso já anunciado para lidar com conexões lixo, e apenas mitiga também esta vulnerabilidade ao tornar mais difícil vencer a condição de corrida
Discussão anterior: https://news.ycombinator.com/item?id=40610621
Fico me perguntando se as pessoas não leem só o primeiro comentário da thread, dão upvote e vão embora com uma impressão equivocada
Um trecho das notas de lançamento do OpenSSH é interessante
“A exploração bem-sucedida foi demonstrada em sistemas Linux/glibc de 32 bits com ASLR ativado. Em condições de laboratório, o ataque exige manter conexões contínuas no limite máximo permitido pelo servidor por, em média, 6 a 8 horas. Acredita-se que também seja possível em sistemas de 64 bits, mas isso ainda não foi demonstrado. É provável que esses ataques sejam aprimorados.”
https://www.openssh.com/releasenotes.html
Olhando o diff [1] que introduziu o bug, pela análise o problema foi que, na refatoração de
sigdie(), que era envolvido por#ifdef DO_LOG_SAFE_IN_SIGHAND, parasshsigdie(), que chamasshlogv()diretamente, o #ifdef foi removidoO que poderia ter evitado isso? Mais gente deveria ter revisado o pull request? É surpreendente que um software do qual o mundo inteiro depende para acesso seguro pareça, na prática, ser mantido por duas pessoas [2]
[1] https://github.com/openssh/openssh-portable/commit/752250caa...
[2] https://github.com/openssh/openssh-portable/graphs/contribut...
Neste caso, um comentário explicando por que o
#ifdefera necessário poderia ter ajudado. Algo como: “o código aqui deve ser seguro para sinais assíncronos, e o estado dos locks pode ser indeterminado”Dito isso, honestamente,
getrlimittambém não está nesta lista: https://man7.org/linux/man-pages/man7/signal-safety.7.htmlAinda assim, se um código com comentários sobre segurança em sinais assíncronos tivesse sido removido ou alterado, isso talvez chamasse atenção na revisão. No código citado, apenas algo como
SAFE_IN_SIGHANDsugere que esse código precisa ser seguro dentro de um manipulador de sinalsyslogreentrante e segura para sinais assíncronos, é possível que o autor desse código tenha simplesmente presumido que a alteração era seguraTalvez tenha esquecido, ou não soubesse, que em outras plataformas que os desenvolvedores do ssh do OpenBSD não afirmam de fato oferecer suporte ainda são usadas funções que não são seguras para sinais assíncronos
Você não tem direito de receber algo de desenvolvedores open source. Eles também podem cometer erros e podem decidir por conta própria quantos mantenedores ou revisores terão
https://gist.github.com/richhickey/1563cddea1002958f96e7ba95...
#ifdefsem justificativa, como em [1]As notas de lançamento também valem a leitura: https://www.openssh.com/releasenotes.html
Na verdade, isto é uma variante de um bug interessante de condição de corrida de sinais. Segundo o relatório da vulnerabilidade, “o OpenBSD, em particular, não é vulnerável, porque o manipulador de SIGALRM chama
syslog_r(), uma versão desyslog()mais segura para sinais assíncronos que o OpenBSD criou em 2001”Ou seja, uma mitigação de segurança de sinais levou os desenvolvedores do OpenBSD a colocar código não trivial dentro de um manipulador de sinais, e esse código, ao ser portado para outros sistemas, tornou-se inseguro. Se tivesse sido feita uma refatoração para minimizar o código dentro do manipulador de sinais, seguindo a sabedoria comum e as convenções de código Unix, esse bug teria sido evitado
Com o tempo, é fácil demais acabar misturando, em algum ponto das chamadas transitivas, uma chamada que não é segura para sinais assíncronos, e nem sempre fica claro que esse caminho é alcançável a partir do contexto de sinal
Depois de atualizar minhas instâncias do OpenSSH, vi que elas estavam linkadas contra musl, não glibc, então fui verificar se o
syslog(3)da musl também faz alocação e, portanto, se seria facilmente explorável da mesma formaPelo que parece, não: https://github.com/bminor/musl/blob/master/src/misc/syslog.c
Tudo ali é alocado na pilha ou são variáveis estáticas com locks para impedir reentrada. As chamadas
{d,sn,vsn}printf()também não alocam na musl, embora talvez possam alocar na glibc. Será que deixei passar algo?Ainda assim, um deadlock dentro de
sigalrmpoderia impedir a limpeza da conexão e levar a uma negação de serviçoSaiu um patch para FreeBSD
Não está claro se ele é afetado. A exploração conhecida só era possível na glibc, e o FreeBSD não usa glibc, mas é melhor prevenir
https://www.freebsd.org/security/advisories/FreeBSD-SA-24:04...
Segundo o relatório, se não for possível atualizar ou recompilar o sshd, apenas definir
LoginGraceTimecomo 0 no arquivo de configuração corrige esta condição de corrida no manipulador de sinaisNesse caso, o sshd fica vulnerável a uma negação de serviço por esgotamento de todas as conexões
MaxStartups, mas fica protegido contra a execução remota de código apresentada neste avisoPortanto, configurar
LoginGraceTime 0emsshd_configparece servir como mitigaçãoEntão isso não seria pior?
Saiu um patch para Debian 12, e o Debian 11 não é afetado
https://security-tracker.debian.org/tracker/CVE-2024-6387
apt updateeupgradeem um servidor Debian 12, e os únicos pacotes atualizados foram os do OpenSSHUma descoberta realmente excelente
Não estou exatamente na posição de quem trabalha diretamente com isso, mas em pesquisa de segurança muitas vezes dá para sentir um clima de que, para “vencer”, não basta encontrar e corrigir um problema isolado, nem receber recompensa por ele: é preciso encontrar a cadeia completa que leve até acesso remoto
Às vezes parece que deveria ser suficiente encontrar um único buraco, por exemplo uma corrupção de memória ou uma fuga de sandbox. Hoje há tantos problemas pequenos que talvez seja necessário demonstrar um hack de ponta a ponta para que as pessoas levem a sério ou paguem um bug bounty
Por exemplo, se um app trava ao receber uma entrada confiável malformada, mas pela natureza do app ele não foi feito para ficar exposto a adversários, e realisticamente isso não aconteceria, a maioria trataria isso apenas como bug, não como bug de segurança. Seria bom corrigir, mas não está no mesmo nível, e esse tipo de coisa nem é tão difícil de encontrar
Por isso é necessário distinguir bugs de segurança “reais”, como este caso, de bugs sem impacto de segurança, e é muito importante demonstrar que o problema é explorável
Como sempre haverá infinitos bugs sem impacto de segurança, não parece que essa exigência de demonstração vá desaparecer tão cedo
Isso é assim por design, e o usuário pode serializar e desserializar qualquer coisa, inclusive funções lambda. Minha biblioteca foi pensada apenas para processar dados de fontes confiáveis
Até onde sei, ninguém usa essa biblioteca para processar dados não confiáveis. Uma biblioteca popular usa a minha biblioteca para ler arquivos de configuração, mas eles consideram esses arquivos de configuração como dados confiáveis. E não é meu trabalho fiscalizar como outras pessoas usam a minha biblioteca
Nesse caso, faz sentido registrar um CVE de prioridade máxima dizendo que meu projeto tem uma vulnerabilidade de execução remota de código?
Recompensas são sempre pagas para a primeira categoria. Relatórios da segunda categoria, sem prova de conceito ou demonstração de explorabilidade, podem até prejudicar a reputação ou o sinal
Fraquezas que só se tornam exploráveis depois que certas condições são atendidas quase sempre existem. Mesmo em competições como a Pwn2Own, é comum encadear várias vulnerabilidades para, no fim, assumir o controle de um dispositivo, e muitas vezes elas permanecem sem patch por anos. Pesquisadores às vezes deixam essas fraquezas guardadas por bastante tempo para maximizar o impacto
É triste, mas essa é a realidade
Notas de lançamento do OpenSSH: https://www.openssh.com/txt/release-9.8
Patch mínimo para quem não pode ou não quer fazer upgrade: https://marc.info/?l=oss-security&m=171982317624594&w=2