ruby_smb 3.3.17 → 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/nt_create_andx_response.rb +19 -6
- 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,39 @@
|
|
|
1
|
+
module RubySMB
|
|
2
|
+
module SMB1
|
|
3
|
+
module Packet
|
|
4
|
+
module Trans2
|
|
5
|
+
module FindInformationLevel
|
|
6
|
+
# SMB_INFO_STANDARD find result entry, as defined in
|
|
7
|
+
# [MS-CIFS 2.2.8.1.1 SMB_INFO_STANDARD](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-cifs/b7cc0966-f87d-41a6-aa1a-48526a9cc729).
|
|
8
|
+
# Used by TRANS2_FIND_FIRST2/FIND_NEXT2 on legacy servers
|
|
9
|
+
# (e.g. Windows 95/98/ME) that don't support NT LANMAN info levels.
|
|
10
|
+
#
|
|
11
|
+
# Unlike NT info levels, these entries have no next_offset field;
|
|
12
|
+
# they are packed sequentially with a variable-length filename.
|
|
13
|
+
# The optional leading ResumeKey (4 bytes) is only present when the
|
|
14
|
+
# SMB_FIND_RETURN_RESUME_KEYS flag is set in the request; this
|
|
15
|
+
# implementation does not set that flag and so omits the field.
|
|
16
|
+
class FindInfoStandard < BinData::Record
|
|
17
|
+
CLASS_LEVEL = FindInformationLevel::SMB_INFO_STANDARD
|
|
18
|
+
|
|
19
|
+
endian :little
|
|
20
|
+
|
|
21
|
+
uint16 :creation_date, label: 'Creation Date (SMB_DATE)'
|
|
22
|
+
uint16 :creation_time, label: 'Creation Time (SMB_TIME)'
|
|
23
|
+
uint16 :last_access_date, label: 'Last Access Date (SMB_DATE)'
|
|
24
|
+
uint16 :last_access_time, label: 'Last Access Time (SMB_TIME)'
|
|
25
|
+
uint16 :last_write_date, label: 'Last Write Date (SMB_DATE)'
|
|
26
|
+
uint16 :last_write_time, label: 'Last Write Time (SMB_TIME)'
|
|
27
|
+
uint32 :data_size, label: 'File Data Size'
|
|
28
|
+
uint32 :allocation_size, label: 'Allocation Size'
|
|
29
|
+
uint16 :file_attributes, label: 'File Attributes'
|
|
30
|
+
uint8 :file_name_length, label: 'File Name Length',
|
|
31
|
+
initial_value: -> { file_name.to_s.bytesize }
|
|
32
|
+
string :file_name, label: 'File Name',
|
|
33
|
+
read_length: -> { file_name_length }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -38,6 +38,7 @@ module RubySMB
|
|
|
38
38
|
|
|
39
39
|
require 'ruby_smb/smb1/packet/trans2/find_information_level/find_file_both_directory_info'
|
|
40
40
|
require 'ruby_smb/smb1/packet/trans2/find_information_level/find_file_full_directory_info'
|
|
41
|
+
require 'ruby_smb/smb1/packet/trans2/find_information_level/find_info_standard'
|
|
41
42
|
end
|
|
42
43
|
end
|
|
43
44
|
end
|
|
@@ -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
|