Privilege Escalation via File Descriptors in Privileged Binaries

Privilege Escalation via File Descriptors in Privileged Binaries
Travis Phillips
Author: Travis Phillips
Share:

    Today I wanted to cover an application security topic that applies to SetUID binaries.  As we all know, making a mistake in a SetUID binary will lead to privilege escalation. Today’s topic is about SetUID binaries that drop privileges, but leave a file they opened, well, opened.  This creates an exploitable condition on Linux systems that is known about, but rarely seen.

 

    Nevertheless, it’s not a bad idea to review these from time to time to ensure history doesn’t repeat itself.  At the bottom of this blog, there are links to research material I stumbled upon that I found useful when learning this myself.  I have also released a tool to make exploiting this issue even easier on the Professional Evil GitHub repository, if the goal is to exploit this to edit a file!  I hope it helps!

 

Overview of the Issue

    On a Linux System, when a process creates a child process, the child process will inherit the file descriptors of the parent process.  If you’ve ever built a server process that forks, this is why the parent will close the client socket, and the child process works with the client socket.  On the surface this doesn’t seem like a big deal.  However, the other part of this issue occurs when you factor in how the kernel handles file permissions.  On Linux, the file permission check only occurs when the file is being opened.  Once it is opened, subsequent reads and writes do not check permissions.

 

    Due to the way the permission check occurs and the way child processes inherit file descriptors, it is possible for a privileged process to open a file, never close it, then drop privileges and still have the file open, which any child processes would inherit.  Since reads and writes do not require permissions checks, the limited user now has access to the file.

 

Our Example Setup

    For this example, I created a Kali Linux VM and created a user called limited.  The limited user has a UID of 1001 (automatically chosen by the system) and doesn’t have sudo privileges.

 

Adding our limited user to the system

 

    I also created a SetUID binary that would open the /etc/sudoers file, then drop the privileges down of the user to limited user and start a shell.  In our example, we will switch to the limited user, run the SetUID binary, and attempt to exploit the file descriptor left behind by the SetUID binary, which didn’t close the file.

 

Reviewing the SetUID Binary

The code for that binary is as follows:

#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>

const uid_t limit_uid = 1001;

int main(){
   int fd;

   // Open /etc/sudoers
   fd = open("/etc/sudoers", O_RDWR);

   // Pretend we do stuff with that file and forget to close it...

   // Dropping privileges to a limited user
   setgid(limit_uid);
   setegid(limit_uid);
   setregid(limit_uid, limit_uid);
   setuid(limit_uid);
   seteuid(limit_uid);
   setreuid(limit_uid, limit_uid);

   // Opening a shell limited shell.
   system("/bin/sh");

   return 0;
}

 

    Which we can compile as the file, dangling_fd, and then use chown and chmod to set the owner to root and mark the SetUID sticky bit, which will cause the binary to run as the binary owner.  Basically, anyone who runs this file will be running it as root.

 

Compiling our example SetUID binary and setting as owned by root with SetUID sticky bit

 

Reviewing the Shell as the limited User

    Now that we have the SetUID binary, we will login as limited and start the binary.  This will put us in a shell where we will run the id command to verify our UID and GID.  After that we will run sudo -l to see we have no sudo privileges.

 

Example run of the SetUID binary showing the limited user shell and lack of sudo permissions.

 

    Now let’s determine our PID and ownership using the htop with the tree view.  You could also use ps aux and reference the PPID’s to determine the ownership, but I think this makes a nicer view.

 

Process tree showing the child tree from our SetUID binary to our child shell process

 

    So the bash shell we are running in has the PID of 2194 and is a child process of the dangling_fd process, which is now running as limited.  Review the proc filesystem to see what file descriptors our process has by running ls -l /proc/2194/fd.

 

A look at the /proc filesystem of our shell.  We can see file descriptor 3 is open to /etc/sudoers

 

    We can also check this using ls -l /proc/self/fd, and while we get similar results and still see this, we do have to remember that /proc/self would be for the ls command itself, which is a child of our shell.

 

reviewing the /proc/self/ filesystem via the ls -l command

 

Attempting to Read the File

    We have an open file descriptor to the /etc/sudoers file open as file descriptor 3, which points to /etc/sudoers.  With that said, there are several ways to attempt to read it, and a lot of these will trigger the permission check.  For example, we can’t cat the file directly, since it would attempt to open the file again, we also can’t use the /proc/self/fd/3 since that needs to be opened as well.

 

trying to cat /etc/sudoers or /proc/self/fd/3 gives permission denied errors.

 

    We can however, use the file descriptor 3 to read the contents via shell redirection by running cat <&3.

 

