
C-Proof-of-Concept für CVE-2026-8933, eine lokale Rechteausweitung in snap-confine, die cgroup-Freezing, Symlink-Races und udev-Regel-Injection nutzt, um Befehle als root auszuführen.
Reproduziere die Qualys-Advisory „Local Privilege Escalation in set-capabilities versions of snap-confine“ (https://cdn2.qualys.com/advisory/2026/07/21/snap-confine-set-capabilities.txt).
Auf diesem Rechner (Ubuntu 25.10 / snapd 2.73+ubuntu25.10.1 / Kernel 6.17) praktisch verifiziert: Ausgehend von uid 1000
wird der Befehl schließlich von systemd-udevd als root ausgeführt, und /tmp/pwned erhält
uid=0(root) gid=0(root) groups=0(root), was mit der abschließenden Demonstration in der Ankündigung übereinstimmt.
Aufzeichnungen über die Fallstricke während der Reproduktion sowie darüber, warum die erste Bewertung fälschlicherweise zu dem Schluss "nicht ausnutzbar" kam, siehe NOTES.md.
| Datei | Beschreibung |
|---|---|
poc.c | Vollständiger PoC (einzelne Datei) |
Makefile | Build (linkt die im System installierte libfuse3.so.3.17.4) |
include/ | libfuse3 3.17.4 Header-Dateien (passend zur Laufzeitbibliothek des Systems) |
NOTES.md | Aufzeichnung der Fallstricke + Retrospektive der Methodik zur Erreichbarkeitsanalyse |
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)
Die Zeilennummern stammen aus dem Quellcode des lokalen snapd 2.73 (cmd/snap-confine/mount-support.c). Die Qualys-
Meldung zitiert ihre eigene Kopie dieser Datei; die entsprechenden Zeilennummern sind 511 / 523 / 530 / 535 / 536 /
362 / 450 / 454 – die Anweisungen entsprechen sich eins zu eins, nur die Zeilennummern unterscheiden sich.
Das lokale /usr/lib/snapd/snap-confine hat mode=755 + Datei-Capabilities (nicht
setuid-root), daher läuft es mit der euid des Angreifers, besitzt aber nahezu Root-Capabilities: Die von ihm erstellten Verzeichnisse und
Dateien gehören für kurze Zeit dem Angreifer, und das open() in Zeile 418 folgt symbolischen Links.
Die Disassemblierung bestätigt: Die Flags von open@plt sind lokal 0x240 (O_CREAT|O_TRUNC, ohne
O_NOFOLLOW); an derselben Stelle im gepatchten 2.77.1 lauten sie 0x20240.
systemd-run --user --scope --unit=snap.<snap>.<app>. Dieser Scope gehört dem aktuellen Benutzer, cgroup.freeze ist beschreibbar,
sodass snap-confine Syscall für Syscall freigegeben werden kann.SNAPD_DEBUG=1 gibt snap-confine
associated snap application process ... with device cgroup aus. Diese Zeile erscheint nach dem
Anhängen des BPF-Programms der Device-cgroup, aber vor mkdtemp(). Nicht früher einfrieren:
Der BPF-Verifier gibt bei vorhandener Freeze-Falle EAGAIN zurück, und snap-confine beendet sich sofort.
stderr ist mit einem pty verbunden (keine Pipe): Andernfalls wäre stdio blockgepuffert, und der Marker würde
oft erst nach mkdtemp() und dem Bind-Mount herausgespült.
Außerdem startet der Scope mit CPUQuota=1%: Vom Marker bis mkdtemp() bleiben nur wenige hundert Mikrosekunden
CPU-Zeit, in derselben Größenordnung wie die Aufwachverzögerung des abgreifenden Prozesses; die Drosselung dehnt
dieses Fenster in realer Zeit auf Millisekunden aus, und nach Bestätigung des Einfrierens wird cpu.max sofort
wieder auf 100% gesetzt.mount(2) folgt
Symlinks im Zielpfad, sodass die Bind-/Unbindable-/tmpfs-Mounts von snap-confine alle auf
<scratch>.dA landen, während die Kopierschleife weiterhin den String des Scratch-Verzeichnisses verwendet.ns_mount_count(sc, .dA) >= 2), den Symlink auf
<scratch>.dB umbiegen und in dB Symlinks mit denselben Namen wie die regulären Dateien der obersten Ebene
des Base-Rootfs ablegen, die auf /run/udev/rules.d/97-cve-2026-8933-<pid>-<rand>.rules zeigen. Nun landet die
Pfadauflösung der Kopierschleife in dB – einem gewöhnlichen Verzeichnis, das wir vollständig kontrollieren. Das
tmpfs bleibt in dA zurück und stört uns nicht mehr.
Achtung: Bei nur 1 Mount auf .dA handelt es sich um einen Bind-Mount (/tmp ist selbst tmpfs, nach dem Bind
ist der fstype weiterhin tmpfs); erst wenn der 2. Mount (das eigentliche tmpfs) erscheint, darf umgebogen werden,
sonst scheitert mount --make-unbindable von snap-confine mit EINVAL, weil der Pfad kein Mount-Root mehr ist.open(O_CREAT|O_TRUNC) aus (folgt dem Symlink und erstellt die Datei als root unter /run/udev/rules.d),
danach folgt fchown(fd,0,0). In diesem Augenblick gehört die Datei noch uns. Der PoC nutzt zwei Mittel
gleichzeitig:
stat() die Datei sieht
und die uid noch die eigene ist, sofort chmod 0666 + Schreiben der udev-Regel (snap-confine ist dann
eingefroren, das Fenster wird „aufgespreizt“);chmod()-Heißschleife dasselbe Fenster ab. Hat ein
Spinner es erwischt, gehört die Datei root, hat aber den Modus 0666 – die Modusbits bleiben nach
fchown(0,0) erhalten, sodass auch der Hauptpfad sie fertig schreiben kann (im Code wird geprüft, ob man
schreiben kann, nicht ob man Eigentümer ist)./run/udev/** rw.
Das Ein- und Aushängen eines FUSE-Dateisystems veranlasst den Kernel, für das bdi-Subsystem ein add-uevent
auszusenden, woraufhin systemd-udevd als root das PROGRAM= aus der Regel ausführt.make
./poc --diagnose # 只读自检,不改动任何系统状态
./poc # exploit,默认最多 5 次尝试
./poc --attempts 8 # 增加尝试次数
./poc --file default256.png # 只用 base rootfs 里的这一个普通文件
sudo ./poc --cleanup # 清理(仅在异常中断留下残留时需要)
Parameter:
| Parameter | Beschreibung |
|---|---|
--snap NAME | Welches snap-Rootfs verwendet wird (Standard: firefox, alternativ über die Umgebungsvariable CVE8933_SNAP) |
--app NAME | Die App dieses snaps (standardmäßig die erste unter apps: in meta/snap.yaml) |
--file NAME | Nur diese eine reguläre Datei aus dem Basis-Rootfs verwenden; bei fehlender Übereinstimmung mit Fehler beenden |
--attempts N | Anzahl der Versuche (Standard: 5) |
--step US / --step2 US | CPU-Zeit pro Slice für die beiden Phasen (Mikrosekunden), zum Debuggen |
--keep | Die Verzeichnisse <scratch>.dA / .dB und Symlinks behalten (zum Debuggen) |
--diagnose | Nur-Lese-Selbsttest |
--cleanup | Historische Reste bereinigen (erfordert root) |
Logs werden nach /tmp/cve8933.log geschrieben, die Debug-Ausgabe von snap-confine nach
/tmp/cve8933-debug.txt.
Mit CVE8933_PAYLOAD_CMD='<Shell-Befehl>' kann der Payload-Rumpf ersetzt werden (um die Ausführung anderer
root-Befehle zu demonstrieren); in diesem Fall erfolgt keine automatische Bereinigung; der benutzerdefinierte Befehl muss weiterhin /tmp/pwned schreiben, da der PoC sonst
davon ausgeht, dass dieser Versuch nicht erfolgreich war.
$ ./poc
[+] WIN: uid=0(root) gid=0(root) groups=0(root)
[+] exploit succeeded, evidence in /tmp/pwned