Skip to content

Commit

Permalink
Update ldap modules to support an ldap session
Browse files Browse the repository at this point in the history
  • Loading branch information
dwelch-r7 committed Apr 25, 2024
1 parent c967158 commit 16e969d
Show file tree
Hide file tree
Showing 15 changed files with 317 additions and 179 deletions.
58 changes: 4 additions & 54 deletions lib/msf/core/exploit/remote/ldap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ module Exploit::Remote::LDAP
include Msf::Exploit::Remote::Kerberos::Ticket::Storage
include Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Options
include Metasploit::Framework::LDAP::Client
include Msf::OptionalSession::LDAP

# Initialize the LDAP client and set up the LDAP specific datastore
# options to allow the client to perform authentication and timeout
Expand All @@ -27,8 +28,6 @@ def initialize(info = {})
super

register_options([
Opt::RHOST,
Opt::RPORT(389),
OptBool.new('SSL', [false, 'Enable SSL on the LDAP connection', false]),
Msf::OptString.new('DOMAIN', [false, 'The domain to authenticate to']),
Msf::OptString.new('USERNAME', [false, 'The username to authenticate with'], aliases: ['BIND_DN']),
Expand Down Expand Up @@ -96,6 +95,7 @@ def get_connect_opts
# @return [Object] The result of whatever the block that was
# passed in via the "block" parameter yielded.
def ldap_connect(opts = {}, &block)
return yield session.client if session
ldap_open(get_connect_opts.merge(opts), &block)
end

Expand All @@ -111,6 +111,7 @@ def ldap_connect(opts = {}, &block)
# @return [Object] The result of whatever the block that was
# passed in via the "block" parameter yielded.
def ldap_open(connect_opts, &block)
return yield session.client if session
opts = resolve_connect_opts(connect_opts)
Rex::Proto::LDAP::Client.open(opts, &block)
end
Expand All @@ -135,6 +136,7 @@ def resolve_connect_opts(connect_opts)
# @yieldparam ldap [Rex::Proto::LDAP::Client] The LDAP connection handle to use for connecting to
# the target LDAP server.
def ldap_new(opts = {})
return yield session.client if session

ldap = Rex::Proto::LDAP::Client.new(resolve_connect_opts(get_connect_opts.merge(opts)))

Expand Down Expand Up @@ -169,58 +171,6 @@ def ldap.use_connection(args)
yield ldap
end

# # Get the naming contexts for the target LDAP server.
# #
# # @param ldap [Rex::Proto::LDAP::Client] The Rex::Proto::LDAP::Client connection handle for the
# # current LDAP connection.
# # @return [Net::BER::BerIdentifiedArray] Array of naming contexts for the target LDAP server.
# def get_naming_contexts(ldap)
# vprint_status("#{peer} Getting root DSE")
#
# unless (root_dse = ldap.search_root_dse)
# print_error("#{peer} Could not retrieve root DSE")
# return
# end
#
# naming_contexts = root_dse[:namingcontexts]
#
# # NOTE: Rex::Proto::LDAP::Client converts attribute names to lowercase
# if naming_contexts.empty?
# print_error("#{peer} Empty namingContexts attribute")
# return
# end
#
# naming_contexts
# end

# Discover the base DN of the target LDAP server via the LDAP
# server's naming contexts.
#
# @param ldap [Rex::Proto::LDAP::Client] The Rex::Proto::LDAP::Client connection handle for the
# current LDAP connection.
# @return [String] A string containing the base DN of the target LDAP server.
# def discover_base_dn(ldap)
# # @type [Net::BER::BerIdentifiedArray]
# naming_contexts = get_naming_contexts(ldap)
#
# unless naming_contexts
# print_error("#{peer} Base DN cannot be determined")
# return
# end
#
# # NOTE: Find the first entry that starts with `DC=` as this will likely be the base DN.
# naming_contexts.select! { |context| context =~ /^([Dd][Cc]=[A-Za-z0-9-]+,?)+$/ }
# naming_contexts.reject! { |context| context =~ /(Configuration)|(Schema)|(ForestDnsZones)/ }
# if naming_contexts.blank?
# print_error("#{peer} A base DN matching the expected format could not be found!")
# return
# end
# base_dn = naming_contexts[0]
#
# print_good("#{peer} Discovered base DN: #{base_dn}")
# base_dn
# end

# Check whether it was possible to successfully bind to the target LDAP
# server. Raise a RuntimeException with an appropriate error message
# if not.
Expand Down
23 changes: 9 additions & 14 deletions lib/msf/core/exploit/remote/ldap/error.rb
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
# frozen_string_literal: true

module Msf
module Exploit
module Remote
module LDAP
class Error < ::StandardError
module Msf::Exploit::Remote::LDAP

attr_reader :error_code
attr_reader :operation_result
def initialize(message: nil, error_code: nil, operation_result: nil)
super(message || 'LDAP Error')
@error_code = error_code
@operation_result = operation_result
end
end
end
class Error < ::StandardError

attr_reader :error_code
attr_reader :operation_result
def initialize(message: nil, error_code: nil, operation_result: nil)
super(message || 'LDAP Error')
@error_code = error_code
@operation_result = operation_result
end
end
end
52 changes: 52 additions & 0 deletions lib/msf/core/optional_session/ldap.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# frozen_string_literal: true

module Msf
module OptionalSession
module LDAP
include Msf::OptionalSession

RHOST_GROUP_OPTIONS = %w[RHOSTS RPORT DOMAIN USERNAME PASSWORD THREADS]
REQUIRED_OPTIONS = %w[RHOSTS RPORT USERNAME PASSWORD THREADS]

def initialize(info = {})
super(
update_info(
info,
'SessionTypes' => %w[ldap]
)
)

if optional_session_enabled?
register_option_group(name: 'SESSION',
description: 'Used when connecting via an existing SESSION',
option_names: ['SESSION'])
register_option_group(name: 'RHOST',
description: 'Used when making a new connection via RHOSTS',
option_names: RHOST_GROUP_OPTIONS,
required_options: REQUIRED_OPTIONS)

register_options(
[
Msf::OptInt.new('SESSION', [ false, 'The session to run this module on' ]),
Msf::Opt::RHOST(nil, false),
Msf::Opt::RPORT(389, false)
]
)

add_info('New in Metasploit 6.4 - This module can target a %grnSESSION%clr or an %grnRHOST%clr')
else
register_options(
[
Msf::Opt::RHOST,
Msf::Opt::RPORT(389),
]
)
end
end

def optional_session_enabled?
framework.features.enabled?(Msf::FeatureManager::LDAP_SESSION_TYPE)
end
end
end
end
4 changes: 2 additions & 2 deletions lib/rex/proto/ldap/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,10 @@ def _open

def discover_schema_naming_context
result = search(base: '', attributes: [:schemanamingcontext], scope: Net::LDAP::SearchScope_BaseObject)
if result.first && result.first[:schemanamingcontext]
if result.first && !result.first[:schemanamingcontext].empty?
schema_dn = result.first[:schemanamingcontext].first
ilog("#{peerinfo} Discovered Schema DN: #{schema_dn}")
schema_dn
return schema_dn
end
wlog("#{peerinfo} Could not discover Schema DN")
nil
Expand Down
2 changes: 1 addition & 1 deletion modules/auxiliary/admin/ldap/ad_cs_cert_template.rb
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ def run
else
print_status('Discovering base DN automatically')

unless (@base_dn = discover_base_dn(ldap))
unless (@base_dn = ldap.base_dn)
fail_with(Failure::NotFound, "Couldn't discover base DN!")
end
end
Expand Down
2 changes: 1 addition & 1 deletion modules/auxiliary/admin/ldap/rbcd.rb
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ def run
else
print_status('Discovering base DN automatically')

unless (@base_dn = discover_base_dn(ldap))
unless (@base_dn = ldap.base_dn)
print_warning("Couldn't discover base DN!")
end
end
Expand Down
6 changes: 4 additions & 2 deletions modules/auxiliary/admin/ldap/shadow_credentials.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@

class MetasploitModule < Msf::Auxiliary

include Msf::Exploit::Remote::LDAP
include Msf::Auxiliary::Report
include Msf::Exploit::Remote::LDAP

ATTRIBUTE = 'msDS-KeyCredentialLink'.freeze

Expand Down Expand Up @@ -114,7 +114,9 @@ def run
else
print_status('Discovering base DN automatically')

unless (@base_dn = ldap.base_dn)
if (@base_dn = ldap.base_dn)
print_status("#{ldap.peerinfo} Discovered base DN: #{@base_dn}")
else
print_warning("Couldn't discover base DN!")
end
end
Expand Down
2 changes: 1 addition & 1 deletion modules/auxiliary/gather/asrep.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def run_ldap

ldap_connect do |ldap|
validate_bind_success!(ldap)
unless (base_dn = discover_base_dn(ldap))
unless (base_dn = ldap.base_dn)
fail_with(Failure::UnexpectedReply, "Couldn't discover base DN!")
end

Expand Down
6 changes: 3 additions & 3 deletions modules/auxiliary/gather/ldap_hashdump.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@

class MetasploitModule < Msf::Auxiliary

include Msf::Exploit::Remote::LDAP
include Msf::Auxiliary::Scanner
include Msf::Auxiliary::Report
include Msf::Exploit::Remote::LDAP

def initialize(info = {})
super(
Expand All @@ -33,7 +33,8 @@ def initialize(info = {})
],
'DefaultAction' => 'Dump',
'DefaultOptions' => {
'SSL' => true
'SSL' => true,
'RPORT' => 636
},
'Notes' => {
'Stability' => [CRASH_SAFE],
Expand All @@ -44,7 +45,6 @@ def initialize(info = {})
)

register_options([
Opt::RPORT(636), # SSL/TLS
OptInt.new('MAX_LOOT', [false, 'Maximum number of LDAP entries to loot', nil]),
OptInt.new('READ_TIMEOUT', [false, 'LDAP read timeout in seconds', 600]),
OptString.new('BASE_DN', [false, 'LDAP base DN if you already have it']),
Expand Down
2 changes: 1 addition & 1 deletion modules/auxiliary/gather/vmware_vcenter_vmdir_ldap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def run
else
print_status('Discovering base DN automatically')

unless (@base_dn = discover_base_dn(ldap))
unless (@base_dn = ldap.base_dn)
print_warning('Falling back on default base DN dc=vsphere,dc=local')
end
end
Expand Down
16 changes: 7 additions & 9 deletions spec/acceptance/ldap_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
{
name: 'auxiliary/gather/ldap_query',
platforms: %i[linux osx windows],
targets: [:rhost],
targets: [:session, :rhost],
skipped: false,
action: 'run_query_file',
datastore: { QUERY_FILE_PATH: 'data/auxiliary/gather/ldap_query/ldap_queries_default.yaml' },
Expand All @@ -39,15 +39,14 @@
/Running ENUM_ACCOUNTS.../,
/Running ENUM_USER_SPNS_KERBEROAST.../,
/Running ENUM_USER_PASSWORD_NOT_REQUIRED.../,

]
}
}
},
{
name: 'auxiliary/gather/ldap_query',
platforms: %i[linux osx windows],
targets: [:rhost],
targets: [:session, :rhost],
skipped: false,
action: 'enum_accounts',
lines: {
Expand All @@ -62,13 +61,11 @@
{
name: 'auxiliary/gather/ldap_hashdump',
platforms: %i[linux osx windows],
targets: [:rhost],
targets: [:session, :rhost],
skipped: false,
lines: {
all: {
required: [
/Discovering base DN\(s\) automatically/,
/Dumping data for root DSE/,
/Searching base DN='DC=ldap,DC=example,DC=com'/,
/Storing LDAP data for base DN='DC=ldap,DC=example,DC=com' in loot/,
/266 entries, 0 creds found in 'DC=ldap,DC=example,DC=com'./
Expand All @@ -79,13 +76,12 @@
{
name: 'auxiliary/admin/ldap/shadow_credentials',
platforms: %i[linux osx windows],
targets: [:rhost],
targets: [:session, :rhost],
skipped: false,
datastore: { TARGET_USER: 'administrator' },
lines: {
all: {
required: [
/Discovering base DN automatically/,
/Discovered base DN: DC=ldap,DC=example,DC=com/,
/The msDS-KeyCredentialLink field is empty./
]
Expand Down Expand Up @@ -338,7 +334,9 @@ def with_test_harness(module_test)
end)

use_module = "use #{module_test[:name]}"
run_module = "run session=#{session_id} Verbose=true"
run_command = module_test.key?(:action) ? module_test.fetch(:action) : 'run'
run_module = "#{run_command} session=#{session_id} #{target.datastore_options(default_module_datastore: default_module_datastore.merge(module_test.fetch(:datastore, {})))} Verbose=true"


replication_commands << use_module
console.sendline(use_module)
Expand Down
Loading

0 comments on commit 16e969d

Please sign in to comment.