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

Enable Kerberos auth for DCERPC #253

Merged
merged 9 commits into from
Oct 25, 2023
270 changes: 150 additions & 120 deletions lib/ruby_smb/dcerpc.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,106 @@ module Dcerpc
require 'ruby_smb/dcerpc/print_system'
require 'ruby_smb/dcerpc/encrypting_file_system'

# Initialize the auth provider using NTLM. This function should be overriden for other providers (e.g. Kerberos, etc.)
smashery marked this conversation as resolved.
Show resolved Hide resolved
def auth_provider_init
raise ArgumentError, "NTLM Client not initialized. Username and password must be provided" unless @ntlm_client
type1_message = @ntlm_client.init_context

type1_message.serialize
end

# Encrypt the value in dcerpc_req.stub, and add a valid signature to the request.
# This function modifies the request object in-place, and does not return anything.
# This function should be overriden for other providers (e.g. Kerberos, etc.)
smashery marked this conversation as resolved.
Show resolved Hide resolved
def auth_provider_encrypt_and_sign(dcerpc_req)
auth_type = dcerpc_req.sec_trailer.auth_type
auth_level = dcerpc_req.sec_trailer.auth_level
unless [RPC_C_AUTHN_WINNT, RPC_C_AUTHN_DEFAULT].include?(auth_type)
raise ArgumentError, "Unsupported Auth Type: #{dcerpc_req.sec_trailer.auth_type}"
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
raise ArgumentError, "Unsupported Auth Type: #{dcerpc_req.sec_trailer.auth_type}"
raise ArgumentError, "Unsupported Auth Type: #{auth_type}"

end
plaintext = dcerpc_req.stub.to_binary_s
pad_length = get_auth_padding_length(plaintext.length)
dcerpc_req.auth_pad = "\x00" * pad_length
data_to_sign = plain_stub_with_padding = dcerpc_req.stub.to_binary_s + dcerpc_req.auth_pad.to_binary_s
dcerpc_req.sec_trailer.auth_pad_length = pad_length
if @ntlm_client.flags & NTLM::NEGOTIATE_FLAGS[:EXTENDED_SECURITY] != 0
data_to_sign = dcerpc_req.to_binary_s[0..-(dcerpc_req.pdu_header.auth_length + 1)]
end

if auth_level == RPC_C_AUTHN_LEVEL_PKT_PRIVACY
encrypted = @ntlm_client.session.seal_message(plain_stub_with_padding)
set_encrypted_packet(dcerpc_req, encrypted, pad_length)
end
signature = @ntlm_client.session.sign_message(data_to_sign)

set_signature_on_packet(dcerpc_req, signature)
end

# Get the response's full stub value (which will include the auth-pad)
def get_response_full_stub(dcerpc_response)
dcerpc_response.stub.to_binary_s + dcerpc_response.auth_pad.to_binary_s
end

# Decrypt the value in dcerpc_req.stub, and validate its signature.
# This function modifies the request object in-place, and returns whether the signature was valid.
# This function should be overriden for other providers (e.g. Kerberos, etc.)
def auth_provider_decrypt_and_verify(dcerpc_response)
auth_type = dcerpc_response.sec_trailer.auth_type
auth_level = dcerpc_response.sec_trailer.auth_level
unless [RPC_C_AUTHN_WINNT, RPC_C_AUTHN_DEFAULT].include?(auth_type)
raise ArgumentError, "Unsupported Auth Type: #{dcerpc_response.sec_trailer.auth_type}"
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
raise ArgumentError, "Unsupported Auth Type: #{dcerpc_response.sec_trailer.auth_type}"
raise ArgumentError, "Unsupported Auth Type: #{auth_type}"

