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 @@ -49,7 +49,7 @@ def initialize(info = {})
register_advanced_options(
[
OptTimedelta.new('KrbClockSkew', [true, 'Adjust Kerberos client clock by this offset (e.g. 90s, -5m, 1h)', '0s']),
OptEnum.new('KerberosTicketTrace', [false, 'Kerberos ticket trace mode', 'off', %w[off metadata ticket full]]),
OptEnum.new('KerberosTicketTrace', [false, 'Kerberos protocol 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
272 changes: 255 additions & 17 deletions lib/msf/core/exploit/remote/kerberos/service_authenticator/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -288,20 +288,43 @@ def authenticate(options = {})
auth_context = authenticate_via_krb5_ccache_credential_tgt(auth_context[:credential], options)
end

service_ap_request = auth_context.delete(:service_ap_request)
service_ap_request = auth_context[:service_ap_request]
trace_context = service_authentication_trace_context(auth_context, options)
kerberos_trace_subscriber.on_ap_req(trace_context)
kerberos_trace_subscriber.on_request(service_ap_request)
ap_request_asn1 = service_ap_request.to_asn1
ap_request_der = ap_request_asn1.to_der
gss_kerberos_token = encode_gss_kerberos_ap_request(ap_request_asn1, ap_request_der: ap_request_der)
kerberos_trace_subscriber.on_gss_token(
trace_context.merge(
token: gss_kerberos_token,
ap_req_der: ap_request_der,
direction: 'outbound'
)
)

mechanism = options.fetch(:mechanism) { self.mechanism }
if mechanism == Rex::Proto::Gss::Mechanism::SPNEGO
security_blob = encode_gss_spnego_ap_request(ap_request_asn1)
security_blob = encode_gss_spnego_ap_request(ap_request_asn1, ap_request_mech: gss_kerberos_token)
kerberos_trace_subscriber.on_spnego_token(
trace_context.merge(
token: security_blob,
gss_token: gss_kerberos_token,
ap_req_der: ap_request_der,
direction: 'outbound'
)
)
elsif mechanism == Rex::Proto::Gss::Mechanism::KERBEROS
security_blob = encode_gss_kerberos_ap_request(ap_request_asn1)
security_blob = gss_kerberos_token
else
raise RuntimeError, "Unknown GSS mechanism: #{mechanism}"
end

auth_context[:security_blob] = security_blob
auth_context.delete(:service_ap_request)
auth_context.delete(:authenticator)
auth_context.delete(:trace_credential)
auth_context.delete(:delegated_credentials)
auth_context
end

Expand All @@ -316,38 +339,111 @@ def get_message_encryptor(key, client_sequence_number, server_sequence_number, u
end

def parse_gss_init_response(token, session_key)
response_trace_emitted = false
mech_id, encapsulated_token = unwrap_pseudo_asn1(token)

if mech_id.value == Rex::Proto::Gss::OID_KERBEROS_5.value
tok_id = encapsulated_token[0,2]
tok_id = encapsulated_token[0, 2]
data = encapsulated_token[2, encapsulated_token.length]
case tok_id
when TOK_ID_KRB_AP_REP
ap_rep = Rex::Proto::Kerberos::Model::ApRep.decode(data)
print_good("#{peer} - Received AP-REQ. Extracting session key...")

raise ::Rex::Proto::Kerberos::Model::Error::KerberosError, 'Mismatching etypes' if session_key.type != ap_rep.enc_part.etype
print_good("#{peer} - Received AP-REP. Extracting session key...")

if session_key.type != ap_rep.enc_part.etype
trace_response_token(
token: token,
token_type: 'GSS-Kerberos',
carrier: 'GSS init response',
message: ap_rep,
session_key: session_key,
parse_failure: 'unable to decrypt AP-REP encrypted part: mismatching etypes',
mutual_authentication: mutual_authentication_trace_result(false)
)
response_trace_emitted = true
raise ::Rex::Proto::Kerberos::Model::Error::KerberosError, 'Mismatching etypes'
end

decrypted = ap_rep.decrypt_enc_part(session_key.value)
trace_response_token(
token: token,
token_type: 'GSS-Kerberos',
carrier: 'GSS init response',
message: ap_rep,
decrypted_part: decrypted,
session_key: session_key,
mutual_authentication: mutual_authentication_trace_result(true)
)
response_trace_emitted = true

result = {
{
ap_rep_subkey: decrypted.subkey,
server_sequence_number: decrypted.sequence_number,
etype: ap_rep.enc_part.etype
}
when TOK_ID_KRB_ERROR
krb_err = Rex::Proto::Kerberos::Model::KrbError.decode(data)
trace_response_token(
token: token,
token_type: 'GSS-Kerberos',
carrier: 'GSS init response',
message: krb_err,
session_key: session_key,
mutual_authentication: mutual_authentication_trace_result(false)
)
response_trace_emitted = true
kerberos_trace_subscriber.on_response(krb_err)
print_error("#{peer} - Received KRB-ERR.")

raise ::Rex::Proto::Kerberos::Model::Error::KerberosError.new(res: krb_err)
else
trace_response_token(
token: token,
token_type: 'GSS-Kerberos',
carrier: 'GSS init response',
parse_failure: "unable to parse response token as AP-REP/KRB-ERROR/SPNEGO: unknown token id #{tok_id.inspect}",
mutual_authentication: mutual_authentication_trace_result(false)
)
response_trace_emitted = true
raise ::Rex::Proto::Kerberos::Model::Error::KerberosError, "Unknown token id: #{tok_id.inspect}"
end
else
trace_response_token(
token: token,
token_type: 'GSS-Kerberos',
carrier: 'GSS init response',
parse_failure: "Parsing mechtype #{mech_id.value} not supported",
mutual_authentication: mutual_authentication_trace_result(false)
)
response_trace_emitted = true
raise ::NotImplementedError, "Parsing mechtype #{mech_id.value} not supported"
end

rescue ::Rex::Proto::Kerberos::Model::Error::KerberosError => e
unless response_trace_emitted
trace_response_token(
token: token,
token_type: 'GSS-Kerberos',
carrier: 'GSS init response',
session_key: session_key,
parse_failure: "unable to parse response token as AP-REP/KRB-ERROR/SPNEGO: #{e.class}: #{e.message}",
mutual_authentication: mutual_authentication_trace_result(false)
)
end
raise
rescue ::NotImplementedError
raise
rescue StandardError => e
unless response_trace_emitted
trace_response_token(
token: token,
token_type: 'GSS-Kerberos',
carrier: 'GSS init response',
session_key: session_key,
parse_failure: "unable to parse response token as AP-REP/KRB-ERROR/SPNEGO: #{e.class}: #{e.message}",
mutual_authentication: mutual_authentication_trace_result(false)
)
end
raise
end

# @param security_blob [String] SPNEGO GSS Blob
Expand All @@ -360,13 +456,29 @@ def validate_response!(security_blob, accept_incomplete: false)
neg_result = ::RubySMB::Gss.asn1dig(gss_api, 0, 0, 0)&.value.to_i
supported_neg = ::RubySMB::Gss.asn1dig(gss_api, 0, 1, 0)&.value
rescue OpenSSL::ASN1::ASN1Error
raise ::Rex::Proto::Kerberos::Model::Error::KerberosDecodingError.new('Invalid GSS Response')
trace_response_token(
token: security_blob,
token_type: 'SPNEGO NegTokenResp',
carrier: 'GSS/SPNEGO response',
parse_failure: 'unable to parse response token as SPNEGO NegTokenResp',
mutual_authentication: mutual_authentication_trace_result(false)
)
raise ::Rex::Proto::Kerberos::Model::Error::KerberosDecodingError, 'Invalid GSS Response'
end

is_success = (neg_result == NEG_TOKEN_ACCEPT_COMPLETED || (accept_incomplete && neg_result == NEG_TOKEN_ACCEPT_INCOMPLETE)) &&
supported_neg == ::Rex::Proto::Gss::OID_MICROSOFT_KERBEROS_5.value
is_kerberos = supported_neg == ::Rex::Proto::Gss::OID_MICROSOFT_KERBEROS_5.value
is_complete = neg_result == NEG_TOKEN_ACCEPT_COMPLETED && is_kerberos
is_incomplete = accept_incomplete && neg_result == NEG_TOKEN_ACCEPT_INCOMPLETE && is_kerberos
is_success = is_complete || is_incomplete

trace_response_token(
token: security_blob,
token_type: 'SPNEGO NegTokenResp',
carrier: 'GSS/SPNEGO response',
mutual_authentication: spnego_mutual_authentication_trace_result(is_success)
)
Comment thread
eve0805 marked this conversation as resolved.

raise ::Rex::Proto::Kerberos::Model::Error::KerberosError.new('Failed to negotiate Kerberos GSS') unless is_success
raise ::Rex::Proto::Kerberos::Model::Error::KerberosError, 'Failed to negotiate Kerberos GSS' unless is_success

is_success
end
Expand Down Expand Up @@ -801,6 +913,102 @@ def request_service_ticket(session_key, tgt_ticket, realm, client_name, etypes,

private

def service_authentication_trace_context(auth_context, options)
ap_req = auth_context[:service_ap_request]
authenticator = auth_context[:authenticator]
ticket = ap_req&.ticket
sname = options[:sname] || ticket&.sname

service_trace_context.merge(
ap_req: ap_req,
authenticator: authenticator,
credential: auth_context.fetch(:trace_credential) { auth_context[:credential] },
ticket: ticket,
session_key: auth_context[:session_key],
sequence_number: auth_context[:client_sequence_number],
client_principal: client_principal_for_trace(authenticator),
service_principal: service_principal_for_trace(sname, ticket&.realm),
spn: sname&.to_s,
realm: ticket&.realm || realm&.upcase,
mutual_auth: mutual_auth,
use_session_key: authenticator&.subkey.nil?,
use_subkey: !authenticator&.subkey.nil?,
replay_detection: use_gss_checksum ? 'requested by GSS checksum flags' : nil,
channel_binding: channel_binding_trace(options[:gss_channel_binding]),
delegated_credentials: auth_context[:delegated_credentials]
).compact
end

def service_trace_context
{
realm: realm&.upcase,
mutual_auth: mutual_auth,
service_principal: service_principal_for_trace(build_spn, realm&.upcase),
host: hostname || host
}.compact
rescue StandardError
{
realm: realm&.upcase,
mutual_auth: mutual_auth,
host: hostname || host
}.compact
end

def trace_response_token(metadata)
kerberos_trace_subscriber.on_response_token(
service_trace_context.merge(
{
direction: 'inbound'
}.merge(metadata)
)
)
rescue StandardError => e
vprint_warning("Unable to trace Kerberos response token: #{e.class}: #{e.message}")
nil
end

def mutual_authentication_trace_result(success, incomplete: false)
return nil unless mutual_auth
return 'incomplete' if incomplete

success ? 'success' : 'failed'
end

def spnego_mutual_authentication_trace_result(negotiation_success)
return nil unless mutual_auth

negotiation_success ? 'incomplete' : 'failed'
end

def client_principal_for_trace(authenticator)
cname = authenticator&.cname
crealm = authenticator&.crealm || realm&.upcase
return "#{username}@#{crealm}" if cname.nil? && username

principal_with_realm(cname, crealm)
end

def service_principal_for_trace(principal, principal_realm)
principal_with_realm(principal, principal_realm || realm&.upcase)
end

def principal_with_realm(principal, principal_realm)
name = principal&.to_s
return nil if name.nil?

principal_realm ? "#{name}@#{principal_realm}" : name
end

def channel_binding_trace(channel_binding)
return { present: false } unless channel_binding

token = channel_binding.channel_binding_token if channel_binding.respond_to?(:channel_binding_token)
{
present: true,
length: token&.bytesize
}.compact
end

# Authenticate with a ticket-granting-service (TGS). This method will not contact the KDC and can not request a
# delegation ticket.
#
Expand Down Expand Up @@ -835,7 +1043,7 @@ def authenticate_via_krb5_ccache_credential_tgs(credential, options = {})
checksum = build_gss_ap_req_checksum_value(options: options) if use_gss_checksum

sequence_number = rand(1 << 32)
service_ap_request = build_service_ap_request(
service_ap_request_options = {
session_key: tgs_auth_key,
checksum: checksum,
ticket: tgs_ticket,
Expand All @@ -850,12 +1058,26 @@ def authenticate_via_krb5_ccache_credential_tgs(credential, options = {})
]
),
sequence_number: sequence_number
}
authenticator = build_authenticator(
service_ap_request_options.merge(
subkey: nil,
authenticator_enc_key_usage: Rex::Proto::Kerberos::Crypto::KeyUsage::AP_REQ_AUTHENTICATOR
)
)
service_ap_request = build_service_ap_request(service_ap_request_options.merge(authenticator: authenticator))

{
service_ap_request: service_ap_request,
authenticator: authenticator,
session_key: tgs_auth_key,
client_sequence_number: sequence_number
client_sequence_number: sequence_number,
trace_credential: credential,
delegated_credentials: {
mode: send_delegated_creds,
requested: do_delegation == true,
present: false
}
}
end

Expand Down Expand Up @@ -897,6 +1119,8 @@ def authenticate_via_krb5_ccache_credential_tgt(credential, options = {})
sname,
tgs_options
)
# Delegation may replace `credential` below, but the AP-REQ continues to use this service ticket.
service_credential = credential

case send_delegated_creds
when Delegation::ALWAYS
Expand Down Expand Up @@ -932,7 +1156,7 @@ def authenticate_via_krb5_ccache_credential_tgt(credential, options = {})
end

sequence_number = rand(1 << 32)
service_ap_request = build_service_ap_request(
service_ap_request_options = {
session_key: tgs_auth.key,
checksum: checksum,
ticket: tgs_ticket,
Expand All @@ -948,13 +1172,27 @@ def authenticate_via_krb5_ccache_credential_tgt(credential, options = {})
),
sequence_number: sequence_number,
subkey_type: ticket.enc_part.etype # The AP-REP will come back with this same type of subkey
}
authenticator = build_authenticator(
service_ap_request_options.merge(
subkey: nil,
authenticator_enc_key_usage: Rex::Proto::Kerberos::Crypto::KeyUsage::AP_REQ_AUTHENTICATOR
)
)
service_ap_request = build_service_ap_request(service_ap_request_options.merge(authenticator: authenticator))

{
service_ap_request: service_ap_request,
authenticator: authenticator,
session_key: tgs_auth.key,
client_sequence_number: sequence_number,
credential: credential
credential: credential,
trace_credential: service_credential,
delegated_credentials: {
mode: send_delegated_creds,
requested: do_delegation == true,
present: !delegated_tgs_ticket.nil?
}
}
end

Expand Down
Loading
Loading