CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/lib/rex/proto/ldap.rb
Views: 11655
1
require 'net/ldap'
2
require 'rex/socket'
3
4
#
5
# This file monkeypatches the upstream net/ldap library to add support for the proxies datastore option,
6
# supporting blocking synchronrous reads, and using a Rex Socket to work with Rex's Switchboard functionality
7
# TODO: write a real LDAP client in Rex and migrate all consumers
8
#
9
10
# Update Net::LDAP's initialize and new_connection method to honor a tracking proxies setting
11
class Net::LDAP
12
WhoamiOid = '1.3.6.1.4.1.4203.1.11.3'.freeze
13
14
# fix the definition for ExtendedResponse
15
AsnSyntax[Net::BER::TAG_CLASS[:universal] + Net::BER::ENCODING_TYPE[:constructed] + 107] = :string
16
17
# Reference the old initialize method, and ensure `reload_lib -a` doesn't attempt to refine the method
18
alias_method :_old_initialize, :initialize unless defined?(_old_initialize)
19
20
# Original Source:
21
# https://github.com/ruby-ldap/ruby-net-ldap/blob/95cec3822cd2f60787971e19714f74fd5999595c/lib/net/ldap.rb#L548
22
# Additionally tracks proxies configuration, used when making a new_connection
23
def initialize(args = {})
24
_old_initialize(args)
25
@proxies = args[:proxies]
26
end
27
28
private
29
30
# Original source:
31
# https://github.com/ruby-ldap/ruby-net-ldap/blob/95cec3822cd2f60787971e19714f74fd5999595c/lib/net/ldap.rb#L1321
32
# Updated to include proxies configuration
33
def new_connection
34
connection = Net::LDAP::Connection.new \
35
:host => @host,
36
:port => @port,
37
:hosts => @hosts,
38
:encryption => @encryption,
39
:instrumentation_service => @instrumentation_service,
40
:connect_timeout => @connect_timeout,
41
# New:
42
:proxies => @proxies
43
44
# Force connect to see if there's a connection error
45
connection.socket
46
connection
47
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT => e
48
@result = {
49
:resultCode => 52,
50
:errorMessage => ResultStrings[ResultCodeUnavailable],
51
}
52
raise e
53
end
54
end
55
56
# Update Net::LDAP's initialize and new_connection method to honor a tracking proxies setting
57
class Net::LDAP::Connection # :nodoc:
58
module SynchronousRead
59
# Read `length` bytes of data from the LDAP connection socket and
60
# return this data as a string.
61
#
62
# @param length [Integer] Length of the data to be read from the LDAP connection socket.
63
# @param _opts [Hash] Unused
64
#
65
# @return [String] A string containing the data read from the LDAP connection socket.
66
def read(length = nil, _opts = {})
67
data = ''
68
loop do
69
chunk = super(length - data.length)
70
if chunk.nil?
71
return data == '' ? nil : data
72
end
73
74
data << chunk
75
break if data.length == length
76
end
77
78
data
79
end
80
end
81
82
# Allow wrapping the socket to read and write SASL data
83
module SocketSaslIO
84
include Rex::Proto::Sasl
85
86
# This seems hacky, but we're just fitting in with how net-ldap does it
87
def get_ber_length(data)
88
n = data[0].ord
89
90
if n <= 0x7f
91
[n, 1]
92
elsif n == 0x80
93
raise Net::BER::BerError,
94
'Indeterminite BER content length not implemented.'
95
elsif n == 0xff
96
raise Net::BER::BerError, 'Invalid BER length 0xFF detected.'
97
else
98
v = 0
99
extra_length = n & 0x7f
100
data[1,n & 0x7f].each_byte do |b|
101
v = (v << 8) + b
102
end
103
104
[v, extra_length + 1]
105
end
106
end
107
108
def read_ber(syntax = nil)
109
unless @wrap_read.nil?
110
if ber_cache.any?
111
return ber_cache.shift
112
end
113
# SASL buffer length
114
length_bytes = read(4)
115
# The implementation in net-ldap returns nil if it doesn't read any data
116
return nil unless length_bytes
117
118
length = length_bytes.unpack('N')[0]
119
120
# Now read the actual data
121
data = read(length)
122
123
# Decrypt it
124
plaintext = @wrap_read.call(data)
125
126
while plaintext.length > 0
127
id = plaintext[0].ord
128
ber_length, used_chars = get_ber_length(plaintext[1,plaintext.length])
129
plaintext = plaintext[1+used_chars, plaintext.length]
130
131
# We may receive several objects in the one packet
132
# Ideally we'd refactor all of ruby-net-ldap to use
133
# yields for this, but it's all a bit messy. So instead,
134
# just store them all and return the next one each time
135
# we're asked.
136
ber_cache.append(parse_ber_object(syntax, id, plaintext[0,ber_length]))
137
138
plaintext = plaintext[ber_length,plaintext.length]
139
end
140
141
return ber_cache.shift
142
else
143
super(syntax)
144
end
145
end
146
147
def write(data)
148
unless @wrap_write.nil?
149
# Encrypt it
150
data = @wrap_write.call(data)
151
152
# Prepend the length bytes
153
data = wrap_sasl(data)
154
end
155
156
super(data)
157
end
158
159
def setup(wrap_read, wrap_write)
160
@wrap_read = wrap_read
161
@wrap_write = wrap_write
162
@ber_cache = []
163
end
164
165
private
166
167
attr_accessor :wrap_read
168
attr_accessor :wrap_write
169
attr_accessor :ber_cache
170
end
171
172
module ConnectionSaslIO
173
# Provide the encryption wrapper for the caller to set up
174
def wrap_read_write(wrap_read, wrap_write)
175
@conn.extend(SocketSaslIO)
176
@conn.setup(wrap_read, wrap_write)
177
end
178
end
179
180
# Initialize the LDAP connection using Rex::Socket::TCP,
181
# and optionally set up encryption on the connection if configured.
182
#
183
# @param server [Hash] Hash of the options needed to set
184
# up the Rex::Socket::TCP socket for the LDAP connection.
185
# @see http://gemdocs.org/gems/rex-socket/0.1.43/Rex/Socket.html#create-class_method
186
# @see http://gemdocs.org/gems/rex-socket/0.1.43/Rex/Socket.html#create_param-class_method
187
# @see http://gemdocs.org/gems/rex-socket/0.1.43/Rex/Socket/Parameters.html#from_hash-class_method
188
def initialize(server)
189
begin
190
@conn = Rex::Socket::Tcp.create(
191
'PeerHost' => server[:host],
192
'PeerPort' => server[:port],
193
'Proxies' => server[:proxies],
194
'Timeout' => server[:connect_timeout]
195
)
196
@conn.extend(SynchronousRead)
197
198
# Set up read/write wrapping
199
self.extend(ConnectionSaslIO)
200
rescue SocketError
201
raise Net::LDAP::LdapError, 'No such address or other socket error.'
202
rescue Errno::ECONNREFUSED
203
raise Net::LDAP::LdapError, "Server #{server[:host]} refused connection on port #{server[:port]}."
204
end
205
206
if server[:encryption]
207
setup_encryption server[:encryption]
208
end
209
210
yield self if block_given?
211
end
212
213
# Monkeypatch upstream library for now to support :controls
214
# hash option in `args` so that we can provide controls within
215
# searches. Needed so we can specify the LDAP_SERVER_SD_FLAGS_OID
216
# flag for searches to prevent getting the SACL when querying for
217
# ntSecurityDescriptor, as this is retrieved by default and non-admin
218
# users are not allowed to retrieve SACLs for objects. Therefore by
219
# adjusting the search to not retrieve SACLs, non-admin users can still
220
# retrieve information about the security of objects without violating this rule.
221
#
222
# @see https://github.com/rapid7/metasploit-framework/issues/17324
223
# @see https://github.com/ruby-ldap/ruby-net-ldap/pull/411
224
#
225
# @param [Hash] args A hash of the arguments to be utilized by the search operation.
226
# @return [Net::LDAP::PDU] A Protocol Data Unit (PDU) object, represented by
227
# the Net::LDAP::PDU class, containing the results of the search operation.
228
def search(args = nil)
229
args ||= {}
230
231
# filtering, scoping, search base
232
# filter: https://tools.ietf.org/html/rfc4511#section-4.5.1.7
233
# base: https://tools.ietf.org/html/rfc4511#section-4.5.1.1
234
# scope: https://tools.ietf.org/html/rfc4511#section-4.5.1.2
235
filter = args[:filter] || Net::LDAP::Filter.eq("objectClass", "*")
236
base = args[:base]
237
scope = args[:scope] || Net::LDAP::SearchScope_WholeSubtree
238
239
# attr handling
240
# attrs: https://tools.ietf.org/html/rfc4511#section-4.5.1.8
241
# attrs_only: https://tools.ietf.org/html/rfc4511#section-4.5.1.6
242
attrs = Array(args[:attributes])
243
attrs_only = args[:attributes_only] == true
244
245
# references
246
# refs: https://tools.ietf.org/html/rfc4511#section-4.5.3
247
# deref: https://tools.ietf.org/html/rfc4511#section-4.5.1.3
248
refs = args[:return_referrals] == true
249
deref = args[:deref] || Net::LDAP::DerefAliases_Never
250
251
# limiting, paging, sorting
252
# size: https://tools.ietf.org/html/rfc4511#section-4.5.1.4
253
# time: https://tools.ietf.org/html/rfc4511#section-4.5.1.5
254
size = args[:size].to_i
255
time = args[:time].to_i
256
paged = args[:paged_searches_supported]
257
sort = args.fetch(:sort_controls, false)
258
259
# arg validation
260
raise ArgumentError, "search base is required" unless base
261
raise ArgumentError, "invalid search-size" unless size >= 0
262
raise ArgumentError, "invalid search scope" unless Net::LDAP::SearchScopes.include?(scope)
263
raise ArgumentError, "invalid alias dereferencing value" unless Net::LDAP::DerefAliasesArray.include?(deref)
264
265
# arg transforms
266
filter = Net::LDAP::Filter.construct(filter) if filter.is_a?(String)
267
ber_attrs = attrs.map { |attr| attr.to_s.to_ber }
268
ber_sort = encode_sort_controls(sort)
269
270
# An interesting value for the size limit would be close to A/D's
271
# built-in page limit of 1000 records, but openLDAP newer than version
272
# 2.2.0 chokes on anything bigger than 126. You get a silent error that
273
# is easily visible by running slapd in debug mode. Go figure.
274
#
275
# Changed this around 06Sep06 to support a caller-specified search-size
276
# limit. Because we ALWAYS do paged searches, we have to work around the
277
# problem that it's not legal to specify a "normal" sizelimit (in the
278
# body of the search request) that is larger than the page size we're
279
# requesting. Unfortunately, I have the feeling that this will break
280
# with LDAP servers that don't support paged searches!!!
281
#
282
# (Because we pass zero as the sizelimit on search rounds when the
283
# remaining limit is larger than our max page size of 126. In these
284
# cases, I think the caller's search limit will be ignored!)
285
#
286
# CONFIRMED: This code doesn't work on LDAPs that don't support paged
287
# searches when the size limit is larger than 126. We're going to have
288
# to do a root-DSE record search and not do a paged search if the LDAP
289
# doesn't support it. Yuck.
290
rfc2696_cookie = [126, ""]
291
result_pdu = nil
292
n_results = 0
293
294
message_id = next_msgid
295
296
instrument "search.net_ldap_connection",
297
message_id: message_id,
298
filter: filter,
299
base: base,
300
scope: scope,
301
size: size,
302
time: time,
303
sort: sort,
304
referrals: refs,
305
deref: deref,
306
attributes: attrs do |payload|
307
loop do
308
# should collect this into a private helper to clarify the structure
309
query_limit = 0
310
if size > 0
311
query_limit = if paged
312
(((size - n_results) < 126) ? (size - n_results) : 0)
313
else
314
size
315
end
316
end
317
318
request = [
319
base.to_ber,
320
scope.to_ber_enumerated,
321
deref.to_ber_enumerated,
322
query_limit.to_ber, # size limit
323
time.to_ber,
324
attrs_only.to_ber,
325
filter.to_ber,
326
ber_attrs.to_ber_sequence,
327
].to_ber_appsequence(Net::LDAP::PDU::SearchRequest)
328
329
# rfc2696_cookie sometimes contains binary data from Microsoft Active Directory
330
# this breaks when calling to_ber. (Can't force binary data to UTF-8)
331
# we have to disable paging (even though server supports it) to get around this...
332
333
user_controls = args.fetch(:controls, [])
334
controls = []
335
controls <<
336
[
337
Net::LDAP::LDAPControls::PAGED_RESULTS.to_ber,
338
# Criticality MUST be false to interoperate with normal LDAPs.
339
false.to_ber,
340
rfc2696_cookie.map(&:to_ber).to_ber_sequence.to_s.to_ber,
341
].to_ber_sequence if paged
342
controls << ber_sort if ber_sort
343
if controls.empty? && user_controls.empty?
344
controls = nil
345
else
346
controls += user_controls
347
controls = controls.to_ber_contextspecific(0)
348
end
349
350
write(request, controls, message_id)
351
352
result_pdu = nil
353
controls = []
354
355
while pdu = queued_read(message_id)
356
case pdu.app_tag
357
when Net::LDAP::PDU::SearchReturnedData
358
n_results += 1
359
yield pdu.search_entry if block_given?
360
when Net::LDAP::PDU::SearchResultReferral
361
if refs
362
if block_given?
363
se = Net::LDAP::Entry.new
364
se[:search_referrals] = (pdu.search_referrals || [])
365
yield se
366
end
367
end
368
when Net::LDAP::PDU::SearchResult
369
result_pdu = pdu
370
controls = pdu.result_controls
371
if refs && pdu.result_code == Net::LDAP::ResultCodeReferral
372
if block_given?
373
se = Net::LDAP::Entry.new
374
se[:search_referrals] = (pdu.search_referrals || [])
375
yield se
376
end
377
end
378
break
379
else
380
raise Net::LDAP::ResponseTypeInvalidError, "invalid response-type in search: #{pdu.app_tag}"
381
end
382
end
383
384
if result_pdu.nil?
385
raise Net::LDAP::ResponseMissingOrInvalidError, "response missing"
386
end
387
388
# count number of pages of results
389
payload[:page_count] ||= 0
390
payload[:page_count] += 1
391
392
# When we get here, we have seen a type-5 response. If there is no
393
# error AND there is an RFC-2696 cookie, then query again for the next
394
# page of results. If not, we're done. Don't screw this up or we'll
395
# break every search we do.
396
#
397
# Noticed 02Sep06, look at the read_ber call in this loop, shouldn't
398
# that have a parameter of AsnSyntax? Does this just accidentally
399
# work? According to RFC-2696, the value expected in this position is
400
# of type OCTET STRING, covered in the default syntax supported by
401
# read_ber, so I guess we're ok.
402
more_pages = false
403
if result_pdu.result_code == Net::LDAP::ResultCodeSuccess and controls
404
controls.each do |c|
405
if c.oid == Net::LDAP::LDAPControls::PAGED_RESULTS
406
# just in case some bogus server sends us more than 1 of these.
407
more_pages = false
408
if c.value and c.value.length > 0
409
cookie = c.value.read_ber[1]
410
if cookie and cookie.length > 0
411
rfc2696_cookie[1] = cookie
412
more_pages = true
413
end
414
end
415
end
416
end
417
end
418
419
break unless more_pages
420
end # loop
421
422
# track total result count
423
payload[:result_count] = n_results
424
425
result_pdu || OpenStruct.new(:status => :failure, :result_code => Net::LDAP::ResultCodeOperationsError, :message => "Invalid search")
426
end # instrument
427
ensure
428
429
# clean up message queue for this search
430
messages = message_queue.delete(message_id)
431
432
# in the exceptional case some messages were *not* consumed from the queue,
433
# instrument the event but do not fail.
434
if !messages.nil? && !messages.empty?
435
instrument "search_messages_unread.net_ldap_connection",
436
message_id: message_id, messages: messages
437
end
438
end
439
440
# Another monkeypatch to support :controls
441
def modify(args)
442
modify_dn = args[:dn] or raise "Unable to modify empty DN"
443
ops = self.class.modify_ops args[:operations]
444
445
message_id = next_msgid
446
request = [
447
modify_dn.to_ber,
448
ops.to_ber_sequence,
449
].to_ber_appsequence(Net::LDAP::PDU::ModifyRequest)
450
451
controls = args.fetch(:controls, nil)
452
unless controls.nil?
453
controls = controls.to_ber_contextspecific(0)
454
end
455
456
write(request, controls, message_id)
457
pdu = queued_read(message_id)
458
459
if !pdu || pdu.app_tag != Net::LDAP::PDU::ModifyResponse
460
raise Net::LDAP::ResponseMissingOrInvalidError, "response missing or invalid"
461
end
462
463
pdu
464
end
465
466
# Monkeypatch upstream library to support the extended Whoami request. Delete
467
# this after https://github.com/ruby-ldap/ruby-net-ldap/pull/425 is landed.
468
# This is not the only occurrence of a patch for this functionality.
469
def ldapwhoami
470
ext_seq = [Net::LDAP::WhoamiOid.to_ber_contextspecific(0)]
471
request = ext_seq.to_ber_appsequence(Net::LDAP::PDU::ExtendedRequest)
472
473
message_id = next_msgid
474
475
write(request, nil, message_id)
476
pdu = queued_read(message_id)
477
478
if !pdu || pdu.app_tag != Net::LDAP::PDU::ExtendedResponse
479
raise Net::LDAP::ResponseMissingOrInvalidError, "response missing or invalid"
480
end
481
482
pdu
483
end
484
end
485
486
class Net::LDAP::PDU
487
# Monkeypatch upstream library to support the extended Whoami request. Delete
488
# this after https://github.com/ruby-ldap/ruby-net-ldap/pull/425 is landed.
489
# This is not the only occurrence of a patch for this functionality.
490
def parse_extended_response(sequence)
491
sequence.length.between?(3, 5) or raise Net::LDAP::PDU::Error, "Invalid LDAP result length."
492
@ldap_result = {
493
:resultCode => sequence[0],
494
:matchedDN => sequence[1],
495
:errorMessage => sequence[2],
496
}
497
@extended_response = sequence.last
498
end
499
end
500
501
module Rex
502
module Proto
503
module LDAP
504
end
505
end
506
end
507
508