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.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/lib/ruby_smb/client/authentication.rb +53 -0
  3. data/lib/ruby_smb/client/negotiation.rb +10 -2
  4. data/lib/ruby_smb/client/tree_connect.rb +8 -1
  5. data/lib/ruby_smb/client.rb +16 -5
  6. data/lib/ruby_smb/rap/net_share_enum.rb +166 -0
  7. data/lib/ruby_smb/rap.rb +10 -0
  8. data/lib/ruby_smb/smb1/commands.rb +1 -0
  9. data/lib/ruby_smb/smb1/packet/negotiate_response.rb +11 -0
  10. data/lib/ruby_smb/smb1/packet/open_andx_request.rb +39 -0
  11. data/lib/ruby_smb/smb1/packet/open_andx_response.rb +40 -0
  12. data/lib/ruby_smb/smb1/packet/session_setup_legacy_request.rb +2 -2
  13. data/lib/ruby_smb/smb1/packet/session_setup_legacy_response.rb +11 -0
  14. data/lib/ruby_smb/smb1/packet/trans2/find_first2_response.rb +53 -13
  15. data/lib/ruby_smb/smb1/packet/trans2/find_information_level/find_info_standard.rb +39 -0
  16. data/lib/ruby_smb/smb1/packet/trans2/find_information_level.rb +1 -0
  17. data/lib/ruby_smb/smb1/packet/trans2/win9x_framing.rb +68 -0
  18. data/lib/ruby_smb/smb1/packet/trans2.rb +1 -0
  19. data/lib/ruby_smb/smb1/packet/tree_connect_request.rb +1 -1
  20. data/lib/ruby_smb/smb1/packet/tree_connect_response.rb +10 -1
  21. data/lib/ruby_smb/smb1/packet.rb +2 -0
  22. data/lib/ruby_smb/smb1/pipe.rb +2 -0
  23. data/lib/ruby_smb/smb1/tree.rb +113 -9
  24. data/lib/ruby_smb/version.rb +1 -1
  25. data/lib/ruby_smb.rb +1 -0
  26. data/spec/lib/ruby_smb/client_spec.rb +2 -1
  27. data/spec/lib/ruby_smb/rap/net_share_enum_spec.rb +185 -0
  28. data/spec/lib/ruby_smb/smb1/packet/trans2/win9x_framing_spec.rb +113 -0
  29. data/spec/lib/ruby_smb/smb1/tree_spec.rb +188 -2
  30. metadata +12 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: df07f2b672976e5cff3e2a4a724038b9557ce8db1d01faa089348f0dc472592a
4
- data.tar.gz: 936642cf5e5d3dfcef265af694eb37d58f83cbdfd9f3e337fade9dd5f9f4d39f
3
+ metadata.gz: e21d9dbff1980df2ec7b1917034f5989ad2ce369f380d2e9035baec86c7e8f30
4
+ data.tar.gz: 261147a5cd1f625e1b71efde4cc795e0ca01482e9e84f5d14e9657839e0f609e
5
5
  SHA512:
6
- metadata.gz: 33201873ef84ff27d62f8381e52d25da7ce2e4831a262987682050b2deedebcdfe382f4177c5d375268a43fcddc78d195a06368f0d6919a21be34a32544254e3
7
- data.tar.gz: 978a64324e79c8f183599ad05d1291f4b172deb9ddc1d1e669acbc68e8c179d17cfd367138a4c5bb49aa7abd4063758a8e7b36023ecd8b11a427b086c2ecd154
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::NegotiateResponseExtended
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.negotiation_security_buffer = packet.data_block.security_blob
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)
@@ -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 = RubySMB::SMB2::File::MAX_PACKET_SIZE
342
- @server_max_write_size = RubySMB::SMB2::File::MAX_PACKET_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 = "#{''.ljust(15)}\x00"
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
@@ -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
@@ -4,6 +4,7 @@ module RubySMB
4
4
  SMB_COM_CLOSE = 0x04
5
5
  SMB_COM_TRANSACTION = 0x25
6
6
  SMB_COM_ECHO = 0x2B
7
+ SMB_COM_OPEN_ANDX = 0x2D
7
8
  SMB_COM_READ_ANDX = 0x2E
8
9
  SMB_COM_WRITE_ANDX = 0x2F
9
10
  SMB_COM_TRANSACTION2 = 0x32
@@ -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
@@ -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
- string :account_name, label: 'Account Name(username)', length: 2
31
- string :primary_domain, label: 'Primary Domain', length: 2
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
- information_classes = []
68
- blob = data_block.trans2_data.buffer.to_binary_s.dup
69
- until blob.empty?
70
- length = blob[0, 4].unpack('V').first
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
- data = if length.zero?
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
- information_classes << file_info.read(data)
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
- information_classes
126
+ entries
87
127
  end
88
128
  end
89
129
  end
@@ -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