ruby_smb 3.3.24 → 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.
- checksums.yaml +4 -4
- data/lib/ruby_smb/client.rb +14 -3
- data/lib/ruby_smb/error.rb +11 -1
- data/lib/ruby_smb/nbss/name_service_header_flags.rb +18 -0
- data/lib/ruby_smb/nbss/negative_session_response.rb +5 -5
- data/lib/ruby_smb/nbss/node_status.rb +137 -0
- data/lib/ruby_smb/nbss/node_status_request.rb +38 -0
- data/lib/ruby_smb/nbss/node_status_response.rb +85 -0
- data/lib/ruby_smb/nbss.rb +11 -0
- data/lib/ruby_smb/version.rb +1 -1
- data/spec/lib/ruby_smb/client_spec.rb +23 -0
- data/spec/lib/ruby_smb/nbss/node_status_request_spec.rb +52 -0
- data/spec/lib/ruby_smb/nbss/node_status_response_spec.rb +75 -0
- data/spec/lib/ruby_smb/nbss/node_status_spec.rb +146 -0
- metadata +12 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2bda3450e062ff69f5c20600086e7c8686d539b44a339f560764c077c6a1a3cc
|
|
4
|
+
data.tar.gz: 3c2ec535d37e7880e4ba7d023ff8d381f33da0bb47c28424bc4f3828d13f9e2b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e57991b0b74090c01a6b6568dce52828e16045255ea2a3debc0afac2fa2f5bbff85e7319b21c36377a5e8a49e327bad518ebf58792e0e3c5a3d338d953064b1a
|
|
7
|
+
data.tar.gz: cb6a8ef6b78ca308df71d89b8c290dcce2ffcc64f155f41be7a66060713aa2c3c990ee4aa8e95d7308f0467e8b58d98ed0891523451086932f5205a26e1df214
|
data/lib/ruby_smb/client.rb
CHANGED
|
@@ -350,7 +350,7 @@ module RubySMB
|
|
|
350
350
|
@server_max_write_size = RubySMB::SMB2::File::MAX_PACKET_SIZE
|
|
351
351
|
@server_max_transact_size = RubySMB::SMB2::File::MAX_PACKET_SIZE
|
|
352
352
|
@server_supports_multi_credit = false
|
|
353
|
-
@server_supports_nt_smbs
|
|
353
|
+
@server_supports_nt_smbs = true
|
|
354
354
|
|
|
355
355
|
# SMB 3.x options
|
|
356
356
|
# this merely initializes the default value for session encryption, it may be changed as necessary when a
|
|
@@ -668,6 +668,14 @@ module RubySMB
|
|
|
668
668
|
|
|
669
669
|
# Requests a NetBIOS Session Service using the provided name.
|
|
670
670
|
#
|
|
671
|
+
# On refusal the raised {RubySMB::Error::NetBiosSessionService} carries the
|
|
672
|
+
# numeric NBSS `error_code`. A {RubySMB::Nbss::CALLED_NAME_NOT_PRESENT} rejection of
|
|
673
|
+
# the default `'*SMBSERVER'` name means the server (e.g. Windows 9x) wants
|
|
674
|
+
# its real name: resolve it with {RubySMB::Nbss::NodeStatus.file_server_name},
|
|
675
|
+
# reconnect (the server drops the connection after a negative response), and
|
|
676
|
+
# retry. Reconnecting is left to the caller so the new socket is routed the
|
|
677
|
+
# same way as the original (e.g. through a Metasploit pivot).
|
|
678
|
+
#
|
|
671
679
|
# @param name [String] the NetBIOS name to request
|
|
672
680
|
# @return [TrueClass] if session request is granted
|
|
673
681
|
# @raise [RubySMB::Error::NetBiosSessionService] if session request is refused
|
|
@@ -679,8 +687,11 @@ module RubySMB
|
|
|
679
687
|
begin
|
|
680
688
|
session_header = RubySMB::Nbss::SessionHeader.read(raw_response)
|
|
681
689
|
if session_header.session_packet_type == RubySMB::Nbss::NEGATIVE_SESSION_RESPONSE
|
|
682
|
-
negative_session_response =
|
|
683
|
-
raise RubySMB::Error::NetBiosSessionService
|
|
690
|
+
negative_session_response = RubySMB::Nbss::NegativeSessionResponse.read(raw_response)
|
|
691
|
+
raise RubySMB::Error::NetBiosSessionService.new(
|
|
692
|
+
"Session Request failed: #{negative_session_response.error_msg}",
|
|
693
|
+
error_code: negative_session_response.error_code.to_i
|
|
694
|
+
)
|
|
684
695
|
end
|
|
685
696
|
rescue IOError
|
|
686
697
|
raise RubySMB::Error::InvalidPacket, 'Not a NBSS packet'
|
data/lib/ruby_smb/error.rb
CHANGED
|
@@ -11,7 +11,17 @@ module RubySMB
|
|
|
11
11
|
|
|
12
12
|
# Raised when there is a problem with communication over NetBios Session Service
|
|
13
13
|
# @see https://wiki.wireshark.org/NetBIOS/NBSS
|
|
14
|
-
class NetBiosSessionService < RubySMBError
|
|
14
|
+
class NetBiosSessionService < RubySMBError
|
|
15
|
+
# The numeric NBSS error code from a NEGATIVE_SESSION_RESPONSE, or nil
|
|
16
|
+
# if the error was raised outside that context.
|
|
17
|
+
# @return [Integer, nil]
|
|
18
|
+
attr_reader :error_code
|
|
19
|
+
|
|
20
|
+
def initialize(msg = nil, error_code: nil)
|
|
21
|
+
@error_code = error_code
|
|
22
|
+
super(msg)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
15
25
|
|
|
16
26
|
# Raised when trying to parse raw binary into a Packet and the data
|
|
17
27
|
# is invalid.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module RubySMB
|
|
2
|
+
module Nbss
|
|
3
|
+
# The NM_FLAGS field of the NetBIOS Name Service header, as defined in
|
|
4
|
+
# RFC 1002 section 4.2.1.1. The surrounding R/OPCODE and RCODE bits are
|
|
5
|
+
# declared inline in the header records that embed this field
|
|
6
|
+
# ({NodeStatusRequest} and {NodeStatusResponse}).
|
|
7
|
+
class NameServiceHeaderFlags < BinData::Record
|
|
8
|
+
endian :big
|
|
9
|
+
|
|
10
|
+
bit1 :authoritative_answer, label: 'Authoritative Answer', initial_value: 0
|
|
11
|
+
bit1 :truncated, label: 'Truncated', initial_value: 0
|
|
12
|
+
bit1 :recursion_desired, label: 'Recursion Desired', initial_value: 0
|
|
13
|
+
bit1 :recursion_available, label: 'Recursion Available', initial_value: 0
|
|
14
|
+
bit2 :reserved, label: 'Reserved', initial_value: 0
|
|
15
|
+
bit1 :broadcast, label: 'Broadcast', initial_value: 0
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -12,15 +12,15 @@ module RubySMB
|
|
|
12
12
|
|
|
13
13
|
def error_msg
|
|
14
14
|
case error_code
|
|
15
|
-
when
|
|
15
|
+
when NOT_LISTENING_ON_CALLED_NAME
|
|
16
16
|
'Not listening on called name'
|
|
17
|
-
when
|
|
17
|
+
when NOT_LISTENING_FOR_CALLING_NAME
|
|
18
18
|
'Not listening for calling name'
|
|
19
|
-
when
|
|
19
|
+
when CALLED_NAME_NOT_PRESENT
|
|
20
20
|
'Called name not present'
|
|
21
|
-
when
|
|
21
|
+
when CALLED_NAME_INSUFFICIENT_RESOURCES
|
|
22
22
|
'Called name present, but insufficient resources'
|
|
23
|
-
when
|
|
23
|
+
when UNSPECIFIED_ERROR
|
|
24
24
|
'Unspecified error'
|
|
25
25
|
end
|
|
26
26
|
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
require 'socket'
|
|
2
|
+
require 'ipaddr'
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module RubySMB
|
|
6
|
+
module Nbss
|
|
7
|
+
# Pure-Ruby implementation of `nmblookup -A <ip>`: sends an NBNS Node
|
|
8
|
+
# Status Request (RFC 1002 4.2.17) over UDP/137 and returns the
|
|
9
|
+
# server's name table.
|
|
10
|
+
#
|
|
11
|
+
# No external binaries are invoked. Compare to Samba's `nmblookup`,
|
|
12
|
+
# which shells out and requires the `samba-common-bin` package to be
|
|
13
|
+
# installed.
|
|
14
|
+
module NodeStatus
|
|
15
|
+
NBNS_PORT = 137
|
|
16
|
+
|
|
17
|
+
# Default per-attempt receive timeout, in seconds.
|
|
18
|
+
DEFAULT_TIMEOUT = 2.0
|
|
19
|
+
|
|
20
|
+
# Default number of attempts before giving up.
|
|
21
|
+
DEFAULT_RETRIES = 3
|
|
22
|
+
|
|
23
|
+
# One entry in the returned name table.
|
|
24
|
+
#
|
|
25
|
+
# @!attribute [rw] name [String] the NetBIOS name (trimmed)
|
|
26
|
+
# @!attribute [rw] suffix [Integer] 1-byte NetBIOS suffix
|
|
27
|
+
# @!attribute [rw] group [Boolean] true for a group name, false for unique
|
|
28
|
+
# @!attribute [rw] active [Boolean] true if the name is registered
|
|
29
|
+
Entry = Struct.new(:name, :suffix, :group, :active) do
|
|
30
|
+
def unique?
|
|
31
|
+
!group
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Human-readable form like `WIN95 <20> UNIQUE ACTIVE`.
|
|
35
|
+
def to_s
|
|
36
|
+
flags = [group ? 'GROUP' : 'UNIQUE', active ? 'ACTIVE' : 'INACTIVE'].join(' ')
|
|
37
|
+
format('%-16s <%02X> %s', name, suffix, flags)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Query a host for its NetBIOS name table.
|
|
42
|
+
#
|
|
43
|
+
# NBNS is IPv4-only and this is a unicast query, so `host` must be a
|
|
44
|
+
# numeric IPv4 address (e.g. `10.0.0.1`). A hostname is rejected because
|
|
45
|
+
# replies are matched against the numeric peer address returned by
|
|
46
|
+
# `recvfrom`, which a hostname would never equal.
|
|
47
|
+
#
|
|
48
|
+
# @param host [String] target IPv4 address (unicast — no broadcast)
|
|
49
|
+
# @param port [Integer] destination UDP port (default 137)
|
|
50
|
+
# @param timeout [Numeric] per-attempt receive timeout in seconds
|
|
51
|
+
# @param retries [Integer] total number of attempts
|
|
52
|
+
# @param udp_socket [UDPSocket, Rex::Socket::Udp] caller-owned UDP socket.
|
|
53
|
+
# The caller is responsible for binding and closing it. It must
|
|
54
|
+
# implement `#send(mesg, flags, host, port)` and `#recvfrom(maxlen)`,
|
|
55
|
+
# and leave `do_not_reverse_lookup` at its default so `recvfrom` does
|
|
56
|
+
# not perform a reverse DNS lookup per datagram.
|
|
57
|
+
# @return [Array<Entry>, nil] the name table, or nil on timeout/parse failure
|
|
58
|
+
# @raise [ArgumentError] if `host` is not a numeric IPv4 address
|
|
59
|
+
def self.query(host, port: NBNS_PORT, timeout: DEFAULT_TIMEOUT,
|
|
60
|
+
retries: DEFAULT_RETRIES, udp_socket:)
|
|
61
|
+
expected_address = begin
|
|
62
|
+
IPAddr.new(host).native
|
|
63
|
+
rescue IPAddr::InvalidAddressError
|
|
64
|
+
raise ArgumentError, "host must be an IPv4 address, got #{host.inspect}"
|
|
65
|
+
end
|
|
66
|
+
raise ArgumentError, "NBNS is IPv4-only, got #{host.inspect}" unless expected_address.ipv4?
|
|
67
|
+
|
|
68
|
+
request = NodeStatusRequest.new(transaction_id: SecureRandom.random_number(0x10000))
|
|
69
|
+
bytes = request.to_binary_s
|
|
70
|
+
|
|
71
|
+
retries.times do
|
|
72
|
+
udp_socket.send(bytes, 0, host, port)
|
|
73
|
+
next unless IO.select([udp_socket], nil, nil, timeout)
|
|
74
|
+
|
|
75
|
+
data, addr = udp_socket.recvfrom(4096)
|
|
76
|
+
next if data.nil? || data.empty?
|
|
77
|
+
next unless source_matches?(addr, expected_address)
|
|
78
|
+
|
|
79
|
+
response = NodeStatusResponse.read(data)
|
|
80
|
+
# Reject anything that isn't the response to our own query.
|
|
81
|
+
next unless response.transaction_id.to_i == request.transaction_id.to_i
|
|
82
|
+
next unless response.response.to_i == 1
|
|
83
|
+
next unless response.rr_type.to_i == NodeStatusRequest::QUESTION_TYPE_NBSTAT
|
|
84
|
+
|
|
85
|
+
return entries_from(response)
|
|
86
|
+
rescue IOError, EOFError, SystemCallError
|
|
87
|
+
next
|
|
88
|
+
end
|
|
89
|
+
nil
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Return the unique file-server name (suffix 0x20) from a host, or nil
|
|
93
|
+
# if the name table doesn't contain one. Convenience helper for the
|
|
94
|
+
# common case of "give me this host's file-server name."
|
|
95
|
+
#
|
|
96
|
+
# @param host [String] target IPv4 address
|
|
97
|
+
# @param kwargs [Hash] forwarded to {.query}
|
|
98
|
+
# @return [String, nil]
|
|
99
|
+
def self.file_server_name(host, **kwargs)
|
|
100
|
+
entries = query(host, **kwargs) or return nil
|
|
101
|
+
entry = entries.find { |e| e.suffix == 0x20 && e.unique? }
|
|
102
|
+
entry&.name
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def self.entries_from(response)
|
|
106
|
+
response.node_names.map do |n|
|
|
107
|
+
Entry.new(
|
|
108
|
+
n.netbios_name.to_s.rstrip,
|
|
109
|
+
n.suffix.to_i,
|
|
110
|
+
n.group?,
|
|
111
|
+
n.active?
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
private_class_method :entries_from
|
|
116
|
+
|
|
117
|
+
# Accept a reply only when it comes from the queried address. A UDP
|
|
118
|
+
# socket's `#recvfrom` always returns the sender as
|
|
119
|
+
# `[address_family, port, host, numeric_host]`, so the numeric address
|
|
120
|
+
# lives at `addr[3]`, which is compared against the normalized target.
|
|
121
|
+
# Any datagram whose source cannot be determined (not an Array, or a nil
|
|
122
|
+
# numeric address) is rejected — an unverifiable source is untrusted.
|
|
123
|
+
def self.source_matches?(addr, expected_address)
|
|
124
|
+
source = addr.is_a?(Array) ? addr[3] : nil
|
|
125
|
+
return false if source.nil?
|
|
126
|
+
|
|
127
|
+
source_address = begin
|
|
128
|
+
IPAddr.new(source).native
|
|
129
|
+
rescue IPAddr::InvalidAddressError
|
|
130
|
+
return false
|
|
131
|
+
end
|
|
132
|
+
source_address == expected_address
|
|
133
|
+
end
|
|
134
|
+
private_class_method :source_matches?
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
require 'ruby_smb/nbss/name_service_header_flags'
|
|
2
|
+
|
|
3
|
+
module RubySMB
|
|
4
|
+
module Nbss
|
|
5
|
+
# NetBIOS Name Service (NBNS) Node Status Request packet, as defined in
|
|
6
|
+
# [RFC 1002 4.2.17](https://tools.ietf.org/html/rfc1002#section-4.2.17).
|
|
7
|
+
# Sent over UDP to port 137 to retrieve a host's NetBIOS name table.
|
|
8
|
+
class NodeStatusRequest < BinData::Record
|
|
9
|
+
# NBSTAT question type, RFC 1002 4.2.1.3.
|
|
10
|
+
QUESTION_TYPE_NBSTAT = 0x0021
|
|
11
|
+
# Internet class.
|
|
12
|
+
QUESTION_CLASS_IN = 0x0001
|
|
13
|
+
# RFC 1002 4.2.17: a node status query always asks for the wildcard name,
|
|
14
|
+
# 16 bytes of 0x2A ('*') padded with 0x00.
|
|
15
|
+
WILDCARD_NAME = '*'.ljust(16, "\x00").freeze
|
|
16
|
+
|
|
17
|
+
endian :big
|
|
18
|
+
|
|
19
|
+
# 12-byte NBNS header (RFC 1002 4.2.1.1 and 4.2.1.2). The second word is
|
|
20
|
+
# R | OPCODE | NM_FLAGS | RCODE.
|
|
21
|
+
uint16 :transaction_id, label: 'Transaction ID'
|
|
22
|
+
bit1 :response, label: 'Response', initial_value: 0
|
|
23
|
+
bit4 :opcode, label: 'Opcode', initial_value: 0
|
|
24
|
+
name_service_header_flags :nm_flags, label: 'Flags'
|
|
25
|
+
bit4 :rcode, label: 'Result Code', initial_value: 0
|
|
26
|
+
uint16 :qdcount, label: 'QDCount', initial_value: 1
|
|
27
|
+
uint16 :ancount, label: 'ANCount', initial_value: 0
|
|
28
|
+
uint16 :nscount, label: 'NSCount', initial_value: 0
|
|
29
|
+
uint16 :arcount, label: 'ARCount', initial_value: 0
|
|
30
|
+
|
|
31
|
+
# Question section. For a node status query this is always the wildcard
|
|
32
|
+
# NetBIOS name, L1-encoded.
|
|
33
|
+
netbios_name :question_name, label: 'Question Name', initial_value: WILDCARD_NAME
|
|
34
|
+
uint16 :question_type, label: 'Question Type', initial_value: QUESTION_TYPE_NBSTAT
|
|
35
|
+
uint16 :question_class, label: 'Question Class', initial_value: QUESTION_CLASS_IN
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
require 'ruby_smb/nbss/name_service_header_flags'
|
|
2
|
+
|
|
3
|
+
module RubySMB
|
|
4
|
+
module Nbss
|
|
5
|
+
# The NAME_FLAGS field for each Node Status Response name entry.
|
|
6
|
+
class NodeStatusNameFlags < BinData::Record
|
|
7
|
+
endian :big
|
|
8
|
+
|
|
9
|
+
bit1 :group, label: 'Group Name', initial_value: 0
|
|
10
|
+
bit2 :owner_node_type, label: 'Owner Node Type', initial_value: 0
|
|
11
|
+
bit1 :deregister, label: 'Deregister', initial_value: 0
|
|
12
|
+
bit1 :conflict, label: 'Conflict', initial_value: 0
|
|
13
|
+
bit1 :active, label: 'Active', initial_value: 0
|
|
14
|
+
bit1 :permanent, label: 'Permanent', initial_value: 0
|
|
15
|
+
bit9 :reserved, label: 'Reserved', initial_value: 0
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Single entry in the NODE_NAME_ARRAY of a Node Status Response,
|
|
19
|
+
# as defined in [RFC 1002 4.2.18](https://tools.ietf.org/html/rfc1002#section-4.2.18).
|
|
20
|
+
# Fixed 18-byte layout (15-byte name, 1-byte suffix, 16-bit flags).
|
|
21
|
+
class NodeStatusName < BinData::Record
|
|
22
|
+
# NAME_FLAGS bits (RFC 1002 4.2.18).
|
|
23
|
+
GROUP_BIT = 0x8000 # 1 = group name, 0 = unique name
|
|
24
|
+
ACTIVE_BIT = 0x0400 # 1 = name registered
|
|
25
|
+
|
|
26
|
+
endian :big
|
|
27
|
+
|
|
28
|
+
string :netbios_name, label: 'NetBIOS Name', length: 15
|
|
29
|
+
uint8 :suffix, label: 'Suffix'
|
|
30
|
+
node_status_name_flags :name_flags, label: 'Name Flags'
|
|
31
|
+
|
|
32
|
+
def group?
|
|
33
|
+
(raw_name_flags & GROUP_BIT) != 0
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def unique?
|
|
37
|
+
!group?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def active?
|
|
41
|
+
(raw_name_flags & ACTIVE_BIT) != 0
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# The 16-bit NAME_FLAGS field as a raw integer, for masking against
|
|
47
|
+
# the GROUP_BIT / ACTIVE_BIT constants.
|
|
48
|
+
def raw_name_flags
|
|
49
|
+
name_flags.to_binary_s.unpack1('n')
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# NetBIOS Name Service (NBNS) Node Status Response packet, as defined in
|
|
54
|
+
# [RFC 1002 4.2.18](https://tools.ietf.org/html/rfc1002#section-4.2.18).
|
|
55
|
+
# Received over UDP from port 137 in reply to a {NodeStatusRequest}.
|
|
56
|
+
# Does not decode the trailing STATISTICS field; callers only need the
|
|
57
|
+
# name table.
|
|
58
|
+
class NodeStatusResponse < BinData::Record
|
|
59
|
+
endian :big
|
|
60
|
+
|
|
61
|
+
# 12-byte NBNS header. The second word is R | OPCODE | NM_FLAGS | RCODE.
|
|
62
|
+
uint16 :transaction_id, label: 'Transaction ID'
|
|
63
|
+
bit1 :response, label: 'Response', initial_value: 0
|
|
64
|
+
bit4 :opcode, label: 'Opcode', initial_value: 0
|
|
65
|
+
name_service_header_flags :nm_flags, label: 'Flags'
|
|
66
|
+
bit4 :rcode, label: 'Result Code', initial_value: 0
|
|
67
|
+
uint16 :qdcount, label: 'QDCount'
|
|
68
|
+
uint16 :ancount, label: 'ANCount'
|
|
69
|
+
uint16 :nscount, label: 'NSCount'
|
|
70
|
+
uint16 :arcount, label: 'ARCount'
|
|
71
|
+
|
|
72
|
+
# Answer section. Microsoft's implementation omits the question-echo,
|
|
73
|
+
# so the owner name appears directly after the header.
|
|
74
|
+
netbios_name :owner_name, label: 'Owner Name'
|
|
75
|
+
uint16 :rr_type, label: 'RR Type'
|
|
76
|
+
uint16 :rr_class, label: 'RR Class'
|
|
77
|
+
uint32 :ttl, label: 'TTL'
|
|
78
|
+
uint16 :rdlength, label: 'RDLENGTH'
|
|
79
|
+
|
|
80
|
+
# RDATA begins here. NODE_NAME_ARRAY is preceded by an 8-bit count.
|
|
81
|
+
uint8 :num_names, label: 'Number of Names'
|
|
82
|
+
array :node_names, type: :node_status_name, initial_length: :num_names
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
data/lib/ruby_smb/nbss.rb
CHANGED
|
@@ -9,9 +9,20 @@ module RubySMB
|
|
|
9
9
|
RETARGET_SESSION_RESPONSE = 0x84
|
|
10
10
|
SESSION_KEEP_ALIVE = 0x85
|
|
11
11
|
|
|
12
|
+
# NBSS negative session response error codes (RFC 1002 section 4.3.6)
|
|
13
|
+
NOT_LISTENING_ON_CALLED_NAME = 0x80
|
|
14
|
+
NOT_LISTENING_FOR_CALLING_NAME = 0x81
|
|
15
|
+
CALLED_NAME_NOT_PRESENT = 0x82
|
|
16
|
+
CALLED_NAME_INSUFFICIENT_RESOURCES = 0x83
|
|
17
|
+
UNSPECIFIED_ERROR = 0x8F
|
|
18
|
+
|
|
12
19
|
require 'ruby_smb/nbss/netbios_name'
|
|
13
20
|
require 'ruby_smb/nbss/session_header'
|
|
14
21
|
require 'ruby_smb/nbss/session_request'
|
|
15
22
|
require 'ruby_smb/nbss/negative_session_response'
|
|
23
|
+
require 'ruby_smb/nbss/name_service_header_flags'
|
|
24
|
+
require 'ruby_smb/nbss/node_status_request'
|
|
25
|
+
require 'ruby_smb/nbss/node_status_response'
|
|
26
|
+
require 'ruby_smb/nbss/node_status'
|
|
16
27
|
end
|
|
17
28
|
end
|
data/lib/ruby_smb/version.rb
CHANGED
|
@@ -742,6 +742,28 @@ RSpec.describe RubySMB::Client do
|
|
|
742
742
|
allow(RubySMB::Nbss::SessionHeader).to receive(:read).and_raise(IOError)
|
|
743
743
|
expect { client.session_request }.to raise_error(RubySMB::Error::InvalidPacket)
|
|
744
744
|
end
|
|
745
|
+
|
|
746
|
+
describe 'NetBiosSessionService error propagates the error_code' do
|
|
747
|
+
it 'attaches the numeric NBSS error code to the raised exception' do
|
|
748
|
+
negative = RubySMB::Nbss::NegativeSessionResponse.new
|
|
749
|
+
negative.session_header.session_packet_type = RubySMB::Nbss::NEGATIVE_SESSION_RESPONSE
|
|
750
|
+
negative.error_code = RubySMB::Nbss::NOT_LISTENING_ON_CALLED_NAME
|
|
751
|
+
allow(dispatcher).to receive(:recv_packet).and_return(negative.to_binary_s)
|
|
752
|
+
expect { client.session_request('OTHERNAME') }.to raise_error(RubySMB::Error::NetBiosSessionService) do |e|
|
|
753
|
+
expect(e.error_code.to_i).to eq(RubySMB::Nbss::NOT_LISTENING_ON_CALLED_NAME)
|
|
754
|
+
end
|
|
755
|
+
end
|
|
756
|
+
|
|
757
|
+
it 'reports CALLED_NAME_NOT_PRESENT so the caller can resolve and retry' do
|
|
758
|
+
negative = RubySMB::Nbss::NegativeSessionResponse.new
|
|
759
|
+
negative.session_header.session_packet_type = RubySMB::Nbss::NEGATIVE_SESSION_RESPONSE
|
|
760
|
+
negative.error_code = RubySMB::Nbss::CALLED_NAME_NOT_PRESENT
|
|
761
|
+
allow(dispatcher).to receive(:recv_packet).and_return(negative.to_binary_s)
|
|
762
|
+
expect { client.session_request }.to raise_error(RubySMB::Error::NetBiosSessionService) do |e|
|
|
763
|
+
expect(e.error_code.to_i).to eq(RubySMB::Nbss::CALLED_NAME_NOT_PRESENT)
|
|
764
|
+
end
|
|
765
|
+
end
|
|
766
|
+
end
|
|
745
767
|
end
|
|
746
768
|
|
|
747
769
|
describe '#session_request_packet' do
|
|
@@ -776,6 +798,7 @@ RSpec.describe RubySMB::Client do
|
|
|
776
798
|
expect(client.session_request_packet.called_name).to eq('*SMBSERVER ')
|
|
777
799
|
end
|
|
778
800
|
end
|
|
801
|
+
|
|
779
802
|
end
|
|
780
803
|
|
|
781
804
|
context 'Protocol Negotiation' do
|
|
@@ -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.
|
|
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-
|
|
16
|
+
date: 2026-10-01 00:00:00.000000000 Z
|
|
17
17
|
dependencies:
|
|
18
18
|
- !ruby/object:Gem::Dependency
|
|
19
19
|
name: redcarpet
|
|
@@ -476,8 +476,12 @@ files:
|
|
|
476
476
|
- lib/ruby_smb/gss/spnego_neg_token_targ.rb
|
|
477
477
|
- lib/ruby_smb/impersonation_levels.rb
|
|
478
478
|
- lib/ruby_smb/nbss.rb
|
|
479
|
+
- lib/ruby_smb/nbss/name_service_header_flags.rb
|
|
479
480
|
- lib/ruby_smb/nbss/negative_session_response.rb
|
|
480
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
|
|
481
485
|
- lib/ruby_smb/nbss/session_header.rb
|
|
482
486
|
- lib/ruby_smb/nbss/session_request.rb
|
|
483
487
|
- lib/ruby_smb/ntlm.rb
|
|
@@ -866,6 +870,9 @@ files:
|
|
|
866
870
|
- spec/lib/ruby_smb/gss/spnego_spec.rb
|
|
867
871
|
- spec/lib/ruby_smb/nbss/negative_session_response_spec.rb
|
|
868
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
|
|
869
876
|
- spec/lib/ruby_smb/nbss/session_header_spec.rb
|
|
870
877
|
- spec/lib/ruby_smb/nbss/session_request_spec.rb
|
|
871
878
|
- spec/lib/ruby_smb/ntlm/client/session_spec.rb
|
|
@@ -1226,6 +1233,9 @@ test_files:
|
|
|
1226
1233
|
- spec/lib/ruby_smb/gss/spnego_spec.rb
|
|
1227
1234
|
- spec/lib/ruby_smb/nbss/negative_session_response_spec.rb
|
|
1228
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
|
|
1229
1239
|
- spec/lib/ruby_smb/nbss/session_header_spec.rb
|
|
1230
1240
|
- spec/lib/ruby_smb/nbss/session_request_spec.rb
|
|
1231
1241
|
- spec/lib/ruby_smb/ntlm/client/session_spec.rb
|