# -*- coding: binary -*-
require 'uri'
require 'digest'
require 'net/winrm/connection'

module Msf
module Exploit::Remote::WinRM
  include Exploit::Remote::NTLM::Client
  include Exploit::Remote::HttpClient
  include Msf::Exploit::Remote::Kerberos::Ticket::Storage
  include Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Options

  #
  # Constants
  #
  NTLM_CRYPT ||= Rex::Proto::NTLM::Crypt
  NTLM_CONST ||= Rex::Proto::NTLM::Constants
  NTLM_UTILS ||= Rex::Proto::NTLM::Utils
  NTLM_XCEPT ||= Rex::Proto::NTLM::Exceptions

  def initialize(info = {})
    super
    register_options(
      [
        Opt::RPORT(5985),
        OptString.new('DOMAIN', [ true, 'The domain to use for Windows authentication', 'WORKSTATION']),
        OptString.new('URI', [ true, "The URI of the WinRM service", "/wsman" ]),
        OptString.new('USERNAME', [ false, 'A specific username to authenticate as' ]),
        OptString.new('PASSWORD', [ false, 'A specific password to authenticate with' ]),
      ], self.class
    )

    register_advanced_options(
      [
        *kerberos_storage_options(protocol: 'Winrm'),
        *kerberos_auth_options(protocol: 'Winrm', auth_methods: Msf::Exploit::Remote::AuthOption::WINRM_OPTIONS),
      ],
    )

    register_autofilter_ports([ 80,443,5985,5986 ])
    register_autofilter_services(%W{ winrm })
  end

  def check_winrm_parameters
    if datastore['Winrm::Auth'] == Msf::Exploit::Remote::AuthOption::KERBEROS
      fail_with(Msf::Exploit::Failure::BadConfig, 'The Winrm::Rhostname option is required when using Kerberos authentication.') if datastore['Winrm::Rhostname'].blank?
      fail_with(Msf::Exploit::Failure::BadConfig, 'The DOMAIN option is required when using Kerberos authentication.') if datastore['DOMAIN'].blank?
      offered_etypes = Msf::Exploit::Remote::AuthOption.as_default_offered_etypes(datastore['Winrm::KrbOfferedEncryptionTypes'])
      fail_with(Msf::Exploit::Failure::BadConfig, 'At least one encryption type is required when using Kerberos authentication.') if offered_etypes.empty?
    else
      fail_with(Msf::Exploit::Failure::BadConfig, 'The PASSWORD option is required unless using Kerberos authentication.') if datastore['PASSWORD'].blank?
    end
  end

  # Sets up a connection to a WinRM server, based on the datastore parameters
  # May use NTLM or Kerberos auth, depending on the params
  # @return [Net::MsfWinRM::RexWinRMConnection] Connection to the WinRM server
  def create_winrm_connection
    rhost = datastore['RHOST']
    rport = datastore['RPORT']
    uri = datastore['URI']
    ssl = datastore['SSL']
    schema = ssl ? 'https' : 'http'
    endpoint = URI.join("#{schema}://#{rhost}:#{rport}", uri)
    opts = {
      endpoint: endpoint,
      host: rhost,
      port: rport,
      proxies: datastore['Proxies'],
      uri: uri,
      ssl: ssl,
      transport: :rexhttp,
      no_ssl_peer_verification: true,
      operation_timeout: 1,
      timeout: 20,
      retry_limit: 1,
      realm: datastore['DOMAIN']
    }
    case datastore['Winrm::Auth']
    when Msf::Exploit::Remote::AuthOption::KERBEROS
      kerberos_authenticator = Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::HTTP.new(
        host: datastore['DomainControllerRhost'].blank? ? nil : datastore['DomainControllerRhost'],
        hostname: datastore['Winrm::Rhostname'],
        proxies: datastore['Proxies'],
        realm: datastore['DOMAIN'],
        username: datastore['USERNAME'],
        password: datastore['PASSWORD'],
        timeout: 20, # datastore['timeout']
        framework: framework,
        framework_module: self,
        cache_file: datastore['Winrm::Krb5Ccname'].blank? ? nil : datastore['Winrm::Krb5Ccname'],
        offered_etypes: Msf::Exploit::Remote::AuthOption.as_default_offered_etypes(datastore['Winrm::KrbOfferedEncryptionTypes']),
        mutual_auth: true,
        use_gss_checksum: true,
        clock_skew: kerberos_clock_skew_seconds
      )
      opts = opts.merge({
        user: '', # Need to provide it, otherwise the WinRM module complains
        password: '', # Need to provide it, otherwise the WinRM module complains
        kerberos_authenticator: kerberos_authenticator,
        vhost: datastore['RHOSTNAME']
      })
    else
      opts = opts.merge({
        user: datastore['USERNAME'],
        password: datastore['PASSWORD']
      })
    end

    return Net::MsfWinRM::RexWinRMConnection.new(opts)
  end

  # Make an unauthenticated request to the WinRM server
  # @param [Integer] timeout Timeout for the request in seconds
  # @return [Rex::Proto::Http::Response] The HTTP response from an unauthenticated request
  def make_unauthenticated_request(timeout = 20)
    opts = {
      'uri' => datastore['URI'],
      'method' => 'POST',
      'data' => Rex::Text.rand_text_alpha(8),
      'ctype' => "application/soap+xml;charset=UTF-8"
    }
    send_request_cgi(opts,timeout)
  end

  # Parse the available auth methods from a WinRM response
  # @param [Rex::Proto::Http::Response] resp The HTTP response from the WinRM server
  # @return [Array<String>] The auth methods parsed from the HTTP response
  def parse_auth_methods(resp)
    return [] unless resp and resp.code == 401
    methods = []
    methods << "Negotiate" if resp.headers['WWW-Authenticate'].include? "Negotiate"
    methods << "Kerberos" if resp.headers['WWW-Authenticate'].include? "Kerberos"
    methods << "Basic" if resp.headers['WWW-Authenticate'].include? "Basic"
    return methods
  end

  # Parse out the results from a WQL query
  # @param [Hash] The response from a call to Connection.run_wql
  # @return [Rex::Text::Table] The results from the WQL query in table form
  def parse_wql_hash(response)
    columns = []
    rows = []
    fragments = response[:xml_fragment]
    fragments.each do |fragment|
      row_data = []
      fragment.keys.each do |key|
        unless key.starts_with?('@') # xmlns stuff
          columns << key.to_s
          row_data << fragment[key]
        end
      end
      rows << row_data
    end

    columns.uniq!
    response_data = Rex::Text::Table.new(
      'Header'    => "#{datastore['WQL']} (#{rhost})",
      'Indent'    => 1,
      'Columns'   => columns
    )
    rows.each do |row|
      response_data << row
    end
    return response_data
  end

  # The namespace for WQL queries
  # @return [String] The WMI namespace
  def wmi_namespace
    return datastore['NAMESPACE'] if datastore['NAMESPACE']
    return @namespace_override if @namespace_override
    return "root/cimv2"
  end
end
end
