Una comparación entre el ID real de usuario y el ID efectivo de usuario no es suficiente para prevenir la escalación de privilegios.

·6 min de lectura

Introducción

Los bugs sutiles que resultan en vulnerabilidades de seguridad siempre tocan nuestras fibras más sensibles. Estos evocan las principales razones por las cuales muchos de nosotros nos sentimos atraídos a la ciberseguridad en primer lugar. Es esa sensación emocionante de pensar más allá de lo ordinario, de profundizar en esos detalles que son pasados por alto por la mayoría. El encanto reside en entender y sentir las complejidades, los detalles, las relaciones, que nos recuerdan la profundidad y humanidad que yace en cada línea de código; sin embargo, también funcionan como humildes recordatorios de nuestra falibilidad – que las cosas aún pueden salir mal sin importar que tan cuidadosos seamos.

En este artículo discutiré un patrón de errores que he detectado en múltiples proyectos recientemente, el cual no es realmente nuevo. Los recientes avisos de vulnerabilidades y CVE’s deberían ser publicados pronto. Dada su sutileza, se ha mantenido oculto, acechando en las sombras por mucho tiempo. Como profesionales de la seguridad, debemos arrojar luz a estos errores a los que nadie ve. Solo a través de documentación diligente y compartiendo nuestros conocimientos podemos esperar erradicarlos.

El problema de solo comparar el ID real de usuario y el ID efectivo de usuario como control de seguridad

En sistemas operativos UNIX-like cada proceso tiene un ID real de usuario y un ID efectivo de usuario. El sistema uso estos ID’s para determinar los permisos del proceso para acceder a determinados recursos. En la mayoría de los casos, el ID real de usuario y el ID efectivo de usuario de un proceso son el mismo, pero estos pueden diferir en ciertas situaciones, como cuando el bit setuid es asignado en archivos ejecutables, como se muestra en el siguiente ejemplo:

%cat getuid_ejemplo1.c

#include <stdio.h>

#include <unistd.h>

 

int main() {

   printf("ID real de usuario: %d\n", getuid());

   printf("ID efectivo de usuario: %d\n", geteuid());

 

   return 0;

}

% gcc getuid_ejemplo1.c -o getuid_ejemplo1

% ./getuid_ejemplo1

ID real de usuario: 501
ID efectivo de usuario: 501

% sudo chown root getuid_ejemplo1

% sudo chmod u+s getuid_ejemplo1

% ./getuid_ejemplo1

ID real de usuario: 501

ID efectivo de usuario: 0

Aplicaciones que realizan operaciones críticas, como cargar librerías dinámicas o ejecutar código bajo el ID efectivo de usuario actual, pueden usar la función C getuid() y geteuid() para obtener los valores del ID de usuario correspondientes y usarlos como parte de los controles de seguridad en las mismas. Comparar los valores que retornan estas funciones es una técnica común entre los desarrolladores para detectar aplicaciones que se estén ejecutando con el bit setuid, y a su vez prevenir un posible bug de escalación de privilegios. Veamos un ejemplo de estas funciones usadas como control de seguridad.

%cat libfoo.c

#include <stdio.h>

 

