ruby_smb 3.3.23 → 3.3.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,174 @@
1
+ RSpec.describe RubySMB::Gss::Provider::Multi do
2
+ let(:username) { 'RubySMB' }
3
+ let(:domain) { 'WORKGROUP' }
4
+ let(:password) { 'password' }
5
+ let(:ntlm_provider) do
6
+ RubySMB::Gss::Provider::NTLM.new.tap { |provider| provider.put_account(username, password, domain: domain) }
7
+ end
8
+ let(:other_authenticator) { double('authenticator', process: nil, reset!: nil, session_key: nil) }
9
+ # a stand-in for any non-NTLM mechanism, so the routing can be exercised without a second real provider
10
+ let(:other_provider) do
11
+ authenticator = other_authenticator
12
+ Class.new(RubySMB::Gss::Provider::Base) do
13
+ define_method(:mech_types) do
14
+ [RubySMB::Gss::OID_KERBEROS_5, RubySMB::Gss::OID_MICROSOFT_KERBEROS_5]
15
+ end
16
+
17
+ define_method(:new_authenticator) { |_server_client| authenticator }
18
+ end.new
19
+ end
20
+ let(:server_client) { double('server_client', logger: Logger.new(IO::NULL)) }
21
+
22
+ # referenced explicitly rather than via described_class, which resolves to the authenticator inside the nested group
23
+ subject(:provider) { RubySMB::Gss::Provider::Multi.new([other_provider, ntlm_provider]) }
24
+
25
+ describe '#initialize' do
26
+ it 'requires at least one provider' do
27
+ expect { RubySMB::Gss::Provider::Multi.new([]) }.to raise_error(ArgumentError)
28
+ expect { RubySMB::Gss::Provider::Multi.new(nil) }.to raise_error(ArgumentError)
29
+ end
30
+ end
31
+
32
+ describe '#mech_types' do
33
+ it 'advertises every mechanism of every provider' do
34
+ expect(provider.mech_types.map(&:oid)).to eq(
35
+ [
36
+ RubySMB::Gss::OID_KERBEROS_5.oid,
37
+ RubySMB::Gss::OID_MICROSOFT_KERBEROS_5.oid,
38
+ RubySMB::Gss::OID_NTLMSSP.oid
39
+ ]
40
+ )
41
+ end
42
+
43
+ it 'preserves the order the providers were given in' do
44
+ reversed = RubySMB::Gss::Provider::Multi.new([ntlm_provider, other_provider])
45
+ expect(reversed.mech_types.first.oid).to eq(RubySMB::Gss::OID_NTLMSSP.oid)
46
+ end
47
+
48
+ it 'advertises a mechanism supported by two providers only once' do
49
+ duplicated = RubySMB::Gss::Provider::Multi.new([ntlm_provider, RubySMB::Gss::Provider::NTLM.new])
50
+ expect(duplicated.mech_types.length).to eq(1)
51
+ end
52
+ end
53
+
54
+ describe '#provider_for' do
55
+ it 'finds the provider that handles the mechanism' do
56
+ expect(provider.provider_for(RubySMB::Gss::OID_KERBEROS_5)).to be(other_provider)
57
+ expect(provider.provider_for(RubySMB::Gss::OID_NTLMSSP)).to be(ntlm_provider)
58
+ end
59
+
60
+ it 'is nil when no provider handles the mechanism' do
61
+ expect(provider.provider_for(RubySMB::Gss::OID_NEGOEX)).to be_nil
62
+ end
63
+ end
64
+
65
+ describe RubySMB::Gss::Provider::Multi::Authenticator do
66
+ subject(:authenticator) { provider.new_authenticator(server_client) }
67
+
68
+ describe '#process' do
69
+ context 'when building the advertisement' do
70
+ it 'offers all of the mechanisms' do
71
+ buffer = authenticator.process(nil).buffer
72
+ expect(buffer).to eq(RubySMB::Gss.gss_neg_token_init(provider.mech_types))
73
+ end
74
+
75
+ it 'matches the underlying provider when only one is held' do
76
+ single = described_class.new(RubySMB::Gss::Provider::Multi.new([ntlm_provider]), server_client)
77
+ expect(single.process(nil).buffer).to eq(ntlm_provider.new_authenticator(server_client).process(nil).buffer)
78
+ end
79
+
80
+ it 'succeeds' do
81
+ expect(authenticator.process(nil).nt_status).to eq(WindowsError::NTStatus::STATUS_SUCCESS)
82
+ end
83
+ end
84
+
85
+ context 'when the client selects a mechanism' do
86
+ it 'routes the token to the provider that handles it' do
87
+ expect(other_authenticator).to receive(:process)
88
+ authenticator.process(gss_init(RubySMB::Gss::OID_KERBEROS_5))
89
+ end
90
+
91
+ it 'routes an NTLM token to the NTLM provider' do
92
+ type1 = Net::NTLM::Message::Type1.new.tap { |msg| msg.domain = domain }
93
+ result = authenticator.process(RubySMB::Gss.gss_type1(type1.serialize))
94
+ expect(result.nt_status).to eq(WindowsError::NTStatus::STATUS_MORE_PROCESSING_REQUIRED)
95
+ end
96
+
97
+ it 'refuses a mechanism no provider handles' do
98
+ expect(authenticator.process(gss_init(RubySMB::Gss::OID_NEGOEX))).to be_nil
99
+ end
100
+ end
101
+
102
+ context 'when the client continues an exchange' do
103
+ it 'refuses a continuation before a mechanism has been selected' do
104
+ # a NegTokenResp carries no mechanism OID, so there is nothing to route on
105
+ expect(authenticator.process(RubySMB::Gss.gss_type3('anything'))).to be_nil
106
+ end
107
+ end
108
+
109
+ it 'returns nil for a malformed request' do
110
+ expect(authenticator.process('not asn1 at all')).to be_nil
111
+ end
112
+ end
113
+
114
+ describe 'a complete NTLM exchange' do
115
+ it 'authenticates the same as the NTLM provider on its own' do
116
+ expect(complete_ntlm_exchange(authenticator)).to eq(
117
+ complete_ntlm_exchange(ntlm_provider.new_authenticator(server_client))
118
+ )
119
+ end
120
+
121
+ it 'succeeds for a known account' do
122
+ status, identity = complete_ntlm_exchange(authenticator)
123
+ expect(status).to eq(WindowsError::NTStatus::STATUS_SUCCESS)
124
+ expect(identity).to eq("#{domain}\\#{username}")
125
+ end
126
+
127
+ it 'exposes the session key of the mechanism that authenticated' do
128
+ complete_ntlm_exchange(authenticator)
129
+ expect(authenticator.session_key).to_not be_nil
130
+ end
131
+ end
132
+
133
+ describe '#reset!' do
134
+ it 'forgets the selected mechanism' do
135
+ complete_ntlm_exchange(authenticator)
136
+ authenticator.reset!
137
+ expect(authenticator.session_key).to be_nil
138
+ # with no mechanism selected, a continuation token has nothing to route to
139
+ expect(authenticator.process(RubySMB::Gss.gss_type3('anything'))).to be_nil
140
+ end
141
+ end
142
+ end
143
+
144
+ # Build a NegTokenInit that selects the specified mechanism, with an empty mechToken.
145
+ def gss_init(mech_type)
146
+ OpenSSL::ASN1::ASN1Data.new(
147
+ [
148
+ RubySMB::Gss::OID_SPNEGO,
149
+ OpenSSL::ASN1::ASN1Data.new(
150
+ [
151
+ OpenSSL::ASN1::Sequence.new(
152
+ [
153
+ OpenSSL::ASN1::ASN1Data.new([OpenSSL::ASN1::Sequence.new([mech_type])], 0, :CONTEXT_SPECIFIC),
154
+ OpenSSL::ASN1::ASN1Data.new([OpenSSL::ASN1::OctetString.new('')], 2, :CONTEXT_SPECIFIC)
155
+ ]
156
+ )
157
+ ], 0, :CONTEXT_SPECIFIC
158
+ )
159
+ ], 0, :APPLICATION
160
+ ).to_der
161
+ end
162
+
163
+ # Drive a full NTLM negotiation through the authenticator, returning the final status and identity.
164
+ def complete_ntlm_exchange(authenticator)
165
+ authenticator.process(nil)
166
+ type1 = Net::NTLM::Message::Type1.new.tap { |msg| msg.domain = domain }
167
+ challenge_result = authenticator.process(RubySMB::Gss.gss_type1(type1.serialize))
168
+ raw_type2 = RubySMB::Gss.asn1dig(OpenSSL::ASN1.decode(challenge_result.buffer), 0, 2, 0).value
169
+ type2 = Net::NTLM::Message.parse(raw_type2)
170
+ type3 = type2.response({ user: username, password: password, domain: domain }, { ntlmv2: true })
171
+ result = authenticator.process(RubySMB::Gss.gss_type3(type3.serialize))
172
+ [result.nt_status, result.identity]
173
+ end
174
+ end
@@ -0,0 +1,80 @@
1
+ RSpec.describe 'SPNEGO negotiation tokens' do
2
+ let(:mech_types) do
3
+ [RubySMB::Gss::OID_KERBEROS_5, RubySMB::Gss::OID_MICROSOFT_KERBEROS_5, RubySMB::Gss::OID_NTLMSSP]
4
+ end
5
+
6
+ describe RubySMB::Gss::SpnegoNegTokenInit do
7
+ describe '.build' do
8
+ subject(:token) { described_class.build(mech_types) }
9
+
10
+ # the exact bytes a server advertised before this was modelled with RASN1, kept so the wire format does not
11
+ # drift: the SPNEGO OID, the three mechanisms, and the Microsoft negHints placeholder
12
+ let(:legacy_der) do
13
+ [
14
+ '605e06062b0601050502a0543052a024302206092a864886f71201020206092a864882' \
15
+ 'f712010202060a2b06010401823702020aa32a3028a0261b246e6f745f646566696e65' \
16
+ '645f696e5f5246433431373840706c656173655f69676e6f7265'
17
+ ].pack('H*')
18
+ end
19
+
20
+ it 'is byte-identical to the token the hand-rolled builder produced' do
21
+ expect(token).to eq(legacy_der)
22
+ end
23
+
24
+ # the exact NTLM-only advertisement a default server built before this change, when the NTLM provider
25
+ # hardcoded a single OID_NTLMSSP. existing servers still emit this, so lock it against a wire regression.
26
+ it 'is byte-identical to the NTLM-only advertisement a default server built before this change' do
27
+ legacy_ntlm_der = [
28
+ '604806062b0601050502a03e303ca00e300c060a2b06010401823702020aa32a3028' \
29
+ 'a0261b246e6f745f646566696e65645f696e5f5246433431373840706c656173655f' \
30
+ '69676e6f7265'
31
+ ].pack('H*')
32
+ expect(described_class.build([RubySMB::Gss::OID_NTLMSSP])).to eq(legacy_ntlm_der)
33
+ end
34
+
35
+ it 'advertises the mechanisms in order' do
36
+ decoded = OpenSSL::ASN1.decode(token)
37
+ mech_list = decoded.value[1].value[0].value[0].value[0].value
38
+ expect(mech_list.map(&:oid)).to eq(mech_types.map(&:oid))
39
+ end
40
+
41
+ it 'carries the Microsoft negHints placeholder' do
42
+ decoded = OpenSSL::ASN1.decode(token)
43
+ hint = decoded.value[1].value[0].value[1].value[0].value[0].value[0].value
44
+ expect(hint).to eq(RubySMB::Gss::SpnegoNegTokenInit::NEG_HINTS_NAME)
45
+ end
46
+ end
47
+
48
+ describe '.parse' do
49
+ # a SPNEGO NegTokenInit selecting Kerberos and carrying the given mechanism token
50
+ def neg_token_init(token)
51
+ inner = [OpenSSL::ASN1::ASN1Data.new([OpenSSL::ASN1::Sequence.new([RubySMB::Gss::OID_KERBEROS_5])], 0, :CONTEXT_SPECIFIC)]
52
+ inner << OpenSSL::ASN1::ASN1Data.new([OpenSSL::ASN1::OctetString.new(token)], 2, :CONTEXT_SPECIFIC) unless token.nil?
53
+
54
+ OpenSSL::ASN1::ASN1Data.new(
55
+ [
56
+ RubySMB::Gss::OID_SPNEGO,
57
+ OpenSSL::ASN1::ASN1Data.new([OpenSSL::ASN1::Sequence.new(inner)], 0, :CONTEXT_SPECIFIC)
58
+ ], 0, :APPLICATION
59
+ ).to_der
60
+ end
61
+
62
+ it 'reads the mechanism token' do
63
+ expect(described_class.parse(neg_token_init('a mechanism token')).mech_token).to eq('a mechanism token')
64
+ end
65
+
66
+ it 'is nil when the token carries no mechanism token' do
67
+ expect(described_class.parse(neg_token_init(nil)).mech_token).to be_nil
68
+ end
69
+ end
70
+ end
71
+
72
+ describe RubySMB::Gss::SpnegoNegTokenTarg do
73
+ describe '.parse' do
74
+ it 'reads the response token from a continuation' do
75
+ targ = described_class.parse(RubySMB::Gss.gss_type3('a continuation token'))
76
+ expect(targ.response_token).to eq('a continuation token')
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,52 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe RubySMB::Nbss::NodeStatusRequest do
4
+ subject(:request) { described_class.new(transaction_id: 0x1234) }
5
+
6
+ describe 'encoded bytes' do
7
+ let(:bytes) { request.to_binary_s }
8
+
9
+ it 'starts with a 12-byte NBNS header' do
10
+ expect(bytes[0, 2].unpack1('n')).to eq(0x1234)
11
+ expect(bytes[2, 2].unpack1('n')).to eq(0x0000) # flags
12
+ expect(bytes[4, 2].unpack1('n')).to eq(1) # qdcount
13
+ expect(bytes[6, 2].unpack1('n')).to eq(0) # ancount
14
+ expect(bytes[8, 2].unpack1('n')).to eq(0) # nscount
15
+ expect(bytes[10, 2].unpack1('n')).to eq(0) # arcount
16
+ end
17
+
18
+ it 'encodes the wildcard question name as 34 bytes (length + 32-char L1 + null)' do
19
+ expect(bytes[12].unpack1('C')).to eq(0x20) # label length
20
+ expect(bytes[13, 32]).to eq('CKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA')
21
+ expect(bytes[45].unpack1('C')).to eq(0x00) # null label terminator
22
+ end
23
+
24
+ it 'ends with QTYPE=NBSTAT and QCLASS=IN' do
25
+ expect(bytes[46, 2].unpack1('n')).to eq(described_class::QUESTION_TYPE_NBSTAT)
26
+ expect(bytes[48, 2].unpack1('n')).to eq(described_class::QUESTION_CLASS_IN)
27
+ end
28
+
29
+ it 'is exactly 50 bytes long' do
30
+ expect(bytes.bytesize).to eq(50)
31
+ end
32
+ end
33
+
34
+ describe 'flags' do
35
+ it 'packs the OPCODE, NM_FLAGS and RCODE fields in RFC bit order' do
36
+ request.response = 1
37
+ request.nm_flags.authoritative_answer = 1
38
+ request.nm_flags.broadcast = 1
39
+ request.rcode = 0x5
40
+
41
+ expect(request.to_binary_s[2, 2].unpack1('n')).to eq(0x8415)
42
+ end
43
+
44
+ it 'defaults every request flag bit to zero' do
45
+ expect(request.to_binary_s[2, 2].unpack1('n')).to eq(0x0000)
46
+ expect(request.response.to_i).to eq(0)
47
+ expect(request.opcode.to_i).to eq(0)
48
+ expect(request.nm_flags.broadcast.to_i).to eq(0)
49
+ expect(request.rcode.to_i).to eq(0)
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,75 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe RubySMB::Nbss::NodeStatusResponse do
4
+ def build_response(names)
5
+ data = ''.b
6
+ data << [0x1234].pack('n') # transaction_id
7
+ data << [0x8400].pack('n') # flags: response, authoritative
8
+ data << [0].pack('n') # qdcount
9
+ data << [1].pack('n') # ancount
10
+ data << [0].pack('n') << [0].pack('n') # nscount, arcount
11
+ data << [0x20].pack('C') << ('A' * 32) << "\x00".b # owner name L1
12
+ data << [0x0021].pack('n') # RR type NBSTAT
13
+ data << [0x0001].pack('n') # RR class IN
14
+ data << [0].pack('N') # TTL
15
+ data << [1 + names.length * 18 + 46].pack('n') # rdlength
16
+ data << [names.length].pack('C')
17
+ names.each do |name, suffix, flags|
18
+ data << name.to_s.ljust(15, ' ') << [suffix].pack('C') << [flags].pack('n')
19
+ end
20
+ data << ("\x00".b * 46) # statistics (unused)
21
+ data
22
+ end
23
+
24
+ describe 'parsing' do
25
+ it 'decodes the name table' do
26
+ response = described_class.read(build_response([
27
+ ['WIN95', 0x00, 0x0400],
28
+ ['WIN95', 0x20, 0x0400],
29
+ ['WORKGROUP', 0x00, 0x8400]
30
+ ]))
31
+ expect(response.num_names).to eq(3)
32
+ expect(response.node_names[0].netbios_name.to_s.rstrip).to eq('WIN95')
33
+ expect(response.node_names[0].suffix).to eq(0x00)
34
+ expect(response.node_names[1].suffix).to eq(0x20)
35
+ expect(response.node_names[2].group?).to be true
36
+ expect(response.response.to_i).to eq(1)
37
+ expect(response.nm_flags.authoritative_answer.to_i).to eq(1)
38
+ end
39
+ end
40
+
41
+ describe 'flags' do
42
+ it 'decodes the header OPCODE and NM_FLAGS fields from the second word' do
43
+ response = described_class.read(build_response([['WIN95', 0x20, 0x0400]]))
44
+
45
+ expect(response.response.to_i).to eq(1) # high bit of 0x8400
46
+ expect(response.nm_flags.authoritative_answer.to_i).to eq(1)
47
+ end
48
+
49
+ it 'defines NODE_NAME flags in RFC bit order' do
50
+ flags = RubySMB::Nbss::NodeStatusNameFlags.new
51
+ flags.group = 1
52
+ flags.active = 1
53
+
54
+ expect(flags.to_binary_s.unpack1('n')).to eq(0x8400)
55
+ expect(flags.group.to_i).to eq(1)
56
+ expect(flags.active.to_i).to eq(1)
57
+ end
58
+
59
+ it 'exposes parsed NODE_NAME flags as named fields' do
60
+ response = described_class.read(build_response([
61
+ ['WORKGROUP', 0x00, 0x8400],
62
+ ['FILESERVER', 0x20, 0x0400]
63
+ ]))
64
+
65
+ group_entry = response.node_names[0]
66
+ unique_entry = response.node_names[1]
67
+
68
+ expect(group_entry.name_flags.group.to_i).to eq(1)
69
+ expect(group_entry.name_flags.active.to_i).to eq(1)
70
+ expect(group_entry.group?).to be true
71
+ expect(unique_entry.name_flags.group.to_i).to eq(0)
72
+ expect(unique_entry.unique?).to be true
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,146 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe RubySMB::Nbss::NodeStatus do
4
+ let(:udp_sock) { double('UDPSocket') }
5
+ # #recvfrom returns the sender as [address_family, port, host, numeric_host];
6
+ # the numeric address at index 3 is matched against the queried target.
7
+ let(:peer) { ['AF_INET', 137, '10.0.0.2', '10.0.0.2'] }
8
+
9
+ # Node Status requests carry a random transaction ID; the response is only
10
+ # accepted when it echoes that ID back. Pin it so the fixtures match.
11
+ before { allow(SecureRandom).to receive(:random_number).and_return(0x1234) }
12
+
13
+ def build_response(names)
14
+ data = ''.b
15
+ data << [0x1234].pack('n') # transaction_id
16
+ data << [0x8400].pack('n') # flags
17
+ data << [0].pack('n') << [1].pack('n') # qdcount, ancount
18
+ data << [0].pack('n') << [0].pack('n') # nscount, arcount
19
+ data << [0x20].pack('C') << ('A' * 32) << "\x00".b # owner name
20
+ data << [0x0021].pack('n') << [0x0001].pack('n')
21
+ data << [0].pack('N') # TTL
22
+ data << [1 + names.length * 18 + 46].pack('n')
23
+ data << [names.length].pack('C')
24
+ names.each do |name, suffix, flags|
25
+ data << name.to_s.ljust(15, ' ') << [suffix].pack('C') << [flags].pack('n')
26
+ end
27
+ data << ("\x00".b * 46)
28
+ data
29
+ end
30
+
31
+ describe '.query' do
32
+ it 'sends the request with #send(mesg, flags, host, port) and reads via recvfrom(maxlen)' do
33
+ response_bytes = build_response([
34
+ ['WIN95', 0x00, 0x0400],
35
+ ['WIN95', 0x20, 0x0400],
36
+ ['WORKGROUP', 0x00, 0x8400]
37
+ ])
38
+
39
+ expect(udp_sock).to receive(:send) do |bytes, flags, host, port|
40
+ expect(flags).to eq(0)
41
+ expect(host).to eq('10.0.0.2')
42
+ expect(port).to eq(137)
43
+ expect(bytes.bytesize).to eq(50)
44
+ end
45
+ expect(IO).to receive(:select).and_return([udp_sock])
46
+ expect(udp_sock).to receive(:recvfrom).with(4096).and_return([response_bytes, peer])
47
+
48
+ entries = described_class.query('10.0.0.2', udp_socket: udp_sock)
49
+ expect(entries.size).to eq(3)
50
+ expect(entries[1].name).to eq('WIN95')
51
+ expect(entries[1].suffix).to eq(0x20)
52
+ expect(entries[1].unique?).to be true
53
+ expect(entries[2].group).to be true
54
+ end
55
+
56
+ it 'retries up to the configured limit before giving up' do
57
+ call_count = 0
58
+ allow(udp_sock).to receive(:send) { call_count += 1 }
59
+ allow(IO).to receive(:select).and_return(nil) # always time out
60
+
61
+ expect(described_class.query('10.0.0.2', retries: 4, timeout: 0.01, udp_socket: udp_sock)).to be_nil
62
+ expect(call_count).to eq(4)
63
+ end
64
+
65
+ it 'returns nil when the response can not be parsed' do
66
+ expect(udp_sock).to receive(:send)
67
+ expect(IO).to receive(:select).and_return([udp_sock])
68
+ expect(udp_sock).to receive(:recvfrom).and_return(["\xff\xff".b, peer])
69
+ expect(described_class.query('10.0.0.2', retries: 1, timeout: 0.01, udp_socket: udp_sock)).to be_nil
70
+ end
71
+
72
+ it 'retries after a malformed datagram and succeeds on a later attempt' do
73
+ good = build_response([['WIN95', 0x20, 0x0400]])
74
+ allow(udp_sock).to receive(:send)
75
+ allow(IO).to receive(:select).and_return([udp_sock])
76
+ # First datagram is truncated (NodeStatusResponse.read raises); the second
77
+ # is well-formed. The rescue must stay inside the retry loop for this.
78
+ allow(udp_sock).to receive(:recvfrom).and_return(["\xff\xff".b, peer], [good, peer])
79
+
80
+ entries = described_class.query('10.0.0.2', retries: 3, timeout: 0.01, udp_socket: udp_sock)
81
+ expect(entries).not_to be_nil
82
+ expect(entries.first.suffix).to eq(0x20)
83
+ end
84
+
85
+ it 'raises ArgumentError when host is not a numeric IPv4 address' do
86
+ expect { described_class.query('example.com', udp_socket: udp_sock) }.to raise_error(ArgumentError)
87
+ end
88
+
89
+ it 'rejects a reply whose transaction ID does not match the request' do
90
+ allow(SecureRandom).to receive(:random_number).and_return(0x1234)
91
+ mismatched = build_response([['WIN95', 0x20, 0x0400]])
92
+ mismatched[0, 2] = [0x9999].pack('n') # overwrite transaction_id
93
+ expect(udp_sock).to receive(:send)
94
+ expect(IO).to receive(:select).and_return([udp_sock])
95
+ expect(udp_sock).to receive(:recvfrom).and_return([mismatched, peer])
96
+ expect(described_class.query('10.0.0.2', retries: 1, timeout: 0.01, udp_socket: udp_sock)).to be_nil
97
+ end
98
+
99
+ it 'rejects a reply from an unexpected source address' do
100
+ spoofed = build_response([['WIN95', 0x20, 0x0400]])
101
+ expect(udp_sock).to receive(:send)
102
+ expect(IO).to receive(:select).and_return([udp_sock])
103
+ expect(udp_sock).to receive(:recvfrom).and_return([spoofed, ['AF_INET', 137, '10.0.0.9', '10.0.0.9']])
104
+ expect(described_class.query('10.0.0.2', retries: 1, timeout: 0.01, udp_socket: udp_sock)).to be_nil
105
+ end
106
+
107
+ it 'returns nil on IOError and does not close the socket' do
108
+ allow(udp_sock).to receive(:send).and_raise(IOError, 'boom')
109
+ expect(udp_sock).not_to receive(:close)
110
+ expect(described_class.query('10.0.0.2', retries: 1, udp_socket: udp_sock)).to be_nil
111
+ end
112
+ end
113
+
114
+ describe '.file_server_name' do
115
+ it 'returns the unique 0x20 entry' do
116
+ response_bytes = build_response([
117
+ ['WORKGROUP', 0x00, 0x8400],
118
+ ['FILESERVER', 0x20, 0x0400]
119
+ ])
120
+ allow(udp_sock).to receive(:send)
121
+ allow(IO).to receive(:select).and_return([udp_sock])
122
+ allow(udp_sock).to receive(:recvfrom).and_return([response_bytes, peer])
123
+
124
+ expect(described_class.file_server_name('10.0.0.2', udp_socket: udp_sock)).to eq('FILESERVER')
125
+ end
126
+
127
+ it 'returns nil when no unique 0x20 entry is present' do
128
+ response_bytes = build_response([['HOST', 0x00, 0x0400]])
129
+ allow(udp_sock).to receive(:send)
130
+ allow(IO).to receive(:select).and_return([udp_sock])
131
+ allow(udp_sock).to receive(:recvfrom).and_return([response_bytes, peer])
132
+
133
+ expect(described_class.file_server_name('10.0.0.2', udp_socket: udp_sock)).to be_nil
134
+ end
135
+ end
136
+
137
+ describe RubySMB::Nbss::NodeStatus::Entry do
138
+ it '#to_s formats like nmblookup output' do
139
+ entry = described_class.new('WIN95', 0x20, false, true)
140
+ expect(entry.to_s).to include('WIN95')
141
+ expect(entry.to_s).to include('<20>')
142
+ expect(entry.to_s).to include('UNIQUE')
143
+ expect(entry.to_s).to include('ACTIVE')
144
+ end
145
+ end
146
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_smb
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.3.23
4
+ version: 3.3.25
5
5
  platform: ruby
