CVEs

Analizando y Explotando CVE’s – Parte I

Analizando y Explotando CVE's

Analizando y Explotando CVE’s es una serie de artículos sobre vulnerabilidades, ya sea con exploits conocidos o sin exploits existentes, en donde analizaré cada detalle acerca de la vulnerabilidad y cuáles son aquellos componentes que la rodean para así lograr su explotación.

Vulnerabilidad en NodeJs que permite elevar privilegios en Linux

Se trata de una vulnerabilidad en una de las funciones de NodeJs, la cual permite a un usuario no privilegiado aprovecharse de un bug a la hora de proteger el proceso “Nodejs” de “capacidades” extras cuando este está siendo ejecutado con altos privilegios.

Este bug se encuentra en las siguientes versiones de Nodejs:

  • >= 18.19.0
  • >= 21.6.1
  • >= 20.11.0

¿Qué se puede realizar con esto?
Logrando explotar esta vulnerabilidad y tomando control de las capacidades asignadas al proceso podemos elevar nuestros privilegios dentro del sistema Linux.

Este bug fue reportado por Tobias Nießen .

Investigando el bug

El primer vistazo que tengo es el ” Wednesday February 14 2024 Security Releases”, que publican en el sitio web de Nodejs, el cual dice:


Lo que dice aquí en resumidas cuentas es que Nodejs ignora algunas variables de entornos de usuarios no privilegiados cuando está siendo ejecutado con privilegios de administrador. Además contiene solo una excepción y es CAP_NET_BIND_SERVICE; debido a un bug dentro de esta excepción se ignora otras capacidades activadas.

CAP_NET_BIND_SERVICE
Es una capacidad de Linux que permite a un proceso a asociar un socket a un puerto de red privilegiado. O sea un puerto cuyo número sea menor de 1024.
Ejemplo: Cuando usas Nodejs y necesitas que tu servidor se exponga por el puerto 80 (HTTP) no lo podrías hacer con un usuario no privilegiado, en cambio si posees esta capacidad entonces tienes el privilegio para hacerlo.

Este bug nos abre las puertas para inyectar código y así hacernos con una terminal root… o al menos eso espero, veamos.

La información que expone la publicación dentro del ” Wednesday February 14 2024 Security Release” está muy condensada, por lo tanto necesitamos más.

Si nos vamos al Github de NodeJs, en específico al commit 10ecf40, del 14 de Febrero de 2024, podemos ver esto:

Aja! Tenemos más información acerca de este bug. Ahora vemos que se nos menciona una función llamada HasOnly() que recibe como argumento una capacidad; y esto no es todo, también nos muestra el código que fue cambiado, como es usual.

Aclaración: Lo rojo es el código viejo y lo verde el código nuevo.

La declaración de HasOnly() nos debe devolver un resultado booleano. ¿Entonces Podemos decir que está verificando si X capacidad está permitida? En la siguiente linea vemos DCHECK(cap_valid(capability)); verifica si la capacidad es válida mediante esta función cap_valid().

Capacidades
Antes de continuar, por si aún no sabes qué es una capacidad en Linux; digamos que tu eres un proceso dentro de Linux y quieres hacer varias cosas como cambiar los UID y los GUID, y además abrir un puerto como el 80, por ejemplo. Estas acciones las puede realizar solamente un proceso con privilegios altos – ¿Entonces debería correr este proceso como root? – no, ya que para ello existen las capacidades que se te pueden asignar o activar, como por ejemplo si quieres cambiar ciertos UID’s y GUID’s se te da la capacidad CAP_CHOWN (quizás te suene familiar gracias al comando chown para cambiar de propietarios a los archivos), y CAP_NET_BIND_SERVICE para utilizar puertos por debajo de 1024; todo esto sin la necesidad de que tu proceso sea ejecutado por el usuario root.
Recomiendo leer el siguiente man page: https://man7.org/linux/man-pages/man7/capabilities.7.html

Sabiendo para qué nos sirven las capacidades podemos continuar con el código. Veamos la primera estructura:

struct __user_cap_data_struct cap_data[2];

__user_cap_data_struct lo podemos ver por primera vez en <linux/capability.h>

  • effective: capacidades que el proceso puede utilizar actualmente.
  • permitted: capacidades que el proceso puede activar si lo necesita.
  • inheritable: capacidades que el proceso puede heredar de un proceso padre.
struct __user_cap_data_struct cap_data[2]; \\un array con dos elementos

Cuando se hace el “fix” de la vulnerabilidad, lo que hacen es modificar el código de arriba y cambiarlo por este que se ve abajo:

