⏱️ Lectura: 11 min
Un servidor SSH sobre Alpine Linux puede dejar entrar a una cuenta marcada como bloqueada si la contraseña que enviás supera los 256 bytes. No hace falta adivinar ningún secreto: alcanza con que el campo de esa cuenta en /etc/shadow sea un simple asterisco y con que el sistema use musl libc sin PAM.
📑 En este artículo
- TL;DR
- Qué es musl
- Qué pasó
- Cómo surge la vulnerabilidad en musl libc
- Detalles técnicos: el mecanismo detrás del bug
- Impacto y análisis
- Qué sigue
- Preguntas frecuentes
- ¿Qué distribuciones usan musl libc y podrían verse afectadas?
- ¿Alpine Linux con PAM también queda expuesto?
- ¿Ya existe un CVE para este bug de musl?
- ¿Qué cuentas suelen tener ‘*’ en /etc/shadow?
- ¿Cómo se relaciona esto con el CVE-2016-6210 de OpenSSH?
- ¿musl ya lanzó un parche para esta vulnerabilidad en musl libc?
- Referencias
El hallazgo, publicado en sshhole.com, apunta a una vulnerabilidad en musl libc que lleva catorce años escondida. Desde 2012, la función crypt() de musl devuelve el mismo carácter que marca las cuentas bloqueadas cuando falla, y hasta ahora nadie lo había conectado con sshd.
TL;DR
- En Alpine con musl y sshd sin PAM, una contraseña de más de 256 bytes abre cualquier cuenta bloqueada en /etc/shadow.
- El fallo nace en 2012: crypt() de musl devuelve ‘*’ al fallar, igual que el símbolo de cuentas bloqueadas desde 1980.
- Musl limita sha512crypt a 256 bytes desde el 29 de agosto de 2012, y al superar ese límite crypt() devuelve ese mismo ‘*’.
- El hallazgo surgió al probar 17.600 logins contra seis builds de sshd; solo musl sin PAM aceptó entradas que debía rechazar.
- La falla pasó 10 años inadvertida: OpenSSH se prueba en OpenBSD y glibc, y la mayoría de distros usan PAM, que evita el bug.
Qué es musl
Musl es la biblioteca C estándar (libc) minimalista que usa Alpine Linux y muchas imágenes de Docker, pensada para producir binarios pequeños y estáticos. Implementa las funciones POSIX que un programa en C necesita, entre ellas crypt(), la que sshd llama para comparar la contraseña escrita con el hash guardado en el sistema.
Qué pasó
La prueba fue simple en su planteo y enorme en escala: compilar sshd de seis formas distintas (con glibc o musl, con o sin PAM, con o sin /etc/shadow) y lanzarle cerca de 17.600 intentos de login con hashes raros, cuentas rotas y contraseñas demasiado largas. Según sshhole.com, el autor usó enjambres de agentes automatizados para generar y correr esos intentos en paralelo.
Cinco de las seis combinaciones coincidieron en cada resultado. El patrón era siempre el mismo: una cuenta con ‘*’ en el campo de contraseña de /etc/shadow, la marca estándar de cuenta bloqueada desde 1980, dejaba entrar a cualquiera que enviara una contraseña de más de 256 bytes, pero solo en la build de musl sin PAM.
El hallazgo se publicó en noviembre de 2026, apenas cuatro días después de que una revisión de seguridad de OpenSSH, fechada el 30 de octubre de 2026, corrigiera una función vecina (pick_salt()) sin tocar el bug real, que vivía cuatro líneas más abajo en el mismo archivo.
Cómo surge la vulnerabilidad en musl libc
La marca de cuenta bloqueada nació en 1980, cuando Bill Joy la agregó en 3BSD para la cuenta daemon, con un detalle curioso: el nombre real que anotó para ese usuario fue «the devil himself». Un asterisco no es un hash válido, así que ninguna contraseña puede coincidir con él, y por eso durante 46 años funcionó como un candado seguro.
En los noventa, el hash se mudó a /etc/shadow con el formato $6$sal$hash (sha512crypt). sshd toma la contraseña escrita, la cifra con la misma sal guardada y compara el resultado contra ese campo; si coinciden, el login entra. La especificación Single UNIX de 1997 estableció que, si crypt() no puede hacer su trabajo, debe devolver NULL. Muchos programas nunca revisaron ese caso y terminaban comparando contra NULL, lo que los hacía crashear.
musl resolvió eso en agosto de 2012 con el commit b3c4cc12: en vez de NULL, crypt() devuelve un carácter que ninguna contraseña real puede producir. Ese carácter es ‘*’, el mismo símbolo que desde 1980 marca una cuenta bloqueada.
Veinte días más tarde, el commit 88bf5a8a sumó sha512crypt a musl y le puso un límite de 256 bytes (constante KEY_MAX), porque sha512crypt se vuelve más lento cuanto más larga es la entrada, y una contraseña gigante podría ocupar el servidor por un rato largo. Pasado ese límite, crypt() falla y, por el cambio de veinte días antes, devuelve ‘*’.
En 2016 llegó el CVE-2016-6210: preguntarle a sshd por un usuario que no existe respondía más rápido que por uno real, porque no había ningún hash que calcular, y cronometrando esa diferencia un atacante podía enumerar cuentas válidas. El parche de OpenSSH hizo que, ante un usuario inválido, sshd tome la sal de otra cuenta cualquiera y cifre la contraseña igual, solo para gastar el mismo tiempo; el mensaje del commit decía «for invalid users», pero terminó aplicándose también a cuentas reales con ‘*’ en el campo.
En 2019, Alpine enfrentó el CVE-2019-5021: sus imágenes oficiales de Docker no traían contraseña de root. La primera corrección bloqueó la cuenta con ‘!’, pero sin PAM ese símbolo también tumbaba el login por llave SSH, que es como se accede a casi todas las imágenes en la nube. En 2022, Alpine 3.16 volvió a usar ‘*’ para bloquear root (commit fbe926af), lo que restauró el acceso por llave. Diez años después de b3c4cc12, el root de Alpine volvía a depender del mismo asterisco.
Hubo quienes estuvieron cerca de notarlo. En 2013, un desarrollador de Debian le pidió a glibc copiar el ‘*’ de musl en la lista libc-alpha; seis años después, Debian migró a libxcrypt, que usa ‘*0’ (un carácter distinto), y cerró el pedido ahí. En diciembre de 2024, un hilo de la lista de musl discutió un bug de busybox passwd con contraseñas de 257 caracteres, el mismo límite y el mismo ‘*’, pero nadie mencionó a sshd.
Detalles técnicos: el mecanismo detrás del bug
El mecanismo es una colisión de representación, no una falla criptográfica. sshd, cuando no usa PAM, llama directamente a crypt() con la contraseña y el campo de shadow, y compara el resultado contra ese mismo campo tratando ambos como simples cadenas de texto. Si el campo guardado ya es ‘*’ y crypt() también devuelve ‘*’ porque la entrada es demasiado larga, la comparación de strings da verdadero, aunque ninguno de los dos lados sea un hash real.
char *computed = crypt(password, shadow_field);
if (strcmp(computed, shadow_field) == 0) {
// acceso concedido
}
Si password mide más de 256 bytes, crypt() devuelve «*». Si shadow_field ya es «*» porque la cuenta está bloqueada, strcmp(«*», «*») da 0 y la condición se cumple sin que exista ningún hash de verdad detrás.
sequenceDiagram
participant C as Cliente SSH
participant S as sshd sin PAM
participant M as crypt de musl
C->>S: envia una contrasena de 300 bytes
S->>M: pide el crypt de la contrasena
M-->>S: responde con el caracter asterisco
Note over S: el asterisco de shadow coincide con el de crypt
S-->>C: acceso concedido
La ruta con PAM nunca pasa por ahí. PAM intercepta la autenticación antes de llegar a crypt(), y cuando el campo de la cuenta ya es ‘*’ ni siquiera intenta calcular un hash; por eso la colisión solo aparece en builds sin PAM, que son comunes en contenedores Alpine mínimos.
La tabla siguiente resume qué combinación de libc y PAM resultó vulnerable en las pruebas descritas en sshhole.com.
| Configuración de sshd | ¿Usa PAM? | Resultado con contraseña mayor a 256 bytes |
|---|---|---|
| glibc con PAM | Sí | Rechaza el login; PAM nunca llama a crypt() para un campo ‘*’ |
| glibc sin PAM | No | Rechaza el login; crypt() de glibc no devuelve ‘*’ al fallar |
| musl con PAM | Sí | Rechaza el login, por el mismo motivo que glibc con PAM |
| musl sin PAM | No | Acepta el login en cuentas bloqueadas con ‘*’ |
Impacto y análisis
La vulnerabilidad en musl libc tiene un alcance acotado, porque solo afecta a cuentas marcadas como bloqueadas y a builds de sshd sin PAM, no a los inicios de sesión normales con contraseña real. Aun así, Alpine es la base de una enorme cantidad de imágenes Docker, y muchas de ellas corren sin PAM por diseño, para mantenerse livianas.
En la práctica, el riesgo depende de qué cuentas tengan ese asterisco y de si sshd las deja intentar login. Muchas cuentas de servicio usan un shell /sbin/nologin que corta el acceso igual, y root suele tener PermitRootLogin en no. El caso más sensible es un contenedor Alpine con PAM deshabilitado donde alguien reactivó el login por contraseña para una cuenta pensada para quedar bloqueada.
⚠️ Ojo: el bug no requiere adivinar ninguna contraseña real: alcanza con mandar cualquier cadena de más de 256 bytes contra una cuenta con ‘*’ en /etc/shadow.
Qué sigue
Mientras tanto, la vulnerabilidad en musl libc sigue documentada pero sin parche oficial confirmado, ya que ni musl ni OpenSSH publicaron todavía un commit que la cierre. La revisión de OpenSSH del 30 de octubre de 2026 corrigió pick_salt() sin tocar el bug, que según sshhole.com está apenas cuatro líneas más abajo en el mismo archivo.
Tampoco hay, por ahora, un identificador CVE asignado a este hallazgo puntual. Los administradores de sistemas Alpine pueden mitigarlo revisando si PAM está activo, confirmando que las cuentas de servicio usen un shell sin login y verificando el estado de PermitRootLogin en su configuración de sshd.
📖 Resumen en Telegram: Ver resumen
Probalo vos: revisá el campo de contraseña de tus cuentas de servicio en /etc/shadow y confirmá si tu sshd corre con PAM antes de confiar en que un asterisco te protege.
Preguntas frecuentes
¿Qué distribuciones usan musl libc y podrían verse afectadas?
Alpine Linux es la distribución más extendida con musl como libc por defecto, y por eso domina las imágenes base de contenedores Docker; también la usan Void Linux y algunos sistemas embebidos. El bug solo se dispara en builds de sshd sin PAM.
¿Alpine Linux con PAM también queda expuesto?
No, según las pruebas de sshhole.com: PAM nunca llega a llamar a crypt() cuando el campo de la cuenta ya es ‘*’, así que esa ruta de autenticación nunca produce la colisión.
¿Ya existe un CVE para este bug de musl?
No hay uno asignado públicamente al momento de escribir esto. Sí existen CVEs relacionados con el mismo asterisco en Alpine, como el CVE-2016-6210 de OpenSSH y el CVE-2019-5021 del root sin contraseña en Docker.
¿Qué cuentas suelen tener ‘*’ en /etc/shadow?
Tradicionalmente, cuentas de sistema que no deben iniciar sesión interactiva, como daemon, bin o mail; en Alpine 3.16 en adelante, también root cuando está bloqueado.
¿Cómo se relaciona esto con el CVE-2016-6210 de OpenSSH?
Ese CVE de 2016 ya forzaba a sshd a llamar a crypt() incluso para usuarios sin hash real, para evitar que el tiempo de respuesta delatara si la cuenta existía. Esa misma llamada es la que, en musl sin PAM, termina comparando dos asteriscos.
¿musl ya lanzó un parche para esta vulnerabilidad en musl libc?
No hay confirmación pública de un commit que lo corrija. La revisión de seguridad de OpenSSH del 30 de octubre de 2026 arregló pick_salt() sin tocar el bug, que queda cuatro líneas más abajo en el mismo archivo.
Referencias
- sshhole.com: artículo original con el hallazgo y una demo interactiva de Debian y Alpine.
- git.musl-libc.org: commit de 2012 que hace que crypt() devuelva ‘*’ en lugar de NULL.
- git.musl-libc.org: commit que agrega sha512crypt y el límite de 256 bytes.
- lists.mindrot.org: parche de OpenSSH de 2016 para el CVE-2016-6210.
- gitlab.alpinelinux.org: commit de Alpine 3.16 que vuelve a usar ‘*’ para root.
- pubs.opengroup.org: especificación Single UNIX de 1997 sobre crypt().
- lists.mindrot.org: revisión de seguridad de OpenSSH del 30 de octubre de 2026.
📱 ¿Te gusta este contenido? Únete a nuestro canal de Telegram @programacion donde publicamos a diario lo más relevante de tecnología, IA y desarrollo. Resúmenes rápidos, contenido fresco todos los días.
Imagen destacada: Foto de Kaffeebart en Unsplash
¿Te sirvió? ¿Te dio otro error? Contalo abajo: las preguntas se responden y le sirven al siguiente que llegue.
Dejar un comentario
0 Comentarios