Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/lib/rex/proto/ldap.rb
Views: 11655
require 'net/ldap'1require 'rex/socket'23#4# This file monkeypatches the upstream net/ldap library to add support for the proxies datastore option,5# supporting blocking synchronrous reads, and using a Rex Socket to work with Rex's Switchboard functionality6# TODO: write a real LDAP client in Rex and migrate all consumers7#89# Update Net::LDAP's initialize and new_connection method to honor a tracking proxies setting10class Net::LDAP11WhoamiOid = '1.3.6.1.4.1.4203.1.11.3'.freeze1213# fix the definition for ExtendedResponse14AsnSyntax[Net::BER::TAG_CLASS[:universal] + Net::BER::ENCODING_TYPE[:constructed] + 107] = :string1516# Reference the old initialize method, and ensure `reload_lib -a` doesn't attempt to refine the method17alias_method :_old_initialize, :initialize unless defined?(_old_initialize)1819# Original Source:20# https://github.com/ruby-ldap/ruby-net-ldap/blob/95cec3822cd2f60787971e19714f74fd5999595c/lib/net/ldap.rb#L54821# Additionally tracks proxies configuration, used when making a new_connection22def initialize(args = {})23_old_initialize(args)24@proxies = args[:proxies]25end2627private2829# Original source:30# https://github.com/ruby-ldap/ruby-net-ldap/blob/95cec3822cd2f60787971e19714f74fd5999595c/lib/net/ldap.rb#L132131# Updated to include proxies configuration32def new_connection33connection = Net::LDAP::Connection.new \34:host => @host,35:port => @port,36:hosts => @hosts,37:encryption => @encryption,38:instrumentation_service => @instrumentation_service,39:connect_timeout => @connect_timeout,40# New:41:proxies => @proxies4243# Force connect to see if there's a connection error44connection.socket45connection46rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT => e47@result = {48:resultCode => 52,49:errorMessage => ResultStrings[ResultCodeUnavailable],50}51raise e52end53end5455# Update Net::LDAP's initialize and new_connection method to honor a tracking proxies setting56class Net::LDAP::Connection # :nodoc:57module SynchronousRead58# Read `length` bytes of data from the LDAP connection socket and59# return this data as a string.60#61# @param length [Integer] Length of the data to be read from the LDAP connection socket.62# @param _opts [Hash] Unused63#64# @return [String] A string containing the data read from the LDAP connection socket.65def read(length = nil, _opts = {})66data = ''67loop do68chunk = super(length - data.length)69if chunk.nil?70return data == '' ? nil : data71end7273data << chunk74break if data.length == length75end7677data78end79end8081# Allow wrapping the socket to read and write SASL data82module SocketSaslIO83include Rex::Proto::Sasl8485# This seems hacky, but we're just fitting in with how net-ldap does it86def get_ber_length(data)87n = data[0].ord8889if n <= 0x7f90[n, 1]91elsif n == 0x8092raise Net::BER::BerError,93'Indeterminite BER content length not implemented.'94elsif n == 0xff95raise Net::BER::BerError, 'Invalid BER length 0xFF detected.'96else97v = 098extra_length = n & 0x7f99data[1,n & 0x7f].each_byte do |b|100v = (v << 8) + b101end102103[v, extra_length + 1]104end105end106107def read_ber(syntax = nil)108unless @wrap_read.nil?109if ber_cache.any?110return ber_cache.shift111end112# SASL buffer length113length_bytes = read(4)114# The implementation in net-ldap returns nil if it doesn't read any data115return nil unless length_bytes116117length = length_bytes.unpack('N')[0]118119# Now read the actual data120data = read(length)121122# Decrypt it123plaintext = @wrap_read.call(data)124125while plaintext.length > 0126id = plaintext[0].ord127ber_length, used_chars = get_ber_length(plaintext[1,plaintext.length])128plaintext = plaintext[1+used_chars, plaintext.length]129130# We may receive several objects in the one packet131# Ideally we'd refactor all of ruby-net-ldap to use132# yields for this, but it's all a bit messy. So instead,133# just store them all and return the next one each time134# we're asked.135ber_cache.append(parse_ber_object(syntax, id, plaintext[0,ber_length]))136137plaintext = plaintext[ber_length,plaintext.length]138end139140return ber_cache.shift141else142super(syntax)143end144end145146def write(data)147unless @wrap_write.nil?148# Encrypt it149data = @wrap_write.call(data)150151# Prepend the length bytes152data = wrap_sasl(data)153end154155super(data)156end157158def setup(wrap_read, wrap_write)159@wrap_read = wrap_read160@wrap_write = wrap_write161@ber_cache = []162end163164private165166attr_accessor :wrap_read167attr_accessor :wrap_write168attr_accessor :ber_cache169end170171module ConnectionSaslIO172# Provide the encryption wrapper for the caller to set up173def wrap_read_write(wrap_read, wrap_write)174@conn.extend(SocketSaslIO)175@conn.setup(wrap_read, wrap_write)176end177end178179# Initialize the LDAP connection using Rex::Socket::TCP,180# and optionally set up encryption on the connection if configured.181#182# @param server [Hash] Hash of the options needed to set183# up the Rex::Socket::TCP socket for the LDAP connection.184# @see http://gemdocs.org/gems/rex-socket/0.1.43/Rex/Socket.html#create-class_method185# @see http://gemdocs.org/gems/rex-socket/0.1.43/Rex/Socket.html#create_param-class_method186# @see http://gemdocs.org/gems/rex-socket/0.1.43/Rex/Socket/Parameters.html#from_hash-class_method187def initialize(server)188begin189@conn = Rex::Socket::Tcp.create(190'PeerHost' => server[:host],191'PeerPort' => server[:port],192'Proxies' => server[:proxies],193'Timeout' => server[:connect_timeout]194)195@conn.extend(SynchronousRead)196197# Set up read/write wrapping198self.extend(ConnectionSaslIO)199rescue SocketError200raise Net::LDAP::LdapError, 'No such address or other socket error.'201rescue Errno::ECONNREFUSED202raise Net::LDAP::LdapError, "Server #{server[:host]} refused connection on port #{server[:port]}."203end204205if server[:encryption]206setup_encryption server[:encryption]207end208209yield self if block_given?210end211212# Monkeypatch upstream library for now to support :controls213# hash option in `args` so that we can provide controls within214# searches. Needed so we can specify the LDAP_SERVER_SD_FLAGS_OID215# flag for searches to prevent getting the SACL when querying for216# ntSecurityDescriptor, as this is retrieved by default and non-admin217# users are not allowed to retrieve SACLs for objects. Therefore by218# adjusting the search to not retrieve SACLs, non-admin users can still219# retrieve information about the security of objects without violating this rule.220#221# @see https://github.com/rapid7/metasploit-framework/issues/17324222# @see https://github.com/ruby-ldap/ruby-net-ldap/pull/411223#224# @param [Hash] args A hash of the arguments to be utilized by the search operation.225# @return [Net::LDAP::PDU] A Protocol Data Unit (PDU) object, represented by226# the Net::LDAP::PDU class, containing the results of the search operation.227def search(args = nil)228args ||= {}229230# filtering, scoping, search base231# filter: https://tools.ietf.org/html/rfc4511#section-4.5.1.7232# base: https://tools.ietf.org/html/rfc4511#section-4.5.1.1233# scope: https://tools.ietf.org/html/rfc4511#section-4.5.1.2234filter = args[:filter] || Net::LDAP::Filter.eq("objectClass", "*")235base = args[:base]236scope = args[:scope] || Net::LDAP::SearchScope_WholeSubtree237238# attr handling239# attrs: https://tools.ietf.org/html/rfc4511#section-4.5.1.8240# attrs_only: https://tools.ietf.org/html/rfc4511#section-4.5.1.6241attrs = Array(args[:attributes])242attrs_only = args[:attributes_only] == true243244# references245# refs: https://tools.ietf.org/html/rfc4511#section-4.5.3246# deref: https://tools.ietf.org/html/rfc4511#section-4.5.1.3247refs = args[:return_referrals] == true248deref = args[:deref] || Net::LDAP::DerefAliases_Never249250# limiting, paging, sorting251# size: https://tools.ietf.org/html/rfc4511#section-4.5.1.4252# time: https://tools.ietf.org/html/rfc4511#section-4.5.1.5253size = args[:size].to_i254time = args[:time].to_i255paged = args[:paged_searches_supported]256sort = args.fetch(:sort_controls, false)257258# arg validation259raise ArgumentError, "search base is required" unless base260raise ArgumentError, "invalid search-size" unless size >= 0261raise ArgumentError, "invalid search scope" unless Net::LDAP::SearchScopes.include?(scope)262raise ArgumentError, "invalid alias dereferencing value" unless Net::LDAP::DerefAliasesArray.include?(deref)263264# arg transforms265filter = Net::LDAP::Filter.construct(filter) if filter.is_a?(String)266ber_attrs = attrs.map { |attr| attr.to_s.to_ber }267ber_sort = encode_sort_controls(sort)268269# An interesting value for the size limit would be close to A/D's270# built-in page limit of 1000 records, but openLDAP newer than version271# 2.2.0 chokes on anything bigger than 126. You get a silent error that272# is easily visible by running slapd in debug mode. Go figure.273#274# Changed this around 06Sep06 to support a caller-specified search-size275# limit. Because we ALWAYS do paged searches, we have to work around the276# problem that it's not legal to specify a "normal" sizelimit (in the277# body of the search request) that is larger than the page size we're278# requesting. Unfortunately, I have the feeling that this will break279# with LDAP servers that don't support paged searches!!!280#281# (Because we pass zero as the sizelimit on search rounds when the282# remaining limit is larger than our max page size of 126. In these283# cases, I think the caller's search limit will be ignored!)284#285# CONFIRMED: This code doesn't work on LDAPs that don't support paged286# searches when the size limit is larger than 126. We're going to have287# to do a root-DSE record search and not do a paged search if the LDAP288# doesn't support it. Yuck.289rfc2696_cookie = [126, ""]290result_pdu = nil291n_results = 0292293message_id = next_msgid294295instrument "search.net_ldap_connection",296message_id: message_id,297filter: filter,298base: base,299scope: scope,300size: size,301time: time,302sort: sort,303referrals: refs,304deref: deref,305attributes: attrs do |payload|306loop do307# should collect this into a private helper to clarify the structure308query_limit = 0309if size > 0310query_limit = if paged311(((size - n_results) < 126) ? (size - n_results) : 0)312else313size314end315end316317request = [318base.to_ber,319scope.to_ber_enumerated,320deref.to_ber_enumerated,321query_limit.to_ber, # size limit322time.to_ber,323attrs_only.to_ber,324filter.to_ber,325ber_attrs.to_ber_sequence,326].to_ber_appsequence(Net::LDAP::PDU::SearchRequest)327328# rfc2696_cookie sometimes contains binary data from Microsoft Active Directory329# this breaks when calling to_ber. (Can't force binary data to UTF-8)330# we have to disable paging (even though server supports it) to get around this...331332user_controls = args.fetch(:controls, [])333controls = []334controls <<335[336Net::LDAP::LDAPControls::PAGED_RESULTS.to_ber,337# Criticality MUST be false to interoperate with normal LDAPs.338false.to_ber,339rfc2696_cookie.map(&:to_ber).to_ber_sequence.to_s.to_ber,340].to_ber_sequence if paged341controls << ber_sort if ber_sort342if controls.empty? && user_controls.empty?343controls = nil344else345controls += user_controls346controls = controls.to_ber_contextspecific(0)347end348349write(request, controls, message_id)350351result_pdu = nil352controls = []353354while pdu = queued_read(message_id)355case pdu.app_tag356when Net::LDAP::PDU::SearchReturnedData357n_results += 1358yield pdu.search_entry if block_given?359when Net::LDAP::PDU::SearchResultReferral360if refs361if block_given?362se = Net::LDAP::Entry.new363se[:search_referrals] = (pdu.search_referrals || [])364yield se365end366end367when Net::LDAP::PDU::SearchResult368result_pdu = pdu369controls = pdu.result_controls370if refs && pdu.result_code == Net::LDAP::ResultCodeReferral371if block_given?372se = Net::LDAP::Entry.new373se[:search_referrals] = (pdu.search_referrals || [])374yield se375end376end377break378else379raise Net::LDAP::ResponseTypeInvalidError, "invalid response-type in search: #{pdu.app_tag}"380end381end382383if result_pdu.nil?384raise Net::LDAP::ResponseMissingOrInvalidError, "response missing"385end386387# count number of pages of results388payload[:page_count] ||= 0389payload[:page_count] += 1390391# When we get here, we have seen a type-5 response. If there is no392# error AND there is an RFC-2696 cookie, then query again for the next393# page of results. If not, we're done. Don't screw this up or we'll394# break every search we do.395#396# Noticed 02Sep06, look at the read_ber call in this loop, shouldn't397# that have a parameter of AsnSyntax? Does this just accidentally398# work? According to RFC-2696, the value expected in this position is399# of type OCTET STRING, covered in the default syntax supported by400# read_ber, so I guess we're ok.401more_pages = false402if result_pdu.result_code == Net::LDAP::ResultCodeSuccess and controls403controls.each do |c|404if c.oid == Net::LDAP::LDAPControls::PAGED_RESULTS405# just in case some bogus server sends us more than 1 of these.406more_pages = false407if c.value and c.value.length > 0408cookie = c.value.read_ber[1]409if cookie and cookie.length > 0410rfc2696_cookie[1] = cookie411more_pages = true412end413end414end415end416end417418break unless more_pages419end # loop420421# track total result count422payload[:result_count] = n_results423424result_pdu || OpenStruct.new(:status => :failure, :result_code => Net::LDAP::ResultCodeOperationsError, :message => "Invalid search")425end # instrument426ensure427428# clean up message queue for this search429messages = message_queue.delete(message_id)430431# in the exceptional case some messages were *not* consumed from the queue,432# instrument the event but do not fail.433if !messages.nil? && !messages.empty?434instrument "search_messages_unread.net_ldap_connection",435message_id: message_id, messages: messages436end437end438439# Another monkeypatch to support :controls440def modify(args)441modify_dn = args[:dn] or raise "Unable to modify empty DN"442ops = self.class.modify_ops args[:operations]443444message_id = next_msgid445request = [446modify_dn.to_ber,447ops.to_ber_sequence,448].to_ber_appsequence(Net::LDAP::PDU::ModifyRequest)449450controls = args.fetch(:controls, nil)451unless controls.nil?452controls = controls.to_ber_contextspecific(0)453end454455write(request, controls, message_id)456pdu = queued_read(message_id)457458if !pdu || pdu.app_tag != Net::LDAP::PDU::ModifyResponse459raise Net::LDAP::ResponseMissingOrInvalidError, "response missing or invalid"460end461462pdu463end464465# Monkeypatch upstream library to support the extended Whoami request. Delete466# this after https://github.com/ruby-ldap/ruby-net-ldap/pull/425 is landed.467# This is not the only occurrence of a patch for this functionality.468def ldapwhoami469ext_seq = [Net::LDAP::WhoamiOid.to_ber_contextspecific(0)]470request = ext_seq.to_ber_appsequence(Net::LDAP::PDU::ExtendedRequest)471472message_id = next_msgid473474write(request, nil, message_id)475pdu = queued_read(message_id)476477if !pdu || pdu.app_tag != Net::LDAP::PDU::ExtendedResponse478raise Net::LDAP::ResponseMissingOrInvalidError, "response missing or invalid"479end480481pdu482end483end484485class Net::LDAP::PDU486# Monkeypatch upstream library to support the extended Whoami request. Delete487# this after https://github.com/ruby-ldap/ruby-net-ldap/pull/425 is landed.488# This is not the only occurrence of a patch for this functionality.489def parse_extended_response(sequence)490sequence.length.between?(3, 5) or raise Net::LDAP::PDU::Error, "Invalid LDAP result length."491@ldap_result = {492:resultCode => sequence[0],493:matchedDN => sequence[1],494:errorMessage => sequence[2],495}496@extended_response = sequence.last497end498end499500module Rex501module Proto502module LDAP503end504end505end506507508