hacktricks/linux-hardening/privilege-escalation/euid-ruid-suid.md
carlospolop 466ebcbb16 f
2023-06-05 20:30:03 +02:00

9.8 KiB

euid, ruid, suid

☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥

Este post fue copiado de https://0xdf.gitlab.io/2022/05/31/setuid-rabbithole.html#testing-on-jail

*uid

  • ruid: Este es el ID de usuario real del usuario que inició el proceso.
  • euid: Este es el ID de usuario efectivo, es lo que el sistema mira cuando decide qué privilegios debe tener el proceso. En la mayoría de los casos, el euid será el mismo que el ruid, pero un binario SetUID es un ejemplo de un caso en el que difieren. Cuando se inicia un binario SetUID, el euid se establece en el propietario del archivo, lo que permite que estos binarios funcionen.
  • suid: Este es el ID de usuario guardado, se utiliza cuando un proceso privilegiado (en la mayoría de los casos que se ejecuta como root) necesita abandonar los privilegios para realizar algún comportamiento, pero luego necesita volver al estado privilegiado.

{% hint style="info" %} Si un proceso que no es root quiere cambiar su euid, solo puede establecerlo en los valores actuales de ruid, euid o suid. {% endhint %}

set*uid

A primera vista, es fácil pensar que las llamadas al sistema setuid establecerían el ruid. De hecho, cuando se trata de un proceso privilegiado, lo hace. Pero en el caso general, en realidad establece el euid. Según la página del manual:

setuid() establece el ID de usuario efectivo del proceso que llama. Si el proceso que llama tiene privilegios (más precisamente: si el proceso tiene la capacidad CAP_SETUID en su espacio de nombres de usuario), también se establecen el UID real y el ID de usuario guardado.

Por lo tanto, en el caso en el que se está ejecutando setuid(0) como root, esto establece todos los IDs en root y básicamente los bloquea (porque suid es 0, pierde el conocimiento o cualquier usuario anterior - por supuesto, los procesos root pueden cambiar a cualquier usuario que deseen).

Dos llamadas al sistema menos comunes, setreuid (re para real y efectivo) y setresuid (res incluye guardado) establecen los IDs específicos. Estar en un proceso no privilegiado limita estas llamadas (de la página del manual para setresuid, aunque la página para setreuid tiene un lenguaje similar):

Un proceso no privilegiado puede cambiar su UID real, UID efectivo e ID de usuario guardado, cada uno a uno de: el UID real actual, el UID efectivo actual o el ID de usuario guardado actual.

Un proceso privilegiado (en Linux, uno que tiene la capacidad CAP_SETUID) puede establecer su UID real, UID efectivo e ID de usuario guardado en valores arbitrarios.

Es importante recordar que estos no están aquí como una característica de seguridad, sino que reflejan el flujo de trabajo previsto. Cuando un programa quiere cambiar a otro usuario, cambia el ID de usuario efectivo para que pueda actuar como ese usuario.

Como atacante, es fácil adquirir el mal hábito de simplemente llamar a setuid porque el caso más común es ir a root, y en ese caso, setuid es efectivamente lo mismo que setresuid.

Ejecución

execve (y otros execs)

La llamada al sistema execve ejecuta un programa especificado en el primer argumento. El segundo y tercer argumento son matrices, los argumentos (argv) y el entorno (envp). Hay varios otros llamados al sistema que se basan en execve, denominados exec (página del manual). Cada uno es solo un envoltorio sobre execve para proporcionar diferentes abreviaturas para llamar a execve.

Hay muchos detalles en la página del manual, sobre cómo funciona. En resumen, cuando execve inicia un programa, utiliza el mismo espacio de memoria que el programa que llama, reemplazando ese programa

#define _GNU_SOURCE
#include <stdlib.h>
#include <unistd.h>

int main(void) {
    setuid(1000);
    system("id");
    return 0;
}

Este programa está compilado y configurado como SetUID en Jail sobre NFS:

oxdf@hacky$ gcc a.c -o /mnt/nfsshare/a;
...[snip]...
oxdf@hacky$ chmod 4755 /mnt/nfsshare/a

Como root, puedo ver este archivo:

[root@localhost nfsshare]# ls -l a 
-rwsr-xr-x. 1 frank frank 16736 May 30 04:58 a

Cuando ejecuto esto como nobody, id se ejecuta como nobody:

bash-4.2$ $ ./a
uid=99(nobody) gid=99(nobody) groups=99(nobody) context=system_u:system_r:unconfined_service_t:s0

El programa comienza con un ruid de 99 (nadie) y un euid de 1000 (frank). Cuando llega a la llamada setuid, se establecen esos mismos valores.

Luego se llama a system, y esperaría ver un uid de 99, pero también un euid de 1000. ¿Por qué no hay uno? El problema es que sh está vinculado simbólicamente a bash en esta distribución:

$ ls -l /bin/sh
lrwxrwxrwx. 1 root root 4 Jun 25  2017 /bin/sh -> bash

Entonces, system llama a /bin/sh sh -c id, que es efectivamente /bin/bash bash -c id. Cuando se llama a bash, sin -p, entonces ve ruid de 99 y euid de 1000, y establece euid en 99.

setreuid / system

Para probar esa teoría, intentaré reemplazar setuid con setreuid:

#define _GNU_SOURCE
#include <stdlib.h>
#include <unistd.h>

int main(void) {
    setreuid(1000, 1000);
    system("id");
    return 0;
}

Compilación y permisos:

oxdf@hacky$ gcc b.c -o /mnt/nfsshare/b; chmod 4755 /mnt/nfsshare/b

Ahora en la cárcel, ahora id devuelve el uid de 1000:

bash-4.2$ $ ./b
uid=1000(frank) gid=99(nobody) groups=99(nobody) context=system_u:system_r:unconfined_service_t:s0

La llamada setreuid establece tanto ruid como euid en 1000, por lo que cuando system llama a bash, coinciden y las cosas continúan como frank.

setuid / execve

Llamando a execve, si mi comprensión anterior es correcta, también podría no preocuparme por manipular los uids y, en su lugar, llamar a execve, ya que llevará a cabo los IDs existentes. Eso funcionará, pero hay trampas. Por ejemplo, el código común podría verse así:

#define _GNU_SOURCE
#include <stdlib.h>
#include <unistd.h>

int main(void) {
    setuid(1000);
    execve("/usr/bin/id", NULL, NULL);
    return 0;
}

Sin el entorno (estoy pasando NULL para simplificar), necesitaré una ruta completa en id. Esto funciona, devolviendo lo que espero:

bash-4.2$ $ ./c
uid=99(nobody) gid=99(nobody) euid=1000(frank) groups=99(nobody) context=system_u:system_r:unconfined_service_t:s0

El [r]uid es 99, pero el euid es 1000.

Si intento obtener una shell a partir de esto, debo tener cuidado. Por ejemplo, simplemente llamando a bash:

#define _GNU_SOURCE
#include <stdlib.h>
#include <unistd.h>

int main(void) {
    setuid(1000);
    execve("/bin/bash", NULL, NULL);
    return 0;
}

Voy a compilar eso y establecerlo como SetUID:

oxdf@hacky$ gcc d.c -o /mnt/nfsshare/d
oxdf@hacky$ chmod 4755 /mnt/nfsshare/d

Aún así, esto devolverá todo nobody:

bash-4.2$ $ ./d
bash-4.2$ $ id
uid=99(nobody) gid=99(nobody) groups=99(nobody) context=system_u:system_r:unconfined_service_t:s0

Si fuera setuid(0), entonces funcionaría bien (suponiendo que el proceso tuviera permiso para hacerlo), ya que entonces cambia los tres ids a 0. Pero como usuario no root, esto solo establece el euid en 1000 (que ya estaba), y luego llama a sh. Pero sh es bash en Jail. Y cuando bash se inicia con ruid de 99 y euid de 1000, volverá a dejar caer el euid a 99.

Para solucionar esto, llamaré a bash -p:

#define _GNU_SOURCE
#include <stdlib.h>
#include <unistd.h>

int main(void) {
    char *const paramList[10] = {"/bin/bash", "-p", NULL};
    setuid(1000);
    execve(paramList[0], paramList, NULL);
    return 0;
}

Esta vez el euid está presente:

bash-4.2$ $ ./e
bash-4.2$ $ id
uid=99(nobody) gid=99(nobody) euid=1000(frank) groups=99(nobody) context=system_u:system_r:unconfined_service_t:s0

O también podría llamar a setreuid o setresuid en lugar de setuid.