Allow setuid on shell scripts
Solution 1
Linux ignores the setuid¹ bit on all interpreted executables (i.e. executables starting with a #!
line). The comp.unix.questions FAQ explains the security problems with setuid shell scripts. These problems are of two kinds: shebang-related and shell-related; I go into more details below.
If you don't care about security and want to allow setuid scripts, under Linux, you'll need to patch the kernel. As of 3.x kernels, I think you need to add a call to install_exec_creds
in the load_script
function, before the call to open_exec
, but I haven't tested.
Setuid shebang
There is a race condition inherent to the way shebang (#!
) is typically implemented:
- The kernel opens the executable, and finds that it starts with
#!
. - The kernel closes the executable and opens the interpreter instead.
- The kernel inserts the path to the script to the argument list (as
argv[1]
), and executes the interpreter.
If setuid scripts are allowed with this implementation, an attacker can invoke an arbitrary script by creating a symbolic link to an existing setuid script, executing it, and arranging to change the link after the kernel has performed step 1 and before the interpreter gets around to opening its first argument. For this reason, most unices ignore the setuid bit when they detect a shebang.
One way to secure this implementation would be for the kernel to lock the script file until the interpreter has opened it (note that this must prevent not only unlinking or overwriting the file, but also renaming any directory in the path). But unix systems tend to shy away from mandatory locks, and symbolic links would make a correct lock feature especially difficult and invasive. I don't think anyone does it this way.
A few unix systems (mainly OpenBSD, NetBSD and Mac OS X, all of which require a kernel setting to be enabled) implement secure setuid shebang using an additional feature: the path /dev/fd/N
refers to the file already opened on file descriptor N (so opening /dev/fd/N
is roughly equivalent to dup(N)
). Many unix systems (including Linux) have /dev/fd
but not setuid scripts.
- The kernel opens the executable, and finds that it starts with
#!
. Let's say the file descriptor for the executable is 3. - The kernel opens the interpreter.
- The kernel inserts
/dev/fd/3
the argument list (asargv[1]
), and executes the interpreter.
Sven Mascheck's shebang page has a lot of information on shebang across unices, including setuid support.
Setuid interpreters
Let's assume you've managed to make your program run as root, either because your OS supports setuid shebang or because you've used a native binary wrapper (such as sudo
). Have you opened a security hole? Maybe. The issue here is not about interpreted vs compiled programs. The issue is whether your runtime system behaves safely if executed with privileges.
Any dynamically linked native binary executable is in a way interpreted by the dynamic loader (e.g.
/lib/ld.so
), which loads the dynamic libraries required by the program. On many unices, you can configure the search path for dynamic libraries through the environment (LD_LIBRARY_PATH
is a common name for the environment variable), and even load additional libraries into all executed binaries (LD_PRELOAD
). The invoker of the program can execute arbitrary code in that program's context by placing a specially-craftedlibc.so
in$LD_LIBRARY_PATH
(amongst other tactics). All sane systems ignore theLD_*
variables in setuid executables.In shells such as sh, csh and derivatives, environment variables automatically become shell parameters. Through parameters such as
PATH
,IFS
, and many more, the invoker of the script has many opportunities to execute arbitrary code in the shell scripts's context. Some shells set these variables to sane defaults if they detect that the script has been invoked with privileges, but I don't know that there is any particular implementation that I would trust.Most runtime environments (whether native, bytecode or interpreted) have similar features. Few take special precautions in setuid executables, though the ones that run native code often don't do anything fancier than dynamic linking (which does take precautions).
Perl is a notable exception. It explicitly supports setuid scripts in a secure way. In fact, your script can run setuid even if your OS ignored the setuid bit on scripts. This is because perl ships with a setuid root helper that performs the necessary checks and reinvokes the interpreter on the desired scripts with the desired privileges. This is explained in the perlsec manual. It used to be that setuid perl scripts needed
#!/usr/bin/suidperl -wT
instead of#!/usr/bin/perl -wT
, but on most modern systems,#!/usr/bin/perl -wT
is sufficient.
Note that using a native binary wrapper does nothing in itself to prevent these problems. In fact, it can make the situation worse, because it might prevent your runtime environment from detecting that it is invoked with privileges and bypassing its runtime configurability.
A native binary wrapper can make a shell script safe if the wrapper sanitizes the environment. The script must take care not to make too many assumptions (e.g. about the current directory) but this goes. You can use sudo for this provided that it's set up to sanitize the environment. Blacklisting variables is error-prone, so always whitelist. With sudo, make sure that the env_reset
option is turned on, that setenv
is off, and that env_file
and env_keep
only contain innocuous variables.
TL,DR:
- Setuid shebang is insecure but usually ignored.
- If you run a program with privileges (either through sudo or setuid), write native code or perl, or start the program with a wrapper that sanitizes the environment (such as sudo with the
env_reset
option).
¹ This discussion applies equally if you substitute “setgid” for “setuid”; they are both ignored by the Linux kernel on scripts
Solution 2
One way of solving this problem is to call the shell script from a program that can use the setuid bit.
its something like sudo.
For example, here is how you would accomplish this in a C program:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
setuid( 0 ); // you can set it at run time also
system( "/home/pubuntu/setuid-test2.sh" );
return 0;
}
Save it as setuid-test2.c.
compile
Now do the setuid on this program binary:
su - nobody
[enter password]
chown nobody:nobody a.out
chmod 4755 a.out
Now, you should be able to run it, and you'll see your script being executed with nobody permissions.
But here also either you need to hardcode the script path or pass it as command line arg to above exe.
Solution 3
I prefix a few scripts that are in this boat thus:
#!/bin/sh
[ "root" != "$USER" ] && exec sudo $0 "$@"
Note that this does not use setuid
but simply executes the current file with sudo
.
Solution 4
If you want to avoid calling sudo some_script
you can just do:
#!/usr/bin/env sh
sudo /usr/local/scripts/your_script
SETUID programs need to be designed with extreme care as they run with root privileges and users have large control over them. They need to sanity-check everything. You cannot do it with scripts because:
- Shells are large pieces of software which interact heavily with user. It is nearly impossible to sanity check everything — especially since most of the code is not intended to run in such mode.
- Scripts are a mostly quick'n'dirty solution and usually are not prepared with such care that they would allow setuid. They have many potentially dangerous features.
- They depend heavily on other programs. It is not sufficient that the shell was checked.
sed
,awk
, etc. would need to be checked as well
Please note that sudo
provides some sanity-checking but it isn't sufficient — check every line in your own code.
As a last note: consider using capabilities. They allow you to give a process running as a user special privileges that would normally require root privileges. However for example, while ping
needs to manipulate the network, it does not need to have access to files. Capabilities can optionally be inherited to sub-processes.
Solution 5
super [ -r reqpath] command [ args ]
Super allows specified users to execute scripts (or other commands) as if they were root; or it can set the uid, gid, and/or supplementary groups on a per-command basis before executing the command. It is intended to be a secure alternative to making scripts setuid root. Super also allows ordinary users to supply commands for execution by others; these execute with the uid, gid, and groups of the user offering the command.
Super consults a ``super.tab'' file to see if the user is allowed to execute the requested command. If permission is granted, super will exec pgm [ args ], where pgm is the program that is associated with this command. (Root is allowed execution by default, but can still be denied if a rule excludes root. Ordinary users are disallowed execution by default.)
If command is a symbolic link (or hard link, too) to the super program, then typing % command args is equivalent to typing % super command args (The command must not be super, or super will not recognize that it's being invoked via a link.)
http://www.ucolick.org/~will/RUE/super/README
http://manpages.ubuntu.com/manpages/utopic/en/man1/super.1.html
Related videos on Youtube
Michael Mrozek
Updated on September 17, 2022Comments
-
Michael Mrozek over 1 year
The
setuid
permission bit tells Linux to run a program with the effective user id of the owner instead of the executor:> cat setuid-test.c #include <stdio.h> #include <unistd.h> int main(int argc, char** argv) { printf("%d", geteuid()); return 0; } > gcc -o setuid-test setuid-test.c > ./setuid-test 1000 > sudo chown nobody ./setuid-test; sudo chmod +s ./setuid-test > ./setuid-test 65534
However, this only applies to executables; shell scripts ignore the setuid bit:
> cat setuid-test2 #!/bin/bash id -u > ./setuid-test2 1000 > sudo chown nobody ./setuid-test2; sudo chmod +s ./setuid-test2 > ./setuid-test2 1000
Wikipedia says:
Due to the increased likelihood of security flaws, many operating systems ignore the setuid attribute when applied to executable shell scripts.
Assuming I'm willing to accept those risks, is there any way to tell Linux to treat the setuid bit the same on shell scripts as it does on executables?
If not, is there a common workaround for this problem? My current solution is to add a
sudoers
entry to allowALL
to run a given script as the user I want it run as, withNOPASSWD
to avoid the password prompt. The main downsides to that is the need for asudoers
entry every time I want to do this, and the need for the caller tosudo some-script
instead of justsome-script
-
dsp over 13 yearsI would advise against the suggestion to allow passing of the script as a command line argument, as that essentially gives anyone who can execute the program the ability to run any script as that defined user.
-
Dan over 13 yearsI would add some sanity checking if you implement that C solution: ensure that the script being run is owned by the right user+group, is not world-writable, and not of the directories in its path are world- or group-writable (essentially the same checks that suPHP performs) - this will reduce the chance of your tool becoming a security problem through other users being able to somehow edit or replace the script.
-
Louis Gerbarg over 13 yearsIt will work. Horribly as you stated. SETUID bit allows execution with the owner right. Setuid shell (unless it was designed to work with setuid) will run as root for any user. I.e. anyone can run
rm -rf /
(and other commands from series DON'T DO IT AT HOME). -
Louis Gerbarg over 13 yearsI voted down for suggestion of passing as parameter. IT WOULD MEAN ANY SCRIPT CAN BE RUN WITH ROOT PRIVILAGES. YES - ONE CONTAINING
rm -rf /
AS WELL. -
Hemant over 13 years@Maciej: your point is valid. My assumption was from original Q. "Assuming I'm willing to accept those risks, is there any way to tell Linux to treat the setuid bit the same on shell scripts as it does on executables?"
-
Louis Gerbarg over 13 years@Hemant: The problem is that passing script as parameter means that I can execute any script (i.e. effectively giving any user root rights). Giving setuid to one program does not involve such risk.
-
Gilles 'SO- stop being evil' over 13 yearsNote that THIS IS INSECURE even if the full path to the script is hardcoded. The shell will inherit variables from the environment, and many of them allow the invoker to inject arbitrary code.
PATH
andLD_LIBRARY_PATH
are obvious vectors. Some shells execute$ENV
or$BASHENV
or~/.zshenv
even before they start executing the script proper, so you can't protect from these at all from within the script. The only safe way to invoke a shell script with privileges is to clean up the environment. Sudo knows how to do it safely. So do not write your own wrapper, use sudo. -
Michael Mrozek over 13 yearsI feel bad that he's suddenly getting downvoted for this -- I did specifically say I wanted to hear insecure versions too, and I was imagining an executable that took a shell script argument when I said it. Obviously it's massively insecure, but I wanted to know what possibilities exist
-
Hugo over 13 yearsWow, you just scared the crap out of me, and I think I need to spend the next 4 days re-writing a lot of shell scripts in native code instead! ;-)
-
Gilles 'SO- stop being evil' over 13 years@Josh: Secure setuid shell scripts are possible, but only if the both the shell implementer and the script writer are very careful. Rather than native code, I recommend Perl, where the implementers have taken care that setuid scripts should be secure with little effort on the script writer's part.
-
jmtd almost 13 yearsapparently the
suidperl
stuff has been deprecated and marked for removal for years (but persists non-the-less) -
ctrl-alt-delor almost 13 yearsare there any other languages that support setuid safely?
-
Randy Stauner almost 13 yearsActually
suidperl
has been removed as of perl 5.11 (5.12 stable): perl5110delta: > "suidperl" has been removed. It used to provide a mechanism to emulate setuid permission bits on systems that don't support it properly. perl5120delta: > "suidperl" is no longer part of Perl. It used to provide a mechanism to emulate setuid permission bits on systems that don't support it properly. -
Randy Stauner almost 13 yearsAlso note this line from perl 5.6.1 docs (nearly a decade ago)... perl561delta: > Note that suidperl is neither built nor installed by default in any recent version of perl. Use of suidperl is highly discouraged. If you think you need it, try alternatives such as sudo first. See courtesan.com/sudo .
-
user1686 about 12 years@Gilles: FYI, Linux unsets
LD_LIBRARY_PATH
among other things when it encounters the setuid bit. -
Demi over 10 yearsDoesn't sudo sanitize the environment (or can at least be set to do so)?
-
Gilles 'SO- stop being evil' over 10 years@Demetri Yes, sudo can sanitize the environment if configured appropriately (
env_reset
option). -
peterph about 10 years@MaciejPiechotka by DON'T DO IT AT HOME you mean Feel free to do that at work? :)
-
terdon over 9 yearsHi and welcome to the site! We expect answers to be more detailed here. Could you edit your answer and explain what this program is and how it could help solve the OP's problem?
-
Tom about 9 yearsI don't understand: this seems to be an explanation of the reasons for the problems, but is there an actual answer to the OP's question here? Is there a way to tell my OS to run setuid shell scripts?
-
Toby Speight almost 9 yearsInstead of using
system
, you may find it simpler (and more efficient) to use one of theexec
family - most likelyexecve
. That way, you don't create a new process or start a shell, and you can forward arguments (assuming that your privileged script can safely handle arguments). -
ylluminate almost 8 yearsExcellent suggestion! We don't need our forks taken from us if we want to use them in dangerous ways... ;)
-
Chad over 7 yearsThank you Nizam, for hours trying to find something like super for accounts to be allowed to be executed by another user as the user of the file.
-
ceremcem over 7 yearsI'm surprized how come nobody asked what if we write a C program and set it's setuid bit where this C program will calculate the script's checksum (eg. sha1) and filesize, and will run the script with sudo if those results match with the ones hardcoded in the same C program.
-
Bob about 7 yearsI presume
/ust/bin/env
should be/usr/bin/env
. -
Joshua about 7 yearsWhat old system are you using that hasn't closed the LD_PRELOAD and LD_LIBRARY_PATH holes yet? Normally the loader just ignores them if ruid != euid.
-
Theodore R. Smith almost 7 yearsWhy the downvotes???
-
siride over 6 yearsProbably because it's an overly complicated solution that does string manipulation and has embedded shell code. Either you make a simple launcher, or you use sudo. This is the worst of all options.
-
phyatt about 6 years
-
Luc over 5 yearsThis does not use setuid but just gives you a
sudo
prompt. (For me, the whole point of setuid is allowing things to run as root without needing sudo.) -
CainBot over 5 yearsCommenting on the situation, not your explanation: This approach is so damn shortsighted. It makes not only attacks harder, but also good practices, such as running things under a separate, restricted account.
-
mosvy over 4 yearsOpenBSD (6.5 and 6.6 snapshot as of 2019) comes with "secure" setuid scripts enabled by default; no kernel setting has to be enabled. See my demo.
-
mosvy over 4 yearsAnd suidperl went the way of the dodo a long time ago
-
K.S. Bilodeau almost 4 years@Luc This does not give you a
sudo
prompt if you use theNOPASSWD
tag in your sudoers file, as the OP said that he does. I use this method as well and it also has the extra environment sanitation that sudoers provides. -
Luc almost 4 years@dannyw Ah yes, fair enough. I guess there was a mismatch between my expectations given the title (and the answer I was looking for, coming from a search engine) and what OP actually wrote in their question. I can't remove the downvote, though...... "You last voted on this answer Dec 13 '18 at 8:26. Your vote is now locked in unless this answer is edited." wtf stackexchange
-
Uncle Billy over 3 yearsThe produced
test.bin
will allow anybody to run any command as root. -
ceztko over 3 yearsThat's the point of setuid anyway. You still need root to create such wrapper.
-
Uncle Billy over 3 yearsYou don't get it? Once created,
test.bin
will allow anybody to run any command as root, not justwhoami
. Just think what will happen if a regular user runsln -fs /lib/<...>/libc.so.6 /tmp/tmpsetuidexec
before runningtest.bin
. And that's just one of the thousand ways you can exploit that. -
ceztko over 3 yearsIf the existence of the temporary file it's your only concern, that is fixable by piping the embedded resource to a shell process in the same process, without creating the temporary file. I can do it later, or when I have time.
-
Uncle Billy over 3 yearsIt will still allow anybody to run any command as root, even then.
-
ceztko over 3 yearsThats the point of setuid, whats wrong with that? If I create a compiled program that reproduces the exact behavior of an unsecure script, I still have the security hole and the system will not be able from stopping me from doing it anyway. setuid should be used only on trusted stuff, like on all the compiled programs where it is set.
-
Uncle Billy over 3 yearsNo, that's not the point of setuid. Notice that
test.bin
will allow anybody to run any command (sorry for repeating ;-)), and do it irrespective of the content of its sourcetest.sh
script. Yes, most other answers are just as bad. -
ceztko over 3 yearsLet us continue this discussion in chat.
-
ceztko over 3 yearsThe copy on temporary file on the setuid executable has been removed. Now the embedded script is executed directly with
fork
andfexecve
-
Uncle Billy over 3 yearsYour program still allows any regular user to run any command as root -- you would have to remove and/or sanitize all the environment variables. Just for a start. I see that you had removed the example script -- well, even a file containing just
#! /bin/bash
can be exploited to run any command when "compiled" into a setuid binary with your script. As to a setuid binary having to be read-only, I don't see how that helps with anything at all ;-) -
Uncle Billy over 3 yearsAnd btw, notice that the answer you link to is replete with bogus and outdated information. E.g. wrt Linux -- if you really want to break your Linux system, you can easily configure it to run setuid scripts via
binfmt_misc
-- no need to patch the kernel, or turn scripts into binaries (as in your answer and many others) -
ceztko over 3 years@UncleBilly Having the executable read-only inhibits from modifying the embedded string. The script now run from an in memory file descriptor and and can't be replaced externally, so I don't see a vulnerability linked to just embedding a shebang script in the executable. The race condition described in the same post above which allows arbitrary commands execution doesn't apply, please check the updated version of my script. Also I introduced rudimental environment variables sanitizing, which is incomplete but the number of common variables that need sanification is finite so it's a doable task
-
Uncle Billy over 3 yearsNo, the environment variables which have to be sanitized when running a shell are NOT a closed set. E.g. with your original sample script (which was using the
#! /usr/bin/env bash
cargo cult shebang), trivially changing thePATH
would allow anybody to run any command instead of its content. But that is not the only to do it, and not even explicitly running/bin/bash
with and empty script is safe. -
Uncle Billy over 3 yearsAs to read-only, etc the kernel will wipe any setuid bits when other users than the root are modifying a file, and the root can disregard read/write permissions anyway.
-
ceztko over 3 yearsAdded
PATH
sanitization, such as thePATH
value is the same as the time embedding of the script into the native wrapper took place. Super user should be responsible in ensuring the script will execute correctly at a later stage. Ensuring the system is non compromised when running the script is also key in ensuring it doesn't open doors to security holes, but this holds for all scripts execution, also regular root executed ones. -
ceztko over 3 years@UncleBilly I noticed your objections diminished in merit since I moved the embedded script to a python one. To each of your correct arguments, there's probably a proper countermeasure/mitigation, such as fixing
PATH
at the moment of embedding. More important, there's nothing like a permanent formally verifiable security of the system: a compromised system can introduced holes in all existing script execution, also regular root executions. Restricting scripts embedding to a subset of supported/trusted interpreters would also be key in being able to formally prove security of such method -
Kai Petzke almost 3 yearsWhy was this downvoted?
-
Thiago Barcala almost 3 yearsI created an utility that is able to create this kind of binary executable wrappers: github.com/thiagorb/suid-wrapper, without the need to write a new C program or compile it everytime.
-
kgendron almost 3 years@ThiagoBarcala the point of using a thin wrapper script is to make it as bullet proof as possible. Can you say with confidence your program contains zero bugs?
-
kgendron almost 3 yearsJust at a glance you use strcpy to copy a path into a buffer of length PATH_MAX. But Linux doesn't actually do a good job of enforcing that paths are shorter than PATH_MAX, so that's a potential buffer overflow.
-
Thiago Barcala almost 3 yearsNot at all! It is extremely experimental, considering I just wrote it, and barely used it myself.
-
Thiago Barcala almost 3 yearsBTW, I fixed the potential overflow. Thanks for pointing it out!