struct __user_cap_data_struct cap_data[_LINUX_CAPABILITY_U32S_3];

Podemos ver que cambian el 2 por LINUX_CAPABILITY_U32S_3, esto es una constante definida en <linux/capability.h>. Esta constante representa el número necesario para almacenar las capacidades definidas en cierta versión del sistemas operativo, y este numero es de 32 bits o _u32.

Cuando LINUX_CAPABILITY_U32S_3 es 2 representa 64 capacidades, ya que se requiere dos enteros de 32 bits para ello. Mientras que en el anterior __user_cap_data_struct cap_data[2] se fijaba manualmente dos enteros de 32 bits.

Dato: Al momento que se está escribiendo este artículo solo existen 40 capacidades.

Por ahora esta corrección solo parece ser buena práctica para evitar tener que cambiar manualmente cap_data[2] si en un futuro existen más capacidades.

Continuando con el Código

Ya vimos que ese primer cambio nos da algo de información pero no la suficiente como para saber dónde está la vulnerabilidad o cómo podemos aprovecharnos de ella. En el siguiente código:

El if verifica que las capacidades se encuentran en las primeras 32, comprendiendo así que cap_data[0] representa las capacidades almacenadas de 0 a 31, entonces se realiza la operación booleana en el “return” cap_data[0].permitted == static_cast<unsigned int>(CAP_TO_MASK(capability));.

  • Primero se toma la capacidad permitida que comprende cap_data[0] y ve si es igual a el valor pasado a “capability”.
  • Se utiliza CAP_TO_MASK, la cual es una macro previamente definida por la librería, para convertir esa capacidad, que le es pasada, en una máscara de bits.

Ejemplo de CAP_NET_BIND_SERVICE como máscara de bits obtenido con un pequeño programa en C que hice para obtener estos valores.

Se ve como solamente un bit está activado.

Relacionado con la función definida CAP_TO_MASK dentro de <linux\types.h>:

#define CAP_TO_MASK(x)   (1 << ((x) & 31))

Esta realiza una operación booleana "AND 31" para verificar que la capacidad esté entre un rango de 0 a 31, y luego hace un desplazamiento a la izquierda. Para saber un poco más sobre las operaciones bitwise left and right shift te recomiendo leer este artículo -> Aquí

Igualmente aquí te dejo el código para ver los bitmask con el PID de los binarios que ejecutes.

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/capability.h>
#include <linux/capability.h>


void print_capability_masks(cap_t caps) {
    cap_flag_value_t value;
    cap_flag_t flags[] = {CAP_EFFECTIVE, CAP_PERMITTED};
    const char *flag_names[] = {"Effective", "Permitted"};
    unsigned int num_caps = 0;

    
#ifdef _LINUX_CAPABILITY_VERSION_3
    num_caps = CAP_LAST_CAP + 1;
#else
    num_caps = CAP_LAST_CAP + 1;  
#endif

    for (int i = 0; i < 2; i++) {
        printf("%s capabilities mask:\n", flag_names[i]);
        printf("    ");
        for (int cap = 0; cap < num_caps; cap++) {
            if (cap_get_flag(caps, cap, flags[i], &value) == -1) {
                perror("cap_get_flag");
                cap_free(caps);
                exit(EXIT_FAILURE);
            }
            printf("%d", value); 
        }
        printf("\n");
    }
}

int main(int argc, char *argv[]) {
    pid_t pid;
    cap_t caps;

    if (argc == 1) {
        pid = getpid();
        printf("Using current process (PID: %d)\n", pid);
    } else {
        pid = atoi(argv[1]);
        if (pid <= 0) {
            fprintf(stderr, "Invalid PID: %s\n", argv[1]);
            exit(EXIT_FAILURE);
        }
        printf("Using specified process (PID: %d)\n", pid);
    }

    caps = cap_get_pid(pid);
    if (!caps) {
        perror("cap_get_pid");
        exit(EXIT_FAILURE);
    }


    print_capability_masks(caps);
    cap_free(caps);

    return 0;
}

Y por último, si no se cumple la condición anterior, el if con < 32, retorna esto:


Retorna el booleano de esta operación: si la capacidad dentro de la variable “capability” es igual a la permitida en un rango de 32 a 63.

