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

Add attributes to client #117

Merged
5 changes: 5 additions & 0 deletions examples/anonymous_auth.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ def run_authentication(address, smb1, smb2, username, password)
protocol = client.negotiate
status = client.authenticate
puts "#{protocol} : #{status}"
if status.name == 'STATUS_SUCCESS'
puts "Native OS: #{client.peer_native_os}"
puts "Native LAN Manager: #{client.peer_native_lm}"
puts "Domain/Workgroup: #{client.primary_domain}"
end
end

address = ARGV[0]
Expand Down
18 changes: 12 additions & 6 deletions examples/authenticate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,18 @@ def run_authentication(address, smb1, smb2, username, password)
client = RubySMB::Client.new(dispatcher, smb1: smb1, smb2: smb2, username: username, password: password)
protocol = client.negotiate
status = client.authenticate
native_os = if client.peer_native_os
"(#{client.peer_native_os})"
else
''
end
puts "#{protocol} : #{status} #{native_os}"
puts "#{protocol} : #{status}"
if protocol == 'SMB1'
puts "Native OS: #{client.peer_native_os}"
puts "Native LAN Manager: #{client.peer_native_lm}"
end
puts "Netbios Name: #{client.default_name}"
puts "Netbios Domain: #{client.default_domain}"
puts "FQDN of the computer: #{client.dns_host_name}"
puts "FQDN of the domain: #{client.dns_domain_name}"
puts "FQDN of the forest: #{client.dns_tree_name}"
puts "Dialect: #{client.dialect}"
puts "OS Version: #{client.os_version}"
end

address = ARGV[0]
Expand Down
56 changes: 55 additions & 1 deletion lib/ruby_smb/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,54 @@ class Client
# @return [String]
attr_accessor :peer_native_os

# The Native LAN Manager of the Peer/Server.
# Currently only available with SMB1.
# @!attribute [rw] peer_native_lm
# @return [String]
attr_accessor :peer_native_lm

# The Primary Domain of the Peer/Server.
# Currently only available with SMB1 and only when authentiation
# without NTLMSSP is used.
# @!attribute [rw] primary_domain
# @return [String]
attr_accessor :primary_domain

# The Netbios Name of the Peer/Server.
# @!attribute [rw] default_name
# @return [String]
attr_accessor :default_name

# The Netbios Domain of the Peer/Server.
# @!attribute [rw] default_domain
# @return [String]
attr_accessor :default_domain

# The Fully Qualified Domain Name (FQDN) of the computer.
# @!attribute [rw] dns_host_name
# @return [String]
attr_accessor :dns_host_name

# The Fully Qualified Domain Name (FQDN) of the domain.
# @!attribute [rw] dns_domain_name
# @return [String]
attr_accessor :dns_domain_name

# The Fully Qualified Domain Name (FQDN) of the forest.
# @!attribute [rw] dns_tree_name
# @return [String]
attr_accessor :dns_tree_name

# The OS version number (<major>.<minor>.<build>) of the Peer/Server.
# @!attribute [rw] os_version
# @return [String]
attr_accessor :os_version

# The negotiated dialect.
# @!attribute [rw] dialect
# @return [Integer]
attr_accessor :dialect

