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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e21d9dbff1980df2ec7b1917034f5989ad2ce369f380d2e9035baec86c7e8f30
|
|
4
|
+
data.tar.gz: 261147a5cd1f625e1b71efde4cc795e0ca01482e9e84f5d14e9657839e0f609e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bf16b49158f0117f7485c94a0a61094c93c05dfdc563fcc7d4852d243dc2c07be83eef8d31f4500b0c904417cf14b6219b4fe78a1188c2d5952e4256c7990952
|
|
7
|
+
data.tar.gz: 448ab8fdd1237825646c3328d6e8268ff38b039126cee2a6987748fdcdb6203121f3df3fc53fe906c9cd6df1f4ca64e47a717ae1b6687816694251780758a351
|
|
@@ -15,6 +15,10 @@ module RubySMB
|
|
|
15
15
|
if smb1
|
|
16
16
|
if username.empty? && password.empty?
|
|
17
17
|
smb1_anonymous_auth
|
|
18
|
+
elsif @smb1_negotiate_challenge
|
|
19
|
+
# Non-extended security negotiated (e.g. Windows 95/98). Use legacy
|
|
20
|
+
# LM/NTLM challenge-response rather than NTLMSSP.
|
|
21
|
+
smb1_legacy_authenticate
|
|
18
22
|
else
|
|
19
23
|
smb1_authenticate
|
|
20
24
|
end
|
|
@@ -200,6 +204,55 @@ module RubySMB
|
|
|
200
204
|
[type2_blob].pack('m')
|
|
201
205
|
end
|
|
202
206
|
|
|
207
|
+
# Handles SMB1 authentication against servers that negotiated non-extended
|
|
208
|
+
# (legacy) security — Windows 95/98/ME and old Samba builds. These hosts
|
|
209
|
+
# provide a raw 8-byte challenge in the Negotiate response and expect
|
|
210
|
+
# LM + NTLM hash responses in SessionSetupLegacyRequest.
|
|
211
|
+
def smb1_legacy_authenticate
|
|
212
|
+
challenge = @smb1_negotiate_challenge
|
|
213
|
+
lm_hash = Net::NTLM.lm_hash(@password)
|
|
214
|
+
ntlm_hash = Net::NTLM.ntlm_hash(@password)
|
|
215
|
+
lm_resp = Net::NTLM.lm_response(lm_hash: lm_hash, challenge: challenge)
|
|
216
|
+
ntlm_resp = Net::NTLM.ntlm_response(ntlm_hash: ntlm_hash, challenge: challenge)
|
|
217
|
+
|
|
218
|
+
packet = smb1_legacy_auth_request(lm_resp, ntlm_resp)
|
|
219
|
+
raw_response = send_recv(packet)
|
|
220
|
+
response = smb1_legacy_auth_response(raw_response)
|
|
221
|
+
response_code = response.status_code
|
|
222
|
+
|
|
223
|
+
if response_code == WindowsError::NTStatus::STATUS_SUCCESS
|
|
224
|
+
self.user_id = response.smb_header.uid
|
|
225
|
+
self.peer_native_os = response.data_block.native_os.to_s
|
|
226
|
+
self.peer_native_lm = response.data_block.native_lan_man.to_s
|
|
227
|
+
self.primary_domain = response.data_block.primary_domain.to_s
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
response_code
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def smb1_legacy_auth_request(lm_response, ntlm_response)
|
|
234
|
+
packet = RubySMB::SMB1::Packet::SessionSetupLegacyRequest.new
|
|
235
|
+
packet.parameter_block.max_buffer_size = self.max_buffer_size
|
|
236
|
+
packet.parameter_block.max_mpx_count = 50
|
|
237
|
+
packet.data_block.oem_password = lm_response
|
|
238
|
+
packet.data_block.unicode_password = ntlm_response
|
|
239
|
+
packet.data_block.account_name = @username.encode('ASCII', invalid: :replace, undef: :replace)
|
|
240
|
+
packet.data_block.primary_domain = @domain.encode('ASCII', invalid: :replace, undef: :replace)
|
|
241
|
+
packet
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def smb1_legacy_auth_response(raw_response)
|
|
245
|
+
packet = RubySMB::SMB1::Packet::SessionSetupLegacyResponse.read(raw_response)
|
|
246
|
+
unless packet.valid?
|
|
247
|
+
raise RubySMB::Error::InvalidPacket.new(
|
|
248
|
+
expected_proto: RubySMB::SMB1::SMB_PROTOCOL_ID,
|
|
249
|
+
expected_cmd: RubySMB::SMB1::Packet::SessionSetupLegacyResponse::COMMAND,
|
|
250
|
+
packet: packet
|
|
251
|
+
)
|
|
252
|
+
end
|
|
253
|
+
packet
|
|
254
|
+
end
|
|
255
|
+
|
|
203
256
|
#
|
|
204
257
|
# SMB 2 Methods
|
|
205
258
|
#
|
|
@@ -106,7 +106,8 @@ module RubySMB
|
|
|
106
106
|
# @return [String] The SMB version as a string ('SMB1', 'SMB2')
|
|
107
107
|
def parse_negotiate_response(packet)
|
|
108
108
|
case packet
|
|
109
|
-
when RubySMB::SMB1::Packet::
|
|
109
|
+
when RubySMB::SMB1::Packet::NegotiateResponse,
|
|
110
|
+
RubySMB::SMB1::Packet::NegotiateResponseExtended
|
|
110
111
|
self.smb1 = true
|
|
111
112
|
self.smb2 = false
|
|
112
113
|
self.smb3 = false
|
|
@@ -118,7 +119,14 @@ module RubySMB
|
|
|
118
119
|
self.server_max_buffer_size = packet.parameter_block.max_buffer_size - 260
|
|
119
120
|
self.negotiated_smb_version = 1
|
|
120
121
|
self.session_encrypt_data = false
|
|
121
|
-
self.
|
|
122
|
+
self.server_supports_nt_smbs = packet.parameter_block.capabilities.nt_smbs != 0
|
|
123
|
+
if packet.is_a?(RubySMB::SMB1::Packet::NegotiateResponseExtended)
|
|
124
|
+
self.negotiation_security_buffer = packet.data_block.security_blob
|
|
125
|
+
else
|
|
126
|
+
# Non-extended security (e.g. Windows 95/98/ME, old Samba). Server provides a raw
|
|
127
|
+
# 8-byte challenge instead of a SPNEGO blob; store it so auth can compute LM/NTLM responses.
|
|
128
|
+
@smb1_negotiate_challenge = packet.data_block.challenge.to_s
|
|
129
|
+
end
|
|
122
130
|
'SMB1'
|
|
123
131
|
when RubySMB::SMB2::Packet::NegotiateResponse
|
|
124
132
|
self.smb1 = false
|
|
@@ -11,10 +11,17 @@ module RubySMB
|
|
|
11
11
|
# {RubySMB::SMB1::Tree}
|
|
12
12
|
#
|
|
13
13
|
# @param share [String] the share path to connect to
|
|
14
|
+
# @param password [String, nil] share-level password for servers using
|
|
15
|
+
# share-level authentication (e.g. Windows 95/98/ME)
|
|
14
16
|
# @return [RubySMB::SMB1::Tree] the connected Tree
|
|
15
|
-
def smb1_tree_connect(share)
|
|
17
|
+
def smb1_tree_connect(share, password: nil)
|
|
16
18
|
request = RubySMB::SMB1::Packet::TreeConnectRequest.new
|
|
17
19
|
request.smb_header.tid = 65_535
|
|
20
|
+
if password
|
|
21
|
+
pass_bytes = password + "\x00".b
|
|
22
|
+
request.parameter_block.password_length = pass_bytes.length
|
|
23
|
+
request.data_block.password = pass_bytes
|
|
24
|
+
end
|
|
18
25
|
request.data_block.path = share
|
|
19
26
|
raw_response = send_recv(request)
|
|
20
27
|
response = RubySMB::SMB1::Packet::TreeConnectResponse.read(raw_response)
|
data/lib/ruby_smb/client.rb
CHANGED
|
@@ -309,6 +309,14 @@ module RubySMB
|
|
|
309
309
|
# @return [String] The raw security buffer bytes
|
|
310
310
|
attr_accessor :negotiation_security_buffer
|
|
311
311
|
|
|
312
|
+
# Whether the negotiated SMB1 server supports the "NT SMBs" capability
|
|
313
|
+
# (i.e. SMB_COM_NT_CREATE_ANDX). Set false for Windows 95/98/ME and other
|
|
314
|
+
# LAN Manager-era servers, which must use SMB_COM_OPEN_ANDX instead.
|
|
315
|
+
# Always true for SMB2/3.
|
|
316
|
+
# @!attribute [rw] supports_nt_smbs
|
|
317
|
+
# @return [Boolean]
|
|
318
|
+
attr_accessor :server_supports_nt_smbs
|
|
319
|
+
|
|
312
320
|
# @param dispatcher [RubySMB::Dispatcher::Socket] the packet dispatcher to use
|
|
313
321
|
# @param smb1 [Boolean] whether or not to enable SMB1 support
|
|
314
322
|
# @param smb2 [Boolean] whether or not to enable SMB2 support
|
|
@@ -338,10 +346,11 @@ module RubySMB
|
|
|
338
346
|
@max_buffer_size = MAX_BUFFER_SIZE
|
|
339
347
|
# These sizes will be modified during negotiation
|
|
340
348
|
@server_max_buffer_size = SERVER_MAX_BUFFER_SIZE
|
|
341
|
-
@server_max_read_size
|
|
342
|
-
@server_max_write_size
|
|
349
|
+
@server_max_read_size = RubySMB::SMB2::File::MAX_PACKET_SIZE
|
|
350
|
+
@server_max_write_size = RubySMB::SMB2::File::MAX_PACKET_SIZE
|
|
343
351
|
@server_max_transact_size = RubySMB::SMB2::File::MAX_PACKET_SIZE
|
|
344
352
|
@server_supports_multi_credit = false
|
|
353
|
+
@server_supports_nt_smbs = true
|
|
345
354
|
|
|
346
355
|
# SMB 3.x options
|
|
347
356
|
# this merely initializes the default value for session encryption, it may be changed as necessary when a
|
|
@@ -615,13 +624,15 @@ module RubySMB
|
|
|
615
624
|
# Connects to the supplied share
|
|
616
625
|
#
|
|
617
626
|
# @param share [String] the path to the share in `\\server\share_name` format
|
|
627
|
+
# @param password [String, nil] share-level password (SMB1 only, for
|
|
628
|
+
# servers using share-level auth such as Windows 95/98/ME)
|
|
618
629
|
# @return [RubySMB::SMB1::Tree] if talking over SMB1
|
|
619
630
|
# @return [RubySMB::SMB2::Tree] if talking over SMB2
|
|
620
|
-
def tree_connect(share)
|
|
631
|
+
def tree_connect(share, password: nil)
|
|
621
632
|
connected_tree = if smb2 || smb3
|
|
622
633
|
smb2_tree_connect(share)
|
|
623
634
|
else
|
|
624
|
-
smb1_tree_connect(share)
|
|
635
|
+
smb1_tree_connect(share, password: password)
|
|
625
636
|
end
|
|
626
637
|
@tree_connects << connected_tree
|
|
627
638
|
connected_tree
|
|
@@ -684,7 +695,7 @@ module RubySMB
|
|
|
684
695
|
# @return [RubySMB::Nbss::SessionRequest] the SessionRequest packet
|
|
685
696
|
def session_request_packet(name = '*SMBSERVER')
|
|
686
697
|
called_name = "#{name.upcase.ljust(15)}\x20"
|
|
687
|
-
calling_name = "#{
|
|
698
|
+
calling_name = "#{@local_workstation.upcase.ljust(15)}\x00"
|
|
688
699
|
|
|
689
700
|
session_request = RubySMB::Nbss::SessionRequest.new
|
|
690
701
|
session_request.session_header.session_packet_type = RubySMB::Nbss::SESSION_REQUEST
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
module RubySMB
|
|
2
|
+
module Rap
|
|
3
|
+
# NetShareEnum (RAP opcode 0), as defined in [MS-RAP 3.3.4.1](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rap/48dd86d2-4092-49a4-9024-308f0ed77520).
|
|
4
|
+
# Carried over `\PIPE\LANMAN` using SMB_COM_TRANSACTION. Request parameters
|
|
5
|
+
# describe the shape of the data the server returns; response parameters
|
|
6
|
+
# carry the RAP status, entry count, and buffer sizing hint, and the
|
|
7
|
+
# response data block is an array of `share_info_1` records.
|
|
8
|
+
module NetShareEnum
|
|
9
|
+
OPCODE = 0
|
|
10
|
+
|
|
11
|
+
# Parameter descriptor for the RAP call itself: (W)ord info level,
|
|
12
|
+
# (r)eturn buffer pointer, (L)ength hint, (e)ntry count, (h)andle.
|
|
13
|
+
PARAM_DESCRIPTOR = 'WrLeh'.freeze
|
|
14
|
+
|
|
15
|
+
# Data descriptor for `share_info_1`: (B)13 name, (B)yte pad, (W)ord type,
|
|
16
|
+
# (z) pointer to remark. See MS-RAP 3.2.4 for descriptor syntax.
|
|
17
|
+
DATA_DESCRIPTOR_LEVEL_1 = 'B13BWz'.freeze
|
|
18
|
+
|
|
19
|
+
# Default server receive-buffer size.
|
|
20
|
+
DEFAULT_RECEIVE_BUFFER_SIZE = 0x1000
|
|
21
|
+
|
|
22
|
+
# Share type codes carried in the low bits of `share_info_1.shi1_type`
|
|
23
|
+
# per MS-RAP 2.5.14. The RAP field is 16 bits wide, unlike the 32-bit
|
|
24
|
+
# SRVSVC variant in {RubySMB::Dcerpc::Srvsvc::SHARE_TYPES}.
|
|
25
|
+
SHARE_TYPES = {
|
|
26
|
+
0x0000 => 'DISK',
|
|
27
|
+
0x0001 => 'PRINTER',
|
|
28
|
+
0x0002 => 'DEVICE',
|
|
29
|
+
0x0003 => 'IPC'
|
|
30
|
+
}.freeze
|
|
31
|
+
STYPE_SPECIAL = 0x8000
|
|
32
|
+
STYPE_TEMPORARY = 0x4000
|
|
33
|
+
|
|
34
|
+
# Single share entry (`share_info_1`) as it appears on the wire.
|
|
35
|
+
# MS-RAP 2.5.21. Fixed 20-byte layout.
|
|
36
|
+
class ShareInfo1 < BinData::Record
|
|
37
|
+
endian :little
|
|
38
|
+
|
|
39
|
+
string :netname, length: 13, trim_padding: true
|
|
40
|
+
uint8 :pad1
|
|
41
|
+
uint16 :share_type
|
|
42
|
+
uint32 :remark_offset
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Parameters block of the RAP request (sent in SMB trans_parameters).
|
|
46
|
+
# Variable-length because of the null-terminated descriptor strings.
|
|
47
|
+
class Request < BinData::Record
|
|
48
|
+
endian :little
|
|
49
|
+
|
|
50
|
+
uint16 :opcode, asserted_value: OPCODE
|
|
51
|
+
stringz :param_descriptor, initial_value: PARAM_DESCRIPTOR
|
|
52
|
+
stringz :data_descriptor, initial_value: DATA_DESCRIPTOR_LEVEL_1
|
|
53
|
+
uint16 :info_level, initial_value: 1
|
|
54
|
+
uint16 :receive_buffer_size, initial_value: DEFAULT_RECEIVE_BUFFER_SIZE
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Parameters block of the RAP response.
|
|
58
|
+
# MS-RAP 3.3.5.1 NetShareEnum Response.
|
|
59
|
+
class Response < BinData::Record
|
|
60
|
+
endian :little
|
|
61
|
+
|
|
62
|
+
uint16 :status
|
|
63
|
+
uint16 :converter
|
|
64
|
+
uint16 :entry_count
|
|
65
|
+
uint16 :available
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Sends a RAP NetShareEnum over `\PIPE\LANMAN` using the tree's
|
|
69
|
+
# existing SMB1 connection. Does not rely on having an opened pipe FID
|
|
70
|
+
# because Win9x does not permit OPEN_ANDX on `\PIPE\LANMAN`; RAP trans
|
|
71
|
+
# is accepted directly against the IPC$ tree.
|
|
72
|
+
#
|
|
73
|
+
# @return [Array<Hash>] each entry has :name (String) and :type (Integer).
|
|
74
|
+
# @raise [RubySMB::Error::InvalidPacket] on a malformed SMB response.
|
|
75
|
+
# @raise [RubySMB::Error::UnexpectedStatusCode] on a non-success SMB status.
|
|
76
|
+
# @raise [RubySMB::Error::RubySMBError] on a non-zero RAP status.
|
|
77
|
+
def net_share_enum
|
|
78
|
+
request = build_net_share_enum_request
|
|
79
|
+
raw_response = rap_client.send_recv(request)
|
|
80
|
+
response = RubySMB::SMB1::Packet::Trans::Response.read(raw_response)
|
|
81
|
+
validate_trans_response!(response)
|
|
82
|
+
parse_net_share_enum_response(response, raw_response)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
# The SMB1 tree used to carry RAP traffic. `self` when this module is
|
|
88
|
+
# mixed into {RubySMB::SMB1::Tree}; the pipe's `tree` when mixed into
|
|
89
|
+
# {RubySMB::SMB1::Pipe}.
|
|
90
|
+
def rap_tree
|
|
91
|
+
is_a?(RubySMB::SMB1::Tree) ? self : tree
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def rap_client
|
|
95
|
+
rap_tree.client
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def build_net_share_enum_request
|
|
99
|
+
request = RubySMB::SMB1::Packet::Trans::Request.new
|
|
100
|
+
request.smb_header.tid = rap_tree.id
|
|
101
|
+
request.smb_header.flags2.unicode = 0
|
|
102
|
+
request.data_block.name = "\\PIPE\\LANMAN\x00".b
|
|
103
|
+
request.data_block.trans_parameters = Request.new.to_binary_s
|
|
104
|
+
request.parameter_block.max_parameter_count = 8
|
|
105
|
+
request.parameter_block.max_data_count = DEFAULT_RECEIVE_BUFFER_SIZE
|
|
106
|
+
request
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def validate_trans_response!(response)
|
|
110
|
+
unless response.valid?
|
|
111
|
+
raise RubySMB::Error::InvalidPacket.new(
|
|
112
|
+
expected_proto: RubySMB::SMB1::SMB_PROTOCOL_ID,
|
|
113
|
+
expected_cmd: RubySMB::SMB1::Packet::Trans::Response::COMMAND,
|
|
114
|
+
packet: response
|
|
115
|
+
)
|
|
116
|
+
end
|
|
117
|
+
unless response.status_code == WindowsError::NTStatus::STATUS_SUCCESS
|
|
118
|
+
raise RubySMB::Error::UnexpectedStatusCode, response.status_code
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def parse_net_share_enum_response(response, raw_response)
|
|
123
|
+
# Slice the parameter and data sections using the offsets the server
|
|
124
|
+
# reported, not the ones BinData computed. Win9x packs trans_parameters
|
|
125
|
+
# right after byte_count with no 4-byte-alignment padding, which
|
|
126
|
+
# confuses Trans::Response::DataBlock#pad1_length and shifts the
|
|
127
|
+
# trans_parameters window by 1 byte.
|
|
128
|
+
params_bytes = raw_response[response.parameter_block.parameter_offset,
|
|
129
|
+
response.parameter_block.parameter_count].to_s
|
|
130
|
+
data_bytes = raw_response[response.parameter_block.data_offset,
|
|
131
|
+
response.parameter_block.data_count].to_s
|
|
132
|
+
|
|
133
|
+
if params_bytes.bytesize < Response.new.num_bytes
|
|
134
|
+
raise RubySMB::Error::InvalidPacket,
|
|
135
|
+
"Truncated RAP NetShareEnum response parameters: #{params_bytes.unpack1('H*')}"
|
|
136
|
+
end
|
|
137
|
+
params = Response.read(params_bytes)
|
|
138
|
+
unless params.status.zero?
|
|
139
|
+
raise RubySMB::Error::RubySMBError,
|
|
140
|
+
"RAP NetShareEnum failed with status 0x#{params.status.to_i.to_s(16)}"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
params.entry_count.times.map do |i|
|
|
144
|
+
offset = i * ShareInfo1.new.num_bytes
|
|
145
|
+
break [] if offset + ShareInfo1.new.num_bytes > data_bytes.bytesize
|
|
146
|
+
entry = ShareInfo1.read(data_bytes[offset, ShareInfo1.new.num_bytes])
|
|
147
|
+
{
|
|
148
|
+
name: entry.netname.to_s.delete("\x00"),
|
|
149
|
+
type: format_share_type(entry.share_type.to_i)
|
|
150
|
+
}
|
|
151
|
+
end.compact
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Format a RAP `share_info_1.shi1_type` value as a pipe-joined string in
|
|
155
|
+
# the same style as {RubySMB::Dcerpc::Srvsvc#net_share_enum_all}, so
|
|
156
|
+
# callers can consume both APIs uniformly.
|
|
157
|
+
def format_share_type(share_type)
|
|
158
|
+
base_bits = share_type & ~(STYPE_SPECIAL | STYPE_TEMPORARY)
|
|
159
|
+
parts = [SHARE_TYPES[base_bits] || format('UNKNOWN(0x%04x)', base_bits)]
|
|
160
|
+
parts << 'SPECIAL' unless (share_type & STYPE_SPECIAL).zero?
|
|
161
|
+
parts << 'TEMPORARY' unless (share_type & STYPE_TEMPORARY).zero?
|
|
162
|
+
parts.join('|')
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
data/lib/ruby_smb/rap.rb
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module RubySMB
|
|
2
|
+
# Remote Administration Protocol (RAP), as defined in [MS-RAP]
|
|
3
|
+
# (https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rap/).
|
|
4
|
+
# RAP is the LAN Manager remote-administration API, carried over the
|
|
5
|
+
# `\PIPE\LANMAN` named pipe using SMB1 SMB_COM_TRANSACTION. It is the only
|
|
6
|
+
# share-enumeration path supported by pre-NT servers (e.g. Windows 95/98/ME).
|
|
7
|
+
module Rap
|
|
8
|
+
require 'ruby_smb/rap/net_share_enum'
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -22,10 +22,21 @@ module RubySMB
|
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
# An SMB_Data Block as defined by the {NegotiateResponse}
|
|
25
|
+
# Windows 95/98/ME may only return the challenge with no domain/server names.
|
|
25
26
|
class DataBlock < RubySMB::SMB1::DataBlock
|
|
26
27
|
string :challenge, label: 'Auth Challenge', length: 8
|
|
27
28
|
stringz16 :domain_name, label: 'Primary Domain'
|
|
28
29
|
stringz16 :server_name, label: 'Server Name'
|
|
30
|
+
|
|
31
|
+
# Override to handle Win95 responses that only contain the challenge
|
|
32
|
+
# (byte_count=8) without domain_name or server_name fields.
|
|
33
|
+
def do_read(io)
|
|
34
|
+
byte_count.do_read(io)
|
|
35
|
+
challenge.do_read(io)
|
|
36
|
+
return unless byte_count > 8
|
|
37
|
+
domain_name.do_read(io)
|
|
38
|
+
server_name.do_read(io)
|
|
39
|
+
end
|
|
29
40
|
end
|
|
30
41
|
|
|
31
42
|
smb_header :smb_header
|
|
@@ -2,8 +2,8 @@ module RubySMB
|
|
|
2
2
|
module SMB1
|
|
3
3
|
module Packet
|
|
4
4
|
# A SMB1 SMB_COM_NT_CREATE_ANDX Response Packet as defined in
|
|
5
|
-
# [2.2.4.64.2 Response](https://msdn.microsoft.com/en-us/library/ee441612.aspx) and
|
|
6
|
-
# [2.2.4.9.2 Server Response Extensions](https://msdn.microsoft.com/en-us/library/cc246334.aspx)
|
|
5
|
+
# [MS-CIFS: 2.2.4.64.2 Response](https://msdn.microsoft.com/en-us/library/ee441612.aspx) and
|
|
6
|
+
# [MS-SMB : 2.2.4.9.2 Server Response Extensions](https://msdn.microsoft.com/en-us/library/cc246334.aspx)
|
|
7
7
|
class NtCreateAndxResponse < RubySMB::GenericPacket
|
|
8
8
|
COMMAND = RubySMB::SMB1::Commands::SMB_COM_NT_CREATE_ANDX
|
|
9
9
|
|
|
@@ -35,15 +35,28 @@ module RubySMB
|
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
uint8 :directory, label: 'Directory'
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
# MS-CIFS: 2.2.4.64.2 (WC=34)
|
|
39
|
+
# MS-SMB: 2.2.4.9.2 (WC=42) - VolumeGUID only, per the spec (so we get the right WordCount, WC)
|
|
40
|
+
# MS-SMB: 2.2.4.9.2 (WC=50) - all four fields as per spec (but then the spec is then wrong for the WC)
|
|
41
|
+
# VolumeGUID, FileId, MaximalAccessRights & GuestMaximalAccessRights
|
|
42
|
+
# MS-SMB 2.2.4.9.2 (WC=42): VolumeGUID
|
|
43
|
+
string :volume_guid, label: 'Volume GUID', length: 16,
|
|
44
|
+
onlyif: -> { word_count >= 42 }
|
|
40
45
|
|
|
41
|
-
|
|
46
|
+
# MS-SMB 2.2.4.9.2 (WC=50): FileId
|
|
47
|
+
uint64 :file_id, label: 'File ID',
|
|
48
|
+
onlyif: -> { word_count >= 50 }
|
|
49
|
+
|
|
50
|
+
# MS-SMB 2.2.4.9.2 (WC=50): MaximalAccessRights
|
|
51
|
+
choice :maximal_access_rights, selection: -> { ext_file_attributes.directory },
|
|
52
|
+
onlyif: -> { word_count >= 50 } do
|
|
42
53
|
file_access_mask 0, label: 'Maximal Access Rights'
|
|
43
54
|
directory_access_mask 1, label: 'Maximal Access Rights'
|
|
44
55
|
end
|
|
45
56
|
|
|
46
|
-
|
|
57
|
+
# MS-SMB 2.2.4.9.2 (WC=50): GuestMaximalAccessRights
|
|
58
|
+
choice :guest_maximal_access_rights, selection: -> { ext_file_attributes.directory },
|
|
59
|
+
onlyif: -> { word_count >= 50 } do
|
|
47
60
|
file_access_mask 0, label: 'Guest Maximal Access Rights'
|
|
48
61
|
directory_access_mask 1, label: 'Guest Maximal Access Rights'
|
|
49
62
|
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module RubySMB
|
|
2
|
+
module SMB1
|
|
3
|
+
module Packet
|
|
4
|
+
# A SMB1 SMB_COM_OPEN_ANDX Request Packet as defined in
|
|
5
|
+
# [MS-CIFS 2.2.4.41.1 Request](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-cifs/3a760987-f60d-4012-930b-fe90328775cc)
|
|
6
|
+
#
|
|
7
|
+
# This is the LANMAN 1.0 file-open command, supported by all SMB1 servers
|
|
8
|
+
# including Windows 95/98/ME which lack NT_CREATE_ANDX (0xA2).
|
|
9
|
+
class OpenAndxRequest < RubySMB::GenericPacket
|
|
10
|
+
COMMAND = RubySMB::SMB1::Commands::SMB_COM_OPEN_ANDX
|
|
11
|
+
|
|
12
|
+
# A SMB1 Parameter Block as defined by the {OpenAndxRequest}
|
|
13
|
+
class ParameterBlock < RubySMB::SMB1::ParameterBlock
|
|
14
|
+
endian :little
|
|
15
|
+
|
|
16
|
+
and_x_block :andx_block
|
|
17
|
+
uint16 :flags, label: 'Flags'
|
|
18
|
+
uint16 :access_mode, label: 'Access Mode'
|
|
19
|
+
smb_file_attributes :search_attributes, label: 'Search Attributes'
|
|
20
|
+
smb_file_attributes :file_attributes, label: 'File Attributes'
|
|
21
|
+
uint32 :creation_time, label: 'Creation Time'
|
|
22
|
+
uint16 :open_mode, label: 'Open Mode'
|
|
23
|
+
uint32 :allocation_size, label: 'Allocation Size'
|
|
24
|
+
uint32 :timeout, label: 'Timeout'
|
|
25
|
+
uint32 :reserved, label: 'Reserved'
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Represents the specific layout of the DataBlock for an {OpenAndxRequest} Packet.
|
|
29
|
+
class DataBlock < RubySMB::SMB1::DataBlock
|
|
30
|
+
stringz :file_name, label: 'File Name'
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
smb_header :smb_header
|
|
34
|
+
parameter_block :parameter_block
|
|
35
|
+
data_block :data_block
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module RubySMB
|
|
2
|
+
module SMB1
|
|
3
|
+
module Packet
|
|
4
|
+
# A SMB1 SMB_COM_OPEN_ANDX Response Packet as defined in
|
|
5
|
+
# [MS-CIFS 2.2.4.41.2 Response](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-cifs/dbce00e7-68a1-41c6-982d-9483c902ad9b)
|
|
6
|
+
class OpenAndxResponse < RubySMB::GenericPacket
|
|
7
|
+
COMMAND = RubySMB::SMB1::Commands::SMB_COM_OPEN_ANDX
|
|
8
|
+
|
|
9
|
+
# A SMB1 Parameter Block as defined by the {OpenAndxResponse}.
|
|
10
|
+
# Field names and layout follow MS-CIFS 2.2.4.41.2.
|
|
11
|
+
class ParameterBlock < RubySMB::SMB1::ParameterBlock
|
|
12
|
+
endian :little
|
|
13
|
+
|
|
14
|
+
and_x_block :andx_block
|
|
15
|
+
uint16 :fid, label: 'FID'
|
|
16
|
+
smb_file_attributes :file_attributes, label: 'File Attributes'
|
|
17
|
+
utime :last_write_time, label: 'Last Write Time'
|
|
18
|
+
uint32 :file_data_size, label: 'File Data Size'
|
|
19
|
+
uint16 :access_rights, label: 'Access Rights'
|
|
20
|
+
uint16 :resource_type, label: 'Resource Type'
|
|
21
|
+
smb_nmpipe_status :nmpipe_status, label: 'Named Pipe Status'
|
|
22
|
+
uint16 :open_results, label: 'Open Results'
|
|
23
|
+
array :reserved, type: :uint16, initial_length: 3
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class DataBlock < RubySMB::SMB1::DataBlock
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
smb_header :smb_header
|
|
30
|
+
parameter_block :parameter_block
|
|
31
|
+
data_block :data_block
|
|
32
|
+
|
|
33
|
+
def initialize_instance
|
|
34
|
+
super
|
|
35
|
+
smb_header.flags.reply = 1
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -27,8 +27,8 @@ module RubySMB
|
|
|
27
27
|
string :oem_password, label: 'OEM Password'
|
|
28
28
|
string :unicode_password, label: 'Unicode password'
|
|
29
29
|
string :padding, label: 'Padding'
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
stringz :account_name, label: 'Account Name(username)'
|
|
31
|
+
stringz :primary_domain, label: 'Primary Domain'
|
|
32
32
|
stringz :native_os, label: 'Native OS', initial_value: 'Windows 7 Ultimate N 7601 Service Pack 1'
|
|
33
33
|
stringz :native_lan_man, label: 'Native LAN Manager', initial_value: 'Windows 7 Ultimate N 6.1'
|
|
34
34
|
end
|
|
@@ -13,11 +13,22 @@ module RubySMB
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
# Represents the specific layout of the DataBlock for a {SessionSetupResponse} Packet.
|
|
16
|
+
# Windows 95/98/ME may return byte_count=0 with no string fields.
|
|
16
17
|
class DataBlock < RubySMB::SMB1::DataBlock
|
|
17
18
|
string :pad, label: 'Padding', length: 0
|
|
18
19
|
stringz :native_os, label: 'Native OS'
|
|
19
20
|
stringz :native_lan_man, label: 'Native LAN Manager'
|
|
20
21
|
stringz :primary_domain, label: 'Primary Domain'
|
|
22
|
+
|
|
23
|
+
# Override to handle Win95 responses with byte_count=0.
|
|
24
|
+
def do_read(io)
|
|
25
|
+
byte_count.do_read(io)
|
|
26
|
+
return unless byte_count > 0
|
|
27
|
+
pad.do_read(io)
|
|
28
|
+
native_os.do_read(io)
|
|
29
|
+
native_lan_man.do_read(io)
|
|
30
|
+
primary_domain.do_read(io)
|
|
31
|
+
end
|
|
21
32
|
end
|
|
22
33
|
|
|
23
34
|
smb_header :smb_header
|
|
@@ -41,6 +41,8 @@ module RubySMB
|
|
|
41
41
|
# This class represents an SMB1 Trans2 FIND_FIRST2 Response Packet as defined in
|
|
42
42
|
# [2.2.6.2.2 Response](https://msdn.microsoft.com/en-us/library/ee441704.aspx)
|
|
43
43
|
class FindFirst2Response < RubySMB::GenericPacket
|
|
44
|
+
include RubySMB::SMB1::Packet::Trans2::Win9xFraming
|
|
45
|
+
|
|
44
46
|
COMMAND = RubySMB::SMB1::Commands::SMB_COM_TRANSACTION2
|
|
45
47
|
|
|
46
48
|
class ParameterBlock < RubySMB::SMB1::Packet::Trans2::Response::ParameterBlock
|
|
@@ -60,30 +62,68 @@ module RubySMB
|
|
|
60
62
|
# structs for the given FileInformationClass. Pulled out of
|
|
61
63
|
# the string buffer.
|
|
62
64
|
#
|
|
65
|
+
# Info levels that carry a leading NextEntryOffset (e.g.
|
|
66
|
+
# FindFileFullDirectoryInfo) are framed by that field. Info levels
|
|
67
|
+
# without one (e.g. SMB_INFO_STANDARD, used by Win95/98/ME) are
|
|
68
|
+
# packed sequentially; each entry's length is derived from the
|
|
69
|
+
# record itself, and servers may insert an optional single NULL
|
|
70
|
+
# pad byte between entries (see MS-CIFS Appendix A, note <153>).
|
|
71
|
+
#
|
|
63
72
|
# @param klass [Class] the FileInformationClass class to read the data as
|
|
73
|
+
# @param buffer [String, nil] raw trans2_data bytes to parse instead of
|
|
74
|
+
# the BinData-parsed buffer. Used by callers that detect a padding
|
|
75
|
+
# mismatch between BinData's expected layout and what a Win9x-era
|
|
76
|
+
# server actually sent (no 4-byte alignment pad before the data),
|
|
77
|
+
# and want to re-feed the bytes from the server-reported data_offset.
|
|
64
78
|
# @return [array<BinData::Record>] An array of structs holding the requested information
|
|
65
79
|
# @raise [RubySMB::Error::InvalidPacket] if the string buffer is not a valid File Information packet
|
|
66
|
-
def results(klass, unicode:)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
80
|
+
def results(klass, unicode:, buffer: nil)
|
|
81
|
+
blob = (buffer || data_block.trans2_data.buffer.to_binary_s).dup
|
|
82
|
+
if klass.new.respond_to?(:next_offset)
|
|
83
|
+
read_next_offset_entries(klass, blob, unicode: unicode)
|
|
84
|
+
else
|
|
85
|
+
read_sequential_entries(klass, blob, unicode: unicode)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
71
88
|
|
|
72
|
-
|
|
73
|
-
blob.slice!(0, blob.length)
|
|
74
|
-
else
|
|
75
|
-
blob.slice!(0, length)
|
|
76
|
-
end
|
|
89
|
+
private
|
|
77
90
|
|
|
91
|
+
def read_next_offset_entries(klass, blob, unicode:)
|
|
92
|
+
entries = []
|
|
93
|
+
until blob.empty?
|
|
94
|
+
length = blob[0, 4].unpack1('V')
|
|
95
|
+
data = length.zero? ? blob.slice!(0, blob.length) : blob.slice!(0, length)
|
|
96
|
+
file_info = klass.new
|
|
97
|
+
file_info.unicode = unicode if file_info.respond_to?(:unicode=)
|
|
98
|
+
begin
|
|
99
|
+
entries << file_info.read(data)
|
|
100
|
+
rescue IOError
|
|
101
|
+
raise RubySMB::Error::InvalidPacket, "Invalid #{klass} File Information packet in the string buffer"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
entries
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def read_sequential_entries(klass, blob, unicode:)
|
|
108
|
+
entries = []
|
|
109
|
+
until blob.empty?
|
|
78
110
|
file_info = klass.new
|
|
79
|
-
file_info.unicode = unicode
|
|
111
|
+
file_info.unicode = unicode if file_info.respond_to?(:unicode=)
|
|
80
112
|
begin
|
|
81
|
-
|
|
113
|
+
file_info.read(blob)
|
|
82
114
|
rescue IOError
|
|
83
115
|
raise RubySMB::Error::InvalidPacket, "Invalid #{klass} File Information packet in the string buffer"
|
|
84
116
|
end
|
|
117
|
+
consumed = file_info.num_bytes
|
|
118
|
+
break if consumed.zero?
|
|
119
|
+
blob.slice!(0, consumed)
|
|
120
|
+
# An entry with an empty file_name is a buffer-padding artifact, not a real entry; stop here.
|
|
121
|
+
break if file_info.respond_to?(:file_name_length) && file_info.file_name_length.zero?
|
|
122
|
+
entries << file_info
|
|
123
|
+
# Skip optional single NULL pad byte inserted by some servers between entries.
|
|
124
|
+
blob.slice!(0, 1) if blob.bytesize > 0 && blob.getbyte(0) == 0
|
|
85
125
|
end
|
|
86
|
-
|
|
126
|
+
entries
|
|
87
127
|
end
|
|
88
128
|
end
|
|
89
129
|
end
|