Scripting – How to Use the setuid Bit Properly

scriptingsetuid

I have a process that needs root privileges when run by a normal user. Apparently I can use the "setuid bit" to accomplish this. What is the proper way of doing this on a POSIX system?

Also, how can I do this with a script that uses an interpreter (bash, perl, python, php, etc)?

Best Answer

The setuid bit can be set on an executable file so that when run, the program will have the privileges of the owner of the file instead of the real user, if they are different. This is the difference between effective uid (user id) and real uid.

Some common utilities, such as passwd, are owned root and configured this way out of necessity (passwd needs to access /etc/shadow which can only be read by root).

The best strategy when doing this is to do whatever you need to do as superuser right away then lower privileges so that bugs or misuse are less likely to happen while running root. To do this, you set the process's effective uid to its real uid. In POSIX C:

#define _POSIX_C_SOURCE 200112L // Needed with glibc (e.g., linux).
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

void report (uid_t real) {
    printf (
        "Real UID: %d Effective UID: %d\n",
        real,
        geteuid()
    );
}

int main (void) {
    uid_t real = getuid();
    report(real);
    seteuid(real);
    report(real);
    return 0;
}

The relevant functions, which should have an equivalent in most higher level languages if they are used commonly on POSIX systems:

  • getuid(): Get the real uid.
  • geteuid(): Get the effective uid.
  • seteuid(): Set the effective uid.

You can't do anything with the last one inappropriate to the real uid except in so far as the setuid bit was set on the executable. So to try this, compile gcc test.c -o testuid. You then need to, with privileges:

chown root testuid
chmod u+s testuid

The last one sets the setuid bit. If you now run ./testuid as a normal user you'll see the process by default runs with effective uid 0, root.

What about scripts?

This varies from platform to platform, but on Linux, things that require an interpreter, including bytecode, can't make use of the setuid bit unless it is set on the interpreter (which would be very very stupid). Here's a simple perl script that mimics the C code above:

#!/usr/bin/perl
use strict;
use warnings FATAL => qw(all);

print "Real UID: $< Effective UID: $>\n";
$> = $<; # Not an ASCII art greedy face, but genuine perl...
print "Real UID: $< Effective UID: $>\n"; 

True to it's *nixy roots, perl has build in special variables for effective uid ($>) and real uid ($<). But if you try the same chown and chmod used with the compiled (from C, previous example) executable, it won't make any difference. The script can't get privileges.

The answer to this is to use a setuid binary to execute the script:

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

int main (int argc, char *argv[]) {
    if (argc < 2) {
        puts("Path to perl script required.");
        return 1;
    }
    const char *perl = "perl";
    argv[0] = (char*)perl;
    return execv("/usr/bin/perl", argv);
}

Compile this gcc --std=c99 whatever.c -o perlsuid, then chown root perlsuid && chmod u+s perlsuid. You can now execute any perl script with with an effective uid of 0, regardless of who owns it.

A similar strategy will work with php, python, etc. But...

# Think hard, very important:
>_< # Genuine ASCII art "Oh tish!" face

PLEASE PLEASE DO NOT leave this kind of thing lying around. Most likely, you actually want to compile in the name of the script as an absolute path, i.e., replace all the code in main() with:

    const char *args[] = { "perl", "/opt/suid_scripts/whatever.pl" }
    return execv("/usr/bin/perl", (char * const*)args);

Them make sure /opt/suid_scripts and everything in it are read-only for non-root users. Otherwise, someone could swap in anything for whatever.pl.

In addition, beware that many scripting languages allow environment variables to change the way they execute a script. For example, an environment variable might cause a library supplied by the caller to be loaded, allowing the caller to execute arbitrary code as root. Thus, unless you know that both the interpreter and the script itself are robust against all possible environment variables, DON'T DO THIS.

So what should I do instead?

A safer way to allow a non-root user to run a script as root is to add a sudo rule and have the user run sudo /path/to/script. Sudo strips most environment variables, and also allows the administrator to finely select who can run the command and with what arguments. See How to run a specific program as root without a password prompt? for an example.

Related Question