| 항목 | 내용 |
|---|---|
| CVE ID | CVE-2018-19788 |
| 영향 패키지 | PolicyKit (polkit), 0.115 및 그 이전 (0.116 미만) |
| 영향 배포판 | Debian 8/9, Ubuntu 12.04 ESM/14.04/16.04/18.04/18.10 등 polkit 0.115 계열을 배포한 모든 배포판 |
| CVSS | v3.0 8.8 (High) — AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H (NVD 기준) |
| 공개일 | 2018-12-03 (GitLab 이슈/패치), 2018-12-09 (공개 writeup), 2019-01 전후 각 배포판 advisory 게시 |
| 수정 버전 | polkit 0.116 (NEWS 파일에 "Fix of CVE-2018-19788, high UIDs caused overflow in polkit" 로 명시) |
polkit 은 D-Bus 로 들어오는 호출자의 UID 를 확인해 "이 프로세스가 정말 그 사용자가 맞는가",
"이 사용자가 루트인가" 를 판단하고 이를 근거로 pkexec/systemctl 같은 특권 동작의 인가 여부를
결정한다. 그런데 polkit 내부에서 UID 를 저장하는 GObject 프로퍼티와 C 구조체 필드가 커널의
uid_t(부호 없는 32비트)가 아니라 부호 있는 32비트 정수(gint)로 선언되어 있었다. 리눅스에서는
useradd -u 4000000000 처럼 INT_MAX(2147483647)를 넘는 UID 를 가진 계정을 실제로 만들 수
있는데, 이런 사용자가 D-Bus 로 systemctl/pkexec 류 특권 동작을 요청하면, 그 UID 값이 polkit
내부에서 gint 범위로 클램프(clamp)되면서 원래 값과 다른 값(전형적으로 0, 즉 루트)으로
뒤바뀌어 버린다. 그 결과 실제로는 전혀 권한이 없는 일반 사용자가 "루트로 인식"되거나 인가 검사
로직이 기대와 다르게 동작해, 어떤 systemctl/pkexec 동작이든 승인 대화상자 없이 실행할 수
있게 된다. 근본 원인은 자료형 불일치(부호 없는 값을 부호 있는 좁은 타입에 담음)이며, 공격
자체는 매우 단순하다 — 공격자가 임의로 UID 를 고를 수 있는 상황(신규 계정 생성 권한이 있거나,
이미 그런 UID 로 계정이 존재하는 컨테이너/시스템)에서만 성립한다.
polkit(PolicyKit)은 리눅스 데스크톱/서버에서 "권한이 없는 프로세스가 권한이 있는 프로세스에게
특정 동작을 요청해도 되는가" 를 판단해주는 중앙 인가(authorization) 프레임워크다. systemd,
NetworkManager, udisks, systemctl, pkexec 등 다양한 서비스가 "이 사용자가 이 액션을 할
자격이 있는가?" 를 직접 판단하지 않고 D-Bus 를 통해 polkit 데몬(polkitd)에게 위임한다. polkit
은 이 요청을 세 가지 개념으로 나눠 다룬다.
- Subject(주체): 요청을 보낸 프로세스. 보통 D-Bus 커넥션의 PID/시작시각, 혹은 그로부터
얻어낸 UID 로 식별된다(
PolkitUnixProcess,PolkitSystemBusName). - Action(액션):
org.freedesktop.systemd1.manage-units같은 문자열 ID 로 식별되는, "이 동작을 해도 되는가" 의 대상. - Authority(권한 판단자): 정책 파일(
.rules,.pkla)과 현재 인증 상태를 참고해 "허용/거부/인증 필요" 를 결정하는 polkit 데몬 자체.
호출 흐름은 대략 다음과 같다.
사용자 프로세스 (systemctl, pkexec 등)
│ D-Bus 메서드 호출 (예: org.freedesktop.PolicyKit1.Authority.CheckAuthorization)
▼
polkitd (Authority 구현체)
│ 1) D-Bus 로부터 호출자의 실제 uid/pid 를 얻음 (커널이 검증한 값, 위조 불가)
│ 2) 그 uid 로 PolkitUnixUser/PolkitUnixProcess Subject 객체 생성
│ 3) 이 Subject 가 "root(uid 0)"인지, 정책이 허용하는 대상인지 검사
▼
허용 / 거부 / 인증(암호 재확인) 필요 응답
핵심은 2)번 단계다 — D-Bus/커널이 넘겨준 UID 자체는 조작할 수 없는 신뢰된 값이지만, 그 값을 polkit 내부 객체에 담는 과정에서 문제가 생기면 이후의 모든 판단이 틀어진다.
리눅스에서 사용자 ID 를 나타내는 커널/POSIX 표준 타입은 uid_t이며, glibc 기준
typedef unsigned int uid_t; 로 정의된 부호 없는 32비트 정수다. 표현 가능한 범위는
0 ~ 4294967295(2^32 - 1)이고, 관례상 (uid_t) -1(즉 4294967295)은 "값 없음/실패"를 나타내는
특수값으로 예약되어 대개 실제 사용자에게는 배정하지 않는다. 반면 C 의 일반 int 는 대부분의
64비트 리눅스 플랫폼에서 부호 있는 32비트 정수이고, INT_MAX(2147483647, 2^31 - 1)를 넘는
값은 표현할 수 없다. uid_t 값을 int 변수에 대입하면, INT_MAX 이하일 때는 문제가 없지만
그 이상(예: 3000000000)일 때는 구현 정의(implementation-defined) 동작으로 값이 음수로
재해석되거나, 뒤에서 볼 GObject 프로퍼티처럼 별도 검증 로직이 있으면 그 로직의 허용 범위로
"클램프"될 수 있다.
uid_t (unsigned 32bit) 범위: 0 ─────────────────────────────────── 4294967295
↑ INT_MAX(2147483647) 여기서 부호있는 int 범위 끝
int (signed 32bit) 범위: -2147483648 ─────────────── 0 ─────────────── 2147483647
↑ 여기까지만 안전하게 표현 가능
리눅스는 커널 레벨에서 uid_t 가 부호 없는 32비트이므로 이론상 INT_MAX 를 넘는 UID 를 가진
계정을 만드는 것을 막지 않는다(useradd -u 3000000000 testuser 가 실제로 동작한다). 실무에서는
이런 UID 를 잘 쓰지 않지만, 컨테이너 UID 네임스페이스 매핑, 대규모 조직의 UID 자동 할당 정책,
LDAP/AD 연동 등에서 큰 UID 값이 실제로 등장할 수 있다.
polkit 은 GLib/GObject 위에 작성되어 있고, PolkitUnixUser/PolkitUnixGroup/
PolkitUnixProcess 같은 핵심 타입은 모두 GObject 클래스다. GObject 프로퍼티는
g_param_spec_int() 로 선언할 때 최소/최대값을 지정하며, g_object_new()/
g_object_set_property() 로 값을 설정할 때 GObject 내부의 g_param_value_validate() 가 그
값이 선언된 범위 안에 있는지 검사한다. 범위를 벗어난 값이 들어오면 에러를 내지 않고, 그
범위 안으로 값을 잘라내는(clamp) 방식으로 조용히 수정한다. 이 특성 자체는 GObject 의 정상
동작이지만, 만약 "uid" 프로퍼티의 선언 범위가 [0, G_MAXINT](즉 int 로 표현 가능한 최대
양수까지)로 잡혀 있다면, INT_MAX 를 넘는 UID 값 — 예를 들어 D-Bus 에서 이미 guint32 로
올바르게 넘어온 4000000000 — 이 이 프로퍼티를 거치는 순간 조용히 다른 값으로 바뀌어버릴 수
있다는 뜻이 된다.
취약 버전(polkit 0.115 및 그 이전)의 세 핵심 타입은 모두 UID/GID 를 부호 있는 gint 로
저장한다(src/polkit/polkitunixuser.c, polkitunixgroup.c, polkitunixprocess.c).
/* src/polkit/polkitunixuser.c (0.115) */
struct _PolkitUnixUser
{
GObject parent_instance;
gint uid; /* 부호 있는 32비트 — uid_t(부호 없는 32비트)를 담기엔 부족 */
gchar *name;
};/* src/polkit/polkitunixprocess.c (0.115) */
struct _PolkitUnixProcess
{
GObject parent_instance;
gint pid;
guint64 start_time;
guint64 cgroupid;
gint uid; /* 여기도 동일하게 gint */
...
};D-Bus 계층(polkitsystembusname.c)은 커널이 검증한 UID 를 정확히 guint32 로 받아온다.
/* src/polkit/polkitsystembusname.c (0.115) — polkit_system_bus_name_get_user_sync() */
PolkitUnixUser *
polkit_system_bus_name_get_user_sync (PolkitSystemBusName *system_bus_name,
GCancellable *cancellable,
GError **error)
{
PolkitUnixUser *ret = NULL;
guint32 uid; /* 부호 없음, 값 정확 */
...
if (!polkit_system_bus_name_get_creds_sync (system_bus_name, &uid, NULL,
cancellable, error))
goto out;
ret = (PolkitUnixUser*)polkit_unix_user_new (uid); /* 여기서 폭이 좁아짐 */
...
}그런데 이 값을 넘겨받는 생성자는 인자를 부호 있는 gint 로 선언하고 있다.
/* src/polkit/polkitunixuser.c (0.115) */
PolkitIdentity *
polkit_unix_user_new (gint uid) /* guint32 4000000000 → 암묵적 변환 발생 */
{
return POLKIT_IDENTITY (g_object_new (POLKIT_TYPE_UNIX_USER,
"uid", uid,
NULL));
}그리고 결정적으로, "uid" GObject 프로퍼티 자체가 [0, G_MAXINT] 범위로 선언되어 있었다.
/* src/polkit/polkitunixuser.c (0.115) — polkit_unix_user_class_init() */
g_object_class_install_property (gobject_class,
PROP_UID,
g_param_spec_int ("uid",
"User ID",
"The UNIX user ID",
0, /* 최소값 */
G_MAXINT, /* 최대값 = 2147483647 */
0, /* 기본값 */
G_PARAM_CONSTRUCT | G_PARAM_READWRITE | ...));g_object_new(..., "uid", uid, ...) 를 거치면 GObject 는 g_param_value_validate() 로 이
프로퍼티 값을 [0, G_MAXINT] 범위에 맞게 검증·클램프한다. uid_t/guint32 로는 정확히
4000000000 이었던 값이, gint 로 좁혀지는 과정과 이 프로퍼티 검증을 거치면서 원래 의도와
다른 값(구현별로 음수로 재해석되거나 G_MAXINT 로 잘리는 등)으로 바뀐다. 실사용 사례로
보고된 것은 4000000000 같은 큰 UID 값을 가진 사용자가 systemctl/pkexec 호출을 할 때
"인가 검사가 기대와 다르게 통과"하는 것이었고, 상위 계층의 identity_is_root_user() 는 다음처럼
단순히 uid == 0 비교만 한다(src/polkitbackend/polkitbackendinteractiveauthority.c).
static gboolean
identity_is_root_user (PolkitIdentity *user)
{
if (!POLKIT_IS_UNIX_USER (user))
return FALSE;
return polkit_unix_user_get_uid (POLKIT_UNIX_USER (user)) == 0;
}즉 polkit 은 "이 Subject 가 루트인가" 를 판단할 때 원래의 실제 UID 값이 아니라, 좁은 gint
필드에 저장된(그리고 프로퍼티 검증을 거쳐 재해석된) 값을 기준으로 비교한다. UID 값 자체가
왜곡되는 지점(gint 캐스팅 + GObject 프로퍼티 클램프)과, 그 값을 "루트인지" 판단하는 지점
(== 0 비교)이 서로 다른 계층에 있다 보니, 앞단에서 값이 이미 오염된 채로 뒷단의 신뢰 판단에
쓰이는 구조적 문제다.
[커널/D-Bus] [polkit 내부]
guint32 uid = 4000000000 ──────▶ gint uid (polkit_unix_user_new 인자)
(정확한 값, 위조 불가) │
▼
g_object_new(..., "uid", uid, ...)
│
▼
g_param_value_validate() : "uid" 프로퍼티 범위 [0, G_MAXINT] 로 검증
│
▼
struct._PolkitUnixUser.uid ← 원래 값과 다른 값으로 저장 ★버그 지점
│
▼
identity_is_root_user(): uid == 0 ? ← 값이 왜곡된 채로 판단
│
▼
systemctl / pkexec 인가 검사가 기대와 다르게 "허용"으로 귀결
공개된 PoC/writeup 에 따르면, 실제 UID 를 정확히 INT_MAX 초과 값(예: 4000000000)으로 가진
계정을 만든 뒤 그 계정으로 systemctl link(임의 유닛 파일을 유닛 검색 경로에 심는 옵션)를
호출하면 승인 절차 없이 성공한다. 이 유닛 파일의 ExecStart 를 조작하면 결과적으로 루트 권한
임의 명령 실행으로 이어진다(다른 연구자는 systemd-run 으로도 동일 효과를 재현했다고 언급됨).
공격이 성립하려면 공격자가 스스로 그런 UID 계정을 만들 수 있거나(로컬 관리 권한 또는
컨테이너/멀티테넌시 환경에서 UID 매핑을 조작할 수 있는 상황), 혹은 이미 그런 UID 를 가진
계정이 시스템에 존재해야 한다 — 그래서 CVSS 상 "Privileges Required: Low" 로 분류되어 있고,
실제 노출 범위는 UID 배정 정책에 따라 제한적이다.
수정은 2018년 12월 3일 Zbigniew Jędrzejewski-Szmek 이 작성한 커밋
2cb40c4d5feeaa09325522bd7d97910f1b59e379
("Allow negative uids/gids in PolkitUnixUser and Group objects", GitLab MR negative-uids →
merge 커밋 5230646)로 이루어졌고, src/polkit/polkitunixuser.c,
src/polkit/polkitunixgroup.c, src/polkit/polkitunixprocess.c 세 파일을 수정했다. 이 커밋은
GitLab 이슈 polkit/polkit#74 를 닫으며, upstream NEWS 파일에 "CVE-2018-19788 High UIDs
overflow fix" 로 명시적으로 기록되어 polkit 0.116 릴리스에 포함되었다.
핵심 아이디어는 "UID 를 여전히 gint 필드에 저장하는 근본 자료형은 바꾸지 않되(API 호환성
때문에 큰 변경을 피함), 대신 GObject 프로퍼티가 음수 값도 그대로 통과시키도록 범위를
[G_MININT, G_MAXINT] 로 넓혀서 guint32 UID 가 gint 로 재해석되어 음수가 되더라도 그
비트 패턴이 클램프로 훼손되지 않고 그대로 보존되게 만드는 것"이다. 대신 (uid_t) -1 값 하나만
"미설정"을 뜻하는 예약값으로 명시적으로 거부한다.
--- a/src/polkit/polkitunixuser.c
+++ b/src/polkit/polkitunixuser.c
@@ polkit_unix_user_init()
+ unix_user->uid = -1; /* (uid_t) -1 is not a valid UID under Linux */
@@ polkit_unix_user_set_property()
+ gint val;
case PROP_UID:
- unix_user->uid = g_value_get_int (value);
+ val = g_value_get_int (value);
+ g_return_if_fail (val != -1);
+ unix_user->uid = val;
@@ polkit_unix_user_class_init()
g_object_class_install_property (..., g_param_spec_int ("uid", ...,
- 0,
+ G_MININT,
G_MAXINT,
- 0,
+ -1,
...));
@@ polkit_unix_user_set_uid()
g_return_if_fail (POLKIT_IS_UNIX_USER (user));
+ g_return_if_fail (uid != -1);
user->uid = uid;
@@ polkit_unix_user_new()
PolkitIdentity *
polkit_unix_user_new (gint uid)
{
+ g_return_val_if_fail (uid != -1, NULL);
return POLKIT_IDENTITY (g_object_new (POLKIT_TYPE_UNIX_USER, "uid", uid, NULL));
}polkitunixgroup.c 도 gid 에 대해 동일한 패턴(G_MININT 로 범위 확장, -1 을 예약값으로
거부)을 적용했고, polkitunixprocess.c 는 uid 프로퍼티 범위를 -1, G_MAXINT 에서
G_MININT, G_MAXINT 로 넓히면서 기존에 있던 g_return_if_fail (uid >= -1); 하한 검사를
제거했다(이 검사 자체가 큰 UID 가 음수로 들어오는 것을 걸러내던 또 다른 걸림돌이었기 때문).
이 수정 직후, PolkitUnixProcess 가 "UID 미지정" 상태를 나타내는 데 정말 -1 을 그대로 쓰던
polkit_unix_process_new_for_owner() 경로에서 회귀(regression)가 발생해, 12월 11일 Matthew
Leeds 가 커밋 c05472b86222a72505adc5eec460493980224ef8
("Allow uid of -1 for a PolkitUnixProcess")로 PolkitUnixProcess 의 set_property 에서만
-1 거부 체크를 다시 제거했다. 커밋 메시지에 명시된 대로 "이 되돌림은 CVE-2018-19788 에 대한
보호에는 영향을 주지 않는다"(보호는 "UID 가 4294967295(-1) 미만인 값까지는 그대로 통과시킨다"
는 원칙에 기반하므로).
패치 후 흐름을 다시 그리면, 문제의 그 지점(GObject 프로퍼티 클램프)에서 값이 더 이상 훼손되지 않는다.
[커널/D-Bus] [polkit 내부, 패치 후]
guint32 uid = 4000000000 ──────▶ gint uid (polkit_unix_user_new 인자)
(정확한 값, 위조 불가) │ 4000000000 은 gint 로 재해석되면 음수 비트패턴이 되지만
│ '값이 왜곡되는가' 가 아니라 '그 비트패턴이 보존되는가' 가 관건
▼
g_object_new(..., "uid", uid, ...)
│
▼
g_param_value_validate() : "uid" 프로퍼티 범위 [G_MININT, G_MAXINT] ← 넓어짐
│ 범위 안에 들어오므로 더 이상 클램프되지 않음
▼
struct._PolkitUnixUser.uid ← 원래 uid_t 값과 대응하는 비트패턴 그대로 보존
│
▼
identity_is_root_user(): uid == 0 ? ← 4000000000 에 대응하는 값은 0이 아니므로 거짓
│
▼
systemctl / pkexec 인가 검사가 실제 권한(비루트)대로 정상 거부/인증요구
즉 패치는 "부호 없는 UID 를 부호 있는 gint 로 좁혀 담는다" 는 근본 설계는 그대로 둔 채,
"그 좁히기 과정에서 GObject 프로퍼티가 값을 조용히 클램프해 서로 다른 UID 를 같은 값으로
뭉개버리는 부작용" 만 제거한 임시방편에 가깝다. 커밋 메시지 자체도 "더 나은 수정은 근본 타입을
uint32 로 바꾸는 것이지만, 이는 API 를 깨뜨리므로 별도의 큰 패치가 필요하다" 고 명시적으로
인정하고 있다.
- NVD — CVE-2018-19788
- Red Hat Bugzilla 1655925 — CVE-2018-19788 polkit: Improper handling of user with uid > INT_MAX leading to authentication bypass
- Red Hat CVE-2018-19788
- RHSA-2019:2046
- RHSA-2019:3232
- Debian Bug #915332
- Debian DSA-4350-1 polkit
- Debian LTS 공지 (2019-01)
- Ubuntu USN-3861-1
- Ubuntu USN-3861-2
- Gentoo GLSA 201908-14
- Hacker News 스레드 — "PolicyKit: Users with UID greater than INT_MAX can execute any systemctl command"
- polkit GitLab 이슈 #74 (원본 버그 리포트, 접근 시 봇 차단으로 본문 재확인은 실패 — 아래 "미해결/불확실 지점" 참고)
- 수정 커밋
2cb40c4d5feeaa09325522bd7d97910f1b59e379("Allow negative uids/gids in PolkitUnixUser and Group objects") — polkit 공식 저장소를 미러링하는polkit-org/polkitGitHub 저장소의 git 이력에서 동일 해시로 직접 확인 - 후속 회귀 수정 커밋
c05472b86222a72505adc5eec460493980224ef8("Allow uid of -1 for a PolkitUnixProcess") — 위와 동일 저장소에서 확인 - polkit 0.116
NEWS파일 — "Fix of CVE-2018-19788, high UIDs caused overflow in polkit" / "Zbigniew Jędrzejewski-Szmek: CVE-2018-19788 High UIDs overflow fix" 명시 (polkit-org/polkit저장소 커밋cd7a41d1a331ce38eedfb4ed3196d7b0a8987263) - Mirch 블로그 — CVE-2018-19788 PoC 분석 글 (비공식, 미검증 서드파티 writeup)
polkit/polkit#74GitLab 이슈 페이지 본문은 Anubis 봇 차단 페이지에 막혀 직접 인용할 수 없었다. 이슈 내용 자체는 수정 커밋 메시지("Fixes gitlab.freedesktop.org/polkit/polkit/issues/74") 와 각 배포판 advisory 를 통해 간접적으로만 교차 확인했으며, 이슈 원문의 최초 재현 절차나 보고자 신원은 검증하지 못했다.- Red Hat Bugzilla 에 언급된 두 커밋 해시(
fbaab32cb4ed9ed5f1e3eea6cd317d443aa427dc,7c8c3abdedbb991a69bc5f1ab0f96576958b55de)는 Zbigniew Jędrzejewski-Szmek 개인 GitLab 포크(gitlab.freedesktop.org/zbyszek/polkit) 상의 초기 작업용 커밋으로 보이며, 이 리포트가 분석 근거로 삼은polkit-org/polkit공식 미러 저장소의 git 이력에는 해당 해시가 존재하지 않는다(머지 시 스쿼시/리베이스되어 최종적으로2cb40c4d5feeaa09325522bd7d97910f1b59e379해시로master에 반영된 것으로 추정). 개인 포크 저장소는 봇 차단으로 직접 열어보지 못해, 두 해시가 정확히2cb40c4이전 어떤 중간 상태였는지는 확인하지 못했다. - 공개 PoC 코드는 이번 분석에서 직접 열어보거나 실행하지 않았다. 위 참고 자료의 Mirch 블로그 글은 문자열/절차 설명만 참고했고, 별도로 발견한 GitHub 공개 PoC 저장소는 없어 이 리포트에는 포함하지 않았다.