Búsqueda de Ciberamenazas

Un fallo de seguridad PwnKit te hace root en la mayoría de las distribuciones de Linux: qué hacer

Los investigadores de Qualys han revelado un agujero de seguridad ya parcheado en un kit de herramientas de seguridad de Linux muy utilizado que se incluye en casi todas las distribuciones de Linux.

El fallo se conoce oficialmente como CVE-2021-4034, pero Qualys le ha dado un nombre gracioso, un logotipo y una página web propia, apodándolo PwnKit.

El código defectuoso forma parte del sistema Polkit de Linux, una forma popular de permitir que aplicaciones normales, que no se ejecutan con privilegios especiales, interactúen de forma segura con otro software o servicios del sistema que necesitan o tienen superpoderes administrativos.

Por ejemplo, si tienes un gestor de archivos que te permite ocuparte gestionar los discos USB extraíbles, el gestor de archivos a menudo tendrá que negociar con el sistema operativo para asegurarse de que estás debidamente autorizado para acceder a esos dispositivos.

Si decides que quieres borrar y formatear el disco, puede que necesites acceso a nivel de root para hacerlo, y el sistema Polkit ayudará al gestor de archivos a negociar esos permisos de acceso temporales, normalmente mostrando una ventana de contraseña para verificar tus credenciales.

Si eres un usuario habitual de Linux, probablemente habrás visto los diálogos de Polkit; de hecho, la página man de Polkit, basada en texto, ofrece una representación de arte ASCII en plan vieja escuela:

Polkit como alternativa a sudo

Lo que quizás no sepas de Polkit es que, aunque está orientado a añadir autenticación segura bajo demanda para aplicaciones gráficas, viene con una práctica herramienta de línea de comandos llamada pkexec, abreviatura de Polkit Execute.

En pocas palabras, pkexec es un poco como la conocida utilidad sudo, donde sudo es la abreviatura de Set UID and Do a Command (Establecer UID y ejecutar un comando), en la medida en que permite cambiar temporalmente a un ID de usuario diferente, normalmente root, o UID 0, la cuenta de superusuario con todos los permisos.

De hecho, se utiliza pkexec de la misma manera que sudo, añadiendo pkexec al inicio de una línea de comandos que no tiene derecho a ejecutar para que pkexec lo ejecute por usted, asumiendo que Polkit piensa que está autorizado a hacerlo:

# Regular users are locked out of the Polkit configuration directory...

$ ls -l /etc/polkit-1/rules.d/
/bin/ls: cannot open directory '/etc/polkit-1/rules.d/': Permission denied

# Using an account that hasn't been authorised to run "root level"
# commands via the Polkit configuration files...

