Using the setuid bit properly
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.
Yes you can, but it is probably a very bad idea. Usually, you would not set SUID bit directly on your executable, but use a sudo(8) or su(1) to execute it (and limit who can execute it)
Note however, there are many many security problems with allowing regular users to run programs (and especially scripts!) as root. Most of them having to do that unless the program is specifically and very very carefully written to handle that, malicious users could use it to obtain root shell easily.
So even if you were to do it, it would be very good idea to first sanitize ALL input to the program you want to run as root, including its environment, command line parameters, STDIN and any files it processes, data coming from network connections it opens, etc etc. It is extremely hard to do, and even harder to do right.
For example, you may allow users that only edit some file with editor. But the editor allows command execution of external helpers or suspending, thus giving users root shell to do as they wish. Or you might be executing shell scripts which look very security-tight to you, but user can run it with modified $IFS or other environment variables completely changing it's behavior. Or it might be PHP script that's supposed to execute "ls", but user runs it with $PATH modified and thus makes it run a shell which he name "ls". Or dozens of other security problems.
If the program really must do part of its work as root, it would probably be best to modify it so it runs as normal users, but just execute (after sanitizing as much as possible) the part that absolutely needs root through suid helper program.
Note that even if you completely trust all your local users (like, you give them root password), it is a bad idea, as any unintentional security oversight by ANY of them (like, having a PHP script online, or weak password, or whatever) suddenly allows remote attacker to completely compromise whole machine.