diff --git a/lib/ruby_smb/gss/provider/ntlm.rb b/lib/ruby_smb/gss/provider/ntlm.rb index 5f774f253..b032cd436 100644 --- a/lib/ruby_smb/gss/provider/ntlm.rb +++ b/lib/ruby_smb/gss/provider/ntlm.rb @@ -85,7 +85,7 @@ def process_ntlm_type1(type1_msg) @server_challenge = @provider.generate_server_challenge msg.challenge = @server_challenge.unpack1('Q<') # 64-bit unsigned, little endian (uint64_t) - target_info = Net::NTLM::TargetInfo.new('') + target_info = Net::NTLM::TargetInfo.new(nil) target_info.av_pairs.merge!({ Net::NTLM::TargetInfo::MSV_AV_NB_DOMAIN_NAME => @provider.netbios_domain.encode('UTF-16LE').b, Net::NTLM::TargetInfo::MSV_AV_NB_COMPUTER_NAME => @provider.netbios_hostname.encode('UTF-16LE').b, diff --git a/lib/ruby_smb/peer_info.rb b/lib/ruby_smb/peer_info.rb index 2736b941f..05b42044c 100644 --- a/lib/ruby_smb/peer_info.rb +++ b/lib/ruby_smb/peer_info.rb @@ -3,8 +3,10 @@ module PeerInfo # 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 + # @param target_info_str [String, nil] the Target Info string def store_target_info(target_info_str) + return if target_info_str.nil? || target_info_str.empty? + target_info = Net::NTLM::TargetInfo.new(target_info_str) { Net::NTLM::TargetInfo::MSV_AV_NB_COMPUTER_NAME => :@default_name, diff --git a/spec/lib/ruby_smb/client_spec.rb b/spec/lib/ruby_smb/client_spec.rb index 999cb1800..77351db4c 100644 --- a/spec/lib/ruby_smb/client_spec.rb +++ b/spec/lib/ruby_smb/client_spec.rb @@ -1614,6 +1614,15 @@ expect(smb1_client.session_key).to eq ntlm_client.session_key end + it 'continues authentication when the server advertises empty target info' do + challenge = Net::NTLM::Message.decode64(type2_string) + challenge.set_flag(:TARGET_INFO) + challenge.target_info = ''.b + allow(smb1_client).to receive(:smb1_type2_message).and_return(challenge.encode64) + + expect(smb1_client.smb1_authenticate).to eq WindowsError::NTStatus::STATUS_SUCCESS + end + it 'stores the OS version number from the challenge message' do smb1_client.smb1_authenticate expect(smb1_client.os_version).to eq '6.1.7601' @@ -1860,6 +1869,15 @@ expect(smb2_client.session_key).to eq ntlm_client.session_key end + it 'continues authentication when the server advertises empty target info' do + challenge = Net::NTLM::Message.decode64(type2_string) + challenge.set_flag(:TARGET_INFO) + challenge.target_info = ''.b + allow(smb2_client).to receive(:smb2_type2_message).and_return(challenge.encode64) + + expect(smb2_client.smb2_authenticate).to eq WindowsError::NTStatus::STATUS_SUCCESS + end + it 'stores the OS version number from the challenge message' do smb2_client.smb2_authenticate expect(smb2_client.os_version).to eq '6.1.7601' @@ -2068,6 +2086,25 @@ client.store_target_info(target_info_str) end + it 'preserves existing peer information when given empty target info' do + client.store_target_info(target_info_str) + expect { client.store_target_info(''.b) }.to_not change { + [client.default_name, client.default_domain, client.dns_host_name, client.dns_domain_name, client.dns_tree_name] + } + end + + it 'accepts nil target info' do + expect { client.store_target_info(nil) }.to_not raise_error + end + + it 'accepts empty target info encoded with an end-of-list marker' do + expect { client.store_target_info("\x00\x00\x00\x00".b) }.to_not raise_error + end + + it 'rejects nonempty invalid target info' do + expect { client.store_target_info("\xff\xff\x00\x00".b) }.to raise_error(Net::NTLM::InvalidTargetDataError) + end + it 'sets the expected Client\'s attribute' do client.store_target_info(target_info_str) expect(client.default_name).to eq 'TESTNAME' diff --git a/spec/lib/ruby_smb/dcerpc/client_spec.rb b/spec/lib/ruby_smb/dcerpc/client_spec.rb index dcea6b16c..5f3e44f31 100644 --- a/spec/lib/ruby_smb/dcerpc/client_spec.rb +++ b/spec/lib/ruby_smb/dcerpc/client_spec.rb @@ -173,6 +173,14 @@ expect(auth_client.process_ntlm_type2(type2_message)).to start_with("NTLMSSP\x00\x03\x00\x00\x00") end + it 'returns a type3 message when the server advertises empty target info' do + challenge = Net::NTLM::Message.parse(type2_message) + challenge.set_flag(:TARGET_INFO) + challenge.target_info = ''.b + + expect(auth_client.process_ntlm_type2(challenge.serialize)).to start_with("NTLMSSP\x00\x03\x00\x00\x00") + end + it 'stores the session key' do auth_client.process_ntlm_type2(type2_message) expect(auth_client.instance_variable_get(:@session_key).size).to eq(16) diff --git a/spec/lib/ruby_smb/gss/provider/ntlm/authenticator_spec.rb b/spec/lib/ruby_smb/gss/provider/ntlm/authenticator_spec.rb index 8fafc3339..b8c539bdb 100644 --- a/spec/lib/ruby_smb/gss/provider/ntlm/authenticator_spec.rb +++ b/spec/lib/ruby_smb/gss/provider/ntlm/authenticator_spec.rb @@ -65,6 +65,19 @@ it 'should process a NTLM type 1 message and return a type2 message' do expect(authenticator.process_ntlm_type1(type1_msg)).to be_a Net::NTLM::Message::Type2 end + + it 'includes the server names in the serialized target info' do + type2_msg = Net::NTLM::Message.parse(authenticator.process_ntlm_type1(type1_msg).serialize) + target_info = Net::NTLM::TargetInfo.new(type2_msg.target_info) + + expect(target_info.av_pairs).to include( + Net::NTLM::TargetInfo::MSV_AV_NB_DOMAIN_NAME => provider.netbios_domain.encode('UTF-16LE').b, + Net::NTLM::TargetInfo::MSV_AV_NB_COMPUTER_NAME => provider.netbios_hostname.encode('UTF-16LE').b, + Net::NTLM::TargetInfo::MSV_AV_DNS_DOMAIN_NAME => provider.dns_domain.encode('UTF-16LE').b, + Net::NTLM::TargetInfo::MSV_AV_DNS_COMPUTER_NAME => provider.dns_hostname.encode('UTF-16LE').b + ) + expect(target_info.av_pairs[Net::NTLM::TargetInfo::MSV_AV_TIMESTAMP].bytesize).to eq 8 + end end describe '#process_ntlm_type3' do diff --git a/spec/lib/ruby_smb/ntlm/client/session_spec.rb b/spec/lib/ruby_smb/ntlm/client/session_spec.rb index 53a80423c..ef334ef42 100644 --- a/spec/lib/ruby_smb/ntlm/client/session_spec.rb +++ b/spec/lib/ruby_smb/ntlm/client/session_spec.rb @@ -8,7 +8,9 @@ AGwAbwBjAGEAbAADADgAVwBJAE4ALQAzAE0AUwBQADgASwAyAEwAQwBHAEMA LgBtAHMAZgBsAGEAYgAuAGwAbwBjAGEAbAAHAAgAS6UAWjxl2AEAAAAA }) } - subject(:client) { RubySMB::NTLM::Client.new('rubysmb', 'rubysmb', flags: RubySMB::NTLM::DEFAULT_CLIENT_FLAGS) } + let(:username) { 'rubysmb' } + let(:password) { 'rubysmb' } + let(:client) { RubySMB::NTLM::Client.new(username, password, flags: RubySMB::NTLM::DEFAULT_CLIENT_FLAGS) } subject(:session) { described_class.new(client, message) } describe '#authenticate!' do @@ -17,72 +19,49 @@ session.authenticate! end - it 'checks if it is anonymous' do - expect(session).to receive(:is_anonymous?).at_least(1).times.and_call_original - session.authenticate! - end - it 'returns a Type3 message' do expect(session.authenticate!).to be_a Net::NTLM::Message::Type3 expect(session.authenticate!).to be_a Net::NTLM::Message end context 'when it is anonymous' do - before(:each) { allow(session).to receive(:is_anonymous?).and_return(true) } - after(:each) { session.authenticate! } + let(:username) { '' } + let(:password) { '' } it 'uses the correct lm response' do - expect(session).to_not receive(:lmv2_resp) - expect(Net::NTLM::Message::Type3).to receive(:create).and_wrap_original do |method, params| - expect(params).to include :lm_response - expect(params[:lm_response]).to eq "\x00".b - method.call(params) - end + expect(session.authenticate!.lm_response).to eq "\x00".b end it 'uses the correct ntlm response' do - expect(session).to_not receive(:ntlmv2_resp) - expect(Net::NTLM::Message::Type3).to receive(:create).and_wrap_original do |method, params| - expect(params).to include :ntlm_response - expect(params[:ntlm_response]).to eq '' - method.call(params) - end + expect(session.authenticate!.ntlm_response).to eq '' end end context 'when it is not anonymous' do - before(:each) { allow(session).to receive(:is_anonymous?).and_return(false) } - after(:each) { session.authenticate! } - it 'uses the correct lm response' do - expect(session).to receive(:lmv2_resp).and_call_original - expect(Net::NTLM::Message::Type3).to receive(:create).and_wrap_original do |method, params| - expect(params).to include :lm_response - expect(params[:lm_response].length).to be > 16 - method.call(params) - end + expect(session.authenticate!.lm_response.length).to be > 16 end it 'uses the correct ntlm response' do - expect(session).to receive(:ntlmv2_resp).and_call_original - expect(Net::NTLM::Message::Type3).to receive(:create).and_wrap_original do |method, params| - expect(params).to include :ntlm_response - expect(params[:ntlm_response].length).to be > 16 - method.call(params) - end + expect(session.authenticate!.ntlm_response.length).to be > 16 end end end describe '#calculate_user_session_key!' do - it 'returns an all zero key when it is anonymous' do - expect(session).to receive(:is_anonymous?).and_return(true) - expect(session.send(:calculate_user_session_key!)).to eq "\x00".b * 16 + context 'when it is anonymous' do + let(:username) { '' } + let(:password) { '' } + + it 'returns an all zero key' do + expect(session.send(:calculate_user_session_key!)).to eq "\x00".b * 16 + end end it 'returns a session key' do - expect(session).to receive(:is_anonymous?).and_return(false) - expect(session.send(:calculate_user_session_key!)).to_not eq "\x00".b * 16 + session_key = session.send(:calculate_user_session_key!) + expect(session_key.bytesize).to eq 16 + expect(session_key).to_not eq "\x00".b * 16 end end