Linux setuid and setgid Programs

By | 2020-12-13

Linux setuid and setgid programs allow a regular user to alter the user or group it runs as.

Below, I provide a program written in C to demonstrate the concepts. Don’t worry if you aren’t a programmer or have good grasp on C. I will walk you through how the program works.

I am assuming you are familiar with the Linux command line environment and can do basic things like list files and change directories. Following this guide would be easiest on a Linux desktop, but a machine that you can remotely access with SSH using PuTTY or OpenSSH on Windows 10 will suffice. You will need to be able to access the root user as well.

The Demo Program

Copy and paste the following into a file called setuid.c:

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>

int main(int argc, char *argv[])
{
   if(argc != 3)
      return 1;

   struct stat pre_stat, post_stat;

   uid_t uid = atoi(argv[1]);
   gid_t gid = atoi(argv[2]);
   
   int pre = creat("/tmp/tg-suid-demo-pre", 00777);
   stat("/tmp/tg-suid-demo-pre",  &pre_stat);
   unlink("/tmp/tg-suid-demo-pre");

   printf("Before setuid: euid=%d, uid=%d\n", geteuid(), getuid());
   printf("setuid return: %d\n", setuid(uid));
   printf(" After setuid: euid=%d, uid=%d\n\n", geteuid(), getuid());

   printf("Before setgid: egid=%d, gid=%d\n", getegid(), getgid());
   printf("setgid return: %d\n", setgid(gid));
   printf(" After setgid: egid=%d, gid=%d\n\n", getegid(), getgid());

   int post = creat("/tmp/tg-suid-demo-post", 00777);
   stat("/tmp/tg-suid-demo-post", &post_stat);
   unlink("/tmp/tg-suid-demo-post");

   printf(" File owner pre setting uid/gid: u=%d g=%d\n",
      pre_stat.st_uid, pre_stat.st_gid);

   printf("File owner post setting uid/gid: u=%d g=%d\n",
      post_stat.st_uid, post_stat.st_gid);

   return 0;

}

This program takes two arguments from the command line. The first is the user you want it to change to, the second is the group. You must use the numeric version. The program doesn’t work with the names of groups or users.

First, it creates a file, reads the file’s filesystem metadata, then immediately deletes it. The metadata it reads includes the owner and group of the file.

The next section of the program outputs the real and effective user ID the program is running as, then makes the setuid() system call, outputs the return status of setuid(), and finally outputs the ID values again.

The following three lines do the same thing, except with the GIDs and setgid() instead of UIDs and setuid().

Next, it creates another file, reads the metadata, and then deletes it.

Finally, the program outputs the owner and group of the files it created and deleted.

Compile The Program

Before compiling the demo program, you need to ensure you have a C compiler installed. I am assuming you are running a YUM or APT based distribution. If not, you will have to look up how to do this for your system. One of the commands below will ensure you have the gcc C compiler installed. Make sure to run them as root or with sudo.

Debian/Ubuntu:

# apt install gcc

CentOS/Fedora/RHEL/OL:

# yum install gcc

Once that is done, we are going to compile the program into an executable. Run the following command to do so:

$ gcc -o setuid setuid.c

Experiment

Now that we have the demo program built, we can run some experiments to get a better understanding of how setuid and setgid work. Open two terminal windows.

In one terminal, simply cd into the directory the program is in as a regular user. In the other obtain a root shell and then cd into the directory.

Before getting started, know that when setuid() or setgid are successfully able to modify the user or group of the process, they return 0. If they fail, they return -1.

When you are the root user, you can pretty much do what you want. Run the program as root a few times with normal permissions:

# ls -l setuid
-rwxr-xr-x 1 root root 17184 Dec 11 12:10 setuid
# ./setuid 0 1
Before setuid: euid=0, uid=0
setuid return: 0
 After setuid: euid=0, uid=0

Before setgid: egid=0, gid=0
getuid return: 0
 After setgid: egid=1, gid=1

 File owner pre setting uid/gid: u=0 g=0
