Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DCSync using Kerberos #18419

Merged
merged 8 commits into from
Oct 30, 2023
Merged
Show file tree
Hide file tree
Changes from 3 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
114 changes: 114 additions & 0 deletions lib/msf/core/exploit/remote/dcerpc/kerberos_authentication.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# -*- coding: binary -*-

#
# This class implements an override for RubySMB's default authentication method to instead
# use a kerberos authenticator
#
module Msf::Exploit::Remote::DCERPC::KerberosAuthentication
# @param [Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::SMB] kerberos_authenticator The authenticator to make the required Kerberos requests
def kerberos_authenticator=(kerberos_authenticator)
@kerberos_authenticator = kerberos_authenticator
end

def auth_provider_init
kerberos_result = @kerberos_authenticator.authenticate
@application_key = @session_key = kerberos_result[:session_key]
@client_sequence_number = kerberos_result[:client_sequence_number]
kerberos_result[:security_blob]
end

def auth_provider_encrypt_and_sign(dcerpc_req)
auth_pad_length = get_auth_padding_length(dcerpc_req.stub.to_binary_s.length)
plain_stub = dcerpc_req.stub.to_binary_s + "\x00" * auth_pad_length
emessage, header_length, krb_pad_length = self.krb_encryptor.encrypt_and_increment(plain_stub)

encrypted_stub = emessage[header_length..-1]
signature = emessage[0,header_length]
dcerpc_req.sec_trailer.auth_pad_length = auth_pad_length
smashery marked this conversation as resolved.
Show resolved Hide resolved
set_encrypted_packet(dcerpc_req, encrypted_stub, auth_pad_length)
set_signature_on_packet(dcerpc_req, signature)
end

def auth_provider_decrypt_and_verify(dcerpc_response)
encrypted_stub = get_response_full_stub(dcerpc_response)
signature = dcerpc_response.auth_value
data = signature + encrypted_stub

begin
result = self.krb_encryptor.decrypt_and_verify(data)
rescue Rex::Proto::Kerberos::Model::Error::KerberosError
return false
end
set_decrypted_packet(dcerpc_response, result)
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand this correctly, self.krb_encryptor.decrypt_and_verify should only verify if the decrypted data is correct (checksum, sequence number, etc.). I believe the signature in the DCERPC response is not verified. This method is supposed to also verify the signature and return a boolean.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

decrypt_and_verify passes off to the respective Kerberos encryption routines to do the decryption+verification. These throw exceptions if it's invalid (error message will be "HMAC integrity error". So the rescue above will catch that and return false. But good catch - function needs to return true if it passes through without error.


def build_ap_rep(session_key, sequence_number)
pvno = Rex::Proto::Kerberos::Model::VERSION
msg_type = Rex::Proto::Kerberos::Model::AP_REP
ctime = Time.now.utc
cusec = ctime&.usec

encrypted_part = Rex::Proto::Kerberos::Model::EncApRepPart.new(
ctime: ctime,
cusec: cusec,
sequence_number: sequence_number,
enc_key_usage: Rex::Proto::Kerberos::Crypto::KeyUsage::AP_REP_ENCPART
)
enc_aprep = Rex::Proto::Kerberos::Model::EncryptedData.new(
etype: session_key.type,
cipher: encrypted_part.encrypt(session_key.type, session_key.value)
)

Rex::Proto::Kerberos::Model::ApRep.new(
pvno: pvno,
msg_type: msg_type,
enc_part: enc_aprep
)
end

