Tornar al Blog

257 bytes obren el compte al qual vas treure la contrasenya

Armari de comunicacions de paret obert, amb un commutador de 16 ports i una embolicada de cables a dins

La nota de seguretat d'OpenSSH 10.6p2, del 5 de novembre, té un sol paràgraf, i una de les seves frases diu: «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, mantenidor de musl, respon amb la mateixa calma que el seu tampoc és un error, sinó «an erroneous use by the caller». Tots dos tenen raó. I entre les dues frases hi cap una sessió com a root.

Tres peces, cap de trencada

CVE-2026-123158, que l'investigador que el va trobar va batejar *sshole, té CVSS 8,1 i una explicació que es conta en tres passos. Cap dels tres no és un error.

Un: musl retorna un asterisc quan no pot. La implementació de crypt_sha512() es planta amb claus de més de 256 bytes, i en aquest cas crypt() retorna la cadena "*". Funciona així des del 2012, i no per descuit: POSIX diu que davant d'un error cal retornar un punter nul i posar errno, però musl va decidir retornar una cadena expressament perquè, en les seves paraules, «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». És a dir: es van saltar la norma per protegir-te del teu propi codi descurat.

No cal creure'ns. Són unes poques línies de src/crypt/crypt_sha512.c, que hem anat a llegir al repositori 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;

Allà hi ha dues coses que no hem vist explicades a cap cobertura. La primera: el topall de 256 bytes tampoc no és un caprici, porta a sobre el seu propi comentari explicant que es va afegir «for DoS protection», perquè el cost de l'algorisme creix amb el quadrat de la longitud de la clau. La segona: aquell return "*" salta per tres motius alhora, i dos dels tres són que la funció falli una autocomprovació que es fa a si mateixa a cada crida. Totes dues són mesures d'enduriment. La cadena que acaba obrint el compte és, de cap a peus, el resultat de protegir la màquina.

Dos: a Unix, un asterisc vol dir «per aquí no s'entra». Quan el camp de contrasenya de /etc/shadow conté un *, el compte no té contrasenya vàlida i cap hash no podrà coincidir mai amb aquell valor. És la convenció de sempre per a un compte de sistema o per a un compte que només entra amb clau pública.

Tres: sshd calcula el hash igualment, per no delatar-te. Des de la 7.3, d'agost del 2016, OpenSSH executa sempre el càlcul de contrasenya encara que el compte estigui bloquejat o no existeixi, perquè el temps de resposta no digui si l'usuari és real. I com que un * és massa curt per servir de sal, pren prestada la sal del primer compte local que sí que tingui un hash de debò. La funció es diu pick_salt(), viu a openbsd-compat/xcrypt.c i el seu comentari explica el criteri sense embuts: recorre els comptes del sistema «until we find one that has a valid salt. Usually this will be root unless the root account is locked out». Aquesta és la raó de la precondició més estranya de l'avís: cal que a la màquina hi hagi, a més, algun compte amb contrasenya normal.

Ajunta les tres i el resultat cap en tres línies. A auth-passwd.c, la funció sys_auth_passwd() compara amb strcmp() el que ha retornat crypt() contra el que hi ha desat:

# el que hi ha a /etc/shadow per a un compte només-amb-clau
deploy:*:20400:0:99999:7:::

# el que retorna musl si la clau passa de 256 bytes
crypt("AAAA...257...AAAA", salt)  ->  "*"

# el que fa sshd amb totes dues coses
strcmp("*", "*") == 0  ->  autenticat

