10

I'd like to check whether a program is installed on an UNIX system.

I could use commands like:

  • command -v,
  • hash,
  • type,
  • which...

...and all of them are already mentioned in this answer.

However, none of them work if I want to test, as a normal user, whether I or any superuser can run given command.

Here's an example of what I mean:

dummy:~$ command -v poweroff; echo $?
1
dummy:~$ su
root:~# command -v poweroff; echo $?
/sbin/poweroff
0

As you see, normal user didn't find out about the existence of the poweroff command. Note that dummy users can freely see what's in /sbin anyway.

Community
  • 1
  • 1
rr-
  • 14,303
  • 6
  • 45
  • 67

2 Answers2

14

Source of the problem

The reason the commands you tried do not work is that they only look for executables in $PATH variable. First, let's test our hypothesis.

dummy:~$ mkdir test
dummy:~$ cd test
dummy:~/test$ echo '#!/bin/sh' >test.sh
dummy:~/test$ chmod +x test.sh
dummy:~/test$ cd
dummy:~$ command -v test.sh
dummy:~$ PATH+=:/home/dummy/test/
dummy:~$ command -v test.sh
/home/dummy/test/test.sh

This confirms my statement above.
Now, let's have a look what $PATH looks like for different users:

dummy:~$ echo $PATH
/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games
dummy:~$ su
root:~# echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

So in order to check whether given command is available to given user (in your question, namely: root), you need to know his $PATH environment variable.

The solution

Values of such environment variables on Debian can be usually found in /etc/profile and in /etc/environment/ files. There is no easy way to get these values by fishing them out of files.

The most basic solution is to temporarily add known directories to your $PATH variable and then use command -v:

dummy~$ OLDPATH=$PATH
dummy~$ PATH=$OLDPATH:/sbin:/usr/sbin/:/usr/local/sbin/
dummy~$ command -v poweroff
/sbin/poweroff
dummy~$ PATH=$OLDPATH

There is one problem with this solution: if you want to be portable, you don't really know what are the folders that you should concatenate. In most cases this approach should be sufficient, though.

Alternative solution

What you can do instead is to write a script program that makes use of setuid bit. Setuid bit is a somewhat hidden feature of Linux operating systems that allows programs to be executed on their owner privileges. So you write a program that executes some commands like superuser would, except that it can be run by normal users. That way you can see output of command -v poweroff like a root would do.

Unfortunately, stuff that uses shebang can't have setuid bit, so you cannot create a shell script for this and you need a program in C. Here's an example program that would do the job:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char** argv)
{
    if (argc <= 1)
    {
        fprintf(stderr, "No arguments.\n");
        return 1;
    }

    //validate the argv
    char* prog = argv[1];
    int i;
    for (i = 0; i < strlen(prog); i ++)
    {
        if (prog[i] < 'a' || prog[i] > 'z')
        {
            fprintf(stderr, "%s contains invalid characters (%c), exiting.", prog, prog[i]);
            return 1;
        }
    }

    //here's the `which` command. We start it in new interactive shell,
    //since this program inherits environment variables from its
    //parent shell. We need to start *new* shell that will initialize
    //and overwrite existing PATH environment variable.
    char* command = (char*) malloc(strlen(prog) + 30);
    if (!command)
    {
        fprintf(stderr, "No memory!\n");
        return 1;
    }
    sprintf(command, "bash -cli 'command -v %s'", prog);

    int exists = 0;
    //first we try to execute the command as a dummy user.
    exists |= system(command) == 0;
    if (!exists)
    {
        //then we try to execute the command as a root user.
        setuid(0);
        exists |= system(command) == 0;
    }
    return exists ? 0 : 1;
}

Security note: the version above has very simple argument validation (it lets through only strings matching ^[a-z]*$). Real program should probably include better validation.

Testing

Suppose we saved the file in test.c. We compile it and add setuid bit:

root:~# gcc ./test.c -o ./test
root:~# chown root:root ./test
root:~# chmod 4755 ./test

Note that chown goes before chmod. The 4 before usual 755 pattern is the setuid bit.
Now we can test the program as a normal user.

dummy:~$ ./test ls; echo $?
alias ls='ls -vhF1 --color=auto --group-directories-first'
0
dummy:~$ ./test blah; echo $?
1
dummy:~$ ./test poweroff; echo $?
/sbin/poweroff
0

And best of all - it's portable enough to work on cygwin with no problems. :)

Community
  • 1
  • 1
rr-
  • 14,303
  • 6
  • 45
  • 67
  • Your answer is really well presented ! – Chaos Jun 28 '13 at 17:13
  • You should post the question and the answer on http://unix.stackexchange.com as well – iruvar Jun 28 '13 at 17:17
  • 1
    According to http://meta.stackexchange.com/questions/64068/ I shouldn't do it. I posted it here going by the fact that there are 23k questions in here tagged with `bash`, while unix.stackexchange.com has only 2k of them. – rr- Jun 28 '13 at 17:25
  • You're missing includes for stdio.h, malloc.h, and string.h. Also, the effective UID for a set-root-ID process is root, and the setuid(geteuid()) simply copies root - your test using "blah" doesn't expose the fact that the program is running as root the entire time. – Alex North-Keys Jun 28 '13 at 22:24
  • 2
    Ah, and one can run any desired command by using an argv[1] containing a semicolon - the second part will be run with full root access ("whoami ; whoami") . Your example command backdoors the entire system to local users. Exposing the shell to command injection by a hostile user is very difficult to address without removing the invocation of the shell itself. – Alex North-Keys Jun 28 '13 at 22:36
  • 2
    The example really shouldn't be named the same as a system command. – Alex North-Keys Jun 28 '13 at 22:59
3

The real answer is that you can't satisfy the "any superuser" aspect IF you mean: "Does this command appear in the search path of any user with sudo access?". The reason is that you'd have to run each user's startup scripts to find out what his search path ends up being - many users will include their own ~/bin or ~/pod/abi/x86_64-ubu-1204/bin or whatever and god knows what else (/afs//bin, anyone?) - and many startup scripts have side effects that could turn the whole thing into a real mess, including generating logs, starting up daemons of various kinds, and so on. You'll really be in trouble if one of those users' startup scripts tries to run your new command itself, since they'll then recurse and inflict a denial-of-service attack on your own system.

What you can test more safely is:

  • can anyone can run a command given by full pathname? (skips the startup script insanity)
  • can the current user run the simple (not fully pathed) command?
  • can the current user run the simple command with sudo? (this makes some assumptions about root's startup scripts)
  • can any user running a default environment run the command? (use a dummy user with a known setup).

On a larger system with thousands of users, the "anyone" option isn't practical. Mature sites will NOT want a root-capable command running arbitrary users' scripts, either, even those of sudo-enabled, generally trustworthy admins.

Alex North-Keys
  • 4,200
  • 1
  • 20
  • 22