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 allow ALL
to run a given script as the user I want it run as, with NOPASSWD
to avoid the password prompt. The main downsides to that is the need for a sudoers
entry every time I want to do this, and the need for the caller to sudo some-script
instead of just some-script
11 Answers 11
Linux ignores the setuid1 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).
1 This discussion applies equally if you substitute "setgid" for "setuid"; they are both ignored by the Linux kernel on scripts
-
4@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.Gilles 'SO- stop being evil'– Gilles 'SO- stop being evil'2010年12月11日 13:52:43 +00:00Commented Dec 11, 2010 at 13:52
-
4apparently the
suidperl
stuff has been deprecated and marked for removal for years (but persists non-the-less)jmtd– jmtd2011年05月12日 14:32:57 +00:00Commented May 12, 2011 at 14:32 -
8Actually
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– Randy Stauner2011年07月14日 17:40:25 +00:00Commented Jul 14, 2011 at 17:40 -
5Also 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 .Randy Stauner– Randy Stauner2011年07月14日 17:45:19 +00:00Commented Jul 14, 2011 at 17:45
-
3I 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?Tom– Tom2015年03月31日 13:36:40 +00:00Commented Mar 31, 2015 at 13:36
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.
-
32I 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.dsp– dsp2010年08月12日 08:05:10 +00:00Commented Aug 12, 2010 at 8:05
-
59Note 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.Gilles 'SO- stop being evil'– Gilles 'SO- stop being evil'2010年10月08日 20:26:07 +00:00Commented Oct 8, 2010 at 20:26 -
40I 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 existMichael Mrozek– Michael Mrozek2010年10月08日 22:58:37 +00:00Commented Oct 8, 2010 at 22:58
-
10@Gilles: FYI, Linux unsets
LD_LIBRARY_PATH
among other things when it encounters the setuid bit.grawity– grawity2012年02月15日 15:31:44 +00:00Commented Feb 15, 2012 at 15:31 -
3Instead 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).Toby Speight– Toby Speight2015年07月23日 18:52:31 +00:00Commented Jul 23, 2015 at 18:52
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
.
-
10This 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.)Luc– Luc2018年12月12日 21:13:42 +00:00Commented Dec 12, 2018 at 21:13 -
4@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.dannyw– dannyw2020年07月26日 22:59:11 +00:00Commented Jul 26, 2020 at 22:59 -
1@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 stackexchangeLuc– Luc2020年07月26日 23:28:24 +00:00Commented Jul 26, 2020 at 23:28
-
To add: I find this a perfect solution. Sometimes I have to run a script as root (because it needs to load SSL files and run gunicorn on :443), but sometimes just want to run it plain HTTP on :80 so no root required. This way always run it as user and only ask password if needed.Robin van Leeuwen– Robin van Leeuwen2023年04月03日 10:41:18 +00:00Commented Apr 3, 2023 at 10:41
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.
-
4I presume
/ust/bin/env
should be/usr/bin/env
.Bob– Bob2017年04月18日 16:07:27 +00:00Commented Apr 18, 2017 at 16:07 -
Or you can use the
-S
env option to make your script directly runable without having to embed it's name:#!/usr/bin/env -S -i MYVAR=foo sudo --preserve-env perl -w -T
as described in answer for stackoverflow.com/questions/21597300/…Britton Kerin– Britton Kerin2022年11月18日 21:39:51 +00:00Commented Nov 18, 2022 at 21:39
If for some reason sudo
is not available, you can write a thin wrapper script in C:
#include <unistd.h>
int main() {
setuid(0);
execle("/bin/bash","bash","/full/path/to/script",(char*) NULL,(char*) NULL);
}
And once you compile it set it as setuid
with chmod 4511 wrapper_script
.
This is similar to another posted answer, but runs the script with a clean environment and explicitly uses /bin/bash
instead of the shell called by system()
, and so closes some potential security holes.
Note that this discards the environment entirely. If you want to use some environmental variables without opening up vulnerabilities, you really just need to use sudo
.
Obviously, you want to make sure the script itself is only writable by root.
-
1I 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.Thiago Barcala– Thiago Barcala2021年08月07日 17:35:33 +00:00Commented Aug 7, 2021 at 17:35
-
1@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?Chris– Chris2021年08月07日 18:44:07 +00:00Commented Aug 7, 2021 at 18:44
-
1Just 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.Chris– Chris2021年08月07日 18:47:03 +00:00Commented Aug 7, 2021 at 18:47
-
Not at all! It is extremely experimental, considering I just wrote it, and barely used it myself.Thiago Barcala– Thiago Barcala2021年08月07日 18:47:54 +00:00Commented Aug 7, 2021 at 18:47
-
BTW, I fixed the potential overflow. Thanks for pointing it out!Thiago Barcala– Thiago Barcala2021年08月07日 19:42:44 +00:00Commented Aug 7, 2021 at 19:42
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
-
-
Thank 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.Chad– Chad2016年09月14日 15:39:05 +00:00Commented Sep 14, 2016 at 15:39
You can compile the script with a newer version of shc with the -S
argument (enable SETUID) set.
shc -S -f your-script.sh
chmod u+s your-script.sh.x
your-script.sh.x
would be the executable that can be used by a regular user.
-
Why was this downvoted?Kai Petzke– Kai Petzke2021年07月31日 16:30:02 +00:00Commented Jul 31, 2021 at 16:30
-
I think that the downvotes are based on the fact that this solution may have the same security risks as described in the top answer. However, this solution is actually a valid one, in particular, because the question explcitly states that the administrator is will to take the risks. So I'm puzzled by the downvotes as well :)Marcus– Marcus2022年09月18日 20:13:22 +00:00Commented Sep 18, 2022 at 20:13
-
truth is every single method mentioned under this question has tradeoffs. There's no perfect answer.ZigZagT– ZigZagT2022年09月19日 23:57:32 +00:00Commented Sep 19, 2022 at 23:57
You can create an alias for sudo + the name of the script. Of course, that is even more work to set up, since you then have to setup an alias, too, but it saves you from having to type sudo.
But if you don't mind horrible security risks, use a setuid shell as the interpreter for the shell script. Don't know whether that'll work for you, but I guess it might.
Let me state that I advise against actually doing this, though. I'm just mentioning it for educational purposes ;-)
-
7It 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).Maja Piechotka– Maja Piechotka2010年08月17日 10:52:18 +00:00Commented Aug 17, 2010 at 10:52 -
14@MaciejPiechotka by DON'T DO IT AT HOME you mean Feel free to do that at work? :)peterph– peterph2014年02月27日 21:22:05 +00:00Commented Feb 27, 2014 at 21:22
First, a disclaimer: the OP said "Assuming I'm willing to accept those risks, ...". I found myself in a similar situation where I had some one-off tasks where I was not concerned with security risks, because it was a short-term situation... Before you haters downvote, this is what the OP wanted...
So all my solution is is an extension of what Hemant came up with above. The only difference from his solution is a bit of automation so that you can both hard-code a specific executable you want to launch, but with a one-liner that's sort of like a combination of ln
and setuid
(e.g. chmod 6755
).
Some would say it's overkill for something that you "ought not do in the first place" -- but again, that's what the OP wanted... About half the code could be deleted if you don't care about debugging options; I wanted the ability to turn on xtrace
, and some of the debug features were just to help me develop it...
And to repeat, if it wasn't clear already: just plain sudo
is a better option in nearly every case, except for sheer trade-off between laziness and speed... (I agree with most of the other posts)
Here is the code:
cat > makeSetUIDExecutable <<\ENDSCRIPT
#!/bin/bash
usage() { echo "Usage: 0ドル -c <command> -o <output> [-u <user>] [-v (verbose)] [-x (xtrace)] [-X (xtrace inherit)]" 1>&2; exit 1; }
PrintArray() {
local -n arr=1ドル
echo -n "1ドル: "
for elem in "${!arr[@]}"; do
echo -n "[${elem@Q}]=${arr[$elem]@Q}, "
done | sed 's/, $//'; echo
}
PrintVariable() {
echo "1ドル: ${!1}"
}
unset makeSetUIDExecutableOpts quotedCommand
declare -A makeSetUIDExecutableOpts
OPTIND=1
while getopts ":c:o:u:vxX" makeSetUIDExecutableOpt; do
# echo -e "\nProcessing argument...\nmakeSetUIDExecutableOpt = ${makeSetUIDExecutableOpt}\nOPTARG = ${OPTARG}"
[[ "${makeSetUIDExecutableOpt}" == ":" ]] && usage
makeSetUIDExecutableOpts["${makeSetUIDExecutableOpt}"]="${OPTARG}"
done
[[ -z "${makeSetUIDExecutableOpts[c]}" ]] || [[ -z "${makeSetUIDExecutableOpts[o]}" ]] && usage
[[ "${makeSetUIDExecutableOpts[x]+-}" ]] && {
set -x
trap "set +x" EXIT
}
makeSetUIDExecutableOpts[u]="${makeSetUIDExecutableOpts[u]:-root}"
userID=$(id -u "${makeSetUIDExecutableOpts[u]}")
[[ "${makeSetUIDExecutableOpts[X]+-}" ]] && quotedCommand="set -x; "
quotedCommand+="${makeSetUIDExecutableOpts[c]//\\\"/\\\\\"}"
quotedCommand="\"${quotedCommand//\"/\\\"}\""
[[ "${makeSetUIDExecutableOpts[v]+-}" ]] && {
echo -e "\nSummary -- Arguments provided:\n"
PrintArray makeSetUIDExecutableOpts
echo
PrintVariable quotedCommand
echo
}
cat > "${makeSetUIDExecutableOpts[o]}.c" <<ENDSUBSCRIPT
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
int main()
{
if( setuid( ${userID} ) ) {
printf("setuid failed, errno = %i\n", errno);
}
system( ${quotedCommand} );
return 0;
}
ENDSUBSCRIPT
cc "${makeSetUIDExecutableOpts[o]}.c" -o "${makeSetUIDExecutableOpts[o]}"
sudo chown "${makeSetUIDExecutableOpts[u]}" "${makeSetUIDExecutableOpts[o]}"
sudo chmod 6755 "${makeSetUIDExecutableOpts[o]}"
ENDSCRIPT
Example usage - a rather large command, to prove three-ways that all the user info is as-expected... Your command can be one word or ten lines...:
printUidGidInfo='set -- $(cat /proc/self/status | grep -P -o "(?<=Uid:|Gid:).*$"); unset outputString; i=1; while [ ${i} -le 4 ]; do outputString=${outputString}"\n"$(id -nu ${1}); shift; i=$(( i + 1 )); done; while [ ${i} -le 8 ]; do temp=$(getent group ${1}); outputString=${outputString}"\n"${temp%%:*}; shift; i=$(( i + 1 )); done; printf "\nReal UID: %s\nEffective UID: %s\nSaved set UID: %s\nFilesystem UID: %s\nReal GID: %s\nEffective GID: %s\nSaved set GID: %s\nFilesystem UID: %s\n\n" ${outputString}'
./makeSetUIDExecutable -c 'echo; echo "id -u -n:"; id -u -n; echo; echo stat -c "User: %U, Group: %G" /proc/$$/; stat -c "User: %U, Group: %G" /proc/$$/; echo; echo "/proc/self/status"; '"${printUidGidInfo}" -o proveUserInfo -v
./proveUserInfo
Output:
id -u -n:
root
stat -c User: %U, Group: %G /proc/10977/
User: root, Group: users
/proc/self/status
Real UID: root
Effective UID: root
Saved set UID: root
Filesystem UID: root
Real GID: users
Effective GID: users
Saved set GID: users
Filesystem UID: users
IMPORTANT NOTE: the task described in the original question can be achieved in a safe way using sudo
and sudoers
configurations. Regardless of negative votes, this answer is going to stay for people that are still committed in running scripts with setuid, also after being warned of security implications. The method described also put bases for writing a more robust treatment of environment, like the one in sudo
. Suggestions are welcome in chat.
The below shell script will create an a native wrapper with setuid and a given script embedded.
DISCLAIMER: READ CAREFULLY
To use the following snippet in a safe manner, some assumptions are necessary:
- the setuid executables created must be read only;
- the embedded script should be trusted and not being able to create security holes, such as execute another arbitrary script in the filesystem;
- The interpreter of your script, ant the script itself, should not be sensitive to environment manipulation attacks, such as modifying environment variables to force execution of untrusted code;
- The system is not already compromised, such as executables in system
PATH
reachable directories were already replaced with malicious versions.
If you can't guarantee the above conditions, or if you don't accept the security risks involved in being able to run trusted or non-trusted scripts with setuid, stop reading now. Also the native wrapper is probably incomplete at mitigating some common environment manipulation attacks, so use it at your own risk. Suggestions to improve the code are welcome.
If you read the above warning, and you think you'll be able to guarantee the above conditions, or you can ensure safety of your system by other means, create a file create-setuid-wrapper.sh
as it follows:
#!/usr/bin/env bash
set -e
if [[ `id -u` != 0 ]]; then
echo "Execute this command with super user credentials"
exit 1
fi
if [[ $# -ne 1 || ! -f 1ドル ]]; then
echo "A file argument is necessary"
exit 1
fi
INPUT=1ドル
TEMPFOLDER=/tmp/tmpsuidwrapper
TEMPINPUT=$TEMPFOLDER/input.data
TEMPINPUTOBJ=$TEMPFOLDER/input.o
WRAPPERSRC=$TEMPFOLDER/wrapper.c
OUTFILE=$(basename -- "$INPUT")
OUTFILE="${OUTFILE%.*}.bin"
# Create temporary directory and copy the input
mkdir -p $TEMPFOLDER
cp $INPUT $TEMPINPUT
# Write wrapper source
cat > $WRAPPERSRC <<- EOF
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <err.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/wait.h>
extern const char _binary_input_data_start[];
extern void* _binary_input_data_size;
int main (int argc, char *argv[])
{
int rc = setuid(0);
if (rc == -1)
{
perror("setuid failed");
return -1;
}
int fd = memfd_create("script", 0);
if (fd == -1)
{
perror("memfd_create failed");
return -1;
}
size_t size = (size_t)&_binary_input_data_size;
rc = write(fd, _binary_input_data_start, size);
if (rc == -1)
{
perror("write failed");
return -1;
}
// Clean the environment to protect from some common
// environment manipulation attacks
// CHECK-ME: Check if some variables unset is actually needed
// and if more variables should be unset, sanitize
unsetenv("LD_LIBRARY_PATH");
unsetenv("LD_PRELOAD");
unsetenv("BASHENV");
unsetenv("ENV");
setenv("PATH", "${PATH}", 1);
pid_t pid = fork();
if (pid == 0)
{
// Child process, execute the script
{
const char * const argv[] = { "script", NULL };
const char * const envp[] = { NULL };
int rc = fexecve(fd, (char * const *) argv, (char * const *) envp);
if (rc == -1)
{
perror("fexecve failed");
return -1;
}
}
}
if (pid == -1)
{
perror("fork failed");
return -1;
}
// Parent process
wait(NULL);
return 0;
}
EOF
# NOTE: Crappy 'ld' syntax apparenty doesn't allow to
# specify a name for the symbol being created, so it will
# use path as the name. We move the input to temp folder
# to keep the symbol name short
CWD=`pwd`
cd $TEMPFOLDER
# Create a linkable object with the data to be embedded
ld -r -b binary -o input.o input.data
cd $CWD
# Compile the program and set setuid
cc -fPIC -no-pie -o $OUTFILE $WRAPPERSRC $TEMPINPUTOBJ
chmod 4755 $OUTFILE
# Cleanup
rm -fr $TEMPFOLDER
Then prepare, as an example, a test.py
script like the following:
#!/usr/bin/env python3
import getpass
print(getpass.getuser())
Call create_setuid_wrapper.sh
with the above test.py
. The script will produce an executable test.bin
. Ensure your system allow execution of arbitrary executables with setuid (SELinux enabled systems will not). Execution will print root
or the name of your super user.
EDIT1: Removed copy to temporary file on the setuid executable. Now the embedded script is just executed directly from memory with a fork
and a fexecve
.
EDIT2: Attempt to clean the environment from some common environment manipulation attacks. It may be wrong and/or insufficient.
-
2The produced
test.bin
will allow anybody to run any command as root.user313992– user3139922021年02月04日 21:40:01 +00:00Commented Feb 4, 2021 at 21:40 -
That's the point of setuid anyway. You still need root to create such wrapper.ceztko– ceztko2021年02月04日 21:50:35 +00:00Commented Feb 4, 2021 at 21:50
-
3You 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.user313992– user3139922021年02月04日 21:54:40 +00:00Commented Feb 4, 2021 at 21:54 -
1It will still allow anybody to run any command as root, even then.user313992– user3139922021年02月04日 22:00:45 +00:00Commented Feb 4, 2021 at 22:00
-
1No, 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.user313992– user3139922021年02月04日 22:07:45 +00:00Commented Feb 4, 2021 at 22:07
I first found this question, unconvinced by all the answers, here is a much better one, which also allows you to completely obfuscate your bash script, if you are so inclined!
It should be self-explanatory.
#include <string>
#include <unistd.h>
template <typename T, typename U>
T &replace (
T &str,
const U &from,
const U &to)
{
size_t pos;
size_t offset = 0;
const size_t increment = to.size();
while ((pos = str.find(from, offset)) != T::npos)
{
str.replace(pos, from.size(), to);
offset = pos + increment;
}
return str;
}
int main(int argc, char* argv[])
{
// Set UUID to root
setuid(0);
std::string script =
R"(#!/bin/bash
whoami
echo 1ドル
)";
// Escape single quotes.
replace(script, std::string("'"), std::string("'\"'\"'"));
std::string command;
command = command + "bash -c '" + script + "'";
// Append the command line arguments.
for (int a = 0; a < argc; ++a)
{
command = command + " " + argv[a];
}
return system(command.c_str());
}
Then you run
g++ embedded.cpp -o embedded
sudo chown root embedded
sudo chmod u+s embedded
-
1Why the downvotes???Theodore R. Smith– Theodore R. Smith2017年07月27日 08:05:53 +00:00Commented Jul 27, 2017 at 8:05
-
9Probably 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.siride– siride2017年09月27日 22:01:01 +00:00Commented Sep 27, 2017 at 22:01