
Доказательство концепции на C для CVE-2026-8933 — локальное повышение привилегий в snap-confine с использованием замораживания cgroup, гонок символических ссылок и внедрения правил udev для выполнения команд от имени root.
Воспроизвести бюллетень Qualys «Local Privilege Escalation in set-capabilities versions of snap-confine» (https://cdn2.qualys.com/advisory/2026/07/21/snap-confine-set-capabilities.txt).
Проверено на локальной машине (Ubuntu 25.10 / snapd 2.73+ubuntu25.10.1 / ядро 6.17): начиная с uid 1000,
в итоге systemd-udevd выполняет команду от имени root, /tmp/pwned получает
uid=0(root) gid=0(root) groups=0(root), что совпадает с эффектом, продемонстрированным в конце объявления.
Записи о подводных камнях в процессе воспроизведения и о том, почему при первой оценке было ошибочно решено, что «эксплуатация невозможна», см. в NOTES.md.
| Файл | Описание |
|---|---|
poc.c | Полный PoC (один файл) |
Makefile | Сборка (линковка с установленной в системе libfuse3.so.3.17.4) |
include/ | Заголовочные файлы libfuse3 3.17.4 (соответствуют системной библиотеке времени выполнения) |
NOTES.md | Записи о подводных камнях + ретроспектива методологии анализа достижимости |
cmd/snap-confine/mount-support.c (snapd 2.73):
sc_bootstrap_mount_namespace() // 第 475 行起
479: mkdtemp("/tmp/snap.rootfs_XXXXXX") <- 目录先归调用者所有
491: sc_do_mount(scratch, scratch, NULL, MS_BIND)
498: sc_do_mount("none", scratch, NULL, MS_UNBINDABLE)
503: sc_do_mount("none", scratch, "tmpfs", ...)
504: sc_replicate_base_rootfs(scratch, rootfs_dir, mounts)
sc_replicate_base_rootfs()
330: chown(scratch, 0, 0)
418: fd = open(full_path, O_CREAT | O_TRUNC, 0644) <- 缺少 O_NOFOLLOW
422: fchown(fd, 0, 0)
Номера строк взяты из исходного кода snapd 2.73 на этой машине (cmd/snap-confine/mount-support.c). В бюллетене Qualys
цитируется собственная копия файла, и соответствующие номера строк — 511 / 523 / 530 / 535 / 536 /
362 / 450 / 454 — операторы соответствуют один к одному, различаются только номера строк.
Локальный /usr/lib/snapd/snap-confine имеет mode=755 + файловые capabilities (а не
setuid-root), поэтому он работает с euid атакующего, но при этом обладает почти root-овыми возможностями: создаваемые им каталоги и
файлы в течение короткого времени принадлежат атакующему, а open() в строке 418 следует по символическим ссылкам.
Дизассемблирование подтверждает: на этой машине флаги open@plt равны 0x240 (O_CREAT|O_TRUNC, без
O_NOFOLLOW); в исправленной версии 2.77.1 в той же точке — 0x20240.
systemd-run --user --scope --unit=snap.<snap>.<app>. Этот scope принадлежит текущему пользователю, cgroup.freeze доступен для записи,
поэтому можно пропускать snap-confine по одному системному вызову за раз.SNAPD_DEBUG=1 snap-confine печатает
associated snap application process ... with device cgroup. Эта строка появляется после
attach BPF-программы к device cgroup и до mkdtemp(). Нельзя замораживать раньше:
BPF-верификатор при наличии freeze trap возвращает EAGAIN, и snap-confine просто завершается.
stderr подключён к pty (не к каналу): иначе stdio будет блочно буферизован, и маркер часто
сбрасывается только после mkdtemp() и bind mount.
Кроме того, scope запускается с CPUQuota=1%: от маркера до mkdtemp() проходит всего несколько сотен микросекунд
процессорного времени, что сравнимо с задержкой пробуждения перехватывающего процесса; ограничение растягивает это окно в реальном времени
до миллисекунд, а после подтверждения заморозки cpu.max немедленно восстанавливается до 100%.mount(2) следует по символьным ссылкам в целевом
пути, поэтому bind/unbindable/tmpfs snap-confine все оказываются на
<scratch>.dA, а цикл копирования по-прежнему использует scratch-каталог как строку.ns_mount_count(sc, .dA) >= 2), затем перенаправить символьную ссылку на
<scratch>.dB и поместить в dB символьные ссылки с теми же именами, что у обычных файлов верхнего уровня
base rootfs, указывающие на /run/udev/rules.d/97-cve-2026-8933-<pid>-<rand>.rules. В этот момент разрешение пути
в цикле копирования попадает в dB — обычный каталог, полностью контролируемый нами. tmpfs остаётся в dA и больше
нам не мешает.
Внимание: когда на .dA только 1 монтирование, это bind mount (/tmp сам по себе tmpfs, после bind
fstype остаётся tmpfs); перенаправлять можно только после появления 2-го монтирования (настоящего tmpfs), иначе
mount --make-unbindable в snap-confine завершится с EINVAL, так как путь больше не является корнем монтирования.open(O_CREAT|O_TRUNC) (следуя по символьной ссылке, создавая файл от имени root в /run/udev/rules.d),
затем fchown(fd,0,0). В этот момент файл всё ещё принадлежит нам. PoC использует
два средства одновременно:
stat() увидит файл
и uid всё ещё наш, немедленно chmod 0666 + записать правило udev (в этот момент snap-confine
заморожен, окно «растянуто»);chmod() захватывают то же окно. После захвата spinner'ом
файл принадлежит root, но имеет режим 0666 — биты режима сохраняются и после fchown(0,0),
поэтому основной путь тоже может дописать его (в коде проверяется «можно ли писать», а не «являемся ли мы
владельцем»)./run/udev/** rw.
Монтирование/размонтирование файловой системы FUSE заставляет ядро отправить uevent add для подсистемы bdi,
и systemd-udevd немедленно выполняет PROGRAM= из правила от имени root.make
./poc --diagnose # 只读自检,不改动任何系统状态
./poc # exploit,默认最多 5 次尝试
./poc --attempts 8 # 增加尝试次数
./poc --file default256.png # 只用 base rootfs 里的这一个普通文件
sudo ./poc --cleanup # 清理(仅在异常中断留下残留时需要)
Параметры:
| Параметр | Описание |
|---|---|
--snap NAME | Какой snap использовать для rootfs (по умолчанию firefox, также можно задать через переменную окружения CVE8933_SNAP) |
--app NAME | Приложение этого snap (по умолчанию берётся первое из раздела apps: в meta/snap.yaml) |
--file NAME | Использовать только этот обычный файл из base rootfs; если совпадений нет — завершиться с ошибкой |
--attempts N | Количество попыток (по умолчанию 5) |
--step US / --step2 US | Время CPU на каждый фрагмент для двух этапов (в микросекундах), для отладки |
--keep | Сохранять каталоги <scratch>.dA / .dB и символические ссылки (для отладки) |
--diagnose | Самопроверка только для чтения |
--cleanup | Очистка исторических остатков (требуются права root) |
Журнал записывается в /tmp/cve8933.log, отладочный вывод snap-confine — в
/tmp/cve8933-debug.txt.
CVE8933_PAYLOAD_CMD='<shell-команда>' позволяет заменить тело payload (для демонстрации выполнения других
root-команд); в этом случае автоматическая очистка не выполняется; пользовательская команда всё равно должна записать /tmp/pwned, иначе PoC
посчитает, что на этот раз успеха не было.
$ ./poc
[+] WIN: uid=0(root) gid=0(root) groups=0(root)
[+] exploit succeeded, evidence in /tmp/pwned
/tmp/cve8933.log — соответствующий успешный прогон (2026-10-23, повторный тест после изменений):