def auth_provider_complete_handshake(response, options)
smashery marked this conversation as resolved.
Show resolved Hide resolved
@kerberos_authenticator.validate_response!(response.auth_value, accept_incomplete: true)
gss_api = OpenSSL::ASN1.decode(response.auth_value)
security_blob = ::RubySMB::Gss.asn1dig(gss_api, 0, 2, 0)&.value
ap_rep = Rex::Proto::Kerberos::Model::ApRep.decode(security_blob)
ap_rep_enc_part = ap_rep.decrypt_enc_part(@session_key.value)
server_sequence_number = ap_rep_enc_part.sequence_number
# Now complete the handshake - see [MS-KILE] 3.4.5.1 - https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-kile/190ab8de-dc42-49cf-bf1b-ea5705b7a087
response_ap_rep = build_ap_rep(@session_key, server_sequence_number)

wrapped_ap_rep = OpenSSL::ASN1::ASN1Data.new([
OpenSSL::ASN1::Sequence.new([
OpenSSL::ASN1::ASN1Data.new([
OpenSSL::ASN1::OctetString(response_ap_rep.encode)
], 2, :CONTEXT_SPECIFIC)
])
], 1, :CONTEXT_SPECIFIC).to_der

alter_ctx = RubySMB::Dcerpc::Bind.new(options)
# Alter context (3rd message) is identical to Bind requests by definition
alter_ctx.pdu_header.ptype = RubySMB::Dcerpc::PTypes::ALTER_CONTEXT
alter_ctx.pdu_header.call_id = @call_id
smashery marked this conversation as resolved.
Show resolved Hide resolved

add_auth_verifier(alter_ctx, wrapped_ap_rep)

send_packet(alter_ctx)

begin
dcerpc_response = recv_struct(RubySMB::Dcerpc::AlterContextResp)
rescue RubySMB::Dcerpc::Error::InvalidPacket
raise RubySMB::Dcerpc::Error::BindError # raise the more context-specific BindError
smashery marked this conversation as resolved.
Show resolved Hide resolved
end

self.krb_encryptor = @kerberos_authenticator.get_message_encryptor(ap_rep_enc_part.subkey,
@client_sequence_number,
server_sequence_number)
# Set the session key value on the parent class - needed for decrypting attribute values in e.g. DRSR
@session_key = ap_rep_enc_part.subkey.value
smashery marked this conversation as resolved.
Show resolved Hide resolved
end

def get_auth_padding_length(plaintext_len)
(16 - (self.krb_encryptor.calculate_encrypted_length(plaintext_len) % 16)) % 16
end

attr_accessor :krb_encryptor
end
42 changes: 27 additions & 15 deletions lib/msf/core/exploit/remote/kerberos/service_authenticator/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ class Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Base
# @return [String] whether to send delegated creds (from the set Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Base::Delegation)
attr_reader :send_delegated_creds

# @!attribute [r] is_dcerpc
# @return [Boolean] Whether this encryptor will be used for DCERPC purposes (since the behaviour is subtly different)
attr_reader :is_dcerpc

# @!attribute [r] ticket_storage
# @return [Msf::Exploit::Remote::Kerberos::Ticket::Storage::Base] the ticket storage driver
attr_reader :ticket_storage
Expand All @@ -89,12 +93,13 @@ class Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Base
:workspace

# Flags - https://datatracker.ietf.org/doc/html/rfc4121#section-4.1.1.1
GSS_DELEGATE = 1
GSS_MUTUAL = 2
GSS_REPLAY_DETECT = 4
GSS_SEQUENCE = 8
GSS_CONFIDENTIAL = 16
GSS_INTEGRITY = 32
GSS_DELEGATE = 0x01
GSS_MUTUAL = 0x02
GSS_REPLAY_DETECT = 0x04
GSS_SEQUENCE = 0x08
GSS_CONFIDENTIAL = 0x10
GSS_INTEGRITY = 0x20
GSS_DCE_STYLE = 0x1000

