ruby_smb 3.2.0 → 3.3.0

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 (71) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/.github/workflows/verify.yml +8 -9
  4. data/README.md +1 -0
  5. data/cortex.yaml +15 -0
  6. data/examples/dump_secrets_from_sid.rb +2 -1
  7. data/lib/ruby_smb/client.rb +6 -4
  8. data/lib/ruby_smb/dcerpc/alter_context.rb +30 -0
  9. data/lib/ruby_smb/dcerpc/alter_context_resp.rb +42 -0
  10. data/lib/ruby_smb/dcerpc/bind.rb +3 -35
  11. data/lib/ruby_smb/dcerpc/bind_ack.rb +0 -31
  12. data/lib/ruby_smb/dcerpc/client.rb +10 -4
  13. data/lib/ruby_smb/dcerpc/drsr.rb +13 -13
  14. data/lib/ruby_smb/dcerpc/encrypting_file_system/efs_rpc_decrypt_file_srv_request.rb +22 -0
  15. data/lib/ruby_smb/dcerpc/encrypting_file_system/efs_rpc_decrypt_file_srv_response.rb +21 -0
  16. data/lib/ruby_smb/dcerpc/encrypting_file_system/efs_rpc_query_recover_agents_request.rb +20 -0
  17. data/lib/ruby_smb/dcerpc/encrypting_file_system/efs_rpc_query_recover_agents_response.rb +21 -0
  18. data/lib/ruby_smb/dcerpc/encrypting_file_system/efs_rpc_query_users_on_file_request.rb +20 -0
  19. data/lib/ruby_smb/dcerpc/encrypting_file_system/efs_rpc_query_users_on_file_response.rb +21 -0
  20. data/lib/ruby_smb/dcerpc/encrypting_file_system.rb +52 -0
  21. data/lib/ruby_smb/dcerpc/icpr.rb +3 -2
  22. data/lib/ruby_smb/dcerpc/ndr.rb +148 -3
  23. data/lib/ruby_smb/dcerpc/p_cont_list_t.rb +37 -0
  24. data/lib/ruby_smb/dcerpc/p_result_list_t.rb +13 -0
  25. data/lib/ruby_smb/dcerpc/p_result_t.rb +15 -0
  26. data/lib/ruby_smb/dcerpc/port_any_t.rb +11 -0
  27. data/lib/ruby_smb/dcerpc/request.rb +8 -3
  28. data/lib/ruby_smb/dcerpc/response.rb +6 -1
  29. data/lib/ruby_smb/dcerpc/samr.rb +4 -2
  30. data/lib/ruby_smb/dcerpc.rb +165 -122
  31. data/lib/ruby_smb/dispatcher/socket.rb +1 -1
  32. data/lib/ruby_smb/field/file_time.rb +2 -2
  33. data/lib/ruby_smb/field/utime.rb +2 -2
  34. data/lib/ruby_smb/generic_packet.rb +2 -2
  35. data/lib/ruby_smb/gss/provider/ntlm.rb +12 -5
  36. data/lib/ruby_smb/ntlm/client.rb +4 -0
  37. data/lib/ruby_smb/ntlm/custom/string_encoder.rb +22 -0
  38. data/lib/ruby_smb/ntlm.rb +37 -0
  39. data/lib/ruby_smb/server/server_client/share_io.rb +2 -2
  40. data/lib/ruby_smb/server/server_client/tree_connect.rb +2 -2
  41. data/lib/ruby_smb/server/share/provider/disk.rb +2 -2
  42. data/lib/ruby_smb/server/share/provider/processor.rb +25 -0
  43. data/lib/ruby_smb/server/share/provider/virtual_disk.rb +3 -3
  44. data/lib/ruby_smb/server/share/provider.rb +48 -1
  45. data/lib/ruby_smb/smb1/data_block.rb +2 -2
  46. data/lib/ruby_smb/smb1/packet/nt_create_andx_request.rb +1 -1
  47. data/lib/ruby_smb/smb1/parameter_block.rb +2 -2
  48. data/lib/ruby_smb/smb1/pipe.rb +4 -0
  49. data/lib/ruby_smb/smb2/create_context.rb +5 -2
  50. data/lib/ruby_smb/smb2/packet/negotiate_request.rb +5 -5
  51. data/lib/ruby_smb/smb2/packet/negotiate_response.rb +1 -1
  52. data/lib/ruby_smb/smb2/pipe.rb +4 -0
  53. data/lib/ruby_smb/smb2/tree.rb +3 -1
  54. data/lib/ruby_smb/utils.rb +15 -0
  55. data/lib/ruby_smb/version.rb +1 -1
  56. data/lib/ruby_smb.rb +2 -0
  57. data/spec/lib/ruby_smb/client_spec.rb +4 -4
  58. data/spec/lib/ruby_smb/dcerpc/client_spec.rb +31 -16
  59. data/spec/lib/ruby_smb/dcerpc/drsr_spec.rb +4 -1
  60. data/spec/lib/ruby_smb/dcerpc/ndr_spec.rb +198 -0
  61. data/spec/lib/ruby_smb/dcerpc/request_spec.rb +0 -6
  62. data/spec/lib/ruby_smb/dcerpc/response_spec.rb +4 -12
  63. data/spec/lib/ruby_smb/dcerpc/sec_trailer_spec.rb +0 -14
  64. data/spec/lib/ruby_smb/dcerpc/winreg_spec.rb +2 -2
  65. data/spec/lib/ruby_smb/smb1/packet/read_andx_request_spec.rb +2 -2
  66. data/spec/lib/ruby_smb/smb1/packet/write_andx_request_spec.rb +1 -1
  67. data/spec/lib/ruby_smb/smb2/create_context/create_context_request_spec.rb +35 -0
  68. data/spec/lib/ruby_smb/smb2/tree_spec.rb +5 -0
  69. data.tar.gz.sig +0 -0
  70. metadata +41 -86
  71. metadata.gz.sig +0 -0
