Ozgun Kultekin, de Wakefield, ha publicado sshhole, un fallo de OpenSSH en sistemas Linux que usan musl como biblioteca C en vez de glibc. En esos sistemas, y bajo ciertas condiciones, sshd acepta como válida cualquier contraseña de más de 256 bytes para las cuentas que, en teoría, no admiten contraseña. Entre ellas, root en Alpine.
Lo que más me ha gustado del write-up es que no hay un único culpable. Es la suma de cuatro decisiones razonables tomadas a lo largo de 46 años, cada una en un proyecto distinto, que juntas abren la puerta.
A quién afecta
Hace falta que se cumpla todo esto a la vez:
sshdcompilado contra musl y sin PAM.- Login por contraseña activado, que es el valor por defecto.
- Una cuenta cuyo campo de contraseña sea
*: usuarios que sólo entran con clave, usuarios de LDAP o el propio root en Alpine. - Otra cuenta en la misma máquina con una contraseña real.
Eso apunta a Alpine, OpenWrt, Buildroot, Yocto y muchas imágenes Docker basadas en Alpine. No afecta a distribuciones con glibc (Debian, Ubuntu, Fedora), a instalaciones que usan PAM, a Dropbear ni a OpenBSD.
El caso más grave que señala el autor son las imágenes oficiales de Alpine para AWS y Azure: traen un usuario alpine con doas sin contraseña, así que entrar con esa cuenta equivale a ser root.
Cómo se llega hasta aquí
La historia empieza en 1980, cuando BSD usaba * en /etc/passwd para cuentas como daemon. La idea es sencilla: sshd (y cualquier programa que compruebe contraseñas) calcula el hash de lo que escribe el usuario con la sal guardada y lo compara con el campo almacenado. Ningún hash válido es *, así que nada puede coincidir.
| Año | Qué pasa |
|---|---|
| 1980 | BSD usa * para marcar cuentas que nunca aceptan contraseña |
| 2012 | musl hace que crypt() devuelva "*" cuando falla, en vez de NULL como pide POSIX, para evitar que los programas que no comprueban el NULL se caigan |
| 2012 | musl añade SHA-256 y SHA-512 crypt con un límite de 256 bytes de contraseña, para evitar ataques de denegación de servicio; si se pasa del límite, falla y devuelve "*" |
| 2016 | Para corregir la enumeración de usuarios (CVE-2016-6210), OpenSSH empieza a calcular el hash con la sal de otra cuenta real cuando el usuario no tiene una utilizable, para que todas las respuestas tarden lo mismo |
| 2022 | Alpine 3.16 cambia la contraseña de root de ! a * para que root pueda entrar con clave sin PAM |
Juntando las piezas, tal y como lo describe el autor: alguien intenta entrar como root con una contraseña de 300 bytes. Como el campo de root es *, sshd toma prestada la sal SHA-512 de otra cuenta y llama a crypt(). musl ve que la contraseña pasa de 256 bytes, falla y devuelve "*". sshd compara "*" con el campo guardado, que también es "*", y la contraseña es correcta.
flowchart LR
A["Contraseña de 300 bytes"] --> B["sshd usa la sal de otra cuenta"]
B --> C["crypt de musl falla · devuelve *"]
C --> D["Compara * con el campo de root · *"]
D --> E["Acceso concedido"]El detalle de glibc es casi anecdótico: en 2013 alguien pidió en Debian que glibc copiara el comportamiento de musl. Debian acabó pasándose a libxcrypt, que en caso de fallo devuelve "*0". Un carácter de diferencia, y por eso las distribuciones con glibc no están afectadas.
Qué hacer
La solución es actualizar OpenSSH a la 10.6p2 o posterior. En Alpine:
apk update && apk upgrade openssh
rc-service sshd restart
Si no puedes actualizar todavía, desactiva el login por contraseña, que en un servidor expuesto debería estar desactivado de todas formas:
echo "PasswordAuthentication no" > /etc/ssh/sshd_config.d/00-no-passwords.conf
rc-service sshd restart
Para comprobar una máquina, el autor ha publicado una herramienta en github.com/oz9un/sshole. Si en su salida aparece una línea como root@tu-maquina, una cuenta sin contraseña ha aceptado un login por contraseña, y esa máquina está expuesta.
En contenedores, lo habitual es que no haya sshd, y entonces no hay nada que hacer. Pero si tienes imágenes basadas en Alpine que sí lo incluyen (para depurar, para acceso de mantenimiento, para algún servicio antiguo), conviene revisarlas y reconstruirlas.
Lo que me llevo
Hace una semana escribía sobre el incidente de Alpine con GitLab y Ruby, donde actualizar no bastaba por cómo interactuaban dos piezas. Este caso tiene algo parecido: musl hizo bien en no devolver NULL, el límite de 256 bytes tiene sentido, la sal prestada de OpenSSH arreglaba un problema real y Alpine cambió root a * por una buena razón. Ninguna de esas decisiones es un error si se mira sola, y el fallo sólo aparece cuando coinciden las cuatro.
Es un buen recordatorio de que el valor que devuelve una función cuando falla también forma parte de su contrato. Y de que PasswordAuthentication no sigue siendo una de las líneas más rentables que se pueden poner en la configuración de un servidor.
Fuente: sshhole, Ozgun Kultekin, Wakefield.




