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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/.github/workflows/verify.yml +8 -9
- data/README.md +1 -0
- data/cortex.yaml +15 -0
- data/examples/dump_secrets_from_sid.rb +2 -1
- data/lib/ruby_smb/client.rb +6 -4
- data/lib/ruby_smb/dcerpc/alter_context.rb +30 -0
- data/lib/ruby_smb/dcerpc/alter_context_resp.rb +42 -0
- data/lib/ruby_smb/dcerpc/bind.rb +3 -35
- data/lib/ruby_smb/dcerpc/bind_ack.rb +0 -31
- data/lib/ruby_smb/dcerpc/client.rb +10 -4
- data/lib/ruby_smb/dcerpc/drsr.rb +13 -13
- data/lib/ruby_smb/dcerpc/encrypting_file_system/efs_rpc_decrypt_file_srv_request.rb +22 -0
- data/lib/ruby_smb/dcerpc/encrypting_file_system/efs_rpc_decrypt_file_srv_response.rb +21 -0
- data/lib/ruby_smb/dcerpc/encrypting_file_system/efs_rpc_query_recover_agents_request.rb +20 -0
- data/lib/ruby_smb/dcerpc/encrypting_file_system/efs_rpc_query_recover_agents_response.rb +21 -0
- data/lib/ruby_smb/dcerpc/encrypting_file_system/efs_rpc_query_users_on_file_request.rb +20 -0
- data/lib/ruby_smb/dcerpc/encrypting_file_system/efs_rpc_query_users_on_file_response.rb +21 -0
- data/lib/ruby_smb/dcerpc/encrypting_file_system.rb +52 -0
- data/lib/ruby_smb/dcerpc/icpr.rb +3 -2
- data/lib/ruby_smb/dcerpc/ndr.rb +148 -3
- data/lib/ruby_smb/dcerpc/p_cont_list_t.rb +37 -0
- data/lib/ruby_smb/dcerpc/p_result_list_t.rb +13 -0
- data/lib/ruby_smb/dcerpc/p_result_t.rb +15 -0
- data/lib/ruby_smb/dcerpc/port_any_t.rb +11 -0
- data/lib/ruby_smb/dcerpc/request.rb +8 -3
- data/lib/ruby_smb/dcerpc/response.rb +6 -1
- data/lib/ruby_smb/dcerpc/samr.rb +4 -2
- data/lib/ruby_smb/dcerpc.rb +165 -122
- data/lib/ruby_smb/dispatcher/socket.rb +1 -1
- data/lib/ruby_smb/field/file_time.rb +2 -2
- data/lib/ruby_smb/field/utime.rb +2 -2
- data/lib/ruby_smb/generic_packet.rb +2 -2
- data/lib/ruby_smb/gss/provider/ntlm.rb +12 -5
- data/lib/ruby_smb/ntlm/client.rb +4 -0
- data/lib/ruby_smb/ntlm/custom/string_encoder.rb +22 -0
- data/lib/ruby_smb/ntlm.rb +37 -0
- data/lib/ruby_smb/server/server_client/share_io.rb +2 -2
- data/lib/ruby_smb/server/server_client/tree_connect.rb +2 -2
- data/lib/ruby_smb/server/share/provider/disk.rb +2 -2
- data/lib/ruby_smb/server/share/provider/processor.rb +25 -0
- data/lib/ruby_smb/server/share/provider/virtual_disk.rb +3 -3
- data/lib/ruby_smb/server/share/provider.rb +48 -1
- data/lib/ruby_smb/smb1/data_block.rb +2 -2
- data/lib/ruby_smb/smb1/packet/nt_create_andx_request.rb +1 -1
- data/lib/ruby_smb/smb1/parameter_block.rb +2 -2
- data/lib/ruby_smb/smb1/pipe.rb +4 -0
- data/lib/ruby_smb/smb2/create_context.rb +5 -2
- data/lib/ruby_smb/smb2/packet/negotiate_request.rb +5 -5
- data/lib/ruby_smb/smb2/packet/negotiate_response.rb +1 -1
- data/lib/ruby_smb/smb2/pipe.rb +4 -0
- data/lib/ruby_smb/smb2/tree.rb +3 -1
- data/lib/ruby_smb/utils.rb +15 -0
- data/lib/ruby_smb/version.rb +1 -1
- data/lib/ruby_smb.rb +2 -0
- data/spec/lib/ruby_smb/client_spec.rb +4 -4
- data/spec/lib/ruby_smb/dcerpc/client_spec.rb +31 -16
- data/spec/lib/ruby_smb/dcerpc/drsr_spec.rb +4 -1
- data/spec/lib/ruby_smb/dcerpc/ndr_spec.rb +198 -0
- data/spec/lib/ruby_smb/dcerpc/request_spec.rb +0 -6
- data/spec/lib/ruby_smb/dcerpc/response_spec.rb +4 -12
- data/spec/lib/ruby_smb/dcerpc/sec_trailer_spec.rb +0 -14
- data/spec/lib/ruby_smb/dcerpc/winreg_spec.rb +2 -2
- data/spec/lib/ruby_smb/smb1/packet/read_andx_request_spec.rb +2 -2
- data/spec/lib/ruby_smb/smb1/packet/write_andx_request_spec.rb +1 -1
- data/spec/lib/ruby_smb/smb2/create_context/create_context_request_spec.rb +35 -0
- data/spec/lib/ruby_smb/smb2/tree_spec.rb +5 -0
- data.tar.gz.sig +0 -0
- metadata +41 -86
- metadata.gz.sig +0 -0
data/lib/ruby_smb/dcerpc.rb
CHANGED
|
@@ -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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
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://
|
|
117
|
-
|
|
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
|
|
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
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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
|
|
202
|
-
|
|
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
|
|
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
|
-
|
|
224
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 [
|
|
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,
|
|
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,
|
|
26
|
+
# @param value [DateTime,Time,Integer,#to_i] the value to set
|
|
27
27
|
# @return
|
|
28
28
|
def set(value)
|
|
29
29
|
case value
|
data/lib/ruby_smb/field/utime.rb
CHANGED
|
@@ -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,
|
|
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,
|
|
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 [
|
|
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 [
|
|
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(
|
|
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
|
|
155
|
-
account.password
|
|
156
|
-
type3_msg.domain
|
|
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
|
|
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
|
#
|
data/lib/ruby_smb/ntlm/client.rb
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
|
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
|
|
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)
|