@@ -51,11 +51,128 @@ module RubySMB
51
51
  require 'ruby_smb/dcerpc/request'
52
52
  require 'ruby_smb/dcerpc/response'
53
53
  require 'ruby_smb/dcerpc/rpc_auth3'
54
+ require 'ruby_smb/dcerpc/port_any_t'
55
+ require 'ruby_smb/dcerpc/p_cont_list_t'
56
+ require 'ruby_smb/dcerpc/p_result_t'
57
+ require 'ruby_smb/dcerpc/p_result_list_t'
58
+ require 'ruby_smb/dcerpc/alter_context'
54
59
  require 'ruby_smb/dcerpc/bind'
55
60
  require 'ruby_smb/dcerpc/bind_ack'
61
+ require 'ruby_smb/dcerpc/alter_context_resp'
56
62
  require 'ruby_smb/dcerpc/print_system'
57
63
  require 'ruby_smb/dcerpc/encrypting_file_system'
58
64
 
65
+ # Initialize the auth provider using NTLM. This function should be overriden for other providers (e.g. Kerberos, etc.)
66
+ # @raise ArgumentError If @ntlm_client isn't initialized with a username and password.
67
+ # @return Serialized message for initializing the auth provider (NTLM, unless this class is extended/overridden)
68
+ def auth_provider_init
69
+ raise ArgumentError, "NTLM Client not initialized. Username and password must be provided" unless @ntlm_client
70
+ type1_message = @ntlm_client.init_context
71
+
72
+ type1_message.serialize
73
+ end
74
+
75
+ # Encrypt the value in dcerpc_req.stub, and add a valid signature to the request.
76
+ # This function modifies the request object in-place, and does not return anything.
77
+ # This function should be overriden for other providers (e.g. Kerberos, etc.)
78
+ # @param dcerpc_req [Request] The Request object to be encrypted and signed in-place
79
+ def auth_provider_encrypt_and_sign(dcerpc_req)
80
+ auth_type = dcerpc_req.sec_trailer.auth_type
81
+ auth_level = dcerpc_req.sec_trailer.auth_level
82
+ unless [RPC_C_AUTHN_WINNT, RPC_C_AUTHN_DEFAULT].include?(auth_type)
83
+ raise ArgumentError, "Unsupported Auth Type: #{auth_type}"
84
+ end
85
+ plaintext = dcerpc_req.stub.to_binary_s
86
+ pad_length = get_auth_padding_length(plaintext.length)
87
+ dcerpc_req.auth_pad = "\x00" * pad_length
88
+ data_to_sign = plain_stub_with_padding = dcerpc_req.stub.to_binary_s + dcerpc_req.auth_pad.to_binary_s
89
+ dcerpc_req.sec_trailer.auth_pad_length = pad_length
90
+ if @ntlm_client.flags & NTLM::NEGOTIATE_FLAGS[:EXTENDED_SECURITY] != 0
91
+ data_to_sign = dcerpc_req.to_binary_s[0..-(dcerpc_req.pdu_header.auth_length + 1)]
92
+ end
93
+
94
+ if auth_level == RPC_C_AUTHN_LEVEL_PKT_PRIVACY
95
+ encrypted = @ntlm_client.session.seal_message(plain_stub_with_padding)
96
+ set_encrypted_packet(dcerpc_req, encrypted, pad_length)
97
+ end
98
+ signature = @ntlm_client.session.sign_message(data_to_sign)
99
+
100
+ set_signature_on_packet(dcerpc_req, signature)
101
+ end
102
+
103
+ # Get the response's full stub value (which will include the auth-pad)
104
+ # @param dcerpc_response [Response] The Response object to extract from
105
+ # @return [String] The full stub, including auth_pad
106
+ def get_response_full_stub(dcerpc_response)
107
+ dcerpc_response.stub.to_binary_s + dcerpc_response.auth_pad.to_binary_s
108
+ end
109
+
110
+ # Decrypt the value in dcerpc_req.stub, and validate its signature.
111
+ # This function modifies the request object in-place, and returns whether the signature was valid.
112
+ # This function should be overriden for other providers (e.g. Kerberos, etc.)
113
+ # @param dcerpc_response [Response] The Response packet to decrypt and verify in-place
114
+ # @raise ArgumentError If the auth type is not NTLM
115
+ # @return [Boolean] Is the packet's signature valid?
116
+ def auth_provider_decrypt_and_verify(dcerpc_response)
117
+ auth_type = dcerpc_response.sec_trailer.auth_type
118
+ auth_level = dcerpc_response.sec_trailer.auth_level
119
+ unless [RPC_C_AUTHN_WINNT, RPC_C_AUTHN_DEFAULT].include?(auth_type)
120
+ raise ArgumentError, "Unsupported Auth Type: #{auth_type}"
121
+ end
122
+ signature = dcerpc_response.auth_value
123
+ if auth_level == RPC_C_AUTHN_LEVEL_PKT_PRIVACY
124
+ encrypted_stub = get_response_full_stub(dcerpc_response)
125
+ plaintext = @ntlm_client.session.unseal_message(encrypted_stub)
126
+ set_decrypted_packet(dcerpc_response, plaintext)
127
+ end
128
+ data_to_check = dcerpc_response.stub.to_binary_s
129
+ if @ntlm_client.flags & NTLM::NEGOTIATE_FLAGS[:EXTENDED_SECURITY] != 0
130
+ data_to_check = dcerpc_response.to_binary_s[0..-(dcerpc_response.pdu_header.auth_length + 1)]
131
+ end
132
+
133
+ @ntlm_client.session.verify_signature(signature, data_to_check)
134
+ end
135
+
136
+ # Completes local initialisation of @ntlm_client using the server's response
137
+ #
138
+ # @param type2_message [String] NTLM type 2 message sent from server
139
+ # @return [String] Type 3 message to be sent to the server to complete the NTLM handshake
140
+ def process_ntlm_type2(type2_message)
141
+ ntlmssp_offset = type2_message.index('NTLMSSP')
142
+ type2_blob = type2_message.slice(ntlmssp_offset..-1)
143
+ type2_b64_message = [type2_blob].pack('m')
144
+ type3_message = @ntlm_client.init_context(type2_b64_message)
145
+ auth3 = type3_message.serialize
146
+
147
+ @session_key = @ntlm_client.session_key
148
+ auth3
149
+ end
150
+
151
+ # Send a rpc_auth3 PDU that ends the authentication handshake.
152
+ # This function should be overriden for other providers (e.g. Kerberos, etc.)
153
+ #
154
+ # @param response [BindAck] the BindAck response packet
155
+ # @param options [Hash] Unused by the NTLM auth provider
156
+ def auth_provider_complete_handshake(response, options)
157
+ auth3 = process_ntlm_type2(response.auth_value)
158
+
159
+ rpc_auth3 = RpcAuth3.new
160
+ add_auth_verifier(rpc_auth3, auth3)
161
+ rpc_auth3.pdu_header.call_id = @call_id
162
+
163
+ # The server should not respond
164
+ send_packet(rpc_auth3)
165
+ @call_id += 1
166
+
167
+ nil
168
+ end
169
+
170
+ def force_set_auth_params(auth_type, auth_level)
171
+ @auth_type = auth_type
172
+ @auth_level = auth_level
173
+ end
174
+
175
+
59
176
  # Bind to the remote server interface endpoint. It takes care of adding
