Contorno do login do OpenSSH no Alpine e no musl (CVE-2026-123158)

Os patches do php2shell (CVE-2026-109618 e CVE-2026-109619) para o PHP em fim de vida (EOL) já estão disponíveis. Como as respostas do atendimento de vendas estão mais demoradas do que o normal devido à alta demanda, a maneira mais rápida de obter os pacotes corrigidos é adquirir o ELS para Runtimes on-line (abre em uma nova aba). Leia as perguntas frequentes sobre os patches

Vá direto para um resumo gerado por IA desta postagem do blog

  • Resuma esta postagem com o Perplexity
  • Resuma esta postagem com o Gemini

Principais conclusões

  • Uma senha com tamanho entre 257 e 1024 bytes permite o acesso a qualquer conta que utilize apenas chave em sistemas Linux que executem o sshd com a libc musl sem PAM, principalmente no Alpine Linux. O OpenSSH 10.6p2 inclui a solução provisória, e o bug está registrado como CVE-2026-123158.
  • A falha está entre dois projetos. Desde 2016, o sshd utiliza o salt de outra conta para uma conta *, e a função crypt() da biblioteca musl responde a uma senha excessivamente longa com a string “*”, que corresponde ao * armazenado. O OpenSSH afirma que a causa raiz está fora do sshd; o mantenedor da musl considera isso um uso incorreto por parte do chamador.
  • Reproduzimos o problema no Alpine Linux 3.18: na segunda tentativa em uma mesma conexão, o login é bem-sucedido; um hash bcrypt remove o limite de comprimento; e o pacote corrigido bloqueia todas as tentativas. Atualize e reinicie o sshd ou desative o login por senha; no Alpine 3.18 e versões anteriores, apenas a aplicação do arquivo sshd_config.d não surte efeito.
  • O Alpine Linux 3.18 chegou ao fim de vida útil em maio de 2025 e, desta vez, recebeu uma correção como exceção. O Endless Lifecycle Support (ELS) da TuxCare transforma isso em uma promessa: o Alpine 3.18 ELS recebe o mesmo commit do upstream que o openssh 9.3_p2.tuxcare.els7.

Em 5 de novembro de 2026, a equipe do OpenSSH lançou a versão 10.6p2 com uma correção provisória crítica de segurança. Em alguns sistemas Linux, um invasor pode fazer login via SSH em uma conta que nunca deveria aceitar nenhuma senha. Não há senha para adivinhar. O invasor precisa apenas do nome dessa conta. Na configuração mais comum, o invasor envia uma senha errada e, em seguida, uma senha com comprimento entre 257 e 1024 bytes, na mesma conexão. Em algumas configurações, até mesmo uma senha curta funciona, como mostramos a seguir.

A falha está registrada como CVE-2026-123158 (pontuação CVSS 3.1: 8,1). Ela foi identificada por Özgün Kültekin, da Wakefield (wakefield.co). O caminho de código responsável por ela está presente no OpenSSH portátil desde 2016, da versão 7.3 até a 10.6p1.

Essa falha é incomum porque se situa entre dois projetos. O ataque não requer estouro de buffer, travamento ou corrupção de memória. Trata-se de uma falha lógica que só ocorre quando o OpenSSH e a biblioteca C musl são executados juntos no mesmo sistema, e os dois projetos não chegam a um consenso sobre a quem cabe a responsabilidade pela falha. As notas de lançamento do OpenSSH explicam da seguinte forma.

“Consideramos que a causa principal desse bug esteja fora do sshd, mas ele pode ser explorado por meio do sshd e pode ser evitado nesse contexto.”

O mantenedor do musl, Rich Felker, vê a questão de outra maneira.

“Minha opinião é que isso não é um bug no musl, mas apenas um uso incorreto por parte de quem chamou a função.”

Nesta postagem, analisamos o código dos dois projetos, mostramos passo a passo como ocorre o login e reproduzimos o ataque ao Alpine Linux em nosso laboratório.

Quem é afetado

