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 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 Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,7 @@ GEM
ruby-progressbar (1.13.0)
ruby-rc4 (0.1.5)
ruby2_keywords (0.0.5)
ruby_smb (3.2.5)
ruby_smb (3.2.6)
bindata
openssl-ccm
openssl-cmac
Expand Down
2 changes: 1 addition & 1 deletion LICENSE_GEMS
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ ruby-prof, 1.4.2, "Simplified BSD"
ruby-progressbar, 1.13.0, MIT
ruby-rc4, 0.1.5, MIT
ruby2_keywords, 0.0.5, "ruby, Simplified BSD"
ruby_smb, 3.2.5, "New BSD"
ruby_smb, 3.2.6, "New BSD"
rubyntlm, 0.6.3, MIT
rubyzip, 2.3.2, "Simplified BSD"
sawyer, 0.9.2, MIT
Expand Down
133 changes: 133 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,133 @@
# -*- 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

# Initialize the auth provider using Kerberos
# @return Serialized message for initializing the auth provider
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

# Encrypt the value in dcerpc_req, and add a valid signature to the request.
# This function modifies the request object in-place, and does not return anything.
# @param dcerpc_req [Request] The Request object to be encrypted and signed in-place
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]
set_encrypted_packet(dcerpc_req, encrypted_stub, auth_pad_length)
set_signature_on_packet(dcerpc_req, signature)
end

# Decrypt the value in dcerpc_response, and validate its signature.
# This function modifies the request object in-place, and returns whether the signature was valid.
# @param dcerpc_response [Response] The Response packet to decrypt and verify in-place
# @raise ArgumentError If the auth type is not SPNEGO (which ultimately wraps Kerberos)
# @return [Boolean] Is the packet's signature valid?
def auth_provider_decrypt_and_verify(dcerpc_response)
auth_type = dcerpc_response.sec_trailer.auth_type
unless [RubySMB::Dcerpc::RPC_C_AUTHN_GSS_NEGOTIATE].include?(auth_type)
raise ArgumentError, "Unsupported Auth Type: #{dcerpc_response.sec_trailer.auth_type}"
end
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)

true
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)
begin
@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)
rescue ::Rex::Proto::Kerberos::Model::Error::KerberosDecodingError,
::Rex::Proto::Kerberos::Model::Error::KerberosError,
OpenSSL::ASN1::ASN1Error => e
raise RubySMB::Dcerpc::Error::BindError, e.message # raise the more context-specific BindError
end
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::AlterContext.new(options)
alter_ctx.pdu_header.call_id = @call_id

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, e.message # raise the more context-specific BindError
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
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
55 changes: 36 additions & 19 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] dce_style
# @return [Boolean] Whether this encryptor will be used for DCERPC purposes (since the behaviour is subtly different)
attr_reader :dce_style

# @!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,
dce_style: 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
@dce_style = dce_style
@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,
dce_style: @dce_style)
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,19 @@ def parse_gss_init_response(token, session_key, mechanism: 'kerberos')
end

# @param security_blob [String] SPNEGO GSS Blob
# @raise [Rex::Proto::Kerberos::Model::Error::KerberosDecodingError] if the response was not successful
def validate_response!(security_blob)
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
# @param accept_incomplete [Boolean] Whether an Incomplete value is an acceptable response
# @raise [Rex::Proto::Kerberos::Model::Error::KerberosError] if the response was not successful
# @raise [Rex::Proto::Kerberos::Model::Error::KerberosDecodingError] if the response was invalid per the Kerberos/GSS protocol
def validate_response!(security_blob, accept_incomplete: false)
begin
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
rescue OpenSSL::ASN1::ASN1Error
raise ::Rex::Proto::Kerberos::Model::Error::KerberosDecodingError.new('Invalid GSS Response')
end

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 +631,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, dce_style, 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 +721,7 @@ def authenticate_via_krb5_ccache_credential_tgt(credential, options = {})
if use_gss_checksum
checksum = build_gss_ap_req_checksum_value(
mutual_auth,
dce_style,
delegated_tgs_ticket,
delegated_tgs_auth,
tgs_auth.key,
Expand Down Expand Up @@ -740,14 +756,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, dce_style, 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 dce_style
flags |= GSS_DELEGATE if ticket

flags = [flags].pack('V')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@ def kerberos_authenticator=(kerberos_authenticator)
def authenticate
raise ::RubySMB::Error::AuthenticationFailure, "Missing negotiation security buffer" if negotiation_security_buffer.nil?

gss_api = OpenSSL::ASN1.decode(negotiation_security_buffer)
mech_types = RubySMB::Gss.asn1dig(gss_api, 1, 0, 0, 0)&.value || []
has_kerberos_gss_mech_type = mech_types&.any? { |mech_type| mech_type.value == ::Rex::Proto::Gss::OID_MICROSOFT_KERBEROS_5.value }
begin
gss_api = OpenSSL::ASN1.decode(negotiation_security_buffer)
mech_types = RubySMB::Gss.asn1dig(gss_api, 1, 0, 0, 0)&.value || []
has_kerberos_gss_mech_type = mech_types&.any? { |mech_type| mech_type.value == ::Rex::Proto::Gss::OID_MICROSOFT_KERBEROS_5.value }
rescue OpenSSL::ASN1::ASN1Error
raise ::Rex::Proto::Kerberos::Model::Error::KerberosDecodingError.new('Invalid GSS Response')
end

error = "Unable to negotiate kerberos with the remote host. Expected oid #{::Rex::Proto::Gss::OID_MICROSOFT_KERBEROS_5.value} in #{mech_types.map(&:value).inspect}"
raise ::RubySMB::Error::AuthenticationFailure, error unless has_kerberos_gss_mech_type
Expand Down
19 changes: 17 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,14 @@ 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)
# @param [Boolean] dce_style Is the format of the encrypted blob DCE-style?
def initialize(key, encrypt_sequence_number, decrypt_sequence_number, is_initiator: true, use_acceptor_subkey: true, dce_style: 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
@dce_style = dce_style
@encryptor = Rex::Proto::Kerberos::Crypto::Encryption::from_etype(key.type)
end

Expand All @@ -28,7 +30,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, dce_style: @dce_style)
@encrypt_sequence_number += 1

result
Expand All @@ -44,6 +46,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 +76,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 :dce_style

#
# [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
Loading