60
177
  # the necessary authentication verifier if `:auth_level` is set to
61
178
  # anything different than RPC_C_AUTHN_LEVEL_NONE
@@ -74,23 +191,16 @@ module RubySMB
74
191
  @call_id ||= 1
75
192
  bind_req = Bind.new(options)
76
193
  bind_req.pdu_header.call_id = @call_id
194
+ auth_type = options.fetch(:auth_type) { RPC_C_AUTHN_WINNT }
195
+ auth_level = options.fetch(:auth_level) { RPC_C_AUTHN_LEVEL_NONE }
77
196
 
78
- if options[:auth_level] && options[:auth_level] != RPC_C_AUTHN_LEVEL_NONE
79
- case options[:auth_type]
80
- when RPC_C_AUTHN_WINNT, RPC_C_AUTHN_DEFAULT
81
- @ctx_id = 0
82
- @auth_ctx_id_base = rand(0xFFFFFFFF)
83
- raise ArgumentError, "NTLM Client not initialized. Username and password must be provided" unless @ntlm_client
84
- type1_message = @ntlm_client.init_context
85
- auth = type1_message.serialize
86
- when RPC_C_AUTHN_GSS_KERBEROS, RPC_C_AUTHN_NETLOGON, RPC_C_AUTHN_GSS_NEGOTIATE
87
- when RPC_C_AUTHN_GSS_KERBEROS, RPC_C_AUTHN_NETLOGON, RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_GSS_SCHANNEL
88
- # TODO
89
- raise NotImplementedError
90
- else
91
- raise ArgumentError, "Unsupported Auth Type: #{options[:auth_type]}"
92
- end
93
- add_auth_verifier(bind_req, auth, options[:auth_type], options[:auth_level])
197
+ force_set_auth_params(auth_type, auth_level)
198
+
199
+ if @auth_level != RPC_C_AUTHN_LEVEL_NONE
200
+ @ctx_id = 0
201
+ @auth_ctx_id_base = rand(0xFFFFFFFF)
202
+ auth = auth_provider_init
203
+ add_auth_verifier(bind_req, auth)
94
204
  end