O ataque só funciona quando todas essas condições se aplicam no mesmo servidor.

  • O sshd está vinculado à libc musl. O Alpine Linux é o caso mais conhecido e também serve de base para muitas imagens do Docker, mas apenas as imagens que executam o sshd estão expostas. O bug não está restrito ao Alpine. Qualquer sistema ou dispositivo Linux que execute um sshd compilado com musl e sem PAM pode ser afetado, incluindo firmwares personalizados e binários do sshd compilados pelo próprio usuário.
  • O sshd não utiliza o PAM (a estrutura de autenticação que a maioria das distribuições Linux usa para logins) para verificar senhas. No Alpine, o pacote padrão openssh-server é compilado sem o PAM. O pacote openssh-server-pam, executado com a opção UsePAM desativada, também é afetado.
  • O login por senha está habilitado com a configuração `PasswordAuthentication yes`, que é a configuração padrão.
  • A conta em questão tem um único * como senha. O caso típico é um usuário que usa apenas chave, com * no arquivo /etc/shadow. No Alpine sem PAM, uma conta com ! não consegue fazer login, mesmo com uma chave SSH; por isso, os administradores alteram ! para * para que o login por chave funcione. O usuário root na imagem base do Alpine também tem *, desde aproximadamente a versão 3.16 do Alpine, mas o sshd bloqueia o login por senha para o root, a menos que a opção `PermitRootLogin yes` esteja definida. Usuários provenientes de um serviço de diretório por meio do musl-nscd não possuem entrada no arquivo shadow e também podem acabar com *.
  • A primeira conta na ordem do arquivo /etc/passwd cujo hash comece com $ deve usar um tipo de hash no qual o musl possa falhar. Com um hash normal $5$ ou $6$ (SHA-256 ou SHA-512 crypt), o invasor precisa de 257 a 1024 bytes, e o arquivo passwd do Alpine grava $6$ por padrão. Com um hash bcrypt ($2a$, $2b$ ou $2y$), qualquer senha não vazia de até 1.024 bytes funciona. Com um hash antigo $1$ (MD5), um hash yescrypt $y$ ou sem nenhum hash $, o ataque não funciona.

Essa vulnerabilidade não existe nesses sistemas.

  • Sistemas que não sejam Linux.
  • Sistemas Linux com glibc, como RHEL, AlmaLinux, CloudLinux, Debian ou Ubuntu.
  • Sistemas Linux nos quais o sshd verifica senhas por meio do PAM.

As contas bloqueadas com o símbolo ! em vez de * também estão seguras, assim como os servidores com a opção PasswordAuthentication definida como “no”. Se você precisar apenas das etapas, pule para“O que fazer agora”, no final do texto.

O que significa uma única asterisco no arquivo /etc/shadow

No Linux, os hashes das senhas ficam no arquivo /etc/shadow. Um hash é uma forma codificada unidirecional da senha. Quando você faz login, o sistema gera o hash do que você digitou da mesma maneira e compara os dois resultados. Se forem iguais, você consegue acessar o sistema.

Uma entrada normal tem a seguinte aparência:

alice:$6$Qx7Lm2Pa$k3J9...long hash...:20000:0:99999:7:::

O “$6” no início indica a criptografia SHA-512, um método de hash de senha baseado no SHA-512. A parte seguinte, Qx7Lm2Pa, é o salt. Um salt é uma pequena sequência aleatória incorporada à senha, de modo que dois usuários com a mesma senha ainda obtenham hashes diferentes.

Desde por volta de 1980, quando os hashes ainda estavam armazenados em /etc/passwd, os administradores do Unix colocavam um único * nesse campo quando uma conta não deveria, em hipótese alguma, fazer login com uma senha. A lógica é simples. Nenhuma senha no mundo gera um hash que resulte apenas em *, pois um hash real é sempre muito mais longo. Assim, a conta ainda pode fazer login com uma chave SSH, mas nunca com uma senha. O bug em questão consiste justamente em burlar essa ideia.

Como o sshd verifica uma senha sem o PAM

Quando o PAM está desativado, o sshd verifica a senha por conta própria na função sys_auth_passwd() no arquivo auth-passwd.c. Este é o código do OpenSSH 9.3p2, a versão fornecida com o Alpine Linux 3.18, antes da correção.

/* Just use the supplied fake password if authctxt is invalid */
char *pw_password = authctxt->valid ? shadow_pw(pw) : pw->pw_passwd;
...
/*
 * Encrypt the candidate password using the proper salt, or pass a
 * NULL and let xcrypt pick one.
 */
