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