6
6
  authors:
7
7
  - Metasploit Hackers
@@ -13,7 +13,7 @@ authors:
13
13
  autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
- date: 2026-09-28 00:00:00.000000000 Z
16
+ date: 2026-10-01 00:00:00.000000000 Z
17
17
  dependencies:
18
18
  - !ruby/object:Gem::Dependency
19
19
  name: redcarpet
@@ -127,6 +127,20 @@ dependencies:
127
127
  - - '='
128
128
  - !ruby/object:Gem::Version
129
129
  version: 2.4.15
130
+ - !ruby/object:Gem::Dependency
131
+ name: rasn1
132
+ requirement: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ version: '0.12'
137
+ type: :runtime
138
+ prerelease: false
139
+ version_requirements: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - ">="
142
+ - !ruby/object:Gem::Version
143
+ version: '0.12'
130
144
  - !ruby/object:Gem::Dependency
131
145
  name: openssl-ccm
132
146
  requirement: !ruby/object:Gem::Requirement
@@ -455,11 +469,19 @@ files:
455
469
  - lib/ruby_smb/gss.rb
456
470
  - lib/ruby_smb/gss/provider.rb
457
471
  - lib/ruby_smb/gss/provider/authenticator.rb
472
+ - lib/ruby_smb/gss/provider/kerberos.rb
473
+ - lib/ruby_smb/gss/provider/multi.rb
458
474
  - lib/ruby_smb/gss/provider/ntlm.rb