if (authctxt->valid && pw_password[0] && pw_password[1])
    salt = pw_password;
encrypted_password = xcrypt(password, salt);

/*
 * Authentication is accepted if the encrypted passwords
 * are identical.
 */
return encrypted_password != NULL &&
    strcmp(encrypted_password, pw_password) == 0;

Essa é a maneira clássica. Leia o hash armazenado, gere o hash da senha digitada usando o hash armazenado como “salt” e compare as duas sequências de caracteres.

Agora observe a linha do salt. O sshd usa o valor armazenado como salt somente quando ele tiver pelo menos dois caracteres. Um * tem apenas um. Portanto, para uma conta com *, o salt permanece NULL, e a função xcrypt() precisa escolher um salt por conta própria.

A mudança de 2016

Por que a função xcrypt() escolhe um salt? A resposta está no CVE-2016-6210. Em 2016, um pesquisador demonstrou que o sshd respondia em velocidades diferentes para usuários que não existiam. Ao enviar senhas longas e medir o tempo de resposta, um invasor poderia criar uma lista de nomes de usuários reais.

Para preencher essa lacuna, Darren Tucker modificou o sshd de modo que, quando não houver um salt utilizável, ele utilize o tipo de hash e o salt de uma conta real no sistema. O primeiro commit, 9286875a “Determinar o salt apropriado para usuários inválidos”, utilizou o salt do usuário root. Seis dias depois, o commit dbf788b4, “Buscar usuários com um salt válido”, fez com que ele pesquisasse todas as contas. Ambos foram incluídos no OpenSSH 7.3 em agosto de 2016. A alteração foi pensada para usuários que não existem. Mas a verificação de dois caracteres mencionada acima direciona uma conta válida com * pelo mesmo caminho, de modo que ela também recebe o salt emprestado.

Esta é a função pick_salt() no arquivo openbsd-compat/xcrypt.c, conforme aparece na versão 9.3p2.

if (salt[0] != '\0')
    return salt;
strlcpy(salt, "xx", sizeof(salt));
setpwent();
while ((pw = getpwent()) != NULL) {
    if ((passwd = shadow_pw(pw)) == NULL)
        continue;
    if (passwd[0] == '$' && (p = strrchr(passwd+1, '$')) != NULL) {
        typelen = p - passwd + 1;
        strlcpy(salt, passwd, MINIMUM(typelen, sizeof(salt)));
        explicit_bzero(passwd, strlen(passwd));
        goto out;
    }
}
out:
endpwent();
return salt;

Ele percorre as contas na ordem em que aparecem no arquivo /etc/passwd e utiliza o primeiro salt $…$ que encontrar, por exemplo, $6$Qx7Lm2Pa da conta alice. Ele armazena o resultado; assim, tentativas posteriores na mesma conexão utilizam o salt armazenado em cache. Se nenhuma conta tiver um hash $, ele recorre ao xx, o antigo tipo DES.

Antes dessa alteração, o sshd usava “xx” para entradas curtas como *. O DES antigo não falha com senhas longas; ele simplesmente usa os primeiros 8 caracteres, por isso nunca poderia retornar *. Após a alteração, uma conta * recebe o salt de outro usuário. Foi assim que o problema começou em 2016. A alteração foi uma boa solução para um problema real de tempo e, pelo que se sabe, ninguém percebeu o efeito colateral.

O que a função `crypt()` do musl retorna em caso de erro

O musl é uma pequena biblioteca em C utilizada pelo Alpine e por muitos sistemas embarcados. Seu código de criptografia SHA-512 está localizado em src/crypt/crypt_sha512.c. Ele limita o comprimento da senha para proteger o sistema contra solicitações lentas e que sobrecarregam a CPU.

/* key limit is not part of the original design, added for DoS protection.
 * rounds limit has been lowered (versus the reference/spec), also for DoS
 * protection. runtime is O(klen^2 + klen*rounds) */
#define KEY_MAX 256
...
    /* reject large keys */
    for (i = 0; i <= KEY_MAX && key[i]; i++);
    if (i > KEY_MAX)
        return 0;

