
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 argv[0] es \x00 y argv[1] apunta a la variable de entorno. ¡Cuando se lee argv[1], se desborda y lee 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:

¿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