File owner post setting uid/gid: u=0 g=1
# ./setuid 3 0
Before setuid: euid=0, uid=0
setuid return: 0
 After setuid: euid=3, uid=3

Before setgid: egid=0, gid=0
getuid return: 0
 After setgid: egid=0, gid=0

 File owner pre setting uid/gid: u=0 g=0
File owner post setting uid/gid: u=3 g=0
# ./setuid 3 1
Before setuid: euid=0, uid=0
setuid return: 0
 After setuid: euid=3, uid=3

Before setgid: egid=0, gid=0
getuid return: -1
 After setgid: egid=0, gid=0

 File owner pre setting uid/gid: u=0 g=0
File owner post setting uid/gid: u=3 g=0

Notice how the file does not have the special permissions on it, yet the process was usually able to successfully set the user or group ID? Also take note that files are owned by the user and group the process is running as at the time of creation.

The last run in the example above failed to set the group ID. This is because it had previously called setuid(3) and was running with a UID of 3. Since the process is unprivileged at this point, the call failed.

Try running the program as a regular user:

$ ls -l setuid
-rwxr-xr-x 1 root root 17184 Dec 11 12:10 setuid
$ ./setuid 0 0
Before setuid: euid=1000, uid=1000
setuid return: -1
 After setuid: euid=1000, uid=1000

Before setgid: egid=1000, gid=1000
getuid return: -1
 After setgid: egid=1000, gid=1000

 File owner pre setting uid/gid: u=1000 g=1000
File owner post setting uid/gid: u=1000 g=1000
$ ./setuid 1 1
Before setuid: euid=1000, uid=1000
setuid return: -1
 After setuid: euid=1000, uid=1000

Before setgid: egid=1000, gid=1000
getuid return: -1
 After setgid: egid=1000, gid=1000

 File owner pre setting uid/gid: u=1000 g=1000
File owner post setting uid/gid: u=1000 g=1000
$ ./setuid 1000 1000
Before setuid: euid=1000, uid=1000
setuid return: 0
 After setuid: euid=1000, uid=1000

Before setgid: egid=1000, gid=1000
getuid return: 0
 After setgid: egid=1000, gid=1000

 File owner pre setting uid/gid: u=1000 g=1000
File owner post setting uid/gid: u=1000 g=1000

Notice how, as a regular user, I was unable to change the user or group the process runs as? The setuid() and setgid() calls failed all attempts but the last. Even the calls succeeded the last time, it doesn’t really matter since it didn’t actually change anything.

Now let’s add the setgid permission to the executable and run it a few more times. We aren’t going to bother running the program as root since we already know what is going to happen. Make sure you run the following command as root to add the setgid permission to the executable file:

# chmod g+s setuid
# ls -l setuid
-rwxr-sr-x 1 root root 17184 Dec 11 12:10 setuid

Now run the program a few times as a regular user and observe the results:

$ ./setuid 1000 1
Before setuid: euid=1000, uid=1000
setuid return: 0
 After setuid: euid=1000, uid=1000

Before setgid: egid=0, gid=1000
getuid return: -1
 After setgid: egid=0, gid=1000

 File owner pre setting uid/gid: u=1000 g=0
File owner post setting uid/gid: u=1000 g=0
$ ./setuid 1000 1000
Before setuid: euid=1000, uid=1000
setuid return: 0
 After setuid: euid=1000, uid=1000

Before setgid: egid=0, gid=1000
getuid return: 0
 After setgid: egid=1000, gid=1000

 File owner pre setting uid/gid: u=1000 g=0
File owner post setting uid/gid: u=1000 g=1000

The interesting thing to note is that the process always runs as group 0 except when I set it to my own GID. If you were to set the group owner on the program to a different group and then re-add the setgid permission as shown below, you would get the same results as above except with the new group. Try it if you like and observe the results:

# chown 0:1 setuid
# chmod g+s setuid
# ls -l setuid
-rwxr-sr-x 1 root daemon 17184 Dec 11 12:10 setuid