Uma senha com mais de 256 bytes é rejeitada. Agora veja o que a função __crypt_sha512(), a função interna chamada pela função crypt(), retorna nesse caso.

p = sha512crypt(key, setting, output);
/* self test and stack cleanup */
q = sha512crypt(testkey, testsetting, testbuf);
if (!p || q != testbuf || memcmp(testbuf, testhash, sizeof testhash))
    return "*";
return p;

Em caso de erro, a função crypt() do musl retorna a string “*”. Os códigos do SHA-256, MD5 e Blowfish fazem o mesmo, e apenas o SHA-256 e o SHA-512 têm o limite de 256 bytes. O musl funciona assim desde 2012. A norma POSIX determina que a função crypt() deve retornar um ponteiro nulo e definir errno em caso de erro, mas o musl optou propositalmente por uma string. Seu commit b3c4cc12 explica que “uma grande parte dos programas que chamam a função crypt não está preparada para sua falha e não verifica se o valor de retorno é diferente de nulo”, e denomina o resultado de “hash incomparável”.

O problema é que * também é o valor que significa “login sem senha” no arquivo /etc/shadow.

O musl realmente preservou o padrão clássico. Observe a última linha do código DES no arquivo crypt_des.c.

return (setting[0]=='*') ? "x" : "*";

Se o próprio salt começar com *, o musl retorna x em vez disso. Portanto, crypt(password, “*”) nunca retorna “*”. Isso protege a verificação clássica, na qual um programa usa o valor armazenado como salt e, em seguida, compara o resultado com esse mesmo valor armazenado.

Mas o sshd não utilizou o valor armazenado como salt. Ele utilizou o salt da Alice. O musl detectou $6$…, portanto, essa proteção não adiantou nada.

Um pequeno teste

Nós mesmos verificamos a parte da biblioteca C com um pequeno programa de teste. Estas são suas duas linhas principais, simplificadas. Ele gera o hash de senhas compostas apenas pela letra A usando um salt de $6$ emprestado e compara o resultado com *.

r = crypt(long_password, "$6$abcdefgh");  /* salt borrowed from another account */
ok = (r != NULL && strcmp(r, "*") == 0);  /* compare with the "*" in /etc/shadow */

Executamos o programa no Alpine Linux 3.18 (musl) e no Debian 12. No Debian, assim como na maioria dos sistemas modernos com glibc, a função crypt() vem de uma biblioteca separada chamada libxcrypt.

== musl (Alpine 3.18) ==
len=   8 crypt()=$6$abcdefgh$65STptHV...  match "*": no
len= 256 crypt()=$6$abcdefgh$9SA4XFln...  match "*": no
len= 257 crypt()=*  match "*": YES
len=1024 crypt()=*  match "*": YES
== libxcrypt (Debian 12) ==
len=   8 crypt()=$6$abcdefgh$65STptHV...  match "*": no
len= 256 crypt()=$6$abcdefgh$9SA4XFln...  match "*": no
len= 257 crypt()=$6$abcdefgh$D4Ayo9g5...  match "*": no
len=1024 crypt()=*0  match "*": no

No musl, toda senha a partir de 257 bytes corresponde ao asterisco. Com a libxcrypt, uma senha de 257 bytes é submetida ao hash normalmente. A libxcrypt tem seu próprio limite de 511 bytes, mas, quando falha, retorna *0, ou *1 quando o salt começa com *0. Uma sequência de erro de dois caracteres nunca pode ser igual a um único caractere *, portanto a comparação falha, como deveria.

Todo o processo, desde a senha longa até o login

Aqui está a sequência completa de ações para um invasor que tenha como alvo um usuário com apenas chave em um servidor Alpine. Esse usuário tem * no arquivo /etc/shadow, e o primeiro hash $ no sistema é o hash $6$ de outro usuário.

  1. O invasor se conecta via SSH e solicita o login por senha como usuário que utiliza apenas chave.
  2. O invasor envia uma senha de 300 bytes. O sshd aceita senhas de até 1.024 bytes (MAX_PASSWORD_LEN), portanto, o processo continua.
  3. O sshd lê * do arquivo /etc/shadow. Como há apenas um caractere, não há salt.
  4. A função pick_salt() utiliza o “salt” $6$ do outro usuário.
  5. O código SHA-512 do musl detecta mais de 256 bytes e falha. A função crypt() retorna “*”.
  6. Nessa primeira tentativa, o sshd já havia perdido o * armazenado, por um motivo secundário que explicaremos a seguir, de modo que a comparação falha. O invasor simplesmente tenta novamente na mesma conexão.
  7. Na segunda tentativa, o sshd compara “*” com o “*” armazenado. Eles são iguais. O login foi aceito.