end
encrypted_stub = ''
smcintyre-r7 marked this conversation as resolved.
Show resolved Hide resolved
if auth_level == RPC_C_AUTHN_LEVEL_PKT_PRIVACY
encrypted_stub = get_response_full_stub(dcerpc_response)
signature = dcerpc_response.auth_value
plaintext = @ntlm_client.session.unseal_message(encrypted_stub)
set_decrypted_packet(dcerpc_response, plaintext)
end
data_to_check = get_response_full_stub(dcerpc_response)
if @ntlm_client.flags & NTLM::NEGOTIATE_FLAGS[:EXTENDED_SECURITY] != 0
data_to_check = dcerpc_response.to_binary_s[0..-(dcerpc_response.pdu_header.auth_length + 1)]
end
valid = @ntlm_client.session.verify_signature(signature, data_to_check)
smashery marked this conversation as resolved.
Show resolved Hide resolved

return valid
smashery marked this conversation as resolved.
Show resolved Hide resolved
end

def process_ntlm_type2(type2_message)
ntlmssp_offset = type2_message.index('NTLMSSP')
type2_blob = type2_message.slice(ntlmssp_offset..-1)
type2_b64_message = [type2_blob].pack('m')
type3_message = @ntlm_client.init_context(type2_b64_message)
auth3 = type3_message.serialize

@session_key = @ntlm_client.session_key
auth3
end

# Send a rpc_auth3 PDU that ends the authentication handshake.
# This function should be overriden for other providers (e.g. Kerberos, etc.)
#
# @param response [BindAck] the BindAck response packet
def auth_provider_complete_handshake(response, options)
cdelafuente-r7 marked this conversation as resolved.
Show resolved Hide resolved
auth3 = process_ntlm_type2(response.auth_value)

rpc_auth3 = RpcAuth3.new
add_auth_verifier(rpc_auth3, auth3)
rpc_auth3.pdu_header.call_id = @call_id

# The server should not respond
send_packet(rpc_auth3)
@call_id += 1

nil
end

def force_set_auth_params(auth_type, auth_level)
@auth_type = auth_type
@auth_level = auth_level
end


# Bind to the remote server interface endpoint. It takes care of adding
# the necessary authentication verifier if `:auth_level` is set to
# anything different than RPC_C_AUTHN_LEVEL_NONE
Expand All @@ -74,23 +174,16 @@ def bind(options={})
@call_id ||= 1
bind_req = Bind.new(options)
bind_req.pdu_header.call_id = @call_id
auth_type = options.fetch(:auth_type) { RPC_C_AUTHN_WINNT }
auth_level = options.fetch(:auth_level) { RPC_C_AUTHN_LEVEL_NONE }

if options[:auth_level] && options[:auth_level] != RPC_C_AUTHN_LEVEL_NONE
case options[:auth_type]
when RPC_C_AUTHN_WINNT, RPC_C_AUTHN_DEFAULT
@ctx_id = 0
@auth_ctx_id_base = rand(0xFFFFFFFF)
raise ArgumentError, "NTLM Client not initialized. Username and password must be provided" unless @ntlm_client
type1_message = @ntlm_client.init_context
auth = type1_message.serialize
when RPC_C_AUTHN_GSS_KERBEROS, RPC_C_AUTHN_NETLOGON, RPC_C_AUTHN_GSS_NEGOTIATE
when RPC_C_AUTHN_GSS_KERBEROS, RPC_C_AUTHN_NETLOGON, RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_GSS_SCHANNEL
# TODO
raise NotImplementedError
else
raise ArgumentError, "Unsupported Auth Type: #{options[:auth_type]}"
end
add_auth_verifier(bind_req, auth, options[:auth_type], options[:auth_level])
force_set_auth_params(auth_type, auth_level)
cdelafuente-r7 marked this conversation as resolved.
Show resolved Hide resolved

if @auth_level != RPC_C_AUTHN_LEVEL_NONE
@ctx_id = 0
@auth_ctx_id_base = rand(0xFFFFFFFF)
auth = auth_provider_init
add_auth_verifier(bind_req, auth)
end

send_packet(bind_req)
Expand All @@ -110,17 +203,11 @@ def bind(options={})
self.max_buffer_size = dcerpc_response.max_xmit_frag
@call_id = dcerpc_response.pdu_header.call_id

