Volver al Blog

257 bytes abren la cuenta a la que le quitaste la contraseña

Armario de comunicaciones de pared abierto, con un conmutador de 16 puertos y una maraña de cables dentro

La nota de seguridad de OpenSSH 10.6p2, del 5 de noviembre, tiene un solo párrafo, y una de sus frases dice: «We consider the root cause of this bug to be outside sshd, but it is exploitable via sshd and can be avoided there.» Rich Felker, mantenedor de musl, responde con la misma calma que lo suyo tampoco es un fallo, sino «an erroneous use by the caller». Los dos tienen razón. Y entre las dos frases cabe una sesión como root.

Tres piezas, ninguna rota

CVE-2026-123158, al que el investigador que lo encontró bautizó *sshole, tiene CVSS 8,1 y una explicación que se cuenta en tres pasos. Ninguno de los tres es un error.

Uno: musl devuelve un asterisco cuando no puede. La implementación de crypt_sha512() se planta con claves de más de 256 bytes, y en ese caso crypt() devuelve la cadena "*". Funciona así desde 2012, y no por descuido: POSIX dice que ante un error hay que devolver un puntero nulo y poner errno, pero musl decidió devolver una cadena a propósito porque, en sus palabras, «a large portion of programs which call crypt are not prepared for its failure and do not check that the return value is non-null». Es decir: se saltaron la norma para protegerte de tu propio código descuidado.

No hace falta creernos. Son unas pocas líneas de src/crypt/crypt_sha512.c, que hemos ido a leer al repositorio de musl:

/* key limit is not part of the original design,
 * added for DoS protection. [...] */
#define KEY_MAX 256

    /* reject large keys */
    for (i = 0; i <= KEY_MAX && key[i]; i++);
    if (i > KEY_MAX)
        return 0;

    [...]

    p = sha512crypt(key, setting, output);
    q = sha512crypt(testkey, testsetting, testbuf);
    if (!p || q != testbuf || memcmp(...))
        return "*";
    return p;

Ahí hay dos cosas que no hemos visto contadas en ninguna cobertura. La primera: el tope de 256 bytes tampoco es un capricho, lleva encima su propio comentario explicando que se añadió «for DoS protection», porque el coste del algoritmo crece con el cuadrado de la longitud de la clave. La segunda: ese return "*" salta por tres motivos a la vez, y dos de los tres son que la función falle una autocomprobación que se hace a sí misma en cada llamada. Las dos son medidas de endurecimiento. La cadena que acaba abriendo la cuenta es, de principio a fin, el resultado de proteger la máquina.

Dos: en Unix, un asterisco quiere decir «por aquí no se entra». Cuando el campo de contraseña de /etc/shadow contiene un *, la cuenta no tiene contraseña válida y ningún hash podrá coincidir jamás con ese valor. Es la convención de toda la vida para una cuenta de sistema o para una cuenta que solo entra con clave pública.

Tres: sshd calcula el hash igualmente, por no delatarte. Desde la 7.3, de agosto de 2016, OpenSSH ejecuta siempre el cálculo de contraseña aunque la cuenta esté bloqueada o no exista, para que el tiempo de respuesta no diga si el usuario es real. Y como un * es demasiado corto para servir de sal, toma prestada la sal de la primera cuenta local que sí tenga un hash de verdad. La función se llama pick_salt(), vive en openbsd-compat/xcrypt.c y su comentario explica el criterio sin rodeos: recorre las cuentas del sistema «until we find one that has a valid salt. Usually this will be root unless the root account is locked out». Esa es la razón de la precondición más rara del aviso: hace falta que en la máquina haya, además, alguna cuenta con contraseña normal.

Juntas las tres y el resultado cabe en tres líneas. En auth-passwd.c, la función sys_auth_passwd() compara con strcmp() lo que ha devuelto crypt() contra lo que hay guardado:

# lo que hay en /etc/shadow para una cuenta solo-con-clave
deploy:*:20400:0:99999:7:::

# lo que devuelve musl si la clave pasa de 256 bytes
crypt("AAAA...257...AAAA", salt)  ->  "*"

# lo que hace sshd con las dos cosas
strcmp("*", "*") == 0  ->  autenticado

Las tres decisiones tienen fecha: 2012 (musl elige devolver una cadena en vez de un nulo), agosto de 2016 (OpenSSH 7.3 empieza a tomar prestada la sal) y la configuración por defecto de Alpine que completa la cadena. Diez años. Nadie metió la pata; simplemente nadie estaba mirando el hueco entre las tres.