É por isso que a janela de ataque varia de 257 a 1.024 bytes quando o salt emprestado é $5$ ou $6$. Abaixo de 257, o musl faz o hash da senha normalmente. Acima de 1.024, o sshd a rejeita antes de fazer o hash. O texto do CVE diz “entre 256 e 1.024 caracteres”, mas a janela real começa em 257 bytes.

Com o bcrypt, o comprimento não importa

Existe mais uma vulnerabilidade, e ela não consta no comunicado da Alpine nem no CVE. Nós a descobrimos ao verificar o código. A função `pick_salt()` corta o hash no último `$`. Um hash bcrypt tem o formato `$2b$10$` seguido por 53 caracteres, sem nenhum `$` entre o salt e o hash. Assim, o sshd mantém apenas `$2b$10` e perde o próprio salt. O musl rejeita essa configuração e, em caso de erro, retorna “*” novamente. Desta vez, o comprimento da senha não importa.

O Upstream corrigiu esse bug relacionado ao salt em 30 de outubro de 2026, no commit 84f8f466, intitulado “correção da geração de salt falso para senhas em hashes bcrypt”.

+            /*
+             * bcrypt ($2*$NN$) has no '$' between its 22 character
+             * salt and the hash, so keep the salt too or crypt(3)
+             * will reject the setting without doing any work.
+             */
+            if (passwd[1] == '2' && strlen(p + 1) >= 22)
+                    typelen += 1 + 22;

A mensagem do commit menciona um problema de temporização, mas no musl o efeito é muito pior. Esse commit está no branch master e não faz parte da versão 10.6p2. A solução alternativa da versão 10.6p2 ainda bloqueia esse caminho, pois um * tem menos de 4 caracteres.

Testamos isso no Alpine 3.18 com um hash bcrypt como o primeiro hash $ do sistema. No pacote antigo, todas as senhas não vazias, de 1 a 1.024 bytes, conseguiram acessar a conta * já na segunda tentativa. O pacote corrigido bloqueou todas elas. O comando `passwd` do Alpine grava $6$ por padrão; portanto, esse caminho precisa de um hash bcrypt que um administrador ou uma ferramenta tenha inserido ali.

Imagens de nuvens alpinas

O comunicado da Alpine identifica o usuário “alpine” nas imagens oficiais do tiny-cloud das versões 3.18.11 a 3.24.3. Esse usuário possui *. Uma imagem recém-criada não fica exposta, pois nenhuma conta possui ainda um hash de senha real. Ela fica exposta assim que alguma conta recebe um hash $6$ enquanto o login por senha estiver ativado. As imagens configuradas por meio do `cloud-init`, que define `PasswordAuthentication` como `no`, não são afetadas. O pesquisador observa que, nas imagens oficiais da AWS e do Azure, o usuário `alpine` pode executar o `doas` (equivalente do `sudo` no Alpine) sem senha; portanto, fazer login como `alpine` nessas plataformas equivale a fazer login como `root`.

Nós o reproduzimos no Alpine

O teste crypt() demonstra a parte da biblioteca C. Também queríamos verificar se um sshd real nos permitiria acessar o sistema, então montamos um pequeno ambiente de teste.

Utilizamos duas imagens:

  • Alpine Linux 3.18 com o openssh-server 9.3_p2-r3, o último pacote do Alpine antes da correção.
  • Alpine Linux 3.18 com o openssh-server 9.3_p2-r4, o pacote corrigido.

Em ambos os casos, atribuímos a uma conta uma senha real usando a própria ferramenta `passwd` do Alpine. Em seguida, adicionamos uma conta com apenas uma chave, representada por *, e uma conta bloqueada, representada por !, e tentamos fazer login com senhas de tamanhos diferentes. “LOGGED IN” significa que a segunda tentativa na mesma conexão foi aceita e que o sshd executou um comando para nós como aquele usuário.

                                   Alpine 3.18     Alpine 3.18
                                   9.3_p2-r3       9.3_p2-r4
                                   (before fix)    (fixed)
