Skip to content
KitploitKITPLOIT
ToolsExploitsBlog
Log in
Submit
ToolsExploitsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

FeedsContactPrivacy© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2026-8933 — C proof-of-concept for CVE-2026-8933, a snap-confine local privilege escalation using cgroup freezing, symlink races, and udev rule injection to execute commands as root. | Kitploit
Tools/GitHubGitHub/yifengchen-cc/cve-2026-8933
Privilege EscalationContainer SecurityVulnerability AnalysisExploitation
GitHubyifengchen-cc/cve-2026-8933

CVE-2026-8933

C proof-of-concept for CVE-2026-8933, a snap-confine local privilege escalation using cgroup freezing, symlink races, and udev rule injection to execute commands as root.

View Repository
14 days agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

CVE-2026-8933 — snap-confine set-capabilities Local Privilege Escalation PoC

复现 Qualys 公告《Local Privilege Escalation in set-capabilities versions of snap-confine》(https://cdn2.qualys.com/advisory/2026/07/21/snap-confine-set-capabilities.txt)。

Tested on this machine (Ubuntu 25.10 / snapd 2.73+ubuntu25.10.1 / kernel 6.17): starting from uid 1000, ultimately systemd-udevd executes the command as root, and /tmp/pwned obtains uid=0(root) gid=0(root) groups=0(root), consistent with the effect demonstrated at the end of the advisory.

For the record of pitfalls encountered during reproduction, and why the first assessment misjudged it as "not exploitable", see NOTES.md.

Files

FileDescription
poc.cComplete PoC (single file)
MakefileBuild (links against the system-installed libfuse3.so.3.17.4)
include/libfuse3 3.17.4 headers (matching the system runtime library)
NOTES.mdPitfall record + retrospective on the reachability analysis methodology

Vulnerability Principle

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)

Line numbers are taken from the source code of snapd 2.73 on this machine (cmd/snap-confine/mount-support.c). The Qualys advisory cites its own copy of the file, where the corresponding line numbers are 511 / 523 / 530 / 535 / 536 / 362 / 450 / 454 — the statements correspond one-to-one, only the line numbers differ.

The local /usr/lib/snapd/snap-confine is mode=755 + file capabilities (not setuid-root), so it runs with the attacker's euid but holds near-root capabilities: the directories and files it creates belong to the attacker for a short period of time, and the open() on line 418 follows symbolic links.

Disassembly confirms: on this machine the flags of open@plt are 0x240 (O_CREAT|O_TRUNC, without O_NOFOLLOW); at the same point in the fixed 2.77.1 it is 0x20240.