Por qué en Debian no pasó nada

El mismo sshd, con el mismo strcmp() y la misma sal prestada, lleva diez años corriendo sobre glibc sin que esto sea explotable. El motivo es casi gracioso: la biblioteca de cifrado de contraseñas que usan esos sistemas devuelve "*0" o "*1" cuando falla, nunca un asterisco pelado. Dos caracteres en vez de uno.

La única lección del caso que dura más que el parche está aquí: lo que separaba tu servidor de una sesión abierta no era un control de seguridad. Era una convención sobre cómo se marca un error en un valor de retorno, elegida por dos proyectos distintos sin hablar entre ellos y sin que nadie la documentara nunca como frontera. En everyWAN llevamos tiempo repitiendo que el fallo es inevitable y la avería es una decisión de diseño; este caso añade un matiz menos cómodo: a veces la avería no llega a ocurrir gracias a una decisión que nadie tomó.

Las cuentas que blindaste son las que se abren

Una cuenta con contraseña —incluso con una contraseña mala— no es explotable así, porque su campo contiene un hash de verdad y el asterisco no coincide con él. Las cuentas que sí se abren son las que tienen un *: las que alguien, en algún momento, decidió dejar solo con clave pública. Es decir, las que están bien hechas.

La lista de objetivos que se vio en los primeros ataques lo dice sin rodeos. La firma de inteligencia Previdian detectó el primer intento a menos de nueve horas de la publicación del CVE: ocho intentos, una sola dirección IP alojada en Japón, un solo sensor. Las cuentas que probaron fueron root, usuarios de despliegue solo-con-clave, usuarios de directorio y el usuario por defecto de las imágenes Alpine de AWS y Azure. En uno de los señuelos llegaron a root.

Esa lista no es casual: es el inventario de todo lo que, en una máquina moderna bien montada, no tiene contraseña. El usuario que usa tu tubería de despliegue para entrar y hacer docker stack deploy. Los usuarios que vienen de un directorio y no tienen entrada local. El usuario de la imagen de nube que nadie ha tocado desde que se creó la instancia. Y un apunte honesto sobre root, porque conviene no exagerar: sshd no permite entrar como root por contraseña en su configuración por defecto, así que ahí el atacante estaba probando suerte. En el resto, no.

El aviso de la propia Alpine lo resume en dos frases que no admiten mucha interpretación: «An attacker who can reach your SSH port can log in without knowing any password. It takes one connection, works every time, and needs no retries.» Lo dice la distribución afectada sobre sí misma, que no es el sitio donde uno suele encontrar exageraciones.

Lo que está expuesto de verdad (y lo que no)

Aquí viene el consejo que no vas a leer en la mayoría de coberturas: no corras a reconstruir tus trescientas imágenes Alpine. La inmensa mayoría de contenedores basados en Alpine no llevan sshd dentro, y sin un sshd escuchando esto sencillamente no existe. Dedicar el viernes a reconstruirlos todos es gastar el único recurso escaso que tienes —la atención del equipo— en el sitio equivocado.

Las condiciones tienen que darse todas a la vez: biblioteca musl; sshd compilado o configurado sin PAM (el paquete openssh-server por defecto de Alpine, o el -pam con UsePAM no); autenticación por contraseña habilitada; una cuenta con asterisco; y otra cuenta con hash real de la que sacar la sal. Quitar cualquiera de las cinco desactiva el ataque. Un matiz contra nuestro propio titular, por si acaso: el umbral de 257 bytes vale para los hashes SHA-256 y SHA-512, que son los normales; según el análisis de TuxCare, con otro tipo de hash en la cuenta de la que se toma la sal el límite de longitud deja de hacer falta. O sea que 257 es el caso típico, no el único.

OpenWrt aparece en casi todas las listas de afectados y casi nunca lo está. OpenWrt usa musl, sí, pero su servidor SSH por defecto es Dropbear, y Dropbear no está afectado —lo dice el propio mantenedor de musl—. Solo cuenta si alguien instaló a mano el paquete de servidor de OpenSSH. La misma precisión vale para Buildroot y Yocto: la pregunta no es «¿esto es musl?», sino «¿esto es musl y tiene OpenSSH escuchando?». Esa distinción te puede ahorrar media semana, y es el tipo de matiz que marca la diferencia entre una lista de afectados y un plan.