475
+ - lib/ruby_smb/gss/spnego_neg_token_init.rb
476
+ - lib/ruby_smb/gss/spnego_neg_token_targ.rb
459
477
  - lib/ruby_smb/impersonation_levels.rb
460
478
  - lib/ruby_smb/nbss.rb
479
+ - lib/ruby_smb/nbss/name_service_header_flags.rb
461
480
  - lib/ruby_smb/nbss/negative_session_response.rb
462
481
  - lib/ruby_smb/nbss/netbios_name.rb
482
+ - lib/ruby_smb/nbss/node_status.rb
483
+ - lib/ruby_smb/nbss/node_status_request.rb
484
+ - lib/ruby_smb/nbss/node_status_response.rb
463
485
  - lib/ruby_smb/nbss/session_header.rb
464
486
  - lib/ruby_smb/nbss/session_request.rb
465
487
  - lib/ruby_smb/ntlm.rb
@@ -839,12 +861,18 @@ files:
839
861
  - spec/lib/ruby_smb/fscc/file_system_information_spec.rb
840
862
  - spec/lib/ruby_smb/fscc/fscc_file_attributes_spec.rb
841
863
  - spec/lib/ruby_smb/generic_packet_spec.rb
864
+ - spec/lib/ruby_smb/gss/provider/kerberos_spec.rb
865
+ - spec/lib/ruby_smb/gss/provider/multi_spec.rb
842
866
  - spec/lib/ruby_smb/gss/provider/ntlm/account_spec.rb
