Principaux enseignements
- Un mot de passe compris entre 257 et 1 024 octets permet de se connecter à n'importe quel compte utilisant uniquement une clé sur les systèmes Linux exécutant sshd avec la bibliothèque musl libc sans PAM, notamment Alpine Linux. OpenSSH 10.6p2 intègre déjà la solution de contournement, et ce bogue est référencé sous le numéro CVE-2026-123158.
- La faille se situe à la jonction de deux projets. Depuis 2016, sshd utilise le sel d’un autre compte pour un compte *, et la fonction crypt() de musl renvoie la chaîne « * » en réponse à un mot de passe trop long, ce qui correspond au * stocké. OpenSSH affirme que la cause première se situe en dehors de sshd ; le responsable de musl estime quant à lui qu’il s’agit d’une utilisation erronée de la part de l’appelant.
- Nous avons reproduit ce problème sous Alpine Linux 3.18 : lors de la deuxième tentative au cours d'une même connexion, la connexion aboutit, un hachage bcrypt supprime la limite de longueur, et le paquet corrigé rejette toutes les tentatives. Mettez à jour et redémarrez sshd, ou désactivez la connexion par mot de passe ; sous Alpine 3.18 et les versions antérieures, le simple fait d'ajouter le fichier sshd_config.d ne change rien.
- Alpine Linux 3.18 a atteint sa fin de vie en mai 2025 et a bénéficié, à titre exceptionnel, d'un correctif. Le programme « Endless Lifecycle Support » (ELS) de TuxCare transforme cela en une promesse : Alpine 3.18 ELS bénéficie du même commit en amont que openssh 9.3_p2.tuxcare.els7.
Le 5 novembre 2026, l'équipe OpenSSH a publié la version 10.6p2, qui apporte une solution provisoire à un problème de sécurité critique. Sur certains systèmes Linux, un attaquant peut se connecter via SSH à un compte qui ne devrait jamais accepter de mot de passe. Il n'y a donc aucun mot de passe à deviner. Il suffit à l'attaquant de connaître le nom d'un tel compte. Dans le scénario le plus courant, l'attaquant envoie un mot de passe erroné, puis un mot de passe d'une longueur comprise entre 257 et 1 024 octets, au cours de la même connexion. Dans certains cas, même un mot de passe court fonctionne, comme nous le montrons ci-dessous.
Cette faille est répertoriée sous le numéro CVE-2026-123158 (score CVSS 3.1 : 8,1). Elle a été découverte par Özgün Kültekin, de Wakefield (wakefield.co). Le chemin d'exécution à l'origine de cette faille est présent dans la version portable d'OpenSSH depuis 2016, de la version 7.3 à la version 10.6p1.
Cette faille est inhabituelle car elle se situe à la jonction de deux projets. L'attaque ne nécessite ni débordement de tampon, ni plantage, ni corruption de mémoire. Il s'agit d'une faille logique qui n'apparaît que lorsque OpenSSH et la bibliothèque C musl s'exécutent conjointement sur le même système, et les deux projets ne parviennent pas à s'accorder sur la responsabilité de cette faille. Les notes de mise à jour d'OpenSSH le formulent ainsi.
« Nous estimons que la cause première de ce bogue se situe en dehors de sshd, mais qu'il peut être exploité via sshd et qu'il est possible de le contourner à ce niveau. »
Le responsable du projet musl, Rich Felker, voit les choses autrement.
« Je considère qu’il ne s’agit pas d’un bug dans musl, mais simplement d’une utilisation erronée de la part de l’appelant. »
Dans cet article, nous analysons le code des deux projets, expliquons étape par étape comment se déroule la connexion et reproduisons l'attaque sur Alpine Linux dans notre laboratoire.
Qui est concerné ?
Cette attaque ne fonctionne que si toutes ces conditions sont réunies sur le même serveur.
- sshd est lié à la bibliothèque libc musl. Alpine Linux en est l'exemple le plus connu, et c'est également la base de nombreuses images Docker, mais seules les images exécutant sshd sont exposées. Ce bogue n'est pas lié à Alpine. Tout système ou périphérique Linux exécutant un sshd compilé avec musl et sans PAM peut être affecté, y compris les firmwares personnalisés et les binaires sshd compilés par l'utilisateur.
- sshd n'utilise pas PAM (le framework d'authentification utilisé par la plupart des distributions Linux pour les connexions) pour vérifier les mots de passe. Sous Alpine, le paquet openssh-server par défaut est compilé sans PAM. Le paquet openssh-server-pam, même lorsqu'il est exécuté avec l'option « UsePAM no », est également concerné.
- La connexion par mot de passe est activée grâce au paramètre « PasswordAuthentication yes », qui est la valeur par défaut.
- Le mot de passe du compte cible est un simple *. Le cas le plus courant est celui d’un utilisateur utilisant uniquement une clé, dont le mot de passe est * dans /etc/shadow. Sous Alpine sans PAM, un compte dont le mot de passe est ! ne peut pas se connecter, même avec une clé SSH ; les administrateurs remplacent donc ! par * pour permettre la connexion par clé. Le compte root de l'image de base Alpine comporte également * depuis la version 3.16 environ, mais sshd bloque la connexion par mot de passe pour root à moins que l'option `PermitRootLogin yes` ne soit activée. Les utilisateurs provenant d'un service d'annuaire via musl-nscd n'ont pas d'entrée dans le fichier shadow et peuvent également se retrouver avec *.
- Le premier compte, selon l'ordre dans /etc/passwd, dont le hachage commence par $ doit utiliser un type de hachage sur lequel musl peut échouer. Avec un hachage $5$ ou $6$ standard (SHA-256 ou SHA-512 crypt), l'attaquant a besoin de 257 à 1 024 octets, et le fichier passwd d'Alpine utilise $6$ par défaut. Avec un hachage bcrypt ($2a$, $2b$ ou $2y$), tout mot de passe non vide d'une longueur maximale de 1 024 octets fonctionne. Avec un ancien hachage $1$ (MD5), un hachage yescrypt $y$ ou en l'absence totale de hachage, l'attaque ne fonctionne pas.
Cette vulnérabilité n'existe pas sur ces systèmes.
- Systèmes autres que Linux.
- Les systèmes Linux équipés de glibc, tels que RHEL, AlmaLinux, CloudLinux, Debian ou Ubuntu.
- Systèmes Linux sur lesquels sshd vérifie les mots de passe via PAM.
Les comptes verrouillés avec un « ! » à la place d’un « * » sont également sécurisés, tout comme les serveurs dont le paramètre PasswordAuthentication est défini sur « no ». Si vous souhaitez uniquement connaître les étapesà suivre, passez directement à la section «Que faire maintenant ? »vers la fin de cet article.
Que signifie une seule étoile dans le fichier /etc/shadow ?
Sous Linux, les hachages des mots de passe sont stockés dans le fichier /etc/shadow. Un hachage est une forme cryptée à sens unique du mot de passe. Lorsque vous vous connectez, le système applique le même hachage à ce que vous avez saisi et compare les deux résultats. S'ils sont identiques, vous êtes connecté.
Une entrée standard se présente comme suit :
alice:$6$Qx7Lm2Pa$k3J9...long hash...:20000:0:99999:7:::
Le « $6 » au début désigne l'algorithme SHA-512, une méthode de hachage des mots de passe basée sur SHA-512. La partie qui suit, « Qx7Lm2Pa », correspond au sel. Un sel est une courte chaîne aléatoire intégrée au mot de passe, ce qui permet à deux utilisateurs ayant le même mot de passe d'obtenir des hachages différents.
Depuis environ 1980, à l’époque où les hachages figuraient encore dans le fichier /etc/passwd, les administrateurs Unix inséraient un simple * dans ce champ lorsqu’un compte ne devait en aucun cas pouvoir se connecter à l’aide d’un mot de passe. Le raisonnement est simple : aucun mot de passe au monde ne donne lieu à un hachage qui se résume à *, car un véritable hachage est toujours bien plus long. Ainsi, le compte peut toujours se connecter à l’aide d’une clé SSH, mais jamais avec un mot de passe. Ce bug vise justement à contourner ce principe.
Comment sshd vérifie un mot de passe sans PAM
Lorsque PAM est désactivé, sshd vérifie lui-même le mot de passe dans la fonction sys_auth_passwd() du fichier auth-passwd.c. Il s'agit du code issu d'OpenSSH 9.3p2, la version fournie avec Alpine Linux 3.18, avant l'application du correctif.
/* Just use the supplied fake password if authctxt is invalid */
char *pw_password = authctxt->valid ? shadow_pw(pw) : pw->pw_passwd;
...
/*
* Encrypt the candidate password using the proper salt, or pass a
* NULL and let xcrypt pick one.
*/
if (authctxt->valid && pw_password[0] && pw_password[1])
salt = pw_password;
encrypted_password = xcrypt(password, salt);
/*
* Authentication is accepted if the encrypted passwords
* are identical.
*/
return encrypted_password != NULL &&
strcmp(encrypted_password, pw_password) == 0;
C'est la méthode classique. Lire la valeur de hachage enregistrée, calculer le hachage du mot de passe saisi en utilisant cette valeur comme sel, puis comparer les deux chaînes de caractères.
Regardez maintenant la ligne « salt ». sshd n'utilise la valeur stockée comme « salt » que si celle-ci comporte au moins deux caractères. Un « * » n'en comporte qu'un seul. Ainsi, pour un compte « * », la valeur « salt » reste NULL, et xcrypt() doit choisir lui-même une valeur « salt ».
Le changement de 2016
Pourquoi la fonction xcrypt() choisit-elle un sel ? La réponse se trouve dans la vulnérabilité CVE-2016-6210. En 2016, un chercheur a démontré que le serveur sshd répondait à des vitesses différentes pour les utilisateurs qui n'existent pas. En envoyant des mots de passe longs et en mesurant le temps de réponse, un attaquant pouvait ainsi dresser une liste de noms d'utilisateurs réels.
Pour combler cette lacune, Darren Tucker a modifié sshd de manière à ce que, lorsqu’il ne dispose d’aucun sel utilisable, il emprunte le type de hachage et le sel d’un compte réel présent sur le système. Le premier commit, 9286875a « Déterminer le sel approprié pour les utilisateurs invalides », empruntait le sel de l’utilisateur root. Six jours plus tard, le commit dbf788b4 « Rechercher parmi les utilisateurs celui disposant d’un sel valide » a permis de rechercher parmi tous les comptes. Ces deux modifications ont été intégrées à OpenSSH 7.3 en août 2016. Ce changement était destiné aux utilisateurs inexistants. Cependant, la vérification à deux caractères décrite ci-dessus envoie un compte valide contenant un astérisque (*) dans le même chemin, ce qui lui permet également d’obtenir le sel emprunté.
Voici la fonction pick_salt() dans le fichier openbsd-compat/xcrypt.c, telle qu'elle apparaît dans la version 9.3p2.
if (salt[0] != '\0')
return salt;
strlcpy(salt, "xx", sizeof(salt));
setpwent();
while ((pw = getpwent()) != NULL) {
if ((passwd = shadow_pw(pw)) == NULL)
continue;
if (passwd[0] == '$' && (p = strrchr(passwd+1, '$')) != NULL) {
typelen = p - passwd + 1;
strlcpy(salt, passwd, MINIMUM(typelen, sizeof(salt)));
explicit_bzero(passwd, strlen(passwd));
goto out;
}
}
out:
endpwent();
return salt;
Il parcourt les comptes dans l'ordre défini dans /etc/passwd et utilise le premier sel $…$ qu'il trouve, par exemple $6$Qx7Lm2Pa pour alice. Il conserve le résultat ; ainsi, les tentatives ultérieures sur la même connexion utilisent le sel mis en cache. Si aucun compte ne dispose d'un hachage $, il revient alors à xx, l'ancien type DES.
Avant cette modification, sshd utilisait « xx » pour les entrées courtes telles que *. L'ancien DES ne rencontre pas de problème avec les mots de passe longs : il utilise simplement les 8 premiers caractères, ce qui fait qu'il ne pouvait jamais renvoyer *. Après la modification, un compte * se voit attribuer le sel d'un autre utilisateur. C'est ainsi que le problème a commencé en 2016. Cette modification constituait une bonne solution à un véritable problème de synchronisation et, à la connaissance de tous, personne n'a remarqué cet effet secondaire.
Ce que la fonction crypt() de musl renvoie en cas d'erreur
musl est une petite bibliothèque C utilisée par Alpine et par de nombreux systèmes embarqués. Son code de chiffrement SHA-512 se trouve dans le fichier src/crypt/crypt_sha512.c. Il limite la longueur des mots de passe afin de protéger le système contre les requêtes lentes et gourmandes en ressources processeur.
/* key limit is not part of the original design, added for DoS protection.
* rounds limit has been lowered (versus the reference/spec), also for DoS
* protection. runtime is O(klen^2 + klen*rounds) */
#define KEY_MAX 256
...
/* reject large keys */
for (i = 0; i <= KEY_MAX && key[i]; i++);
if (i > KEY_MAX)
return 0;
Un mot de passe de plus de 256 octets est rejeté. Voyons maintenant ce que renvoie __crypt_sha512(), la fonction interne appelée par crypt(), dans ce cas précis.
p = sha512crypt(key, setting, output);
/* self test and stack cleanup */
q = sha512crypt(testkey, testsetting, testbuf);
if (!p || q != testbuf || memcmp(testbuf, testhash, sizeof testhash))
return "*";
return p;
En cas d’erreur, la fonction crypt() de musl renvoie la chaîne « * ». Les implémentations SHA-256, MD5 et Blowfish fonctionnent de la même manière, et seules les implémentations SHA-256 et SHA-512 sont soumises à la limite de 256 octets. musl fonctionne ainsi depuis 2012. La norme POSIX stipule que la fonction crypt() doit renvoyer un pointeur nul et définir errno en cas d’erreur, mais musl a délibérément opté pour une chaîne de caractères. Le commit b3c4cc12 explique qu’« une grande partie des programmes qui appellent crypt ne sont pas préparés à un échec et ne vérifient pas que la valeur renvoyée soit non nulle », et qualifie le résultat de « hachage incomparable ».
Le problème, c'est que * correspond également à la valeur qui signifie « connexion sans mot de passe » dans le fichier /etc/shadow.
musl a bien préservé le modèle classique. Regardez la dernière ligne du code DES dans crypt_des.c.
return (setting[0]=='*') ? "x" : "*";
Si le sel lui-même commence par *, la fonction musl renvoie x à la place. Ainsi, crypt(mot de passe, « * ») ne renvoie jamais « * ». Cela permet de contourner la vérification classique, dans laquelle un programme utilise la valeur stockée comme sel, puis compare le résultat à cette même valeur stockée.
Mais sshd n'a pas utilisé la valeur enregistrée comme sel. Il a utilisé le sel d'Alice. Musl a détecté $6$…, cette protection n'a donc pas pu être efficace.
Un petit test
Nous avons vérifié nous-mêmes la partie relative à la bibliothèque C à l'aide d'un petit programme de test. Voici ses deux lignes clés, simplifiées. Il effectue le hachage de mots de passe composés uniquement de la lettre A à l'aide d'un sel emprunté de $6$ caractères, puis compare le résultat avec *.
r = crypt(long_password, "$6$abcdefgh"); /* salt borrowed from another account */
ok = (r != NULL && strcmp(r, "*") == 0); /* compare with the "*" in /etc/shadow */
Nous l'avons testé sous Alpine Linux 3.18 (musl) et sous Debian 12. Sous Debian, comme sur la plupart des systèmes glibc modernes, la fonction crypt() provient d'une bibliothèque distincte appelée libxcrypt.
== musl (Alpine 3.18) ==
len= 8 crypt()=$6$abcdefgh$65STptHV... match "*": no
len= 256 crypt()=$6$abcdefgh$9SA4XFln... match "*": no
len= 257 crypt()=* match "*": YES
len=1024 crypt()=* match "*": YES
== libxcrypt (Debian 12) ==
len= 8 crypt()=$6$abcdefgh$65STptHV... match "*": no
len= 256 crypt()=$6$abcdefgh$9SA4XFln... match "*": no
len= 257 crypt()=$6$abcdefgh$D4Ayo9g5... match "*": no
len=1024 crypt()=*0 match "*": no
Avec musl, tout mot de passe de 257 octets ou plus correspond au caractère « * ». Avec libxcrypt, un mot de passe de 257 octets est haché normalement. libxcrypt a sa propre limite de 511 octets, mais en cas d'échec, elle renvoie « *0 », ou « *1 » lorsque le sel commence par « *0 ». Une chaîne d'erreur de deux caractères ne peut jamais être égale à un caractère « * », donc la comparaison échoue, comme prévu.
L'ensemble du processus, du mot de passe long à la connexion
Voici la chaîne complète d'attaques pour un pirate qui cible un utilisateur disposant uniquement d'une clé sur un serveur Alpine. Cet utilisateur est identifié par * dans le fichier /etc/shadow, et le premier hachage $ du système correspond au hachage $6$ d'un autre utilisateur.
- L'attaquant se connecte via SSH et demande une authentification par mot de passe en tant qu'utilisateur utilisant uniquement une clé.
- L'attaquant envoie un mot de passe de 300 octets. sshd accepte les mots de passe d'une longueur maximale de 1 024 octets (MAX_PASSWORD_LEN) ; il poursuit donc le traitement.
- sshd lit le caractère * dans le fichier /etc/shadow. Comme il ne contient qu'un seul caractère, il n'y a pas de sel.
- La fonction `pick_salt()` emprunte le sel $6$ de l'autre utilisateur.
- Le code SHA-512 de musl détecte plus de 256 octets et échoue. La fonction crypt() renvoie « * ».
- Dès cette première tentative, sshd a déjà perdu la valeur * enregistrée, pour une raison secondaire que nous expliquons ci-dessous, ce qui fait échouer la comparaison. L'attaquant n'a alors qu'à réessayer via la même connexion.
- Lors de la deuxième tentative, sshd compare « * » avec le « * » enregistré. Ils sont identiques. Connexion acceptée.
C'est pourquoi la plage d'attaque est comprise entre 257 et 1 024 octets lorsque le sel emprunté est égal à $5$ ou $6$. En dessous de 257, musl effectue le hachage du mot de passe normalement. Au-delà de 1 024, sshd le rejette avant même de procéder au hachage. Le texte de la CVE indique « entre 256 et 1 024 caractères », mais la plage réelle commence à 257 octets.
Avec bcrypt, la longueur n'a pas d'importance
Il existe une autre faille, qui n'est mentionnée ni dans l'avis d'Alpine ni dans le CVE. Nous l'avons découverte en examinant le code. La fonction `pick_salt()` tronque le hachage au niveau du dernier caractère `$`. Un hachage bcrypt se présente sous la forme `$2b$10$` suivi de 53 caractères, sans caractère `$` entre le sel et le hachage. Ainsi, sshd ne conserve que `$2b$10` et perd le sel lui-même. musl rejette ce paramètre et, en cas d'erreur, renvoie à nouveau « * ». Cette fois-ci, la longueur du mot de passe n'a pas d'importance.
Upstream a corrigé ce bug lié au « salt » le 30 octobre 2026, dans le commit 84f8f466 intitulé « correction de la génération d'un faux « salt » pour les hachages bcrypt ».
+ /*
+ * bcrypt ($2*$NN$) has no '$' between its 22 character
+ * salt and the hash, so keep the salt too or crypt(3)
+ * will reject the setting without doing any work.
+ */
+ if (passwd[1] == '2' && strlen(p + 1) >= 22)
+ typelen += 1 + 22;
Le message de commit fait état d'un problème de synchronisation, mais sur musl, l'impact est bien plus grave. Ce commit se trouve sur la branche master et ne fait pas partie de la version 10.6p2. La solution de contournement de la version 10.6p2 bloque toujours ce chemin, car un * comporte moins de 4 caractères.
Nous l'avons testé sur Alpine 3.18 avec un hachage bcrypt comme premier hachage $ du système. Avec l'ancien paquet, tous les mots de passe non vides compris entre 1 et 1 024 octets permettaient de se connecter au compte * dès la deuxième tentative. Le paquet corrigé les a tous rejetés. Le fichier `passwd` d'Alpine écrit $6$ par défaut ; ce chemin nécessite donc un hachage bcrypt qui doit y être placé par un administrateur ou un outil.
Images de nuages alpins
La note d'alerte d'Alpine identifie l'utilisateur « alpine » dans les images officielles « tiny-cloud » des versions 3.18.11 à 3.24.3. Cet utilisateur dispose de *. Une image vierge n'est pas exposée, car aucun compte ne dispose encore d'un hachage de mot de passe réel. Elle devient exposée dès qu'un compte se voit attribuer un hachage $6$ alors que la connexion par mot de passe est activée. Les images configurées via cloud-init, qui définit PasswordAuthentication sur « no », ne sont pas concernées. Le chercheur note que sur les images officielles d’AWS et d’Azure, l’utilisateur « alpine » peut exécuter la commande « doas » (l’équivalent de « sudo » sous Alpine) sans mot de passe ; par conséquent, une connexion en tant qu’« alpine » revient à se connecter en tant que « root ».
Nous l'avons reproduit sur Alpine
Le test crypt() met en évidence la partie liée à la bibliothèque C. Nous voulions également vérifier qu'un véritable sshd nous autorisait l'accès ; nous avons donc mis en place un petit environnement de test.
Nous avons utilisé deux images :
- Alpine Linux 3.18 avec openssh-server 9.3_p2-r3, le dernier paquet Alpine avant le correctif.
- Alpine Linux 3.18 avec openssh-server 9.3_p2-r4, le paquet corrigé.
Dans les deux cas, nous avons attribué un mot de passe réel à un compte à l'aide de l'outil `passwd` d'Alpine. Nous avons ensuite ajouté un compte fonctionnant uniquement avec une clé (marqué d'un *) et un compte verrouillé (marqué d'un !), puis nous avons tenté de nous connecter avec des mots de passe de différentes longueurs. La mention « LOGGED IN » signifie que la deuxième tentative effectuée au cours de la même connexion a été acceptée et que `sshd` a exécuté une commande pour nous en tant que cet utilisateur.
Alpine 3.18 Alpine 3.18
9.3_p2-r3 9.3_p2-r4
(before fix) (fixed)
key-only (*), 8 bytes denied denied
key-only (*), 256 bytes denied denied
key-only (*), 257 bytes LOGGED IN denied
key-only (*), 1024 bytes LOGGED IN denied
key-only (*), 1025 bytes denied denied
locked (!), 300 bytes denied denied
root (*), default config denied denied
root (*), PermitRootLogin yes LOGGED IN denied
key-only (*), 1 byte, with bcrypt LOGGED IN denied
real password accepted accepted
Sur la version corrigée, toutes les attaques ont échoué, tandis que le mot de passe réel fonctionnait toujours comme auparavant. Le client SSH standard envoie au maximum 1 023 octets ; ainsi, un mot de passe saisi plus long est tout simplement tronqué dans la fenêtre d'attaque. Pour les lignes de 1 024 et 1 025 octets, nous avons utilisé un petit client qui envoie la longueur exacte. À 1 025 octets, sshd a rejeté le mot de passe avant de le hacher. Pour la ligne bcrypt, le premier hachage $ sur le système était un hachage $2b$ au lieu de $6$.
Pourquoi chaque attaque nécessitait deux tentatives
Dans chaque exécution vulnérable du tableau, la première tentative de mot de passe a échoué et la deuxième tentative, effectuée dans le cadre de la même connexion, a abouti. La première tentative peut correspondre à n’importe quel mot de passe erroné d’une taille maximale de 1 024 octets. Nous avons confirmé la cause du problème à l’aide d’un petit test qui enregistre ce que sshd transmet à la fonction strcmp(). Lors de la première tentative, sshd comparait « * » avec ce qui se trouvait à l’adresse du caractère * stocké ; dans notre laboratoire, il s’agissait d’une chaîne vide. Lors de la deuxième tentative, il comparait « * » avec « * ».
La cause réside dans un problème de tampon dans sshd. La fonction shadow_pw() renvoyait un pointeur vers le tampon que la fonction getspnam() de musl réutilise à chaque recherche. Lors de la première tentative, la fonction pick_salt() est exécutée ; celle-ci recherche d’autres comptes dans ce même tampon, puis efface le hachage emprunté à l’aide de explicit_bzero(). Le * vers lequel sshd pointait toujours a donc disparu. À partir de la deuxième tentative, le sel est mis en cache, aucune autre recherche n’a lieu, et * correspond à *. La solution de contournement de la version 10.6p2 corrige également ce problème, comme nous le montrons dans la section consacrée aux solutions de contournement.
La deuxième tentative n'est nécessaire que lorsque le caractère * provient du fichier /etc/shadow et que sshd fonctionne avec les paramètres par défaut. Dans notre environnement de test, un compte comportant un * dans le fichier /etc/passwd et ne disposant d'aucune entrée dans /etc/shadow a été accepté dès la toute première tentative. Avec l'option PermitEmptyPasswords définie sur « yes » (qui autorise la connexion des comptes dont le mot de passe est vide), même un * dans le fichier /etc/shadow a été accepté dès la première tentative de mot de passe.
Les utilisateurs du service d'annuaire se comportent probablement comme dans le cas du fichier /etc/passwd, mais nous n'avons pas testé cette hypothèse. Cela correspond aux deux sources publiques. L'avis de sécurité d'Alpine indique que l'attaque nécessite une seule connexion et ne requiert aucune nouvelle tentative, tandis que la page du chercheur précise qu'un * dans le fichier shadow nécessite d'abord une tentative erronée.
Par défaut, sshd autorise 6 tentatives par connexion (MaxAuthTries) ; la tentative supplémentaire n'empêche donc pas un attaquant d'agir. Dans le cas du fichier /etc/shadow, cela laisse une tentative échouée et une tentative acceptée provenant du même port de connexion.
Failed password for keyonly from 127.0.0.1 port 53734 ssh2
Accepted password for keyonly from 127.0.0.1 port 53734 ssh2
Alors, s'agit-il d'un bug d'OpenSSH ou d'un bug de musl ?
L'équipe OpenSSH affirme que la cause première se situe en dehors de sshd, et il y a de bonnes raisons de le croire. La vérification classique, utilisée depuis des décennies, consiste à calculer le hachage du mot de passe saisi en utilisant la valeur stockée comme « salt », puis à comparer le résultat avec cette même valeur. musl préserve la sécurité de ce modèle, car sa chaîne d'erreur n'est jamais égale au « salt » qui lui a été fourni. Sur les systèmes glibc, ce même code sshd est également sécurisé.
Il existe également un autre aspect à prendre en compte. Depuis 2016, sshd calcule le hachage d’une entrée d’un seul caractère en utilisant le sel d’un autre compte, puis compare toujours le résultat à la valeur stockée ; de plus, il conservait un pointeur vers un tampon de bibliothèque C qui avait été modifié en arrière-plan. Le mécanisme de protection de musl ne pouvait rien y faire. Rich Felker voit les choses ainsi et qualifie cela d’« utilisation erronée par l’appelant ». Il a également écrit que le mécanisme sous-jacent « sera très probablement modifié lors du prochain cycle de publication » de musl.
Ce bug comportait donc trois éléments, et chacun d'entre eux semble correct pris isolément :
- Le * indique un compte accessible sans mot de passe, une pratique Unix datant des années 1980, à l'époque où les hachages se trouvaient encore dans le fichier /etc/passwd.
- La chaîne d'erreur « * » et la limite de 256 octets dans musl, datant de 2012.
- Le sel emprunté dans sshd, datant de 2016.
Aucun projet ne peut à lui seul être tenu pour responsable de l'ensemble du bogue, ce qui explique en partie pourquoi celui-ci est resté caché pendant 10 ans. Il faut observer le comportement global de tous les éléments, et ce bogue n'apparaît que sur un système musl ne disposant pas de PAM. Le chercheur l'a découvert en compilant sshd avec différentes bibliothèques C et différents paramètres PAM, puis en testant des milliers de tentatives de connexion inhabituelles.
Le correctif dans OpenSSH
C'est Damien Miller qui a rédigé la solution de contournement. Sur la branche V_10_6, il s'agit du commit 90f9c2f7 intitulé « éviter le contournement de l'authentification par mot de passe », et sur la branche master, cette même modification correspond au commit 4353fdc4. La partie principale est très courte.
+#define MIN_PASSWORD_HASH_LEN 4
#define MAX_PASSWORD_LEN 1024
...
- return encrypted_password != NULL &&
+ ret = encrypted_password != NULL &&
+ strlen(pw_password) >= MIN_PASSWORD_HASH_LEN &&
strcmp(encrypted_password, pw_password) == 0;
+ freezero(pw_password, strlen(pw_password));
+ return ret;
Un hachage de mot de passe réel ne comporte jamais moins de 4 caractères. Or, si la valeur stockée est *, !, x ou tout autre caractère aussi court, la comparaison des hachages échoue systématiquement, quelle que soit la valeur renvoyée par la fonction crypt(). Cela bloque toutes les méthodes présentées dans cet article, y compris celle utilisant bcrypt. Le cas particulier des mots de passe vides, géré par l'option PermitEmptyPasswords, reste inchangé.
Le correctif modifie également la fonction shadow_pw() afin qu'elle renvoie une copie de l'entrée shadow, créée à l'aide de xstrdup(), et qu'elle efface cette copie après utilisation. Cela résout le problème de tampon à l'origine des deux tentatives.
Sur la liste de diffusion openssh-unix-dev, Damien Miller a expliqué ce choix. Il a écrit : « Nous sommes bien placés pour proposer rapidement une solution de contournement ». Au moment où nous écrivons ces lignes, musl renvoie toujours « * » en cas d'erreur, mais son responsable prévoit de modifier ce mécanisme lors du prochain cycle de publication.
Que faire maintenant ?
Mettre à jour et redémarrer sshd
Sous Alpine, procédez à la mise à jour et redémarrez comme suit. Il s'agit de la commande indiquée dans l'avis d'Alpine. L'option -a permet également à apk de remplacer ou de rétrograder des paquets afin de les aligner sur les dépôts ; vérifiez donc les modifications apportées.
apk upgrade -U -a
rc-service sshd restart
OpenSSH 9.3p2 lance une nouvelle instance de /usr/sbin/sshd à chaque nouvelle connexion ; ainsi, dans notre laboratoire, les nouvelles connexions ont utilisé le code corrigé dès l'installation du paquet. Redémarrez tout de même sshd afin que le processus d’écoute exécute également le nouveau binaire. Dans un conteneur, recompilez l’image à partir d’une base corrigée, puis redémarrez le conteneur. Les clients TuxCare ELS utilisant Alpine Linux 3.18 peuvent obtenir le correctif depuis le dépôt ELS.
Découvrez ELS →
Si vous ne pouvez pas effectuer la mise à jour aujourd'hui, désactivez la connexion par mot de passe.
Cette opération désactive la connexion par mot de passe pour tous les utilisateurs, et pas seulement pour les comptes à risque. Assurez-vous que toutes les personnes ayant besoin d'un accès disposent d'une clé SSH valide, et laissez votre session actuelle ouverte. Faites attention au dossier de configuration /etc/ssh/sshd_config.d. La configuration par défaut d’Alpine ne le lit qu’à partir de la version 3.19 ; ainsi, sur les versions 3.18 et antérieures, un fichier placé dans ce dossier n’aura aucun effet. Ces commandes, issues de l’avis de sécurité d’Alpine, écrivent le fichier dans le dossier de configuration et modifient également le fichier /etc/ssh/sshd_config lui-même, avec une sauvegarde dans /etc/ssh/sshd_config.bak.
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
La dernière commande devrait afficher « passwordauthentication no ». Si ce n'est pas le cas, la connexion par mot de passe est toujours activée.
Recherchez les connexions qui ne devraient pas avoir lieu
Recherchez les lignes « Accepted password » dans /var/log/messages, le fichier syslog par défaut sous Alpine, pour les comptes comportant un astérisque (*). Dans le cas du fichier /etc/shadow, notre laboratoire a toujours observé une ligne « Failed password » juste avant celle-ci, provenant du même port de connexion. Ne vous fiez pas uniquement à cette paire. Dans d’autres configurations, la première tentative peut déjà aboutir ; un utilisateur légitime qui se trompe en tapant son mot de passe laisse la même paire ; de nombreux conteneurs n’exécutent pas de démon syslog et n’enregistrent rien ; et un attaquant disposant des droits root peut effacer les journaux. Vérifiez également la présence de nouveaux utilisateurs, de nouvelles clés dans le fichier `authorized_keys` et l’utilisation de la commande `doas`. Si vous ne pouvez pas exclure une connexion, considérez la machine comme compromise.
Vérifier les systèmes plus anciens et cachés
Les anciennes images Docker et certains appareils embarqués exécutent parfois OpenSSH sur musl, et personne ne les met à jour. Vérifiez-les également.
Pourquoi TuxCare joue un rôle important ici ?
La solution de contournement proposée en amont a été intégrée à OpenSSH 10.6p2. De nombreux systèmes de production n'utilisent pas la dernière version d'OpenSSH. Ils utilisent la version fournie avec leur distribution il y a plusieurs années. Alpine Linux 3.18 est livré avec OpenSSH 9.3p2 et a atteint sa fin de vie en mai 2025.
Alpine a très bien géré cette situation. Le jour de la divulgation, l'équipe a publié des paquets corrigés pour toutes les branches remontant jusqu'à la version 3.16, y compris celles dont la fin de vie était dépassée. Pour Alpine 3.18, il s'agit de la version 9.3_p2-r4, le paquet corrigé utilisé dans notre laboratoire. Cependant, Alpine ne s'engage pas à fournir des correctifs pour les branches en fin de vie. Cette fois-ci, ils en ont publié un, mais personne ne peut dire ce qu'il en sera pour le prochain bug.
Le support à cycle de vie illimité (ELS) de TuxCare transforme cela en une promesse. Pour l’ELS d’Alpine Linux 3.18, notre équipe a rétroporté le même commit en amont vers OpenSSH 9.3p2 et l’a fusionné le jour même de la sortie de la version en amont. Il s’applique en plus des 22 correctifs de notre paquet ELS, et il s’agit du même code que le pw.patch d’Alpine. Le paquet corrigé sera openssh 9.3_p2.tuxcare.els7, pour x86_64 et aarch64.
Découvrir ELS →
Ce bug montre également pourquoi il ne suffit pas de suivre les avis de sécurité d'un seul projet. Le bug concerne à la fois le code de musl et celui d'OpenSSH, la solution de contournement se trouve dans OpenSSH, et les systèmes affectés sont des serveurs, des conteneurs et des appareils embarqués basés sur musl. Quelqu'un doit suivre tous ces éléments et fournir un correctif testé pour les versions que les clients utilisent encore. C'est le travail de notre équipe de sécurité.