Les tres decisions tenen data: 2012 (musl tria retornar una cadena en lloc d'un nul), agost del 2016 (OpenSSH 7.3 comença a prendre prestada la sal) i la configuració per defecte d'Alpine que completa la cadena. Deu anys. Ningú no va ficar la pota; simplement ningú no mirava el buit entre les tres.

Per què a Debian no va passar res

El mateix sshd, amb el mateix strcmp() i la mateixa sal prestada, fa deu anys que corre sobre glibc sense que això sigui explotable. El motiu és gairebé graciós: la biblioteca de xifrat de contrasenyes que fan servir aquests sistemes retorna "*0" o "*1" quan falla, mai un asterisc pelat. Dos caràcters en lloc d'un.

L'única lliçó del cas que dura més que el pedaç és aquesta: el que separava el teu servidor d'una sessió oberta no era un control de seguretat. Era una convenció sobre com es marca un error en un valor de retorn, triada per dos projectes diferents sense parlar entre ells i sense que ningú la documentés mai com a frontera. A everyWAN fa temps que repetim que la fallada és inevitable i que l'avaria és una decisió de disseny; aquest cas hi afegeix un matís menys còmode: de vegades l'avaria no arriba a passar gràcies a una decisió que ningú no va prendre.

Els comptes que vas blindar són els que s'obren

Un compte amb contrasenya —fins i tot amb una contrasenya dolenta— no és explotable així, perquè el seu camp conté un hash de debò i l'asterisc no hi coincideix. Els comptes que sí que s'obren són els que tenen un *: els que algú, en algun moment, va decidir deixar només amb clau pública. És a dir, els que estan ben fets.

La llista d'objectius que es va veure als primers atacs ho diu sense embuts. La firma d'intel·ligència Previdian va detectar el primer intent a menys de nou hores de la publicació del CVE: vuit intents, una sola adreça IP allotjada al Japó, un sol sensor. Els comptes que van provar van ser root, usuaris de desplegament només-amb-clau, usuaris de directori i l'usuari per defecte de les imatges Alpine d'AWS i Azure. En un dels esquers van arribar a root.

Aquella llista no és casual: és l'inventari de tot allò que, en una màquina moderna ben muntada, no té contrasenya. L'usuari que fa servir la teva canonada de desplegament per entrar i fer docker stack deploy. Els usuaris que vénen d'un directori i no tenen entrada local. L'usuari de la imatge de núvol que ningú no ha tocat des que es va crear la instància. I un apunt honest sobre root, perquè convé no exagerar: sshd no permet entrar com a root per contrasenya en la seva configuració per defecte, de manera que allà l'atacant provava sort. A la resta, no.

L'avís de la mateixa Alpine ho resumeix en dues frases que no admeten gaire interpretació: «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.» Ho diu la distribució afectada sobre si mateixa, que no és el lloc on un acostuma a trobar exageracions.

El que està exposat de debò (i el que no)

Aquí ve el consell que no llegiràs a la majoria de cobertures: no corris a reconstruir les teves tres-centes imatges Alpine. La immensa majoria de contenidors basats en Alpine no porten sshd a dins, i sense un sshd escoltant això senzillament no existeix. Dedicar el divendres a reconstruir-los tots és gastar l'únic recurs escàs que tens —l'atenció de l'equip— al lloc equivocat.

Les condicions s'han de donar totes alhora: biblioteca musl; sshd compilat o configurat sense PAM (el paquet openssh-server per defecte d'Alpine, o el -pam amb UsePAM no); autenticació per contrasenya habilitada; un compte amb asterisc; i un altre compte amb hash real d'on treure la sal. Treure'n qualsevol de les cinc desactiva l'atac. Un matís contra el nostre propi titular, per si de cas: el llindar de 257 bytes val per als hashos SHA-256 i SHA-512, que són els normals; segons l'anàlisi de TuxCare, amb un altre tipus de hash al compte d'on es pren la sal el límit de longitud deixa de caldre. És a dir que 257 és el cas típic, no l'únic.

OpenWrt surt a gairebé totes les llistes d'afectats i gairebé mai no ho està. OpenWrt fa servir musl, sí, però el seu servidor SSH per defecte és Dropbear, i Dropbear no està afectat —ho diu el mateix mantenidor de musl—. Només compta si algú va instal·lar a mà el paquet de servidor d'OpenSSH. La mateixa precisió val per a Buildroot i Yocto: la pregunta no és «això és musl?», sinó «això és musl i té OpenSSH escoltant?». Aquesta distinció et pot estalviar mitja setmana, i és la mena de matís que separa una llista d'afectats d'un pla.