843
867
  - spec/lib/ruby_smb/gss/provider/ntlm/authenticator_spec.rb
844
868
  - spec/lib/ruby_smb/gss/provider/ntlm/os_version_spec.rb
845
869
  - spec/lib/ruby_smb/gss/provider/ntlm_spec.rb
870
+ - spec/lib/ruby_smb/gss/spnego_spec.rb
846
871
  - spec/lib/ruby_smb/nbss/negative_session_response_spec.rb
847
872
  - spec/lib/ruby_smb/nbss/netbios_name_spec.rb
873
+ - spec/lib/ruby_smb/nbss/node_status_request_spec.rb
874
+ - spec/lib/ruby_smb/nbss/node_status_response_spec.rb
875
+ - spec/lib/ruby_smb/nbss/node_status_spec.rb
848
876
  - spec/lib/ruby_smb/nbss/session_header_spec.rb
849
877
  - spec/lib/ruby_smb/nbss/session_request_spec.rb
850
878
  - spec/lib/ruby_smb/ntlm/client/session_spec.rb
@@ -1196,12 +1224,18 @@ test_files:
1196
1224
  - spec/lib/ruby_smb/fscc/file_system_information_spec.rb
1197
1225
  - spec/lib/ruby_smb/fscc/fscc_file_attributes_spec.rb
