ruby_smb 3.1.7 → 3.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/.github/workflows/verify.yml +4 -6
- data/README.md +1 -1
- data/lib/ruby_smb/client/authentication.rb +4 -30
- data/lib/ruby_smb/client.rb +6 -4
- data/lib/ruby_smb/dcerpc/client.rb +12 -265
- data/lib/ruby_smb/dcerpc/error.rb +10 -0
- data/lib/ruby_smb/dcerpc/icpr/cert_server_request_request.rb +27 -0
- data/lib/ruby_smb/dcerpc/icpr/cert_server_request_response.rb +28 -0
- data/lib/ruby_smb/dcerpc/icpr.rb +84 -0
- data/lib/ruby_smb/dcerpc/ndr.rb +68 -5
- data/lib/ruby_smb/dcerpc/request.rb +4 -0
- data/lib/ruby_smb/dcerpc/samr.rb +1 -1
- data/lib/ruby_smb/dcerpc.rb +245 -12
- data/lib/ruby_smb/error.rb +5 -1
- 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/ntlm.rb +19 -0
- data/lib/ruby_smb/ntlm.rb +37 -0
- data/lib/ruby_smb/peer_info.rb +39 -0
- data/lib/ruby_smb/server/server_client/tree_connect.rb +2 -2
- data/lib/ruby_smb/smb1/pipe.rb +25 -1
- data/lib/ruby_smb/smb2/packet/tree_connect_response.rb +5 -1
- data/lib/ruby_smb/smb2/pipe.rb +25 -0
- 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/dcerpc/icpr/cert_server_request_request_spec.rb +64 -0
- data/spec/lib/ruby_smb/dcerpc/icpr/cert_server_request_response_spec.rb +71 -0
- data/spec/lib/ruby_smb/dcerpc/icpr/cert_trans_blob_spec.rb +33 -0
- data/spec/lib/ruby_smb/dcerpc_spec.rb +2 -1
- data.tar.gz.sig +0 -0
- metadata +14 -2
- metadata.gz.sig +0 -0
data/lib/ruby_smb/dcerpc/ndr.rb
CHANGED
@@ -247,12 +247,22 @@ module RubySMB::Dcerpc::Ndr
|
|
247
247
|
|
248
248
|
def do_read(io)
|
249
249
|
if is_a?(ConfPlugin) && should_process_max_count?
|
250
|
-
|
250
|
+
max_count = io.readbytes(4).unpack1('L<')
|
251
|
+
BinData.trace_message do |tracer|
|
252
|
+
tracer.trace_obj("#{debug_name}.max_count", max_count.to_s)
|
253
|
+
end
|
254
|
+
set_max_count(max_count)
|
251
255
|
end
|
252
256
|
|
253
257
|
if is_a?(VarPlugin)
|
254
258
|
@offset = io.readbytes(4).unpack('L<').first
|
259
|
+
BinData.trace_message do |tracer|
|
260
|
+
tracer.trace_obj("#{debug_name}.offset", @offset.to_s)
|
261
|
+
end
|
255
262
|
@actual_count = @read_until_index = io.readbytes(4).unpack('L<').first
|
263
|
+
BinData.trace_message do |tracer|
|
264
|
+
tracer.trace_obj("#{debug_name}.actual_count", @actual_count.to_s)
|
265
|
+
end
|
256
266
|
end
|
257
267
|
|
258
268
|
if has_elements_to_read?
|
@@ -523,7 +533,11 @@ module RubySMB::Dcerpc::Ndr
|
|
523
533
|
|
524
534
|
def do_read(io)
|
525
535
|
if should_process_max_count?
|
526
|
-
|
536
|
+
max_count = io.readbytes(4).unpack1('L<')
|
537
|
+
BinData.trace_message do |tracer|
|
538
|
+
tracer.trace_obj("#{debug_name}.max_count", max_count.to_s)
|
539
|
+
end
|
540
|
+
set_max_count(max_count)
|
527
541
|
end
|
528
542
|
super
|
529
543
|
end
|
@@ -582,7 +596,13 @@ module RubySMB::Dcerpc::Ndr
|
|
582
596
|
|
583
597
|
def do_read(io)
|
584
598
|
@offset = io.readbytes(4).unpack('L<').first
|
599
|
+
BinData.trace_message do |tracer|
|
600
|
+
tracer.trace_obj("#{debug_name}.offset", @offset.to_s)
|
601
|
+
end
|
585
602
|
@actual_count = io.readbytes(4).unpack('L<').first
|
603
|
+
BinData.trace_message do |tracer|
|
604
|
+
tracer.trace_obj("#{debug_name}.actual_count", @actual_count.to_s)
|
605
|
+
end
|
586
606
|
super if @actual_count > 0
|
587
607
|
end
|
588
608
|
|
@@ -702,7 +722,11 @@ module RubySMB::Dcerpc::Ndr
|
|
702
722
|
|
703
723
|
def do_read(io)
|
704
724
|
if should_process_max_count?
|
705
|
-
|
725
|
+
max_count = io.readbytes(4).unpack1('L<')
|
726
|
+
BinData.trace_message do |tracer|
|
727
|
+
tracer.trace_obj("#{debug_name}.max_count", max_count.to_s)
|
728
|
+
end
|
729
|
+
set_max_count(max_count)
|
706
730
|
|
707
731
|
# Align the structure according to the alignment rules for the structure
|
708
732
|
if respond_to?(:referent_bytes_align)
|
@@ -1034,6 +1058,9 @@ module RubySMB::Dcerpc::Ndr
|
|
1034
1058
|
end
|
1035
1059
|
else
|
1036
1060
|
@ref_id = io.readbytes(4).unpack('L<').first
|
1061
|
+
BinData.trace_message do |tracer|
|
1062
|
+
tracer.trace_obj("#{debug_name}.ref_id", @ref_id.to_s)
|
1063
|
+
end
|
1037
1064
|
parent_obj = nil
|
1038
1065
|
if parent&.is_a?(ConstructedTypePlugin)
|
1039
1066
|
parent_obj = parent.get_top_level_constructed_type
|
@@ -1208,14 +1235,50 @@ module RubySMB::Dcerpc::Ndr
|
|
1208
1235
|
extend PointerClassPlugin
|
1209
1236
|
end
|
1210
1237
|
|
1238
|
+
|
1239
|
+
# ArrayPtr definitions:
|
1240
|
+
# If the type is Ndr*ArrayPtr, it's a ConfVarArray (Uni-dimensional Conformant-varying Array)
|
1241
|
+
# If the type is Ndr*ConfArrayPtr, it's a ConfArray (Uni-dimensional Conformant Array)
|
1211
1242
|
class NdrByteArrayPtr < NdrConfVarArray
|
1212
1243
|
default_parameters type: :ndr_uint8
|
1213
1244
|
extend PointerClassPlugin
|
1245
|
+
|
1246
|
+
def assign(val)
|
1247
|
+
val = val.bytes if val.is_a?(String)
|
1248
|
+
super(val.to_ary)
|
1249
|
+
end
|
1214
1250
|
end
|
1215
1251
|
|
1216
|
-
class
|
1217
|
-
default_parameters type: :
|
1252
|
+
class NdrByteConfArrayPtr < NdrConfArray
|
1253
|
+
default_parameters type: :ndr_uint8
|
1218
1254
|
extend PointerClassPlugin
|
1255
|
+
|
1256
|
+
def assign(val)
|
1257
|
+
val = val.bytes if val.is_a?(String)
|
1258
|
+
super(val.to_ary)
|
1259
|
+
end
|
1260
|
+
end
|
1261
|
+
|
1262
|
+
%i[ Uint8 Uint16 Uint32 Uint64 ].each do |klass|
|
1263
|
+
new_klass_name = "Ndr#{klass}ArrayPtr"
|
1264
|
+
unless self.const_defined?(new_klass_name)
|
1265
|
+
new_klass = Class.new(NdrConfVarArray) do
|
1266
|
+
default_parameters type: "ndr_#{klass}".downcase.to_sym
|
1267
|
+
extend PointerClassPlugin
|
1268
|
+
end
|
1269
|
+
self.const_set(new_klass_name, new_klass)
|
1270
|
+
BinData::RegisteredClasses.register(new_klass_name, new_klass)
|
1271
|
+
end
|
1272
|
+
|
1273
|
+
new_klass_name = "Ndr#{klass}ConfArrayPtr"
|
1274
|
+
unless self.const_defined?(new_klass_name)
|
1275
|
+
new_klass = Class.new(NdrConfArray) do
|
1276
|
+
default_parameters type: "ndr_#{klass}".downcase.to_sym
|
1277
|
+
extend PointerClassPlugin
|
1278
|
+
end
|
1279
|
+
self.const_set(new_klass_name, new_klass)
|
1280
|
+
BinData::RegisteredClasses.register(new_klass_name, new_klass)
|
1281
|
+
end
|
1219
1282
|
end
|
1220
1283
|
|
1221
1284
|
class NdrFileTimePtr < NdrFileTime
|
@@ -97,6 +97,10 @@ module RubySMB
|
|
97
97
|
netr_dfs_remove_std_root_request Dfsnm::NETR_DFS_REMOVE_STD_ROOT
|
98
98
|
string :default
|
99
99
|
end
|
100
|
+
choice 'Icpr', selection: -> { opnum } do
|
101
|
+
cert_server_request_request Icpr::CERT_SERVER_REQUEST
|
102
|
+
string :default
|
103
|
+
end
|
100
104
|
string :default
|
101
105
|
end
|
102
106
|
string :auth_pad,
|
data/lib/ruby_smb/dcerpc/samr.rb
CHANGED
@@ -320,7 +320,7 @@ module RubySMB
|
|
320
320
|
|
321
321
|
def self.encrypt_password(password, key)
|
322
322
|
# see: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-samr/5fe3c4c4-e71b-440d-b2fd-8448bfaf6e04
|
323
|
-
password =
|
323
|
+
password = RubySMB::Utils.safe_encode(password, 'UTF-16LE').force_encoding('ASCII-8bit')
|
324
324
|
buffer = password.rjust(512, "\x00") + [ password.length ].pack('V')
|
325
325
|
salt = SecureRandom.random_bytes(16)
|
326
326
|
key = OpenSSL::Digest::MD5.new(salt + key).digest
|
data/lib/ruby_smb/dcerpc.rb
CHANGED
@@ -47,6 +47,7 @@ module RubySMB
|
|
47
47
|
require 'ruby_smb/dcerpc/drsr'
|
48
48
|
require 'ruby_smb/dcerpc/sec_trailer'
|
49
49
|
require 'ruby_smb/dcerpc/dfsnm'
|
50
|
+
require 'ruby_smb/dcerpc/icpr'
|
50
51
|
require 'ruby_smb/dcerpc/request'
|
51
52
|
require 'ruby_smb/dcerpc/response'
|
52
53
|
require 'ruby_smb/dcerpc/rpc_auth3'
|
@@ -55,25 +56,50 @@ module RubySMB
|
|
55
56
|
require 'ruby_smb/dcerpc/print_system'
|
56
57
|
require 'ruby_smb/dcerpc/encrypting_file_system'
|
57
58
|
|
58
|
-
# Bind to the remote server interface endpoint.
|
59
|
+
# Bind to the remote server interface endpoint. It takes care of adding
|
60
|
+
# the necessary authentication verifier if `:auth_level` is set to
|
61
|
+
# anything different than RPC_C_AUTHN_LEVEL_NONE
|
59
62
|
#
|
60
|
-
# @param
|
63
|
+
# @param [Hash] options
|
64
|
+
# @option options [Module] :endpoint the endpoint to bind to. This must be a Dcerpc
|
65
|
+
# class with UUID, VER_MAJOR and VER_MINOR constants defined.
|
66
|
+
# @option options [Integer] :auth_level the authentication level
|
67
|
+
# @option options [Integer] :auth_type the authentication type
|
61
68
|
# @return [BindAck] the BindAck response packet
|
62
69
|
# @raise [Error::InvalidPacket] if an invalid packet is received
|
63
70
|
# @raise [Error::BindError] if the response is not a BindAck packet or if the Bind result code is not ACCEPTANCE
|
71
|
+
# @raise [ArgumentError] if `:auth_type` is unknown
|
72
|
+
# @raise [NotImplementedError] if `:auth_type` is not implemented (yet)
|
64
73
|
def bind(options={})
|
74
|
+
@call_id ||= 1
|
65
75
|
bind_req = Bind.new(options)
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
76
|
+
bind_req.pdu_header.call_id = @call_id
|
77
|
+
|
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])
|
73
94
|
end
|
74
|
-
|
75
|
-
|
95
|
+
|
96
|
+
send_packet(bind_req)
|
97
|
+
begin
|
98
|
+
dcerpc_response = recv_struct(BindAck)
|
99
|
+
rescue Error::InvalidPacket
|
100
|
+
raise Error::BindError # raise the more context-specific BindError
|
76
101
|
end
|
102
|
+
# TODO: see if BindNack response should be handled too
|
77
103
|
|
78
104
|
res_list = dcerpc_response.p_result_list
|
79
105
|
if res_list.n_results == 0 ||
|
@@ -81,9 +107,216 @@ module RubySMB
|
|
81
107
|
raise Error::BindError,
|
82
108
|
"Bind Failed (Result: #{res_list.p_results[0].result}, Reason: #{res_list.p_results[0].reason})"
|
83
109
|
end
|
84
|
-
|
110
|
+
self.max_buffer_size = dcerpc_response.max_xmit_frag
|
111
|
+
@call_id = dcerpc_response.pdu_header.call_id
|
112
|
+
|
113
|
+
if options[:auth_level] && options[:auth_level] != RPC_C_AUTHN_LEVEL_NONE
|
114
|
+
# The number of legs needed to build the security context is defined
|
115
|
+
# 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
|
124
|
+
end
|
125
|
+
|
85
126
|
dcerpc_response
|
86
127
|
end
|
87
128
|
|
129
|
+
def max_buffer_size=(value)
|
130
|
+
@tree.client.max_buffer_size = value
|
131
|
+
end
|
132
|
+
|
133
|
+
# Receive a packet from the remote host and parse it according to `struct`
|
134
|
+
#
|
135
|
+
# @param struct [Class] the structure class to parse the response with
|
136
|
+
def recv_struct(struct)
|
137
|
+
raw_response = read
|
138
|
+
begin
|
139
|
+
response = struct.read(raw_response)
|
140
|
+
rescue IOError
|
141
|
+
raise Error::InvalidPacket, "Error reading the #{struct} response"
|
142
|
+
end
|
143
|
+
unless response.pdu_header.ptype == struct::PTYPE
|
144
|
+
raise Error::InvalidPacket, "Not a #{struct} packet"
|
145
|
+
end
|
146
|
+
|
147
|
+
response
|
148
|
+
end
|
149
|
+
|
150
|
+
# Send a packet to the remote host
|
151
|
+
#
|
152
|
+
# @param packet [BinData::Record] the packet to send
|
153
|
+
# @raise [Error::CommunicationError] if socket-related error occurs
|
154
|
+
def send_packet(packet)
|
155
|
+
write(data: packet.to_binary_s)
|
156
|
+
nil
|
157
|
+
end
|
158
|
+
|
159
|
+
# Add the authentication verifier to a Request packet. This includes a
|
160
|
+
# sec trailer and the signature of the packet. This also encrypts the
|
161
|
+
# Request stub if privacy is required (`:auth_level` option is
|
162
|
+
# RPC_C_AUTHN_LEVEL_PKT_PRIVACY).
|
163
|
+
#
|
164
|
+
# @param dcerpc_req [Request] the Request packet to be updated
|
165
|
+
# @param opts [Hash] the authenticaiton options: `:auth_type` and `:auth_level`
|
166
|
+
# @raise [NotImplementedError] if `:auth_type` is not implemented (yet)
|
167
|
+
# @raise [ArgumentError] if `:auth_type` is unknown
|
168
|
+
def set_integrity_privacy(dcerpc_req, auth_level:, auth_type:)
|
169
|
+
dcerpc_req.sec_trailer = {
|
170
|
+
auth_type: auth_type,
|
171
|
+
auth_level: auth_level,
|
172
|
+
auth_context_id: @ctx_id + @auth_ctx_id_base
|
173
|
+
}
|
174
|
+
dcerpc_req.auth_value = ' ' * 16
|
175
|
+
dcerpc_req.pdu_header.auth_length = 16
|
176
|
+
|
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
|
195
|
+
|
196
|
+
signature = @ntlm_client.session.sign_message(data_to_sign)
|
197
|
+
|
198
|
+
unless encrypted_stub.empty?
|
199
|
+
pad_length = dcerpc_req.sec_trailer.auth_pad_length.to_i
|
200
|
+
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]
|
203
|
+
end
|
204
|
+
dcerpc_req.auth_value = signature
|
205
|
+
dcerpc_req.pdu_header.auth_length = signature.size
|
206
|
+
end
|
207
|
+
|
208
|
+
# Process the security context received in a response. It decrypts the
|
209
|
+
# encrypted stub if `:auth_level` is set to anything different than
|
210
|
+
# RPC_C_AUTHN_LEVEL_PKT_PRIVACY. It also checks the packet signature and
|
211
|
+
# raises an InvalidPacket error if it fails. Note that the exception is
|
212
|
+
# disabled by default and can be enabled with the
|
213
|
+
# `:raise_signature_error` option
|
214
|
+
#
|
215
|
+
# @param dcerpc_response [Response] the Response packet
|
216
|
+
# containing the security context to process
|
217
|
+
# @param opts [Hash] the authenticaiton options: `:auth_type` and
|
218
|
+
# `:auth_level`. To enable errors when signature check fails, set the
|
219
|
+
# `:raise_signature_error` option to true
|
220
|
+
# @raise [NotImplementedError] if `:auth_type` is not implemented (yet)
|
221
|
+
# @raise [Error::CommunicationError] if socket-related error occurs
|
222
|
+
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]
|
242
|
+
end
|
243
|
+
|
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)
|
250
|
+
if raise_signature_error
|
251
|
+
raise Error::InvalidPacket.new(
|
252
|
+
"Wrong packet signature received (set `raise_signature_error` to false to ignore)"
|
253
|
+
)
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
@call_id += 1
|
258
|
+
|
259
|
+
nil
|
260
|
+
end
|
261
|
+
|
262
|
+
# Add the authentication verifier to the packet. This includes a sec
|
263
|
+
# trailer and the actual authentication data.
|
264
|
+
#
|
265
|
+
# @param req [BinData::Record] the request to be updated
|
266
|
+
# @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)
|
270
|
+
req.sec_trailer = {
|
271
|
+
auth_type: auth_type,
|
272
|
+
auth_level: auth_level,
|
273
|
+
auth_context_id: @ctx_id + @auth_ctx_id_base
|
274
|
+
}
|
275
|
+
req.auth_value = auth
|
276
|
+
req.pdu_header.auth_length = auth.length
|
277
|
+
|
278
|
+
nil
|
279
|
+
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
|
88
321
|
end
|
89
322
|
end
|
data/lib/ruby_smb/error.rb
CHANGED
@@ -73,6 +73,10 @@ module RubySMB
|
|
73
73
|
module Mixin
|
74
74
|
attr_reader :status_code
|
75
75
|
|
76
|
+
def status_name
|
77
|
+
@status_code.name
|
78
|
+
end
|
79
|
+
|
76
80
|
private
|
77
81
|
|
78
82
|
def status_code=(status_code)
|
@@ -82,7 +86,7 @@ module RubySMB
|
|
82
86
|
when Integer
|
83
87
|
@status_code = WindowsError::NTStatus.find_by_retval(status_code).first
|
84
88
|
if @status_code.nil?
|
85
|
-
@status_code = WindowsError::ErrorCode.new("0x#{status_code.to_s(16).rjust(8, '0')}", status_code, "Unknown status: 0x#{status_code.to_s(16)}")
|
89
|
+
@status_code = WindowsError::ErrorCode.new("0x#{status_code.to_s(16).rjust(8, '0')}", status_code, "Unknown status: 0x#{status_code.to_s(16).rjust(8, '0')}")
|
86
90
|
end
|
87
91
|
else
|
88
92
|
raise ArgumentError, "Status code must be a WindowsError::ErrorCode or an Integer, got #{status_code.class}"
|
@@ -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,19 @@
|
|
1
|
+
require 'net/ntlm'
|
2
|
+
|
3
|
+
module Custom
|
4
|
+
module NTLM
|
5
|
+
|
6
|
+
def self.prepended(base)
|
7
|
+
base.singleton_class.send(:prepend, ClassMethods)
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
def encode_utf16le(str)
|
12
|
+
str.dup.force_encoding('UTF-8').encode(Encoding::UTF_16LE, Encoding::UTF_8).force_encoding('ASCII-8BIT')
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
Net::NTLM::EncodeUtil.send(:prepend, Custom::NTLM)
|
data/lib/ruby_smb/ntlm.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'ruby_smb/ntlm/custom/ntlm'
|
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
|
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module RubySMB
|
2
|
+
module PeerInfo
|
3
|
+
# Extract and store useful information about the peer/server from the
|
4
|
+
# NTLM Type 2 (challenge) TargetInfo fields.
|
5
|
+
#
|
6
|
+
# @param target_info_str [String] the Target Info string
|
7
|
+
def store_target_info(target_info_str)
|
8
|
+
target_info = Net::NTLM::TargetInfo.new(target_info_str)
|
9
|
+
{
|
10
|
+
Net::NTLM::TargetInfo::MSV_AV_NB_COMPUTER_NAME => :@default_name,
|
11
|
+
Net::NTLM::TargetInfo::MSV_AV_NB_DOMAIN_NAME => :@default_domain,
|
12
|
+
Net::NTLM::TargetInfo::MSV_AV_DNS_COMPUTER_NAME => :@dns_host_name,
|
13
|
+
Net::NTLM::TargetInfo::MSV_AV_DNS_DOMAIN_NAME => :@dns_domain_name,
|
14
|
+
Net::NTLM::TargetInfo::MSV_AV_DNS_TREE_NAME => :@dns_tree_name
|
15
|
+
}.each do |constant, attribute|
|
16
|
+
if target_info.av_pairs[constant]
|
17
|
+
value = target_info.av_pairs[constant].dup
|
18
|
+
value.force_encoding('UTF-16LE')
|
19
|
+
instance_variable_set(attribute, value.encode('UTF-8'))
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Extract the peer/server version number from the NTLM Type 2 (challenge)
|
25
|
+
# Version field.
|
26
|
+
#
|
27
|
+
# @param version [String] the version number as a binary string
|
28
|
+
# @return [String] the formatted version number (<major>.<minor>.<build>)
|
29
|
+
def extract_os_version(version)
|
30
|
+
begin
|
31
|
+
os_version = RubySMB::NTLM::OSVersion.read(version)
|
32
|
+
rescue IOError
|
33
|
+
return ''
|
34
|
+
end
|
35
|
+
|
36
|
+
"#{os_version.major}.#{os_version.minor}.#{os_version.build}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -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?
|