95
205
 
96
206
  send_packet(bind_req)
@@ -110,17 +220,11 @@ module RubySMB
110
220
  self.max_buffer_size = dcerpc_response.max_xmit_frag
111
221
  @call_id = dcerpc_response.pdu_header.call_id
112
222
 
113
- if options[:auth_level] && options[:auth_level] != RPC_C_AUTHN_LEVEL_NONE
223
+ if auth_level != RPC_C_AUTHN_LEVEL_NONE
114
224
  # The number of legs needed to build the security context is defined
115
225
  # by the security provider
116
- # (see [2.2.1.1.7 Security Providers](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rpce/d4097450-c62f-484b-872f-ddf59a7a0d36))
117
- case options[:auth_type]
118
- when RPC_C_AUTHN_WINNT
119
- send_auth3(dcerpc_response, options[:auth_type], options[:auth_level])
120
- when RPC_C_AUTHN_GSS_KERBEROS, RPC_C_AUTHN_NETLOGON, RPC_C_AUTHN_GSS_NEGOTIATE
121
- # TODO
122
- raise NotImplementedError
123
- end
226
+ # (see [2.2.1.1.7 Security Providers](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rpce/d4097450-c62f-484b-872f-ddf59a7a0d36))
227
+ auth_provider_complete_handshake(dcerpc_response, options)
124
228
  end
125
229
 
126
230
  dcerpc_response
@@ -156,16 +260,24 @@ module RubySMB
156
260
  nil
157
261
  end
158
262
 
263
+ def get_auth_padding_length(plaintext_len)
264
+ (16 - (plaintext_len % 16)) % 16
265
+ end
266
+
159
267
  # Add the authentication verifier to a Request packet. This includes a
160
268
  # sec trailer and the signature of the packet. This also encrypts the
161
269
  # Request stub if privacy is required (`:auth_level` option is
162
270
  # RPC_C_AUTHN_LEVEL_PKT_PRIVACY).
163
271
  #
164
272
  # @param dcerpc_req [Request] the Request packet to be updated
165
- # @param opts [Hash] the authenticaiton options: `:auth_type` and `:auth_level`
273
+ # @param opts [Hash] the authentication options: `:auth_type` and `:auth_level`
166
274
  # @raise [NotImplementedError] if `:auth_type` is not implemented (yet)
167
275
  # @raise [ArgumentError] if `:auth_type` is unknown
168
276
  def set_integrity_privacy(dcerpc_req, auth_level:, auth_type:)
