How can I allow bash -c for sudoers (followed by multiple commands)? - bash

I have this in /etc/sudoers:
%wheel myhostname =NOPASSWD: /bin/bash -c "echo foo && echo bar", \
/bin/bash -c echo foo
Executing sudo /bin/bash -c echo foo works without being prompted for a password.
However, sudo /bin/bash -c "echo foo && echo bar" still asks for a password.
I've tried many variations to this, but nothing is being accepted.
Where's the error? / How can I allow -c followed by multiple commands?

The problem is in how your shell interprets arguments. If I am in bash (most other shells work the same way), and I type the command
sudo /bin/bash -c "echo foo && echo bar"
sudo is invoked with everything after it as arguments. However, the shell processes each argument before passing it in to sudo. One of the things it does is remove quotes around quoted arguments. Therefore, the arguments that sudo gets as its argv value are an array that looks like this (one argument per line):
/bin/bash
-c
echo foo && echo bar
sudo combines these with spaces and compares that to the commands in the sudoers file (it is actually a bit more complicated than this since it does wildcard replacement, etc.). Thus, the command it actually sees you executing, for the purposes of checking permissions is
/bin/bash -c echo foo && echo bar
When I put that command in my sudoers file, I am not prompted for a password when I enter
sudo /bin/bash -c "echo foo && echo bar"
However I am also not prompted for a password when I enter any of these commands or other like them.
sudo /bin/bash "-c echo foo && echo bar"
sudo /bin/bash "-c echo" foo "&& echo" bar
sudo /bin/bash -c echo "foo && echo" bar
In general, as far as I know, there is no way for sudo (or any program) to know exactly what command got entered, only what it gets converted to by the shell for execution purposes.

