| 항목 | 내용 |
|---|---|
| CVE ID | CVE-2021-3560 |
| 영향 버전 | polkit 0.113 ~ 0.119 미만 (도입: 2013년경 commit, 실제 배포는 0.113부터) |
| CVSS v3.1 | 7.8 (High) — AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H |
| 공개일 | 2022-02-16 (NVD), 실제 수정 공개는 2021-06-03 |
| 비고 | CISA KEV 목록 등재(2023-05-12). 버그 자체는 약 7년간 존재 |
polkit은 리눅스 데스크톱 환경에서 "이 요청을 보낸 프로세스가 이 작업을 할 권한이 있는가"를 판단해주는 인증 프레임워크다. CVE-2021-3560은 polkit이 요청자의 신원을 D-Bus를 통해 조회하는 과정에서, 조회가 실패했을 때의 처리를 잘못해 요청자를 root(UID 0)로 오인하게 만드는 취약점이다. 로컬의 권한 없는 사용자가 정확한 타이밍에 D-Bus 요청을 보냈다가 끊는 것만으로 root 권한이 필요한 작업(예: 새 사용자 계정 생성)을 인증 없이 통과시킬 수 있다.
리눅스 데스크톱에서 일반 사용자 프로세스가 시스템 전역 작업(디스크 마운트, 계정 생성,
전원 관리 등)을 하려면, 그 작업을 실제로 수행하는 시스템 서비스(예: accounts-daemon)에게
D-Bus 라는 프로세스 간 통신(IPC) 메커니즘으로 요청을 보낸다. 시스템 서비스는 이 요청을
바로 수행하지 않고, 먼저 polkit에게 "이 요청을 보낸 프로세스가 이 작업을 해도 되는가?" 를
물어본다. polkit은 요청자의 신원(UID)과 미리 정의된 정책을 대조해 허용/거부를 결정한다.
이 흐름에서 중요한 전제는, polkit이 "누가 요청했는가"를 알아내려면 D-Bus 연결 정보를 통해 그 순간에 그 연결이 실제로 아직 살아있어야 한다는 것이다.
D-Bus에 연결하는 각 프로세스는 :1.96 같은 형식의 고유한 연결 이름을 부여받는다. 어떤
서비스가 "이 연결 이름의 소유자가 누구인지" 알고 싶으면, dbus-daemon에게 그 연결 이름의
UID를 물어본다. 문제는, 그 연결이 이미 끊어진 뒤에 조회하면 정상적인 UID 값이 아니라
오류가 돌아온다는 점이다 — 그리고 이 오류를 어떻게 처리하느냐가 이 취약점의 핵심이다.
polkit이 요청자의 자격 증명(credential)을 가져오는 함수는
polkit_system_bus_name_get_creds_sync 다. 이 함수는 dbus-daemon에게 UID 조회를
요청하는데, 연결이 이미 사라져서 dbus-daemon이 오류를 반환하는 경우에도 함수 자체는
TRUE(성공)를 반환한다. 호출하는 쪽에서 이 오류를 별도로 검사하지 않으면, "조회는
성공했지만 값이 비어있는" 상태를 마치 정상적인 결과처럼 다루게 된다.
check_authorization_sync 경로의 로직을 개념적으로 옮기면 다음과 같다 (GitHub Security Lab
writeup 기준).
user_of_subject = polkit_backend_session_monitor_get_user_for_subject(...);
if (user_of_subject == NULL)
goto out;
/* 여기서 오류(연결 없음)와 "정상적으로 UID 0을 조회함"이 구분되지 않는다 */
if (POLKIT_IS_UNIX_USER(user_of_subject) &&
polkit_unix_user_get_uid(user_of_subject) == 0) {
/* 이 분기가 "오류 상태"에서도 우연히 참이 될 수 있음 */
결과 = 권한 부여됨;
}즉 "UID 조회 실패"와 "조회했더니 UID가 0(root)이더라"가 코드 상에서 같은 경로로 흘러갈 수 있다는 것이 근본 결함이다. 정상 조회와 오류가 결과적으로 같은 상자에 담기는 모습을 그리면:
정상 케이스 (연결이 살아있음)
dbus-daemon 조회 ──▶ ┌──────────────┐
│ UID = 1000 │ ──▶ 일반 사용자 확인 ──▶ 권한 거부 (정상)
└──────────────┘
취약 케이스 (연결이 이미 끊긴 뒤 조회, 패치 전)
dbus-daemon 조회 ──▶ ┌───────────────────┐
│ ERROR (연결 없음) │
└─────────┬─────────┘
│ (오류값 검사 없이 TRUE 반환)
▼
┌──────────────┐
│ UID = 0 로 │ ──▶ root로 오인 ──▶ 권한 부여 (오판) ✗
│ 오인됨 │
└──────────────┘
"오류" 라는 별도의 상자가 있어야 하는데, 취약한 코드에서는 그 상자가 "UID = 0" 상자와 구분되지 않고 겹쳐버린다는 것이 이 그림의 핵심이다.
이 결함만으로는 공격이 안 된다 — 정확한 타이밍에 연결을 끊어야 dbus-daemon이 UID 조회에 실패하기 때문이다. 즉 공격은 다음과 같은 TOCTOU(Time-Of-Check to Time-Of-Use) 패턴을 이용한다.
공격자 프로세스 polkit / accounts-daemon
│
├─ D-Bus로 특권 작업 요청 전송
│ (예: accounts-daemon에 새 사용자 생성 요청)
│ │
│ ├─ polkit에게 "이 요청자, 권한 있어?" 질의
│ │
├─ (정확한 타이밍, 문서 기준 약 8ms 내에)
│ 요청 프로세스를 강제 종료해 D-Bus 연결을 끊음
│ │
│ ├─ polkit이 dbus-daemon에 UID 조회
│ │ → 연결이 이미 없어 오류 반환
│ │ → 오류가 무시되고 UID 0으로 오인
│ ├─ 권한 부여됨 (잘못된 승인)
│ └─ accounts-daemon이 특권 작업 수행
v
결과: 인증 없이 특권 작업(계정 생성 등)이 수행됨
연결이 사라진 시점과 조회 시점이 정확히 겹쳐야 하므로 확률적인 공격이지만, 반복 시도로 안정적으로 재현 가능하다고 보고됐다.
accounts-daemon을 통한 사용자 생성 같은 작업은 원래 root만 할 수 있어야 한다. 이 취약점이
성립하면 권한 없는 로컬 사용자가 이 검사를 우회해 관리자 그룹에 속한 새 계정을 만들고
비밀번호를 설정하는 식으로 최종적으로 root 권한을 얻을 수 있다.
polkit 개발자들은 polkit_system_bus_name_get_creds_sync가 오류 발생 시 FALSE를
반환하도록 수정하고, 호출하는 쪽 모두가 반환값을 확인해 오류를 오류로 제대로 처리하도록
바꿨다. 즉 "조회 실패"와 "조회 성공(UID 확인)"이 더 이상 같은 경로로 흐르지 않도록 오류
전파 경로를 명확히 한 것이 수정의 핵심이다. 이 수정은 polkit 0.119에 반영됐다
(공식 수정 커밋: a04d13affe0fa53ff618e07aa8f57f4c0e3b9b81,
gitlab.freedesktop.org/polkit/polkit).
앞서 그린 "겹쳐진 상자" 그림을 패치 후 기준으로 다시 그리면, 오류 상자와 UID 상자가 분리된다.
취약 케이스 (패치 후)
dbus-daemon 조회 ──▶ ┌───────────────────┐
│ ERROR (연결 없음) │
└─────────┬─────────┘
│ (오류값을 그대로 FALSE 로 반환)
▼
┌──────────────────────┐
│ 인증 자체를 실패 처리 │ ──▶ 권한 거부 (정상) ✓
└──────────────────────┘
이제 "연결이 사라짐"과 "UID가 0으로 확인됨"은 서로 다른 결과로 확실히 갈라지므로, 연결을 끊는 타이밍을 아무리 잘 맞춰도 root로 오인되는 경로 자체가 없어진다.
- Privilege escalation with polkit: How to get root on Linux with a seven-year-old bug — GitHub Security Lab (엘리트 출처)
- CVE-2021-3560 — NVD
- GHSA-7c49-j253-wq5r — GitHub Advisory Database
- 수정 커밋 a04d13affe0fa53ff618e07aa8f57f4c0e3b9b81 — polkit GitLab
- CVE-2021-3560 — Debian Security Tracker
- gitlab.freedesktop.org의 실제 커밋 diff는 접근 제한(Anubis 봇 차단)으로 직접 확인하지 못했다. 위 코드는 GitHub Security Lab의 원문 설명을 근거로 재구성한 것이며, 실제 소스 줄 단위까지 대조하지는 못했다 — 필요하면 다음에 polkit 소스를 직접 클론해 대조할 것.
- "정확히 8ms" 타이밍 값은 2차 자료(GitHub Security Lab 원문 요약)에서 나온 수치이며, 이 문서 작성 과정에서 직접 재현/검증하지는 않았다.