From a7ba39c5297f09169b8bcb69112212a465251aae Mon Sep 17 00:00:00 2001 From: Dean Welch Date: Fri, 5 Apr 2024 15:51:45 +0100 Subject: [PATCH] keep ldap connection open for use in a session --- .../framework/login_scanner/ldap.rb | 15 ++++---- lib/rex/proto/ldap/client.rb | 30 ++++++++++++++- modules/auxiliary/scanner/ldap/ldap_login.rb | 38 +++++++++++-------- spec/lib/rex/proto/ldap/client_spec.rb | 2 +- 4 files changed, 58 insertions(+), 27 deletions(-) diff --git a/lib/metasploit/framework/login_scanner/ldap.rb b/lib/metasploit/framework/login_scanner/ldap.rb index c0c94de6fbb5b..3f99575c09d4a 100644 --- a/lib/metasploit/framework/login_scanner/ldap.rb +++ b/lib/metasploit/framework/login_scanner/ldap.rb @@ -11,11 +11,11 @@ class LDAP include Metasploit::Framework::LDAP::Client include Msf::Exploit::Remote::LDAP - attr_accessor :opts - attr_accessor :realm_key + attr_accessor :opts, :realm_key # @!attribute use_client_as_proof # @return [Boolean] If a login is successful and this attribute is true - an LDAP::Client instance is used as proof attr_accessor :use_client_as_proof + def attempt_login(credential) result_opts = { credential: credential, @@ -38,7 +38,8 @@ def do_login(credential) }.merge(@opts) connect_opts = ldap_connect_opts(host, port, connection_timeout, ssl: opts[:ssl], opts: opts) - ldap_open(connect_opts) do |ldap_client| + begin + ldap_client = Rex::Proto::LDAP::Client._open(connect_opts) return status_code(ldap_client) rescue StandardError => e { status: Metasploit::Model::Login::Status::UNABLE_TO_CONNECT, proof: e } @@ -46,14 +47,13 @@ def do_login(credential) end def status_code(ldap_client) - case ldap_client.get_operation_result.table[:code] + operation_result = ldap_client.get_operation_result.table[:code] + case operation_result when 0 result = { status: Metasploit::Model::Login::Status::SUCCESSFUL } if use_client_as_proof result[:proof] = ldap_client - # client = nil - # self.sock = nil - # self.dispatcher = nil + result[:connection] = ldap_client.socket end result else @@ -93,7 +93,6 @@ def each_credential credential.public = "#{credential.public}@#{opts[:domain]}" yield credential end - end end end diff --git a/lib/rex/proto/ldap/client.rb b/lib/rex/proto/ldap/client.rb index a8c94ce252558..408a1a1a94be2 100644 --- a/lib/rex/proto/ldap/client.rb +++ b/lib/rex/proto/ldap/client.rb @@ -3,12 +3,14 @@ module Rex module Proto module LDAP - # This is a Rex Proto wrapper around the Net::LDAP client which is currently coming from the 'net-ldap' gem. # The purpose of this wrapper is to provide 'peerhost' and 'peerport' methods to ensure the client interfaces # are consistent between various session clients. class Client < Net::LDAP - # @return [String] The remote IP address that LDAPr is running on + + attr_reader :socket + + # @return [String] The remote IP address that LDAP is running on def peerhost host end @@ -22,6 +24,30 @@ def peerport def peerinfo "#{peerhost}:#{peerport}" end + + # https://github.com/ruby-ldap/ruby-net-ldap/issues/11 + # We want to keep the ldap connection open to use later + # but there's no built in way within the `Net::LDAP` library to do that + # so we're + # @param connect_opts [Hash] Options for the LDAP connection. + def self._open(connect_opts) + client = new(connect_opts) + client._open + end + + # https://github.com/ruby-ldap/ruby-net-ldap/issues/11 + def _open + raise Net::LDAP::AlreadyOpenedError, 'Open already in progress' if @open_connection + + instrument 'open.net_ldap' do |payload| + @open_connection = new_connection + @socket = @open_connection.socket + payload[:connection] = @open_connection + payload[:bind] = @result = @open_connection.bind(@auth) + return self + end + end + end end end diff --git a/modules/auxiliary/scanner/ldap/ldap_login.rb b/modules/auxiliary/scanner/ldap/ldap_login.rb index c2594499b14be..eccaa469d61a7 100644 --- a/modules/auxiliary/scanner/ldap/ldap_login.rb +++ b/modules/auxiliary/scanner/ldap/ldap_login.rb @@ -64,8 +64,16 @@ def create_session? def run validate_connect_options! - super - # TODO: collect and log sessions/creds + results = super + logins = results.flat_map { |_k, v| v[:successful_logins] } + sessions = results.flat_map { |_k, v| v[:successful_sessions] } + print_status("Bruteforce completed, #{logins.size} #{logins.size == 1 ? 'credential was' : 'credentials were'} successful.") + if datastore['CreateSession'] + print_status("#{sessions.size} LDAP #{sessions.size == 1 ? 'session was' : 'sessions were'} opened successfully.") + else + print_status('You can open an LDAP session with these credentials and %grnCreateSession%clr set to true') + end + results end def validate_connect_options! @@ -118,6 +126,8 @@ def run_host(ip) use_client_as_proof: create_session? ) + successful_logins = [] + successful_sessions = [] scanner.scan! do |result| credential_data = result.to_h credential_data.merge!( @@ -127,6 +137,7 @@ def run_host(ip) protocol: 'tcp' ) if result.success? + successful_logins << result if opts[:ldap_auth] == Msf::Exploit::Remote::AuthOption::SCHANNEL # Schannel auth has no meaningful credential information to store in the DB print_brute level: :good, ip: ip, msg: "Success: 'Cert File #{opts[:ldap_cert_file]}'" @@ -134,37 +145,32 @@ def run_host(ip) create_credential_and_login(credential_data) print_brute level: :good, ip: ip, msg: "Success: '#{result.credential}'" end - create_session(result) if create_session? + successful_sessions << create_session(result) if create_session? else invalidate_login(credential_data) vprint_error "#{ip}:#{rport} - LOGIN FAILED: #{result.credential} (#{result.status}: #{result.proof})" end end + { successful_logins: successful_logins, successful_sessions: successful_sessions } end private def create_session(result) - session_setup(result, result.proof) + session_setup(result) rescue StandardError => e elog('Failed to setup the session', error: e) - print_brute level: :error, ip: 'fake_ip', msg: "Failed to setup the session - #{e.class} #{e.message}" + print_brute level: :error, ip: ip, msg: "Failed to setup the session - #{e.class} #{e.message}" + result.connection.close unless result.connection.nil? end # @param [Metasploit::Framework::LoginScanner::Result] result - # @param [Rex::Proto::LDAP::Client] client # @return [Msf::Sessions::LDAP] - def session_setup(result, client) - return unless client + def session_setup(result) + return unless (result.connection && result.proof) # Create a new session - # rstream = client.dispatcher.tcp_socket - sess = Msf::Sessions::LDAP.new( - nil, # TODO: make this nil, don't think we need it anymore for the new(er) session types - { - client: client - } - ) + my_session = Msf::Sessions::LDAP.new(result.connection, { client: result.proof }) merge_me = { 'USERPASS_FILE' => nil, @@ -174,6 +180,6 @@ def session_setup(result, client) 'PASSWORD' => result.credential.private } - start_session(self, nil, merge_me, false, sess.rstream, sess) + start_session(self, nil, merge_me, false, my_session.rstream, my_session) end end diff --git a/spec/lib/rex/proto/ldap/client_spec.rb b/spec/lib/rex/proto/ldap/client_spec.rb index 1d8de57acd885..a37c547f774bc 100644 --- a/spec/lib/rex/proto/ldap/client_spec.rb +++ b/spec/lib/rex/proto/ldap/client_spec.rb @@ -1,7 +1,7 @@ # -*- coding: binary -*- require 'spec_helper' -require 'rex/proto/smb/simple_client' +require 'rex/proto/ldap/client' RSpec.describe Rex::Proto::LDAP::Client do let(:host) { '127.0.0.1' }