At least with my sudo (OS X 10.9, sudo 1.7.10p7), quote marks in the /etc/sudoers are matched literally. That is, specifying
/bin/bash -c "echo foo && echo bar"
means that the users literally have to run
sudo /bin/bash -c '"echo foo && echo bar"'
i.e. the quote marks have to be passed to the program.
Therefore, all you have to do is just drop the quote marks in /etc/sudoers:
%wheel myhostname =NOPASSWD: /bin/bash -c echo foo && echo bar
While this looks kind of weird, it completely works on my machine: users can execute /bin/bash -c "echo foo && echo bar" without a password. This works because, according to man sudoers, the only characters that must be escaped are ',', ':', '=' and '\'.
Note that this implies that sudo is basically concatenating all the command-line args together with only spaces to determine a match: users can also execute /bin/bash -c "echo foo" "&&" "echo bar". Therefore, you must take care that none of the arguments could individually be a security risk (e.g. that foo, && and bar aren't things that could be used to exploit your computer).

Related

How can I request elevated permissions in a bash script's begin and let it go at the end?

I have a script (myscript.sh) which runs a few commands which need elevated privileges (i.e. needs to run with sudo).
Script is quite complex, but to demonstrate it is like below:
#!/bin/bash
echo "hello"
command1_which_needs_sudo
echo "hello2"
command2_which_needs_sudo
echo "hello3"
...
If I run it as a normal user without the required privileges:
$ ./myscript.sh
hello
must be super-user to perform this action
However if I run it with the correct privileges, it will work fine:
$ sudo ./myscript.sh
hello
hello2
hello3
Can I somehow achieve to run myscript.sh without sudo, and make the script requesting the elevated privileges only once in the beginning (and pass it back once it has finished)?
So obviously, sudo command1_which_needs_sudo will not be good, as command2 also need privileges.
How can I do this if I don't want to create another file, and due to script complexity I also don't want to do this with heredoc syntax?
If your main concern is code clarity, using wrapper functions can do a lot of good.
# call any named bash function under sudo with arbitrary arguments
run_escalated_function() {
local function_name args_q
function_name=$1; shift || return
printf -v args_q '%q ' "$#"
sudo bash -c "$(declare -f "$function_name"); $function_name $args_q"
}
privileged_bits() {
command1_which_needs_sudo
echo "hello2"
command2_which_needs_sudo
}
echo "hello"
run_escalated_function privileged_bits
echo "hello3"
If you want to run the script with root privileges without having to type sudo in the terminal nor having to type the password more than once then you can use:
#!/bin/bash
if [ "$EUID" -ne 0 ]
then
exec sudo -s "$0" "$#"
fi
echo "hello"
command1_which_needs_sudo
echo "hello2"
command2_which_needs_sudo
echo "hello3"
# ...
sudo -k
Update:
If your goal is to execute one part of the script with sudo rights then using a quoted here‑document is probably the easiest solution; there won't be any syntax issues because the current shell won't expand anything in it.
#!/bin/bash
echo "hello"
sudo -s var="hello2" <<'END_OF_SUDO'
command1_which_needs_sudo
echo "$var"
command2_which_needs_sudo
END_OF_SUDO
sudo -k
echo "hello3"
#...
remark: take notice that you can use external values in the here-document script by setting varname=value in the sudo command.

call a bash function as root but gives 'unexpected end of file'

Why does the last example throw an error but the others work? Bash is invoked in any case.
#!/bin/bash
function hello {
echo "Hello! user=$USER, uid=$UID, home=$HOME";
}
# Test that it works.
hello
# ok
bash -c "$(declare -f hello); hello"
# ok
sudo su $USER bash -c "$(declare -f hello); hello"
# error: bash: -c: line 1: syntax error: unexpected end of file
sudo -i -u $USER bash -c "$(declare -f hello); hello"
It fail because of the -i or --login switch:
It seems like when debugging with -x
$ set -x
$ sudo -i -u $USER bash -c "$(declare -f hello); hello"
++ declare -f hello
+ sudo -i -u lea bash -c 'hello ()
{
echo "Hello! user=$USER, uid=$UID, home=$HOME"
}; hello'
Now if doing it manually it cause the same error:
sudo -i -u lea bash -c 'hello ()
{
echo "Hello! user=$USER, uid=$UID, home=$HOME"
}; hello'
Now lets just do a simple tiny change that make it work:
sudo -i -u lea bash -c 'hello ()
{
echo "Hello! user=$USER, uid=$UID, home=$HOME";}; hello'
The reason is that sudo -i runs everything like an interactive shell. And when doing so, every newline character from the declare -f hello is internally turned into space. The curly-brace code block need a semi-colon before the closing curly-brace when on the same line, which declare -f funcname does not provide since it expands the function source with closing curly brace at a new line.
Now lets make this behaviour very straightforward:
$ sudo bash -c 'echo hello
echo world'
hello
world
It executes both echo statements because they are separated by a newline.
but:
$ sudo -i bash -c 'echo hello
echo world'
hello echo world
It executes the first echo statement that prints everything as arguments because the newline has been replaced by a space.
It is the same code in all examples, so it should be ok.
Yes, "$(declare -f hello); hello" is always the same string. But it is processed differently by sudo su and sudo -i as found out by Lea Gris .
sudo -i quotes its arguments before passing them to bash. This quoting process seems to be undocumented and very poor. To see what was actually executed, you can print the argument passed to bash -c inside your ~/.bash_profile/:
Content of ~/.bash_profile
cat <<EOF
# is executed as
$BASH_EXECUTION_STRING
# resulting in output
EOF
Some examples of sudo -i's terrible and inconsistent quoting
Linebreaks are replaced by line continuations
$ sudo -u $USER -i echo '1
2'
# is executed as
echo 1\
2
# resulting in output
12
Quotes are escaped as literals
$ sudo -u $USER -i echo \'single\' \"double\"
# is executed as
echo \'single\' \"double\"
# resulting in output
'single' "double"
But $ is not quoted
$ sudo -u $USER -i echo \$var
# is executed as
echo $var
# resulting in output
Some side notes:
There might be a misunderstanding in your usage of su.
sudo su $USER bash -c "some command"
does not execute bash -c "echo 1; echo 2". The -c ... is interpreted by su and passed as -c ... to $USER's default shell. Afterwards, the remaining arguments are passed to that shell as well. The executed command is
defaultShellOfUSER -c "some command" bash
You probably wanted to write
sudo su -s bash -c "some command" "$USER"
Interactive shells behave differently
su just executes the command specified by -c. But sudo -i starts a login shell, in your case that login shell seems to be bash (this is not necessarily the case, see section above).
An interactive bash session behaves different from bash -c "..." or bash script.sh. An interactive bash sources files like .profile, .bash_profile, and enables history expansion, aliases, and so on. For a full list see the section Interactive Shell Behavior in bash's manual.

First echo missing when using bash -c over SSH

While debugging a script that runs various commands remotely, I noticed some problems getting output from echo.
I realize that the bash -c isn't necessary here, but it still has me wondering.
In my shell:
> bash -c "echo hello && echo hi"
hello
hi
But, if I bring SSH into the picture:
> ssh ${myhost} bash -c "echo hello && echo hi"
hi
Yet, date outputs, even though that first echo didn't:
> ssh ${myhost} bash -c "date && echo hi"
Thu Jun 3 21:15:26 UTC 2021
hi
What's going on here?
When you run a command via ssh like this, it's parsed twice: first on the local computer (before it's passed to the ssh command as arguments), then again on the remote computer before it's actually executed. Each time it's parsed, the shell will apply and remove quotes and escapes. That means the double-quotes you have around the command get applied and removed by the local shell, before the command is sent to the remote shell. So what looks like this command:
bash -c "echo hello && echo hi"
Turns into this by the time the remote shell sees it:
bash -c echo hello && echo hi
...which is two separate commands, bash -c echo hello and echo hi. The second one, echo hi, works as you expect, but the first may not.
With bash -c, the argument immediately after that is taken as the command string to execute, and any further arguments are assigned to $0, $1, etc as it runs. So bash -c echo hello just runs echo with $0 set to "hello". So it prints a blank line.
If you want the command to be executed as you expect, you need two layers of quotes and/or escapes, one to be applied and removed by the local shell and another to be applied and removed by the remote shell. Any of these will work:
# Single-quotes for local shell, double for remote
ssh ${myhost} 'bash -c "echo hello && echo hi"'
# Double-quotes for local shell, single for remote
ssh ${myhost} "bash -c 'echo hello && echo hi'"
# Double-quotes for local shell, escaped doubles for remote
ssh ${myhost} "bash -c \"echo hello && echo hi\""
# Single-quotes for local shell, escaped characters for remote
ssh ${myhost} 'bash -c echo\ hello\ \&\&\ echo\ hi'
...and many more possibilities. Note that if the command string contains anything like variable references or command substitutions, you need to pay attention to whether you want them to expand on the local or remote computer, and make sure the quoting/escaping method you use accomplishes that.
BTW, in this case since you're running a command with bash -c, there's actually a third layer of parsing done by the shell invoked by bash -c. If that command has anything that needed quoting/escaping, keeping the levels straight will be even more complex.
The command that arrives to the server is : bash -c echo hello && echo hi
ie without quote
and if you run this cde locally, it produces the same output
If you want the good result
ssh mm 'bash -c "echo hello && echo hi"'
Could also try using ";" as a separator
ssh ${myserver} "echo hello; echo hi"
testuser#mymac ~ % ssh ${myserver} "echo hello; echo hi"
hello
hi
testuser#mymac ~ %

