Skip to content

Latest commit

 

History

History
366 lines (314 loc) · 22.3 KB

File metadata and controls

366 lines (314 loc) · 22.3 KB

polkit UID 정수 오버플로 인증 우회 (CVE-2018-19788)

메타데이터

항목 내용
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 이 뭘 하는 컴포넌트인가

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 내부 객체에 담는 과정에서 문제가 생기면 이후의 모든 판단이 틀어진다.

uid_t 는 부호 없는 32비트, int 는 부호 있는 32비트

리눅스에서 사용자 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 값이 실제로 등장할 수 있다.

GObject 프로퍼티와 값 검증(clamping)

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 — 이 이 프로퍼티를 거치는 순간 조용히 다른 값으로 바뀌어버릴 수 있다는 뜻이 된다.

취약점 분석

자료구조: gint uid 필드

취약 버전(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.cgid 에 대해 동일한 패턴(G_MININT 로 범위 확장, -1 을 예약값으로 거부)을 적용했고, polkitunixprocess.cuid 프로퍼티 범위를 -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")로 PolkitUnixProcessset_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 를 깨뜨리므로 별도의 큰 패치가 필요하다" 고 명시적으로 인정하고 있다.

참고 자료

미해결/불확실 지점

  • polkit/polkit#74 GitLab 이슈 페이지 본문은 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 저장소는 없어 이 리포트에는 포함하지 않았다.