void foo() {

   printf("Librería maliciosa cargada.\n);

}

%gcc-shared-fPIClibfoo.c-olibfoo.so

% cat getuid_ejemplo2.c

#include <stdio.h>

#include <dlfcn.h>

#include <unistd.h>

 

int main() {

    printf("ID real de usuario: %d\n", getuid());

    printf("ID efectivo de usuario: %d\n", geteuid());

 

    if (geteuid() != getuid()) {

        printf("Setuid apps shall not pass!\n");

        return 1;

    }

 

    void *handle = dlopen("", RTLD_LAZY);

    if (!handle) {

        fprintf(stderr, "%s\n", dlerror());

        return 1;

    }

 

    void (*foo)() = dlsym(handle, "foo");

    if (foo) {

        foo();

    } else {

        fprintf(stderr, "No se pudo localizar la funcion.\n");

    }

 

    dlclose(handle);

    return 0;

}

% gcc getuid_ejemplo2.c -ldl -o getuid_ejemplo2

% ./getuid_ejemplo2

ID real de usuario: 501

ID efectivo de usuario: 501

Librería maliciosa cargada.

% sudo chown root getuid_ejemplo2

% sudo chmod u+s getuid_ejemplo2

% ./getuid_ejemplo2

ID real de usuario: 501

ID efectivo de usuario: 0

Setuid apps shall not pass! **????**‍??

Lo inadvertido

Aunque este control de seguridad parece robusto, hay una situación crítica que los desarrolladores deberían de tener en cuenta si solo dependen de la comparación de getuid() con geteuid(). Una aplicación también puede correr con permisos de otro grupo si el bit setgid está en juego. Vamos a reescribir el ejemplo previo para ver las implicaciones más claramente.

% cat getuid_ejemplo3.c

#include <stdio.h>

#include <dlfcn.h>

#include <unistd.h>

 

int main() {

    if (geteuid() == getuid()) {

        printf("Setuid no corriendo, podemos continuar de forma segura (cierto?).\n");

 

          printf("ID real de usuario: %d\n", getuid());

          printf("ID efectivo de usuario: %d\n", geteuid());

          printf("ID de grupo: %d\n", getgid());

          printf("ID efectivo de grupo: %d\n", getegid());

          void *handle = dlopen("", RTLD_LAZY);

          if (!handle) {

              fprintf(stderr, "%s\n", dlerror());

              return 1;

}

 

void (*foo)() = dlsym(handle, "foo");

if (foo) {

    foo();

} else {

    fprintf(stderr, "No se pudo localizar la funcion.\n");

}

 

            dlclose(handle);

}

    return 0;

}

% gcc getuid_ejemplo3.c -ldl -o getuid_ejemplo3

% ./getuid_ejemplo3

Setuid no corriendo, podemos continuar de forma segura (cierto?).

ID real de usuario: 501

ID efectivo de usuario: 501

ID de grupo: 20

ID efectivo de grupo: 20

Librería maliciosa cargada

El ID real de usuario versus el ID efectivo de usuario es suficiente para prevenir aplicaciones setuid, pero ¿qué hay de las aplicaciones con setgid? Supongamos que los desarrolladores no revisan este bit. En ese caso, haciendo la comparación entre getuid() y geteuid() como control de seguridad no va a prevenir que usuarios maliciosos accedan una sección crítica de código y obtengan ejecución de código con los permisos de grupo establecidos en un ejecutable determinado.

% sudo chgrp test getuid_ejemplo3

% sudo chmod g+s getuid_ejemplo3

% ./getuid_ejemplo3

Setuid no corriendo, podemos continuar de forma segura (cierto?).

ID real de usuario: 501

ID efectivo de usuario: 501

ID de grupo: 20

ID efectivo de grupo: 888

Librería maliciosa cargada.

Abusando este bug

Echemos un vistazo al CVE-2019-19520. Atacantes podrían elevar privilegios al grupo “auth” en OpenBSD a través de la utilidad xlock instalada por defecto. In este caso, la aplicación lee una ruta de variable de entorno no sanitizada que cargó un driver mediante dlopen(). Los atacantes simplemente necesitaron apuntar la variable de entorno a un driver malicioso que sería ejecutado:

env -i LIBGL_DRIVERS_PATH=. /usr/X11R6/bin/xlock -display :66

Y este es solo un escenario. Como con otros bugs sutiles, la lógica de las aplicaciones vulnerables difiere en cada aplicación, así que el método de explotación y su impacto también tendrán variaciones.

La solución

La manera correcta de prevenir estas posibles situaciones de escalación de privilegios se hace revisando y comparando el ID real de grupo y el ID efectivo de grupo con getgid() y getegid():

if (geteuid() == getuid() && getgid() == getegid())

Y por supuesto, nunca confiar en entradas de usuario que puedan ser usadas para cargar código externo :)

Escribí un query de CodeQL si quieres detectar este problema y otras variantes en tus propias aplicaciones de C/C++. La versión más reciente y otros querys se encuentran en mi repositorio. Los clientes de Checkmarx también pueden detectar esta vulnerabilidad en sus aplicaciones al agregar la regla Checkmarx correspondiente en sus bases de datos.

Conclusiones

En conclusión, entender la diferencia entre el ID real de usuario (UID), ID efectivo de usuario (EUID), ID real de grupo (GID), y el ID efectivo de grupo (EGID) es esencial para los desarrolladores de software ya que juega un papel fundamental en sistemas operativos UNIX-like para asegurar el correcto acceso a los recursos. Reconociendo que una mera comparación de getuid() y geteuid() es insuficiente para prevenir amenazas de seguridad.

Aunque este es un bug sutil, su impacto varía dependiendo de los permisos y operaciones realizadas por la aplicación. Esto resalta la importancia de emplear medidas robustas de seguridad, como manejo adecuado de permisos, usar capacidades de Linux, e implementar controles de acceso obligatorios. Además, subraya la necesidad de la correcta configuración de permisos y de evitar correr programas con privilegios elevados a menos que sea necesario. Podemos crear aplicaciones más seguras a través de la vigilancia y entendimiento de estos mecanismos; y por supuesto, al continuar compartiendo estos singulares, sutiles, y vulnerables patrones de código con la comunidad.

Referencias adicionales

Autor

Paulino Calderón calderon@websec.mx