Path: blob/master/lib/rex/proto/kerberos/kerberos_logger_subscriber.rb
74553 views
# -*- coding: binary -*-12require 'set'3require 'time'4require 'rex/proto/kerberos/model'5require 'rex/proto/kerberos/crypto'6require 'rex/proto/kerberos/kerberos_subscriber'7require 'rex/proto/kerberos/kerberos_readable_text_presenter'8require 'rex/proto/kerberos/credential_cache/krb5_ccache_presenter'910module Rex11module Proto12module Kerberos13# Logs Kerberos requests/responses14class KerberosLoggerSubscriber < KerberosSubscriber15def initialize(logger:)16super()17raise 'Incompatible logger' unless logger.respond_to?(:print_line) && logger.respond_to?(:datastore)1819@logger = logger20end2122# (see Rex::Proto::Kerberos::KerberosSubscriber#on_request)23def on_request(request)24return unless trace_enabled?2526request_color, _response_color = trace_colors27print_header('Request', request)28@logger.print_line("%clr#{request_color}#{format_message(request)}%clr")29end3031# (see Rex::Proto::Kerberos::KerberosSubscriber#on_response)32def on_response(response)33return unless trace_enabled?3435_request_color, response_color = trace_colors36print_header('Response', response)37if response.nil?38@logger.print_line('No response received')39return40end4142@logger.print_line("%clr#{response_color}#{format_message(response)}%clr")43end4445# (see Rex::Proto::Kerberos::KerberosSubscriber#on_credential)46def on_credential(credential, source: nil)47return unless trace_enabled?48return if credential.nil?4950print_credential_header(source)51@logger.print_line(format_credential(credential))52end5354private5556def trace_enabled?57@logger.datastore['KerberosTicketTrace']58end5960def trace_colors61configured_trace_colors = @logger.datastore['KerberosTicketTraceColors']62# Keep HttpTrace-compatible default formatting: request/response color pair.63trace_colors = blank_value?(configured_trace_colors) ? 'red/blu' : configured_trace_colors64trace_colors += '/' if trace_colors.count('/') == 065trace_colors.gsub('/', ' / ').split('/').map do |color|66blank_value?(color&.strip) ? '' : "%bld%#{color.strip}"67end68end6970def print_header(direction, message)71@logger.print_line('#' * 20)72@logger.print_line("# Kerberos #{direction}: #{message_type_name(message)}")73@logger.print_line('#' * 20)74end7576def print_credential_header(source)77@logger.print_line('#' * 20)78@logger.print_line("# Kerberos Credential#{source ? ": #{source}" : ''}")79@logger.print_line('#' * 20)80end8182def message_type_name(message)83msg_type = message.msg_type if message.respond_to?(:msg_type)84case msg_type85when Rex::Proto::Kerberos::Model::AS_REQ86'AS-REQ'87when Rex::Proto::Kerberos::Model::AS_REP88'AS-REP'89when Rex::Proto::Kerberos::Model::TGS_REQ90'TGS-REQ'91when Rex::Proto::Kerberos::Model::TGS_REP92'TGS-REP'93when Rex::Proto::Kerberos::Model::AP_REQ94'AP-REQ'95when Rex::Proto::Kerberos::Model::AP_REP96'AP-REP'97when Rex::Proto::Kerberos::Model::KRB_ERROR98'KRB-ERROR'99when nil100'UNKNOWN'101else102"UNKNOWN (#{msg_type})"103end104end105106def format_message(message)107return 'null' if message.nil?108109if message.respond_to?(:attributes)110serialized_message = serialize_element(message)111readable_text_presenter.present(serialized_message)112else113# Fall back for non-model objects.114message.to_s115end116rescue StandardError => e117"Kerberos trace rendering error: #{e.class}: #{e.message}"118end119120def format_credential(credential)121rendered_credential = ticket_presenter.present_cred(credential)122[123'Creds: 1',124" Credential[0]:\n#{rendered_credential.indent(4)}"125].join("\n")126rescue StandardError => e127"Credential presenter error: #{e.class}: #{e.message}"128end129130def serialize_element(element)131element.attributes.each_with_object({}) do |attribute, output|132value = element.public_send(attribute)133next if value.nil?134135output[serialized_attribute_key(element, attribute)] = serialize_value(value, element: element, attribute: attribute.to_sym)136end137end138139def serialized_attribute_key(element, attribute)140if attribute == :options && element.is_a?(Rex::Proto::Kerberos::Model::ApReq)141'ap_options'142elsif attribute == :options && element.is_a?(Rex::Proto::Kerberos::Model::KdcRequestBody)143'kdc_options'144else145attribute.to_s146end147end148149def serialize_value(value, element: nil, attribute: nil)150if value.respond_to?(:attributes)151# Recursively serialize nested Kerberos model objects.152serialize_element(value)153elsif kerberos_error_code?(value)154# Normalize ErrorCode-like objects to a compact structured form.155{156'name' => value.name,157'value' => value.value,158'description' => value.description159}160else161serialize_scalar_value(value, element: element, attribute: attribute)162end163end164165def serialize_scalar_value(value, element: nil, attribute: nil)166case value167when Array168value.map { |entry| serialize_value(entry, element: element, attribute: attribute) }169when Set170value.to_a.map { |entry| serialize_value(entry, element: element, attribute: attribute) }171when Hash172value.each_with_object({}) do |(key, entry), output|173output[key.to_s] = serialize_value(entry)174end175when Rex::Proto::Kerberos::Model::KerberosFlags176{177'value' => value.to_i,178'flags' => value.enabled_flag_names.map(&:to_s)179}180when Time181value.utc.iso8601182when String183serialize_string(value)184when Symbol185value.to_s186when Integer187serialize_enum_value(value, element: element, attribute: attribute) || value188when Float, TrueClass, FalseClass, NilClass189value190else191value.to_s192end193end194195def serialize_enum_value(value, element:, attribute:)196enum_name = case attribute197when :msg_type198message_type_name_for_value(value)199when :type200enum_type_name(value, element)201when :etype202enum_etype_name(value)203when :name_type204enum_name_type_name(value, element)205end206return nil if enum_name.nil?207208"#{value} (#{enum_name})"209end210211def message_type_name_for_value(msg_type)212case msg_type213when Rex::Proto::Kerberos::Model::AS_REQ214'AS-REQ'215when Rex::Proto::Kerberos::Model::AS_REP216'AS-REP'217when Rex::Proto::Kerberos::Model::TGS_REQ218'TGS-REQ'219when Rex::Proto::Kerberos::Model::TGS_REP220'TGS-REP'221when Rex::Proto::Kerberos::Model::AP_REQ222'AP-REQ'223when Rex::Proto::Kerberos::Model::AP_REP224'AP-REP'225when Rex::Proto::Kerberos::Model::KRB_ERROR226'KRB-ERROR'227else228'UNKNOWN'229end230end231232def enum_type_name(value, element)233if element.is_a?(Rex::Proto::Kerberos::Model::PreAuthDataEntry)234const_name_for_value(Rex::Proto::Kerberos::Model::PreAuthType, value)235elsif element.is_a?(Rex::Proto::Kerberos::Model::EncryptionKey)236enum_etype_name(value)237end238end239240def enum_etype_name(value)241Rex::Proto::Kerberos::Crypto::Encryption.const_name(value) || 'UNKNOWN'242end243244def enum_name_type_name(value, element)245return nil unless element.is_a?(Rex::Proto::Kerberos::Model::PrincipalName)246247const_name_for_value(Rex::Proto::Kerberos::Model::NameType, value)248end249250def const_name_for_value(mod, value)251mod.constants.each do |const_name|252return const_name.to_s if mod.const_get(const_name) == value253rescue StandardError254next255end256257'UNKNOWN'258end259260def kerberos_error_code?(value)261value.respond_to?(:name) && value.respond_to?(:value) && value.respond_to?(:description)262end263264def serialize_string(value)265return value if printable_string?(value)266267# Expand binary/non-printable strings fully in hex.268"[binary #{value.bytesize} bytes: #{value.unpack1('H*')}]"269end270271def ticket_presenter272@ticket_presenter ||= Rex::Proto::Kerberos::CredentialCache::Krb5CcachePresenter.new(nil)273end274275def readable_text_presenter276@readable_text_presenter ||= Rex::Proto::Kerberos::KerberosReadableTextPresenter.new277end278279def printable_string?(value)280utf8_value = value.dup.force_encoding(::Encoding::UTF_8)281utf8_value.valid_encoding? && utf8_value.match?(/\A[[:print:]\r\n\t ]*\z/)282rescue ::Encoding::CompatibilityError283false284end285286def blank_value?(value)287# Avoid depending on ActiveSupport's `blank?` for this Rex-level helper.288return true if value.nil? || value == false289return value.strip.empty? if value.respond_to?(:strip)290return value.empty? if value.respond_to?(:empty?)291292false293end294end295end296end297end298299300