ruby_smb 3.3.18 → 3.3.19
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/authentication.rb +53 -0
- data/lib/ruby_smb/client/negotiation.rb +10 -2
- data/lib/ruby_smb/client/tree_connect.rb +8 -1
- data/lib/ruby_smb/client.rb +16 -5
- data/lib/ruby_smb/rap/net_share_enum.rb +166 -0
- data/lib/ruby_smb/rap.rb +10 -0
- data/lib/ruby_smb/smb1/commands.rb +1 -0
- data/lib/ruby_smb/smb1/packet/negotiate_response.rb +11 -0
- data/lib/ruby_smb/smb1/packet/open_andx_request.rb +39 -0
- data/lib/ruby_smb/smb1/packet/open_andx_response.rb +40 -0
- data/lib/ruby_smb/smb1/packet/session_setup_legacy_request.rb +2 -2
- data/lib/ruby_smb/smb1/packet/session_setup_legacy_response.rb +11 -0
- data/lib/ruby_smb/smb1/packet/trans2/find_first2_response.rb +53 -13
- data/lib/ruby_smb/smb1/packet/trans2/find_information_level/find_info_standard.rb +39 -0
- data/lib/ruby_smb/smb1/packet/trans2/find_information_level.rb +1 -0
- data/lib/ruby_smb/smb1/packet/trans2/win9x_framing.rb +68 -0
- data/lib/ruby_smb/smb1/packet/trans2.rb +1 -0
- data/lib/ruby_smb/smb1/packet/tree_connect_request.rb +1 -1
- data/lib/ruby_smb/smb1/packet/tree_connect_response.rb +10 -1
- data/lib/ruby_smb/smb1/packet.rb +2 -0
- data/lib/ruby_smb/smb1/pipe.rb +2 -0
- data/lib/ruby_smb/smb1/tree.rb +113 -9
- data/lib/ruby_smb/version.rb +1 -1
- data/lib/ruby_smb.rb +1 -0
- data/spec/lib/ruby_smb/client_spec.rb +2 -1
- data/spec/lib/ruby_smb/rap/net_share_enum_spec.rb +185 -0
- data/spec/lib/ruby_smb/smb1/packet/trans2/win9x_framing_spec.rb +113 -0
- data/spec/lib/ruby_smb/smb1/tree_spec.rb +188 -2
- metadata +12 -2
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
module RubySMB
|
|
2
|
+
module SMB1
|
|
3
|
+
module Packet
|
|
4
|
+
module Trans2
|
|
5
|
+
# Shared workaround for pre-NT / LAN Manager-era servers (observed on
|
|
6
|
+
# Windows 9x / ME) that pack `trans2_parameters` directly after
|
|
7
|
+
# `byte_count` with no 4-byte-alignment pad, and `trans2_data` with
|
|
8
|
+
# whatever padding they feel like — always smaller than the NT-style
|
|
9
|
+
# alignment BinData unconditionally assumes via {DataBlock#pad1_length}
|
|
10
|
+
# and {DataBlock#pad2_length}. When that happens both sections land in
|
|
11
|
+
# the wrong place and `eos`, `sid`, `last_name_offset`, and every
|
|
12
|
+
# entry in the data buffer come back garbled.
|
|
13
|
+
#
|
|
14
|
+
# Fixing this in BinData itself (by having pad1/pad2 consult
|
|
15
|
+
# `parameter_block.parameter_offset` / `data_offset`) is the natural
|
|
16
|
+
# design, but cross-field lookups during field-read callbacks corrupt
|
|
17
|
+
# BinData's registered-class resolution cache, causing unrelated
|
|
18
|
+
# Trans2 responses to round-trip their `parameter_block` / `data_block`
|
|
19
|
+
# through the base classes instead of the concrete subclasses. So
|
|
20
|
+
# instead we surface the raw response bytes at the call site and let
|
|
21
|
+
# the response slice both sections from the offsets the server
|
|
22
|
+
# reported in its `parameter_block`.
|
|
23
|
+
#
|
|
24
|
+
# Mix into any {RubySMB::SMB1::Packet::Trans2} response whose caller
|
|
25
|
+
# holds on to the raw response bytes. The response itself must have
|
|
26
|
+
# the standard {Trans2::Response::ParameterBlock} shape
|
|
27
|
+
# (`parameter_offset` / `parameter_count` / `data_offset` /
|
|
28
|
+
# `data_count`) and a `data_block` with `trans2_parameters` and
|
|
29
|
+
# `trans2_data.buffer` fields — every concrete Trans2 response does.
|
|
30
|
+
#
|
|
31
|
+
# Same slicing pattern as {RubySMB::Rap::NetShareEnum#parse_net_share_enum_response}
|
|
32
|
+
# uses for the sibling Trans (not Trans2) response type.
|
|
33
|
+
module Win9xFraming
|
|
34
|
+
# Returns `[effective_trans2_parameters, effective_trans2_data_bytes]`
|
|
35
|
+
# when the server's layout differs from BinData's, or `[nil, nil]`
|
|
36
|
+
# when BinData already read the full buffer (standard NT-era servers).
|
|
37
|
+
#
|
|
38
|
+
# When a non-nil pair is returned, callers should prefer the override
|
|
39
|
+
# values over the BinData-parsed ones:
|
|
40
|
+
#
|
|
41
|
+
# params_ovr, data_ovr = response.win9x_trans2_overrides(raw)
|
|
42
|
+
# params = params_ovr || response.data_block.trans2_parameters
|
|
43
|
+
# data = data_ovr || response.data_block.trans2_data.buffer.to_binary_s
|
|
44
|
+
#
|
|
45
|
+
# @param raw_response [String] the raw bytes the response was read from.
|
|
46
|
+
# @return [Array(BinData::Record, String), Array(nil, nil)]
|
|
47
|
+
def win9x_trans2_overrides(raw_response)
|
|
48
|
+
declared_data = parameter_block.data_count.to_i
|
|
49
|
+
parsed_data = data_block.trans2_data.buffer.to_binary_s.bytesize
|
|
50
|
+
return [nil, nil] if declared_data.zero? || parsed_data == declared_data
|
|
51
|
+
|
|
52
|
+
param_offset = parameter_block.parameter_offset.to_i
|
|
53
|
+
param_count = parameter_block.parameter_count.to_i
|
|
54
|
+
data_offset = parameter_block.data_offset.to_i
|
|
55
|
+
return [nil, nil] if raw_response.bytesize < data_offset + declared_data
|
|
56
|
+
return [nil, nil] if raw_response.bytesize < param_offset + param_count
|
|
57
|
+
|
|
58
|
+
params_bytes = raw_response.byteslice(param_offset, param_count)
|
|
59
|
+
params_class = data_block.trans2_parameters.class
|
|
60
|
+
params = params_class.read(params_bytes)
|
|
61
|
+
data_bytes = raw_response.byteslice(data_offset, declared_data)
|
|
62
|
+
[params, data_bytes]
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -8,6 +8,7 @@ module RubySMB
|
|
|
8
8
|
require 'ruby_smb/smb1/packet/trans2/query_information_level'
|
|
9
9
|
require 'ruby_smb/smb1/packet/trans2/query_fs_information_level'
|
|
10
10
|
require 'ruby_smb/smb1/packet/trans2/data_block'
|
|
11
|
+
require 'ruby_smb/smb1/packet/trans2/win9x_framing'
|
|
11
12
|
require 'ruby_smb/smb1/packet/trans2/subcommands'
|
|
12
13
|
require 'ruby_smb/smb1/packet/trans2/request'
|
|
13
14
|
require 'ruby_smb/smb1/packet/trans2/request_secondary'
|
|
@@ -15,7 +15,7 @@ module RubySMB
|
|
|
15
15
|
|
|
16
16
|
# The {RubySMB::SMB1::DataBlock} specific to this packet type.
|
|
17
17
|
class DataBlock < RubySMB::SMB1::DataBlock
|
|
18
|
-
|
|
18
|
+
string :password, label: 'Password Field', initial_value: "\x00", length: -> { parent.parameter_block.password_length }
|
|
19
19
|
choice :path, selection: -> { parent.smb_header.flags2.unicode } do
|
|
20
20
|
stringz 0
|
|
21
21
|
stringz16 1
|
|
@@ -9,15 +9,24 @@ module RubySMB
|
|
|
9
9
|
# A SMB1 Parameter Block as defined by the {SessionSetupResponse}
|
|
10
10
|
class ParameterBlock < RubySMB::SMB1::ParameterBlock
|
|
11
11
|
and_x_block :andx_block
|
|
12
|
-
optional_support :optional_support
|
|
12
|
+
optional_support :optional_support, onlyif: -> { word_count >= 3 }
|
|
13
13
|
directory_access_mask :access_rights, label: 'Maximal Share Access Rights', onlyif: -> { word_count >= 5 }
|
|
14
14
|
directory_access_mask :guest_access_rights, label: 'Guest Share Access Rights', onlyif: -> { word_count == 7 }
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
# Represents the specific layout of the DataBlock for a {SessionSetupResponse} Packet.
|
|
18
|
+
# Windows 95/98/ME may return a minimal DataBlock without the native file system.
|
|
18
19
|
class DataBlock < RubySMB::SMB1::DataBlock
|
|
19
20
|
stringz :service, label: 'Service Type'
|
|
20
21
|
stringz :native_file_system, label: 'Native File System'
|
|
22
|
+
|
|
23
|
+
# Override to handle Win95 responses that may omit native_file_system.
|
|
24
|
+
def do_read(io)
|
|
25
|
+
byte_count.do_read(io)
|
|
26
|
+
return unless byte_count > 0
|
|
27
|
+
service.do_read(io)
|
|
28
|
+
native_file_system.do_read(io) if byte_count > service.num_bytes
|
|
29
|
+
end
|
|
21
30
|
end
|
|
22
31
|
|
|
23
32
|
smb_header :smb_header
|
data/lib/ruby_smb/smb1/packet.rb
CHANGED
|
@@ -22,6 +22,8 @@ module RubySMB
|
|
|
22
22
|
require 'ruby_smb/smb1/packet/trans'
|
|
23
23
|
require 'ruby_smb/smb1/packet/trans2'
|
|
24
24
|
require 'ruby_smb/smb1/packet/nt_trans'
|
|
25
|
+
require 'ruby_smb/smb1/packet/open_andx_request'
|
|
26
|
+
require 'ruby_smb/smb1/packet/open_andx_response'
|
|
25
27
|
require 'ruby_smb/smb1/packet/nt_create_andx_request'
|
|
26
28
|
require 'ruby_smb/smb1/packet/nt_create_andx_response'
|
|
27
29
|
require 'ruby_smb/smb1/packet/read_andx_request'
|
data/lib/ruby_smb/smb1/pipe.rb
CHANGED
|
@@ -36,6 +36,8 @@ module RubySMB
|
|
|
36
36
|
extend RubySMB::Dcerpc::Icpr
|
|
37
37
|
when 'efsrpc', '\\efsrpc'
|
|
38
38
|
extend RubySMB::Dcerpc::Efsrpc
|
|
39
|
+
when 'lanman', 'LANMAN', '\\PIPE\\LANMAN', '\\lanman'
|
|
40
|
+
extend RubySMB::Rap::NetShareEnum
|
|
39
41
|
end
|
|
40
42
|
super(tree: tree, response: response, name: name)
|
|
41
43
|
end
|
data/lib/ruby_smb/smb1/tree.rb
CHANGED
|
@@ -3,6 +3,11 @@ module RubySMB
|
|
|
3
3
|
# An SMB1 connected remote Tree, as returned by a
|
|
4
4
|
# [RubySMB::SMB1::Packet::TreeConnectRequest]
|
|
5
5
|
class Tree
|
|
6
|
+
# Exposes #net_share_enum directly on the tree for callers that need
|
|
7
|
+
# RAP against \PIPE\LANMAN without opening the pipe (Win9x servers do
|
|
8
|
+
# not permit OPEN_ANDX on it).
|
|
9
|
+
include RubySMB::Rap::NetShareEnum
|
|
10
|
+
|
|
6
11
|
# The client this Tree is connected through
|
|
7
12
|
# @!attribute [rw] client
|
|
8
13
|
# @return [RubySMB::Client]
|
|
@@ -102,9 +107,11 @@ module RubySMB
|
|
|
102
107
|
# @raise [RubySMB::Error::UnexpectedStatusCode] if the response NTStatus is not STATUS_SUCCESS
|
|
103
108
|
def list(directory: '\\', pattern: '*', unicode: true,
|
|
104
109
|
type: RubySMB::SMB1::Packet::Trans2::FindInformationLevel::FindFileFullDirectoryInfo)
|
|
110
|
+
info_standard = (type == RubySMB::SMB1::Packet::Trans2::FindInformationLevel::FindInfoStandard)
|
|
111
|
+
|
|
105
112
|
find_first_request = RubySMB::SMB1::Packet::Trans2::FindFirst2Request.new
|
|
106
113
|
find_first_request = set_header_fields(find_first_request)
|
|
107
|
-
find_first_request.smb_header.flags2.unicode = 1 if unicode
|
|
114
|
+
find_first_request.smb_header.flags2.unicode = 1 if unicode && !info_standard
|
|
108
115
|
|
|
109
116
|
search_path = directory.dup
|
|
110
117
|
search_path << '\\' unless search_path.end_with?('\\')
|
|
@@ -120,7 +127,7 @@ module RubySMB
|
|
|
120
127
|
t2_params.flags.resume_keys = 0
|
|
121
128
|
t2_params.information_level = type::CLASS_LEVEL
|
|
122
129
|
t2_params.filename = search_path
|
|
123
|
-
t2_params.search_count = 10
|
|
130
|
+
t2_params.search_count = info_standard ? 255 : 10
|
|
124
131
|
|
|
125
132
|
find_first_request = set_find_params(find_first_request)
|
|
126
133
|
|
|
@@ -137,13 +144,19 @@ module RubySMB
|
|
|
137
144
|
raise RubySMB::Error::UnexpectedStatusCode, response.status_code
|
|
138
145
|
end
|
|
139
146
|
|
|
140
|
-
|
|
147
|
+
t2p_override, t2d_override = response.win9x_trans2_overrides(raw_response)
|
|
148
|
+
results = if t2d_override
|
|
149
|
+
response.results(type, unicode: unicode, buffer: t2d_override)
|
|
150
|
+
else
|
|
151
|
+
response.results(type, unicode: unicode)
|
|
152
|
+
end
|
|
141
153
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
154
|
+
effective_params = t2p_override || response.data_block.trans2_parameters
|
|
155
|
+
eos = effective_params.eos
|
|
156
|
+
sid = effective_params.sid
|
|
157
|
+
last = results.last&.file_name
|
|
145
158
|
|
|
146
|
-
while eos.zero?
|
|
159
|
+
while eos.zero? && last
|
|
147
160
|
find_next_request = RubySMB::SMB1::Packet::Trans2::FindNext2Request.new
|
|
148
161
|
find_next_request = set_header_fields(find_next_request)
|
|
149
162
|
find_next_request.smb_header.flags2.unicode = 1 if unicode
|
|
@@ -171,8 +184,10 @@ module RubySMB
|
|
|
171
184
|
raise RubySMB::Error::UnexpectedStatusCode, response.status_code
|
|
172
185
|
end
|
|
173
186
|
|
|
174
|
-
|
|
187
|
+
batch = response.results(type, unicode: unicode)
|
|
188
|
+
break if batch.empty?
|
|
175
189
|
|
|
190
|
+
results += batch
|
|
176
191
|
eos = response.data_block.trans2_parameters.eos
|
|
177
192
|
last = results.last.file_name
|
|
178
193
|
end
|
|
@@ -195,6 +210,9 @@ module RubySMB
|
|
|
195
210
|
|
|
196
211
|
def _open(filename:, flags: nil, options: nil, disposition: RubySMB::Dispositions::FILE_OPEN,
|
|
197
212
|
impersonation: RubySMB::ImpersonationLevels::SEC_IMPERSONATE, read: true, write: false, delete: false)
|
|
213
|
+
unless client.server_supports_nt_smbs
|
|
214
|
+
return _open_andx(filename: filename, disposition: disposition, read: read, write: write)
|
|
215
|
+
end
|
|
198
216
|
nt_create_andx_request = RubySMB::SMB1::Packet::NtCreateAndxRequest.new
|
|
199
217
|
nt_create_andx_request = set_header_fields(nt_create_andx_request)
|
|
200
218
|
|
|
@@ -272,6 +290,91 @@ module RubySMB
|
|
|
272
290
|
end
|
|
273
291
|
end
|
|
274
292
|
|
|
293
|
+
# Open a file or pipe using SMB_COM_OPEN_ANDX (0x2D), the LAN Manager 1.0
|
|
294
|
+
# open command used by Windows 95/98/ME and other servers that don't
|
|
295
|
+
# advertise the NT SMBs capability. Accepts the same NT-style disposition
|
|
296
|
+
# constants as {#_open} and maps them to the OpenMode encoding defined in
|
|
297
|
+
# MS-CIFS 2.2.4.41.1.
|
|
298
|
+
#
|
|
299
|
+
# @param filename [String] path to the file on the share
|
|
300
|
+
# @param disposition [Integer] a RubySMB::Dispositions constant
|
|
301
|
+
# @param read [Boolean] request read access
|
|
302
|
+
# @param write [Boolean] request write access
|
|
303
|
+
# @return [RubySMB::SMB1::File, RubySMB::SMB1::Pipe] the opened resource
|
|
304
|
+
# @raise [RubySMB::Error::InvalidPacket] if the response is not valid
|
|
305
|
+
# @raise [RubySMB::Error::UnexpectedStatusCode] if the response NTStatus is not STATUS_SUCCESS
|
|
306
|
+
def _open_andx(filename:, disposition:, read: true, write: false)
|
|
307
|
+
request = RubySMB::SMB1::Packet::OpenAndxRequest.new
|
|
308
|
+
request = set_header_fields(request)
|
|
309
|
+
request.smb_header.flags2.unicode = 0
|
|
310
|
+
|
|
311
|
+
access = 0x0040 # sharing: deny-nothing
|
|
312
|
+
if read && write
|
|
313
|
+
access |= 0x02
|
|
314
|
+
elsif write
|
|
315
|
+
access |= 0x01
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
request.parameter_block.access_mode = access
|
|
319
|
+
# search_attributes / file_attributes are SMB_FILE_ATTRIBUTES BitField
|
|
320
|
+
# records, not plain uint16s — assign through #read to avoid BinData's
|
|
321
|
+
# each_pair-on-Integer NoMethodError when given a literal mask.
|
|
322
|
+
request.parameter_block.search_attributes.read([0x0016].pack('v'))
|
|
323
|
+
request.parameter_block.file_attributes.read([(write ? 0x0020 : 0x0000)].pack('v'))
|
|
324
|
+
request.parameter_block.open_mode = nt_disposition_to_open_mode(disposition)
|
|
325
|
+
|
|
326
|
+
fname = filename.dup
|
|
327
|
+
fname.prepend('\\') unless fname.start_with?('\\')
|
|
328
|
+
request.data_block.file_name = fname
|
|
329
|
+
|
|
330
|
+
raw_response = client.send_recv(request)
|
|
331
|
+
response = RubySMB::SMB1::Packet::OpenAndxResponse.read(raw_response)
|
|
332
|
+
unless response.valid?
|
|
333
|
+
raise RubySMB::Error::InvalidPacket.new(
|
|
334
|
+
expected_proto: RubySMB::SMB1::SMB_PROTOCOL_ID,
|
|
335
|
+
expected_cmd: RubySMB::SMB1::Packet::OpenAndxResponse::COMMAND,
|
|
336
|
+
packet: response
|
|
337
|
+
)
|
|
338
|
+
end
|
|
339
|
+
unless response.status_code == WindowsError::NTStatus::STATUS_SUCCESS
|
|
340
|
+
raise RubySMB::Error::UnexpectedStatusCode, response.status_code
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
build_open_andx_handle(filename, response)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Map an NT-style disposition (RubySMB::Dispositions) to an
|
|
347
|
+
# SMB_COM_OPEN_ANDX OpenMode word. FileExistsOpts is bits 0-1
|
|
348
|
+
# (0=fail, 1=open, 2=truncate); CreateFile is bit 4.
|
|
349
|
+
def nt_disposition_to_open_mode(disposition)
|
|
350
|
+
case disposition
|
|
351
|
+
when RubySMB::Dispositions::FILE_OPEN then 0x0001
|
|
352
|
+
when RubySMB::Dispositions::FILE_CREATE then 0x0010
|
|
353
|
+
when RubySMB::Dispositions::FILE_OPEN_IF then 0x0011
|
|
354
|
+
when RubySMB::Dispositions::FILE_OVERWRITE then 0x0002
|
|
355
|
+
when RubySMB::Dispositions::FILE_OVERWRITE_IF,
|
|
356
|
+
RubySMB::Dispositions::FILE_SUPERSEDE then 0x0012
|
|
357
|
+
else
|
|
358
|
+
raise RubySMB::Error::RubySMBError,
|
|
359
|
+
"Unsupported disposition for SMB_COM_OPEN_ANDX: #{disposition}"
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def build_open_andx_handle(filename, response)
|
|
364
|
+
unless response.parameter_block.resource_type == RubySMB::SMB1::ResourceType::DISK
|
|
365
|
+
raise RubySMB::Error::RubySMBError,
|
|
366
|
+
"SMB_COM_OPEN_ANDX resource type 0x#{response.parameter_block.resource_type.to_s(16)} not supported"
|
|
367
|
+
end
|
|
368
|
+
file = RubySMB::SMB1::File.allocate
|
|
369
|
+
file.tree = self
|
|
370
|
+
file.name = filename
|
|
371
|
+
file.fid = response.parameter_block.fid
|
|
372
|
+
file.size = response.parameter_block.file_data_size
|
|
373
|
+
file.size_on_disk = response.parameter_block.file_data_size
|
|
374
|
+
file.attributes = response.parameter_block.file_attributes
|
|
375
|
+
file
|
|
376
|
+
end
|
|
377
|
+
|
|
275
378
|
# Sets ParameterBlock options for FIND_FIRST2 and
|
|
276
379
|
# FIND_NEXT2 requests. In particular we need to do this
|
|
277
380
|
# to tell the server to ignore the Trans2DataBlock as we are
|
|
@@ -281,7 +384,8 @@ module RubySMB
|
|
|
281
384
|
request.parameter_block.data_offset = 0
|
|
282
385
|
request.parameter_block.total_parameter_count = request.parameter_block.parameter_count
|
|
283
386
|
request.parameter_block.max_parameter_count = request.parameter_block.parameter_count
|
|
284
|
-
|
|
387
|
+
max_data = [16_384, client.server_max_buffer_size].min
|
|
388
|
+
request.parameter_block.max_data_count = max_data
|
|
285
389
|
request
|
|
286
390
|
end
|
|
287
391
|
|
data/lib/ruby_smb/version.rb
CHANGED
data/lib/ruby_smb.rb
CHANGED
|
@@ -754,7 +754,7 @@ RSpec.describe RubySMB::Client do
|
|
|
754
754
|
it 'sets the expected fields of the SessionRequest packet' do
|
|
755
755
|
name = 'NBNAMESPEC'
|
|
756
756
|
called_name = 'NBNAMESPEC '
|
|
757
|
-
calling_name = "
|
|
757
|
+
calling_name = "WORKSTATION \x00"
|
|
758
758
|
|
|
759
759
|
session_packet = client.session_request_packet(name)
|
|
760
760
|
expect(session_packet).to be_a(RubySMB::Nbss::SessionRequest)
|
|
@@ -2438,6 +2438,7 @@ RSpec.describe RubySMB::Client do
|
|
|
2438
2438
|
end
|
|
2439
2439
|
end
|
|
2440
2440
|
end
|
|
2441
|
+
|
|
2441
2442
|
end
|
|
2442
2443
|
end
|
|
2443
2444
|
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
RSpec.describe RubySMB::Rap::NetShareEnum do
|
|
4
|
+
let(:client) { instance_double(RubySMB::Client) }
|
|
5
|
+
let(:tree) { instance_double(RubySMB::SMB1::Tree, id: 1, client: client) }
|
|
6
|
+
let(:pipe) do
|
|
7
|
+
captured_tree = tree
|
|
8
|
+
p = RubySMB::SMB1::Pipe.allocate
|
|
9
|
+
p.instance_variable_set(:@tree, captured_tree)
|
|
10
|
+
p.define_singleton_method(:tree) { captured_tree }
|
|
11
|
+
p.extend(described_class)
|
|
12
|
+
p
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def build_rap_response(status:, entries: [])
|
|
16
|
+
resp = RubySMB::SMB1::Packet::Trans::Response.new
|
|
17
|
+
params = RubySMB::Rap::NetShareEnum::Response.new(
|
|
18
|
+
status: status, converter: 0, entry_count: entries.length, available: entries.length
|
|
19
|
+
)
|
|
20
|
+
data = entries.map do |e|
|
|
21
|
+
si = RubySMB::Rap::NetShareEnum::ShareInfo1.new(
|
|
22
|
+
netname: e[:name], pad1: 0, share_type: e[:type], remark_offset: 0
|
|
23
|
+
)
|
|
24
|
+
si.to_binary_s
|
|
25
|
+
end.join
|
|
26
|
+
resp.data_block.trans_parameters = params.to_binary_s
|
|
27
|
+
resp.data_block.trans_data = data
|
|
28
|
+
resp.to_binary_s
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
describe '.new request layout' do
|
|
32
|
+
it 'encodes the RAP NetShareEnum opcode, descriptors, level and buffer size' do
|
|
33
|
+
bytes = RubySMB::Rap::NetShareEnum::Request.new.to_binary_s
|
|
34
|
+
expect(bytes[0, 2]).to eq([0].pack('v')) # opcode
|
|
35
|
+
expect(bytes[2, 6]).to eq("WrLeh\x00") # param descriptor
|
|
36
|
+
expect(bytes[8, 7]).to eq("B13BWz\x00") # data descriptor
|
|
37
|
+
expect(bytes[15, 2]).to eq([1].pack('v')) # info level 1
|
|
38
|
+
expect(bytes[17, 2]).to eq([0x1000].pack('v')) # receive buffer size
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
describe '#net_share_enum' do
|
|
43
|
+
it 'returns the parsed share list on RAP status 0' do
|
|
44
|
+
entries = [
|
|
45
|
+
{ name: 'IPC$', type: 0x0003 }, # STYPE_IPC
|
|
46
|
+
{ name: 'DATA', type: 0x0000 } # STYPE_DISKTREE
|
|
47
|
+
]
|
|
48
|
+
allow(client).to receive(:send_recv).and_return(build_rap_response(status: 0, entries: entries))
|
|
49
|
+
expect(pipe.net_share_enum).to eq([
|
|
50
|
+
{ name: 'IPC$', type: 'IPC' },
|
|
51
|
+
{ name: 'DATA', type: 'DISK' }
|
|
52
|
+
])
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it 'stringifies all MS-RAP 2.5.14 base share types' do
|
|
56
|
+
entries = [
|
|
57
|
+
{ name: 'DSK', type: 0x0000 },
|
|
58
|
+
{ name: 'PRN', type: 0x0001 },
|
|
59
|
+
{ name: 'DEV', type: 0x0002 },
|
|
60
|
+
{ name: 'IPC$', type: 0x0003 }
|
|
61
|
+
]
|
|
62
|
+
allow(client).to receive(:send_recv).and_return(build_rap_response(status: 0, entries: entries))
|
|
63
|
+
expect(pipe.net_share_enum.map { |s| s[:type] }).to eq(%w[DISK PRINTER DEVICE IPC])
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'appends SPECIAL / TEMPORARY modifiers to the base type string' do
|
|
67
|
+
entries = [
|
|
68
|
+
{ name: 'HIDDEN$', type: 0x0000 | RubySMB::Rap::NetShareEnum::STYPE_SPECIAL },
|
|
69
|
+
{ name: 'TMP', type: 0x0000 | RubySMB::Rap::NetShareEnum::STYPE_TEMPORARY },
|
|
70
|
+
{ name: 'IPCH$', type: 0x0003 | RubySMB::Rap::NetShareEnum::STYPE_SPECIAL |
|
|
71
|
+
RubySMB::Rap::NetShareEnum::STYPE_TEMPORARY }
|
|
72
|
+
]
|
|
73
|
+
allow(client).to receive(:send_recv).and_return(build_rap_response(status: 0, entries: entries))
|
|
74
|
+
expect(pipe.net_share_enum.map { |s| s[:type] }).to eq([
|
|
75
|
+
'DISK|SPECIAL',
|
|
76
|
+
'DISK|TEMPORARY',
|
|
77
|
+
'IPC|SPECIAL|TEMPORARY'
|
|
78
|
+
])
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it 'formats an unknown base type code as UNKNOWN(0xXXXX)' do
|
|
82
|
+
entries = [{ name: 'Q', type: 0x0007 }]
|
|
83
|
+
allow(client).to receive(:send_recv).and_return(build_rap_response(status: 0, entries: entries))
|
|
84
|
+
expect(pipe.net_share_enum.first[:type]).to eq('UNKNOWN(0x0007)')
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it 'sends a Trans request targeting \\PIPE\\LANMAN with the tree id' do
|
|
88
|
+
allow(client).to receive(:send_recv) do |request|
|
|
89
|
+
expect(request).to be_a(RubySMB::SMB1::Packet::Trans::Request)
|
|
90
|
+
expect(request.smb_header.tid).to eq(tree.id)
|
|
91
|
+
expect(request.smb_header.flags2.unicode).to eq(0)
|
|
92
|
+
expect(request.data_block.name.to_s).to eq("\\PIPE\\LANMAN".b)
|
|
93
|
+
expect(request.data_block.trans_parameters.to_s).to eq(RubySMB::Rap::NetShareEnum::Request.new.to_binary_s)
|
|
94
|
+
build_rap_response(status: 0)
|
|
95
|
+
end
|
|
96
|
+
pipe.net_share_enum
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it 'raises RubySMBError when the RAP status is non-zero' do
|
|
100
|
+
allow(client).to receive(:send_recv).and_return(build_rap_response(status: 5))
|
|
101
|
+
expect {
|
|
102
|
+
pipe.net_share_enum
|
|
103
|
+
}.to raise_error(RubySMB::Error::RubySMBError, /RAP NetShareEnum failed with status 0x5/)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
it 'raises InvalidPacket when the RAP params are truncated' do
|
|
107
|
+
resp = RubySMB::SMB1::Packet::Trans::Response.new
|
|
108
|
+
resp.data_block.trans_parameters = "\x00\x00\x00"
|
|
109
|
+
resp.data_block.trans_data = ''
|
|
110
|
+
allow(client).to receive(:send_recv).and_return(resp.to_binary_s)
|
|
111
|
+
expect {
|
|
112
|
+
pipe.net_share_enum
|
|
113
|
+
}.to raise_error(RubySMB::Error::InvalidPacket)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it 'raises UnexpectedStatusCode when the SMB status is not success' do
|
|
117
|
+
resp = RubySMB::SMB1::Packet::Trans::Response.new
|
|
118
|
+
resp.smb_header.nt_status = WindowsError::NTStatus::STATUS_ACCESS_DENIED.value
|
|
119
|
+
resp.data_block.trans_parameters = RubySMB::Rap::NetShareEnum::Response.new(
|
|
120
|
+
status: 0, converter: 0, entry_count: 0, available: 0
|
|
121
|
+
).to_binary_s
|
|
122
|
+
resp.data_block.trans_data = ''
|
|
123
|
+
allow(client).to receive(:send_recv).and_return(resp.to_binary_s)
|
|
124
|
+
expect {
|
|
125
|
+
pipe.net_share_enum
|
|
126
|
+
}.to raise_error(RubySMB::Error::UnexpectedStatusCode)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
describe 'Win9x-style response layout (no 4-byte pad before trans_parameters)' do
|
|
131
|
+
it 'slices trans_parameters using the server-reported parameter_offset' do
|
|
132
|
+
# Win9x packs parameters at offset 55 (immediately after byte_count)
|
|
133
|
+
# rather than padding to a 4-byte boundary. Build a response matching
|
|
134
|
+
# that layout by hand.
|
|
135
|
+
entry = RubySMB::Rap::NetShareEnum::ShareInfo1.new(
|
|
136
|
+
netname: 'ABCDEFGHIJKL', pad1: 0, share_type: 0, remark_offset: 0
|
|
137
|
+
).to_binary_s
|
|
138
|
+
params = RubySMB::Rap::NetShareEnum::Response.new(
|
|
139
|
+
status: 0, converter: 0, entry_count: 1, available: 1
|
|
140
|
+
).to_binary_s
|
|
141
|
+
|
|
142
|
+
# 32-byte SMB1 header (command=0x25 SMB_COM_TRANSACTION, status=0).
|
|
143
|
+
header = "\xffSMB\x25".b + ("\x00".b * 27)
|
|
144
|
+
parameter_offset = 32 + 1 + 20 + 2 # right after byte_count
|
|
145
|
+
data_offset = parameter_offset + params.bytesize
|
|
146
|
+
|
|
147
|
+
parameter_block = [
|
|
148
|
+
params.bytesize, # total_parameter_count
|
|
149
|
+
entry.bytesize, # total_data_count
|
|
150
|
+
0, # reserved
|
|
151
|
+
params.bytesize, # parameter_count
|
|
152
|
+
parameter_offset, # parameter_offset (55 - no pad)
|
|
153
|
+
0, # parameter_displacement
|
|
154
|
+
entry.bytesize, # data_count
|
|
155
|
+
data_offset, # data_offset
|
|
156
|
+
0 # data_displacement
|
|
157
|
+
].pack('v9') + "\x00\x00".b # setup_count + reserved2
|
|
158
|
+
|
|
159
|
+
byte_count = [params.bytesize + entry.bytesize].pack('v')
|
|
160
|
+
raw = header + "\x0a".b + parameter_block + byte_count + params + entry
|
|
161
|
+
# BinData's default Trans::Response#pad1_length forces a 4-byte align
|
|
162
|
+
# and reads trans_parameters starting 1 byte past what the server sent.
|
|
163
|
+
# Pad the tail so BinData's (mis-aligned) read doesn't hit EOF before
|
|
164
|
+
# our parser takes over with the server-reported offsets.
|
|
165
|
+
raw << "\x00".b * 8
|
|
166
|
+
|
|
167
|
+
allow(client).to receive(:send_recv).and_return(raw)
|
|
168
|
+
shares = pipe.net_share_enum
|
|
169
|
+
expect(shares.length).to eq(1)
|
|
170
|
+
expect(shares[0][:name]).to eq('ABCDEFGHIJKL')
|
|
171
|
+
expect(shares[0][:type]).to eq('DISK')
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
describe 'SMB1::Pipe integration' do
|
|
176
|
+
it 'extends the pipe with NetShareEnum when opened as \\PIPE\\LANMAN' do
|
|
177
|
+
# Use a minimal response to drive Pipe#initialize through File#initialize.
|
|
178
|
+
nt_resp = RubySMB::SMB1::Packet::NtCreateAndxResponse.new
|
|
179
|
+
nt_resp.parameter_block.fid = 0x1001
|
|
180
|
+
nt_resp.parameter_block.resource_type = RubySMB::SMB1::ResourceType::BYTE_MODE_PIPE
|
|
181
|
+
p = RubySMB::SMB1::Pipe.new(tree: tree, response: nt_resp, name: '\\PIPE\\LANMAN')
|
|
182
|
+
expect(p).to respond_to(:net_share_enum)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
RSpec.describe RubySMB::SMB1::Packet::Trans2::Win9xFraming do
|
|
4
|
+
# FindFirst2Response is the first production consumer of the mixin; its
|
|
5
|
+
# fixture data already covers every code path in #win9x_trans2_overrides
|
|
6
|
+
# (zero-length buffer, on-wire match, server-reported mismatch, truncated
|
|
7
|
+
# raw response). Using it here keeps the spec grounded in real field
|
|
8
|
+
# layouts without standing up an anonymous host class.
|
|
9
|
+
let(:info_std) do
|
|
10
|
+
RubySMB::SMB1::Packet::Trans2::FindInformationLevel::FindInfoStandard
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Reusable fixtures: NT-style (with pad1=3) and Win9x-style (pad1=0) raw
|
|
14
|
+
# FindFirst2Response frames carrying the same single-entry payload so the
|
|
15
|
+
# overrides helper sees the same server-declared offsets differ from what
|
|
16
|
+
# BinData positionally reads.
|
|
17
|
+
let(:single_entry_bytes) do
|
|
18
|
+
"\x98\x5c\x38\x70\x98\x5c\x00\x00\x98\x5c\x39\x70".b +
|
|
19
|
+
"\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x01".b + '.'
|
|
20
|
+
end
|
|
21
|
+
let(:data_count) { single_entry_bytes.bytesize }
|
|
22
|
+
|
|
23
|
+
def smb_header
|
|
24
|
+
"\xffSMB\x32".b + "\x00".b * 4 + "\x98".b + "\x03\x60".b + ("\x00".b * 20)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def build_response(parameter_offset:, data_offset:, word_count:, pad1: 0, pad2: 0)
|
|
28
|
+
# The concrete field layout of FindFirst2Response's parameter_block
|
|
29
|
+
# changes with word_count: 11 words include a 1-entry setup array;
|
|
30
|
+
# 10 words (Win9x style) omit it. trans2_parameters itself is a
|
|
31
|
+
# fixed 10-byte struct (sid, search_count, eos, ea_err_off, last_name_off).
|
|
32
|
+
trans2_params = [0x0300, 1, 1, 0, 0].pack('v*')
|
|
33
|
+
pb_values = [10, data_count, 0, 10, parameter_offset, 0,
|
|
34
|
+
data_count, data_offset, 0]
|
|
35
|
+
# word_count=11 → setup_count(1) + reserved2(0) + 1-word setup array
|
|
36
|
+
# word_count=10 → setup_count(0) + reserved2(0), no setup array
|
|
37
|
+
pb_tail = word_count == 11 ? "\x01\x00".b + [1].pack('v') : "\x00\x00".b
|
|
38
|
+
param_block = pb_values.pack('v*') + pb_tail
|
|
39
|
+
byte_count = pad1 + 10 + pad2 + data_count
|
|
40
|
+
smb_header + [word_count].pack('C') + param_block +
|
|
41
|
+
[byte_count].pack('v') + ("\x00".b * pad1) + trans2_params +
|
|
42
|
+
("\x00".b * pad2) + single_entry_bytes
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
describe '#win9x_trans2_overrides' do
|
|
46
|
+
context 'when BinData has already read the full buffer (NT-style server)' do
|
|
47
|
+
it 'returns [nil, nil]' do
|
|
48
|
+
# NT-era response: word_count=11 with 1-word setup, pad1=3, pad2=2
|
|
49
|
+
# → trans2_parameters at offset 60, trans2_data at 72. BinData's
|
|
50
|
+
# Trans2::DataBlock positional read lands exactly on the wire data.
|
|
51
|
+
raw = build_response(
|
|
52
|
+
parameter_offset: 60, data_offset: 72,
|
|
53
|
+
word_count: 11, pad1: 3, pad2: 2
|
|
54
|
+
)
|
|
55
|
+
response = RubySMB::SMB1::Packet::Trans2::FindFirst2Response.read(raw)
|
|
56
|
+
expect(response.win9x_trans2_overrides(raw)).to eq([nil, nil])
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
context 'when the server declared no trans2_data (data_count == 0)' do
|
|
61
|
+
it 'returns [nil, nil]' do
|
|
62
|
+
raw = build_response(
|
|
63
|
+
parameter_offset: 60, data_offset: 72,
|
|
64
|
+
word_count: 11, pad1: 3, pad2: 2
|
|
65
|
+
)
|
|
66
|
+
response = RubySMB::SMB1::Packet::Trans2::FindFirst2Response.read(raw)
|
|
67
|
+
response.parameter_block.data_count = 0
|
|
68
|
+
expect(response.win9x_trans2_overrides(raw)).to eq([nil, nil])
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
context 'when the server used Win9x-era framing (no pad1)' do
|
|
73
|
+
it 'returns trans2_parameters re-read at the server-reported offset' do
|
|
74
|
+
raw = build_response(
|
|
75
|
+
parameter_offset: 55, data_offset: 66,
|
|
76
|
+
word_count: 10, pad1: 0, pad2: 1
|
|
77
|
+
)
|
|
78
|
+
response = RubySMB::SMB1::Packet::Trans2::FindFirst2Response.read(raw)
|
|
79
|
+
params, = response.win9x_trans2_overrides(raw)
|
|
80
|
+
expect(params).to be_a(RubySMB::SMB1::Packet::Trans2::FindFirst2ResponseTrans2Parameters)
|
|
81
|
+
expect(params.sid).to eq 0x0300
|
|
82
|
+
expect(params.search_count).to eq 1
|
|
83
|
+
expect(params.eos).to eq 1
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it 'returns the trans2_data bytes sliced from the server-reported offset' do
|
|
87
|
+
raw = build_response(
|
|
88
|
+
parameter_offset: 55, data_offset: 66,
|
|
89
|
+
word_count: 10, pad1: 0, pad2: 1
|
|
90
|
+
)
|
|
91
|
+
response = RubySMB::SMB1::Packet::Trans2::FindFirst2Response.read(raw)
|
|
92
|
+
_, data_bytes = response.win9x_trans2_overrides(raw)
|
|
93
|
+
expect(data_bytes).to eq single_entry_bytes
|
|
94
|
+
# And #results can read entries from it through the buffer: kwarg.
|
|
95
|
+
entries = response.results(info_std, unicode: false, buffer: data_bytes)
|
|
96
|
+
expect(entries.length).to eq 1
|
|
97
|
+
expect(entries.first.file_name.to_s).to eq '.'
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
context 'when the raw response is truncated before the server-reported offsets' do
|
|
102
|
+
it 'returns [nil, nil] rather than raising' do
|
|
103
|
+
raw = build_response(
|
|
104
|
+
parameter_offset: 55, data_offset: 66,
|
|
105
|
+
word_count: 10, pad1: 0, pad2: 1
|
|
106
|
+
)
|
|
107
|
+
response = RubySMB::SMB1::Packet::Trans2::FindFirst2Response.read(raw)
|
|
108
|
+
truncated = raw.byteslice(0, raw.bytesize - 20)
|
|
109
|
+
expect(response.win9x_trans2_overrides(truncated)).to eq([nil, nil])
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|