lib/ruby_smb/server/server_client/negotiation.rb
require 'securerandom'
module RubySMB
class Server
class ServerClient
module Negotiation
#
# Handle an SMB negotiation request. Once negotiation is complete, the state will be updated to :session_setup.
# At this point the @dialect will have been set along with other dialect-specific values.
#
# @param [String] raw_request the negotiation request to process
def handle_negotiate(raw_request)
response = nil
case raw_request[0...4].unpack1('L>')
when RubySMB::SMB1::SMB_PROTOCOL_ID
request = SMB1::Packet::NegotiateRequest.read(raw_request)
response = do_negotiate_smb1(request) if request.is_a?(SMB1::Packet::NegotiateRequest)
when RubySMB::SMB2::SMB2_PROTOCOL_ID
request = SMB2::Packet::NegotiateRequest.read(raw_request)
response = do_negotiate_smb2(request) if request.is_a?(SMB2::Packet::NegotiateRequest)
end
if response.nil?
disconnect!
else
send_packet(response)
end
nil
end
def do_negotiate_smb1(request)
client_dialects = request.dialects.map(&:dialect_string).map(&:value)
if client_dialects.include?(Client::SMB1_DIALECT_SMB2_WILDCARD) && \
@server.dialects.any? { |dialect| Dialect[dialect].order == Dialect::ORDER_SMB2 }
response = SMB2::Packet::NegotiateResponse.new
response.smb2_header.credits = 1
response.security_mode.signing_enabled = 1
response.dialect_revision = SMB2::SMB2_WILDCARD_REVISION
response.server_guid = @server.guid
response.max_transact_size = 0x800000
response.max_read_size = 0x800000
response.max_write_size = 0x800000
response.system_time.set(Time.now)
response.security_buffer_offset = response.security_buffer.abs_offset
response.security_buffer = process_gss.buffer
return response
end
server_dialects = @server.dialects.select { |dialect| Dialect[dialect].order == Dialect::ORDER_SMB1 }
dialect = (server_dialects & client_dialects).first
if dialect.nil?
# 'NT LM 0.12' is currently the only supported dialect
# see: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-cifs/80850595-e301-4464-9745-58e4945eb99b
response = SMB1::Packet::NegotiateResponse.new
response.parameter_block.word_count = 1
response.parameter_block.dialect_index = 0xffff
response.data_block.byte_count = 0
return response
end
response = SMB1::Packet::NegotiateResponseExtended.new
response.parameter_block.dialect_index = client_dialects.index(dialect)
response.parameter_block.max_mpx_count = 50
response.parameter_block.max_number_vcs = 1
response.parameter_block.max_buffer_size = 16644
response.parameter_block.max_raw_size = 65536
server_time = Time.now
response.parameter_block.system_time.set(server_time)
response.parameter_block.server_time_zone = server_time.utc_offset
response.data_block.server_guid = @server.guid
response.data_block.security_blob = process_gss.buffer
@dialect = dialect
response
end
def do_negotiate_smb2(request)
client_dialects = request.dialects.map { |d| "0x%04x" % d }
server_dialects = @server.dialects.select { |dialect| Dialect[dialect].order == Dialect::ORDER_SMB2 }
dialect = (server_dialects & client_dialects).first
response = SMB2::Packet::NegotiateResponse.new
response.smb2_header.credits = 1
response.smb2_header.message_id = request.smb2_header.message_id
response.security_mode.signing_enabled = 1
response.server_guid = @server.guid
response.max_transact_size = 0x800000
response.max_read_size = 0x800000
response.max_write_size = 0x800000
response.system_time.set(Time.now)
if dialect.nil?
# see: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/b39f253e-4963-40df-8dff-2f9040ebbeb1
# > If a common dialect is not found, the server MUST fail the request with STATUS_NOT_SUPPORTED.
response.smb2_header.nt_status = WindowsError::NTStatus::STATUS_NOT_SUPPORTED.value
return response
end
contexts = []
hash_algorithm = hash_value = nil
if dialect == '0x0311'
# see: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/b39f253e-4963-40df-8dff-2f9040ebbeb1
nc = request.find_negotiate_context(SMB2::NegotiateContext::SMB2_PREAUTH_INTEGRITY_CAPABILITIES)
hash_algorithm = SMB2::PreauthIntegrityCapabilities::HASH_ALGORITM_MAP[nc&.data&.hash_algorithms&.first]
hash_value = "\x00" * 64
unless hash_algorithm
response.smb2_header.nt_status = WindowsError::NTStatus::STATUS_INVALID_PARAMETER.value
return response
end
contexts << SMB2::NegotiateContext.new(
context_type: SMB2::NegotiateContext::SMB2_PREAUTH_INTEGRITY_CAPABILITIES,
data: {
hash_algorithms: [ SMB2::PreauthIntegrityCapabilities::SHA_512 ],
salt: SecureRandom.random_bytes(32)
}
)
nc = request.find_negotiate_context(SMB2::NegotiateContext::SMB2_ENCRYPTION_CAPABILITIES)
ciphers = nc&.data&.ciphers
if ciphers
cipher = ciphers.find { |cipher| SMB2::EncryptionCapabilities::ENCRYPTION_ALGORITHM_MAP.include?(cipher) }
@cipher_id = cipher unless cipher.nil?
end
contexts << SMB2::NegotiateContext.new(
context_type: SMB2::NegotiateContext::SMB2_ENCRYPTION_CAPABILITIES,
data: {
ciphers: [ @cipher_id ]
}
)
elsif dialect == '0x0300' || dialect == '0x0302'
if request.capabilities.encryption == 1
response.capabilities.encryption = 1
@cipher_id = SMB2::EncryptionCapabilities::AES_128_CCM
else
response.capabilities = 0
end
end
# the order in which the response is built is important to ensure it is valid
response.dialect_revision = dialect.to_i(16)
response.security_buffer_offset = response.security_buffer.abs_offset
response.security_buffer = process_gss.buffer
if dialect == '0x0311'
response.negotiate_context_offset = response.negotiate_context_list.abs_offset
contexts.each { |nc| response.add_negotiate_context(nc) }
end
@preauth_integrity_hash_algorithm = hash_algorithm
@preauth_integrity_hash_value = hash_value
if dialect == '0x0311'
update_preauth_hash(request)
update_preauth_hash(response)
end
@dialect = dialect
response
end
end
end
end
end