next up previous
Next: The Mechanics Up: Setuid applications Previous: Creating a file

Running another program

Occasionally, you want to invoke another program from your setuid application. There's a lot of things that can go wrong here, so if you can avoid it, better don't do it. But if you really really can't avoid doing it, this section is for you.

Let's begin with a real life example. The crontab command, which lets a user edit his list of cron jobs. This list of jobs is stored in a file inside a root-owned directory such as /var/spool/cron. crontab has to be set-user root so in order to retrieve the user's file from the directory, and write it back.

As a matter of convenience, crontab invokes the user's favorite editor on the retrieved cron file. The name of the editor is retrieved from the EDITOR environment variable. There are several problems why it would be bad to execute the editor with root privilege. The first is of course, that most editors let you run arbitrary shell commands, so running an editor lile emacs or vi as root is not very different from directly giving the user a root shell.

However, this is not the only issue. Let's assume we solve this by not letting the user choose his favorite editor, but force him to use a restricted editor called safe_ed instead, which will not let him run shell commands.

A naive implementation might do something like this:

    /* DANGER: security hole ahead! */
    char       tempfile[MAXPATHLEN], *s, command[256];

    if ((s = getenv("TMPDIR")) == NULL)
        s = "/tmp";
    snprintf(tempfile, sizeof(tempfile),
            "%s/cron.%d", s, getpid());
    /* Retrieve crontab and store in temp file */
    retrieve(tempfile);
    snprintf(command, sizeof(command), "safe_ed %s", filename);
    system(command);
    /* Store modified crontab */
    store(tempfile);

Using system() is usually the most convenient way to execute other programs. You give it a single string, and it passes it to /bin/sh for evaluation. This is just as if you were typing your string at the shell prompt.

The most glaring problem with this piece of code is that the shell will search the PATH variable to find the safe_ed program. So the user can control whether crontab ends up calling the real safe_ed, or just a random program called safed_ed which does something entirely different. For instance, the attacker might make /tmp the first directory in PATH, and store the following shell script in /tmp/safe_ed, which creates a copy of /bin/sh and makes it set-user root:

    #!/bin/sh
    cp /bin/bash /tmp/.rootsh
    chown root /tmp/.rootsh
    chmod +s /tmp/.rootsh

Of course, this attack can be avoided by providing the full path name to safe_ed in the call to system.3.9

However, this is not the only reason why using using system in a set-user application is a bad idea. As I said above, passing a string to system is just like typing it at the shell prompt. In particular, all shell meta characters are evaluated, including shell variable expansions, backticks for command substitution, pipe symbols: the whole panopticum. This lends itself to a different sort of attack via the TMPDIR environment variable.

Assume the attacker stores the mini shell script shown above in a file called evilscript, and sets TMPDIR to /tmp/`evilscript`. What happens is that crontab will store the current list of jobs in /tmp/`evilscript`/cron.1234, and invoke safe_ed with this filename. When interpreting the command, the shell will see the backtick characters and run evilscript!

Again, we could defend against this attack by restricting what can be specified via TMPDIR, or always using /tmp. But then there would probably be yet another way the user could fool system -- and I know of at least one more attack that used to work (hint: check your bash manual page for ENV).

To make a long story short, this is yet another case that can be solved by dropping privilege. All we need to do is make sure that we have dropped the root uid when the external program is started. If an attacker then manages to make us run another program, we don't need to worry anymore because there's nothing the attacker would gain.

Unfortunately, we cannot just drop all privilege before calling system, because we will still need it afterwards to store the modified list of cron jobs. On the other hand, just dropping privilege temporarily doesn't help either, at least on a BSD system. Recall that on BSD, all you can do is swap real and effective uid, and we need to have the root uid around for later. Unfortunately, both real and effective uids are inherited by subprocesses.

In order to come up with a solution, we have to take a look under the Unix hood and understand how to run an external program, and where and when to drop privilege.


next up previous
Next: The Mechanics Up: Setuid applications Previous: Creating a file
Olaf Kirch 2002-01-16