$ pkexec ls -l /etc/polkit-1/rules.d/
==== AUTHENTICATING FOR org.freedesktop.policykit.exec ====
Authentication is needed to run `/usr/bin/ls' as the super user
Authenticating as: root
Password: 
polkit-agent-helper-1: pam_authenticate failed: Authentication failure
==== AUTHENTICATION FAILED ====
Error executing command as another user: Not authorized

This incident has been reported.

# After adding a Polkit rule to permit our account to do "root" stuff,
# we get automatic, temporary authorisation to run as the root user...

$ pkexec ls -l /etc/polkit-1/rules.d/
total 20
-rw-r--r-- 1 root root 360 Dec 31  2021 10-enable-powerdevil-discrete-gpu.rules
-rw-r--r-- 1 root root 512 Dec 31  2021 10-enable-session-power.rules
-rw-r--r-- 1 root root 812 Dec 31  2021 10-enable-upower-suspend.rules
-rw-r--r-- 1 root root 132 Dec 31  2021 10-org.freedesktop.NetworkManager.rules
-rw-r--r-- 1 root root 404 Dec 31  2021 20-plugdev-group-mount-override.rules
-rw-r--r-- 1 root root 542 Dec 31  2021 30-blueman-netdev-allow-access.rules

# And if we put no command and no username on the command line, pkexec
# assumes that we want a shell, so it runs our preferred shell (bash),
# making us root (UID 0) until we exit back to the parent shell...

$ pkexec 
bash-5.1# id
uid=0(root) gid=0(root) groups=0(root),...
exit
$ id
uid=1042(duck) gid=1042(duck) groups=1042(duck),...

Además de verificar las reglas de control de acceso (a las que se hace referencia en el listado de archivos anterior), pkexec también realiza otra serie de operaciones de “reforzamiento de la seguridad” antes de ejecutar el comando elegido con privilegios adicionales.

Por ejemplo, considera este programa, que imprime una lista de sus propios argumentos de línea de comandos y variables de entorno:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/*----------------------------ENVP.C---------------------------*/
#include <stdio.h>
#include <string.h>
void showlist(char* name, char* list[]) {
   int i = 0;
   while (1) {
      /* Print both the address where the pointer    */
      /* is stored and the address that it points to */
      printf("%s %2d @ %p [%p]",name,i,list,*list);
      /* If the pointer isn't NULL then print the    */
      /* string that it actually points to as well   */
      if (*list != NULL) { printf(" -> %s",*list); }
      printf("\n");
      /* List ends with NULL, so exit if we're there */
      if (*list == NULL) { break; }
      /* Otherwise move on to the next item in list  */
      list++;
      i = i+1;
   }
}
/* Command-line C programs almost all start with a boilerplate */
/* function called main(), to which the runtime library        */
/* supplies an argument count, the arguments given, and the    */
/* environment variables of the process.                       */
/* argv[] lists the arguments; envp[] lists the env. variables */
      
int main(int argc, char* argv[], char* envp[]) {
   showlist("argv",argv);
   showlist("envp",envp);
   return 0;
}

 

Si compilas este programa y lo ejecutas, verás algo como esto, una larga lista de variables de entorno que reflejan tus propias preferencias y configuraciones:

$ LD_LIBRARY_PATH=risky-variable GCONV_PATH=more-risk ./envp first second
argv  0 @ 0x7fff2ec882c8 [0x7fff2ec895b8] -> ./envp
argv  1 @ 0x7fff2ec882d0 [0x7fff2ec895bf] -> first
argv  2 @ 0x7fff2ec882d8 [0x7fff2ec895c5] -> second
argv  3 @ 0x7fff2ec882e0 [(nil)]
envp  0 @ 0x7fff2ec882e8 [0x7fff2ec895cc] -> GCONV_PATH=more-risk
envp  1 @ 0x7fff2ec882f0 [0x7fff2ec895e1] -> LD_LIBRARY_PATH=risky-variable
envp  2 @ 0x7fff2ec882f8 [0x7fff2ec89600] -> SHELL=/bin/bash
envp  3 @ 0x7fff2ec88300 [0x7fff2ec89610] -> WINDOWID=25165830
envp  4 @ 0x7fff2ec88308 [0x7fff2ec89622] -> COLORTERM=rxvt-xpm
[...]
envp 38 @ 0x7fff2ec88418 [0x7fff2ec89f07] -> PATH=/opt/redacted/bin:/home/duck/...
envp 39 @ 0x7fff2ec88420 [0x7fff2ec89fb9] -> MAIL=/var/mail/duck
envp 40 @ 0x7fff2ec88428 [0x7fff2ec89fcd] -> OLDPWD=/home/duck
envp 41 @ 0x7fff2ec88430 [0x7fff2ec89fe8] -> _=./envp
envp 42 @ 0x7fff2ec88438 [(nil)]

Tenga en cuenta dos cosas:

  • Las matrices de punteros para los argumentos y las variables de entorno son contiguas en la memoria. Al puntero NULL al final de la matriz de argumentos, que se muestra en la dirección de memoria 0x7fff2ec882e0 como [(nil)], le sigue inmediatamente el puntero a la primera cadena de entorno (GCONV_PATH) en 0x7fff2ec882e8. Los punteros tienen 8 bytes cada uno en Linux de 64 bits, y las listas de punteros argv y envp se ejecutan desde 0x7fff2ec882c8 hasta 0x7fff2ec88438 en pasos contiguos de 8 bytes cada vez. No hay memoria sin usar entre argc[] y envp[].
  • Tanto la lista argv como la lista envp están completamente bajo tu control. Puedes elegir los argumentos y establecer las variables de entorno que desees, incluida la adición de otras en la línea de comando para usar solo cuando se ejecuta este comando. Algunas variables de entorno, como LD_PRELOAD y LD_LIBRARY_PATH, se pueden usar para modificar el comportamiento del programa que se está ejecutando, incluida la carga silenciosa y automática de comandos adicionales o módulos ejecutables.

Ejecutemos el comando nuevamente como root, usando pkexec:

$ LD_LIBRARY_PATH=risky-variable GCONV_PATH=more-risk pkexec ./envp first second
argv  0 @ 0x7ffdf900fc98 [0x7ffdf9010eec] -> /home/duck/Articles/pwnkit/./envp
argv  1 @ 0x7ffdf900fca0 [0x7ffdf9010f0e] -> first
argv  2 @ 0x7ffdf900fca8 [0x7ffdf9010f14] -> second
argv  3 @ 0x7ffdf900fcb0 [(nil)]
envp  0 @ 0x7ffdf900fcb8 [0x7ffdf9010f1b] -> SHELL=/bin/bash
envp  1 @ 0x7ffdf900fcc0 [0x7ffdf9010f2b] -> LANG=en_US.UTF-8
envp  2 @ 0x7ffdf900fcc8 [0x7ffdf9010f3c] -> LC_COLLATE=C
envp  3 @ 0x7ffdf900fcd0 [0x7ffdf9010f49] -> TERM=rxvt-unicode-256color
envp  4 @ 0x7ffdf900fcd8 [0x7ffdf9010f64] -> COLORTERM=rxvt-xpm
envp  5 @ 0x7ffdf900fce0 [0x7ffdf9010f77] -> PATH=/usr/sbin:/usr/bin:/sbin:/bin:/root/bin
envp  6 @ 0x7ffdf900fce8 [0x7ffdf9010fa4] -> LOGNAME=root
envp  7 @ 0x7ffdf900fcf0 [0x7ffdf9010fb1] -> USER=root
envp  8 @ 0x7ffdf900fcf8 [0x7ffdf9010fbb] -> HOME=/root
envp  9 @ 0x7ffdf900fd00 [0x7ffdf9010fc6] -> PKEXEC_UID=1000
envp 10 @ 0x7ffdf900fd08 [(nil)]

Esta vez, se puede observar que:

  • El nombre del comando (argv[0]) se convirtió en un nombre de ruta completo. El programa pkexec hace esto desde el principio, para evitar ambigüedades cuando el programa se ejecuta con poderes de superusuario. Ten en cuenta que esta conversión ocurre antes de que el sistema Polkit subyacente intervenga para verificar si tiene permiso para ejecutar el programa elegido y, por lo tanto, antes de que aparezcan solicitudes de contraseña.
  • La lista de variables de entorno se ha recortado y ajustado por motivos de seguridad. En particular, el propio sistema operativo elimina automáticamente varias variables de entorno conocidas como malas de cualquier programa, como pkexec, que tiene el privilegio de promocionar otro software para que se ejecute como root. (En la jerga técnica, esto significa cualquier programa con el conjunto de bits setuid, de los cuales pkexec es un ejemplo).

Cuidado con el desbordamiento del búfer

Hasta ahora todo va bien.

Excepto que Qualys descubrió que si se lanza deliberadamente el programa pkexec de manera que el valor de su propio parámetro argv[0] (por convención, establecido en el nombre del programa en sí) se borra y se establece en NULL en el proceso de convertir el nombre del comando que se desea ejecutar (./envp arriba) en un nombre de ruta completo (/home/duck/Articles/pwnkit/./envp), el código de inicio de pkexec realizará un desbordamiento de búfer.

Por razones de seguridad, pkexec debería detectar que se inició de forma inusual sin ningún argumento en la línea de comandos, ni siquiera su propio nombre, y negarse a ejecutarse.

En cambio, pkexec mira a ciegas lo que cree que es argv[1] (generalmente, este sería el nombre del comando que le está pidiendo que ejecute como root) e intenta encontrar ese programa en su camino.

Pero si argv[0] ya era NULL, entonces no hay argumentos de línea de comando, y lo que pkexec cree que es argv[1] es en realidad envp[0], la primera variable de entorno, porque las matrices argv[] y envp[] son directamente adyacentes en la memoria.

Entonces, si se configura la primera variable de entorno para que sea el nombre de un programa que se puede encontrar en tu RUTA, y luego se ejecuta pkexec sin ningún argumento de comando, ni siquiera argv[0], entonces el programa combinará tu ruta con valor de la variable de entorno que erróneamente cree que es el nombre del programa que desea ejecutar y escribe esa versión “más segura” del “nombre de archivo” en lo que cree que es argv[1], listo para ejecutar el programa elegido a través de su nombre de ruta completo, en lugar de uno relativo.

Desafortunadamente, la cadena modificada escrita en argv[1] en realidad termina en envp[0], lo que significa que un usuario malintencionado podría, en teoría, explotar esta desalineación del búfer de argv a envp para reintroducir variables de entorno peligrosas que el propio sistema operativo ya se había tomado la molestia de borrar de la memoria.

Elevación completa de privilegios

Para acortar una larga historia, los investigadores de Qualys descubrieron una forma de usar una variable de entorno peligrosamente “reintroducida” de este tipo para engañar a pkexec para que ejecute un programa de su elección antes de que el programa llegara a verificar si la cuenta tenía derecho a usar pkexec.

Debido a que pkexec es un programa “setuid-root” (esto significa que cuando lo inicias, mágicamente se ejecuta como root en lugar de bajo tu propia cuenta), cualquier subprograma que pueda forzar a ejecutar heredará privilegios de superusuario.

Esto significa que cualquier usuario que ya tenga acceso a tu sistema, incluso si está conectado con una cuenta que casi no tiene ningún poder, podría, en teoría, usar pkexec para promocionarse instantáneamente a la ID de usuario 0: la cuenta root o superusuario,.

Los investigadores no proporcionaron un código funcional como prueba de concepto, aunque, como señalaron irónicamente:

No publicaremos nuestro exploit inmediatamente; sin embargo, ten en cuenta que esta vulnerabilidad se puede explotar de manera trivial, y otros investigadores podrían publicar sus vulnerabilidades poco después de que los parches estén disponibles.

¿Qué hacer?

Parchea rápido y parchea a menudo. Muchas, si no la mayoría, de las distribuciones de Linux ya deberían tener una actualización. Puedes ejecutar (de forma segura) pkexec –version para verificar la versión que tiene. Tendría que ser la 0.120 o posterior.

Si no puedes actualizar, considera modificar pkexec para quitarle sus privilegios. Si eliminas el bit setuid del archivo ejecutable pkexec, este error ya no será explotable, porque pkexec no se iniciará automáticamente con poderes de superusuario. Cualquiera que intente explotar el error simplemente terminaría con el mismo privilegio que ya tenía.

Encuentra y corrige pkexec: cómo utilizar las solución

Encontrar pkexec en tu ruta:

$ which pkexec
/usr/bin/pkexec   <---probable location on most distros

Comprobando la versión que tienes. Por debajo de 0.120, probablemente sea vulnerable, al menos en Linux:

$ /usr/bin/pkexec --version
pkexec version 0.120    <-- our distro already has the updated Polkit package

Comprobación de los bits de modo de archivo. Ten en cuenta que la letra s en la primera columna significa setuid y significa que cuando se ejecuta el archivo, se ejecutará automáticamente con el nombre de cuenta que figura en la columna tres como propietario del archivo; en este caso, eso significa root. En distribuciones compatibles con colores, es posible que vea el nombre del archivo resaltado con un fondo rojo brillante:

$ ls -l /usr/bin/pkexec
-rwsr-xr-x 1 root root 35544 2022-01-26 02:16 /usr/bin/pkexec*

Cambiando el bit setuid. Observa cómo, después de degradar el archivo “restando” la letra s de los bits de modo, la primera columna ya no contiene un marcador S de setuid. En una versióna color, el fondo rojo también desaparecerá:

$ sudo chmod -s /usr/bin/pkexec 
Password: ***************
$ ls -l /usr/bin/pkexec
-rwxr-xr-x 1 root root 35544 2022-01-26 02:16 /usr/bin/pkexec*   <-- setuid bit removed

Volviendo a activar  setuid. Si necesitas volver a habilitar los poderes de root de pkexec antes de obtener la última actualización, o si la actualización del paquete Polkit no restaura el bit setuid automáticamente, puedes usar el comando chmod +s … de manera similar a cómo se usó -s antes para “volver a agregar” la letra s a los bits de modo:

$ sudo chmod u+s /usr/bin/pkexec 
Password: ***************
$ ls -l /usr/bin/pkexec
-rwsr-xr-x 1 root root 35544 2022-01-26 02:16 /usr/bin/pkexec*   <-- setuid bit restored for file user