module Delegation
ALWAYS = 'always' # Always send delegated creds
Expand All @@ -117,6 +122,7 @@ def initialize(
use_gss_checksum: false,
mechanism: Rex::Proto::Gss::Mechanism::SPNEGO,
send_delegated_creds: Delegation::ALWAYS,
is_dcerpc: false,
cache_file: nil,
ticket_storage: nil,
key: nil,
Expand All @@ -138,6 +144,7 @@ def initialize(
@use_gss_checksum = use_gss_checksum
@mechanism = mechanism
@send_delegated_creds = send_delegated_creds
@is_dcerpc = is_dcerpc
@ticket_storage = ticket_storage || Msf::Exploit::Remote::Kerberos::Ticket::Storage::ReadWrite.new(
framework: framework,
framework_module: framework_module
Expand Down Expand Up @@ -247,7 +254,8 @@ def get_message_encryptor(key, client_sequence_number, server_sequence_number)
client_sequence_number,
server_sequence_number,
is_initiator: true,
use_acceptor_subkey: true)
use_acceptor_subkey: true,
is_dcerpc: @is_dcerpc)
end

def parse_gss_init_response(token, session_key, mechanism: 'kerberos')
Expand All @@ -258,16 +266,17 @@ def parse_gss_init_response(token, session_key, mechanism: 'kerberos')
data = encapsulated_token[2, encapsulated_token.length]
case tok_id
when TOK_ID_KRB_AP_REP
ap_req = Rex::Proto::Kerberos::Model::ApRep.decode(data)
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_req.enc_part.etype
raise ::Rex::Proto::Kerberos::Model::Error::KerberosError, 'Mismatching etypes' if session_key.type != ap_rep.enc_part.etype

decrypted = ap_req.decrypt_enc_part(session_key.value)
decrypted = ap_rep.decrypt_enc_part(session_key.value)

result = {
ap_rep_subkey: decrypted.subkey,
server_sequence_number: decrypted.sequence_number
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)
Expand All @@ -284,13 +293,14 @@ def parse_gss_init_response(token, session_key, mechanism: 'kerberos')
end

# @param security_blob [String] SPNEGO GSS Blob
# @param accept_incomplete [Boolean] Whether an Incomplete value is an acceptable response
# @raise [Rex::Proto::Kerberos::Model::Error::KerberosDecodingError] if the response was not successful
smashery marked this conversation as resolved.
Show resolved Hide resolved
def validate_response!(security_blob)
def validate_response!(security_blob, accept_incomplete: false)
gss_api = OpenSSL::ASN1.decode(security_blob)
neg_result = ::RubySMB::Gss.asn1dig(gss_api, 0, 0, 0)&.value.to_i
supported_neg = ::RubySMB::Gss.asn1dig(gss_api, 0, 1, 0)&.value

is_success = neg_result == NEG_TOKEN_ACCEPT_COMPLETED &&
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

raise ::Rex::Proto::Kerberos::Model::Error::KerberosError.new('Failed to negotiate Kerberos GSS') unless is_success
Expand Down Expand Up @@ -616,7 +626,7 @@ def authenticate_via_krb5_ccache_credential_tgs(credential, _options = {})

## Service Authentication
checksum = nil
checksum = build_gss_ap_req_checksum_value(mutual_auth, nil, nil, nil, nil, nil) if use_gss_checksum
checksum = build_gss_ap_req_checksum_value(mutual_auth, is_dcerpc, nil, nil, nil, nil, nil) if use_gss_checksum

sequence_number = rand(1 << 32)
service_ap_request = build_service_ap_request(
Expand Down Expand Up @@ -706,6 +716,7 @@ def authenticate_via_krb5_ccache_credential_tgt(credential, options = {})
if use_gss_checksum
checksum = build_gss_ap_req_checksum_value(
mutual_auth,
is_dcerpc,
delegated_tgs_ticket,
delegated_tgs_auth,
tgs_auth.key,
Expand Down Expand Up @@ -740,14 +751,15 @@ def authenticate_via_krb5_ccache_credential_tgt(credential, options = {})
}
end

def build_gss_ap_req_checksum_value(mutual_auth, ticket, decrypted_part, session_key, realm, client_name)
def build_gss_ap_req_checksum_value(mutual_auth, is_dcerpc, ticket, decrypted_part, session_key, realm, client_name)
# @see https://datatracker.ietf.org/doc/html/rfc4121#section-4.1.1
# No channel binding
channel_binding_info = "\x00" * 16
channel_binding_info_len = [channel_binding_info.length].pack('V')

flags = GSS_REPLAY_DETECT | GSS_SEQUENCE | GSS_CONFIDENTIAL | GSS_INTEGRITY
flags |= GSS_MUTUAL if mutual_auth
flags |= GSS_DCE_STYLE if is_dcerpc
flags |= GSS_DELEGATE if ticket

flags = [flags].pack('V')
Expand Down
18 changes: 16 additions & 2 deletions lib/rex/proto/gss/kerberos/message_encryptor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ class MessageEncryptor
# @param [Integer] decrypt_sequence_number The starting sequence number we expect to see when we decrypt messages
# @param [Boolean] is_initiator Are we the initiator in this communication (used for setting flags and key usage values)
# @param [Boolean] use_acceptor_subkey Are we using the subkey provided by the acceptor? (used for setting appropriate flags)
def initialize(key, encrypt_sequence_number, decrypt_sequence_number, is_initiator: true, use_acceptor_subkey: true)
def initialize(key, encrypt_sequence_number, decrypt_sequence_number, is_initiator: true, use_acceptor_subkey: true, is_dcerpc: false)
@key = key
@encrypt_sequence_number = encrypt_sequence_number
@decrypt_sequence_number = decrypt_sequence_number
@is_initiator = is_initiator
@use_acceptor_subkey = use_acceptor_subkey
@is_dcerpc = is_dcerpc
@encryptor = Rex::Proto::Kerberos::Crypto::Encryption::from_etype(key.type)
end

Expand All @@ -28,7 +29,7 @@ def initialize(key, encrypt_sequence_number, decrypt_sequence_number, is_initiat
# @return [String, Integer, Integer] The encrypted data, the length of its header, and the length of padding added to it prior to encryption
#
def encrypt_and_increment(data)
result = encryptor.gss_wrap(data, @key, @encrypt_sequence_number, @is_initiator, use_acceptor_subkey: @use_acceptor_subkey)
result = encryptor.gss_wrap(data, @key, @encrypt_sequence_number, @is_initiator, use_acceptor_subkey: @use_acceptor_subkey, is_dcerpc: @is_dcerpc)
@encrypt_sequence_number += 1

result
Expand All @@ -44,6 +45,10 @@ def decrypt_and_verify(data)
result
end

def calculate_encrypted_length(plaintext_len)
encryptor.calculate_encrypted_length(plaintext_len)
end

#
# The sequence number to use when we are encrypting, which should be incremented for each message
#
Expand All @@ -70,6 +75,15 @@ def decrypt_and_verify(data)
#
attr_accessor :use_acceptor_subkey

#
# [Boolean] Whether this encryptor will be used for DCERPC purposes (since the behaviour is subtly different)
# See MS-KILE 3.4.5.4.1 for details about the exception to the rule:
# https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-kile/e94b3acd-8415-4d0d-9786-749d0c39d550
#
# "For [MS-RPCE], the length field in the above pseudo ASN.1 header does not include the length of the concatenated data if [RFC1964] is used."
#
attr_accessor :is_dcerpc
smashery marked this conversation as resolved.
Show resolved Hide resolved

#
# [Rex::Proto::Kerberos::Crypto::*] Encryption class for encrypting/decrypting messages
#
Expand Down
4 changes: 4 additions & 0 deletions lib/rex/proto/kerberos/crypto/block_cipher_base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ def gss_unwrap(ciphertext, key, expected_sequence_number, is_initiator, use_acce
raise NotImplementedError
end

def calculate_encrypted_length(plaintext_len)
raise NotImplementedError
end

private

# Functions must be overridden by subclasses:
Expand Down
27 changes: 17 additions & 10 deletions lib/rex/proto/kerberos/crypto/gss_new_encryption_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ module GssNewEncryptionType
# Encrypt the message, wrapping it in GSS structures
# @return [String, Integer, Integer] The encrypted data, the length of its header, and the length of padding added to it prior to encryption
#
def gss_wrap(plaintext, key, sequence_number, is_initiator, use_acceptor_subkey: true)
def gss_wrap(plaintext, key, sequence_number, is_initiator, opts={})
use_acceptor_subkey = opts.fetch(:use_acceptor_subkey) { true }
# Handle wrap-around
sequence_number &= 0xFFFFFFFFFFFFFFFF

Expand All @@ -36,7 +37,7 @@ def gss_wrap(plaintext, key, sequence_number, is_initiator, use_acceptor_subkey:

tok_id = TOK_ID_GSS_WRAP
filler = 0xFF
ec = calculate_ec(plaintext)
ec = calculate_ec(plaintext.length)
rrc = calculate_rrc

# RFC4121, Section 4.2.4
Expand All @@ -48,12 +49,13 @@ def gss_wrap(plaintext, key, sequence_number, is_initiator, use_acceptor_subkey:
ec_filler = "x" * ec
plaintext = plaintext + ec_filler + plaintext_header
ciphertext = self.encrypt(plaintext, key.value, key_usage)
rotated = rotate(ciphertext, rrc)
rotated = rotate(ciphertext, rrc+ec)

result = [header + rotated, header_length, ec]
result = [header + rotated, header_length + ec, ec]
end

def gss_unwrap(ciphertext, key, expected_sequence_number, is_initiator, use_acceptor_subkey: true)
def gss_unwrap(ciphertext, key, expected_sequence_number, is_initiator, opts={})
use_acceptor_subkey = opts.fetch(:use_acceptor_subkey) { true }
# Handle wrap-around
sequence_number &= 0xFFFFFFFFFFFFFFFF

Expand All @@ -76,11 +78,16 @@ def gss_unwrap(ciphertext, key, expected_sequence_number, is_initiator, use_acce
raise Rex::Proto::Kerberos::Model::Error::KerberosError, "Invalid sequence number (received #{snd_seq}; expected #{expected_sequence_number})" unless expected_sequence_number == snd_seq

# Could do some sanity checking here of those values
ciphertext = rotate(ciphertext, -rrc)
ciphertext = rotate(ciphertext, -(rrc+ec))

plaintext = self.decrypt(ciphertext, key.value, key_usage)

plaintext = plaintext[0, plaintext.length - ec - GSS_HEADER_LEN]
plaintext
end

def calculate_encrypted_length(plaintext_len)
plaintext_len
end

private
Expand All @@ -102,13 +109,13 @@ def calculate_rrc
# need to add "residue" (padding). This seems to be relevant only to DES,
# which leave padding removal as an exercise to the user (AES strips the padding
# prior to returning it)
def calculate_ec(plaintext)
padding_size = self.class::PADDING_SIZE
def calculate_ec(plaintext_len)
padding_size = 1
if padding_size == 0
# No padding, so don't need to buffer up to a multiple of the pad length
0
else
(padding_size - (plaintext.length + GSS_HEADER_LEN)) % padding_size
(padding_size - (plaintext_len + GSS_HEADER_LEN)) % padding_size
end
end

Expand All @@ -128,7 +135,7 @@ def rotate(ciphertext, rrc)
# The length of the encrypted header portion of the message.
# This includes information that is part of the encryption process, such as
# confounders, padding, and checksums. As a result, it is dependent on the
# encyrption algorithm.
# encryption algorithm.
# This is defined in MS-WSMV section 2.2.9.1.1.2.2
def header_length
GSS_HEADER_LEN + GSS_HEADER_LEN + self.header_byte_count + self.trailing_byte_count
Expand Down
Loading
Loading