From 6c7181838bb7a6c2b2ab936d64dcb1107d813def Mon Sep 17 00:00:00 2001 From: cyan <1570302455@qq.com> Date: Mon, 15 Jun 2026 02:46:27 -0700 Subject: [PATCH] Add Kerberos ticket trace enum modes --- .../core/exploit/remote/kerberos/client.rb | 2 +- .../kerberos/service_authenticator/options.rb | 7 +- .../kerberos/kerberos_logger_subscriber.rb | 113 ++++++++-- .../msf/base/serializer/readable_text_spec.rb | 4 +- .../exploit/remote/kerberos/client_spec.rb | 8 + .../kerberos_logger_subscriber_spec.rb | 205 +++++++++++++++++- 6 files changed, 303 insertions(+), 36 deletions(-) diff --git a/lib/msf/core/exploit/remote/kerberos/client.rb b/lib/msf/core/exploit/remote/kerberos/client.rb index d501ed4ca4a72..d6c9d7a8a8827 100644 --- a/lib/msf/core/exploit/remote/kerberos/client.rb +++ b/lib/msf/core/exploit/remote/kerberos/client.rb @@ -48,7 +48,7 @@ def initialize(info = {}) register_advanced_options( [ OptTimedelta.new('KrbClockSkew', [true, 'Adjust Kerberos client clock by this offset (e.g. 90s, -5m, 1h)', '0s']), - OptBool.new('KerberosTicketTrace', [false, 'Show AS/TGS/AP Kerberos requests and responses', false]), + OptEnum.new('KerberosTicketTrace', [false, 'Kerberos ticket trace mode', 'off', %w[off metadata ticket full]]), OptString.new('KerberosTicketTraceColors', [false, 'Kerberos request and response colors for KerberosTicketTrace (unset to disable)', 'red/blu']) ], self.class ) diff --git a/lib/msf/core/exploit/remote/kerberos/service_authenticator/options.rb b/lib/msf/core/exploit/remote/kerberos/service_authenticator/options.rb index e36fdb5237ce4..6f7ee354d98f9 100644 --- a/lib/msf/core/exploit/remote/kerberos/service_authenticator/options.rb +++ b/lib/msf/core/exploit/remote/kerberos/service_authenticator/options.rb @@ -5,6 +5,7 @@ # require 'msf/core/opt_timedelta' +# Kerberos service authentication option helpers. module Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Options # Create the list of options that a module must provide for Kerberos authentication via the given protocol # This method exists for ensuring consistency across service authentication modules @@ -30,7 +31,7 @@ def kerberos_auth_options(protocol:, auth_methods:) auth_options = [ Msf::OptEnum.new( "#{protocol}::Auth", - [true, 'The Authentication mechanism to use', Msf::Exploit::Remote::AuthOption::AUTO, auth_methods], + [true, 'The Authentication mechanism to use', Msf::Exploit::Remote::AuthOption::AUTO, auth_methods] ), Msf::OptString.new( "#{protocol}::Rhostname", @@ -43,9 +44,9 @@ def kerberos_auth_options(protocol:, auth_methods:) [true, 'Adjust Kerberos client clock by this offset (e.g. 90s, -5m, 1h)', '0s'], conditions: option_conditions ), - Msf::OptBool.new( + Msf::OptEnum.new( 'KerberosTicketTrace', - [false, 'Show AS/TGS/AP Kerberos requests and responses', false], + [false, 'Kerberos ticket trace mode', 'off', %w[off metadata ticket full]], conditions: option_conditions ), Msf::OptString.new( diff --git a/lib/rex/proto/kerberos/kerberos_logger_subscriber.rb b/lib/rex/proto/kerberos/kerberos_logger_subscriber.rb index 8d15eea7364e1..bed9a7aa5779e 100644 --- a/lib/rex/proto/kerberos/kerberos_logger_subscriber.rb +++ b/lib/rex/proto/kerberos/kerberos_logger_subscriber.rb @@ -13,6 +13,17 @@ module Proto module Kerberos # Logs Kerberos requests/responses class KerberosLoggerSubscriber < KerberosSubscriber + TRACE_MODE_OFF = 'off' + TRACE_MODE_METADATA = 'metadata' + TRACE_MODE_TICKET = 'ticket' + TRACE_MODE_FULL = 'full' + TRACE_MODES = [ + TRACE_MODE_OFF, + TRACE_MODE_METADATA, + TRACE_MODE_TICKET, + TRACE_MODE_FULL + ].freeze + def initialize(logger:) super() raise 'Incompatible logger' unless logger.respond_to?(:print_line) && logger.respond_to?(:datastore) @@ -22,16 +33,16 @@ def initialize(logger:) # (see Rex::Proto::Kerberos::KerberosSubscriber#on_request) def on_request(request) - return unless trace_enabled? + return unless message_trace_enabled? request_color, _response_color = trace_colors print_header('Request', request) - @logger.print_line("%clr#{request_color}#{format_message(request)}%clr") + @logger.print_line("%clr#{request_color}#{format_message_for_trace_mode(request)}%clr") end # (see Rex::Proto::Kerberos::KerberosSubscriber#on_response) def on_response(response) - return unless trace_enabled? + return unless message_trace_enabled? _request_color, response_color = trace_colors print_header('Response', response) @@ -40,22 +51,35 @@ def on_response(response) return end - @logger.print_line("%clr#{response_color}#{format_message(response)}%clr") + @logger.print_line("%clr#{response_color}#{format_message_for_trace_mode(response)}%clr") end # (see Rex::Proto::Kerberos::KerberosSubscriber#on_credential) def on_credential(credential, source: nil) - return unless trace_enabled? + return unless credential_trace_enabled? return if credential.nil? print_credential_header(source) - @logger.print_line(format_credential(credential)) + @logger.print_line(format_credential_for_trace_mode(credential)) end private - def trace_enabled? - @logger.datastore['KerberosTicketTrace'] + def message_trace_enabled? + [TRACE_MODE_METADATA, TRACE_MODE_FULL].include?(trace_mode) + end + + def credential_trace_enabled? + [TRACE_MODE_METADATA, TRACE_MODE_TICKET, TRACE_MODE_FULL].include?(trace_mode) + end + + def trace_mode + configured_trace_mode = @logger.datastore['KerberosTicketTrace'] + return TRACE_MODE_FULL if configured_trace_mode == true + return TRACE_MODE_OFF if blank_value?(configured_trace_mode) + + normalized_trace_mode = configured_trace_mode.to_s.downcase + TRACE_MODES.include?(normalized_trace_mode) ? normalized_trace_mode : TRACE_MODE_OFF end def trace_colors @@ -104,11 +128,21 @@ def message_type_name(message) end end - def format_message(message) + def format_message_for_trace_mode(message) + format_message(message, redact_binary: trace_mode != TRACE_MODE_FULL) + end + + def format_credential_for_trace_mode(credential) + return format_credential_metadata(credential) if trace_mode == TRACE_MODE_METADATA + + format_credential(credential) + end + + def format_message(message, redact_binary:) return 'null' if message.nil? if message.respond_to?(:attributes) - serialized_message = serialize_element(message) + serialized_message = serialize_element(message, redact_binary: redact_binary) readable_text_presenter.present(serialized_message) else # Fall back for non-model objects. @@ -128,12 +162,42 @@ def format_credential(credential) "Credential presenter error: #{e.class}: #{e.message}" end - def serialize_element(element) + def format_credential_metadata(credential) + [ + 'Creds: 1', + " Credential[0]:\n#{present_credential_metadata(credential).indent(4)}" + ].join("\n") + rescue StandardError => e + "Credential presenter error: #{e.class}: #{e.message}" + end + + def present_credential_metadata(credential) + ticket_flags = credential.ticket_flags.to_i + output = [] + + output << "Server: #{credential.server}" + output << "Client: #{credential.client}" + output << "Ticket etype: #{credential.keyblock.enctype} (#{Rex::Proto::Kerberos::Crypto::Encryption.const_name(credential.keyblock.enctype)})" + output << "Subkey: #{credential.is_skey == 1}" + output << "Ticket Length: #{credential.ticket.length}" + output << "Ticket Flags: 0x#{ticket_flags.to_s(16).rjust(8, '0')} (#{Rex::Proto::Kerberos::Model::KdcOptionFlags.new(ticket_flags).enabled_flag_names.join(', ')})" + output << "Addresses: #{credential.address_count}" + output << "Authdatas: #{credential.authdata_count}" + output << 'Times:' + output << "Auth time: #{present_time(credential.authtime)}".indent(2) + output << "Start time: #{present_time(credential.starttime)}".indent(2) + output << "End time: #{present_time(credential.endtime)}".indent(2) + output << "Renew Till: #{present_time(credential.renew_till)}".indent(2) + + output.join("\n") + end + + def serialize_element(element, redact_binary:) element.attributes.each_with_object({}) do |attribute, output| value = element.public_send(attribute) next if value.nil? - output[serialized_attribute_key(element, attribute)] = serialize_value(value, element: element, attribute: attribute.to_sym) + output[serialized_attribute_key(element, attribute)] = serialize_value(value, element: element, attribute: attribute.to_sym, redact_binary: redact_binary) end end @@ -147,10 +211,10 @@ def serialized_attribute_key(element, attribute) end end - def serialize_value(value, element: nil, attribute: nil) + def serialize_value(value, redact_binary:, element: nil, attribute: nil) if value.respond_to?(:attributes) # Recursively serialize nested Kerberos model objects. - serialize_element(value) + serialize_element(value, redact_binary: redact_binary) elsif kerberos_error_code?(value) # Normalize ErrorCode-like objects to a compact structured form. { @@ -159,19 +223,19 @@ def serialize_value(value, element: nil, attribute: nil) 'description' => value.description } else - serialize_scalar_value(value, element: element, attribute: attribute) + serialize_scalar_value(value, element: element, attribute: attribute, redact_binary: redact_binary) end end - def serialize_scalar_value(value, element: nil, attribute: nil) + def serialize_scalar_value(value, redact_binary:, element: nil, attribute: nil) case value when Array - value.map { |entry| serialize_value(entry, element: element, attribute: attribute) } + value.map { |entry| serialize_value(entry, element: element, attribute: attribute, redact_binary: redact_binary) } when Set - value.to_a.map { |entry| serialize_value(entry, element: element, attribute: attribute) } + value.to_a.map { |entry| serialize_value(entry, element: element, attribute: attribute, redact_binary: redact_binary) } when Hash value.each_with_object({}) do |(key, entry), output| - output[key.to_s] = serialize_value(entry) + output[key.to_s] = serialize_value(entry, redact_binary: redact_binary) end when Rex::Proto::Kerberos::Model::KerberosFlags { @@ -181,7 +245,7 @@ def serialize_scalar_value(value, element: nil, attribute: nil) when Time value.utc.iso8601 when String - serialize_string(value) + serialize_string(value, redact_binary: redact_binary) when Symbol value.to_s when Integer @@ -262,13 +326,18 @@ def kerberos_error_code?(value) value.respond_to?(:name) && value.respond_to?(:value) && value.respond_to?(:description) end - def serialize_string(value) + def serialize_string(value, redact_binary:) return value if printable_string?(value) - # Expand binary/non-printable strings fully in hex. + return "[binary #{value.bytesize} bytes]" if redact_binary + "[binary #{value.bytesize} bytes: #{value.unpack1('H*')}]" end + def present_time(time) + time.localtime.to_s + end + def ticket_presenter @ticket_presenter ||= Rex::Proto::Kerberos::CredentialCache::Krb5CcachePresenter.new(nil) end diff --git a/spec/lib/msf/base/serializer/readable_text_spec.rb b/spec/lib/msf/base/serializer/readable_text_spec.rb index c63fb1c4ba273..07bebcda1128f 100644 --- a/spec/lib/msf/base/serializer/readable_text_spec.rb +++ b/spec/lib/msf/base/serializer/readable_text_spec.rb @@ -291,7 +291,7 @@ def initialize Name Current Setting Required Description ---- --------------- -------- ----------- DomainControllerRhost no The resolvable rhost for the Domain Controller - KerberosTicketTrace false no Show AS/TGS/AP Kerberos requests and responses + KerberosTicketTrace off no Kerberos ticket trace mode (Accepted: off, metadata, ticket, full) KerberosTicketTraceColors red/blu no Kerberos request and response colors for KerberosTicketTrace (unset to disable) KrbClockSkew 0s yes Adjust Kerberos client clock by this offset (e.g. 90s, -5m, 1h) Winrm::Krb5Ccname no The ccache file to use for kerberos authentication @@ -353,7 +353,7 @@ def initialize Name Current Setting Required Description ---- --------------- -------- ----------- DomainControllerRhost no The resolvable rhost for the Domain Controller - KerberosTicketTrace false no Show AS/TGS/AP Kerberos requests and responses + KerberosTicketTrace off no Kerberos ticket trace mode (Accepted: off, metadata, ticket, full) KerberosTicketTraceColors red/blu no Kerberos request and response colors for KerberosTicketTrace (unset to disable) KrbClockSkew 0s yes Adjust Kerberos client clock by this offset (e.g. 90s, -5m, 1h) Winrm::Krb5Ccname no The ccache file to use for kerberos authentication diff --git a/spec/lib/msf/core/exploit/remote/kerberos/client_spec.rb b/spec/lib/msf/core/exploit/remote/kerberos/client_spec.rb index 872c41aa51ae0..64270e58ae3ff 100644 --- a/spec/lib/msf/core/exploit/remote/kerberos/client_spec.rb +++ b/spec/lib/msf/core/exploit/remote/kerberos/client_spec.rb @@ -18,6 +18,14 @@ end end + describe 'KerberosTicketTrace option' do + it 'is registered as an OptEnum datastore option defaulting to off' do + expect(subject.options['KerberosTicketTrace']).to be_a(Msf::OptEnum) + expect(subject.datastore['KerberosTicketTrace']).to eq('off') + expect(subject.options['KerberosTicketTraceLevel']).to be_nil + end + end + describe '#kerberos_clock_skew' do it 'defaults to zero' do expect(subject.kerberos_clock_skew).to eq(0) diff --git a/spec/lib/rex/proto/kerberos/kerberos_logger_subscriber_spec.rb b/spec/lib/rex/proto/kerberos/kerberos_logger_subscriber_spec.rb index 92365d4d15656..f344f76a7ed91 100644 --- a/spec/lib/rex/proto/kerberos/kerberos_logger_subscriber_spec.rb +++ b/spec/lib/rex/proto/kerberos/kerberos_logger_subscriber_spec.rb @@ -13,13 +13,13 @@ subject(:subscriber) { described_class.new(logger: logger) } - let(:trace_enabled) { true } + let(:trace_mode) { 'full' } let(:trace_colors) { nil } let(:mock_datastore) do { - 'KerberosTicketTrace' => trace_enabled, + 'KerberosTicketTrace' => trace_mode, 'KerberosTicketTraceColors' => trace_colors } end @@ -154,6 +154,45 @@ ) end + context 'when KerberosTicketTrace is metadata' do + let(:trace_mode) { 'metadata' } + + it 'prints a concise payload without expanding binary field contents' do + request = build_kerberos_message( + msg_type: Rex::Proto::Kerberos::Model::AS_REQ, + fields: { + ticket: ("\x00".b * 40), + req_body: Rex::Proto::Kerberos::Model::KdcRequestBody.new( + etype: [ + Rex::Proto::Kerberos::Crypto::Encryption::AES256, + Rex::Proto::Kerberos::Crypto::Encryption::AES128 + ] + ) + } + ) + + subscriber.on_request(request) + + expect(output_text).to eq( + expected_trace_output( + direction: 'Request', + message_name: 'AS-REQ', + color_prefix: '%bld%red', + body: <<~EOF.rstrip + Protocol Version: 5 + Message Type: #{Rex::Proto::Kerberos::Model::AS_REQ} (AS-REQ) + Client Realm: EXAMPLE.LOCAL + Ticket: [binary 40 bytes] + Request Body: + Encryption Type: + - #{Rex::Proto::Kerberos::Crypto::Encryption::AES256} (AES256) + - #{Rex::Proto::Kerberos::Crypto::Encryption::AES128} (AES128) + EOF + ) + ) + end + end + it 'normalizes time, symbols, flags, sets and error code objects' do error_code = double( 'error_code', @@ -393,8 +432,18 @@ ) end + context 'when KerberosTicketTrace is off' do + let(:trace_mode) { 'off' } + + it 'does not print anything' do + log_request + + expect(output_text).to be_nil + end + end + context 'when KerberosTicketTrace is false' do - let(:trace_enabled) { false } + let(:trace_mode) { false } it 'does not print anything' do log_request @@ -404,7 +453,7 @@ end context 'when KerberosTicketTrace is nil' do - let(:trace_enabled) { nil } + let(:trace_mode) { nil } it 'does not print anything' do log_request @@ -413,6 +462,16 @@ end end + context 'when KerberosTicketTrace is ticket' do + let(:trace_mode) { 'ticket' } + + it 'does not print request messages' do + log_request + + expect(output_text).to be_nil + end + end + { 'red/blu' => '%bld%red', 'yel/' => '%bld%yel', @@ -527,8 +586,64 @@ ) end + context 'when KerberosTicketTrace is metadata' do + let(:trace_mode) { 'metadata' } + + it 'prints response metadata without expanding encrypted data contents' do + response = build_kerberos_message( + msg_type: Rex::Proto::Kerberos::Model::AS_REP, + fields: { + enc_part: Rex::Proto::Kerberos::Model::EncryptedData.new( + etype: Rex::Proto::Kerberos::Crypto::Encryption::AES256, + kvno: 2, + cipher: "\x01\x02\x03".b + ) + } + ) + + subscriber.on_response(response) + + expect(output_text).to eq( + expected_trace_output( + direction: 'Response', + message_name: 'AS-REP', + color_prefix: '%bld%blu', + body: <<~EOF.rstrip + Protocol Version: 5 + Message Type: #{Rex::Proto::Kerberos::Model::AS_REP} (AS-REP) + Client Realm: EXAMPLE.LOCAL + Encrypted Part: + Encryption Type: #{Rex::Proto::Kerberos::Crypto::Encryption::AES256} (AES256) + Key Version Number: 2 + Cipher: [binary 3 bytes] + EOF + ) + ) + end + end + + context 'when KerberosTicketTrace is ticket' do + let(:trace_mode) { 'ticket' } + + it 'does not print response messages' do + log_response + + expect(output_text).to be_nil + end + end + + context 'when KerberosTicketTrace is off' do + let(:trace_mode) { 'off' } + + it 'does not print anything' do + log_response + + expect(output_text).to be_nil + end + end + context 'when KerberosTicketTrace is false' do - let(:trace_enabled) { false } + let(:trace_mode) { false } it 'does not print anything' do log_response @@ -538,7 +653,7 @@ end context 'when KerberosTicketTrace is nil' do - let(:trace_enabled) { nil } + let(:trace_mode) { nil } it 'does not print anything' do log_response @@ -649,6 +764,80 @@ ) end + context 'when KerberosTicketTrace is ticket' do + let(:trace_mode) { 'ticket' } + + it 'prints the full credential presenter output' do + log_credential + + expect(output_text).to eq( + <<~EOF.rstrip + #################### + # Kerberos Credential: TGS + #################### + Creds: 1 + Credential[0]: + Server: krbtgt/EXAMPLE.LOCAL@EXAMPLE.LOCAL + Client: Administrator@EXAMPLE.LOCAL + Ticket etype: 18 (AES256) + Key: deadbeef + EOF + ) + end + end + + context 'when KerberosTicketTrace is metadata' do + let(:trace_mode) { 'metadata' } + let(:auth_time) { Time.utc(2026, 1, 1, 0, 0, 0) } + let(:start_time) { Time.utc(2026, 1, 1, 0, 5, 0) } + let(:end_time) { Time.utc(2026, 1, 1, 10, 0, 0) } + let(:renew_till) { Time.utc(2026, 1, 8, 0, 0, 0) } + let(:credential) do + double( + 'credential', + server: 'krbtgt/EXAMPLE.LOCAL@EXAMPLE.LOCAL', + client: 'Administrator@EXAMPLE.LOCAL', + keyblock: double('keyblock', enctype: Rex::Proto::Kerberos::Crypto::Encryption::AES256), + is_skey: 0, + ticket: double('ticket', length: 1234), + ticket_flags: 0, + address_count: 0, + authdata_count: 0, + authtime: auth_time, + starttime: start_time, + endtime: end_time, + renew_till: renew_till + ) + end + + it 'prints only high-level credential metadata' do + log_credential + + expect(output_text).to eq( + <<~EOF.rstrip + #################### + # Kerberos Credential: TGS + #################### + Creds: 1 + Credential[0]: + Server: krbtgt/EXAMPLE.LOCAL@EXAMPLE.LOCAL + Client: Administrator@EXAMPLE.LOCAL + Ticket etype: 18 (AES256) + Subkey: false + Ticket Length: 1234 + Ticket Flags: 0x00000000 () + Addresses: 0 + Authdatas: 0 + Times: + Auth time: #{auth_time.localtime} + Start time: #{start_time.localtime} + End time: #{end_time.localtime} + Renew Till: #{renew_till.localtime} + EOF + ) + end + end + it 'omits the source suffix from the header when source is nil' do subscriber.on_credential(credential, source: nil) @@ -673,8 +862,8 @@ expect(output_text).to be_nil end - context 'when KerberosTicketTrace is false' do - let(:trace_enabled) { false } + context 'when KerberosTicketTrace is off' do + let(:trace_mode) { 'off' } it 'does not print anything' do log_credential