When executable shell scripts were first introduced they honored the suid bit. This was a disaster for security. Consider a script call /usr/sbin/disaster. And we did:
chown root /usr/sbin/disaster
chmod 4755 /usr/sbin/disaster
Naturally, /, /usr, /usr/sbin are only writable by root. And the script itself is just:
#! /bin/sh
That's right. The script is a single line with no hidden characters. The shell will ignore it because it is a comment and then exit because there are no other lines. This script is already insecure and is vulnerable to two different attacks.
Attack 1: link to -i
If we do:
ln -s /usr/sbin/disaster ./-i
and arrange to execute our new symbolic link called "-i". If we execute the real /usr/sbin/disaster, we do the equivalent of:
execl("/bin/sh", "/usr/sbin/disaster", "/usr/sbin/disaster", (char *) NULL)
No problem there. That is what we expected. But when we run copy special link called "-i" we do:
execl("/bin/sh", "-i", "-i", (char *) NULL)
This causes the suid shell to behave as an interactive shell! The shell was quickly modified to accept a single hyphen as the end of switch setting arguments. So we modify our one line script to read:
#! /bin/sh -
Now the same trick would result in:
execl("/bin/sh", "-i", "-", "-i", (char *) NULL)
Augument zero is still "-i", but this is harmless. It might even be considered useful because it makes this attack more noticable with the ps command.
Attack 2: changing the link
This one is harder to explain. As before, we make a symbolic link to the script under attack, but this time the name does not matter. Then we run the script via our new symbolic link. Then we
quickly change the symbolic link to point to an evil script. We want to change what the symbolic link points to after the kernel opens it but before the interpreter opens it. This is a race condition and it will not work every time. It also requires a bit of clever programming to pull this one off. But done correctly, we will have our evil script running as root.
Suid Scripts Disabled
Because it was not possible to write a secure suid shell script, the concept of suid shell scripts was removed from Unix. Around this time the program sudo was written and this largely oviated the need for suid shell scripts. I don't believe any version of unix released in the past 15 years has these problems. (And if I did, I would not have discussed these attacks.
) By now I hope you can see why many old time system admins (such as myself) still have a dim view of suid shell scripts.
The Return of Suid Scripts
Solaris now supports suid shell scripts but it is immune to these attacks. It does this by ensuring that the script is opened only once in the case of a suid script. If the suid bit is set, Solaris uses the fd filesystem to pass the script to the interpreter. Had my perlargs script been suid on Solaris, it would have been run something like:
execl("/usr/local/bin/perl", "/usr/local/bin/perl", "-w", "/dev/fd/3", "one", "two", "three", (char *) NULL)
and when perl opened /dev/fd/3, it just gets another open file descripter pointing to the same file as whatever has been opened as fd 3. Note that inside the script, there is no way to obtain the name other than /dev/fd/3.
Because the name used is concealed from the intrepreter, there is no harm if that name was something odd like "-i". Because the script is opened one time, there is no harm if a symbolic link is suddenly switched to an evil script.
But note that this finally gets us to a stage where a one line script containing only a comment can be safely run. There can still be other security problems with a poorly written suid script. If you must use suid shell scripts, here are a few tips:
1. Use ksh
Dave Korn analysed all of the attacks on shell scripts and closed as many holes as he could. In particular, ksh is immune to IFS based attacks. Also if it finds that it is going interactive with an effective uid of root together with a real uid which is not root, it will use its root authority to set the effective uid to the real uid prior to issuing a prompt.
2. Control PATH
Explicitly set your PATH variable as the first step in your script. Make the list of directories as short as possible. Do not start or end the list with a colon or have two consecutive colons. And do not put . or .. in the list. Explicitly export PATH. And ensure that every directory mentioned in PATH is writable only by root. For example, /usr/local/bin is on the PATH, then in addition to the obvious need for /usr/local/bin being non-writable, you also need to ensure that /, /usr, /usr/local are all not be writable. If /usr/local is a mounted filesystem, it must not be possible for a non-root user to arrange for /usr/local to be unmounted. You can further protect your script by relying on PATH as little as possible. So, for example, use "/usr/bin/rm" rather than just "rm".
Note: while I used /usr/local/bin as an example, I would strongly resist putting /usr/local/bin in the PATH of a suid script. Again: make the list of directories as short as possible. I would rarely go beyond "PATH=/usr/bin" in a suid script.
3. Control IFS
Set IFS to a space, a tab, and a newline. And then export IFS. While ksh is immune to the IFS attack, some other shells are not immune. Something you do in your script may indirectly invoke another shell.
4. Make sure that the script is not writable
Most versions of unix these days will remove the suid bit on a file as it is being written by a process owned by a user different than the owner of the file. But don't depend on that behavior.
5. Do not execute directly or indirectly any user supplied input
You must ensure that user input is never delivered to the shell to interpret as executable code. If you don't know how to ensure this, then you should not process any user input. Note that the parameters of the script must be considered user input.
6. Be careful if you invoke programs that solicit input from the user
Do not invoke programs that allow the user to execute arbitrary programs. For example, do not ask the user to vi a file. vi offers a way for the user to execute a subshell. Yes, ksh's built-in protection will protect you if the user invokes ksh interactively. But, unfortunately, other shells exist. And the user doesn't need an interactive shell with vi. A command like ":!rm /etc/passwd" will work from vi and this is not using an interactive shell.
These steps will go a long way toward securing any suid scripts you write, but I can't guarantee that they are enough to completely secure a script.