Skip to content

Instantly share code, notes, and snippets.

@sroettger sroettger/drive.md Secret
Last active Jul 1, 2018

Embed
What would you like to do?
GoogleCTF 2018 pwn-drive writeup

I made a challenge for this year's GoogleCTF qualification round. Unfortunately it got 0 solves in the end, even though it was open for pretty much the whole competition. Exploiting it is not very hard, but spotting the bugs seems to be more difficult than I thought.

The challenge is a sandbox service. You can send an ELF file and it will be executed in a namespace jail that only includes /lib, /lib64, /proc and /tmp. Besides that, you have access to a file descriptor that can be used to talk to a file broker process running outside of the sandbox. The broker will give you read and write access to a directory in the global tmp. The goal is to exploit this process and get code execution outside of the sandbox.

To do so you will need two bugs, one for reading and one for writing. And both of them are in the code snippet below:

void file_broker(int fd, void *unused) {
  int val = 1;
  check(setsockopt(fd, SOL_SOCKET, SO_PASSCRED, &val, sizeof(val)), "setsockopt(SO_PASSCRED)");

  while (1) {
    int sandboxee_pid = recv_pid(fd);

    char proc_cwd[PATH_MAX];
    snprintf(proc_cwd, PATH_MAX, "/proc/%d/cwd", sandboxee_pid);
    char proc_root[PATH_MAX];
    snprintf(proc_root, PATH_MAX, "/proc/%d/root", sandboxee_pid);

    unsigned long long mode = read_ull(fd);
    if (mode == EXIT) {
      break;
    }

    char *path = read_str(fd);
    // Shared path is easy to check, block ".*/.*", "." and ".."
    if (regexec(&shared_path_preg, path, 0, NULL, 0) != REG_NOMATCH) {
      err(1, "hax attempt (path)");
    }

    char shared_path[PATH_MAX];
    snprintf(shared_path, sizeof(shared_path), "%s/%s", SHARED_DIR, path);

    if (mode == GET_FILE) {
      char out_path_abs[PATH_MAX];
      char *out_path_rel = read_str(fd);
      char *out_path = out_path_rel;

      // checking the out_path is more complicated as it will be relative to the
      // user's mount namespace.
      if (out_path_rel[0] != '/') {
        // First, if the outpath is relative, make it absolute.
        ssize_t cwd_len = readlink(proc_cwd, out_path_abs, sizeof(out_path_abs)-1);
        out_path_abs[cwd_len] = 0;

        if (out_path_abs[cwd_len-1] != '/') {
          // We left space for the / in the readlink call
          strncat(out_path_abs, "/", 1);
        }
        strncat(out_path_abs, out_path_rel, sizeof(out_path_abs)-cwd_len-1);
        out_path = out_path_abs;
      }

      // This function will make sure that we don't follow any symlinks that
      // point outside of the task's root directory.
      int write_fd = create_under_root(proc_root, out_path);
      int read_fd = check(open(shared_path, O_RDONLY | O_CLOEXEC | O_EXCL | O_NOFOLLOW), "open(user file)");

      copy_fd(read_fd, write_fd);

      check(close(write_fd), "close");
      check(close(read_fd), "close");

      free(out_path_rel);
    } else { // PUT_FILE
      unsigned long long len = read_ull(fd);
      char *content = (char*) check_malloc(len);
      readn(fd, content, len);
      int user_fd = check(open(shared_path, O_WRONLY | O_CREAT | O_CLOEXEC | O_EXCL | O_NOFOLLOW, 0700), "open(user file)");
      if (write(user_fd, content, len) != len) {
        err(1, "write(user file)");
      }
      check(close(user_fd), "close");
      free(content);
    }

    free(path);
    send_str(fd, "OK");
  }
}

Let me summarize in short what this is doing. The broker has two modes: GET_FILE and PUT_FILE. PUT_FILE is the easier one, it reads a filename and a length, checks with a regex that there is no "/" in the filename and that it's neither "." or ".." and then creates the file in "/tmp/shared" and writes given content to it.

The GET_FILE is more complicated. Instead of opening the file and passing the fd to the sandboxee, it will copy the file to the sandboxee's chroot using /proc/PID/root. It can handle both absolute and relative files, the latter by reading the /proc/PID/cwd link, and will resolve any symlinks in the path relative to the chroot. This is implemented in the create_under_root function from util.c.

I expected that the players would find the issue with readlink first when a relative path is turned into an absolute one. The return value is not checked, thus if the call fails it will return -1 without touching the out_path_abs buffer. If the buffer is already filled with some contents, the strncat call will overflow and allows you to overwrite the return address on the stack. So how can you make readlink on /proc/PID/cwd fail? Entering a directory so that the full path is larger than PATH_MAX will work for example.

So far so good, there's one problem though. The only return address on the stack points to a page full with hlt instructions and it's not possible to do a relative overwrite, so you'll need an info leak to exploit this. After not finding any leaks like that, I hoped that the players will take another close look at how the input filename is checked when reading files. Isn't it overkill to use a regex for this even though we pretty much only need to check for a "/"? The regex is this: "/|(^[.][.]$)|(^[.]$)" which I believe should be secure. It shouldn't be possible to craft a filename that will point outside of the /tmp/shared directory.

But just as with the first bug, the code doesn't check for error conditions properly. regexec will return REG_NOMATCH either if there really was no match or in case of an error. For example if you send a very long path (2GB), regexec will return REG_NOMATCH and the broker will read any file you want.

To put it together:

  • use the regexec bug to leak /proc/self/maps
  • enter a directory with a long path to make readlink fail
  • exploit the buffer overflow to get a shell
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.