Why can't we execute a list of commands as different user without sudo?
Solution 1
The information is already in my other answer, but it's a bit buried there. So I thought, I'd add it here.
bash
doesn't have provision for changing users, but zsh
does.
In zsh
, you change users by assigning values to those variables:
-
$EUID
: change the effective user id (and nothing else). Usually, you can change your euid between your real userid and the saved set user id (if called from a setuid executable) or change to anything if your euid is 0. -
$UID
: change the effective user id, real user id and saved set user id to the new value. Unless that new value is 0, there's no coming back, as once all 3 have been set to the same value, there's no way to change it to anything else. -
$EGID
and$GID
: same thing but for group ids. -
$USERNAME
. That is like usingsudo
orsu
. It sets your euid, ruid, ssuid to the uid of that user. It also sets the egid, rgid and ssgid and supplementary groups based on the group memberships as defined in the user database. Like for$UID
, unless you set$USERNAME
toroot
, there's no coming back, but like for$UID
, you can change the user only for a subshell.
If you run these scripts as "root":
#! /bin/zsh -
UID=0 # make sure all our uids are 0
id -u # run a command as root
EUID=1000
id -u # run a command as uid 1000 (but the real user id is still 0
# so that command would be able to change its euid to that.
# As for the gids, we only have those we had initially, so
# if started as "sudo the-script", only the groups root is a
# member of.
EUID=0 # we're allowed to do that because our ruid is 0. We need to do
# that because as a non-priviledged user, we can't set our euid
# to anything else.
EUID=1001 # now we can change our euid since we're superuser again.
id -u # same as above
Now, to change user as in sudo
or su
, we can only do it by using subshells, otherwise we could only do it once:
#! /bin/zsh -
id -u # run as root
(
USERNAME=rag
# that's a subshell running as "rag"
id # see all relevant group memberships are applied
)
# now back to the parent shell process running as root
(
USERNAME=stephane
# another subshell this time running as "stephane"
id
)
Solution 2
Kernel offers man 2 setuid
and friends.
Now, it works on calling process. More importantly you can't elevate your privileges. That's why su
and sudo
have SETUID bit set so they run always with highest privileges (root
) and drop to desired user accordingly.
That combination means that you can't change the shell UID by running some other program to do that (that's why you can't expect sudo
or any other program to do that) and you can't (as shell) do it yourself unless you are willing to run as root or the same user you are wanting to switch to which doesn't make sense. Moreover once you drop privileges, there's no way back.
Solution 3
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.
Related videos on Youtube
oe.elvik
Updated on September 18, 2022Comments
-
oe.elvik over 1 year
This question has been asked in a different way in other forums. But there has not been a decent explanation why you can't do the below in bash.
#!/bin/bash command1 SWITCH_USER_TO rag command2 command3
Usually, the suggested way is
#!/bin/bash command1 sudo -u rag command2 sudo -u rag command3
but why is it not possible in
bash
to change to a different user at some point during the bash script execution and execute the rest of the commands as a different user?-
Admin almost 11 yearsThis question was also asked on Superuser, at superuser.com/questions/93385/…, and this answer superuser.com/a/573882/76251 provides a HERE DOC -like method for changing users within a script.
-
-
oe.elvik almost 11 yearscan you please explain what is your script doing?
-
Stéphane Chazelas almost 11 years@rag, you asked for it...
-
ott-- almost 11 yearsIs that your entry for the
ioshcc
? You should have entered in just one line tho.