| 항목 | 내용 |
|---|---|
| CVE ID | CVE-2021-4034 |
| 영향 버전 | polkit 0.121 미만 (사실상 2009년 pkexec 최초 버전부터 12년간) |
| CVSS v3.1 | 7.8 (High) — AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H |
| 공개일 | 2022-01-28 |
| 비고 | CISA KEV 목록 등재. Qualys Research Team 발견 |
pkexec 는 polkit이 제공하는, 일반 사용자가 정책에 따라 다른 사용자(주로 root) 권한으로
명령을 실행할 수 있게 해주는 SUID-root 프로그램이다. PwnKit은 pkexec 가 argc
(인자 개수)가 0인 비정상적인 실행 상황을 전혀 예상하지 못해서 발생하는 취약점이다. 로컬의
권한 없는 사용자가 execve() 를 직접 조작해 pkexec 를 argc=0 으로 실행하면, 프로그램이
자신의 인자 배열이라고 믿고 읽는 메모리가 실제로는 환경변수 배열이 되어버리고, 이를
이용해 SUID 프로그램에서 원래 제거됐어야 할 위험한 환경변수(LD_PRELOAD 계열)를 다시
주입할 수 있게 된다. 결과적으로 로컬 사용자가 완전한 root 권한을 얻는다.
리눅스에서 execve() 로 새 프로세스를 시작하면, 커널은 스택에 인자 배열(argv)과 환경변수
배열(envp)을 연속해서 배치한다. 개념적으로는 다음과 같은 모양이다.
정상적인 경우 (argc = 2)
스택: ┌──────────┬──────────┬──────┬──────────┬──────────┬──────┐
│ argv[0] │ argv[1] │ NULL │ envp[0] │ envp[1] │ NULL │
│"pkexec" │ "-opt" │ │"PATH=.." │"HOME=.." │ │
└──────────┴──────────┴──────┴──────────┴──────────┴──────┘
▲argv 배열 끝(NULL)이 envp 시작 바로 앞에 옴
argv 배열은 NULL 포인터로 끝난다는 규약이 있을 뿐, argv와 envp가 메모리 상에서
서로 이어져 있다는 사실 자체는 프로그램이 평소에는 신경 쓰지 않는 구현 세부사항이다.
pkexec 처럼 SUID-root 로 표시된 프로그램은 실행되는 순간 동적 링커(ld.so)가
LD_PRELOAD, LD_LIBRARY_PATH 같은 "임의 코드/라이브러리를 로드하게 만들 수 있는"
환경변수를 자동으로 제거한다. 그래서 일반적으로는 SUID 프로그램에게 LD_PRELOAD를 심어도
무시된다 — 이 안전장치를 무력화하는 것이 이 취약점의 최종 목표다.
execve() 는 argv 배열을 "무엇을 넣든" 호출자가 자유롭게 구성할 수 있다. 정상적인
셸은 항상 argv[0] 에 프로그램 이름을 넣지만, 공격자는 직접 execve() 를 호출해 argc
를 0으로, 즉 argv = { NULL } 로 만들 수 있다.
pkexec 의 main() 은 이런 경우를 고려하지 않고 "인자가 최소 1개는 있다"고 가정한 채
다음과 같은 방식으로 동작한다 (Qualys 분석 기준, 개념적으로 재구성).
argc = 0 일 때의 메모리:
┌──────┬──────────┬──────────┬──────┐
│ NULL │ envp[0] │ envp[1] │ NULL │ ← argv 배열은 NULL 하나뿐
└──┬───┴──────────┴──────────┴──────┘
│ ▲
│ └─ pkexec 입장에서는 이게 "argv[1]"로 보임
▼
argv[0] = NULL (argc=0 이므로 존재하지 않아야 할 값)
argv[1] = envp[0] ← 원래는 없어야 할 접근인데, 실제로는 환경변수의 시작 지점을 가리킴
pkexec 코드는 이 상황에서도 argv[1] 을 프로그램 이름으로 읽어 path = g_strdup(argv[1])
같은 식으로 처리한다 — 이 시점에 이미 "환경변수 문자열을 인자로 착각"하고 있는 것이다.
이어서 이 값을 PATH 안에서 검색해 실행 파일 경로를 찾고, 그 결과를 다시 같은 위치
(argv[1], 즉 실제로는 envp[0]) 에 덮어쓴다. 즉 자신의 인자를 "정리"한다고 믿고 쓴
값이, 사실은 환경변수 배열의 시작 부분을 덮어쓰는 결과가 된다.
- 공격자는
PATH=공격자디렉토리라는 환경변수와, 그 디렉토리 안에 특정 이름의 실행 파일을 미리 준비한다. execve()를 직접 호출해pkexec를argc = 0으로 실행한다.pkexec가 (원래는 없어야 할)argv[1]을 읽으면 그게 실제로는envp[0]— 즉 공격자가 심어둔PATH=...문자열이다.pkexec는 이 값을 실행 파일 이름으로 착각해PATH에서 검색하고, 찾은 전체 경로 문자열을 다시envp[0]자리에 덮어쓴다.- 이 과정을 반복적으로 조작하면, 원래 SUID 실행 시 제거됐어야 할
LD_PRELOAD류의 환경변수를 다시 끼워 넣을 수 있는 상태가 된다. pkexec가 root 권한으로 계속 실행되는 동안 이 환경변수가 로드되면서, 공격자가 지정한 공유 라이브러리가 root 권한으로 실행된다.
pkexec 는 거의 모든 주요 리눅스 배포판에 기본 설치된 SUID-root 프로그램이다. 특별한 설정
없이 로컬 사용자 계정만 있으면 공격이 성립하고, 2009년 pkexec 최초 버전부터 존재했던
문제라 사실상 모든 배포판의 오래된 시스템까지 영향을 받았다.
공식 수정 커밋(a2bf5c9c83b6ae46cbd5c779d3055bff81ded683, 작성자 Jan Rybar)은
argc < 1 인 비정상적인 실행을 애초에 차단한다 — 프로그램 시작 시점에 인자 개수를 확인해서
조건을 만족하지 않으면 즉시 종료 코드 126으로 종료시킨다. 즉 "argv[1] 이 사실은
envp[0] 일 수 있는 상황" 자체가 발생하지 않도록 입구에서 막는 방식이다.
argc = 0 일 때의 메모리 (패치 후):
┌──────┬──────────┬──────────┬──────┐
│ NULL │ envp[0] │ envp[1] │ NULL │
└──┬───┴──────────┴──────────┴──────┘
│
▼
main() 시작 직후 "argc < 1" 확인 → 즉시 종료(exit 126)
→ argv[1] 을 읽는 코드까지 도달하지 못함 → envp 오염 발생 안 함
임시 완화책으로는 chmod 0755 /usr/bin/pkexec (SUID 비트 제거)가 제시되기도 했지만, 이는
pkexec 자체의 정상 기능을 막는 방식이라 임시방편에 가깝다.
- CVE-2021-4034: How PwnKit Exploits Polkit's pkexec — Qualys (원저자 writeup)
- CVE-2021-4034 — NVD
- RHSB-2022-001 — Red Hat
- [수정 커밋 a2bf5c9c83b6ae46cbd5c779d3055bff81ded683 관련 논의 — 검색 결과 기반, 원문 diff 미확인]
- 수정 커밋 해시(
a2bf5c9c83b6ae46cbd5c779d3055bff81ded683)는 검색 결과 요약을 통해 확인한 것이며, 실제 polkit 저장소에서 diff 원문을 직접 열어보지는 못했다. "argc < 1 검사 후 exit 126" 이라는 수정 방식의 세부 구현(정확히 어느 파일의 어느 함수인지)은 다음에 polkit 소스를 직접 클론해 대조할 필요가 있다. g_strdup(argv[1])이후의 정확한 변수명과 라인 번호는 Qualys 공개 자료의 요약을 기반으로 재구성한 것이라, 실제 취약 버전 소스 코드와 완전히 일치하지 않을 수 있다.