cloud's Blog

Security blog

View on GitHub
16 May 2026

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 :


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 :

  1. Il ouvre les clés privées de l’hôte (/etc/ssh/ssh_host_*_key) au démarrage
  2. Il appelle permanently_set_uid() pour abandonner les privilèges root
  3. Si EnableSSHKeysign est désactivé dans sshd_config, il bail immédiatement après avoir ouvert les clés — mais openat(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 :

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 :

  1. Ouvre /etc/shadow (euid=root)
  2. Appelle setreuid(ruid, ruid) pour abandonner les privilèges
  3. 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 à :

  1. Créer de nombreuses cibles (fork + exec de ssh-keysign en boucle)
  2. Attendre le SIGCHLD de chaque cible
  3. À réception de SIGCHLD, tenter immédiatement pidfd_getfd() sur tous les fd probables (3, 4, 5…)
  4. Si la cible est entre exit_mm() et exit_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 :

⚠️ 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


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

  1. Appliquez le patch noyau immédiatement dès qu’il est disponible pour votre distribution
  2. En attendant, activez kernel.yama.ptrace_scope=3 sur les systèmes sensibles (effet secondaire : casse strace/gdb)
  3. Surveillez les mises à jour de noyau de votre distribution
  4. Restreignez l’accès local aux machines critiques — cette vulnérabilité nécessite un accès shell
  5. 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