Principales conclusiones
- Una contraseña de entre 257 y 1024 bytes permite acceder a cualquier cuenta que utilice únicamente claves en sistemas Linux que ejecuten sshd con la biblioteca musl libc sin PAM, sobre todo en Alpine Linux. OpenSSH 10.6p2 incluye la solución provisional, y el fallo se ha registrado con el número CVE-2026-123158.
- El error se da en dos proyectos. Desde 2016, sshd ha estado utilizando la sal de otra cuenta para una cuenta *, y la función crypt() de musl responde a una contraseña demasiado larga con la cadena «*», que coincide con el * almacenado. OpenSSH afirma que la causa principal se encuentra fuera de sshd; el responsable del mantenimiento de musl lo atribuye a un uso erróneo por parte del usuario que realiza la llamada.
- Lo hemos reproducido en Alpine Linux 3.18: en el segundo intento, con una sola conexión, se inicia sesión; un hash bcrypt elimina el límite de longitud y el paquete corregido rechaza todos los intentos. Actualiza y reinicia sshd, o desactiva el inicio de sesión con contraseña; en Alpine 3.18 y versiones anteriores, el archivo sshd_config.d por sí solo no tiene ningún efecto.
- Alpine Linux 3.18 llegó al final de su ciclo de vida en mayo de 2025 y, en esta ocasión, se le ha aplicado una corrección a modo de excepción. El servicio Endless Lifecycle Support (ELS) de TuxCare convierte eso en una promesa: Alpine 3.18 ELS recibe la misma actualización del código fuente que openssh 9.3_p2.tuxcare.els7.
El 5 de noviembre de 2026, el equipo de OpenSSH lanzó la versión 10.6p2 con una solución provisional crítica de seguridad. En algunos sistemas Linux, un atacante puede iniciar sesión a través de SSH en una cuenta que, en principio, nunca debería aceptar ninguna contraseña. No hay ninguna contraseña que adivinar. El atacante solo necesita el nombre de dicha cuenta. En la configuración más habitual, el atacante envía una contraseña incorrecta y, a continuación, una contraseña de entre 257 y 1024 bytes de longitud, en la misma conexión. En algunas configuraciones, incluso una contraseña corta funciona, como mostramos a continuación.
El fallo se ha registrado con el número CVE-2026-123158 (puntuación CVSS 3.1: 8,1). Fue detectado por Özgün Kültekin, de Wakefield (wakefield.co). La ruta de código responsable de este fallo ha estado presente en la versión portátil de OpenSSH desde 2016, desde la versión 7.3 hasta la 10.6p1.
Este fallo es inusual porque se da entre dos proyectos. El ataque no requiere un desbordamiento de búfer, un bloqueo ni una corrupción de memoria. Se trata de un fallo lógico que solo se manifiesta cuando OpenSSH y la biblioteca C musl se ejecutan conjuntamente en el mismo sistema, y los dos proyectos no se ponen de acuerdo sobre a quién corresponde el fallo. Las notas de la versión de OpenSSH lo explican así.
«Consideramos que la causa principal de este error se encuentra fuera de sshd, pero puede explotarse a través de sshd y puede evitarse en ese contexto».
El responsable del mantenimiento de musl, Rich Felker, lo ve de otra manera.
«Mi opinión es que no se trata de un error en musl, sino simplemente de un uso incorrecto por parte de quien lo invoca».
En esta entrada, analizamos el código de ambos proyectos, mostramos paso a paso cómo se lleva a cabo el inicio de sesión y reproducimos el ataque contra Alpine Linux en nuestro laboratorio.
¿A quién afecta?
El ataque solo funciona cuando todas estas condiciones se cumplen en el mismo servidor.
- sshd está enlazado con la biblioteca libc de musl. Alpine Linux es el caso más conocido y también constituye la base de muchas imágenes de Docker, pero solo quedan expuestas aquellas imágenes que ejecutan sshd. El fallo no está vinculado a Alpine. Cualquier sistema o dispositivo Linux que ejecute un sshd compilado con musl y sin PAM puede verse afectado, incluidos los firmwares personalizados y los binarios de sshd compilados por el propio usuario.
- sshd no utiliza PAM (el marco de autenticación que emplean la mayoría de las distribuciones de Linux para los inicios de sesión) para comprobar las contraseñas. En Alpine, el paquete openssh-server predeterminado se compila sin PAM. El paquete openssh-server-pam, que se ejecuta con UsePAM desactivado, tampoco se ve afectado.
- El inicio de sesión con contraseña está habilitado con la opción «PasswordAuthentication yes», que es la configuración predeterminada.
- La cuenta en cuestión tiene un único * como contraseña. El caso más habitual es el de un usuario que solo utiliza clave, con * en /etc/shadow. En Alpine sin PAM, una cuenta con ! no puede iniciar sesión ni siquiera con una clave SSH, por lo que los administradores cambian ! por * para que funcione el inicio de sesión con clave. La cuenta root en la imagen base de Alpine también tiene *, desde aproximadamente la versión 3.16 de Alpine, pero sshd bloquea el inicio de sesión con contraseña para root a menos que se establezca PermitRootLogin yes. Los usuarios que provienen de un servicio de directorio a través de musl-nscd no tienen entrada en shadow y también pueden acabar teniendo *.
- La primera cuenta, según el orden de /etc/passwd, cuyo hash comience por $ debe utilizar un tipo de hash en el que musl pueda fallar. Con un hash normal de tipo $5$ o $6$ (SHA-256 o SHA-512 crypt), el atacante necesita entre 257 y 1024 bytes, y el archivo passwd de Alpine escribe $6$ por defecto. Con un hash bcrypt ($2a$, $2b$ o $2y$), cualquier contraseña no vacía de hasta 1024 bytes funciona. Con un hash antiguo $1$ (MD5), un hash yescrypt $y$ o sin hash alguno, el ataque no funciona.
Esta vulnerabilidad no existe en estos sistemas.
- Sistemas que no son Linux.
- Sistemas Linux con glibc, como RHEL, AlmaLinux, CloudLinux, Debian o Ubuntu.
- Sistemas Linux en los que sshd comprueba las contraseñas mediante PAM.
Las cuentas bloqueadas con «!» en lugar de «*» también son seguras, al igual que los servidores con la opción «PasswordAuthentication no». Si solo necesitas los pasosa seguir, ve directamente a«Qué hacer ahora», al final del texto.
Qué significa una sola estrella en /etc/shadow
En Linux, los hash de las contraseñas se almacenan en /etc/shadow. Un hash es una forma cifrada unidireccional de la contraseña. Cuando inicias sesión, el sistema aplica el mismo proceso de hash a lo que has introducido y compara ambos resultados. Si coinciden, se te permite el acceso.
Una entrada normal tiene este aspecto:
alice:$6$Qx7Lm2Pa$k3J9...long hash...:20000:0:99999:7:::
El «$6» al principio indica el cifrado SHA-512, un método de hash de contraseñas basado en SHA-512. La parte que le sigue, «Qx7Lm2Pa», es la «salt». Una «salt» es una pequeña cadena aleatoria que se mezcla con la contraseña, de modo que dos usuarios con la misma contraseña obtienen hash diferentes.
Desde aproximadamente 1980, cuando los hash aún se almacenaban en /etc/passwd, los administradores de Unix introducían un único * en este campo cuando una cuenta no debía iniciar sesión nunca con contraseña. La lógica es sencilla: ninguna contraseña del mundo genera un hash que sea simplemente *, ya que un hash real siempre es mucho más largo. Así, la cuenta puede seguir iniciando sesión con una clave SSH, pero nunca con contraseña. El error consiste precisamente en romper esta idea.
Cómo comprueba sshd una contraseña sin PAM
Cuando PAM está desactivado, sshd comprueba la contraseña por sí mismo en la función sys_auth_passwd() del archivo auth-passwd.c. Este es el código de OpenSSH 9.3p2, la versión incluida en Alpine Linux 3.18, antes de la corrección.
/* 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;
Esta es la forma clásica de hacerlo. Se lee el hash almacenado, se calcula el hash de la contraseña introducida utilizando el hash almacenado como sal y se comparan ambas cadenas.
Ahora fíjate en la línea de la sal. sshd utiliza el valor almacenado como sal solo cuando tiene al menos dos caracteres. Un * solo tiene uno. Por lo tanto, para una cuenta *, la sal se mantiene en NULL, y xcrypt() tiene que elegir una sal por sí mismo.
El cambio de 2016
¿Por qué xcrypt() elige una sal? La respuesta es CVE-2016-6210. En 2016, un investigador demostró que sshd respondía a una velocidad diferente cuando se trataba de usuarios que no existían. Al enviar contraseñas largas y medir el tiempo de respuesta, un atacante podía elaborar una lista de nombres de usuario reales.
Para subsanar esa deficiencia, Darren Tucker modificó sshd de modo que, cuando no dispone de una «salt» válida, toma prestados el tipo de hash y la «salt» de una cuenta real del sistema. La primera confirmación, 9286875a «Determinar la «salt» adecuada para usuarios no válidos», tomó prestada la «salt» de root. Seis días después, la confirmación dbf788b4, «Buscar entre los usuarios uno con una sal válida», hizo que se buscara en todas las cuentas. Ambas se incluyeron en OpenSSH 7.3 en agosto de 2016. El cambio estaba destinado a los usuarios que no existen. Sin embargo, la comprobación de dos caracteres mencionada anteriormente hace que una cuenta válida con * siga el mismo proceso, por lo que también obtiene la sal tomada prestada.
Esta es la función `pick_salt()` en `openbsd-compat/xcrypt.c`, tal y como aparece en la versión 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;
Recorre las cuentas en el orden de /etc/passwd y toma la primera clave de sal $…$ que encuentra, por ejemplo, $6$Qx7Lm2Pa de alice. Guarda el resultado, de modo que los intentos posteriores en la misma conexión utilizan la clave de sal almacenada en caché. Si ninguna cuenta tiene un hash $, recurre a xx, el antiguo tipo DES.
Antes de este cambio, sshd utilizaba «xx» para entradas cortas como *. El antiguo DES no falla con contraseñas largas, sino que simplemente utiliza los primeros 8 caracteres, por lo que nunca podría devolver *. Tras el cambio, una cuenta * recibe la sal de otro usuario. Así es como surgió el problema en 2016. El cambio supuso una buena solución para un problema real de sincronización y, por lo que se sabe, nadie se percató del efecto secundario.
Qué devuelve la función `crypt()` de musl en caso de error
musl es una pequeña biblioteca en C que utilizan Alpine y muchos sistemas embebidos. Su código de cifrado SHA-512 se encuentra en src/crypt/crypt_sha512.c. Limita la longitud de la contraseña para proteger el sistema frente a solicitudes lentas que consumen muchos recursos de la 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;
Se rechaza cualquier contraseña que supere los 256 bytes. Ahora fíjate en lo que devuelve __crypt_sha512(), la función interna a la que recurre crypt(), en ese 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;
En caso de error, la función `crypt()` de musl devuelve la cadena «*». El código de SHA-256, MD5 y Blowfish hace lo mismo, y solo SHA-256 y SHA-512 tienen el límite de 256 bytes. musl funciona así desde 2012. La norma POSIX establece que crypt() debe devolver un puntero nulo y establecer errno en caso de error, pero musl optó deliberadamente por una cadena. Su commit b3c4cc12 explica que «una gran parte de los programas que llaman a crypt no están preparados para su fallo y no comprueban que el valor devuelto sea distinto de nulo», y denomina al resultado un «hash incomparable».
El problema es que * es también el valor que significa «inicio de sesión sin contraseña» en /etc/shadow.
musl sí que protegió el patrón clásico. Fíjate en la última línea del código DES en crypt_des.c.
return (setting[0]=='*') ? "x" : "*";
Si la propia sal empieza por *, musl devuelve x en su lugar. Por lo tanto, crypt(contraseña, «*») nunca devuelve «*». Esto protege la comprobación clásica, en la que un programa utiliza el valor almacenado como sal y, a continuación, compara el resultado con ese mismo valor almacenado.
Pero sshd no utilizó el valor almacenado como sal. Utilizó la sal de Alice. Musl vio $6$…, por lo que esta protección no sirvió de nada.
Una pequeña prueba
Hemos comprobado nosotros mismos la parte de la biblioteca C con un pequeño programa de prueba. Estas son sus dos líneas clave, simplificadas. Genera un hash de las contraseñas compuestas únicamente por la letra A con una sal prestada de $6$, y compara el resultado con *.
r = crypt(long_password, "$6$abcdefgh"); /* salt borrowed from another account */
ok = (r != NULL && strcmp(r, "*") == 0); /* compare with the "*" in /etc/shadow */
Lo hemos probado en Alpine Linux 3.18 (musl) y en Debian 12. En Debian, al igual que en la mayoría de los sistemas modernos con glibc, la función crypt() procede de una biblioteca independiente llamada 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
En musl, cualquier contraseña de 257 bytes o más coincide con el asterisco. Con libxcrypt, una contraseña de 257 bytes se somete a un hash de forma normal. libxcrypt tiene su propio límite de 511 bytes, pero cuando falla devuelve *0, o *1 cuando la sal empieza por *0. Una cadena de error de dos caracteres nunca puede ser igual a un * de un solo carácter, por lo que la comparación falla, tal y como debería.
Todo el proceso, desde la contraseña larga hasta el inicio de sesión
A continuación se muestra la secuencia completa de acciones que seguiría un atacante que tuviera como objetivo a un usuario con solo clave en un servidor Alpine. Ese usuario tiene * en /etc/shadow, y el primer hash $ del sistema es el hash $6$ de otro usuario.
- El atacante se conecta por SSH y solicita el inicio de sesión mediante contraseña como usuario que solo utiliza clave.
- El atacante envía una contraseña de 300 bytes. sshd admite contraseñas de hasta 1024 bytes (MAX_PASSWORD_LEN), por lo que continúa.
- sshd lee * en el archivo /etc/shadow. Solo contiene un carácter, por lo que no hay sal.
- La función `pick_salt()` toma prestada la sal $6$ del otro usuario.
- El código SHA-512 de musl detecta más de 256 bytes y falla. La función crypt() devuelve «*».
- En este primer intento, sshd ya ha perdido el * almacenado, por una razón secundaria que explicamos a continuación, por lo que la comparación falla. El atacante simplemente vuelve a intentarlo en la misma conexión.
- En el segundo intento, sshd compara «*» con el «*» almacenado. Son iguales. Se acepta el inicio de sesión.
Por eso, el intervalo de ataque es de 257 a 1024 bytes cuando la sal tomada prestada es $5$ o $6$. Por debajo de 257, musl aplica el hash a la contraseña con normalidad. Por encima de 1024, sshd la rechaza antes de aplicar el hash. El texto de la CVE dice «entre 256 y 1024 caracteres», pero el intervalo real comienza en 257 bytes.
Con bcrypt, la longitud no importa
Hay otra vía más, y no aparece ni en el aviso de Alpine ni en el CVE. La encontramos mientras revisábamos el código. La función `pick_salt()` corta el hash en su último símbolo $. Un hash de bcrypt tiene el formato $2b$10$, seguido de 53 caracteres, sin ningún símbolo $ entre la sal y el hash. Por lo tanto, sshd solo conserva $2b$10 y pierde la sal propiamente dicha. musl rechaza esa configuración y, al producirse un error, vuelve a devolver «*». En esta ocasión, la longitud de la contraseña no importa.
Upstream corrigió este error relacionado con la «salt» el 30 de octubre de 2026, en la confirmación 84f8f466 «corrección de la generación de una «salt» de contraseña falsa para los hash de 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;
El mensaje de la confirmación menciona un problema de sincronización, pero en musl el efecto es mucho peor. Esta confirmación se encuentra en la rama «master» y no forma parte de la versión 10.6p2. La solución provisional de la versión 10.6p2 sigue bloqueando esta ruta, ya que un * tiene menos de 4 caracteres.
Lo probamos en Alpine 3.18 con un hash bcrypt como primer hash $ del sistema. En el paquete antiguo, todas las contraseñas no vacías de entre 1 y 1024 bytes permitían iniciar sesión en la cuenta * al segundo intento. El paquete corregido las rechazó todas. El comando `passwd` de Alpine escribe $6$ por defecto, por lo que esta ruta necesita un hash bcrypt que haya introducido un administrador o una herramienta.
Imágenes de nubes alpinas
El aviso de Alpine señala al usuario «alpine» en las imágenes oficiales de tiny-cloud desde la versión 3.18.11 hasta la 3.24.3. Ese usuario tiene *. Una imagen recién creada no queda expuesta, ya que ninguna cuenta dispone aún de un hash de contraseña real. Queda expuesta en cuanto alguna cuenta obtiene un hash de $6$ mientras el inicio de sesión con contraseña está activado. Las imágenes configuradas mediante cloud-init, que establece «PasswordAuthentication no», no se ven afectadas. El investigador señala que, en las imágenes oficiales de AWS y Azure, el usuario «alpine» puede ejecutar «doas» (el equivalente de Alpine a «sudo») sin contraseña, por lo que iniciar sesión como «alpine» en esos entornos equivale a hacerlo como «root».
Lo hemos reproducido en Alpine
La prueba crypt() muestra la parte correspondiente a la biblioteca de C. Además, queríamos comprobar si un sshd real nos permitía el acceso, así que montamos un pequeño entorno de pruebas.
Hemos utilizado dos imágenes:
- Alpine Linux 3.18 con openssh-server 9.3_p2-r3, el último paquete de Alpine antes de la corrección.
- Alpine Linux 3.18 con openssh-server 9.3_p2-r4, el paquete corregido.
En ambos casos, asignamos a una cuenta una contraseña real mediante la herramienta «passwd» propia de Alpine. A continuación, añadimos una cuenta que solo contenía una clave con * y una cuenta bloqueada con !, e intentamos iniciar sesión con contraseñas de diferentes longitudes. «INICIADA SESIÓN» significa que se aceptó el segundo intento en la misma conexión y que sshd ejecutó un comando por nosotros como ese usuario.
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
En el paquete modificado, todos los ataques fallaron, mientras que la contraseña real seguía funcionando como antes. El cliente SSH normal envía como máximo 1023 bytes, por lo que una contraseña tecleada más larga simplemente queda fuera de la ventana de ataque. Para las filas de 1024 y 1025 bytes utilizamos un pequeño cliente que envía la longitud exacta. A 1025 bytes, sshd rechazó la contraseña antes de aplicarle el hash. En la fila de bcrypt, el primer hash $ del sistema era un hash $2b$ en lugar de $6$.
Por qué cada ataque requería dos intentos
En cada ejecución vulnerable de la tabla, el primer intento de contraseña falló y el segundo intento en la misma conexión funcionó. El primer intento puede ser cualquier contraseña incorrecta de hasta 1024 bytes. Confirmamos el motivo mediante una pequeña prueba que registra lo que sshd pasa a strcmp(). En el primer intento, sshd comparaba «*» con lo que quedara en la dirección del * almacenado; en nuestro laboratorio, una cadena vacía. En el segundo intento, comparaba «*» con «*».
La causa es un problema de búfer en sshd. La función shadow_pw() devolvía un puntero al búfer que la función getspnam() de musl reutiliza en cada consulta. En el primer intento se ejecuta pick_salt(), que busca otras cuentas en ese mismo búfer y, a continuación, borra el hash tomado prestado con explicit_bzero(). Por lo tanto, el * al que sshd seguía apuntando ya no existe. A partir del segundo intento, la sal se almacena en caché, no se produce ninguna otra búsqueda y * coincide con *. La solución provisional de la versión 10.6p2 también corrige esto, tal y como mostramos en la sección de soluciones provisionales.
El segundo intento solo es necesario cuando el * procede de /etc/shadow y sshd se ejecuta con la configuración predeterminada. En nuestro laboratorio, una cuenta con * en /etc/passwd y sin entrada en shadow fue aceptada al primer intento. Con PermitEmptyPasswords activado (yes), la opción de sshd que permite iniciar sesión a cuentas con contraseña vacía, incluso un * en /etc/shadow fue aceptado en el primer intento de contraseña.
Es probable que los usuarios del servicio de directorio se comporten como en el caso de /etc/passwd, pero no lo hemos comprobado. Esto concuerda con ambas fuentes públicas. El aviso de Alpine indica que el ataque requiere una sola conexión y no necesita reintentos, mientras que la página del investigador señala que un * en shadow requiere primero un intento erróneo.
Por defecto, sshd permite 6 intentos por conexión (MaxAuthTries), por lo que ese intento adicional no detiene a un atacante. En el caso de /etc/shadow, deja registrado un intento fallido y otro aceptado desde el mismo puerto de conexión.
Failed password for keyonly from 127.0.0.1 port 53734 ssh2
Accepted password for keyonly from 127.0.0.1 port 53734 ssh2
Entonces, ¿se trata de un error de OpenSSH o de musl?
El equipo de OpenSSH afirma que la causa principal se encuentra fuera de sshd, y hay argumentos de peso que lo respaldan. La comprobación clásica, utilizada desde hace décadas, calcula el hash de la contraseña introducida utilizando el valor almacenado como «salt» y compara el resultado con ese mismo valor. musl mantiene la seguridad de ese patrón, ya que su cadena de error nunca coincide con el «salt» que se le ha proporcionado. En los sistemas glibc, el mismo código de sshd también es seguro.
También hay argumentos en contra. Desde 2016, sshd calcula el hash de una entrada de un solo carácter con la sal de otra cuenta y sigue comparando el resultado con el valor almacenado, y mantenía un puntero a un búfer de la biblioteca C que se modificaba en su interior. La protección de musl no podía hacer nada al respecto. Rich Felker lo ve así y lo califica de «uso erróneo por parte del llamante». También escribió que el mecanismo subyacente «muy probablemente se modificará en el próximo ciclo de lanzamiento» de musl.
Pues bien, este insecto tenía tres piezas, y cada una de ellas se ve bien por sí sola:
- El asterisco * indica una cuenta en la que no es necesario introducir contraseña para iniciar sesión, una costumbre de Unix que se remonta aproximadamente a 1980, cuando los hash aún se almacenaban en /etc/passwd.
- La cadena de error «*» y el límite de 256 bytes en musl, de 2012.
- La sal prestada en sshd, de 2016.
Ningún proyecto en concreto es el único responsable del fallo, lo que ayuda a explicar por qué permaneció oculto durante 10 años. Hay que analizar cómo se comportan las piezas en conjunto, y esto solo se aprecia en un sistema musl sin PAM. El investigador lo descubrió compilando sshd con diferentes bibliotecas C y configuraciones de PAM, y probando miles de inicios de sesión inusuales.
La corrección en OpenSSH
Damien Miller escribió la solución provisional. En la rama V_10_6 se trata del commit 90f9c2f7 «evitar el bypass de la autenticación por contraseña», y en la rama «master» el mismo cambio corresponde al commit 4353fdc4. La parte principal es muy breve.
+#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;
Un hash de contraseña real nunca tiene menos de 4 caracteres. Ahora bien, si el valor almacenado es *, !, x o cualquier otro carácter de esa longitud, la comparación del hash siempre falla, independientemente de lo que devuelva la función crypt(). Esto bloquea todas las opciones descritas en esta entrada, incluida la de bcrypt. El caso específico de las contraseñas vacías, controlado por PermitEmptyPasswords, no sufre ningún cambio.
El parche también modifica la función shadow_pw() para que devuelva una copia de la entrada de shadow, creada con xstrdup(), y borre dicha copia tras su uso. De este modo se soluciona el problema del búfer que provocaba los dos intentos.
En la lista openssh-unix-dev, Damien Miller explicó el motivo de esta decisión. Escribió que «estamos en una buena posición para ofrecer una solución rápidamente». En el momento de redactar este artículo, musl sigue devolviendo «*» en caso de error, pero su responsable espera cambiar ese mecanismo en el próximo ciclo de lanzamiento.
¿Qué hacer ahora?
Actualizar y reiniciar sshd
En Alpine, actualiza y reinicia de esta manera. Este es el comando que se indica en el aviso de Alpine. El parámetro -a también permite que apk sustituya o baje de versión los paquetes para que coincidan con los repositorios, así que comprueba qué cambios se producen.
apk upgrade -U -a
rc-service sshd restart
OpenSSH 9.3p2 inicia una copia nueva de /usr/sbin/sshd para cada nueva conexión, por lo que, en nuestro laboratorio, los nuevos inicios de sesión utilizaron el código corregido tan pronto como se instaló el paquete. Reinicia sshd de todos modos, para que el proceso de escucha también ejecute el nuevo binario. En un contenedor, vuelve a compilar la imagen a partir de una base corregida y reinicia el contenedor. Los clientes de TuxCare ELS que utilicen Alpine Linux 3.18 pueden obtener la corrección desde el repositorio de ELS.
Descubre ELS →
Si hoy no puedes actualizar, desactiva el inicio de sesión con contraseña.
Esto desactiva el inicio de sesión con contraseña para todos los usuarios, no solo para las cuentas en riesgo. Asegúrate de que todas las personas que necesiten acceso dispongan de una clave SSH válida y mantén abierta tu sesión actual. Ten cuidado con la carpeta de configuración temporal /etc/ssh/sshd_config.d. La configuración predeterminada de Alpine solo la lee a partir de la versión 3.19, por lo que en la 3.18 y versiones anteriores un archivo colocado en esa carpeta por sí solo no tiene ningún efecto. Estos comandos, extraídos del aviso de Alpine, escriben el archivo en la carpeta de configuración temporal y también modifican el propio archivo /etc/ssh/sshd_config, creando una copia de seguridad en /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
El último comando debería mostrar «passwordauthentication no». Si no es así, el inicio de sesión con contraseña sigue activado.
Busca los inicios de sesión que no deberían producirse
Busca la línea «Contraseña aceptada» en /var/log/messages, el archivo syslog predeterminado en Alpine, para las cuentas que contengan *. En el caso de /etc/shadow, nuestro laboratorio siempre mostraba una línea «Contraseña fallida» justo antes, procedente del mismo puerto de conexión. No te fíes únicamente de esa combinación. En otras configuraciones, el primer intento ya puede tener éxito; un usuario real que escriba mal la contraseña deja la misma combinación; muchos contenedores no ejecutan ningún demonio syslog y no registran nada; y un atacante con privilegios de root puede borrar los registros. Comprueba también si hay nuevos usuarios, nuevas claves en authorized_keys y el uso de doas. Si no puedes descartar un inicio de sesión, considera que la máquina está comprometida.
Comprueba los sistemas antiguos y ocultos
Las imágenes antiguas de Docker y los dispositivos integrados a veces ejecutan OpenSSH con musl, y nadie las actualiza. Compruébalas también.
¿Por qué TuxCare es importante en este contexto?
La solución provisional del proyecto principal se incorporó a OpenSSH 10.6p2. Muchos sistemas de producción no utilizan la versión más reciente de OpenSSH, sino la que venía incluida en su distribución hace años. Alpine Linux 3.18 incluye OpenSSH 9.3p2 y su ciclo de vida finalizó en mayo de 2025.
Alpine ha gestionado muy bien este asunto. El mismo día en que se dio a conocer el problema, publicaron paquetes corregidos para todas las ramas desde la versión 3.16, incluso para aquellas que ya habían superado su fin de vida útil. En el caso de Alpine 3.18, se trata de la versión 9.3_p2-r4, que es el paquete corregido que utilizamos en nuestro laboratorio. Sin embargo, Alpine no se compromete a ofrecer correcciones para las ramas que han superado su fin de vida útil. Esta vez lo han hecho, pero nadie puede asegurar qué pasará con el próximo error.
El servicio Endless Lifecycle Support (ELS) de TuxCare convierte eso en una promesa. Para Alpine Linux 3.18 ELS, nuestro equipo adaptó el mismo commit del proyecto original a OpenSSH 9.3p2 y lo integró el mismo día en que se publicó la versión del proyecto original. Se aplica sobre los 22 parches de nuestro paquete ELS, y es el mismo código que el propio pw.patch de Alpine. El paquete corregido será openssh 9.3_p2.tuxcare.els7, para x86_64 y aarch64.
Explora ELS →
Este fallo también pone de manifiesto por qué no basta con estar al tanto de los avisos de un solo proyecto. El fallo afecta al código tanto de musl como de OpenSSH, la solución provisional se encuentra en OpenSSH, y los sistemas afectados son servidores, contenedores y dispositivos integrados basados en musl. Alguien tiene que estar al tanto de todos ellos y publicar un parche probado para las versiones que los clientes siguen utilizando. Ese es el trabajo que realiza nuestro equipo de seguridad.