require 'forwardable'1require 'net/ldap'2require 'rex/socket'34#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 functionality7# TODO: write a real LDAP client in Rex and migrate all consumers8#910# Update Net::LDAP's initialize and new_connection method to honor a tracking proxies setting11class Net::LDAP12WhoamiOid = '1.3.6.1.4.1.4203.1.11.3'.freeze1314# fix the definition for ExtendedResponse15AsnSyntax[Net::BER::TAG_CLASS[:universal] + Net::BER::ENCODING_TYPE[:constructed] + 107] = :string1617# Reference the old initialize method, and ensure `reload_lib -a` doesn't attempt to refine the method18alias_method :_old_initialize, :initialize unless defined?(_old_initialize)1920# Original Source:21# https://github.com/ruby-ldap/ruby-net-ldap/blob/95cec3822cd2f60787971e19714f74fd5999595c/lib/net/ldap.rb#L54822# Additionally tracks proxies configuration, used when making a new_connection23def initialize(args = {})24_old_initialize(args)25@proxies = args[:proxies]26end2728private2930# Original source:31# https://github.com/ruby-ldap/ruby-net-ldap/blob/95cec3822cd2f60787971e19714f74fd5999595c/lib/net/ldap.rb#L132132# Updated to include proxies configuration33def new_connection34connection = 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 => @proxies4344# Force connect to see if there's a connection error45connection.socket46connection47rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT => e48@result = {49:resultCode => 52,50:errorMessage => ResultStrings[ResultCodeUnavailable],51}52raise e53end54end5556# Update Net::LDAP's initialize and new_connection method to honor a tracking proxies setting57class Net::LDAP::Connection # :nodoc:58module SynchronousRead59# Read `length` bytes of data from the LDAP connection socket and60# 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] Unused64#65# @return [String] A string containing the data read from the LDAP connection socket.66def read(length = nil, _opts = {})67data = ''68loop do69chunk = super(length - data.length)70if chunk.nil?71return data == '' ? nil : data72end7374data << chunk75break if data.length == length76end7778data79end80end8182# Allow wrapping the socket to read and write SASL data83module SocketSaslIO84include Rex::Proto::Sasl8586# This seems hacky, but we're just fitting in with how net-ldap does it87def get_ber_length(data)88n = data[0].ord8990if n <= 0x7f91[n, 1]92elsif n == 0x8093raise Net::BER::BerError,94'Indeterminite BER content length not implemented.'95elsif n == 0xff96raise Net::BER::BerError, 'Invalid BER length 0xFF detected.'97else98v = 099extra_length = n & 0x7f100data[1,n & 0x7f].each_byte do |b|101v = (v << 8) + b102end103104[v, extra_length + 1]105end106end107108def read_ber(syntax = nil)109unless @wrap_read.nil?110if ber_cache.any?111return ber_cache.shift112end113# SASL buffer length114length_bytes = read(4)115# The implementation in net-ldap returns nil if it doesn't read any data116return nil unless length_bytes117118length = length_bytes.unpack('N')[0]119120# Now read the actual data121data = read(length)122123# Decrypt it124plaintext = @wrap_read.call(data)125126while plaintext.length > 0127id = plaintext[0].ord128ber_length, used_chars = get_ber_length(plaintext[1,plaintext.length])129plaintext = plaintext[1+used_chars, plaintext.length]130131# We may receive several objects in the one packet132# Ideally we'd refactor all of ruby-net-ldap to use133# yields for this, but it's all a bit messy. So instead,134# just store them all and return the next one each time135# we're asked.136ber_cache.append(parse_ber_object(syntax, id, plaintext[0,ber_length]))137138plaintext = plaintext[ber_length,plaintext.length]139end140141return ber_cache.shift142else143super(syntax)144end145end146147def write(data)148unless @wrap_write.nil?149# Encrypt it150data = @wrap_write.call(data)151152# Prepend the length bytes153data = wrap_sasl(data)154end155156super(data)157end158159def setup(wrap_read, wrap_write)160@wrap_read = wrap_read161@wrap_write = wrap_write162@ber_cache = []163end164165private166167attr_accessor :wrap_read168attr_accessor :wrap_write169attr_accessor :ber_cache170end171172module ConnectionSaslIO173# Provide the encryption wrapper for the caller to set up174def wrap_read_write(wrap_read, wrap_write)175@conn.extend(SocketSaslIO)176@conn.setup(wrap_read, wrap_write)177end178end179180# 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 set184# 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_method186# @see http://gemdocs.org/gems/rex-socket/0.1.43/Rex/Socket.html#create_param-class_method187# @see http://gemdocs.org/gems/rex-socket/0.1.43/Rex/Socket/Parameters.html#from_hash-class_method188def initialize(server)189begin190@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)197198# Set up read/write wrapping199self.extend(ConnectionSaslIO)200rescue SocketError201raise Net::LDAP::LdapError, 'No such address or other socket error.'202rescue Errno::ECONNREFUSED203raise Net::LDAP::LdapError, "Server #{server[:host]} refused connection on port #{server[:port]}."204end205206if server[:encryption]207setup_encryption server[:encryption]208@conn.extend Forwardable209@conn.def_delegators :@io, :localinfo, :peerinfo210end211212yield self if block_given?213end214215# Monkeypatch upstream library for now to support :controls216# hash option in `args` so that we can provide controls within217# searches. Needed so we can specify the LDAP_SERVER_SD_FLAGS_OID218# flag for searches to prevent getting the SACL when querying for219# ntSecurityDescriptor, as this is retrieved by default and non-admin220# users are not allowed to retrieve SACLs for objects. Therefore by221# adjusting the search to not retrieve SACLs, non-admin users can still222# retrieve information about the security of objects without violating this rule.223#224# @see https://github.com/rapid7/metasploit-framework/issues/17324225# @see https://github.com/ruby-ldap/ruby-net-ldap/pull/411226#227# @param [Hash] args A hash of the arguments to be utilized by the search operation.228# @return [Net::LDAP::PDU] A Protocol Data Unit (PDU) object, represented by229# the Net::LDAP::PDU class, containing the results of the search operation.230def search(args = nil)231args ||= {}232233# filtering, scoping, search base234# filter: https://tools.ietf.org/html/rfc4511#section-4.5.1.7235# base: https://tools.ietf.org/html/rfc4511#section-4.5.1.1236# scope: https://tools.ietf.org/html/rfc4511#section-4.5.1.2237filter = args[:filter] || Net::LDAP::Filter.eq("objectClass", "*")238base = args[:base]239scope = args[:scope] || Net::LDAP::SearchScope_WholeSubtree240241# attr handling242# attrs: https://tools.ietf.org/html/rfc4511#section-4.5.1.8243# attrs_only: https://tools.ietf.org/html/rfc4511#section-4.5.1.6244attrs = Array(args[:attributes])245attrs_only = args[:attributes_only] == true246247# references248# refs: https://tools.ietf.org/html/rfc4511#section-4.5.3249# deref: https://tools.ietf.org/html/rfc4511#section-4.5.1.3250refs = args[:return_referrals] == true251deref = args[:deref] || Net::LDAP::DerefAliases_Never252253# limiting, paging, sorting254# size: https://tools.ietf.org/html/rfc4511#section-4.5.1.4255# time: https://tools.ietf.org/html/rfc4511#section-4.5.1.5256size = args[:size].to_i257time = args[:time].to_i258paged = args[:paged_searches_supported]259sort = args.fetch(:sort_controls, false)260261# arg validation262raise ArgumentError, "search base is required" unless base263raise ArgumentError, "invalid search-size" unless size >= 0264raise ArgumentError, "invalid search scope" unless Net::LDAP::SearchScopes.include?(scope)265raise ArgumentError, "invalid alias dereferencing value" unless Net::LDAP::DerefAliasesArray.include?(deref)266267# arg transforms268filter = Net::LDAP::Filter.construct(filter) if filter.is_a?(String)269ber_attrs = attrs.map { |attr| attr.to_s.to_ber }270ber_sort = encode_sort_controls(sort)271272# An interesting value for the size limit would be close to A/D's273# built-in page limit of 1000 records, but openLDAP newer than version274# 2.2.0 chokes on anything bigger than 126. You get a silent error that275# is easily visible by running slapd in debug mode. Go figure.276#277# Changed this around 06Sep06 to support a caller-specified search-size278# limit. Because we ALWAYS do paged searches, we have to work around the279# problem that it's not legal to specify a "normal" sizelimit (in the280# body of the search request) that is larger than the page size we're281# requesting. Unfortunately, I have the feeling that this will break282# with LDAP servers that don't support paged searches!!!283#284# (Because we pass zero as the sizelimit on search rounds when the285# remaining limit is larger than our max page size of 126. In these286# cases, I think the caller's search limit will be ignored!)287#288# CONFIRMED: This code doesn't work on LDAPs that don't support paged289# searches when the size limit is larger than 126. We're going to have290# to do a root-DSE record search and not do a paged search if the LDAP291# doesn't support it. Yuck.292rfc2696_cookie = [126, ""]293result_pdu = nil294n_results = 0295296message_id = next_msgid297298instrument "search.net_ldap_connection",299message_id: message_id,300filter: filter,301base: base,302scope: scope,303size: size,304time: time,305sort: sort,306referrals: refs,307deref: deref,308attributes: attrs do |payload|309loop do310# should collect this into a private helper to clarify the structure311query_limit = 0312if size > 0313query_limit = if paged314(((size - n_results) < 126) ? (size - n_results) : 0)315else316size317end318end319320request = [321base.to_ber,322scope.to_ber_enumerated,323deref.to_ber_enumerated,324query_limit.to_ber, # size limit325time.to_ber,326attrs_only.to_ber,327filter.to_ber,328ber_attrs.to_ber_sequence,329].to_ber_appsequence(Net::LDAP::PDU::SearchRequest)330331# rfc2696_cookie sometimes contains binary data from Microsoft Active Directory332# this breaks when calling to_ber. (Can't force binary data to UTF-8)333# we have to disable paging (even though server supports it) to get around this...334335user_controls = args.fetch(:controls, [])336controls = []337controls <<338[339Net::LDAP::LDAPControls::PAGED_RESULTS.to_ber,340# Criticality MUST be false to interoperate with normal LDAPs.341false.to_ber,342rfc2696_cookie.map(&:to_ber).to_ber_sequence.to_s.to_ber,343].to_ber_sequence if paged344controls << ber_sort if ber_sort345if controls.empty? && user_controls.empty?346controls = nil347else348controls += user_controls349controls = controls.to_ber_contextspecific(0)350end351352write(request, controls, message_id)353354result_pdu = nil355controls = []356357while pdu = queued_read(message_id)358case pdu.app_tag359when Net::LDAP::PDU::SearchReturnedData360n_results += 1361yield pdu.search_entry if block_given?362when Net::LDAP::PDU::SearchResultReferral363if refs364if block_given?365se = Net::LDAP::Entry.new366se[:search_referrals] = (pdu.search_referrals || [])367yield se368end369end370when Net::LDAP::PDU::SearchResult371result_pdu = pdu372controls = pdu.result_controls373if refs && pdu.result_code == Net::LDAP::ResultCodeReferral374if block_given?375se = Net::LDAP::Entry.new376se[:search_referrals] = (pdu.search_referrals || [])377yield se378end379end380break381else382raise Net::LDAP::ResponseTypeInvalidError, "invalid response-type in search: #{pdu.app_tag}"383end384end385386if result_pdu.nil?387raise Net::LDAP::ResponseMissingOrInvalidError, "response missing"388end389390# count number of pages of results391payload[:page_count] ||= 0392payload[:page_count] += 1393394# When we get here, we have seen a type-5 response. If there is no395# error AND there is an RFC-2696 cookie, then query again for the next396# page of results. If not, we're done. Don't screw this up or we'll397# break every search we do.398#399# Noticed 02Sep06, look at the read_ber call in this loop, shouldn't400# that have a parameter of AsnSyntax? Does this just accidentally401# work? According to RFC-2696, the value expected in this position is402# of type OCTET STRING, covered in the default syntax supported by403# read_ber, so I guess we're ok.404more_pages = false405if result_pdu.result_code == Net::LDAP::ResultCodeSuccess and controls406controls.each do |c|407if c.oid == Net::LDAP::LDAPControls::PAGED_RESULTS408# just in case some bogus server sends us more than 1 of these.409more_pages = false410if c.value and c.value.length > 0411cookie = c.value.read_ber[1]412if cookie and cookie.length > 0413rfc2696_cookie[1] = cookie414more_pages = true415end416end417end418end419end420421break unless more_pages422end # loop423424# track total result count425payload[:result_count] = n_results426427result_pdu || OpenStruct.new(:status => :failure, :result_code => Net::LDAP::ResultCodeOperationsError, :message => "Invalid search")428end # instrument429ensure430431# clean up message queue for this search432messages = message_queue.delete(message_id)433434# in the exceptional case some messages were *not* consumed from the queue,435# instrument the event but do not fail.436if !messages.nil? && !messages.empty?437instrument "search_messages_unread.net_ldap_connection",438message_id: message_id, messages: messages439end440end441442# Another monkeypatch to support :controls443def modify(args)444modify_dn = args[:dn] or raise "Unable to modify empty DN"445ops = self.class.modify_ops args[:operations]446447message_id = next_msgid448request = [449modify_dn.to_ber,450ops.to_ber_sequence,451].to_ber_appsequence(Net::LDAP::PDU::ModifyRequest)452453controls = args.fetch(:controls, nil)454unless controls.nil?455controls = controls.to_ber_contextspecific(0)456end457458write(request, controls, message_id)459pdu = queued_read(message_id)460461if !pdu || pdu.app_tag != Net::LDAP::PDU::ModifyResponse462raise Net::LDAP::ResponseMissingOrInvalidError, "response missing or invalid"463end464465pdu466end467468# Monkeypatch upstream library to support the extended Whoami request. Delete469# this after https://github.com/ruby-ldap/ruby-net-ldap/pull/425 is landed.470# This is not the only occurrence of a patch for this functionality.471def ldapwhoami472ext_seq = [Net::LDAP::WhoamiOid.to_ber_contextspecific(0)]473request = ext_seq.to_ber_appsequence(Net::LDAP::PDU::ExtendedRequest)474475message_id = next_msgid476477write(request, nil, message_id)478pdu = queued_read(message_id)479480if !pdu || pdu.app_tag != Net::LDAP::PDU::ExtendedResponse481raise Net::LDAP::ResponseMissingOrInvalidError, "response missing or invalid"482end483484pdu485end486end487488class Net::LDAP::PDU489# Monkeypatch upstream library to support the extended Whoami request. Delete490# this after https://github.com/ruby-ldap/ruby-net-ldap/pull/425 is landed.491# This is not the only occurrence of a patch for this functionality.492def parse_extended_response(sequence)493sequence.length.between?(3, 5) or raise Net::LDAP::PDU::Error, "Invalid LDAP result length."494@ldap_result = {495:resultCode => sequence[0],496:matchedDN => sequence[1],497:errorMessage => sequence[2],498}499@extended_response = sequence.last500end501end502503module Rex504module Proto505module LDAP506end507end508end509510511