if options[:auth_level] && options[:auth_level] != RPC_C_AUTHN_LEVEL_NONE
if auth_level != RPC_C_AUTHN_LEVEL_NONE
# The number of legs needed to build the security context is defined
# by the security provider
# (see [2.2.1.1.7 Security Providers](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rpce/d4097450-c62f-484b-872f-ddf59a7a0d36))
case options[:auth_type]
when RPC_C_AUTHN_WINNT
send_auth3(dcerpc_response, options[:auth_type], options[:auth_level])
when RPC_C_AUTHN_GSS_KERBEROS, RPC_C_AUTHN_NETLOGON, RPC_C_AUTHN_GSS_NEGOTIATE
# TODO
raise NotImplementedError
end
# (see [2.2.1.1.7 Security Providers](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rpce/d4097450-c62f-484b-872f-ddf59a7a0d36))
auth_provider_complete_handshake(dcerpc_response, options)
end

dcerpc_response
Expand Down Expand Up @@ -156,16 +243,24 @@ def send_packet(packet)
nil
end

def get_auth_padding_length(plaintext_len)
(16 - (plaintext_len % 16)) % 16
end

# Add the authentication verifier to a Request packet. This includes a
# sec trailer and the signature of the packet. This also encrypts the
# Request stub if privacy is required (`:auth_level` option is
# RPC_C_AUTHN_LEVEL_PKT_PRIVACY).
#
# @param dcerpc_req [Request] the Request packet to be updated
# @param opts [Hash] the authenticaiton options: `:auth_type` and `:auth_level`
# @param opts [Hash] the authentication options: `:auth_type` and `:auth_level`
# @raise [NotImplementedError] if `:auth_type` is not implemented (yet)
# @raise [ArgumentError] if `:auth_type` is unknown
def set_integrity_privacy(dcerpc_req, auth_level:, auth_type:)
unless [RPC_C_AUTHN_LEVEL_PKT_INTEGRITY, RPC_C_AUTHN_LEVEL_PKT_PRIVACY].include?(auth_level)
raise ArgumentError, "Unsupported Auth Type: #{auth_type}"
end

