Well, you could always do:
#! /bin/bash -
{ shopt -s expand_aliases;SWITCH_TO_USER(){ { _u=$*;_x="$(declare;alias
shopt -p;set +o);"'set -- "${_a[@]}";unset _x _a';set +x;} 2>/dev/null
exec sudo -u "$1" env "_x=$_x" bash -c 'eval "$_x" 2> /dev/null;. "$0"
' "$0";};alias skip=":||:<<'SWITCH_TO_USER $_u'"
alias SWITCH_TO_USER="{ eval '"'_a=("$@")'"';} 2>/dev/null;SWITCH_TO_USER"
${_u+:} alias skip=:;} 2>/dev/null
skip
echo test
a=foo
set a b
SWITCH_TO_USER root
echo "$a and $1 as $(id -un)"
set -x
foo() { echo "bar as $(id -un)"; }
SWITCH_TO_USER rag
foo
set +x
SWITCH_TO_USER root again
echo "hi again from $(id -un)"
(ʘ‿ʘ)
That first started as a joke as that implements what's requested though probably not exactly as expected, and is not practically useful. But as it evolved to something that works to some extent and involves a few nice hacks, here is a little explanation:
As Miroslav said, if we leave aside the Linux-style capabilities (which wouldn't really help here either anyway), the only way for an unprivileged process to change uid is by executing a setuid executable.
Once you get superuser privilege though (by executing a setuid executable whose owner is root for instance), you can switch the effective user id back and forth between your original user id, 0 and any other id unless you relinquish your saved set user id (like things like sudo
or su
typically do).
For instance:
$ sudo cp /usr/bin/env .
$ sudo chmod 4755 ./env
Now I've got an env
command that allows me to run any command with an effective user id and saved set user id of 0 (my real user id still being 1000):
$ ./env id -u
0
$ ./env id -ru
1000
$ ./env -u PATH =perl -e '$>=1; system("id -u"); $>=0;$>=2; system("id -u");
$>=0; $>=$<=3; system("id -ru; id -u"); $>=0;$<=$>=4; system("id -ru; id -u")'
1
2
3
3
4
4
perl
has wrappers to setuid
/seteuid
(those $>
and $<
variables).
So does zsh:
$ sudo zsh -c 'EUID=1; id -u; EUID=0; EUID=2; id -u'
1
2
Though above those id
commands are called with a real user id and saved set userid of 0 (though if I had used my ./env
instead of sudo
that would have only been the saved set userid, while the real user id would have remained 1000), which means that if they were untrusted commands, they could still do some damage, so you'd want to write it instead like:
$ sudo zsh -c 'UID=1 id -u; UID=2 id -u'
(that is set all uids (effective, real and saved set) just for the execution of those commands.
bash
doesn't have any such way to change the user ids. So even if you had a setuid executable with which to call your bash
script, that wouldn't help.
With bash
, you're left with executing a setuid executable each time you want to change uid.
The idea in the script above is upon a call to SWITCH_TO_USER, to execute a new bash instance to execute the remaining of the script.
SWITCH_TO_USER someuser
is more or less a function that executes the script again as a different user (using sudo
) but skiping the start of the script until SWITCH_TO_USER someuser
.
Where it gets tricky is that we want to keep the state of the current bash after having started the new bash as a different user.
Let's break it down:
{ shopt -s expand_aliases;
We'll need aliases. One of the tricks in this script is to skip the part of the script until the SWITCH_TO_USER someuser
, with something like:
:||: << 'SWITCH_TO_USER someuser'
part to skip
SWITCH_TO_USER
That form is similar to the #if 0
used in C, that is a way to completly comment out some code.
:
is a no-op that returns true. So in : || :
, the second :
is never executed. However, it is parsed. And the << 'xxx'
is a form of here-document where (because xxx
is quoted), no expansion or interpretation is done.
We could have done:
: << 'SWITCH_TO_USER someuser'
part to skip
SWITCH_TO_USER
But that would have meant that the here-document would have had to be written and passed as stdin to :
. :||:
avoids that.
Now, where it gets hacky is that we use the fact that bash
expands aliases very early in its parsing process. To have skip
being an alias to the :||: << 'SWITCH_TO_USER someuther'
part of the commenting-out construct.
Let's carry on:
SWITCH_TO_USER(){ { _u=$*;_x="$(declare;alias
shopt -p;set +o);"'set -- "${_a[@]}";unset _x _a';set +x;} 2>/dev/null
exec sudo -u "$1" env "_x=$_x" bash -c 'eval "$_x" 2> /dev/null;. "$0"
' "$0";}
Here's the definition of the SWITCH_TO_USER function. We'll see below that SWITCH_TO_USER will eventually be an alias wrapped around that function.
That function does the bulk of re-executing the script. In the end we see that it re-executes (in the same process because of exec
) bash
with the _x
variable in it's environment (we use env
here because sudo
usually sanitizes its environment and doesn't allow passing arbitrary env vars accross). That bash
evaluates the content of that $_x
variable as bash code and sources the script itself.
_x
is defined earlier as:
_x="$(declare;alias;shopt -p;set +o);"'set -- "${_a[@]}";unset _x _a'
All of the declare
, alias
, shopt -p
set +o
output make up a dump of the internal state of the shell. That is, they dump the definition of all variables, functions, aliases and options as shell code ready to be evaluated. On top of that, we add the setting of the positional parameters ($1
, $2
...) based on the value of the $_a
array (see below), and some clean up so that the huge $_x
variable doesn't stay in the environemnt for the remaining of the script.
You'll notice that the first part up to set +x
is wrapped in a command group whose stderr is redirected to /dev/null
({...} 2> /dev/null
). That's because, if at some point in the script set -x
(or set -o xtrace
) is run, we don't want that preamble to generate traces as we want to make it as little intrusive as possible. So we run a set +x
(after having made sure to dump the option (including xtrace
) settings beforehand) where the traces are sent to /dev/null.
The eval "$_X"
stderr is also redirected to /dev/null for similar reasons but also to avoid the errors about writing attempt to special read-only variables.
Let's carry on with the script:
alias skip=":||:<<'SWITCH_TO_USER $_u'"
That's our trick described above. On the initial script invocation, it will be cancelled (see below).
alias SWITCH_TO_USER="{ eval '"'_a=("$@")'"';} 2>/dev/null;SWITCH_TO_USER"
Now the alias wrapper around SWITCH_TO_USER. The main reason is to be able to pass the positional parameters ($1
, $2
...) to the new bash
that will interpret the rest of the script. We couldn't do it in the SWITCH_TO_USER
function because inside the function, "$@"
is the arguments to the functions, not those of the scripts. The stderr redirection to /dev/null is again to hide xtraces, and the eval
is to work around a bug in bash
. Then we call the SWITCH_TO_USER
function.
${_u+:} alias skip=:
That part cancels the skip
alias (replaces it with the :
no-op command) unless the $_u
variable is set.
skip
That's our skip
alias. On the first invocation, it will just be :
(the no-op). On subsequence re-invocations, it will be something like: :||: << 'SWITCH_TO_USER root'
.
echo test
a=foo
set a b
SWITCH_TO_USER root
So here, as an example, at that point, we reinvoke the script as the root
user, and the script will restore the saved state, and skip up to that SWITCH_TO_USER root
line and carry on.
What that means is that it has to be written exactly like stat, with SWITCH_TO_USER
at the beginning of the line and with exactly one space between arguments.
Most of the state, stdin, stdout and stderr will be preserved, but not the other file descriptors because sudo
typically closes them unless explicitely configured not to. So for instance:
exec 3> some-file
SWITCH_TO_USER bob
echo test >&3
will typically not work.
Also note that if you do:
SWITCH_TO_USER alice
SWITCH_TO_USER bob
SWITCH_TO_USER root
That only works if you have the right to sudo
as alice
and alice
has the right to sudo
as bob
, and bob
as root
.
So, in practice, that is not really useful. Using su
instead of sudo
(or a sudo
configuration where sudo
authenticates the target user instead of the caller) might make a little more sense, but that would still mean you'd need to know the passwords of all those guys.