On sí que mossega és als llocs que ningú no anomena «servidor»: imatges de construcció i executors d'integració contínua amb sshd per depurar, contenidors de diagnòstic, bastions muntats sobre Alpine perquè pesava poc, aparells encastats sobre Buildroot o Yocto, i instàncies de núvol aixecades des d'una imatge Alpine oficial. És el mateix patró que vam veure amb l'SSH dels equips de xarxa: la caixa no és a l'inventari perquè no sembla un servidor.

El pitjor cas és a l'avís d'Alpine i gairebé ningú no ho ha destacat: les imatges oficials de núvol tiny-cloud, de la 3.18.11 a la 3.24.3, porten un usuari alpine sense contrasenya i amb doas sense contrasenya. Allà no parlem d'entrar com un usuari qualsevol: entrar com a alpine és entrar com a root, en un sol salt. Si vas aixecar una instància des d'una d'aquelles imatges i la vas deixar amb el port 22 obert, aquella és la màquina per on començar avui. Dos matisos honestos en sentit contrari: la mateixa Alpine acota que això esdevé explotable quan al sistema hi ha algun hash de contrasenya, i l'exposició real depèn del teu grup de seguretat.

Un contenidor no es pedaça: es torna a construir

Si un dels que sí que estan afectats és un contenidor, hi ha un parany clàssic esperant-te. Entrar i fer apk upgrade arregla el problema fins al següent desplegament, moment en què l'orquestrador torna a baixar la imatge de sempre i el forat reapareix sense que ningú se n'assabenti. L'arranjament de debò és reconstruir la imatge i tornar-la a desplegar.

Això exigeix tres coses que no s'improvisen un dijous a la tarda: saber quines imatges tens i on corren, tenir la imatge base ancorada a una versió concreta —no a latest, que no és una versió— i tenir una canonada que reconstrueixi i desplegui sense que ningú toqui un servidor a mà. Nosaltres ho fem així a la infraestructura que operem: Docker Swarm i Kubernetes amb desplegament continu des de GitLab, i NetBox com a font única de veritat del que existeix. No és sofisticació: és que el dia que surt un CVE així, la diferència entre mitja hora i tres dies és exactament aquesta.

La versió d'OpenSSH no et diu si estàs pedaçat

I per al que no és un contenidor —l'aparell de l'armari del passadís, el bastió que va muntar algú que ja no hi és— la via és el paquet de la distribució. Alpine va publicar l'arranjament per a deu branques: les nou estables de la 3.16 a la 3.24, més edge. I convé subratllar què vol dir això, perquè el seu propi avís titula una secció «Releases that are no longer supported (3.21 and older)» i 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». Sis branques fora de suport pedaçades, una d'elles caducada quatre dies abans del CVE. Les versions concretes no surten a gairebé cap cobertura, així que les hem tretes nosaltres de la seva base de dades de seguretat:

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 amb atenció, perquè té una conseqüència pràctica que costa una estona de descobrir sol: només edge porta la 10.6p2 de debò. Les nou branques estables reben el pedaç portat sobre el que ja tenien, que va d'una 9.0 del 2022 a una 10.3. Dit d'una altra manera: la versió d'OpenSSH no et diu si estàs pedaçat. Un sistema que anuncia OpenSSH_9.0 pot estar corregit i un altre que anuncia 10.3 pot no estar-ho. El que cal mirar és la versió del paquet, amb el seu sufix -rN. Si no pots tocar la màquina avui, la mitigació la tens més avall.

I el pas que converteix tot l'anterior en fum si se t'oblida, dit per Alpine amb totes les lletres: «Upgrading the package alone does not replace the running daemon.» Fins que no facis rc-service sshd restart, el procés que accepta connexions continua sent el d'abans. Un parc sencer pot acabar així: actualitzat a l'inventari i vulnerable al port.

Les quatre comprovacions d'aquesta setmana

1. Què del teu parla musl i a més escolta al 22. Les dues preguntes van juntes; per separat no serveixen de res. La primera es contesta en un segon —si existeix /lib/ld-musl-*, és musl— i la segona amb el gestor de paquets, que a més et dóna la revisió -rN, que és l'única cosa que serveix per saber si estàs corregit:

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