dcerpc_req.sec_trailer = {
auth_type: auth_type,
auth_level: auth_level,
Expand All @@ -174,35 +269,33 @@ def set_integrity_privacy(dcerpc_req, auth_level:, auth_type:)
dcerpc_req.auth_value = ' ' * 16
dcerpc_req.pdu_header.auth_length = 16

data_to_sign = plain_stub = dcerpc_req.stub.to_binary_s + dcerpc_req.auth_pad.to_binary_s
if @ntlm_client.flags & NTLM::NEGOTIATE_FLAGS[:EXTENDED_SECURITY] != 0
data_to_sign = dcerpc_req.to_binary_s[0..-(dcerpc_req.pdu_header.auth_length + 1)]
end

encrypted_stub = ''
smashery marked this conversation as resolved.
Show resolved Hide resolved
if auth_level == RPC_C_AUTHN_LEVEL_PKT_PRIVACY
case auth_type
when RPC_C_AUTHN_NONE
when RPC_C_AUTHN_WINNT, RPC_C_AUTHN_DEFAULT
encrypted_stub = @ntlm_client.session.seal_message(plain_stub)
when RPC_C_AUTHN_NETLOGON, RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_GSS_SCHANNEL, RPC_C_AUTHN_GSS_KERBEROS
# TODO
raise NotImplementedError
else
raise ArgumentError, "Unsupported Auth Type: #{auth_type}"
end
if [RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_AUTHN_LEVEL_PKT_INTEGRITY].include?(auth_level)
auth_provider_encrypt_and_sign(dcerpc_req)
Copy link
Contributor

Choose a reason for hiding this comment

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

I might be missing something here, but this has been checked earlier and should have raised an ArgumentError if auth_level is not one of these two values. I believe this can be removed.

Suggested change
if [RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_AUTHN_LEVEL_PKT_INTEGRITY].include?(auth_level)
auth_provider_encrypt_and_sign(dcerpc_req)
auth_provider_encrypt_and_sign(dcerpc_req)

end

signature = @ntlm_client.session.sign_message(data_to_sign)
end

def set_signature_on_packet(dcerpc_req, signature)
dcerpc_req.auth_value = signature
dcerpc_req.pdu_header.auth_length = signature.size
end

def set_encrypted_packet(dcerpc_req, encrypted_stub, pad_length)
unless encrypted_stub.empty?
pad_length = dcerpc_req.sec_trailer.auth_pad_length.to_i
dcerpc_req.enable_encrypted_stub
dcerpc_req.stub = encrypted_stub[0..-(pad_length + 1)]
dcerpc_req.auth_pad = encrypted_stub[-(pad_length)..-1]
dcerpc_req.stub = encrypted_stub[0..-(pad_length+1)]
if pad_length != 0
dcerpc_req.auth_pad = encrypted_stub[-(pad_length)..-1]
end
end
end

def set_decrypted_packet(dcerpc_response, decrypted_stub)
unless decrypted_stub.empty?
pad_length = dcerpc_response.sec_trailer.auth_pad_length.to_i
dcerpc_response.stub = decrypted_stub[0..-(pad_length + 1)]
end
dcerpc_req.auth_value = signature
dcerpc_req.pdu_header.auth_length = signature.size
end

# Process the security context received in a response. It decrypts the
Expand All @@ -214,39 +307,19 @@ def set_integrity_privacy(dcerpc_req, auth_level:, auth_type:)
#
# @param dcerpc_response [Response] the Response packet
# containing the security context to process
# @param opts [Hash] the authenticaiton options: `:auth_type` and
# @param opts [Hash] the authentication options: `:auth_type` and
# `:auth_level`. To enable errors when signature check fails, set the
# `:raise_signature_error` option to true
# @raise [NotImplementedError] if `:auth_type` is not implemented (yet)
# @raise [Error::CommunicationError] if socket-related error occurs
def handle_integrity_privacy(dcerpc_response, auth_level:, auth_type:, raise_signature_error: false)
decrypted_stub = ''
if auth_level == RPC_C_AUTHN_LEVEL_PKT_PRIVACY
encrypted_stub = dcerpc_response.stub.to_binary_s + dcerpc_response.auth_pad.to_binary_s
case auth_type
when RPC_C_AUTHN_NONE
when RPC_C_AUTHN_WINNT, RPC_C_AUTHN_DEFAULT
decrypted_stub = @ntlm_client.session.unseal_message(encrypted_stub)
when RPC_C_AUTHN_NETLOGON, RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_GSS_SCHANNEL, RPC_C_AUTHN_GSS_KERBEROS
# TODO
raise NotImplementedError
else
raise ArgumentError, "Unsupported Auth Type: #{auth_type}"
end
end

unless decrypted_stub.empty?
pad_length = dcerpc_response.sec_trailer.auth_pad_length.to_i
dcerpc_response.stub = decrypted_stub[0..-(pad_length + 1)]
dcerpc_response.auth_pad = decrypted_stub[-(pad_length)..-1]
unless [RPC_C_AUTHN_LEVEL_PKT_INTEGRITY, RPC_C_AUTHN_LEVEL_PKT_PRIVACY].include?(auth_level)
raise ArgumentError, "Unsupported Auth Type: #{auth_type}"
end
decrypted_stub = ''
smcintyre-r7 marked this conversation as resolved.
Show resolved Hide resolved
signature_valid = auth_provider_decrypt_and_verify(dcerpc_response)

signature = dcerpc_response.auth_value
data_to_check = dcerpc_response.stub.to_binary_s
if @ntlm_client.flags & NTLM::NEGOTIATE_FLAGS[:EXTENDED_SECURITY] != 0
data_to_check = dcerpc_response.to_binary_s[0..-(dcerpc_response.pdu_header.auth_length + 1)]
end
unless @ntlm_client.session.verify_signature(signature, data_to_check)
unless signature_valid
smashery marked this conversation as resolved.
Show resolved Hide resolved
if raise_signature_error
raise Error::InvalidPacket.new(
"Wrong packet signature received (set `raise_signature_error` to false to ignore)"
Expand All @@ -264,59 +337,16 @@ def handle_integrity_privacy(dcerpc_response, auth_level:, auth_type:, raise_sig
#
# @param req [BinData::Record] the request to be updated
# @param auth [String] the authentication data
# @param auth_type [Integer] the authentication type
# @param auth_level [Integer] the authentication level
def add_auth_verifier(req, auth, auth_type, auth_level)
def add_auth_verifier(req, auth)
req.sec_trailer = {
auth_type: auth_type,
auth_level: auth_level,
auth_type: @auth_type,
auth_level: @auth_level,
auth_context_id: @ctx_id + @auth_ctx_id_base
}
req.auth_value = auth
req.pdu_header.auth_length = auth.length

nil
end

def process_ntlm_type2(type2_message)
ntlmssp_offset = type2_message.index('NTLMSSP')
type2_blob = type2_message.slice(ntlmssp_offset..-1)
type2_b64_message = [type2_blob].pack('m')
type3_message = @ntlm_client.init_context(type2_b64_message)
auth3 = type3_message.serialize

@session_key = @ntlm_client.session_key
auth3
end

# Send a rpc_auth3 PDU that ends the authentication handshake.
#
# @param response [BindAck] the BindAck response packet
# @param auth_type [Integer] the authentication type
# @param auth_level [Integer] the authentication level
# @raise [ArgumentError] if `:auth_type` is unknown
# @raise [NotImplementedError] if `:auth_type` is not implemented (yet)
def send_auth3(response, auth_type, auth_level)
case auth_type
when RPC_C_AUTHN_NONE
when RPC_C_AUTHN_WINNT, RPC_C_AUTHN_DEFAULT
auth3 = process_ntlm_type2(response.auth_value)
when RPC_C_AUTHN_NETLOGON, RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_GSS_SCHANNEL, RPC_C_AUTHN_GSS_KERBEROS
# TODO
raise NotImplementedError
else
raise ArgumentError, "Unsupported Auth Type: #{auth_type}"
end

rpc_auth3 = RpcAuth3.new
add_auth_verifier(rpc_auth3, auth3, auth_type, auth_level)
rpc_auth3.pdu_header.call_id = @call_id

# The server should not respond
send_packet(rpc_auth3)
@call_id += 1

nil
end
end
end
37 changes: 36 additions & 1 deletion lib/ruby_smb/dcerpc/bind_ack.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,44 @@ class PortAnyT < Ndr::NdrStruct
endian :little

ndr_uint16 :str_length, label: 'Length', initial_value: -> { port_spec.to_binary_s.size }
stringz :port_spec, label: 'Port string spec', byte_align: 2
stringz :port_spec, label: 'Port string spec', byte_align: 2, onlyif: -> { str_length > 0 }
end

class AlterContextResp < BinData::Record
smashery marked this conversation as resolved.
Show resolved Hide resolved
PTYPE = PTypes::ALTER_CONTEXT_RESP

# Presentation context negotiation results
ACCEPTANCE = 0
USER_REJECTION = 1
PROVIDER_REJECTION = 2

# Reasons for rejection of a context element
REASON_NOT_SPECIFIED = 0
ABSTRACT_SYNTAX_NOT_SUPPORTED = 1
PROPOSED_TRANSFER_SYNTAXES_NOT_SUPPORTED = 2
LOCAL_LIMIT_EXCEEDED = 3

endian :little

# PDU Header
pdu_header :pdu_header, label: 'PDU header'
ndr_uint16 :max_xmit_frag, label: 'Max transmit frag size', initial_value: RubySMB::Dcerpc::MAX_XMIT_FRAG
ndr_uint16 :max_recv_frag, label: 'Max receive frag size', initial_value: RubySMB::Dcerpc::MAX_RECV_FRAG
ndr_uint32 :assoc_group_id, label: 'Association group ID'
port_any_t :sec_addr, label: 'Secondary address'
p_result_list_t :p_result_list, label: 'Presentation context result list'

# Auth Verifier
sec_trailer :sec_trailer, onlyif: -> { pdu_header.auth_length > 0 }
string :auth_value,
onlyif: -> { pdu_header.auth_length > 0 },
read_length: -> { pdu_header.auth_length }

def initialize_instance
super
pdu_header.ptype = PTYPE
end
end
class BindAck < BinData::Record
PTYPE = PTypes::BIND_ACK

Expand Down
Loading
Loading