Why can't we execute a list of commands as different user without sudo?

7,362

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 using sudo or su. 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 to root, 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.

Share:
7,362

Related videos on Youtube

oe.elvik
Author by

oe.elvik

Updated on September 18, 2022

Comments

  • oe.elvik
    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?

  • oe.elvik
    oe.elvik almost 11 years
    can you please explain what is your script doing?
  • Stéphane Chazelas
    Stéphane Chazelas almost 11 years
    @rag, you asked for it...
  • ott--
    ott-- almost 11 years
    Is that your entry for the ioshcc? You should have entered in just one line tho.