277
+ unless [RPC_C_AUTHN_LEVEL_PKT_INTEGRITY, RPC_C_AUTHN_LEVEL_PKT_PRIVACY].include?(auth_level)
278
+ raise ArgumentError, "Unsupported Auth Type: #{auth_type}"
279
+ end
280
+
169
281
  dcerpc_req.sec_trailer = {
170
282
  auth_type: auth_type,
171
283
  auth_level: auth_level,
@@ -174,35 +286,30 @@ module RubySMB
174
286
  dcerpc_req.auth_value = ' ' * 16
175
287
  dcerpc_req.pdu_header.auth_length = 16
176
288
 
177
- data_to_sign = plain_stub = dcerpc_req.stub.to_binary_s + dcerpc_req.auth_pad.to_binary_s
178
- if @ntlm_client.flags & NTLM::NEGOTIATE_FLAGS[:EXTENDED_SECURITY] != 0
179
- data_to_sign = dcerpc_req.to_binary_s[0..-(dcerpc_req.pdu_header.auth_length + 1)]
180
- end
181
-
182
- encrypted_stub = ''
183
- if auth_level == RPC_C_AUTHN_LEVEL_PKT_PRIVACY
184
- case auth_type
185
- when RPC_C_AUTHN_NONE
186
- when RPC_C_AUTHN_WINNT, RPC_C_AUTHN_DEFAULT
187
- encrypted_stub = @ntlm_client.session.seal_message(plain_stub)
188
- when RPC_C_AUTHN_NETLOGON, RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_GSS_SCHANNEL, RPC_C_AUTHN_GSS_KERBEROS
189
- # TODO
190
- raise NotImplementedError
191
- else
192
- raise ArgumentError, "Unsupported Auth Type: #{auth_type}"
193
- end
194
- end
289
+ auth_provider_encrypt_and_sign(dcerpc_req)
290
+ end
195
291
 
196
- signature = @ntlm_client.session.sign_message(data_to_sign)
292
+ def set_signature_on_packet(dcerpc_req, signature)
293
+ dcerpc_req.auth_value = signature
294
+ dcerpc_req.pdu_header.auth_length = signature.size
295
+ end
197
296
 
297
+ def set_encrypted_packet(dcerpc_req, encrypted_stub, pad_length)
198
298
  unless encrypted_stub.empty?
199
- pad_length = dcerpc_req.sec_trailer.auth_pad_length.to_i
200
299
  dcerpc_req.enable_encrypted_stub
201
- dcerpc_req.stub = encrypted_stub[0..-(pad_length + 1)]
202
- dcerpc_req.auth_pad = encrypted_stub[-(pad_length)..-1]
300
+ dcerpc_req.stub = encrypted_stub[0..-(pad_length+1)]
301
+ if pad_length != 0
302
+ dcerpc_req.auth_pad = encrypted_stub[-(pad_length)..-1]
303
+ end
304
+ end
305
+ end
306
+
307
+ def set_decrypted_packet(dcerpc_response, decrypted_stub)
308
+ unless decrypted_stub.empty?
309
+ pad_length = dcerpc_response.sec_trailer.auth_pad_length.to_i
310
+ dcerpc_response.stub = decrypted_stub[0..-(pad_length + 1)]
311
+ dcerpc_response.auth_pad = decrypted_stub[-(pad_length + 1)..-1]
203
312
  end
204
- dcerpc_req.auth_value = signature
205
- dcerpc_req.pdu_header.auth_length = signature.size
206
313
  end
207
314
 
208
315
  # Process the security context received in a response. It decrypts the
@@ -214,39 +321,18 @@ module RubySMB
214
321
  #
215
322
  # @param dcerpc_response [Response] the Response packet
216
323
  # containing the security context to process
217
- # @param opts [Hash] the authenticaiton options: `:auth_type` and
324
+ # @param opts [Hash] the authentication options: `:auth_type` and
218
325
  # `:auth_level`. To enable errors when signature check fails, set the
219
326
  # `:raise_signature_error` option to true
220
327
  # @raise [NotImplementedError] if `:auth_type` is not implemented (yet)
221
328
  # @raise [Error::CommunicationError] if socket-related error occurs
222
329
  def handle_integrity_privacy(dcerpc_response, auth_level:, auth_type:, raise_signature_error: false)
223
- decrypted_stub = ''
224
- if auth_level == RPC_C_AUTHN_LEVEL_PKT_PRIVACY
225
- encrypted_stub = dcerpc_response.stub.to_binary_s + dcerpc_response.auth_pad.to_binary_s
226
- case auth_type
227
- when RPC_C_AUTHN_NONE
228
- when RPC_C_AUTHN_WINNT, RPC_C_AUTHN_DEFAULT
229
- decrypted_stub = @ntlm_client.session.unseal_message(encrypted_stub)
230
- when RPC_C_AUTHN_NETLOGON, RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_GSS_SCHANNEL, RPC_C_AUTHN_GSS_KERBEROS
231
- # TODO
232
- raise NotImplementedError
233
- else
234
- raise ArgumentError, "Unsupported Auth Type: #{auth_type}"
235
- end
236
- end
237
-
238
- unless decrypted_stub.empty?
239
- pad_length = dcerpc_response.sec_trailer.auth_pad_length.to_i
240
- dcerpc_response.stub = decrypted_stub[0..-(pad_length + 1)]
241
- dcerpc_response.auth_pad = decrypted_stub[-(pad_length)..-1]
330
+ unless [RPC_C_AUTHN_LEVEL_PKT_INTEGRITY, RPC_C_AUTHN_LEVEL_PKT_PRIVACY].include?(auth_level)
331
+ raise ArgumentError, "Unsupported Auth Type: #{auth_type}"
242
332
  end
333
+ signature_valid = auth_provider_decrypt_and_verify(dcerpc_response)
243
334
 
244
- signature = dcerpc_response.auth_value
245
- data_to_check = dcerpc_response.stub.to_binary_s
246
- if @ntlm_client.flags & NTLM::NEGOTIATE_FLAGS[:EXTENDED_SECURITY] != 0
247
- data_to_check = dcerpc_response.to_binary_s[0..-(dcerpc_response.pdu_header.auth_length + 1)]
248
- end
249
- unless @ntlm_client.session.verify_signature(signature, data_to_check)
335
+ unless signature_valid
250
336
  if raise_signature_error
251
337
  raise Error::InvalidPacket.new(
252
338
  "Wrong packet signature received (set `raise_signature_error` to false to ignore)"
@@ -264,12 +350,10 @@ module RubySMB
264
350
  #
265
351
  # @param req [BinData::Record] the request to be updated
266
352
  # @param auth [String] the authentication data
267
- # @param auth_type [Integer] the authentication type
268
- # @param auth_level [Integer] the authentication level
269
- def add_auth_verifier(req, auth, auth_type, auth_level)
353
+ def add_auth_verifier(req, auth)
270
354
  req.sec_trailer = {
271
- auth_type: auth_type,
272
- auth_level: auth_level,
355
+ auth_type: @auth_type,
356
+ auth_level: @auth_level,
273
357
  auth_context_id: @ctx_id + @auth_ctx_id_base
274
358
  }
275
359
  req.auth_value = auth
@@ -277,46 +361,5 @@ module RubySMB
277
361
 
278
362
  nil
279
363
  end
280
-
281
- def process_ntlm_type2(type2_message)
282
- ntlmssp_offset = type2_message.index('NTLMSSP')
283
- type2_blob = type2_message.slice(ntlmssp_offset..-1)
284
- type2_b64_message = [type2_blob].pack('m')
285
- type3_message = @ntlm_client.init_context(type2_b64_message)
286
- auth3 = type3_message.serialize
287
-
288
- @session_key = @ntlm_client.session_key
289
- auth3
290
- end
291
-
292
- # Send a rpc_auth3 PDU that ends the authentication handshake.
293
- #
294
- # @param response [BindAck] the BindAck response packet
295
- # @param auth_type [Integer] the authentication type
296
- # @param auth_level [Integer] the authentication level
297
- # @raise [ArgumentError] if `:auth_type` is unknown
298
- # @raise [NotImplementedError] if `:auth_type` is not implemented (yet)
299
- def send_auth3(response, auth_type, auth_level)
300
- case auth_type
301
- when RPC_C_AUTHN_NONE
302
- when RPC_C_AUTHN_WINNT, RPC_C_AUTHN_DEFAULT
303
- auth3 = process_ntlm_type2(response.auth_value)
304
- when RPC_C_AUTHN_NETLOGON, RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_GSS_SCHANNEL, RPC_C_AUTHN_GSS_KERBEROS
305
- # TODO
306
- raise NotImplementedError
307
- else
308
- raise ArgumentError, "Unsupported Auth Type: #{auth_type}"
309
- end
310
-
311
- rpc_auth3 = RpcAuth3.new
312
- add_auth_verifier(rpc_auth3, auth3, auth_type, auth_level)
313
- rpc_auth3.pdu_header.call_id = @call_id
314
-
315
- # The server should not respond
316
- send_packet(rpc_auth3)
317
- @call_id += 1
318
-
319
- nil
320
- end
321
364
  end
322
365
  end
@@ -24,7 +24,7 @@ module RubySMB
24
24
  end
25
25
 
26
26
  # @param host [String] passed to TCPSocket.new
27
- # @param port [Fixnum] passed to TCPSocket.new
27
+ # @param port [Integer] passed to TCPSocket.new
28
28
  def self.connect(host, port: 445, socket: TCPSocket.new(host, port))
29
29
  new(socket)
30
30
  end
@@ -18,12 +18,12 @@ module RubySMB
18
18
  val
19
19
  end
20
20
 
21
- # Sets the value of the field from a DateTime,Time,Fixnum, or object
21
+ # Sets the value of the field from a DateTime,Time,Integer, or object
22
22
  # that can be converted to an integer. Datetime and Time objects get
23
23
  # converted to account for the Windows/Unix Epoch difference. Any other
24
24
  # parameter passed in will be assumed to already be correct.
25
25
  #
26
- # @param value [DateTime,Time,Fixnum,#to_i] the value to set
26
+ # @param value [DateTime,Time,Integer,#to_i] the value to set
27
27
  # @return
28
28
  def set(value)
29
29
  case value
@@ -16,11 +16,11 @@ module RubySMB
16
16
  val
17
17
  end
18
18
 
19
- # Sets the value of the field from a DateTime,Time,Fixnum, or object
19
+ # Sets the value of the field from a DateTime,Time,Integer, or object
20
20
  # that can be converted to an integer. Any other
21
21
  # parameter passed in will be assumed to already be correct.
22
22
  #
23
- # @param value [DateTime,Time,Fixnum,#to_i] the value to set
23
+ # @param value [DateTime,Time,Integer,#to_i] the value to set
24
24
  # @return
25
25
  def set(value)
26
26
  case value
@@ -129,7 +129,7 @@ module RubySMB
129
129
  # string representation.
130
130
  #
131
131
  # @param field [Hash] the hash representing the field
132
- # @param depth [Fixnum] the recursive depth level to track indentation
132
+ # @param depth [Integer] the recursive depth level to track indentation
133
133
  # @return [String] the formatted string representation of the field
134
134
  def self.format_field(field, depth = 0)
135
135
  name = field[:name].to_s
@@ -181,7 +181,7 @@ module RubySMB
181
181
  # into a string representing the contents of that field.
182
182
  #
183
183
  # @param field [Hash] hash representation of the field to display
184
- # @param depth [Fixnum] the recursion depth for setting indent levels
184
+ # @param depth [Integer] the recursion depth for setting indent levels
185
185
  # @param parents [Array<Symbol>] the name of the parent field, if any, of this field
186
186
  # @return [String] a formatted string representing the field and it's current contents
187
187
  def display_field(field, depth = 0, parents = [])
@@ -18,6 +18,7 @@ module RubySMB
18
18
  end
19
19
 
20
20
  class Authenticator < Authenticator::Base
21
+
21
22
  def reset!
22
23
  super
23
24
  @server_challenge = nil
@@ -141,7 +142,10 @@ module RubySMB
141
142
  case type3_msg.ntlm_version
142
143
  when :ntlmv1
143
144
  my_ntlm_response = Net::NTLM::ntlm_response(
144
- ntlm_hash: Net::NTLM::ntlm_hash(account.password.encode('UTF-16LE'), unicode: true),
145
+ ntlm_hash: Net::NTLM::ntlm_hash(
146
+ RubySMB::Utils.safe_encode(account.password, 'UTF-16LE'),
147
+ unicode: true
148
+ ),
145
149
  challenge: @server_challenge
146
150
  )
147
151
  matches = my_ntlm_response == type3_msg.ntlm_response
@@ -151,9 +155,9 @@ module RubySMB
151
155
  their_blob = type3_msg.ntlm_response[digest.digest_length..-1]
152
156
 
153
157
  ntlmv2_hash = Net::NTLM.ntlmv2_hash(
154
- account.username.encode('UTF-16LE'),
155
- account.password.encode('UTF-16LE'),
156
- type3_msg.domain.encode('UTF-16LE'), # don't use the account domain because of the special '.' value
158
+ RubySMB::Utils.safe_encode(account.username, 'UTF-16LE'),
159
+ RubySMB::Utils.safe_encode(account.password, 'UTF-16LE'),
160
+ RubySMB::Utils.safe_encode(type3_msg.domain, 'UTF-16LE'), # don't use the account domain because of the special '.' value
157
161
  {client_challenge: their_blob[16...24], unicode: true}
158
162
  )
159
163
 
@@ -305,7 +309,10 @@ module RubySMB
305
309
  username = username.downcase
306
310
  domain = @default_domain if domain.nil? || domain == '.'.encode(domain.encoding)
307
311
  domain = domain.downcase
308
- @accounts.find { |account| account.username.encode(username.encoding).downcase == username && account.domain.encode(domain.encoding).downcase == domain }
312
+ @accounts.find do |account|
313
+ RubySMB::Utils.safe_encode(account.username, username.encoding).downcase == username &&
314
+ RubySMB::Utils.safe_encode(account.domain, domain.encoding).downcase == domain
315
+ end
309
316
  end
310
317
 
311
318
  #
@@ -51,6 +51,10 @@ module RubySMB::NTLM
51
51
  !challenge_message.has_flag?(:UNICODE) && challenge_message.has_flag?(:OEM)
52
52
  end
53
53
 
54
+ def ntlmv2_hash
55
+ @ntlmv2_hash ||= RubySMB::NTLM.ntlmv2_hash(username, password, domain, {:client_challenge => client_challenge, :unicode => !use_oem_strings?})
56
+ end
57
+
54
58
  def calculate_user_session_key!
55
59
  if is_anonymous?
56
60
  # see MS-NLMP section 3.4
@@ -0,0 +1,22 @@
1
+ require 'net/ntlm'
2
+
3
+ module RubySMB
4
+ module NTLM
5
+ module Custom
6
+ module StringEncoder
7
+
8
+ def self.prepended(base)
9
+ base.singleton_class.send(:prepend, ClassMethods)
10
+ end
11
+
12
+ module ClassMethods
13
+ def encode_utf16le(str)
14
+ str.dup.force_encoding('UTF-8').encode(Encoding::UTF_16LE, Encoding::UTF_8).force_encoding('ASCII-8BIT')
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ Net::NTLM::EncodeUtil.send(:prepend, RubySMB::NTLM::Custom::StringEncoder)
data/lib/ruby_smb/ntlm.rb CHANGED
@@ -1,3 +1,5 @@
1
+ require 'ruby_smb/ntlm/custom/string_encoder'
2
+
1
3
  module RubySMB
2
4
  module NTLM
3
5
  # [[MS-NLMP] 2.2.2.5](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/99d90ff4-957f-4c8a-80e4-5bfe5a9a9832)
@@ -56,6 +58,41 @@ module RubySMB
56
58
  "Version #{major}.#{minor} (Build #{build}); NTLM Current Revision #{ntlm_revision}"
57
59
  end
58
60
  end
61
+
62
+ class << self
63
+
64
+ # Generate a NTLMv2 Hash
65
+ # @param [String] user The username
66
+ # @param [String] password The password
67
+ # @param [String] target The domain or workstation to authenticate to
68
+ # @option opt :unicode (false) Unicode encode the domain
69
+ def ntlmv2_hash(user, password, target, opt={})
70
+ if Net::NTLM.is_ntlm_hash? password
71
+ decoded_password = Net::NTLM::EncodeUtil.decode_utf16le(password)
72
+ ntlmhash = [decoded_password.upcase[33,65]].pack('H32')
73
+ else
74
+ ntlmhash = Net::NTLM.ntlm_hash(password, opt)
75
+ end
76
+
77
+ if opt[:unicode]
78
+ # Uppercase operation on username containing non-ASCII characters
79
+ # after being unicode encoded with `EncodeUtil.encode_utf16le`
80
+ # doesn't play well. Upcase should be done before encoding.
81
+ user_upcase = Net::NTLM::EncodeUtil.decode_utf16le(user).upcase
82
+ user_upcase = Net::NTLM::EncodeUtil.encode_utf16le(user_upcase)
83
+ else
84
+ user_upcase = user.upcase
85
+ end
86
+ userdomain = user_upcase + target
87
+
88
+ unless opt[:unicode]
89
+ userdomain = Net::NTLM::EncodeUtil.encode_utf16le(userdomain)
90
+ end
91
+ OpenSSL::HMAC.digest(OpenSSL::Digest::MD5.new, ntlmhash, userdomain)
92
+ end
93
+
94
+ end
95
+
59
96
  end
60
97
  end
61
98
 
@@ -11,7 +11,7 @@ module RubySMB
11
11
  end
12
12
 
13
13
  logger.debug("Received #{SMB1::Commands.name(request.smb_header.command)} request for share: #{share_processor.provider.name}")
14
- share_processor.send(__callee__, request)
14
+ share_processor.share_io(__callee__, request)
15
15
  end
16
16
 
17
17
  alias :do_close_smb1 :proxy_share_io_smb1
@@ -29,7 +29,7 @@ module RubySMB
29
29
  end
30
30
 
31
31
  logger.debug("Received #{SMB2::Commands.name(request.smb2_header.command)} request for share: #{share_processor.provider.name}")
32
- share_processor.send(__callee__, request)
32
+ share_processor.share_io(__callee__, request)
33
33
  end
34
34
 
35
35
  alias :do_close_smb2 :proxy_share_io_smb2
@@ -7,7 +7,7 @@ module RubySMB
7
7
  # see: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-cifs/b062f3e3-1b65-4a9a-854a-0ee432499d8f
8
8
  response = RubySMB::SMB1::Packet::TreeConnectResponse.new
9
9
 
10
- share_name = request.data_block.path.encode('UTF-8').split('\\', 4).last
10
+ share_name = RubySMB::Utils.safe_encode(request.data_block.path, 'UTF-8').split('\\', 4).last
11
11
  share_provider = @server.shares.transform_keys(&:downcase)[share_name.downcase]
12
12
  if share_provider.nil?
13
13
  logger.warn("Received TREE_CONNECT request for non-existent share: #{share_name}")
@@ -49,7 +49,7 @@ module RubySMB
49
49
  return response
50
50
  end
51
51
 
52
- share_name = request.path.encode('UTF-8').split('\\', 4).last
52
+ share_name = RubySMB::Utils.safe_encode(request.path, 'UTF-8').split('\\', 4).last
53
53
  share_provider = @server.shares.transform_keys(&:downcase)[share_name.downcase]
54
54
 
55
55
  if share_provider.nil?
@@ -14,13 +14,13 @@ module RubySMB
14
14
  # @param [String] name The name of this share.
15
15
  # @param [String, Pathname] path The local file system path to share. This path must be an absolute path to an existing
16
16
  # directory.
17
- def initialize(name, path)
17
+ def initialize(name, path, hooks: nil)
18
18
  path = Pathname.new(File.expand_path(path)) if path.is_a?(String)
19
19
  raise ArgumentError.new('path must be a directory') unless path.directory? # it needs to exist
20
20
  raise ArgumentError.new('path must be absolute') unless path.absolute? # it needs to be absolute so it is independent of the cwd
21
21
 
22
22
  @path = path
23
- super(name)
23
+ super(name, hooks: hooks)
24
24
  end
25
25
 
26
26
  attr_accessor :path
@@ -80,6 +80,31 @@ module RubySMB
80
80
  @server_client.server
81
81
  end
82
82
 
83
+ # Forward a share IO method for a particular request. This is a choke point to allow any hooks that were
84
+ # registered with the share provider to be executed before and after the specified method is invoked to
85
+ # process the request and generate the response. This is used for both SMB1 and SMB2 requests.
86
+ #
87
+ # @param [Symbol] method_name The method name to forward the request to
88
+ # @param [RubySMB::GenericPacket] request The request packet to be processed
89
+ # @return [RubySMB::GenericPacket]
90
+ def share_io(method_name, request)
91
+ @provider.hooks.each do |hook|
92
+ next unless hook.request_class == request.class && hook.location == :before
93
+
94
+ request = hook.callback.call(@session, request) || request
95
+ end
96
+
97
+ response = send(method_name, request)
98
+
99
+ @provider.hooks.each do |hook|
100
+ next unless hook.request_class == request.class && hook.location == :after
101
+
102
+ response = hook.callback.call(@session, request, response) || response
103
+ end
104
+
105
+ response
106
+ end
107
+
83
108
  # The underlying share provider that this is a processor for.
84
109
  # @!attribute [r] provider
85
110
  # @return [RubySMB::Server::Share::Provider::Base]
@@ -14,9 +14,9 @@ module RubySMB
14
14
  private_constant :HASH_METHODS
15
15
 
16
16
  # @param [String] name The name of this share.
17
- def initialize(name)
17
+ def initialize(name, hooks: nil)
18
18
  @vfs = {}
19
- super(name, add(VirtualPathname.new(self, File::SEPARATOR)))
19
+ super(name, add(VirtualPathname.new(self, File::SEPARATOR)), hooks: hooks)
20
20
  end
21
21
 
22
22
  # Add a dynamic file to the virtual file system. A dynamic file is one whose contents are generated by the
@@ -86,7 +86,7 @@ module RubySMB
86
86
  end
87
87
 
88
88
  def new_processor(server_client, session)
89
- scoped_virtual_disk = self.class.new(@name)
89
+ scoped_virtual_disk = self.class.new(@name, hooks: @hooks)
90
90
  @vfs.each_value do |path|
91
91
  path = path.dup
92
92
  path.virtual_disk = scoped_virtual_disk if path.is_a?(VirtualPathname)