Donde sí muerde es en los sitios que nadie llama «servidor»: imágenes de construcción y ejecutores de integración continua con sshd para depurar, contenedores de diagnóstico, bastiones montados sobre Alpine porque pesaba poco, aparatos empotrados sobre Buildroot o Yocto, e instancias de nube levantadas desde una imagen Alpine oficial. Es el mismo patrón que vimos con el SSH de los equipos de red: la caja no está en el inventario porque no parece un servidor.

El peor caso está en el aviso de Alpine y casi nadie lo ha destacado: las imágenes oficiales de nube tiny-cloud, de la 3.18.11 a la 3.24.3, traen un usuario alpine sin contraseña y con doas sin contraseña. Ahí no hablamos de entrar como un usuario cualquiera: entrar como alpine es entrar como root, en un salto. Si levantaste una instancia desde una de esas imágenes y la dejaste con el puerto 22 abierto, esa es la máquina por la que empezar hoy. Dos matices honestos en sentido contrario: la propia Alpine acota que eso se vuelve explotable cuando en el sistema hay algún hash de contraseña, y la exposición real depende de tu grupo de seguridad.

Un contenedor no se parchea: se vuelve a construir

Si uno de los que sí están afectados es un contenedor, hay una trampa clásica esperándote. Entrar y hacer apk upgrade arregla el problema hasta el siguiente despliegue, momento en el que el orquestador vuelve a bajar la imagen de siempre y el agujero reaparece sin que nadie se entere. El arreglo de verdad es reconstruir la imagen y volver a desplegarla.

Eso exige tres cosas que no se improvisan un jueves por la tarde: saber qué imágenes tienes y dónde corren, tener la imagen base anclada a una versión concreta —no a latest, que no es una versión— y tener una tubería que reconstruya y despliegue sin que nadie toque un servidor a mano. Nosotros lo hacemos así en la infraestructura que operamos: Docker Swarm y Kubernetes con despliegue continuo desde GitLab, y NetBox como fuente única de verdad de lo que existe. No es sofisticación: es que el día que sale un CVE así, la diferencia entre media hora y tres días es exactamente esa.

La versión de OpenSSH no te dice si estás parcheado

Y para lo que no es un contenedor —el aparato del armario del pasillo, el bastión que montó alguien que ya no está— la vía es el paquete de la distribución. Alpine publicó el arreglo para diez ramas: las nueve estables de la 3.16 a la 3.24, más edge. Y conviene subrayar lo que eso significa, porque su propio aviso titula una sección «Releases that are no longer supported (3.21 and older)» y explica que «3.21 reached its end of support on 2026-11-01, but we have published fixed packages for each release all the way back to 3.16». Seis ramas fuera de soporte parcheadas, una de ellas caducada cuatro días antes del CVE. Las versiones concretas no salen en casi ninguna cobertura, así que las hemos sacado nosotros de su base de datos de seguridad:

3.16  openssh-9.0_p1-r6     3.21  openssh-9.9_p2-r1
3.17  openssh-9.1_p1-r7     3.22  openssh-10.0_p1-r11
3.18  openssh-9.3_p2-r4     3.23  openssh-10.2_p1-r1
3.19  openssh-9.6_p1-r3     3.24  openssh-10.3_p1-r2
3.20  openssh-9.7_p1-r6     edge  openssh-10.6_p2-r0

Mira la columna con atención, porque tiene una consecuencia práctica que cuesta un rato descubrir sola: solo edge lleva la 10.6p2 de verdad. Las nueve ramas estables reciben el parche portado sobre lo que ya tenían, que va de una 9.0 de 2022 a una 10.3. Dicho de otro modo: la versión de OpenSSH no te dice si estás parcheado. Un sistema que anuncia OpenSSH_9.0 puede estar corregido y otro que anuncia 10.3 puede no estarlo. Lo que hay que mirar es la versión del paquete, con su sufijo -rN. Si no puedes tocar la máquina hoy, la mitigación la tienes más abajo.

Y el paso que convierte todo lo anterior en humo si se te olvida, dicho por Alpine con todas las letras: «Upgrading the package alone does not replace the running daemon.» Hasta que no hagas rc-service sshd restart, el proceso que está aceptando conexiones sigue siendo el de antes. Un parque entero puede acabar así: actualizado en el inventario y vulnerable en el puerto.

Las cuatro comprobaciones de esta semana

1. Qué de lo tuyo habla musl y además escucha en el 22. Las dos preguntas van juntas; por separado no sirven de nada. La primera se contesta en un segundo —si existe /lib/ld-musl-*, es musl— y la segunda con el gestor de paquetes, que además te da la revisión -rN, que es lo único que sirve para saber si estás corregido:

ls /lib/ld-musl-* 2>/dev/null
apk list -I 'openssh*'

2. Qué permite de verdad tu configuración, no la que crees. Hay una diferencia importante entre lo que pone en el fichero y lo que sshd aplica después de leer los Include. Se pregunta así, con dos avisos: sshd está en /usr/sbin y necesita root y claves de anfitrión ya generadas, y los bloques Match no se evalúan a menos que le pases un caso concreto con -C user=...,host=...,addr=.... Si tienes políticas por usuario o por origen, la salida de abajo no te las está contando:

/usr/sbin/sshd -T | grep -E '^(usepam|passwordauthentication|permitrootlogin)'

# mitigación provisional de Alpine, si no puedes actualizar hoy
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

Ese bloque es el de Alpine, copiado tal cual, y vale la pena entender por qué no basta con la primera línea. El fichero suelto en sshd_config.d/ solo sirve si el sshd_config principal trae la directiva Include. De ahí el sed, que hace dos cosas: cambia a no todas las líneas PasswordAuthentication yes del fichero incluidas las que estén dentro de un bloque Match, y añade la directiva al principio si no había ninguna. Lo de «al principio» no es cosmético: sshd se queda con el primer valor que encuentra de cada palabra clave, así que una línea añadida al final de un fichero que ya decía yes no anula nada. Y el aviso que Alpine pone antes de todo esto, que suscribimos: asegúrate de que tu acceso por clave funciona y no cierres la sesión actual hasta haber probado una nueva.

3. Qué cuentas llevan el asterisco. Una línea, y es la comprobación que de verdad te dice algo que no sabías:

awk -F: '$2 == "*" { print $1 }' /etc/shadow
awk -F: '$2 == "*" { print $1 }' /etc/passwd

Esa salida es el grueso de la lista de cuentas que este fallo abre en esa máquina, pero no toda: una cuenta puede llevar el asterisco en /etc/passwd y no tener entrada en /etc/shadow, así que repite el mismo awk sobre el otro fichero. Y si tus usuarios vienen de un directorio, no estarán en ninguno de los dos: esos hay que preguntárselos al directorio. Léela despacio, porque hay dos tipos de nombre ahí dentro: cuentas de sistema que nunca van a entrar por SSH de todas formas, y las cuentas que alguien decidió endurecer. Las segundas son las que importan, y son las que en cualquier auditoría salen marcadas en verde.

4. El registro, cruzado con esa lista. Esto no es una tarea de parcheo, es la búsqueda de un incidente, y es la única de las cuatro que hay que hacer aunque ya hayas actualizado. Busca un Accepted password —en Alpine, en /var/log/messages— para cualquiera de las cuentas de la lista anterior. Una cuenta que no tiene contraseña no puede haber entrado con contraseña: si aparece, no es un aviso, es un incidente, y la fecha a partir de la cual mirar es el 5 de noviembre.

El inventario que importa no es de servidores

El parche está en sshd, y no porque el fallo fuera de sshd: está ahí porque es donde se alcanzaba. Hemos ido a leer el código ya corregido, y la pieza son dos condiciones añadidas a un if: que la sal no sea nula y que el campo almacenado mida al menos MIN_PASSWORD_HASH_LEN, una constante que el propio fichero define con el valor 4. Un *, un ! o una x ya no pueden dar nunca una comparación válida. Por el otro lado, en el mismo mensaje donde dice que no es un fallo suyo, Felker añade algo que se cita bastante menos: «the mechanism by which this happened is something we have been wanting to change for a while now, and it very likely will be changed in the upcoming release cycle». Y recomienda, mientras tanto, el mismo ajuste que te hemos dado arriba: «Anyone who cannot immediately upgrade should edit /etc/ssh/sshd_config to set: PasswordAuthentication no». Dos arreglos para un fallo que, formalmente, no tenía dueño.

La conclusión que nos llevamos no es «parchea más rápido». Eso ya lo sabías, y además no siempre hay parche que aplicar. Es otra, y es más incómoda: durante diez años, la línea que separaba a cualquiera de entrar en tu bastión no estaba en ningún documento, en ninguna política y en ningún cuadro de mandos. Estaba en el hueco entre tres proyectos que hacían bien su trabajo por separado. Contra eso no hay control que valga; lo único que ayuda es tener la lista completa de cosas que escuchan —no la lista de servidores, la de cosas que escuchan— y poder reconstruirlas en horas. Esa lista, en la mayoría de empresas que nos encontramos, no la tiene nadie entera.