# The Sequence Counter used for SMB1 Signing.
# It tracks the number of packets both sent and received
# since the NTLM session was initialized with the Challenge.
Expand Down Expand Up @@ -123,11 +171,17 @@ def initialize(dispatcher, smb1: true, smb2: true, username:, password:, domain:
@username = username.encode('utf-8') || ''.encode('utf-8')
@max_buffer_size = MAX_BUFFER_SIZE

negotiate_version_flag = 0x02000000
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The NEGOTIATE_VERSION flag is missing in rubyntlm library. I had to manually add it. I also submitted a PR to add it: WinRb/rubyntlm#38

flags = Net::NTLM::Client::DEFAULT_FLAGS |
Net::NTLM::FLAGS[:TARGET_INFO] |
negotiate_version_flag

@ntlm_client = Net::NTLM::Client.new(
@username,
@password,
workstation: @local_workstation,
domain: @domain
domain: @domain,
flags: flags
)

@smb2_message_id = 0
Expand Down
112 changes: 78 additions & 34 deletions lib/ruby_smb/client/authentication.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@ def smb1_anonymous_auth
response = smb1_anonymous_auth_response(raw_response)
response_code = response.status_code

if response_code.name == 'STATUS_SUCCESS'
if response_code == WindowsError::NTStatus::STATUS_SUCCESS
self.user_id = response.smb_header.uid
self.peer_native_os = response.data_block.native_os
self.peer_native_os = response.data_block.native_os.to_s
self.peer_native_lm = response.data_block.native_lan_man.to_s
self.primary_domain = response.data_block.primary_domain.to_s
end

response_code
Expand Down Expand Up @@ -64,20 +66,30 @@ def smb1_anonymous_auth_response(raw_response)
packet
end

# Handles the SMB1 NTLMSSP 4-way handshake for Authentication
# Handles the SMB1 NTLMSSP 4-way handshake for Authentication and store
# information about the peer/server.
def smb1_authenticate
response = smb1_ntlmssp_negotiate
challenge_packet = smb1_ntlmssp_challenge_packet(response)

# Store the available OS information before going forward.
@peer_native_os = challenge_packet.data_block.native_os.to_s
@peer_native_lm = challenge_packet.data_block.native_lan_man.to_s

user_id = challenge_packet.smb_header.uid
challenge_message = smb1_type2_message(challenge_packet)
raw = smb1_ntlmssp_authenticate(challenge_message, user_id)
type2_b64_message = smb1_type2_message(challenge_packet)
type3_message = @ntlm_client.init_context(type2_b64_message)

@session_key = @ntlm_client.session_key
challenge_message = @ntlm_client.session.challenge_message
store_target_info(challenge_message.target_info) if challenge_message.has_flag?(:TARGET_INFO)
@os_version = extract_os_version(challenge_message.os_version.to_s)

raw = smb1_ntlmssp_authenticate(type3_message, user_id)
response = smb1_ntlmssp_final_packet(raw)
response_code = response.status_code

if response_code.name == 'STATUS_SUCCESS'
self.user_id = user_id
self.peer_native_os = response.data_block.native_os
end
@user_id = user_id if response_code == WindowsError::NTStatus::STATUS_SUCCESS

response_code
end
Expand All @@ -91,27 +103,24 @@ def smb1_ntlmssp_negotiate
send_recv(packet)
end

# Takes the Base64 encoded NTLM Type 2 (Challenge) message
# and calls the routines to build the Auth packet, sends the packet
# and receives the raw response
# Takes the NTLM Type 3 (authenticate) message and calls the routines to
# build the Auth packet, sends the packet and receives the raw response.
#
# @param type2_string [String] the Base64 Encoded NTLM Type 2 message
# @param type3_message [String] the NTLM Type 3 message
# @param user_id [Integer] the temporary user ID from the Type 2 response
# @return [String] the raw binary response from the server
def smb1_ntlmssp_authenticate(type2_string, user_id)
packet = smb1_ntlmssp_auth_packet(type2_string, user_id)
def smb1_ntlmssp_authenticate(type3_message, user_id)
packet = smb1_ntlmssp_auth_packet(type3_message, user_id)
send_recv(packet)
end

# Generates the {RubySMB::SMB1::Packet::SessionSetupRequest} packet
# with the NTLM Type 3 (Auth) message in the security_blob field.
#
# @param type2_string [String] the Base64 encoded Type2 challenge to respond to
# @param type3_message [String] the NTLM Type 3 message
# @param user_id [Integer] the temporary user ID from the Type 2 response
# @return [RubySMB::SMB1::Packet::SessionSetupRequest] the second authentication packet to send
def smb1_ntlmssp_auth_packet(type2_string, user_id)
type3_message = ntlm_client.init_context(type2_string)
self.session_key = ntlm_client.session_key
def smb1_ntlmssp_auth_packet(type3_message, user_id)
packet = RubySMB::SMB1::Packet::SessionSetupRequest.new
packet.smb_header.uid = user_id
packet.set_type3_blob(type3_message.serialize)
Expand Down Expand Up @@ -180,15 +189,23 @@ def smb1_type2_message(response_packet)
# SMB 2 Methods
#

# Handles the SMB1 NTLMSSP 4-way handshake for Authentication
# Handles the SMB2 NTLMSSP 4-way handshake for Authentication and store
# information about the peer/server.
def smb2_authenticate
response = smb2_ntlmssp_negotiate
challenge_packet = smb2_ntlmssp_challenge_packet(response)
session_id = challenge_packet.smb2_header.session_id
self.session_id = session_id
challenge_message = smb2_type2_message(challenge_packet)
raw = smb2_ntlmssp_authenticate(challenge_message, session_id)
@session_id = challenge_packet.smb2_header.session_id
type2_b64_message = smb2_type2_message(challenge_packet)
type3_message = @ntlm_client.init_context(type2_b64_message)

@session_key = @ntlm_client.session_key
challenge_message = ntlm_client.session.challenge_message
store_target_info(challenge_message.target_info) if challenge_message.has_flag?(:TARGET_INFO)
@os_version = extract_os_version(challenge_message.os_version.to_s)

raw = smb2_ntlmssp_authenticate(type3_message, @session_id)
response = smb2_ntlmssp_final_packet(raw)

response.status_code
end

Expand Down Expand Up @@ -251,32 +268,59 @@ def smb2_type2_message(response_packet)
[type2_blob].pack('m')
end

# Takes the Base64 encoded NTLM Type 2 (Challenge) message
# and calls the routines to build the Auth packet, sends the packet
# and receives the raw response
# Takes the NTLM Type 3 (authenticate) message and calls the routines to
# build the Auth packet, sends the packet and receives the raw response.
#
# @param type2_string [String] the Base64 Encoded NTLM Type 2 message
# @param type3_message [String] the NTLM Type 3 message
# @param user_id [Integer] the temporary user ID from the Type 2 response
# @return [String] the raw binary response from the server
def smb2_ntlmssp_authenticate(type2_string, user_id)
packet = smb2_ntlmssp_auth_packet(type2_string, user_id)
def smb2_ntlmssp_authenticate(type3_message, user_id)
packet = smb2_ntlmssp_auth_packet(type3_message, user_id)
send_recv(packet)
end

# Generates the {RubySMB::SMB2::Packet::SessionSetupRequest} packet
# with the NTLM Type 3 (Auth) message in the security_blob field.
#
# @param type2_string [String] the Base64 encoded Type2 challenge to respond to
# @param type3_message [String] the NTLM Type 3 message
# @param session_id [Integer] the temporary session id from the Type 2 response
# @return [RubySMB::SMB2::Packet::SessionSetupRequest] the second authentication packet to send
def smb2_ntlmssp_auth_packet(type2_string, session_id)
type3_message = ntlm_client.init_context(type2_string)
self.session_key = ntlm_client.session_key
def smb2_ntlmssp_auth_packet(type3_message, session_id)
packet = RubySMB::SMB2::Packet::SessionSetupRequest.new
packet.smb2_header.session_id = session_id
packet.set_type3_blob(type3_message.serialize)
packet
end

# Extract and store useful information about the peer/server from the
# NTLM Type 2 (challenge) TargetInfo fields.
#
# @param target_info_str [String] the Target Info string
def store_target_info(target_info_str)
target_info = Net::NTLM::TargetInfo.new(target_info_str)
{
Net::NTLM::TargetInfo::MSV_AV_NB_COMPUTER_NAME => :@default_name,
Net::NTLM::TargetInfo::MSV_AV_NB_DOMAIN_NAME => :@default_domain,
Net::NTLM::TargetInfo::MSV_AV_DNS_COMPUTER_NAME => :@dns_host_name,
Net::NTLM::TargetInfo::MSV_AV_DNS_DOMAIN_NAME => :@dns_domain_name,
Net::NTLM::TargetInfo::MSV_AV_DNS_TREE_NAME => :@dns_tree_name
}.each do |constant, attribute|
if target_info.av_pairs[constant]
value = target_info.av_pairs[constant].dup
value.force_encoding('UTF-16LE')
instance_variable_set(attribute, value.encode('UTF-8'))
end
end
end

# Extract the peer/server version number from the NTLM Type 2 (challenge)
# Version field.
#
# @param version [String] the version number as a binary string
# @return [String] the formated version number (<major>.<minor>.<build>)
def extract_os_version(version)
version.unpack('CCS').join('.')
end
end
end
end
25 changes: 17 additions & 8 deletions lib/ruby_smb/client/negotiation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,35 @@ module Negotiation
# Handles the entire SMB Multi-Protocol Negotiation from the
# Client to the Server. It sets state on the client appropriate
# to the protocol and capabilites negotiated during the exchange.
# It also keeps track of the negotiated dialect.
#
# @return [void]
def negotiate
raw_response = negotiate_request
request_packet = negotiate_request
raw_response = send_recv(request_packet)
response_packet = negotiate_response(raw_response)
# The list of dialect identifiers sent to the server is stored
# internally to be able to retrieve the negotiated dialect later on.
# This is only valid for SMB1.
response_packet.dialects = request_packet.dialects if response_packet.respond_to? :dialects=
parse_negotiate_response(response_packet)
rescue RubySMB::Error::InvalidPacket, Errno::ECONNRESET
error = 'Unable to Negotiate with remote host'
error << ', SMB1 may be disabled' if smb1 && !smb2
raise RubySMB::Error::NegotiationFailure, error
end

# Creates and dispatches the first Negotiate Request Packet and
# returns the raw response data.
# Creates the first Negotiate Request Packet according to the SMB version
# used.
#
# @return [String] the raw binary string containing the response from the server
# @return [RubySMB::SMB1::Packet::NegotiateRequest] a SMB1 Negotiate Request packet if SMB1 is used
# @return [RubySMB::SMB1::Packet::NegotiateRequest] a SMB2 Negotiate Request packet if SMB2 is used
def negotiate_request
if smb1
request = smb1_negotiate_request
smb1_negotiate_request
elsif smb2
request = smb2_negotiate_request
smb2_negotiate_request
end
send_recv(request)
end

# Takes the raw response data from the server and tries
Expand Down Expand Up @@ -64,10 +70,11 @@ def negotiate_response(raw_data)

# Sets the supported SMB Protocol and whether or not
# Signing is enabled based on the Negotiate Response Packet.
# It also stores the negotiated dialect.
#
# @param packet [RubySMB::SMB1::Packet::NegotiateResponseExtended] if SMB1 was negotiated
# @param packet [RubySMB::SMB2::Packet::NegotiateResponse] if SMB2 was negotiated
# @return [void] This method sets state and does not return a meaningful value
# @return [String] The SMB version as a string ('SMB1', 'SMB2')
def parse_negotiate_response(packet)
case packet
when RubySMB::SMB1::Packet::NegotiateResponseExtended
Expand All @@ -78,6 +85,7 @@ def parse_negotiate_response(packet)
else
self.signing_required = false
end
self.dialect = packet.negotiated_dialect.to_s
'SMB1'
when RubySMB::SMB2::Packet::NegotiateResponse
self.smb1 = false
Expand All @@ -87,6 +95,7 @@ def parse_negotiate_response(packet)
else
false
end
self.dialect = "0x%04x" % packet.dialect_revision
'SMB2'
end
end
Expand Down
22 changes: 22 additions & 0 deletions lib/ruby_smb/smb1/packet/negotiate_response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,28 @@ def initialize_instance
def valid?
smb_header.command == RubySMB::SMB1::Commands::SMB_COM_NEGOTIATE
end

# Stores the list of {RubySMB::SMB1::Dialect} that were sent to the
# peer/server in the related Negotiate Request. This will be used by
# the {#negotiated_dialect} method.
#
# @param dialects [Array] array of {RubySMB::SMB1::Dialect}
# @return dialects [Array] array of {RubySMB::SMB1::Dialect}
# @raise [ArgumentError] if dialects is not an array of {RubySMB::SMB1::Dialect}
def dialects=(dialects)
unless dialects.all? { |dialect| dialect.is_a? Dialect }
raise ArgumentError, 'Dialects must be an array of Dialect objects'
end
@dialects = dialects
end

# Returns the negotiated dialect identifier
#
# @return [String] the negotiated dialect identifier or an empty string if the list of {RubySMB::SMB1::Dialect} was not provided.
def negotiated_dialect
return '' if @dialects.nil? || @dialects.empty?
@dialects[parameter_block.dialect_index].dialect_string
end
end
end
end
Expand Down
Loading