Pass a script with arguments via stdin as a string to the bash

Currently I'm having an issue to pass a script with arguments to the bash. The limitation is that I can pass them as a string (see $COMMAND)
ssh user#server 'bash -s' < "$COMMAND"
if my $COMMAND contains "foo.sh bar baz", I would get error no such file or directory: foo.sh bar baz
Additionally, I'm be able to set additional options to bash.
Thank you in advance!!!
The root problem is that < takes a filename to redirect from, not a filename and some arguments. Mixing the two together this way just won't work.
If I'm understanding right, you want to run a script that exists on the local computer, but have it execute (with the supplied arguments) on the remote computer. In that case, what you want to run is something like this:
ssh user#server 'bash -s bar baz' <"foo.sh"
Note that the arguments are passed in a completely different place than the script filename. If possible, I'd recommend never mixing the two; keep them in separate variables, and then use something like this:
ssh user#server "bash -s $scriptargs" <"$scriptfile"
From the sounds of the error, foo.sh doesn't exist on server, so it fails.
The simplest solution seems to be:
ssh user#server 'foo.sh bar baz', provided "foo.sh" exists on the server.
If "foo.sh" doesn't/can't exist on the server, have you tried using here docs? For example:
ssh user#server "cat > foo.sh && chmod u+x foo.sh && ./foo.sh bar baz" << "EOF"
`heredoc> #Put your contents of foo.sh here
`heredoc> #And put them here
`heredoc> echo "Argument 1: $1"
`heredoc> echo "Argument 2: $2"
`heredoc> EOF
user#server's password:
Argument 1: bar
Argument 2: baz
EDIT:
david#localhost ~ % bash -s < test.sh David Finder
Hello David
Hello Finder
Edit to my previous answer, the issue is due to the quoting of the input arguments thrown to Bash, as it sees the entire input as one file (it's looking for a file literally called "foo.sh bar baz", with spaces within.

Why is this bash variable empty when I just set it?

$ sudo sh -c "FOO=bar echo Result:${FOO}"
Result:
Why is the value stored in FOO not displayed?
Because bash replaces ${FOO} before calling sudo. So what sh -c actually sees is:
FOO=bar echo Result:
Besides, even if you tried
FOO=bar echo Result:${FOO}
It still won't work1. To get that right, you can do:
FOO=bar; echo Result:${FOO}
Now that that is fixed, let's get back to sh -c. To prevent bash from interpreting the string you are giving to sh -c, simply put it in ' instead of ":
sudo sh -c 'FOO=bar; echo Result:${FOO}'
1 Refer to the comments for reason.
This doesn't work, because the variable FOO is set for the following command, echo, but the ${FOO} is replaced by the enclosing shell.
If you want it to work, you must set the variable FOO for the enclosing shell and wrap the echo ... in single quotes
sudo FOO=bar sh -c 'echo Result:${FOO}'

Resources