Now I set the user and group ownership to root and set the setuid permission:

# chown 0:0 setuid
# chmod u+s setuid
# ls -l setuid
-rwsr-xr-x 1 root root 17184 Dec 11 12:10 setuid

Let’s run a few more experiments:

$ ./setuid 0 0
Before setuid: euid=0, uid=1000
setuid return: 0
 After setuid: euid=0, uid=0

Before setgid: egid=1000, gid=1000
getuid return: 0
 After setgid: egid=0, gid=0

 File owner pre setting uid/gid: u=0 g=1000
File owner post setting uid/gid: u=0 g=0
$ ./setuid 0 1
Before setuid: euid=0, uid=1000
setuid return: 0
 After setuid: euid=0, uid=0

Before setgid: egid=1000, gid=1000
getuid return: 0
 After setgid: egid=1, gid=1

 File owner pre setting uid/gid: u=0 g=1000
File owner post setting uid/gid: u=0 g=1
$ ./setuid 1 1
Before setuid: euid=0, uid=1000
setuid return: 0
 After setuid: euid=1, uid=1

Before setgid: egid=1000, gid=1000
getuid return: -1
 After setgid: egid=1000, gid=1000

 File owner pre setting uid/gid: u=0 g=1000
File owner post setting uid/gid: u=1 g=1000
$ ./setuid 1 0
Before setuid: euid=0, uid=1000
setuid return: 0
 After setuid: euid=1, uid=1

Before setgid: egid=1000, gid=1000
getuid return: -1
 After setgid: egid=1000, gid=1000

 File owner pre setting uid/gid: u=0 g=1000
File owner post setting uid/gid: u=1 g=1000

With the setuid permission set and the program owned by root, the process can run as whatever user and group you want. If you are developing a program, just make sure you set the group first or you won’t have permission to change the group.

The last scenario I am going to cover is when the program has the setuid permission set and is owned by a non-root user.

# chown 1 setuid
# chmod u+s setuid
# ls -l setuid
-rwsr-xr-x 1 daemon root 17184 Dec 11 12:10 setuid

Now let’s run it a few times with different input and see what happens:

$ ./setuid 1 1000
Before setuid: euid=1, uid=1000
setuid return: 0
 After setuid: euid=1, uid=1000

Before setgid: egid=1000, gid=1000
getuid return: 0
 After setgid: egid=1000, gid=1000

 File owner pre setting uid/gid: u=1 g=1000
File owner post setting uid/gid: u=1 g=1000
$ ./setuid 0 0
Before setuid: euid=1, uid=1000
setuid return: -1
 After setuid: euid=1, uid=1000

Before setgid: egid=1000, gid=1000
getuid return: -1
 After setgid: egid=1000, gid=1000

 File owner pre setting uid/gid: u=1 g=1000
File owner post setting uid/gid: u=1 g=1000
$ ./setuid 3 0
Before setuid: euid=1, uid=1000
setuid return: -1
 After setuid: euid=1, uid=1000

Before setgid: egid=1000, gid=1000
getuid return: -1
 After setgid: egid=1000, gid=1000

 File owner pre setting uid/gid: u=1 g=1000
File owner post setting uid/gid: u=1 g=1000
$ ./setuid 1000 0
Before setuid: euid=1, uid=1000
setuid return: 0
 After setuid: euid=1000, uid=1000

Before setgid: egid=1000, gid=1000
getuid return: -1
 After setgid: egid=1000, gid=1000

 File owner pre setting uid/gid: u=1 g=1000
File owner post setting uid/gid: u=1000 g=1000

When the program is owned by a user other than root, the only thing you can do is run it as the user it is owned by.

Summary

The setuid and setgid permissions make programs such as passwd function. Since /etc/shadow is writable only by root, these permissions allow regular users who run the program the ability to change their password. There are other real world examples. The sudo program is a great example. It relies on setuid to run commands as other users.

For some further exploration, I recommend replacing the setgid() and setuid() calls with setegid() and seteuid(), respectively.

References

.

See Also