Dangers
of SUID Shell Scripts
Thomas Akin
This article attempts to walk the fine line between full disclosure
and published exploits. The object of this article is to illustrate
how SUID programs work in order to help others writing their own
programs avoid some common mistakes. The examples I provide are
detailed enough to help you understand each danger, but I don't
promise that all will work exactly as demonstrated if you try to
use them maliciously.
Normally, UNIX scripts and programs run with the same permissions
as the user who executes them. This is why typical users can't
change their passwords by editing the /etc/passwd file; they
don't have the permission to write to /etc/passwd, and
no command they run will either. SUID programs, however, override
normal permissions and always run with the permissions of the program's
owner. Therefore, users can use the /usr/bin/passwd command
to change their passwords. The /usr/bin/passwd command is
SUID and is owned by root. It always runs with the same permissions
as root.
When new administrators discover SUID, they often see it as a
silver bullet that will solve all of their problems. They immediately
begin using SUID scripts and programs to make their jobs easier.
Unfortunately, they usually do it wrong.
When working with admins new to SUID, I often encounter scripts
like this:
% ls change-pass
-rwsr-x--- 1 root helpdesk 37 Feb 26 16:35 change-pass
% cat change-pass
#!/bin/csh -b
set user = $1
passwd $user
This simple script was set up to allow the help desk reset user passwords,
which is a common need. The script is SUID root and is only executable
by root or the members of the help desk group. This simple script
is also riddled with holes. I'm going to expose seven of these
holes and see whether they can be prevented.
The first problem occurs because this script is written in C-shell.
C-shell scripts are vulnerable to manipulating environment variables.
To take advantage of this, a hacker can compromise a help desk account
(fairly trivial) and give himself a root shell with:
% env TERM='`cp /bin/sh /tmp/sh;chown root /tmp/sh;chmod 4755/tmp/sh`' change-pass
Lesson One -- Never use C-shell for SUID scripts.
% cat change-pass
#!/bin/ksh
user=$1
passwd $user
Rewriting the script in Korn shell helps us avoid the C-shell problem,
but we still have problems. The script is vulnerable to a hacker manipulating
the PATH variable. Because the program uses relative path names, a
hacker can change his PATH to use his own program instead of the regular
/usr/bin/passwd program:
% export PATH='/tmp'
% echo "cp /bin/sh /tmp/sh;chown root /tmp/sh;chmod 4755/tmp/sh" >/tmp/passwd
% ./change-pass
The PATH has been changed, and the change-pass command now
runs the /tmp/passwd program instead of the /usr/bin/passwd
program that we intended.
Lesson Two -- Always manually set the PATH and use absolute
path names.
% cat change-pass
#!/bin/ksh
PATH='/bin:/usr/bin'
user=$1
/usr/bin/passwd $user
Now the PATH is secure and we are using absolute paths; but look closely
and see that this script can change any password, even root's!
We don't want the help desk (or a hacker) using our script to
change root's password.
Lesson Three -- Understand how the programs in your script
work.
% cat change-pass
#!/bin/ksh
PATH='/bin:/usr/bin'
user=$1
rm /tmp/.user
echo "$user" > /tmp/.user
isroot='/usr/bin/grep -c root /tmp/.user'
[ "$isroot" -gt 0 ] && echo "You Can't change root's password!" && exit
/usr/bin/passwd $user
Now this script will exit if someone enters root as the argument.
But what happens if a hacker runs the program and doesn't specify
an argument? The program will run the passwd command without
any arguments. When the passwd command doesn't receive
any arguments, it defaults to the current user. The problem is that
in a root-owned SUID script, the current user is always root. The
help desk (or hacker) can still change root's password by not
giving change-pass any arguments.
Lesson Three (revised) -- Understand how the programs in your
script work, especially how they handle arguments.
% cat change-pass
#!/bin/ksh
PATH='/bin:/usr/bin'
user=$1
[ -z $user ] && echo "Usage: change-pass username" && exit
rm /tmp/.user
echo "$user" > /tmp/.user
isroot='/usr/bin/grep -c root /tmp/.user'
[ "$isroot" -gt 0 ] && echo "You Can't change root's password!" && exit
/usr/bin/passwd $user
We no longer let anyone change root's password, but notice that
we are using a temporary file. This script deletes the temporary file,
recreates it, fills it with the username, and finally checks to see
whether the username is root.
What if a hacker could time things perfectly so that just after
the script removes the /tmp/.user file, but just before it
creates a new /tmp/.user file, he created an empty /tmp/.user
file? Would the hacker's file be overwritten? Possibly, but
possibly not, depending on how file clobbering was set up. If the
hacker's /tmp/.user is not overwritten, the hacker bypasses
the checks and fools the script into changing root's password.
To make this type of attack easier, a hacker could write a program
that will automatically watch for activity and replace the /tmp/.user
file.
Lesson Four -- Don't use temporary files! If you must
use temporary files, don't put them in a publicly writable
area.
% cat change-pass
#!/bin/ksh
PATH='/bin:/usr/bin'
user=$1
[ -z $user ] && echo "Usage: change-pass username" && exit
[ "$user" = root ] && echo "You can't change root's password!" && exit
/usr/bin/passwd $user
There are no temporary files, but now a hacker can use the well-known
semi-colon trick. A semi-colon lets you put more than one command
on a single line. By taking advantage of this, a hacker could type:
% change-pass "user;cp /bin/sh /tmp/sh;chown root /tmp/sh;chmod 4755 /tmp/sh"
Our script would take this input and run:
/usr/bin/passwd user;cp /bin/sh /tmp/sh;chown root /tmp/sh;chmod 4755 /tmp/sh
Each of these commands will be executed in order providing a root
shell. To prevent problems like this, we need to make sure that any
data the user inputs doesn't contain a semi-colon or any of the
other shell meta-characters.
% cat change-pass
#!/bin/ksh
PATH='/bin:/usr/bin'
user=${1##*[ \\$/;()|\>\<& ]}
[ -z $user ] && echo "Usage: change-pass username" && exit
[ "$user" = root ] && "You can't change root's password!" && exit
/usr/bin/passwd $user
We now remove any of the following characters from user input: a space,
\, $, /, ;, (, ), |, >, <, &, and a tab. It is difficult
to see, but there is space after the open bracket ( [ ) and a tab
before the closing bracket ( ] ).
Lesson Five -- Distrust and check all user input, and strip
out any meta-characters.
Another common vulnerability is related to a shell's Internal
Field Separator (IFS). The IFS specifies which characters separate
commands. It is normally set to a space, tab, or new line. By changing
the IFS, a hacker can change what programs our script executes.
Our script calls the /usr/bin/passwd program. Changing the
IFS to "/" with
% export IFS='/'
causes the script to no longer run /usr/bin/passwd, but instead
run usr bin passwd. Now a hacker can create a script called
usr that generates a root shell, and our SUID script will run
that program for him.
Lesson Six -- Always manually set your IFS.
% cat change-pass
#!/bin/ksh
PATH='/bin:/usr/bin'
IFS=' '
user=${1##*[ \\$/;()|\>\<& ]}
[ -z $user ] && echo "Usage: change-pass username" && exit
[ "$user" = root ] && "You can't change root's password!" && exit
/usr/bin/passwd $user
Unfortunately, we are still not safe. There is an inherent race condition
in shell scripts that we can't fix with better programming. The
problem is that running a shell script is a two-step process. First,
the system starts up a new shell. Then, the new shell reads the contents
of the script and executes it. By timing things perfectly, a hacker
can exploit the delay between shell startup and when the script is
read and executed. By creating a link to the SUID script:
% cd /tmp
% ln -s change-pass rootme
running the link, and quickly replacing it with another file:
% ./rootme &
% rm rootme && \
echo "cp /bin/sh /tmp/sh;chown root /tmp/sh;chmod 4755 /tmp/sh" >> rootme
it is possible to run anything as root. Done like this, there is only
a small chance the attack would succeed, but there are techniques
and programs to increase the chances of success and to automate the
procedure for you. There are only two defenses against this attack.
First, do not use SUID shell scripts. Second, some systems (e.g.,
Solaris) prevent this race condition by passing an open file handle
to the new shell, thus avoiding the need to reopen and read the SUID
file.
Lesson Seven -- Don't use SUID shell scripts.
Even after all our work, it is nearly impossible to create safe
SUID shell scripts. (It is impossible on most systems.) Because
of these problems, some systems (e.g., Linux) won't honor SUID
on shell scripts. If you need the functionality of SUID, there are
three more secure options. A wrapper program written in C, a Perl
script, or a program like sudo. If you are new to secure
programming, I recommend either sudo or a Perl script. SUID
Perl scripts have built-in protection to prevent programmers from
making the mistakes addressed in this article. For more information
on secure SUID programming, see Practical UNIX & Internet
Security (O'Reilly & Associates), or visit: http://seclab.cs.ucdavis.edu/~bishop/
\ secprog.html.
Thomas Akin has worked in Information Security for almost a
decade and specializes in UNIX, Network, and Internet security.
In addition to the CISSP, he has four UNIX and three Networking
certifications. Most of his time is spent developing Information
Security training programs, teaching, and writing. He can be reached
at: takin@crossrealm.com.
|