9.8 KiB
euid, ruid, suid
☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥
- ¿Trabajas en una empresa de ciberseguridad? ¿Quieres ver tu empresa anunciada en HackTricks? ¿O quieres tener acceso a la última versión de PEASS o descargar HackTricks en PDF? ¡Consulta los PLANES DE SUSCRIPCIÓN!
- Descubre The PEASS Family, nuestra colección de exclusivos NFTs
- Obtén el oficial PEASS & HackTricks swag
- Únete al 💬 grupo de Discord o al grupo de telegram o sígueme en Twitter 🐦@carlospolopm.
- Comparte tus trucos de hacking enviando PR al repositorio de hacktricks y al repositorio de hacktricks-cloud.
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, eleuid
será el mismo que elruid
, pero un binario SetUID es un ejemplo de un caso en el que difieren. Cuando se inicia un binario SetUID, eleuid
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
.