escalatedquickly

Hacker || Malware Enthusiast || Nerd

Clarifying Linux privilege escalation with SUID programs

2022-08-14
2098 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

Boom Shakalaka Gif

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:

    execl("/bin/sh", "sh", "-c", command, (char *) NULL);

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

That’s a bingo GIF

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:

  1. The SUID bit tells the OS that an executable should be ran as its owner, not the user running the file
  2. 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.
  3. For non-privileged users, only the UID of the executable’s owner is valid. For privileged users, any UID is valid.
  4. To be able to use this to gain root privileges, the executable must therefore be owned by root.
  5. 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!