2. Què permet de debò la teva configuració, no la que creus. Hi ha una diferència important entre el que diu el fitxer i el que sshd aplica després de llegir els Include. Es demana així, amb dos avisos: sshd és a /usr/sbin i necessita root i claus d'amfitrió ja generades, i els blocs Match no s'avaluen si no li passes un cas concret amb -C user=...,host=...,addr=.... Si tens polítiques per usuari o per origen, la sortida de sota no te les està explicant:

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

# mitigació provisional d'Alpine, si no pots actualitzar avui
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

Aquell bloc és el d'Alpine, copiat tal qual, i val la pena entendre per què no n'hi ha prou amb la primera línia. El fitxer solt a sshd_config.d/ només serveix si el sshd_config principal porta la directiva Include. D'aquí el sed, que fa dues coses: canvia a no totes les línies PasswordAuthentication yes del fitxer incloses les que siguin dins d'un bloc Match, i afegeix la directiva al principi si no n'hi havia cap. Això del «al principi» no és cosmètic: sshd es queda amb el primer valor que troba de cada paraula clau, de manera que una línia afegida al final d'un fitxer que ja deia yes no anul·la res. I l'avís que Alpine posa abans de tot això, que subscrivim: assegura't que el teu accés per clau funciona i no tanquis la sessió actual fins haver provat una de nova.

3. Quins comptes porten l'asterisc. Una línia, i és la comprovació que de debò et diu alguna cosa que no sabies:

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

Aquella sortida és el gruix de la llista de comptes que aquesta fallada obre en aquella màquina, però no tota: un compte pot portar l'asterisc a /etc/passwd i no tenir entrada a /etc/shadow, així que repeteix el mateix awk sobre l'altre fitxer. I si els teus usuaris vénen d'un directori, no seran a cap dels dos: aquests cal preguntar-los al directori. Llegeix-la a poc a poc, perquè hi ha dos tipus de nom allà dins: comptes de sistema que no entraran mai per SSH de totes maneres, i els comptes que algú va decidir endurir. Els segons són els que importen, i són els que en qualsevol auditoria surten marcats en verd.

4. El registre, creuat amb aquella llista. Això no és una tasca de pedaços, és la cerca d'un incident, i és l'única de les quatre que cal fer encara que ja hagis actualitzat. Busca un Accepted password —a Alpine, a /var/log/messages— per a qualsevol dels comptes de la llista anterior. Un compte que no té contrasenya no pot haver entrat amb contrasenya: si surt, no és un avís, és un incident, i la data a partir de la qual mirar és el 5 de novembre.

L'inventari que importa no és de servidors

El pedaç és a sshd, i no perquè la fallada fos de sshd: hi és perquè és on s'abastava. Hem anat a llegir el codi ja corregit, i la peça són dues condicions afegides a un if: que la sal no sigui nul·la i que el camp desat mesuri com a mínim MIN_PASSWORD_HASH_LEN, una constant que el mateix fitxer defineix amb el valor 4. Un *, un ! o una x ja no poden donar mai una comparació vàlida. Per l'altra banda, al mateix missatge on diu que no és una fallada seva, Felker hi afegeix una cosa que se cita força menys: «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». I recomana, mentrestant, el mateix ajust que t'hem donat a dalt: «Anyone who cannot immediately upgrade should edit /etc/ssh/sshd_config to set: PasswordAuthentication no». Dues correccions per a una fallada que, formalment, no tenia amo.

La conclusió que ens enduem no és «pedaça més de pressa». Això ja ho sabies, i a més no sempre hi ha pedaç per aplicar. És una altra, i és més incòmoda: durant deu anys, la línia que separava qualsevol d'entrar al teu bastió no era a cap document, a cap política i a cap quadre de comandament. Era al buit entre tres projectes que feien bé la seva feina per separat. Contra això no hi ha control que valgui; l'únic que ajuda és tenir la llista completa de coses que escolten —no la llista de servidors, la de coses que escolten— i poder reconstruir-les en hores. Aquella llista, a la majoria d'empreses que ens trobem, no la té ningú sencera.

