ruby_smb 3.1.7 → 3.2.1
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 +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?
|