setuid equivalent for non-root users
Asked Answered
C

2

6

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).

Canonicate answered 23/10, 2012 at 23:2 Comment(3)
You can switch out of root to an unprivileged user and then switch back (through the saved user ID mechanism) when you need to switch to a different user.Duster
Hmm I don't think that would add any security in my case, assuming that the mechanism to switch back is not protected. So if the main service gets exploited, it will be a trivial extra step until root.Canonicate
It depends what you mean by "gets exploited". If you mean they can cause it to access whatever code they want, then I think that's fundamental. They could add code to log whatever credentials it uses to switches users and then use those credentials to exploit the system anyway. Unless you mean that root specifically would be compromised.Duster
B
4

No, there is no way to change UID using only a username and password. (The concept of a "password" is not recognized by the kernel in any fashion -- it only exists in userspace.) To switch from one non-root UID to another, you must become root as an intermediate step, typically by exec()-uting a setuid binary.

Another option in your situation may be to have the main server run as an unprivileged user, and have it communicate with a back-end process running as root.

Bratislava answered 23/10, 2012 at 23:9 Comment(8)
This back-end process can be written to check the supplied password, which will give the overall effect that's wanted.Mablemabry
Sorta. You still can't elevate a running process from non-root to root that way; the best you can do is have the back-end process launch a process on demand.Bratislava
Well yes, the back-end process has to be running as root, as you say in your answer.Mablemabry
this answer is not correct. there is definitely a way to setuid() for non-root users. check on Linux capabilities man7.org/linux/man-pages/man7/capabilities.7.htmlBrowne
@Browne There is CAP_SETUID/GID, but that capability is root-equivalent -- it allows a process to switch to any UID, including UID 0. As such, it's almost never used. And it doesn't use passwords, which is what the OP asked for.Bratislava
@duskwuff, ability to limit which uids setuid() can work can be limited through namespaces man7.org/linux/man-pages/man7/user_namespaces.7.htmlBrowne
@Browne User namespaces are kind of a separate thing. Putting a process into its own user namespace lets it change its UID however it likes… at the expense of having those changes not be "real" outside the namespace. The namespace has a single UID as its owner, and any actions performed by processes inside the namespace are externally treated as if they were done by that UID.Bratislava
I believe duskwuff is pretty much correct here, @Tagar. The only thing I disagree with is a nitpick: you can switch from one user to another, without becoming root as an intermediate step, by using a setuid (and setgid) binary. This is a nitpick, because each setuid/setgid binary can only allow switching to one fixed UID and/or GID (the owner and group of the binary). Anyway, I seem to recall discussions on LKML for token-based non-root identity/credential changes, to solve the underlying problems here, without temporarily elevating privileges.Intrude
I
10

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.

Intrude answered 24/10, 2012 at 10:13 Comment(2)
nice answer. this should be chosen as the correct answer.Browne
Dear @Nominal Animal very interesting post, do you know some open source code who has impalement this technique? I want to study the codeBurweed
B
4

No, there is no way to change UID using only a username and password. (The concept of a "password" is not recognized by the kernel in any fashion -- it only exists in userspace.) To switch from one non-root UID to another, you must become root as an intermediate step, typically by exec()-uting a setuid binary.

Another option in your situation may be to have the main server run as an unprivileged user, and have it communicate with a back-end process running as root.

Bratislava answered 23/10, 2012 at 23:9 Comment(8)
This back-end process can be written to check the supplied password, which will give the overall effect that's wanted.Mablemabry
Sorta. You still can't elevate a running process from non-root to root that way; the best you can do is have the back-end process launch a process on demand.Bratislava
Well yes, the back-end process has to be running as root, as you say in your answer.Mablemabry
this answer is not correct. there is definitely a way to setuid() for non-root users. check on Linux capabilities man7.org/linux/man-pages/man7/capabilities.7.htmlBrowne
@Browne There is CAP_SETUID/GID, but that capability is root-equivalent -- it allows a process to switch to any UID, including UID 0. As such, it's almost never used. And it doesn't use passwords, which is what the OP asked for.Bratislava
@duskwuff, ability to limit which uids setuid() can work can be limited through namespaces man7.org/linux/man-pages/man7/user_namespaces.7.htmlBrowne
@Browne User namespaces are kind of a separate thing. Putting a process into its own user namespace lets it change its UID however it likes… at the expense of having those changes not be "real" outside the namespace. The namespace has a single UID as its owner, and any actions performed by processes inside the namespace are externally treated as if they were done by that UID.Bratislava
I believe duskwuff is pretty much correct here, @Tagar. The only thing I disagree with is a nitpick: you can switch from one user to another, without becoming root as an intermediate step, by using a setuid (and setgid) binary. This is a nitpick, because each setuid/setgid binary can only allow switching to one fixed UID and/or GID (the owner and group of the binary). Anyway, I seem to recall discussions on LKML for token-based non-root identity/credential changes, to solve the underlying problems here, without temporarily elevating privileges.Intrude

© 2022 - 2024 — McMap. All rights reserved.