Bash can seem pretty random and weird at times, but most of what people see as quirks have very logical (if not very good) explanations behind them. This series of posts looks at some of them.
Why can't bash scripts be SUID?
Bash scripts can’t run with the suid bit set. First of all, Linux doesn’t allow any scripts to be setuid, though some other OS do. Second, bash will detect being run as setuid, and immediately drop the privileges.
This is because shell script security is extremely dependent on the environment, much more so than regular C apps.
Take this script, for example, addmaildomain:
#!/bin/sh
[[ $1 ]] || { man -P cat $0; exit 1; }
if grep -q "^$(whoami)\$" /etc/accesslist
then
echo "$1" > /etc/mail/local-host-names
else
echo "You don't have permissions to add hostnames"
fi
The intention is to allow users in /etc/accesslist to run addmaildomain example.com
to write new names to local-host-names, the file which defines which domains sendmail should accept mail for.
Let’s imagine it runs as suid root. What can we do to abuse it?
We can start by setting the path:
echo "rm -rf /" > ~/hax/grep && chmod a+x ~/hax/grep
PATH=~/hax addmaildomain
Now the script will run our grep instead of the system grep, and we have full root access.
Let’s assume the author was aware of that, had set PATH=/bin:/usr/bin
as the first line in the script. What can we do now?
We can override a library used by grep
gcc -shared -o libc.so.6 myEvilLib.c
LD_LIBRARY_PATH=. addmaildomain
When grep is invoked, it’ll link with our library and run our evil code.
Ok, so let’s say LD_LIBRARY_PATH is closed up.
If the shell is statically linked, we can set LD_TRACE_LOADED_OBJECTS=true. This will cause dynamically linked executables to print out a list of library dependencies and return true. This would cause our grep to always return true, subverting the test. The rest is builtin and wouldn’t be affected.
Even if the shell is statically compiled, all variables starting with LD_* will typically be stripped by the kernel for suid executables anyways.
There is a delay between the kernel starting the interpretter, and the interpretter opening the file. We can try to race it:
while true
do
ln /usr/bin/addmaildomain foo
nice -n 20 ./foo &
echo 'rm -rf /' > foo
done
But let’s assume the OS uses a /dev/fd/* mechanism for passing a fd, instead of passing the file name.
We can rename the script to confuse the interpretter:
ln /usr/bin/addmaildomain ./-i
PATH=.
-i
Now we’ve created a link, which retains suid, and named it “-i”. When running it, the interpretter will run as “/bin/sh -i”, giving us an interactive shell.
So let’s assume we actually had “#!/bin/sh –” to prevent the script from being interpretted as an option.
If we don’t know how to use the command, it helpfully lists info from the man page for us. We can compile a C app that executes the script with “$0” containing “-P /hax/evil ls”, and then man will execute our evil program instead of cat.
So let’s say “$0” is quoted. We can still set MANOPT=-H/hax/evil.
Several of these attacks were based on the inclusion of ‘man’. Is this a particularly vulnerable command?
Perhaps a bit, but a whole lot of apps can be affected by the environment in more and less dramatic ways.
- POSIXLY_CORRECT can make some apps fail or change their output
- LANG/LC_ALL can thwart interpretation of output
- LC_CTYPE and LC_COLLATE can modify string comparisons
- Some apps rely on HOME and USER
- Various runtimes have their own paths, like RUBY_PATH, LUA_PATH and PYTHONPATH
- Many utilities have variables for adding default options, like RUBYOPT and MANOPT
- Tools invoke EDITOR, VISUAL and PAGER under various circumstances
So yes, it’s better not to write suid shell scripts. Sudo is better than you at running commands safely.
Do remember that a script can invoke itself with sudo, if need be, for a simulated suid feel.
So wait, can’t perl scripts be suid?
They can indeed, but there the interpretter will run as the normal user and detect that the file is suid. It will then run a suid interpretter to deal with it.