Fuentes (verificadas el 09-11-2026): la frase «We consider the root cause of this bug to be outside sshd, but it is exploitable via sshd and can be avoided there», el alcance («affects some Linux systems when built and run with PAM support disabled») y el crédito del hallazgo a Özgün Kültekin, de Wakefield — nota de la versión OpenSSH 10.6p2, 05-11-2026. El mecanismo (que musl devuelve la cadena «*» cuando crypt() falla y que su código SHA-512 rechaza claves de más de 256 bytes lo hemos verificado nosotros en el fuente: src/crypt/crypt_sha512.c, con el #define KEY_MAX 256, su comentario «added for DoS protection» y el return "*" de __crypt_sha512, la ventana de 257 a 1.024 bytes, la sal prestada de otra cuenta con hash válido introducida en OpenSSH 7.3 en agosto de 2016 como defensa contra la enumeración de usuarios, la comparación con strcmp() en sys_auth_passwd() de auth-passwd.c, que hemos leído en el repositorio junto con pick_salt() y su comentario en openbsd-compat/xcrypt.c y el #define MIN_PASSWORD_HASH_LEN 4 del código ya corregido, el CVSS 8,1 y el hecho de que glibc con libxcrypt devuelve «*0» o «*1» y por eso no es explotable), la posición de Rich Felker («It is my position that this is not a bug in musl just an erroneous use by the caller») y su anuncio de cambiar el comportamiento, la justificación histórica de musl para no devolver un puntero nulo («a large portion of programs which call crypt are not prepared for its failure and do not check that the return value is non-null») y la forma del arreglo en 10.6p2 (rechazar campos almacenados de menos de cuatro caracteres) — análisis de TuxCare, OX Security y Horizon3.ai. Que Dropbear no está afectado según el propio mantenedor de musl, y que por tanto OpenWrt solo cuenta donde alguien instaló el servidor de OpenSSH, es la precisión que recogen esos mismos análisis. Las versiones corregidas de Alpine las hemos consultado nosotros, rama por rama, en la base de datos de seguridad oficial (secdb.alpinelinux.org, ficheros main.json de v3.16 a v3.24 y edge): las diez tienen entrada para este CVE, y la observación de que solo edge lleva la 10.6p2 mientras las estables reciben el parche portado —y por tanto que la versión de OpenSSH no indica si estás corregido— es lectura nuestra de esa tabla. La detección en campo (primer intento a menos de nueve horas de la publicación, ocho intentos desde una única IP alojada en Japón en un solo sensor, cuentas objetivo root, usuarios de despliegue solo-con-clave, usuarios de directorio y el usuario por defecto de las imágenes Alpine de AWS y Azure, y un acceso como root en un señuelo) — Help Net Security, 06-11-2026. Las dos frases citadas («An attacker who can reach your SSH port…» y «It takes one connection, works every time, and needs no retries»), la tabla de versiones corregidas rama por rama, la advertencia de que actualizar el paquete no sustituye el demonio en ejecución y el dato de las imágenes tiny-cloud de la 3.18.11 a la 3.24.3 con usuario alpine sin contraseña y doas sin contraseña — aviso de seguridad de Alpine Linux, 05-11-2026; conviene decir que esas frases son de ese aviso y no de Damien Miller, a quien Alpine acredita por el arreglo junto a Darren Tucker. El análisis de TuxCare describe un segundo intento necesario en la misma conexión en ciertas configuraciones; el aviso de la distribución afectada dice lo contrario («needs no retries») y nos quedamos con el de la distribución, que es quien responde de sus paquetes. La lectura de que la convención de valor de error hacía de frontera de seguridad sin que nadie la declarara, el consejo de no reconstruir todas las imágenes Alpine y las cuatro comprobaciones son elaboración nuestra. Fotografía: «A front-facing shot of a black wall-mounted network rack…», de Mohammed Kateregga, CC0 1.0 vía el directorio de fotos de WordPress, recortada y aclarada por nosotros.

¿Sabes cuántas cosas tuyas escuchan en el puerto 22?

Levantamos el inventario de lo que tienes encendido —servidores, contenedores, aparatos y lo que montó alguien que ya no está—, con su versión y su fecha de último parche. Es la parte menos vistosa del mantenimiento informático, y es la que decide si un aviso de un miércoles te cuesta media hora o media semana.

Hablar con everyWAN

Etiquetas:

Compartir:

Suscríbete a nuestra newsletter

Para recibir historias del mundo IT, novedades de everyWAN y ofertas exclusivas para suscriptores, date de alta a nuestra lista de correo

everyWAN
everyWAN