Run a subshell as root - bash

Consider you have a Linux/UNIX machine with Bash. You have a file secret.txt that only root can read. You want to use a command that takes a string as an argument, say,
sample-command <string>
Log in as a root user and run the command using the first line of the text file:
root ~ $ sample-command $(sed '1!d' secret.txt)
Can this be done by non-root, sudoer users?
Note. sudo sh -c "<command>" doesn't help since subshells don't carry over the root/sudo privilege. For example,
sarah ~ $ sudo sh -c "echo $(whoami)"
gives you sarah, not root.

Expansions like command substitution will be processed by the shell before executing the actual command line:
sudo sh -c "echo $(whoami)"
foouser
Here the shell will first run whoami, as the current user, replace the expansion by it's result and then execute
sudo sh -c "echo foouser"
Expansions doesn't happen within single quotes:
sudo sh -c 'echo "$(whoami)"'
root
In this example $(whoami) won't get processed by calling shell because it appears within single quotes. $(whoami) will therefore get expanded by subshell before calling echo.

Related

Why sudo -H needs an extra command?

I found this related question What does sudo -H do?
that demonstrates the usage of -H with
sudo -H bash -c 'echo $HOME $USER'
But I don't understand is why is bash necessary, because the echo command can stand on its own, but running
sudo -H echo $HOME $USER
will display instead current user variables (and not root's).
So why is (in this case) bash needed for the commands ran as sudo arguments to take in the -H flag?
This is also true if one specifies another user, like sudo -H -u anotheruser echo $HOME $USER.
When you run the command:
sudo -H echo $HOME $USER
the variables $HOME and $USER are evaluated by the current shell. They are replaced with their values and the command line adjusted this way is passed to sudo.
F.e. if your username is foo and your home is /home/foo, the command above is the same thing as:
sudo -H echo /home/foo foo
It's not necessarily bash that is required to get what you want. It's required a way to evaluate the environment variables by the command that is executed by sudo, not by the current shell.
When you run:
sudo -H bash -c 'echo $HOME $USER'
the $HOME and $USER environment variables are not evaluated by the current shell, because they are enclosed in single quotes.
sudo receives the command line as it is written here (it's fourth argument is the string echo $HOME $USER as a single word). It then launches bash with two arguments: -c and the string mentioned above.
bash interprets -c as "execute a command" that is provided as the next argument. It then tries to run echo $HOME $USER (notice that the variables are not enclosed in quotes here) and, before running echo it replaces $HOME and $USER with their values (that belong to the new user now).
Running bash (or any other shell) is needed to do the variables expansion in the execution environment of the new user.
$HOME and $USER are expanded by the shell before sudo even runs. You need bash -c to add a layer of indirection, so that the shell run by sudo does the expansion.

How do you stop bash from stripping quotes when running a variable as a command?

I have a command stored in a variable, which I then run:
[root#cxpxwly01wel001 init.d]# cat test
#!/bin/bash
command="su - root -c 'java -Xms16m -the_rest_of_my_java_command'"
echo $command
$command
[root#cxpxwly01wel001 init.d]# ./test
su - root -c 'java -Xms16m -the_rest_of_my_java_command'
su: invalid option -- 'X'
Try `su --help' for more information.
The su error is what you get if the single quotes aren't there:
[root#cxpxwly01wel001 init.d]# su - root -c java -Xms16m -the_rest_of_my_java_command
su: invalid option -- 'X'
Try `su --help' for more information.
With the single quotes there the command works as expected:
[root#cxpxwly01wel001 init.d]# su - root -c 'java -Xms16m -the_rest_of_my_java_command'
-bash: java: command not found
Which suggests to me that when running the variable as a command the single quotes aren't preserved. How do I preserve them?
Note: I'm editing an existing script (inserting the su - around the java command), so I'd like to change as little as possible to keep it close to the source.
Let me add the output when run with set -x:
+ command='su - root -c '\''java -Xms16m -the_rest_of_my_java_command'\'''
+ echo su - root -c ''\''java' -Xms16m '-the_rest_of_my_java_command'\'''
su - root -c 'java -Xms16m -the_rest_of_my_java_command'
+ su - root -c ''\''java' -Xms16m '-the_rest_of_my_java_command'\'''
su: invalid option -- 'X'
Usage: su [options] [LOGIN]
Options:
-c, --command COMMAND pass COMMAND to the invoked shell
-h, --help display this help message and exit
-, -l, --login make the shell a login shell
-m, -p,
--preserve-environment do not reset environment variables, and
keep the same shell
-s, --shell SHELL use SHELL instead of the default in passwd
The technique I use to avoid the "quoting inside quotes" issues is to build the command in an array. For instance, instead of :
command="su - root -c 'java -Xms16m -the_rest_of_my_java_command'"
I would use :
declare -a command=(su - root -c 'java -Xms16m -the_rest_of_my_java_command')
You can still echo the command (with an array expansion) :
echo "${command[#]}"
Or run it :
"${command[#]}"
The double quotes around the array expansion are important : they tell the shell to expand items without doing any word splitting, so an item containing whitespace will keep its identity as a single string.
By doing it this way, the last argument, which is inside quotes, will remain a single string, and the su command will not see -Xms16m as an (invalid) option.
As an aside, the [#] in "${command[#]}" means "all elements in the array". Whereas, in an expression like "${command[1]}", [1] would mean "only element at index 1".

Echo variable using sudo bash -c 'echo $myVariable' - bash script

I want to echo a string into the /etc/hosts file. The string is stored in a variable called $myString.
When I run the following code the echo is empty:
finalString="Hello\nWorld"
sudo bash -c 'echo -e "$finalString"'
What am I doing wrong?
You're not exporting the variable into the environment so that it can be picked up by subprocesses.
You haven't told sudo to preserve the environment.
\
finalString="Hello\nWorld"
export finalString
sudo -E bash -c 'echo -e "$finalString"'
Alternatively, you can have the current shell substitute instead:
finalString="Hello\nWorld"
sudo bash -c 'echo -e "'"$finalString"'"'
You can do this:
bash -c "echo -e '$finalString'"
i.e using double quote to pass argument to the subshell, thus the variable ($finalString) is expanded (by the current shell) as expected.
Though I would recommend not using the -e flag with echo. Instead you can just do:
finalString="Hello
World"

Why can't I redirect text to a text file?

I'm writing a bash shell script that has to be run with admin permissions (sudo).
I'm running the following commands
sudo -u $SUDO_USER touch /home/$SUDO_USER/.kde/share/config/kcmfonts > /dev/null
sudo -u $SUDO_USER echo "[General]\ndontChangeAASettings=true\nforceFontDPI=96" >> /home/$SUDO_USER/.kde/share/config/kcmfonts
The first command succeeds and creates the file. However the second command keeps erroring with the following:
cannot create /home/username/.kde/share/config/kcmfonts: Permission denied
I can't understand why this keeps erroring on permissions. I'm running the command as the user who invoked sudo so I should have access to write to this file. The kcmfonts file is created successfully.
Can someone help me out?
Consider doing this:
echo "some text" | sudo -u $SUDO_USER tee -a /home/$SUDO_USER/filename
The tee command can assist you with directing the output to the file. tee's -a option is for append (like >>) without it you'll clobber the file (like >).
You don't need to execute the left side with elevated privs (although it is just echo, this is a good thing to form as a habit), you only need the elevated privs for writing to the file. So with this command you're only elevating permissions for tee.
sudo -u $SUDO_USER echo "some text" >> /home/$SUDO_USER/filename
sudo executes the command echo "some text" as `$SUDO_USER".
But the redirection is done under your account, not under the $SUDO_USER account. Redirection is handled by the shell process, which is yours and is not under the control of sudo.
Try this:
sudo -u $SUDO_USER sh -c 'echo "some text" >> /home/$SUDO_USER/filename'
That way, the sh process will be executed by $SUDO_USER, and that's the process that will handle the redirection (and will write to the output file).
Depending on the complexity of the command, you may need to play some games with escaping quotation marks and other special characters. If that's too complex (which it may well be), you can create a script:
$ cat foo.sh
#!/bin/sh
echo "some text" >> /home/$SUDO_USER/filename
$ sudo -u $SUDO_USER ./foo.sh
Now it's the ./foo.sh command (which executes as /bin/sh ./foo.sh) that will run under the $SUDO_USER account, and it should have permission to write to the output file.

How to specify zeroeth argument

I'm writing a bash script that starts the tcsh interpreter as a login shell and has it execute my_command. The tcsh man page says that there are two ways to start a login shell. The first is to use /bin/tcsh -l with no other arguments. Not an option, because I need the shell to execute my_command. The second is to specify a dash (-) as the zeroeth argument.
Now the bash exec command with the -l option does exactly this, and in fact the following works perfectly:
#!/bin/bash
exec -l /bin/tcsh -c my_command
Except... I can't use exec because I need the script to come back and do some other things afterwards! So how can I specify - as the zeroeth argument to /bin/tcsh without using exec?
You can enclose the exec command into a sub-shell of your script.
#!/bin/bash
(exec -l /bin/tcsh -c my_command)
# ... whatever else you need to do after the command is done
You can write a wrapper (w.sh) script that contains:
#!/bin/bash
exec -l /bin/tcsh -c my_command
and execute w.sh in your main script.

Resources