CVE-2026-23111: Один восклицательный знак — и непривилегированный пользователь получает root
CVE-2026-23111: Один восклицательный знак — и непривилегированный пользователь получает root
Представьте: на вашем сервере работает обычный пользователь. Никаких sudo, никаких специальных прав. Он не может читать /root, не может трогать системные файлы. А через несколько секунд — уже root. Не потому что взломал что-то сложное. Потому что в ядре Linux один разработчик случайно поставил восклицательный знак там, где его не должно было быть.
CVE-2026-23111 — уязвимость типа use-after-free в подсистеме nf_tables ядра Linux, CVSS 7.8 (High) по оценке Ubuntu. Уязвимость позволяет непривилегированному пользователю получить права root на Debian и Ubuntu. Публичный рабочий эксплойт опубликован — от Exodus Intelligence (автор Oliver Sieber) со стабильностью >99% на незагруженной системе, и PoC от FuzzingLabs ещё с апреля. Случаев эксплуатации in the wild на момент публикации не зафиксировано, но патч вышел в феврале — те, кто не обновился, живут с публичным эксплойтом под боком уже несколько месяцев.
ЧТО ТАКОЕ NF_TABLES
Nftables — подсистема ядра Linux, пришедшая на смену iptables. Весь современный Linux firewall работает через неё: фильтрация пакетов, NAT, маркировка трафика. Используете nft напрямую, firewalld или nftables через systemd — под капотом одно и то же. На большинстве современных серверов nf_tables активен по умолчанию, даже если вы явно не настраивали firewall.
Внутри nf_tables объекты выстроены иерархически: таблицы содержат цепочки (chains), цепочки содержат правила, правила состоят из выражений. Ключевая структура для понимания бага — verdict map, карта вердиктов. Это таблица соответствий: пакет → действие (принять, отбросить, перейти в другую цепочку). Представьте её как таблицу маршрутизации, только не для адресов, а для решений о судьбе пакета. Каждая verdict map может иметь обычные элементы с конкретными ключами и специальный catchall элемент — подстановочный знак, срабатывающий когда пакет не совпал ни с чем другим, что-то вроде правила «всё остальное — сделать вот это». Именно в нём и жил баг.
Чтобы изменения в ruleset применялись атомарно — без гонок с живым трафиком — ядро использует механизм поколений (generation mask). Изменения накапливаются в «следующем поколении» и одним переключением становятся активными. Если пакетная операция (batch) падает с ошибкой, ядро вызывает фазу отмены (abort phase), которая должна полностью откатить всё назад. Именно в этом откате и скрылась инвертированная логика.
КАК РАБОТАЕТ БАГ
Баг — единственный символ: восклицательный знак в условии функции nft_map_catchall_activate(). Эта функция вызывается в abort phase, чтобы реактивировать catchall элементы verdict map, которые были деактивированы в ходе отменяемой транзакции.
Правильная логика: пропустить уже активные элементы, обработать неактивные. Именно так написана аналогичная функция для обычных элементов — nft_mapelem_activate(). В версии для catchall условие инвертировано: функция пропускала неактивные и обрабатывала активные — ровно наоборот. Один символ ! перевернул всю логику с ног на голову.
Последствия конкретны. Когда verdict map с catchall элементом типа NFT_GOTO (ссылающимся на цепочку) удаляется, счётчик ссылок цепочки (chain->use) уменьшается — это нормально, ссылка убирается. В abort phase счётчик должен быть восстановлен, потому что удаление отменяется. Из-за инвертированного условия восстановления не происходит — каждый abort цикл навсегда уменьшает chain->use. Когда счётчик достигает нуля, ядро считает что на цепочку никто не ссылается, DELCHAIN проходит успешно и освобождает память — пока другие объекты на неё ссылаются. Это и есть use-after-free: обращение к уже освобождённой памяти.
КАК ЭТО ЭКСПЛУАТИРУЕТСЯ
Никаких привилегий не нужно — только доступ к user namespaces и nf_tables, включённый по умолчанию на Debian и Ubuntu. На Ubuntu 24.04 есть ограничения на создание namespaces, но известен обход через aa-exec -p trinity -- unshare -Urmin /bin/sh — эта команда запускает shell в новом namespace в обход AppArmor-профиля, который ограничивает создание namespaces.
Атака строится из четырёх батчей. Батч 1: удалить pipapo-backed verdict map с catchall элементом, затем намеренно вызвать ошибку в том же батче — это запускает abort phase и уменьшает счётчик ссылок цепочки без восстановления. Батч 2: отправить любую успешную операцию для переключения generation cursor — без этого шага следующий батч не сработает корректно. Батч 3: снова удалить verdict map — теперь catchall элемент активен относительно нового поколения, счётчик ссылок цепочки достигает нуля. Батч 4: удалить цепочку — это проходит успешно, потому что счётчик равен нулю, хотя base chain всё ещё содержит правило, ссылающееся на неё. Use-after-free получен.
Дальше начинается работа с кучей ядра. NFT_MSG_GETRULE запрос к правилу, ссылающемуся на удалённую цепочку, вызывает nft_verdict_dump() — она читает имя цепочки как строку из уже освобождённой памяти. Разместив туда структуру seq_operations через open("/proc/self/stat", 0), атакующий получает утечку указателя на ядерный код и вычисляет базовый адрес ядра, обходя KASLR. Затем утекают адреса кучи, через манипуляции с blob_gen_0 удалённой цепочки перехватывается поток выполнения и запускается ROP-цепочка. Итог: вызов commit_creds(&init_cred) выдаёт процессу root-учётные данные, switch_task_namespaces() на PID 1 разрушает namespace изоляцию, и атакующий оказывается root на хосте.
ЧТО ПРОИСХОДИТ ДАЛЬШЕ — ЗАВИСИТ ОТ КОНФИГУРАЦИИ
На Debian Bookworm и Trixie, Ubuntu 22.04 LTS и Ubuntu 24.04 LTS эксплойт работает — с незначительными различиями в ROP-гаджетах между версиями ядра, поскольку смещения функций и структуры данных различаются между сборками. Стабильность эксплойта — >99% на незагруженной системе и около 80% под нагрузкой Apache benchmark. Два независимых исследовательских коллектива нашли разные пути к root от одного и того же бага — это значит, что блокировка одного пути не закрывает другой автоматически.
Дополнительные средства защиты помогают, но не являются полной защитой. Если SELinux в режиме enforcing — вариант от FuzzingLabs требует дополнительного шага: явного сброса selinux_state.enforcing в ноль через ROP-цепочку. ASLR не защищает — его обходит утечка базового адреса ядра через use-after-free, которая происходит ещё до попытки перехвата управления. Реальную защиту даёт только патч ядра или полное отключение пользовательских namespaces.
РЕАЛЬНАЯ ЦЕПОЧКА АТАКИ
Баг не эксплуатируется удалённо — для старта нужен локальный shell. Именно поэтому он особенно опасен как второй этап атаки: RCE через уязвимое веб-приложение даёт shell от имени www-data, дальше CVE-2026-23111 превращает этот shell в root за несколько секунд. Атакующий полностью контролирует хост: читает /etc/shadow, извлекает SSH-ключи, перехватывает трафик, модифицирует бинарники системных служб. Контейнерная изоляция разрушается через вызов switch_task_namespaces() на PID 1 — атакующий выходит за пределы namespace изоляции контейнера и оказывается на хосте.
Особенно уязвимы многопользовательские серверы, VPS с неразделёнными ядрами, CI/CD раннеры и shared hosting среды — любое окружение, где непривилегированные пользователи или рабочие нагрузки могут создавать namespaces. На shared hosting один скомпрометированный сайт превращается в точку входа для компрометации всего сервера и всех соседних сайтов. На облачном сервере с multi-tenant ядром — в точку входа для побега из контейнера.
ТАЙМЛАЙН
Exodus Intelligence обнаружили уязвимость в начале 2025 года в ходе исследовательской работы по nf_tables. 5 февраля 2026 патч попал в upstream ядра Linux (коммит f41c5d151078c5348271ffaf8e7410d96f2d82f8) — удалена одна строка с инвертированным условием, в тот же день присвоен CVE-2026-23111. 16 апреля 2026 FuzzingLabs (авторы Alexis и Lyes), готовясь к Pwn2Own Berlin 2026, независимо воспроизвели уязвимость и опубликовали полный PoC с техническим разбором. 8 июня 2026 Exodus Intelligence (автор Oliver Sieber) выпустили развёрнутый writeup с рабочим эксплойтом, подтверждённым на Debian Bookworm, Trixie, Ubuntu 22.04 LTS и 24.04 LTS.
Разрыв между патчем (5 февраля) и первым публичным рабочим эксплойтом (16 апреля) составил всего 70 дней. За это время системы, которые не успели обновиться, уже имели против себя рабочий эксплойт в открытом доступе. К июню, когда вышел второй более детальный writeup, этот разрыв превысил четыре месяца — а значит, всё это время администраторы неpatched систем работали с открытой дырой, о которой было публично известно.
ПОЧЕМУ ЭТО ВАЖНО
Linux kernel LPE через nf_tables — не новая история. CVE-2022-1015, CVE-2022-1016, CVE-2022-32250, CVE-2023-32233 — у этой подсистемы богатая CVE-история, и каждый раз по похожей схеме: сложный транзакционный механизм, редкий путь выполнения, corner case который никто не тестировал. CVE-2026-23111 вписывается органично: команда, выбравшая nf_tables для Pwn2Own Berlin 2026 именно потому, что плохо знала подсистему, немедленно нашла эксплуатируемый баг. Это не случайность — это симптом накопленного технического долга в критически важном коде.
CVE-2026-23111 появился на фоне заметного всплеска Linux LPE уязвимостей. За последние месяцы исследователи раскрыли Copy Fail (CVE-2026-31431) — ошибка в механизме copy-on-write, Dirty Frag и его вариант Fragnesia (CVE-2026-46300) — heap fragmentation в подсистеме XFRM ESP-in-TCP, DirtyDecrypt, и девятилетняя уязвимость в ptrace (CVE-2026-46333). Разные подсистемы, разные техники — но один паттерн: непривилегированный foothold внутри, root на хосте снаружи. Общий знаменатель у большинства этих уязвимостей — user namespaces, открывающие непривилегированным пользователям доступ к ядерным интерфейсам. Для организаций без прямой необходимости в unprivileged user namespaces — это сигнал задуматься об отключении по умолчанию.
ДОПОЛНИТЕЛЬНЫЙ СЛОЙ ЗАЩИТЫ
Параллельно с патчем ядра стоит рассмотреть ещё один подход — сокращение поверхности атаки через неиспользуемые модули ядра. Большинство серверов загружают тысячи модулей, хотя реально используют лишь несколько сотен. Каждый незадействованный модуль — потенциальный вектор для следующего LPE. Инструмент modulejail решает эту проблему просто: сканирует список загруженных модулей и создаёт blacklist для всех остальных через modprobe.d. Если модуль не нужен серверу прямо сейчас — он не загрузится вообще.
Важное ограничение: запускать modulejail нужно только после того как система вышла на рабочий режим — все сервисы запущены, все файловые системы смонтированы, все нужные драйверы загружены. Запуск на незагруженной системе рискует занести в blacklist модули которые потребуются позже. Откат ручной: удалить /etc/modprobe.d/modulejail-blacklist.conf и перезагрузить сервер.
Установка на Debian/Ubuntu:
sudo apt install modulejail
На RHEL/Fedora/Alma/Rocky:
sudo dnf install modulejail
Запуск после выхода системы в рабочий режим — консервативный профиль для серверов:
sudo modulejail
Modulejail не патчит уязвимости и не анализирует CVE — это инструмент сокращения поверхности атаки, не замена обновлениям ядра. Используйте его как дополнительный слой после того как ядро обновлено.
ОБНОВЛЕНИЕ
Патч в upstream с 5 февраля 2026, дистрибутивы выпустили обновлённые пакеты ядра. На Debian и Ubuntu обновление запускается стандартной командой — apt update синхронизирует списки пакетов с репозиториями, apt upgrade устанавливает все доступные обновления включая новое ядро:
sudo apt update && sudo apt upgrade
После установки нового ядра обязательна перезагрузка — обновлённый пакет лежит на диске, но система продолжает работать со старым ядром в памяти до рестарта. Команда reboot корректно завершает все процессы и загружает систему с новым ядром:
sudo reboot
После перезагрузки проверьте что система действительно загружена с обновлённым ядром — команда uname -r выводит версию работающего ядра. На Ubuntu и Debian патчи безопасности часто бэкпортируются без смены основного номера версии, поэтому версия может выглядеть как прежде, но дата сборки будет свежее. Чтобы убедиться что установлен актуальный пакет, проверьте дату сборки командой apt-cache policy linux-image-$(uname -r) — в строке Installed должна стоять дата после февраля 2026:
uname -r
apt-cache policy linux-image-$(uname -r)
Если обновление невозможно прямо сейчас, временная мера — отключить пользовательские namespaces. Это блокирует вектор эксплуатации, потому что атакующий больше не сможет создать изолированный namespace для работы с nf_tables без привилегий. Побочный эффект: ломаются rootless-контейнеры (Docker без root, Podman, LXC), некоторые браузерные sandbox и инструменты сборки. На Debian и Ubuntu нужны оба параметра — kernel.unprivileged_userns_clone отключает создание user namespaces на уровне ядра, user.max_user_namespaces обнуляет их лимит:
sudo sysctl -w kernel.unprivileged_userns_clone=0
sudo sysctl -w user.max_user_namespaces=0
Эти значения сбрасываются после перезагрузки. Чтобы сохранить их постоянно, добавьте строки в файл конфигурации sysctl — при загрузке ядро прочитает их оттуда автоматически:
kernel.unprivileged_userns_clone=0
user.max_user_namespaces=0
На RHEL-совместимых системах параметр kernel.unprivileged_userns_clone отсутствует — там достаточно только user.max_user_namespaces=0. На Debian и Ubuntu нужны оба: если установить только один, второй параметр оставит вектор открытым.
ВЫВОДЫ
Один символ в условии abort phase — и непривилегированный пользователь получает root. Не потому что атака сложная, а потому что corner case в транзакционной логике nf_tables годами не был покрыт тестами, а user namespaces открыли к нему доступ без привилегий. Рабочий публичный эксплойт с 99% стабильностью уже существует, эксплуатации in the wild пока не зафиксировано — но это окно закрывается.
Для сисадминов: обновите ядро и перезагрузите сервер. Это единственное надёжное решение. Если немедленно нельзя — отключите unprivileged user namespaces как временную меру, понимая что это затронет контейнерные рабочие нагрузки. Приоритет в первую очередь у shared hosting серверов, container hosts и CI/CD раннеров — там непривилегированный shell превращается в root быстрее всего. Для провайдеров облачных сред с multi-tenant ядрами — это задача номер один прямо сейчас.