key-only (*), 8 bytes              denied          denied
key-only (*), 256 bytes            denied          denied
key-only (*), 257 bytes            LOGGED IN       denied
key-only (*), 1024 bytes           LOGGED IN       denied
key-only (*), 1025 bytes           denied          denied
locked (!), 300 bytes              denied          denied
root (*), default config           denied          denied
root (*), PermitRootLogin yes      LOGGED IN       denied
key-only (*), 1 byte, with bcrypt  LOGGED IN       denied
real password                      accepted        accepted

No pacote corrigido, todos os ataques falharam, enquanto a senha real continuou funcionando como antes. O cliente SSH normal envia no máximo 1.023 bytes; portanto, uma senha digitada mais longa é simplesmente cortada na janela de ataque. Para as linhas de 1.024 e 1.025 bytes, usamos um pequeno cliente que envia o comprimento exato. Com 1.025 bytes, o sshd rejeitou a senha antes mesmo de aplicá-la ao hash. Para a linha bcrypt, o primeiro hash $ no sistema era um hash $2b$ em vez de $6$.

Por que cada ataque precisava de duas tentativas

Em todas as execuções vulneráveis da tabela, a primeira tentativa de senha falhou e a segunda tentativa na mesma conexão funcionou. A primeira tentativa pode ser qualquer senha errada de até 1.024 bytes. Confirmamos o motivo com um pequeno teste que registra o que o sshd passa para a função strcmp(). Na primeira tentativa, o sshd comparou “*” com o que quer que estivesse no endereço do * armazenado; em nosso laboratório, uma string vazia. Na segunda tentativa, ele comparou “*” com “*”.

A causa é um problema de buffer no sshd. A função shadow_pw() retornou um ponteiro para o buffer que a função getspnam() do musl reutiliza a cada consulta. Na primeira tentativa, a função pick_salt() é executada, o que consulta outras contas nesse mesmo buffer e, em seguida, apaga o hash “emprestado” com a função explicit_bzero(). Assim, o * para o qual o sshd ainda estava apontando desaparece. A partir da segunda tentativa, o salt é armazenado em cache, nenhuma outra consulta ocorre e * corresponde a *. A solução alternativa da versão 10.6p2 também corrige isso, conforme mostramos na seção de soluções alternativas.

A segunda tentativa só é necessária quando o * vem do arquivo /etc/shadow e o sshd é executado com as configurações padrão. Em nosso laboratório, uma conta com * no arquivo /etc/passwd e sem entrada no /etc/shadow foi aceita logo na primeira tentativa. Com a opção PermitEmptyPasswords definida como “yes” — que permite que contas com senha vazia façam login —, mesmo um * no arquivo /etc/shadow foi aceito na primeira tentativa de senha.

É provável que os usuários de serviços de diretório se comportem da mesma forma que no caso do arquivo /etc/passwd, mas não testamos isso. Isso se encaixa em ambas as fontes públicas. O comunicado da Alpine afirma que o ataque requer uma única conexão e não precisa de tentativas repetidas, enquanto a página do pesquisador indica que um * no arquivo shadow exige uma tentativa incorreta primeiro.

O sshd permite, por padrão, 6 tentativas por conexão (MaxAuthTries); portanto, a tentativa extra não impede um invasor. No caso do arquivo /etc/shadow, isso resulta em uma tentativa malsucedida e uma tentativa aceita a partir da mesma porta de conexão.

Failed password for keyonly from 127.0.0.1 port 53734 ssh2
Accepted password for keyonly from 127.0.0.1 port 53734 ssh2

Então, trata-se de um bug do OpenSSH ou do musl?

A equipe do OpenSSH afirma que a causa principal está fora do sshd, e há bons argumentos para isso. A verificação clássica, usada há décadas, calcula o hash da senha digitada usando o valor armazenado como “salt” e compara o resultado com esse mesmo valor. O musl mantém esse padrão seguro, pois sua string de erro nunca é igual ao “salt” que lhe foi fornecido. Em sistemas glibc, o mesmo código do sshd também é seguro.