1198
1226
  - spec/lib/ruby_smb/generic_packet_spec.rb
1227
+ - spec/lib/ruby_smb/gss/provider/kerberos_spec.rb
1228
+ - spec/lib/ruby_smb/gss/provider/multi_spec.rb
1199
1229
  - spec/lib/ruby_smb/gss/provider/ntlm/account_spec.rb
1200
1230
  - spec/lib/ruby_smb/gss/provider/ntlm/authenticator_spec.rb
1201
1231
  - spec/lib/ruby_smb/gss/provider/ntlm/os_version_spec.rb
1202
1232
  - spec/lib/ruby_smb/gss/provider/ntlm_spec.rb
1233
+ - spec/lib/ruby_smb/gss/spnego_spec.rb
1203
1234
  - spec/lib/ruby_smb/nbss/negative_session_response_spec.rb
1204
1235
  - spec/lib/ruby_smb/nbss/netbios_name_spec.rb
1236
+ - spec/lib/ruby_smb/nbss/node_status_request_spec.rb
1237
+ - spec/lib/ruby_smb/nbss/node_status_response_spec.rb
1238
+ - spec/lib/ruby_smb/nbss/node_status_spec.rb
1205
1239
  - spec/lib/ruby_smb/nbss/session_header_spec.rb
1206
1240
  - spec/lib/ruby_smb/nbss/session_request_spec.rb
1207
1241
  - spec/lib/ruby_smb/ntlm/client/session_spec.rb