Универсальный LPE на актуальных ядрах Linux: один бинарь поднимает root на Ubuntu 24.04, RHEL 10.1, AlmaLinux 10, Fedora 44, openSUSE Tumbleweed. CVE пока нет, PoC лежит в открытом репозитории. Разбираем механику, объясняем, почему это не «ещё один Dirty Pipe», и даём чеклист действий для системных администраторов.
Контекст
7 мая 2026 года Hyunwoo Kim (@v4bel) опубликовал полный write-up и рабочий PoC уязвимости, которую он назвал Dirty Frag. Это локальное повышение привилегий (LPE) до root, которое работает на всех мажорных дистрибутивах с актуальными ядрами – от 6.12 до 7.0.
-
29 апреля – автор отправляет отчёт по RxRPC-варианту в
security@kernel.orgи патч в netdev. -
30 апреля – туда же уходит ESP-вариант. Информация по обеим уязвимостям появляется в публичных рассылках kernel-сообщества.
-
7 мая – Kim отправляет детали и эксплойт в
linux-distros@vs.openwall.orgс эмбарго на 5 дней. Условие стандартное: если кто-то третий публикует PoC до окончания эмбарго – автор сразу выкатывает свою версию. -
7 мая (через несколько часов) – неизвестная сторона публикует эксплойт ESP-варианта. По обсуждению в LWN и Hacker News видно, что параллельно несколько человек самостоятельно разработали PoC по уже опубликованному в netdev патчу – формально эмбарго не нарушали, но результат тот же.
-
7 мая (вечером) – по согласованию с мейнтейнерами дистрибутивов автор публикует полный документ с PoC на GitHub.
Что мы имеем на руках:
-
CVE не назначено ни одной из двух уязвимостей цепочки.
-
Патч ESP-варианта уже в netdev (смержен 7 мая), патч RxRPC – пока только в LKML.
-
В дистрибутивах патчи в работе: AlmaLinux уже выкатил тестовые ядра, CloudLinux готовит KernelCare livepatch. Остальные – в режиме «ждём бэкпорта».
-
Жизненный цикл бага: ESP-вариант существует с коммита
cac2661c53f3(январь 2017), RxRPC – с2dc334f1a63a(июнь 2023). Около 9 лет на ESP-стороне.
Механика уязвимости
Dirty Frag – это не одна, а две независимые уязвимости одного класса, объединённые в общий exploit chain. Класс – тот же, что у Dirty Pipe и Copy Fail: запись в page cache файлов, к которым у атакующего есть только read-доступ. Только Dirty Pipe «грязнил» struct pipe_buffer, а Dirty Frag «грязнит» frag в struct sk_buff.
Общая идея
Атакующий через splice(file -> pipe -> socket) с MSG_SPLICE_PAGES подсаживает в frag отправляющего skb прямую ссылку на страницу page cache, например, страницу /usr/bin/su или /etc/passwd. Этот же skb проходит через loopback и попадает в receiving-путь ядра, где криптокод выполняет in-place crypto прямо поверх этой страницы. AEAD-проверка падает с ошибкой, но 4–8 байт в page cache уже изменены – и сохраняются до перезагрузки или drop_caches.
Вариант 1: xfrm-ESP Page-Cache Write
Точка входа – esp_input() в net/ipv4/esp4.c и net/ipv6/esp6.c. Перед in-place AEAD-расшифровкой нелинейного skb код должен позвать skb_cow_data() и скопировать данные frag в приватный буфер. Но в исходном коде есть ветка, которая пропускает COW:
cif (!skb_cloned(skb)) { if (!skb_is_nonlinear(skb)) { nfrags = 1; goto skip_cow; } else if (!skb_has_frag_list(skb)) { nfrags = skb_shinfo(skb)->nr_frags; nfrags++; goto skip_cow; // вот сюда улетает skb с подставленной страницей }}
if (!skb_cloned(skb)) { if (!skb_is_nonlinear(skb)) { nfrags = 1; goto skip_cow; } else if (!skb_has_frag_list(skb)) { nfrags = skb_shinfo(skb)->nr_frags; nfrags++; goto skip_cow; // вот сюда улетает skb с подставленной страницей } }
Дальше при комбинации ESP + ESN + authencesn(hmac(sha256), cbc(aes)) функция crypto_authenc_esn_decrypt() в рамках перестановки байт seqno делает 4-байтный STORE по адресу assoclen + cryptlen в dst SGL:
cscatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1);
scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1);
Атакующий контролирует и адрес, и значение этих 4 байт:
-
Адрес – через выбор длины payload (точно подгоняется так, чтобы попасть в нужный file offset страницы P).
-
Значение – это
seq_hiизXFRMA_REPLAY_ESN_VAL, который пользователь сам выставляет при регистрации SA через netlink.
То есть это arbitrary 4-byte STORE primitive в page cache. AEAD-проверка после STORE возвращает -EBADMSG, но изменение уже сделано.
Цена входа: для регистрации XFRM SA нужен CAP_NET_ADMIN, поэтому эксплойт делает unshare(CLONE_NEWUSER | CLONE_NEWNET). На дистрибутивах, где разрешено создание непривилегированных user namespace, всё работает.
Вариант 2: RxRPC Page-Cache Write
Точка входа – rxkad_verify_packet_1() в net/rxrpc/rxkad.c. На уровне RXRPC_SECURITY_AUTH ядро делает in-place pcbc(fcrypt) decrypt первых 8 байт payload skb:
cret = skb_to_sgvec(skb, sg, sp->offset, 8);memset(&iv, 0, sizeof(iv));skcipher_request_set_crypt(req, sg, sg, 8, iv.x); // src == dst, in-placeret = crypto_skcipher_decrypt(req);
ret = skb_to_sgvec(skb, sg, sp->offset, 8); memset(&iv, 0, sizeof(iv)); skcipher_request_set_crypt(req, sg, sg, 8, iv.x); // src == dst, in-place ret = crypto_skcipher_decrypt(req);
Проверка перед этим – только skb_cloned(skb), но не skb->data_len, поэтому нелинейный skb с подсаженной страницей в frag проскакивает.
Тут есть нюанс: STORE-значение – это не произвольные байты, а результат fcrypt_decrypt(C, K), где C – уже лежащий по этому смещению ciphertext, а K – сессионный ключ из RxRPC v1 token (его атакующий сам кладёт через add_key("rxrpc", ...), без каких-либо привилегий).
fcrypt – это AFS-cipher с 56-битным ключом и блоком 8 байт. Детерминированная функция, легко портируется в user-space. Эксплойт перебирает K локально (~18 M ключей/сек) до тех пор, пока fcrypt_decrypt(C, K) не выдаст нужный паттерн plaintext. Полные 8 байт перебрать нереально (~2⁵⁶), но если фиксировать только 2–3 ключевых байта – подбор укладывается в миллисекунды.
Цель – не перезаписать всё /etc/passwd, а превратить первую строку в:
textroot::0:0:GGGGGG:/root:/bin/bash
root::0:0:GGGGGG:/root:/bin/bash
Пустое поле пароля + pam_unix.so nullok в PAM common-auth = su - без запроса пароля. Дальше – setresuid(0,0,0) и /bin/bash от root.
Главное: никаких namespace, никакого CAP_*. Только add_key(), socket(AF_RXRPC), splice(), recvmsg() – всё доступно обычному непривилегированному пользователю.
Почему это не Dirty Pipe-2
Dirty Pipe был race-condition-free, но опирался на специфическое состояние struct pipe_buffer – нужно было «протолкнуть» нужные флаги через splice. Dirty Frag устроен ещё прямолинейнее: in-place crypto по определению пишет в dst, а dst – это страница page cache, на которую у атакующего read-only доступ.
-
Не нужен timing window. Между splice и crypto-STORE нет конкурентного события, которое надо успеть.
-
Не нужен race. Логика детерминированная: подсадили страницу – она прошла до sink – STORE гарантирован.
-
Ядро не паникует при неудаче. AEAD/HMAC возвращает ошибку, skb просто дропается, никаких BUG/oops.
-
Высокий success rate. На тестовых стендах – фактически 100%.
Чем это отличается от Dirty Pipe и Copy Fail
|
Параметр |
Dirty Pipe (CVE-2022-0847) |
Copy Fail (CVE-2026-31431) |
Dirty Frag (CVE pending) |
|---|---|---|---|
|
Объект записи |
|
TX SGL поверх page cache |
|
|
Sink |
|
|
|
|
Triggering subsystem |
pipe + splice |
AF_ALG ( |
xfrm/IPsec ESP + RxRPC |
|
Race condition |
Нет |
Нет |
Нет |
|
Размер примитива |
произвольная запись |
4 байта |
4 байта (ESP) / 8 байт (RxRPC) |
|
Привилегии |
непривилегированный user |
непривилегированный user (если algif_aead доступен) |
|
|
Workaround |
патч ядра |
blacklist |
blacklist |
|
Mitigation Copy Fail помогает? |
– |
– |
Нет, обходится |
|
Affected ядра |
5.8+ |
актуальные |
ESP: с 4.10 (2017), RxRPC: с 6.5 (2023) |
|
CVE |
назначено |
назначено |
не назначено |
Главный вывод из таблицы – публично известный workaround Copy Fail (algif_aead blacklist) не защищает от Dirty Frag. Это уже отдельный sink, через другой crypto-путь.
Кого это реально касается
К счастью не все среды одинаково уязвимы.
Высокий риск
-
Shared hosting и VPS-провайдеры. Любой клиент с обычной shell-сессией поднимается до root на хост-ноде, если применима cgroup-политика без user namespace lockdown. Для CloudLinux уже отдельный пост с готовым livepatch.
-
Kubernetes worker nodes. Контейнер с
hostPID: falseи стандартным seccomp всё равно даёт доступ кadd_key(),socket(AF_RXRPC),splice(). RxRPC-вариант здесь работает «из коробки» на Ubuntu-нодах, где модуль грузится по умолчанию. ESP-вариант – на нодах сkernel.unprivileged_userns_clone=1. -
CI/CD runners (GitLab Runner, GitHub Actions self-hosted, Jenkins agents). Любой PR от внешнего контрибьютора в публичном репозитории = выполнение произвольного кода в runner-окружении. Если runner общий между проектами – компрометация одного проекта = компрометация всех.
-
Multi-tenant sandbox-окружения: code playgrounds, ML-тренажёры, песочницы для статей и обучения.
Средний риск
-
Серверы с локальными непривилегированными пользователями. Корпоративные jump-hosts, dev-боксы, build-серверы. Если у пользователя есть SSH – у него есть root.
-
Контейнерные среды без user namespace lockdown. Docker без
userns-remap, podman в rootfull-режиме на Ubuntu.
Пониженный риск
-
Десктопы с одним пользователем. Без локального доступа постороннего эксплойт не запустить.
-
Single-purpose серверы и сетевые аплайансы. Об этом подробнее ниже.
Наш продукт Ideco NGFW
Уязвим ли Ideco NGFW?
Технически ядро Linux, на котором работает Ideco NGFW, относится к классу затронутых. Но реальный attack surface для Dirty Frag на NGFW принципиально другой:
-
На Ideco NGFW нет shell-сессий обычных пользователей. Управление – через web-консоль и SSH под выделенными аккаунтами администраторов, без
splice/add_key-сценариев. -
Нет CI/CD-нагрузок, нет компиляции произвольного кода, нет multi-tenant изоляции внутри аплайанса.
-
Trigger-вектор Dirty Frag требует локального исполнения произвольного бинаря под непривилегированным пользователем. На правильно сконфигурированном NGFW такой возможности у внешнего атакующего просто нет – нужен сначала RCE из сети, а это уже другая категория угрозы.
То есть для Dirty Frag на NGFW справедлив принцип «не первая дверь, которую можно сломать». Тем не менее, мы готовим обновление с патчем netdev сразу, как только апстрим стабилизируется и пройдёт регрессионное тестирование.
Что делать прямо сейчас
Минимальный чеклист для системного администратора Linux. Порядок шагов важен.
1. Проверить, загружены ли уязвимые модули
lsmod | grep -E '^(esp4|esp6|rxrpc) '
Если ничего не выводится – модули не загружены, риск минимальный (но не нулевой: модули могут подгружаться автоматически по запросу через MODULE_ALIAS_NETPROTO).
2. Оценить, нужны ли модули в проде
-
esp4/esp6нужны, если есть IPsec (strongSwan, libreswan, racoon, FRR с IPsec-туннелями, любые kernel-mode VPN). -
rxrpcнужен только при использовании AFS (Andrew File System) – на 99% серверов не используется, особенно на Ubuntu, где он грузится по умолчанию «на всякий случай». -
WireGuard, OpenVPN, Tailscale, Nebula, GRE – не используют ни esp4/esp6, ни rxrpc. На них не повлияет blacklist модулей esp.
3. Применить workaround там, где это безопасно
Для серверов без IPsec:
bashsudo sh -c "printf 'install esp4 /bin/false\ninstall esp6 /bin/false\ninstall rxrpc /bin/false\n' > /etc/modprobe.d/dirtyfrag.conf; rmmod esp4 esp6 rxrpc 2>/dev/null; true"sudo sync && echo 3 | sudo tee /proc/sys/vm/drop_caches
sudo sh -c "printf 'install esp4 /bin/false\ninstall esp6 /bin/false\ninstall rxrpc /bin/false\n' > /etc/modprobe.d/dirtyfrag.conf; rmmod esp4 esp6 rxrpc 2>/dev/null; true" sudo sync && echo 3 | sudo tee /proc/sys/vm/drop_caches
Для серверов с IPsec – blacklist’ить только rxrpc:
bashsudo sh -c "printf 'install rxrpc /bin/false\n' > /etc/modprobe.d/dirtyfrag-rxrpc.conf; rmmod rxrpc 2>/dev/null; true"
sudo sh -c "printf 'install rxrpc /bin/false\n' > /etc/modprobe.d/dirtyfrag-rxrpc.conf; rmmod rxrpc 2>/dev/null; true"
Это закрывает один из двух векторов цепочки на Ubuntu (где rxrpc загружается по умолчанию даже без AFS). ESP-вариант остаётся открытым до выхода патча от вендора, но требует возможности создания user namespace.
4. Закрыть user namespace там, где они не нужны
На Ubuntu/Debian:
bashsudo sysctl -w kernel.unprivileged_userns_clone=0echo 'kernel.unprivileged_userns_clone = 0' | sudo tee /etc/sysctl.d/99-no-userns.conf
sudo sysctl -w kernel.unprivileged_userns_clone=0 echo 'kernel.unprivileged_userns_clone = 0' | sudo tee /etc/sysctl.d/99-no-userns.conf
На RHEL/Alma/Rocky/Fedora:
bashsudo sysctl -w user.max_user_namespaces=0echo 'user.max_user_namespaces = 0' | sudo tee /etc/sysctl.d/99-no-userns.conf
sudo sysctl -w user.max_user_namespaces=0 echo 'user.max_user_namespaces = 0' | sudo tee /etc/sysctl.d/99-no-userns.conf
Учтите, что это сломает rootless Docker/Podman, Firefox sandbox в некоторых конфигурациях, и часть инструментов вроде bwrap/flatpak. Применять там, где это допустимо.
5. Подписаться на security-уведомления и обновиться, как только выйдет патч
-
Ubuntu – USN-канал, пока тикета нет, но он появится.
-
Debian – DSA.
-
RHEL/Alma/Rocky – AlmaLinux уже выкатил тестовые ядра.
-
CloudLinux – KernelCare livepatch в работе.
-
SUSE/openSUSE – тикет в работе, апдейт ожидается на этой неделе.
После накатки патча проверить отсутствие SKBFL_SHARED_FRAG-обхода:
bashzgrep -E 'SKBFL_SHARED_FRAG|skb_has_shared_frag' /proc/config.gz 2>/dev/null# или, если /proc/config.gz нет:modinfo esp4 | grep -i version
zgrep -E 'SKBFL_SHARED_FRAG|skb_has_shared_frag' /proc/config.gz 2>/dev/null # или, если /proc/config.gz нет: modinfo esp4 | grep -i version
И отдельно – проверка следов компрометации. Если на сервере уже запускался PoC, в page cache могут оставаться модифицированные копии /usr/bin/su и /etc/passwd. Грубая проверка:
bash# Сравнить хеш файла на диске и через cat (page cache)sha256sum /usr/bin/sucat /usr/bin/su | sha256sumdiff <(getent passwd root) <(grep '^root:' /etc/passwd)
# Сравнить хеш файла на диске и через cat (page cache) sha256sum /usr/bin/su cat /usr/bin/su | sha256sum diff <(getent passwd root) <(grep '^root:' /etc/passwd)
При расхождении – echo 3 > /proc/sys/vm/drop_caches и обязательная перезагрузка с проверкой целостности через rpm -V / dpkg --verify / debsums. Если расхождение остаётся после этого – исходим из факта компрометации хоста.
Итог
Dirty Frag – пример того, что класс «in-place crypto over splice’d page» оказался шире, чем казалось после Copy Fail. Локальное повышение привилегий до root на всех мажорных дистрибутивах через детерминированный баг без race condition, без CVE, с публичным PoC и без готовых патчей в большинстве дистрибутивов – это серьезная проблема безопасности.
Мы в Ideco следим за этой историей с момента появления первой публикации в netdev 30 апреля. Мы уже включили в план ближайших минорных релизов выпуска патчей для поддерживаемых версий – как только апстрим стабилизирует фикс.
Похоже, что ближайшие месяцы принесут ещё несколько уязвимостей этого класса – в других подсистемах, где тоже есть splice → in-place transform → STORE.
ссылка на оригинал статьи https://habr.com/ru/articles/1032842/