¿Pero, y dónde está la vulnerabilidad?
Veamos nuevamente el fix, con detenimiento.

  • Con static_assert() y arraysize() verifica que solo haya dos elementos en ese array cap_data.
  • CAP_TO_INDEX obtiene en qué índice del arreglo se encuentra la capacidad pasada, ejemplo si es 3 (CAP_FOWNER) pertenece a cap_data[0] (0-31), y así mismo si la capacidad es 38, entonces pertenece a cap_data[1] (32-64).
  • Se hace la operación si la capacidad es permitida y que no se haya agregado más capacidades, además de las que ya están dentro de la excepción.

Si además de la capacidad permitida activamos otra, el código falla en detectar que existe otra capacidad activa, ya que solo compara la igualdad exacta: cap_data[0].permitted == static_cast<unsigned int>(CAP_TO_MASK(capability)); y cap_data[1].permitted == static_cast<unsigned int>(CAP_TO_MASK(capability));.
Por ello se coloca en el fix: && cap_data[1 - CAP_TO_INDEX(capability)].permitted == 0;.

Ya que sabemos todo esto, y dónde está la vulnerabilidad veamos cuando se utiliza esta función vulnerable para verificar el CAP_NET_BIND_SERVICE:

Aquí vemos como le pasan la capacidad CAP_NET_BIND_SERVICE, pues es la única que debe contener.

Intento de Explotación

Se supone que el binario (node) debe tener solamente la capacidad CAP_NET_BIND_SERVICE activada, pero gracias a la vulnerabilidad, anteriormente analizada no verifica otras capacidades activas, al menos las del rango cap_data[1]: 32 hacia adelante.

Aquí me enfrento a una pared enorme y comprendo el por qué este CVE no llega a ser calificado como crítico, ya que para que una explotación exitosa suceda se necesita que ciertas configuraciones oportunas estén.

Lo primero que debemos hacer para crear un entorno que nos beneficie a la hora de la explotación es:

  1. Agregar el setuid al binario.
  2. Activar las capacidades a utilizar, recordemos que las de 0 a 31 solo podemos tener cap_net_bind_services.

Y aquí tenemos otra pared que nos limita a la hora de explotar esta vulnerabilidad. Como mencionaba hace unos párrafos arriba: “Al momento que se está escribiendo este artículo solo existen 40 capacidades.” Sabiendo esto solo contamos con 9 capacidades.

Lo sé, hay varios obstáculos para poder realizar una explotación exitosa de esta vulnerabilidad en Linux y conseguir el anhelado root, o poder avanzar en nuestro proceso de post explotación.

Teniendo en cuenta lo antes planteado estas son las capacidades que podemos aprovechar:

CAP_SETFCAP,CAP_MAC_OVERRIDE,CAP_MAC_ADMIN,CAP_SYSLOG,CAP_WAKE_ALARM,CAP_BLOCK_SUSPEND,CAP_AUDIT_READ,CAP_PERFMON,CAP_BPF y CAP_CHECKPOINT_RESTORE     

Dependiendo de las capacidades se podría hacer ciertas acciones, por ejemplo como afectar el funcionamiento del módulo SELinux o AppArmour, interactuar con los syslogs, obtener información acerca del rendimiento del sistema, etc.

En caso de que nos encontremos con una de estas capacidades activadas y conocemos una forma de obtener información de valor o aprovecharse de los privilegios obtenidos podemos inyectar el código en una variable de entorno.

Una de las cosas que Tobias Nießen mencionaba sobre la inyección de código relacionada a esta vulnerabilidad, en su commit, era lo siguiente:

Not doing so creates a vulnerability that potentially allows unprivileged users to inject code into a privileged Node.js process through environment variables such as NODE_OPTIONS.

Indicando que puedo utilizar NODE_OPTIONS para inyectar el código al volverse a ejecutar Nodejs. Información sobre NODE_OPTIONS.

NODE_OPTIONS='--require /tmp/malicioso.js'

El archivo malicioso (malicioso.js) será ejecutado cada vez que el administrador o alguien más trate de usar Nodejs.

Conclusión

El proceso de la creación de un exploit para una vulnerabilidad existente depende mucho de la complejidad de la misma; como investigador uno debe conocer las tecnologías subyacentes a la misma vulnerabilidad para poder explotarlas, por ello este primer capitulo cumple con el propósito de mostrar el proceso de recolección de la información y exponernos a la compresión más profunda de temas cotidianos, como son los privilegios en Linux.

A pesar de que no se pudo realizar una explotación exitosa debido a la naturaleza de la vulnerabilidad se ofrece como un precedente el análisis del impacto de las capacidades en los procesos, por ejemplo un programa con capacidades desmedidas e innecesarias podría resultar en un grave problema para la seguridad del sistema.

Referencias

Hi, I’m c3rberu5

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *