Dropping privileges in nodejs

A couple of months ago I wrote a web challenge for the 9447 CTF called ramble. Spoiler alert, the challenge was written in nodejs, but allowed the user to execute arbitrary shell code. Clearly I didn’t want players to be able to destroy the challenge for others, so I wanted my process to have ~no privileges. This proved to be more of a challenge than expected.

Typically, this kind of protection is achieved using normal Linux permissions - you drop privileges in the server process after binding to a socket, then fork each child. This allows you to use Linux file permissions to prevent the no-privilege user from reading or writing to anything damaging.

Despite node being a non-forking server, I started out by going down this route. My searching found this blog post which says that you can drop privileges in node by calling process.setgid and process.setuid. Unfortunately, calling these is not sufficient, since the user will still be in the original group - the following is required to properly drop privileges:

process.setgid('nobody')
process.setuid('nobody')
process.setgroups(['nobody'])

Unfortunately, it turned out that even this wasn’t sufficient - one malicious player could still kill the processes of all other players, including the server process. In a typical forking server model, this could be fixed by dropping privileges only once the child process had forked - this would allow a malicious player to kill other users’ connections, but not the server itself. Unfortunately, nodejs doesn’t fork (and if you try to make it, becomes extremely heavy-weight), so I needed to find another solution.

Enter seccomp - this was a feature added to the Linux kernel in 2005 that allows a process to specify that it wishes to only run certain, trusted syscalls (read, write, exit, and sigreturn in future, and that if it runs others, it should be sent a SIGKILL. An extension was added to this feature that allows a process to specify a whitelist of syscalls that it wants to allow, and that it wants to be sent a SIGTRAP rather than a SIGKILL.

There is a poorly-documented seccomp wrapper for nodejs here, which I used to limit the syscalls that users could call (with some very painful discovery of which syscalls I needed to allow for nodejs to work, that could probably have been better automated).

The restricted list of syscalls allowed prevented users from killing other processes on the box, or from fork-bombing the server. I also learned about the exciting set of syscalls that bash calls when it’s run.