Há também um argumento contrário. Desde 2016, o sshd calcula o hash de uma entrada de um caractere usando o salt de outra conta e ainda compara o resultado com o valor armazenado, além de manter um ponteiro para um buffer da biblioteca C que sofreu alterações por baixo dele. A proteção do musl não pôde resolver esse problema. Rich Felker vê a situação dessa forma e chama isso de “um uso incorreto por parte do chamador”. Ele também escreveu que o mecanismo por trás disso “muito provavelmente será alterado no próximo ciclo de lançamento” do musl.

Então, esse bug precisava de três partes, e cada uma delas parece estar correta por si só:

  • O * indica uma conta que não exige senha para login, um costume do Unix que remonta a por volta de 1980, quando os hashes ainda estavam armazenados em /etc/passwd.
  • A sequência de erro “*” e o limite de 256 bytes no musl, de 2012.
  • O “salt” emprestado no sshd, de 2016.

Nenhum projeto isoladamente é responsável por todo o bug, e isso ajuda a explicar por que ele permaneceu oculto por 10 anos. É preciso observar como as partes se comportam em conjunto, e isso só fica evidente em um sistema musl sem PAM. O pesquisador descobriu o bug ao compilar o sshd com diferentes bibliotecas C e configurações de PAM e ao testar milhares de tentativas de login incomuns.

A correção no OpenSSH

Damien Miller escreveu a solução alternativa. No branch V_10_6, trata-se do commit 90f9c2f7, intitulado “evitar a contornagem da autenticação por senha”, e no branch master, a mesma alteração corresponde ao commit 4353fdc4. A parte principal é bem pequena.

+#define MIN_PASSWORD_HASH_LEN    4
 #define MAX_PASSWORD_LEN 1024
...
-    return encrypted_password != NULL &&
+    ret = encrypted_password != NULL &&
+        strlen(pw_password) >= MIN_PASSWORD_HASH_LEN &&
         strcmp(encrypted_password, pw_password) == 0;
+    freezero(pw_password, strlen(pw_password));
+    return ret;

Um hash de senha real nunca tem menos de 4 caracteres. Ora, se o valor armazenado for *, !, x ou qualquer outro caractere tão curto, a comparação de hash sempre falhará, independentemente do que a função crypt() retornar. Isso bloqueia todas as abordagens apresentadas neste post, incluindo a do bcrypt. O caso específico de senhas vazias, controlado pela variável PermitEmptyPasswords, não sofre alterações.

O patch também altera a função shadow_pw() para que ela retorne uma cópia da entrada do shadow, criada com xstrdup(), e apague essa cópia após o uso. Isso elimina o problema de buffer por trás das duas tentativas.

Na lista openssh-unix-dev, Damien Miller explicou essa escolha. Ele escreveu que “estamos em boa posição para implementar uma medida de mitigação rapidamente”. No momento da redação deste artigo, o musl ainda retorna “*” em caso de erro, mas seu mantenedor espera alterar esse mecanismo no próximo ciclo de lançamento.

O que fazer agora

Atualizar e reiniciar o sshd

No Alpine, atualize e reinicie desta forma. Este é o comando indicado no comunicado do Alpine. O sinalizador -a também permite que o apk substitua ou reverta versões de pacotes para que fiquem em conformidade com os repositórios; portanto, verifique o que ele altera.

apk upgrade -U -a
rc-service sshd restart

O OpenSSH 9.3p2 inicia uma nova instância do /usr/sbin/sshd para cada nova conexão; portanto, em nosso laboratório, os novos logins passaram a utilizar o código corrigido assim que o pacote foi instalado. Reinicie o sshd de qualquer maneira, para que o processo de escuta também execute o novo binário. Em um contêiner, recompile a imagem a partir de uma base corrigida e reinicie o contêiner. Clientes do TuxCare ELS no Alpine Linux 3.18 obtêm a correção no repositório do ELS.
Conheça o ELS →

Se você não puder fazer a atualização hoje, desative o login com senha

