CVE-2026-46333 (ssh-keysign-pwn) : Vol de clés SSH hôte via le noyau Linux
by cloud
CVE-2026-46333 (ssh-keysign-pwn) est une vulnérabilité de type information disclosure dans le noyau Linux, découvverte par l’équipe Qualys Threat Research Unit. Elle permet à tout utilisateur local non privilégié de voler les clés privées SSH de l’hôte ainsi que le contenu de /etc/shadow, en exploitant un contournement du contrôle dumpable dans __ptrace_may_access() via pidfd_getfd(2) combiné à une race condition sur do_exit(). Corrigée en urgence par Linus Torvalds le 14 mai 2026 (commit 31e62c2ebbfd).
Caractéristiques principales :
- ✅ Vol des clés SSH hôte (id_rsa, id_dsa, id_ecdsa, etc.) sans privilège root
- ✅ Vol de
/etc/shadow→ offline cracking des mots de passe système - ✅ Aucun privilège requis — tout utilisateur local non privilégié
- ✅ 6 ans de fenêtre d’exploitation (signalée en octobre 2020 par Jann Horn, jamais corrigée)
- ✅ Correctif en urgence par Linus Torvalds le 14 mai 2026
TL;DR
| Champ | Valeur |
|---|---|
| CVE | CVE-2026-46333 |
| Alias | ssh-keysign-pwn |
| Impact | Importante — vol de clés SSH hôte et /etc/shadow |
| Composant | kernel/ptrace.c — __ptrace_may_access() |
| Type | Information disclosure — contournement dumpable + race condition |
| Auteur | Qualys Threat Research Unit |
| Exploit | github.com/0xdeadbeefnetwork/ssh-keysign-pwn |
| Correctif | 31e62c2ebbfd (Linus Torvalds, 2026-05-14) |
| Mitigation | kernel.yama.ptrace_scope=3 |
1. Le Bug : Pourquoi __ptrace_may_access() laisse passer ?
1.1 La racine : l’ordre de do_exit()
Le bug se situe dans __ptrace_may_access(), la fonction du noyau qui vérifie si un processus peut en tracer un autre. Regardons le code vulnérable :
// kernel/ptrace.c — version vulnérable
static int __ptrace_may_access(struct task_struct *task, unsigned int mode)
{
/* ... */
if (task->mm == NULL) // ← check 1 : mm NULL ?
return 0; // → OK, laisse passer !
/* ... */
if (!ptrace_has_cap(...)) {
if (task->mm->dumpable != SUID_DUMP_USER)
return -EPERM; // ← check 2 : dumpable ?
}
/* ... */
}
Le problème est l’ordre des vérifications. Quand le noyau vérifie task->mm == NULL, un processus en train de mourir a déjà libéré son mm via exit_mm(), mais ses descripteurs de fichiers sont toujours ouverts (car exit_files() n’a pas encore été appelé).
Dans do_exit(), l’ordre des opérations est :
do_exit():
exit_mm() → mm = NULL (mais les fichiers sont encore ouverts)
exit_files() → fermeture des fd
...
Cette fenêtre entre exit_mm() et exit_files() est la clé de l’attaque.
1.2 Pourquoi mm == NULL court-circuite le check dumpable
Quand task->mm == NULL, __ptrace_may_access() retourne 0 (succès) immédiatement, sans jamais atteindre le check dumpable. Cette logique avait un sens à l’origine : un processus sans mm est post-mortem ou un kernel thread, impossible à tracer utilement. Mais avec l’arrivée de pidfd_getfd(2) et la fenêtre de do_exit(), cette supposition est devenue incorrecte.
Note historique : Jann Horn (project-zero) avait identifié ce problème dès octobre 2020. Le correctif n’a jamais été appliqué, laissant la faille dormir pendant 6 ans.
2. Vecteurs d’attaque
Deux binaires système ouvrent une porte de choix pour cette vulnérabilité :
2.1 ssh-keysign — vol des clés SSH hôte
/usr/lib/ssh/ssh-keysign est un binaire setuid helper utilisé par OpenSSH pour l’authentification par clé hôte (HostBasedAuthentication). Son comportement est crucial :
- Il ouvre les clés privées de l’hôte (
/etc/ssh/ssh_host_*_key) au démarrage - Il appelle
permanently_set_uid()pour abandonner les privilèges root - Si
EnableSSHKeysignest désactivé danssshd_config, il bail immédiatement après avoir ouvert les clés — maisopenat(2)a déjà été invoqué
// ssh-keysign.c (simplifié)
int main() {
load_host_keys(); // ← les clés sont ouvertes ici
permanently_set_uid(getuid()); // ← drop root
if (!options.enable_ssh_keysign)
exit(1); // ← bail ! mais les fd sont TOUJOURS là
}
Même en échouant, les descripteurs de fichiers pointant vers les clés hôte restent ouverts dans sa table de fd. Pour un attaquant capable d’appeler pidfd_getfd() via la race condition, ces fd sont accessibles.
Clés volables :
/etc/ssh/ssh_host_rsa_key/etc/ssh/ssh_host_dsa_key/etc/ssh/ssh_host_ecdsa_key/etc/ssh/ssh_host_ed25519_key
2.2 chage — vol de /etc/shadow
Le binaire chage -l liste les informations de vieillissement des mots de passe. Pour lire /etc/shadow, il doit être setuid root. Son comportement est similaire :
- Ouvre
/etc/shadow(euid=root) - Appelle
setreuid(ruid, ruid)pour abandonner les privilèges - Traite les données
La fenêtre entre l’ouverture de /etc/shadow et le drop de privilèges existe, mais la véritable exploitation passe par la primitive pidfd_getfd + race.
3. Technique d’exploitation
3.1 La primitive : pidfd_getfd(2) + race
pidfd_getfd(2) est un appel système introduit dans Linux 5.6 (2020) qui permet de dupliquer un descripteur de fichier d’un autre processus, à condition d’avoir la permission de le tracer (ptrace access). Le flux est le suivant :
1. Attaquant : crée un enfant cible
2. Cible : exécute ssh-keysign (ou chage) via execve()
3. Cible : ouvre les clés SSH → fd 3, 4, 5...
4. Cible : drop privilèges → bail ou continue
5. Attaquant : repère le pid de la cible (via waitid(P_PIDFD, ...))
┌─ RACE WINDOW ─────────────────────────────────────────┐
6. Cible : do_exit() → exit_mm() → mm = NULL │
Attaquant : appelle pidfd_getfd(pidfd, 3) │
Noyau : __ptrace_may_access() → task->mm == NULL → 0 │ → VOL !
Cible : exit_files() → fermeture des fd │
└────────────────────────────────────────────────────────┘
7. Attaquant : lit le contenu du fd dupliqué → clés volées
3.2 La fenêtre de concurrence
La fenêtre entre exit_mm() et exit_files() est extrêmement courte, mais exploitable :
// Schéma de la race (issue de l'article de Qualys)
// temps
// │ Cible (ssh-keysign) Attaquant
// │
// │ open("/etc/ssh/...") waitid(P_PIDFD, ...)
// │ setuid(getuid())
// │ exit(1)
// │ do_exit():
// │ exit_mm() → mm=NULL ←──── reçoit SIGCHLD
// │ ... /* boucle sur les fd connus */
// │ exit_files() → close(3) pidfd_getfd(pidfd, 3) → SUCCÈS
// │ read(fd_dup) → CLÉ VOLÉE
L’attaquant ne peut pas prédire exactement quand la cible atteint exit_mm(). La stratégie consiste à :
- Créer de nombreuses cibles (fork + exec de ssh-keysign en boucle)
- Attendre le SIGCHLD de chaque cible
- À réception de SIGCHLD, tenter immédiatement
pidfd_getfd()sur tous les fd probables (3, 4, 5…) - Si la cible est entre
exit_mm()etexit_files()→ le fd est volé
Le PoC public réalise environ 1000 tentatives par seconde par cœur CPU.
3.3 Code simplifié de l’attaque
int steal_fd(pid_t target, int target_fd) {
int pidfd = syscall(SYS_pidfd_open, target, 0);
if (pidfd < 0)
return -1;
// Tentative de vol du fd via pidfd_getfd
int stolen = syscall(SYS_pidfd_getfd, pidfd, target_fd, 0);
close(pidfd);
if (stolen < 0)
return -1; // fenêtre fermée ou pas de permission
return stolen; // fd volé, on peut read() les clés
}
4. Le Correctif — 31e62c2ebbfd
Linus Torvalds a corrigé le bug en déplaçant la vérification du flag dumpable avant le retour précoce mm == NULL :
--- a/kernel/ptrace.c
+++ b/kernel/ptrace.c
@@ -289,9 +289,15 @@ static int __ptrace_may_access(struct task_struct *task, unsigned int mode)
security_ptrace_access_check(task, mode))
return -EPERM;
+ int dumpable = task->mm ? task->mm->dumpable : SUID_DUMP_USER;
+
+ if (!ptrace_has_cap(...) && dumpable != SUID_DUMP_USER)
+ return -EPERM;
+
if (task->mm == NULL)
return 0;
- ...
+
+ /* suite du code sans le check dumpable (déjà fait) */
La logique avant/apres :
| État | Avant patch | Après patch |
|---|---|---|
mm != NULL, dumpable incorrect |
✅ Bloqué (check dumpable atteint) | ✅ Bloqué |
mm == NULL, dumpable incorrect |
❌ BYPASS (retour précoce) | ✅ Bloqué (check dumpable avant) |
mm == NULL, dumpable correct |
✅ Autorisé (légitime, kernel threads) | ✅ Autorisé |
En déplaçant le check dumpable avant la vérification mm == NULL, le correctif ferme la fenêtre sans impacter les cas légitimes.
5. Analyse d’impact
5.1 Que peut faire un attaquant avec les clés SSH hôte volées ?
| Scénario | Impact |
|---|---|
| HostBasedAuthentication activé | L’attaquant peut se connecter à n’importe quelle machine qui fait confiance à l’hôte compromis |
| Reconnaissance réseau | Les clés hôte permettent d’identifier précisément la version d’OpenSSH et l’OS |
| Attaque Man-in-the-Middle | Avec la clé privée hôte, un attaquant peut se faire passer pour le serveur auprès des clients |
| Propagation horizontale | Si les clés hôte sont partagées entre machines (pratique courante en entreprise), toutes sont compromises |
5.2 Vol de /etc/shadow
Le vol de /etc/shadow permet le offline cracking des mots de passe utilisateur :
# hash extrait de /etc/shadow
$y$j9T$...ABC...$...XYZ...
# John The Ripper ou hashcat
john --wordlist=rockyou.txt shadow.hash
Même avec des mots de passe fortement hashés (yescrypt, bcrypt, argon2), l’offline reste un vecteur de compromission supplémentaire.
6. Mitigation
6.1 kernel.yama.ptrace_scope=3 — la solution radicale
La mitigation immédiate consiste à définir ptrace_scope=3 :
echo 3 > /proc/sys/kernel/yama/ptrace_scope
# Ou via sysctl
sysctl -w kernel.yama.ptrace_scope=3
Pour rendre permanent :
# /etc/sysctl.d/99-ptrace.conf
kernel.yama.ptrace_scope = 3
Ce que fait ptrace_scope=3 :
- Bloque toute utilisation de
ptrace()etpidfd_getfd() - Y compris pour les processus non traçants
- Irréversible jusqu’au prochain reboot
⚠️ Cela casse strace, gdb, perf, et d’autres outils de debugging. À utiliser avec précaution.
Valeur ptrace_scope |
Effet |
|---|---|
| 0 | Tout processus peut tracer tout autre (sauf si no_new_privs) |
| 1 (défaut) | Seul un parent peut tracer son enfant |
| 2 | Seul root peut tracer |
| 3 | Tout ptrace bloqué (y compris root) |
6.2 Autres mitigations
- Appliquer le patch noyau dès que votre distribution le backporte
- Désactiver ssh-keysign si non utilisé :
EnableSSHKeysign nodans/etc/ssh/sshd_config - Limiter l’accès local : moindre privilège, conteneurisation
- Surveiller les appels à
pidfd_getfd(auditd)
7. Contexte : 4 vulnérabilités noyau en 3 semaines
Cette découverte marque une année 2026 record pour les failles noyau Linux :
| Date | CVE | Alias | Type |
|---|---|---|---|
| 2026-04-29 | CVE-2026-31431 | Copy Fail | Écriture cache de pages (algif_aead) |
| 2026-05-07 | CVE-2026-43284 / -43500 | Dirty Frag | Écriture cache de pages (xfrm-ESP + RxRPC) |
| 2026-05-13 | (non attribué) | Fragnesia | Écriture cache de pages (frag_list) |
| 2026-05-16 | CVE-2026-46333 | ssh-keysign-pwn | Race condition / logique (cette faille) |
Contrairement aux trois précédentes (bugs de la famille page-cache write, déterministes, exploitables via splice()), ssh-keysign-pwn est un bug de logique avec fenêtre de concurrence — un profil radicalement différent.
8. Timeline
| Date | Événement |
|---|---|
| 2020-10 | Jann Horn (Google Project Zero) identifie le problème et le signale |
| 2026-05-13 | Qualys Threat Research Unit réactive l’analyse et développe l’exploit |
| 2026-05-14 | Linus Torvalds pousse le correctif (commit 31e62c2ebbfd) |
| 2026-05-16 | Publication du CVE-2026-46333 et divulgation publique |
| 2026-05-16 | Publication du PoC sur GitHub |
9. Recommandations
- Appliquez le patch noyau immédiatement dès qu’il est disponible pour votre distribution
- En attendant, activez
kernel.yama.ptrace_scope=3sur les systèmes sensibles (effet secondaire : casse strace/gdb) - Surveillez les mises à jour de noyau de votre distribution
- Restreignez l’accès local aux machines critiques — cette vulnérabilité nécessite un accès shell
- Changez les clés SSH hôte si vous suspectez une compromission (elles sont probablement déjà lues)
Références
| Source | URL |
|---|---|
| GitHub PoC | https://github.com/0xdeadbeefnetwork/ssh-keysign-pwn |
| Commit de correctif (Torvalds) | https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=31e62c2ebbfd |
| Rapport Qualys (à venir) | https://www.qualys.com/security-advisories/ |
| Dirty Frag (article précédent) | https://madpowah.github.io/2026/05/09/dirtyfrag-cve-2026-43284.html |
| Copy Fail (article précédent) | https://madpowah.github.io/2026/05/01/copy-fail-exploit.html |
| pidfd_getfd(2) man page | https://man7.org/linux/man-pages/man2/pidfd_getfd.2.html |
| Jann Horn — signalement 2020 | https://bugzilla.kernel.org/show_bug.cgi?id=209833 |
Have fun.
tags: security - linux - cve - kernel - exploit - ssh - keysign - ptrace