Fonts (verificades 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», l'abast («affects some Linux systems when built and run with PAM support disabled») i el crèdit de la troballa a Özgün Kültekin, de Wakefield — nota de la versió OpenSSH 10.6p2, 05-11-2026. El mecanisme (que musl retorna la cadena «*» quan crypt() falla i que el seu codi SHA-512 rebutja claus de més de 256 bytes ho hem verificat nosaltres al font: src/crypt/crypt_sha512.c, amb el #define KEY_MAX 256, el seu comentari «added for DoS protection» i el return "*" de __crypt_sha512, la finestra de 257 a 1.024 bytes, la sal prestada d'un altre compte amb hash vàlid introduïda a OpenSSH 7.3 l'agost del 2016 com a defensa contra l'enumeració d'usuaris, la comparació amb strcmp() a sys_auth_passwd() d'auth-passwd.c, el CVSS 8,1 i el fet que glibc amb libxcrypt retorna «*0» o «*1» i per això no és explotable), la posició de Rich Felker («It is my position that this is not a bug in musl just an erroneous use by the caller») i el seu anunci de canviar el comportament, la justificació històrica de musl per no retornar un punter nul («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») i la forma de l'arranjament a 10.6p2 (rebutjar camps desats de menys de quatre caràcters) — anàlisis de TuxCare, OX Security i Horizon3.ai. Que Dropbear no està afectat segons el mateix mantenidor de musl, i que per tant OpenWrt només compta on algú va instal·lar el servidor d'OpenSSH, és la precisió que recullen aquestes mateixes anàlisis. Les versions corregides d'Alpine les hem consultades nosaltres, branca per branca, a la base de dades de seguretat oficial (secdb.alpinelinux.org, fitxers main.json de v3.16 a v3.24 i edge): les deu tenen entrada per a aquest CVE, i l'observació que només edge porta la 10.6p2 mentre les estables reben el pedaç portat —i per tant que la versió d'OpenSSH no indica si estàs corregit— és lectura nostra d'aquella taula. La detecció en camp (primer intent a menys de nou hores de la publicació, vuit intents des d'una única IP allotjada al Japó en un sol sensor, comptes objectiu root, usuaris de desplegament només-amb-clau, usuaris de directori i l'usuari per defecte de les imatges Alpine d'AWS i Azure, i un accés com a root en un esquer) — Help Net Security, 06-11-2026. Les dues frases citades («An attacker who can reach your SSH port…» i «It takes one connection, works every time, and needs no retries»), la taula de versions corregides branca per branca, l'advertiment que actualitzar el paquet no substitueix el dimoni en execució i la dada de les imatges tiny-cloud de la 3.18.11 a la 3.24.3 amb usuari alpine sense contrasenya i doas sense contrasenya — avís de seguretat d'Alpine Linux, 05-11-2026; convé dir que aquelles frases són d'aquell avís i no de Damien Miller, a qui Alpine acredita per la correcció juntament amb Darren Tucker. L'anàlisi de TuxCare descriu un segon intent necessari a la mateixa connexió en certes configuracions; l'avís de la distribució afectada diu el contrari («needs no retries») i ens quedem amb el de la distribució, que és qui respon dels seus paquets. La lectura que la convenció de valor d'error feia de frontera de seguretat sense que ningú la declarés, el consell de no reconstruir totes les imatges Alpine i les quatre comprovacions són elaboració nostra. Fotografia: «A front-facing shot of a black wall-mounted network rack…», de Mohammed Kateregga, CC0 1.0 via el directori de fotos de WordPress, retallada i aclarida per nosaltres.

Saps quantes coses teves escolten al port 22?

Aixequem l'inventari del que tens encès —servidors, contenidors, aparells i el que va muntar algú que ja no hi és—, amb la seva versió i la seva data d'últim pedaç. És la part menys vistosa del manteniment informàtic, i és la que decideix si un avís d'un dimecres et costa mitja hora o mitja setmana.

Parlar amb everyWAN

Etiquetes:

Comparteix:

Subscriu-te al nostre butlletí

Per rebre històries del món IT, novetats d'everyWAN i ofertes exclusives per a subscriptors, dona't d'alta a la nostra llista de correu

everyWAN
everyWAN