Isso desativa o login por senha para todos os usuários, não apenas para as contas em risco. Certifique-se de que todos que precisam de acesso tenham uma chave SSH válida e mantenha sua sessão atual aberta. Tenha cuidado com a pasta /etc/ssh/sshd_config.d. A configuração padrão do Alpine a lê apenas a partir da versão 3.19; portanto, nas versões 3.18 e anteriores, um arquivo colocado nessa pasta por si só não tem efeito. Esses comandos, retirados do comunicado de segurança do Alpine, gravam o arquivo na pasta e também alteram o próprio arquivo /etc/ssh/sshd_config, com um backup em /etc/ssh/sshd_config.bak.

mkdir -p /etc/ssh/sshd_config.d
echo 'PasswordAuthentication no' > /etc/ssh/sshd_config.d/00-no-passwords.conf
sed -i.bak -e '1i PasswordAuthentication no' \
  -e 's/^\(\s*PasswordAuthentication[^y#]*\)yes/\1no/i' \
  /etc/ssh/sshd_config
sshd -t && rc-service sshd restart
sshd -T | grep -i passwordauthentication

O último comando deve exibir “passwordauthentication no”. Se isso não acontecer, o login por senha ainda está ativado.

Fique atento a tentativas de login que não deveriam ocorrer

Procure por “Accepted password” nas linhas do arquivo /var/log/messages — o arquivo syslog padrão no Alpine — para contas que contenham *. No caso do /etc/shadow, nosso laboratório sempre exibia uma linha “Failed password” logo antes dela, proveniente da mesma porta de conexão. Não confie apenas nesse par de informações. Em outras configurações, a primeira tentativa já pode ser bem-sucedida; um usuário real que digitar errado deixa o mesmo par; muitos contêineres não executam o daemon do syslog e não registram nada; e um invasor com privilégios de root pode limpar os logs. Verifique também se há novos usuários, novas chaves no arquivo `authorized_keys` e o uso do comando `doas`. Se você não puder descartar um login, considere a máquina comprometida.

Verifique sistemas mais antigos e ocultos

Imagens antigas do Docker e dispositivos embarcados às vezes executam o OpenSSH com o musl, e ninguém as atualiza. Verifique-as também.

Por que a TuxCare é importante neste contexto

A solução alternativa do upstream foi incorporada ao OpenSSH 10.6p2. Muitos sistemas de produção não utilizam a versão mais recente do OpenSSH. Eles utilizam a versão que veio com sua distribuição há anos. O Alpine Linux 3.18 vem com o OpenSSH 9.3p2 e atingiu o fim de sua vida útil em maio de 2025.

O Alpine lidou muito bem com essa situação. No dia da divulgação, eles publicaram pacotes corrigidos para todos os ramos desde a versão 3.16, inclusive para ramos que já haviam atingido o fim de vida útil. Para o Alpine 3.18, trata-se do 9.3_p2-r4, o pacote corrigido em nosso laboratório. No entanto, o Alpine não promete correções para ramos que já atingiram o fim de vida útil. Desta vez, eles fizeram uma correção, mas quanto ao próximo bug, ninguém pode dizer.

O Suporte de Ciclo de Vida Infinito (ELS) da TuxCare transforma isso em uma promessa. Para o Alpine Linux 3.18 ELS, nossa equipe fez o backport do mesmo commit do upstream para o OpenSSH 9.3p2 e o incorporou no mesmo dia em que a versão do upstream foi lançada. Ele se aplica sobre todos os 22 patches do nosso pacote ELS e é o mesmo código do próprio pw.patch do Alpine. O pacote corrigido será o openssh 9.3_p2.tuxcare.els7, para x86_64 e aarch64.
Explore o ELS →

Esse bug também mostra por que não basta acompanhar os alertas de um único projeto. O bug envolve código tanto do musl quanto do OpenSSH, a solução alternativa está no OpenSSH, e os sistemas afetados são servidores, contêineres e dispositivos embarcados baseados no musl. Alguém precisa acompanhar todos eles e disponibilizar um patch testado para as versões que os clientes ainda utilizam. Esse é o trabalho que nossa equipe de segurança realiza.

Junte-se a mais de 4.500 profissionais de Linux e código aberto!

2 vezes por mês. Sem spam.

Índice
Obtenha as respostas sobre segurança de código aberto de que você precisa

Junte-se a mais de 4.500 profissionais de Linux e código aberto!

2 vezes por mês. Sem spam.