Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

setuid equivalent for non-root users

Does Linux have some C interface similar to setuid, which allows a program to switch to a different user using e.g. the username/password? The problem with setuid is that it can only be used by superusers.

I am running a simple web service which requires jobs to be executed as the logged in user. So the main process runs as root, and after the user logs in it forks and calls setuid to switch to the appropriate uid. However, I am not quite comfortable with the main proc running as root. I would rather have it run as another user, and have some mechanism to switch to another user similar to su (but without starting a new process).

like image 233
Jeroen Ooms Avatar asked Oct 23 '12 23:10

Jeroen Ooms


1 Answers

First, setuid() can most definitely be used by non-superusers. Technically, all you need in Linux is the CAP_SETUID (and/or CAP_SETGID) capability to switch to any user. Second, setuid() and setgid() can change the process identity between the real (user who executed the process), effective (owner of the setuid/setgid binary), and saved identities.

However, none of that is really relevant to your situation.

There exists a relatively straightforward, yet extremely robust solution: Have a setuid root helper, forked and executed by your service daemon before it creates any threads, and use an Unix domain socket pair to communicate between the helper and the service, the service passing both its credentials and the pipe endpoint file descriptors to the helper when user binaries are to be executed. The helper will check everything securely, and if all is in order, it will fork and execute the desired user helper, with the specified pipe endpoints connected to standard input, standard output, and standard error.

The procedure for the service to start the helper, as early as possible, is as follows:

  1. Create an Unix domain socket pair, used for privileged communications between the service and the helper.

  2. Fork.

  3. In the child, close all excess file descriptors, keeping only one end of the socket pair. Redirect standard input, output, and error to /dev/null.

  4. In the parent, close the child end of the socket pair.

  5. In the child, execute the privileged helper binary.

  6. The parent sends a simple message, possibly one without any data at all, but with an ancillary message containing its credentials.

  7. The helper program waits for the initial message from the service. When it receives it, it checks the credentials. If the credentials do not pass muster, it quits immediately.

The credentials in the ancillary message define the originating process' UID, GID, and PID. Although the process needs to fill in these, the kernel verifies they are true. The helper of course verifies that UID and GID are as expected (correspond to the account the service ought to be running as), but the trick is to get the statistics on the file the /proc/PID/exe symlink points to. That is the genuine executable of the process that sent the credentials. You should verify it is the same as the installed system service daemon (owned by root:root, in the system binary directory).

There is a very simple attack that may defeat the security up to this point. A nefarious user may create their own program, that forks and executes the helper binary correctly, sends the initial message with its true credentials -- but replaces itself with the correct system binary before the helper has a chance to check what the credentials actually refer to!

That attack is trivially defeated by three further steps:

  1. The helper program generates a (cryptographically secure) pseudorandom number, say 1024 bits, and sends it back to the parent.

  2. The parent sends the number back, but again adds its credentials in an ancillary message.

  3. The helper program verifies that the UID, GID, and PID have not changed, and that /proc/PID/exe still points to the correct service daemon binary. (I'd just repeat the full checks.)

At step 8, the helper has already ascertained the other end of the socket is executing the binary it ought to be executing. Sending it a random cookie it has to send back, means the other end cannot have "stuffed" the socket with the messages beforehand. Of course this assumes the attacker cannot guess the pseudorandom number beforehand. If you want to be careful, you can read a suitable cookie from /dev/random, but remember it is a limited resource (may block if there is not enough randomness available to the kernel). I'd personally just read say 1024 bits (128 bytes) from /dev/urandom, and use that.

At this point, the helper has ascertained the other end of the socket pair is your service daemon, and the helper can trust the control messages as far as it can trust the service daemon. (I'm assuming this is the only mechanism the service daemon will spawn user processes; otherwise you'd need to re-pass the credentials in every further message, and re-check them every time in the helper.)

Whenever the service daemon wishes to execute a user binary, it

  1. Creates the necessary pipes (one for feeding standard input to the user binary, one to get back the standard output from the user binary)

  2. Sends a message to the helper containing

    • Identity to run the binary as; either user (and group) names, or UID and GID(s)
    • Path to the binary
    • Command-line parameters given to the binary
    • An ancillary message containing the file descriptors for the user binary endpoints of the data pipes

Whenever the helper gets such a message, it forks. In the child, it replaces standard input and output with the file descriptors in the ancillary message, changes identity with setresgid() and setresuid() and/or initgroups(), changes the working directory to somewhere appropriate, and executes the user binary. The parent helper process closes the file descriptors in the ancillary message, and waits for the next message.

If the helper exits when there is going to be no more input from the socket, then it will automatically exit when the service exits.

I could provide some example code, if there is sufficient interest. There's lots of details to get right, so the code is a bit tedious to write. However, correctly written, it is more secure than e.g. Apache SuEXEC.

like image 160
Nominal Animal Avatar answered Oct 03 '22 01:10

Nominal Animal