Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/msf/core/exploit/remote/kerberos/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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",
Expand All @@ -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(
Expand Down
113 changes: 91 additions & 22 deletions lib/rex/proto/kerberos/kerberos_logger_subscriber.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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

Expand All @@ -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.
{
Expand All @@ -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
{
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions spec/lib/msf/base/serializer/readable_text_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions spec/lib/msf/core/exploit/remote/kerberos/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading
Loading