Run command from sudo su

1

0

On a machine I (tlous) have been given access to open a shell as another user (serviceAccount)

Executing: sudo su - serviceAccount has the desired effect of opening a shell as this serviceAccount user. So far so good.

This is nice, but I want to run a command as this user without opening a shell.

Let's say the command is whoami

I've tried:

sudo -u serviceAccount whoami

Sorry, user tlous is not allowed to execute '/usr/bin/whoami' as serviceAccount ...

sudo su - serviceAccount -c whoami

Sorry, user tlous is not allowed to execute '/bin/su - serviceAccount -c whoami' ...

And other variations. What am I missing? Can this be done in a oneliner? The reason is that I actually want to run this as an ssh command: ssh -t tlous@server.example.com sudo su - serviceAccount -c whoami

Tom Lous

Posted 2019-04-12T07:13:47.390

Reputation: 113

Answers

1

Probably the sudoers file on the remote side specifies the exact command you can run and it's su - serviceAccount. One or more additional options (like -c) will make the command not qualify. Any solution should use the exact su - serviceAccount command.

There is a way to do something about it, but it's not perfect. Start by running this on the server:

echo whoami | sudo su - serviceAccount

(sudo without -S will use the terminal to ask for your password (if needed) despite the redirection).

To make the approach more general, create a script on the server:

#!/bin/sh
printf '%s ' "@" | sudo su - serviceAccount

Save it as su-ser, and make it executable. Now you can test these:

./su-ser whoami
./su-ser 'whoami; whoami'
./su-ser ls -l
./su-ser 'ls -l'

Note these quotes don't propagate to the shell spawned by su. Each time printf builds a command from arguments that the script gets, this is passed as a string and then parsed by the final shell, with word splitting, globbing and such. This means all these

./su-ser echo 1 2 3
./su-ser echo "1 2             3"
./su-ser "echo 1 2             3"

will print 1 2 3. To pass a quoted string you need to quote quotes, e.g.:

./su-ser 'echo "1 2             3"'

The next step is to trigger this remote script from your local computer:

ssh -t tlous@server.example.com '"/path/to/su-ser" whoami'

You need -t in case sudo asks for the password. From the ssh command in your question body I can tell you probably understand this.

Note ssh also builds a command string. The above quoting would work even if /path/to/su-ser contained spaces. On the other hand, if the path is without spaces (and without characters like ;, *), a totally unquoted ssh … /path/to/su-ser whoami will work as well.

Proper quoting is not a trivial task at this point. Let's analyze what happens to the command.

  1. At first your local shell parses the string. It uses the most outer quotes (if any) to parse it right.
  2. ssh gets its arguments as an array. The quotes used up by the shell don't get this far.
  3. ssh decides which arguments should be passed to the remote side and builds a string (in similar manner to printf in our remote script).
  4. This string is then parsed by the remote shell (the one spawned by sshd on the server). The remote shell uses the (now) most outer quotes (if any) to parse it right.
  5. su-ser gets its arguments as an array. The quotes used up by the remote shell don't get this far.
  6. su-ser builds a string.
  7. This string is then parsed by yet another shell (spawned by su on the server). The shell uses the (now) most outer quotes (if any) to parse it right.
  8. The command(s) you passed gets its arguments as an array. The quotes used up by the last shell don't get this far.

This means sometimes you need three levels of quotes to get the right result. An uncomplicated local command like

echo "1 2             3"

becomes

ssh -t tlous@server.example.com "'/path/to/su-ser' 'echo \"1 2             3\"'"

where the unescaped double-quotes get stripped by the local shell, the single-quotes get stripped by the remote shell; finally the escaped double-quotes (that in fact become unescaped as soon as the local shell strips the unescaped ones) tell the shell spawned by su that the string with multiple spaces is a single argument to echo. This way the output is

1 2             3

Another problem is the remote script passes commands to stdin of the final shell, so you can't easily use its stdin for another purpose. E.g. cat; whoami (try it) should print back whatever lines you type (terminate it with Ctrl+D), then the output of whoami. However if you run this on the server:

./su-ser 'cat; whoami'

cat will terminate immediately. This should work:

./su-ser 'cat /dev/tty; whoami'

(Again, use Ctrl+D to terminate cat). Now compare the behavior of this:

./su-ser '
whoami
whoami
whoami
'

to this:

./su-ser '
whoami
cat
whoami
'

The same stream feeds cat and the shell; it's not a good thing. Establishing separate channels for actual commands and the real stdin may not be easy because sudo by default closes additional file descriptors and sets a new, minimal environment. I guess a contraption using temporary files/fifos would work, but this is so hairy I won't even try.

So the script may or may not be enough for you; it depends on commands you need to run. Now you know a couple of its limitations. Good luck.

Kamil Maciorowski

Posted 2019-04-12T07:13:47.390

Reputation: 38 429

Exactly what I needed. Thanks for the thorough explanation! – Tom Lous – 2019-04-16T14:46:57.130