SSHole: cuando dos asteriscos coinciden
El 5 de noviembre de 2026, OpenSSH 10.6p2 corrigió un solo problema: en algunos sistemas Linux, una cuenta sin contraseña aceptaba cualquier contraseña de entre 257 y 1024 bytes. Es el CVE-2026-123158, que su descubridor, Özgün Kültekin, llamó SSHole. Según las notas de la versión, la causa no está en sshd, aunque sshd puede evitarla.
No hace falta corromper memoria ni romper la criptografía. Son tres decisiones que en principio se podrían considerar razonables, tomadas por separado entre 2012 y 2016, y que terminaron poniendo el mismo carácter, *, a los dos lados de un strcmp, la función de C que compara dos cadenas y da 0 cuando son iguales.
Qué significa * en /etc/shadow
El segundo campo de /etc/shadow guarda el hash de la contraseña, por ejemplo $6$abcdefgh$9SA4XFln…: $6$ es el algoritmo (SHA-512; $5$ es SHA-256), lo que sigue hasta el próximo $ es la sal, un valor aleatorio que se mezcla con la contraseña, y el resto es el hash. Para verificar una contraseña, la biblioteca C calcula crypt(contraseña, sal) y compara el resultado con lo guardado.
Cuando una cuenta no debe entrar con contraseña, en ese campo se pone algo que ningún cálculo pueda producir. Sin PAM, sshd trata distinto las dos marcas habituales:
-
*, que BSD ya usaba en 1980, cierra solo la contraseña; las claves SSH siguen funcionando. -
!al principio, como lo poneusermod -L, marca la cuenta como bloqueada (LOCKED_PASSWD_PREFIX) ysshdrechaza también las claves.
Por eso Alpine, que en 2019 había bloqueado root con !, pasó en la 3.16 a root:*. Esta historia y la de musl las reconstruyó el descubridor en su informe; acá están contrastadas con cada fuente original.
Toda esa convención supone que crypt nunca devuelve *.
Primera y segunda decisión: musl limita el largo y devuelve * cuando falla
En 2012, musl, la biblioteca C de Alpine, limitó a 256 bytes las contraseñas de su SHA-512: según el comentario del código, el tiempo de cálculo crece con el cuadrado del largo, y sin tope cualquiera podría consumir CPU del servidor. Cuando rechaza una contraseña, la función interna devuelve NULL, la manera habitual de avisar en C que no hubo resultado. Pero crypt no lo entrega así:
p = sha512crypt(key, setting, output); ... if (!p || q != testbuf || memcmp(testbuf, testhash, sizeof testhash)) return "*";
Si p es NULL, sale "*" (las otras dos condiciones son una autoverificación). Según el descubridor, la idea era que los programas que no verifican el NULL no terminaran en un segfault: una cadena que no coincide con ningún hash parecía más segura. Lo que no se tuvo en cuenta es que sí coincide con la marca de "cuenta sin contraseña". Medido en Alpine 3.22, con contraseñas de letras A:
len=256 salt=$6$abcdefgh$ -> $6$abcdefgh$9SA4XFlncNTZP.6D5BQ86mdU... len=257 salt=$6$abcdefgh$ -> * len=257 salt=$5$abcdefgh$ -> * len=257 salt=xx -> xxJJqYuu8A9oE
La sal xx del DES tradicional no falla, porque DES usa solo los primeros 8 caracteres.
Esto solo no abre una cuenta: como explica Rich Felker, mantenedor de musl, si la sal empieza con *, como pasaría al usar el propio campo de la cuenta, al fallar devuelve x. La coincidencia aparece solo cuando un programa calcula con la sal de otra cuenta y compara contra el * de esta. Eso hace sshd.
Tercera decisión: sshd calcula con una sal prestada
En 2016, CVE-2016-6210 mostró que el tiempo de respuesta de sshd delataba qué usuarios existían. La corrección fue pick_salt(), que toma prestada la sal del primer hash $…$ que encuentra entre las cuentas del sistema (o usa xx si no hay ninguno) para hacer un cálculo de mentira con el mismo costo. Este es el código de sys_auth_passwd() antes de la corrección:
if (authctxt->valid && pw_password[0] && pw_password[1]) salt = pw_password; encrypted_password = xcrypt(password, salt); return encrypted_password != NULL && strcmp(encrypted_password, pw_password) == 0;
pw_password es el campo de /etc/shadow, y la condición pide al menos dos caracteres para usarlo como sal. En C una cadena termina con un byte nulo, así que si el campo es *, la posición [1] ya es ese final, salt queda en NULL y xcrypt, el envoltorio de OpenSSH alrededor de crypt, recurre a pick_salt(). Esa rama se pensó para usuarios inexistentes, pero un campo de un carácter cae en el mismo camino.
La comparación
En un Alpine sin PAM, una contraseña de 300 bytes contra deploy:*, un usuario que entra solo con clave SSH, recorre estos pasos:
-
pw_passwordvale"*", y la sal queda enNULL. -
pick_salt()devuelve la sal de otro usuario, digamos$6$Qw3r…$. - musl recibe 300 bytes, supera el límite y devuelve
"*". -
strcmp("*", "*")da 0, y la contraseña se acepta.
De ahí salen las condiciones del advisory de Alpine, que tienen que darse todas a la vez:
-
musl y sin PAM: con PAM, la verificación nunca llega a
sys_auth_passwd(). Elopenssh-serverde Alpine se compila sin PAM. -
PasswordAuthentication yes, el valor por omisión. -
Una cuenta con
*: con!, el paso 4 compara"*"con"!"y falla. -
Un hash
$5$o$6$como primero que encuentrapick_salt(): si es MD5 o bcrypt, que en musl no rechazan 1024 bytes (medido), o si no hay ninguno, no hay falla. -
Entre 257 y 1024 bytes: el techo es
MAX_PASSWORD_LEN, quesshdaplica antes de calcular.
Hace falta, además, un intento fallido previo: StepSecurity reprodujo la falla y vio que la contraseña larga se rechaza si es el primer intento de la conexión, y se acepta desde el segundo, algo que el descubridor ya había anotado. Ninguna de las dos fuentes explica por qué.
Un matiz sobre root
Alpine trae root:*, pero para el UID 0 auth_password() anula el resultado salvo que PermitRootLogin valga yes, y su valor por omisión es prohibit-password desde OpenSSH 7.0. Con la configuración de Alpine, root no entra por esta vía. La cadena llega a root por otro lado, como se ve más abajo.
Por qué RHEL, Fedora y Debian no están afectados
No es solo que usen PAM. Aun con UsePAM no, su crypt es el de libxcrypt, que falla de otra manera. Con el mismo programa y la sal $6$abcdefgh$:
| Sistema | libxcrypt | 257 a 511 bytes | 512 bytes | 1024 bytes |
|---|---|---|---|---|
| AlmaLinux 8.10 | 4.1.1 | hash normal | hash normal | hash normal |
| AlmaLinux 9.8 | 4.4.18 | hash normal | NULL |
NULL |
| AlmaLinux 10.2 | 4.4.36 | hash normal | NULL |
no medido |
| Fedora 44 | 4.5.2 | hash normal | NULL |
NULL |
| Debian trixie | 4.4.38 | hash normal | *0 |
*0 |
Ninguna devuelve *:
-
AlmaLinux 8, compilado del mismo código fuente que RHEL 8, calcula el hash en todo el rango que acepta
sshd, aunque sucrypt.hdeclara un límite de 512. -
AlmaLinux 9 y 10 y Fedora devuelven
NULL, quesys_auth_passwd()ya descartaba. -
Debian devuelve
*0, que no coincide con*.
Los trackers no lo reflejan igual, y eso confunde a los escáneres:
- Red Hat marca Not affected en RHEL 8, 9 y 10, pero le asigna severidad Critical (CVSS 9.8, en borrador).
- Debian marca vulnerable bookworm y trixie, aunque no lo son.
-
Alpine corrigió sin cambiar la versión que anuncia
sshd(10.3_p1-r2sigue diciendoOpenSSH_10.3p1), y StepSecurity vio que el script de verificación del descubridor marcaba como expuesto un equipo ya corregido. - Un escáner que pruebe una sola contraseña larga por conexión da por seguro un equipo vulnerable, por el intento previo que hace falta.
Qué corrige 10.6p2
El commit no toca musl: agrega una condición a la comparación de sshd.
ret = encrypted_password != NULL && strlen(pw_password) >= MIN_PASSWORD_HASH_LEN && strcmp(encrypted_password, pw_password) == 0;
MIN_PASSWORD_HASH_LEN vale 4: un campo más corto no es un hash sino una marca, y no puede coincidir con nada. Otro commit, del 9 de noviembre y todavía sin versión publicada, rechaza además todo cálculo hecho con una sal prestada.
Alpine publicó paquetes corregidos el mismo día para todas las ramas desde la 3.16, pero el sshd que está corriendo sigue siendo el viejo hasta que se reinicia. Mientras tanto, PasswordAuthentication no cierra el camino. En el log, la señal es un Failed password seguido de un Accepted password para la misma cuenta sin contraseña, en la misma sesión.
El alcance real
Para el parque típico de RHEL y derivados, SSHole no cambia nada. Fuera de ese parque, lo que decide si un equipo está expuesto no es la plataforma, sino que se cumplan todas las condiciones a la vez. Basta que falte una:
- una máquina virtual Alpine creada con tiny-cloud (de la 3.18.11 a la 3.24.3, para AWS y Azure) no las cumple recién instalada, según el advisory, pero sí cuando aparece una cuenta con hash
$6$; desde ahí, su usuarioalpine, que tiene*, entra y llega a root condoas, elsudode Alpine, que no le pide contraseña; - una creada con cloud-init, que escribe
PasswordAuthentication no, tampoco, salvo que alguien cambie esa opción; - un equipo OpenWrt con Dropbear o un contenedor que no ejecuta
sshdno tienen el componente vulnerable en uso.
En contenedores el caso es la excepción: ejecutar sshd adentro no es lo habitual, Docker recomienda que cada contenedor tenga una sola responsabilidad, y las plataformas dan su propio acceso. En Azure Container Apps se entra con az containerapp exec, sin SSH, y un puerto TCP solo se publica hacia afuera con una red virtual. La pregunta útil no es qué plataforma se usa, sino si alguna imagen Alpine ejecuta sshd.
Dónde estuvo el error es materia de discusión. Para Felker no es un bug de musl sino un uso erróneo de quien llama: un token de falla garantiza que crypt(contraseña, token) nunca dé el token, pero nadie puede garantizarlo si la sal es de otra cuenta. Aun así, el carácter no era neutral: musl usaba como token el mismo que shadow usa como marca, y por eso el error de sshd abrió cuentas en lugar de fallar sin consecuencias. Felker anunció que va a proponer pasar a *0 y *1, como libxcrypt; mientras ese cambio no llegue (al 10 de noviembre no está), cualquier otro programa que en musl calcule con una sal ajena y compare contra un * queda expuesto al mismo problema.

Comentarios
Comments powered by Disqus