The six steps of the PoC

  1. Put snap-confine into its own cgroup: systemd-run --user --scope --unit=snap.<snap>.<app>. This scope belongs to the current user, cgroup.freeze is writable, so snap-confine can be released one system call at a time.
  2. Freeze at the right place: with SNAPD_DEBUG=1, snap-confine prints associated snap application process ... with device cgroup. This line appears after the device cgroup's BPF program is attached and before mkdtemp(). Do not freeze earlier: the BPF verifier returns EAGAIN when a freeze trap is present, and snap-confine exits immediately. stderr is connected to a pty (not a pipe): otherwise stdio is block-buffered and the marker is often not flushed until after mkdtemp() and the bind mount. In addition, the scope is started with CPUQuota=1%: from the marker to mkdtemp() there are only a few hundred microseconds of CPU time, on the same order as the wake-up latency of the grabbing process; throttling stretches this window in real time to the millisecond level, and once the freeze is confirmed, cpu.max is immediately restored to 100%.
  3. Replace the scratch directory with a symlink pointing to our own directory: mount(2) follows symlinks in the target path, so snap-confine's bind/unbindable/tmpfs all land on <scratch>.dA, while the copy loop still uses the scratch directory as a string.
  4. After the tmpfs is mounted (ns_mount_count(sc, .dA) >= 2), repoint the symlink to <scratch>.dB, and place in dB symlinks with the same names as the top-level regular files of the base rootfs, pointing to /run/udev/rules.d/97-cve-2026-8933-<pid>-<rand>.rules. At this point the path resolution of the copy loop falls into dB — an ordinary directory we fully control. The tmpfs is left in dA and no longer gets in our way. Note: when there is only 1 mount on .dA, it is a bind mount (after binding, /tmp is itself a tmpfs, so the fstype is still tmpfs); you must wait until the 2nd mount (the real tmpfs) appears before repointing, otherwise snap-confine's mount --make-unbindable will fail with EINVAL because the path is no longer a mount root.
  5. Win the window between open()/fchown(): the copy loop executes open(O_CREAT|O_TRUNC) on the first regular file (following the symlink, creating the file as root under /run/udev/rules.d), followed by fchown(fd,0,0). At this instant the file still belongs to us. The PoC uses two methods at once:
    • Main path: keep using the freezer to release only about one system call at a time; once stat() sees the file and the uid is still our own, immediately chmod 0666 + write the udev rule (at this point snap-confine is frozen and the window is "held open");
    • Fallback: several spinner processes race for the same window with a chmod() hot loop. After a spinner wins, the file is owned by root but has mode 0666 — the mode bits are still preserved after fchown(0,0), so the main path can likewise finish writing it (the code checks "can it be written", not "is it the owner").
  6. Trigger udev: snap-confine's AppArmor profile allows /run/udev/** rw. Mounting/unmounting a FUSE filesystem causes the kernel to emit an add uevent for the bdi subsystem, and systemd-udevd then executes the rule's PROGRAM= as root.

Usage

make
./poc --diagnose            # 只读自检,不改动任何系统状态
./poc                       #  exploit,默认最多 5 次尝试
./poc --attempts 8          # 增加尝试次数
./poc --file default256.png # 只用 base rootfs 里的这一个普通文件
sudo ./poc --cleanup        # 清理(仅在异常中断留下残留时需要)

Parameters:

ParameterDescription
--snap NAMEWhich snap's rootfs to use (default firefox, can also use the environment variable CVE8933_SNAP)
--app NAMEThe app for that snap (defaults to the first one under apps: in meta/snap.yaml)
--file NAMEUse only this one regular file from the base rootfs; exit with an error if there is no match
--attempts NNumber of attempts (default 5)
--step US / --step2 USPer-slice CPU time (microseconds) for the two phases, for debugging
--keepKeep the <scratch>.dA / .dB directories and symlinks (for debugging)
--diagnoseRead-only self-check
--cleanupClean up historical leftovers (requires root)

Logs are written to /tmp/cve8933.log, and snap-confine's debug output is written to /tmp/cve8933-debug.txt.

CVE8933_PAYLOAD_CMD='<shell command>' can replace the payload body (used to demonstrate executing other root commands); in this case it will not clean up automatically; the custom command must still write /tmp/pwned, otherwise the PoC will consider this attempt unsuccessful.

Actual Test Results

$ ./poc
[+] WIN: uid=0(root) gid=0(root) groups=0(root)
[+] exploit succeeded, evidence in /tmp/pwned

The corresponding successful run in /tmp/cve8933.log (2026-10-23, retest after changes):

[    0.256] === attempt 1/5 ===
[ 1062.158] marker seen after 750.5 ms
[ 1067.530] scratch dir /tmp/snap.rootfs_oFBraI replaced by a symlink to ...dA
[ 1100.887] bind mount + tmpfs on ...dA after 417 slices
[ 1100.974] scratch directory now points to ...dB with 4 symlinks -> ...rules
[ 1108.782] took the file over while snap-confine was stopped (242 slices)
[ 1108.820] rule written after 797.1 ms
[ 1108.832] target: uid=0 gid=0 mode=0666 size=73 content="ACTION=="add", ..."
Download Tool