Using shell redirection with cat appears to work to read the file the first time

 

    Awesome! But this only works because we opened the file in our SetUID binary and did nothing with it.  If we try that same cat <&3 command, we will now get nothing back.

 

The same trick doesn't work a second time since we already read to the end of the file.

 

    Why is that? This happens because the file descriptor has a file cursor which keeps track of where you are in the file, since we read all the way to the end of the file the first time, we have moved that cursor to the end of the file, and attempting to run cat while in this state results in getting an EOF immediately.  I showed you this because in the real world, this would likely be the state of things as the binary would have read the file it opened.

 

    So how can we fix the cursor to point back to the beginning of the file?  Well, you will need to invoke lseek(), which isn’t really in the shell, so you need some C code here to make a program to do this for you.  The lseek() is defined as such:

 

off_t lseek(int fd, off_t offset, int whence);

 

    The fd parameter would be the file descriptor you want to see against.  The offset is the offset you want to change by, which relates to the parameter whence.  The whence parameter would be one of the following constants:

 

  • SEEK_SET:
    • The file offset is set to offset bytes.
  • SEEK_CUR:
    • The file offset is set to its current location plus offset bytes.
  • SEEK_END:
    • The file offset is set to the size of the file plus offset bytes.

 

    With that information, we are looking to execute a C call like the following to reset the file offset back to the beginning of the file:

 

lseek(3, 0, SEEK_SET);

 

    The link provided to Portcullis Labs provided three C code files that were small and simple to read, write, and lseek a file descriptor.  These will get the job done but can be a little clunky to use if the goal is to edit the existing file.

Exploiting the Issue with our Tool

    I have created a single tool that is designed to be an all-in-one to emulate editing the file that can be found in the Professionally Evil GitHub repository. It will do the following:

 

  • Enumerate the /proc/self/fd file system and gather a list of possible interesting files for edit
  • Prompt the user to select one or exit if none were found
  • lseek() the file descriptor back to the start of the file
  • Read the entire file out to /tmp/si_fd_edit
  • Open /tmp/si_fd_edit with nano for editing (yes, I said nano…)
  • Upon exiting the editor, it will prompt if you want to overwrite the original file
  • If you said yes, it will reset the fd with lseek() again and write the edited /tmp/si_fd_edit file back to the fd

 

Once cloned, we can build it using the supplied MakeFile using the command make or make edit_leaked_file_descriptor.

 

Using the supplied make file to build our edit_leaked_file_descriptor tool.

 

    Once built, we can run it and follow the prompts.  The first one presents us with the file descriptors that might be interesting to us and asks us to enter the one we are interested in.

 

Running the tool give us a menu to select the interesting file descriptors.

 

    Upon hitting enter, it will make a copy of the file in /tmp/si_fd_edit and launch nano to edit it.

 

The tool opens nano to /tmp/si_fd_edit to let us edit the read data.

 

    We will scroll down to where it shows our individual users that are granted access to sudo.

 

the original line in the file for user privliege only contains root.

 

    And we will just make a quick edit here to include the limited user and save it.

 

We add a line here to give the limited user for full sudo privileges.

 

    Once we exit the editor, our program will ask us to confirm the overwrite of the original file with our edits.  We will answer y to that question and see the program show that it wrote 1,610 bytes to the file descriptor 3.  After the overwrite occurs, we can now use full sudo privileges as the limited user!

 

The tool asking to confirm the overwrite, which we accept and test our newly granted sudo privileges

 

    Pretty awesome!  I know the example here was a little unrealistic, but it’s worth checking out in a lot of instances.  It might not be /etc/sudoers, but it could be a daemon’s configuration file or something else you probably shouldn’t be allowed to edit as a limited user it dropped you down into.

 

What Can You Do as a Developer to Prevent this Sort of Attack?

    As a developer, there are a few things to consider:

  • Does this process really need to be root and drop privileges to begin with?
    • You should always try to operate with the least-privileged model possible.
  • If it does need to be root:
    • Close any open file descriptors before dropping privileges.

 

Conclusion

    As seen in the example covered here, failing to close file descriptors in a SetUID binary is dangerous.  It’s rare to see but it’s important nonetheless and I hope that you’ve enjoyed this example and that it helped you either learn something new or just served as a refresher to check that this is happening in your code base.

 

    If you’re interested in security fundamentals, we have an Application Security channel on YouTube that covers a variety of application security related topics.  We also answer general basic questions in our Knowledge Center.  Finally, if you’re looking for a penetration test, professional training for your organization, or just have general security questions please Contact Us.

 

Links

Join the professionally evil newsletter