
PoC educativo y análisis de CVE-2021-4034 (PwnKit), vulnerabilidad de escalada de privilegios local en pkexec de polkit, con un laboratorio basado en Docker para la explotación práctica y la práctica de defensa.
🔗 Proyecto original: berdav/CVE-2021-4034
Este proyecto es una versión analizada y modificada del original con fines educativos.
Cumplimiento de la licencia MIT | Tarea educativa de White Hat School
CVE-2021-4034 es una vulnerabilidad de escalada de privilegios local en policykit-1 (PolicyKit) de Linux. Un usuario normal puede ejecutar pkexec sin argumentos para aprovechar una falla en la estructura de la memoria del proceso y, a través de ella, hacer que glib vuelva a referenciar cadenas de variables de entorno que deberían haber sido filtradas, logrando cargar un archivo .so malicioso y obtener privilegios de root.
⚠️ Solo con fines educativos: este código debe utilizarse únicamente en sistemas modificados.
Su uso para atacar sistemas reales puede acarrear responsabilidad legal.
| Elemento | Contenido |
|---|---|
| CVE ID | CVE-2021-4034 |
| Nombre de la vulnerabilidad | PwnKit |
| Versiones afectadas | Versiones de polkit sin el parche anterior a 0.105 (según el entorno de prueba: policykit-1 0.105-26ubuntu1 de Ubuntu 20.04) |
| Tipo de vulnerabilidad | Local Privilege Escalation (LPE) |
| Severidad | Critical (CVSS 7.8) |
| Versión parcheada | policykit-1 >= 0.105-26ubuntu1.1 |
| Fecha de descubrimiento | Junio de 2021 (publicada en enero de 2022) |
Es fácil malinterpretarlo como un problema exclusivo de Ubuntu, pero como se trata de un defecto de lógica del propio pkexec, la mayoría de las distribuciones que usan polkit se ven afectadas. Como el entorno de prueba con Docker es Ubuntu 20.04, esa versión se incluyó en la tabla anterior.
Al principio del análisis pensé que era un problema "causado por no validar las variables de entorno", pero al revisar juntos el código fuente y el commit del parche me di cuenta de que el orden es un poco diferente. La causa real es otra, y el problema de las variables de entorno es más bien la consecuencia que se deriva de esa causa. A continuación se resume el flujo en orden de causa y efecto.
pkexec es un programa SUID-root que solicita elevación de privilegios a través de PolicyKit.
# 예: root 권한으로 명령어 실행
pkexec /bin/id
pkexec systemctl restart service
Lo usa un usuario normal cuando necesita realizar determinadas tareas con privilegios de administrador.
argc == 0La función main() de pkexec, en la parte que procesa los argumentos de la línea de comandos, no valida el caso en que se ejecuta sin ningún argumento (argc == 0). Este es el verdadero punto de partida de la vulnerabilidad.
argv = {"pkexec", "comando", NULL} → argc >= 1execve("/usr/bin/pkexec", {NULL}, env) → argc == 0Cuando argc es 0, en la lista argv solo queda un NULL que indica el final. Sin embargo, la lógica interna de pkexec intenta leer y escribir en el inexistente argv[1] incluso en esta situación. El problema es que, cuando Linux ejecuta un proceso, coloca el arreglo argv y el arreglo envp (variables de entorno) uno justo al lado del otro en memoria. Por eso, el argv[1] fuera de límites en realidad apunta directamente a envp[0], es decir, a la primera variable de entorno.
정상 상황: argv = [ "pkexec" | NULL ]
공격 상황: argv = [ NULL ] ← argc = 0
↑
존재하지 않는 argv[1]에 접근
↓
메모리상 바로 뒤에 있는 envp[0]을 읽고 쓰게 됨 (out-of-bounds)
¿Por qué es peligroso?
GCONV_PATH, LD_PRELOAD antes de que se ejecute el programa SUID (pkexec), al considerarlas inseguras.argc < 1. (Corresponde a CWE-125 lectura fuera de límites y CWE-787 escritura fuera de límites)📌 En resumen: la falta de validación de variables de entorno es "la condición que permite el ataque", y la vulnerabilidad real (causa raíz) es que pkexec no maneja el caso argc == 0. El punto 3 a continuación es la consecuencia derivada de esta causa.
Gracias al comportamiento OOB descrito en el punto 2, esta cadena se vuelve a utilizar sin validación durante el proceso de inicialización de glib por parte de pkexec.
// CVE-2021-4034_exploit.c
char * const env[] = {
"GCONV_PATH=.", // 원래는 ld.so가 걸러냈어야 함
"CHARSET=PWNKIT", // 존재하지 않는 인코딩
};
execve("/usr/bin/pkexec", args, env); // argv는 비워서 argc=0을 만듦
El problema:
Lo importante aquí es que glib no tiene la culpa. Que glib busque el converter en esa ruta cuando GCONV_PATH está configurada es su funcionamiento normal. El problema es que pkexec ya ha roto el estado de ejecución segura (el estado en el que se eliminan las variables de entorno peligrosas) — glib simplemente actuó con normalidad, y es precisamente ese comportamiento normal el que se explota.
Comprobación de la variable de entorno CHARSET
CHARSET=PWNKIT
Búsqueda de la definición del converter en el archivo gconv-modules
module UTF-8// PWNKIT// pwnkit 1
Carga del archivo .so desde GCONV_PATH
GCONV_PATH=. → 현재 디렉토리에서 pwnkit.so 검색
Ejecución automática de la función de inicialización del archivo .so
// pwnkit.c - .so 파일 로드 시 자동으로 실행됨
void gconv_init(void *step)
{
setuid(0); // root 권한 획득
setgid(0);
execve("/bin/sh"); // root shell 실행!
}
Es fácil llamar a gconv_init una "función constructora", pero estrictamente no es lo mismo que __attribute__((constructor)) de C. En concreto, es la función de inicialización definida en la interfaz del módulo gconv, y glib la invoca explícitamente tras cargar el .so con dlopen.
┌─────────────────────────────────────┐
│ 일반 사용자 (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) │
└─────────────────────────────────────┘