Update ldap modules to support an ldap session

This commit is contained in:
Dean Welch
2024-04-25 17:31:18 +01:00
parent 55cb49c60e
commit e693b9588c
16 changed files with 315 additions and 177 deletions
+4 -54
View File
@@ -12,6 +12,7 @@ module Msf
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
@@ -27,8 +28,6 @@ module Msf
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']),
@@ -122,6 +121,7 @@ module Msf
# @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
@@ -137,6 +137,7 @@ module Msf
# @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
@@ -161,6 +162,7 @@ module Msf
# @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)))
@@ -195,58 +197,6 @@ module Msf
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.
+9 -14
View File
@@ -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
View File
@@ -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
+2 -2
View File
@@ -71,10 +71,10 @@ module Rex
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
@@ -109,7 +109,7 @@ class MetasploitModule < Msf::Auxiliary
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
+1 -1
View File
@@ -138,7 +138,7 @@ class MetasploitModule < Msf::Auxiliary
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
@@ -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
@@ -114,7 +114,9 @@ class MetasploitModule < Msf::Auxiliary
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
+1 -1
View File
@@ -99,7 +99,7 @@ class MetasploitModule < Msf::Auxiliary
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
+3 -3
View File
@@ -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(
@@ -33,7 +33,8 @@ class MetasploitModule < Msf::Auxiliary
],
'DefaultAction' => 'Dump',
'DefaultOptions' => {
'SSL' => true
'SSL' => true,
'RPORT' => 636
},
'Notes' => {
'Stability' => [CRASH_SAFE],
@@ -44,7 +45,6 @@ class MetasploitModule < Msf::Auxiliary
)
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']),
@@ -77,7 +77,7 @@ class MetasploitModule < Msf::Auxiliary
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
+1 -1
View File
@@ -169,7 +169,7 @@ class MetasploitModule < Msf::Auxiliary
# @param [Metasploit::Framework::LoginScanner::Result] result
# @return [Msf::Sessions::LDAP]
def session_setup(result)
return unless (result.connection && result.proof)
return unless result.connection && result.proof
# Create a new session
my_session = Msf::Sessions::LDAP.new(result.connection, { client: result.proof })
+7 -9
View File
@@ -26,7 +26,7 @@ RSpec.describe 'LDAP modules' do
{
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' },
@@ -39,7 +39,6 @@ RSpec.describe 'LDAP modules' do
/Running ENUM_ACCOUNTS.../,
/Running ENUM_USER_SPNS_KERBEROAST.../,
/Running ENUM_USER_PASSWORD_NOT_REQUIRED.../,
]
}
}
@@ -47,7 +46,7 @@ RSpec.describe 'LDAP modules' do
{
name: 'auxiliary/gather/ldap_query',
platforms: %i[linux osx windows],
targets: [:rhost],
targets: [:session, :rhost],
skipped: false,
action: 'enum_accounts',
lines: {
@@ -62,13 +61,11 @@ RSpec.describe 'LDAP modules' do
{
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'./
@@ -79,13 +76,12 @@ RSpec.describe 'LDAP modules' do
{
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./
]
@@ -338,7 +334,9 @@ RSpec.describe 'LDAP modules' do
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)
+88 -84
View File
@@ -3,15 +3,19 @@
require 'spec_helper'
RSpec.describe Msf::Exploit::Remote::LDAP do
include_context 'Msf::Simple::Framework'
subject do
mod = ::Msf::Exploit.new
mod.extend described_class
mod.send(:initialize)
mod
end
before(:each) do
allow(subject).to receive(:framework).and_return(framework)
end
let(:rhost) do
'rhost.example.com'
end
@@ -115,90 +119,90 @@ RSpec.describe Msf::Exploit::Remote::LDAP do
end
end
describe '#get_naming_contexts' do
let(:ldap) do
instance_double(Net::LDAP)
end
context 'Could not retrieve root DSE' do
it do
expect(ldap).to receive(:search_root_dse).and_return(false)
expect(subject.get_naming_contexts(ldap)).to be(nil)
end
end
# describe '#get_naming_contexts' do
# let(:ldap) do
# instance_double(Net::LDAP)
# end
# context 'Could not retrieve root DSE' do
# it do
# expect(ldap).to receive(:search_root_dse).and_return(false)
# expect(subject.get_naming_contexts(ldap)).to be(nil)
# end
# end
context 'Empty naming contexts' do
let(:root_dse) do
{ namingcontexts: [] }
end
it do
expect(ldap).to receive(:search_root_dse).and_return(root_dse)
expect(subject.get_naming_contexts(ldap)).to be(nil)
end
end
# context 'Empty naming contexts' do
# let(:root_dse) do
# { namingcontexts: [] }
# end
# it do
# expect(ldap).to receive(:search_root_dse).and_return(root_dse)
# expect(subject.get_naming_contexts(ldap)).to be(nil)
# end
# end
context 'Naming contexts are present' do
# context 'Naming contexts are present' do
#
# let(:naming_contexts) {
# %w[context1 context2]
# }
#
# let(:root_dse) do
# { namingcontexts: naming_contexts }
# end
#
# it do
# expect(ldap).to receive(:search_root_dse).and_return(root_dse)
# expect(subject.get_naming_contexts(ldap)).to be(naming_contexts)
# end
# end
# end
let(:naming_contexts) {
%w[context1 context2]
}
let(:root_dse) do
{ namingcontexts: naming_contexts }
end
it do
expect(ldap).to receive(:search_root_dse).and_return(root_dse)
expect(subject.get_naming_contexts(ldap)).to be(naming_contexts)
end
end
end
describe '#discover_base_dn' do
let(:ldap) do
instance_double(Net::LDAP)
end
context 'No naming contexts' do
it do
expect(subject).to receive(:get_naming_contexts).with(ldap).and_return(nil)
expect(subject.discover_base_dn(ldap)).to be(nil)
end
end
context 'Invalid naming contexts' do
let(:invalid_naming_contexts) do
%w[invalid1 invalid2]
end
it do
expect(subject).to receive(:get_naming_contexts).with(ldap).and_return(invalid_naming_contexts)
expect(subject.discover_base_dn(ldap)).to be(nil)
end
end
context 'Valid naming contexts' do
let(:base_dn) do
'DC=abcdef'
end
let(:valid_naming_contexts) do
[base_dn]
end
it do
expect(subject).to receive(:get_naming_contexts).with(ldap).and_return(valid_naming_contexts)
expect(subject.discover_base_dn(ldap)).to be(base_dn)
end
end
context 'Valid naming contexts (lowercase dc)' do
let(:base_dn) do
'dc=abcdef'
end
let(:valid_naming_contexts) do
[base_dn]
end
it do
expect(subject).to receive(:get_naming_contexts).with(ldap).and_return(valid_naming_contexts)
expect(subject.discover_base_dn(ldap)).to be(base_dn)
end
end
end
# describe '#discover_base_dn' do
# let(:ldap) do
# instance_double(Net::LDAP)
# end
#
# context 'No naming contexts' do
# it do
# expect(subject).to receive(:get_naming_contexts).with(ldap).and_return(nil)
# expect(subject.discover_base_dn(ldap)).to be(nil)
# end
# end
#
# context 'Invalid naming contexts' do
# let(:invalid_naming_contexts) do
# %w[invalid1 invalid2]
# end
# it do
# expect(subject).to receive(:get_naming_contexts).with(ldap).and_return(invalid_naming_contexts)
# expect(subject.discover_base_dn(ldap)).to be(nil)
# end
# end
#
# context 'Valid naming contexts' do
# let(:base_dn) do
# 'DC=abcdef'
# end
# let(:valid_naming_contexts) do
# [base_dn]
# end
# it do
# expect(subject).to receive(:get_naming_contexts).with(ldap).and_return(valid_naming_contexts)
# expect(subject.discover_base_dn(ldap)).to be(base_dn)
# end
# end
#
# context 'Valid naming contexts (lowercase dc)' do
# let(:base_dn) do
# 'dc=abcdef'
# end
# let(:valid_naming_contexts) do
# [base_dn]
# end
# it do
# expect(subject).to receive(:get_naming_contexts).with(ldap).and_return(valid_naming_contexts)
# expect(subject.discover_base_dn(ldap)).to be(base_dn)
# end
# end
# end
end
+141
View File
@@ -14,4 +14,145 @@ RSpec.describe Rex::Proto::LDAP::Client do
end
it_behaves_like 'session compatible client'
let(:base_dn) { 'DC=ldap,DC=example,DC=com' }
let(:schema_dn) { 'CN=Schema,CN=Configuration,DC=ldap,DC=example,DC=com' }
let(:root_dse_result_ldif) do
"dn: \n" \
"namingcontexts: #{base_dn}\n" \
"namingcontexts: CN=Configuration,DC=ldap,DC=example,DC=com\n" \
"namingcontexts: CN=Schema,CN=Configuration,DC=ldap,DC=example,DC=com\n" \
"namingcontexts: DC=DomainDnsZones,DC=ldap,DC=example,DC=com\n" \
"namingcontexts: DC=ForestDnsZones,DC=ldap,DC=example,DC=com\n" \
"supportedldapversion: 2\n" \
"supportedldapversion: 3\n" \
"supportedsaslmechanisms: GSS-SPNEGO\n" \
"supportedsaslmechanisms: GSSAPI\n" \
"supportedsaslmechanisms: NTLM\n"
end
let(:schema_naming_context) do
"dn: \n" \
"schemanamingcontext: #{schema_dn}\n"
end
let(:empty_response) do
"dn: \n"
end
let(:schema_naming_context_result) do
root_dse_dataset = Net::LDAP::Dataset.read_ldif(StringIO.new(schema_naming_context))
root_dse_dataset.to_entries
end
let(:root_dse_result) do
root_dse_dataset = Net::LDAP::Dataset.read_ldif(StringIO.new(root_dse_result_ldif))
root_dse_dataset.to_entries[0]
end
let(:empty_response_result) do
root_dse_dataset = Net::LDAP::Dataset.read_ldif(StringIO.new(empty_response))
root_dse_dataset.to_entries
end
describe '#naming_contexts' do
before(:each) do
allow(subject).to receive(:search_root_dse).and_return(root_dse_result)
end
it 'should cache the result' do
expect(subject).to receive(:search_root_dse)
subject.naming_contexts
expect(subject).not_to receive(:search_root_dse)
subject.naming_contexts
end
context 'when no naming contexts are available' do
let(:root_dse_result_ldif) do
"dn: \n" \
"supportedldapversion: 2\n" \
"supportedldapversion: 3\n" \
"supportedsaslmechanisms: GSS-SPNEGO\n" \
"supportedsaslmechanisms: GSSAPI\n" \
"supportedsaslmechanisms: NTLM\n"
end
it 'returns an empty array' do
expect(subject.naming_contexts).to be_empty
end
end
context 'when naming contexts are available' do
it 'contains naming contexts' do
expect(subject.naming_contexts).not_to be_empty
end
end
end
describe '#base_dn' do
before(:each) do
allow(subject).to receive(:search_root_dse).and_return(root_dse_result)
end
it 'should cache the result' do
expect(subject).to receive(:discover_base_dn).and_call_original
subject.base_dn
expect(subject).not_to receive(:discover_base_dn)
subject.base_dn
end
context 'when no naming contexts are available' do
let(:root_dse_result_ldif) do
"dn: \n" \
"supportedldapversion: 2\n" \
"supportedldapversion: 3\n" \
"supportedsaslmechanisms: GSS-SPNEGO\n" \
"supportedsaslmechanisms: GSSAPI\n" \
"supportedsaslmechanisms: NTLM\n"
end
it 'should not find the base dn' do
expect(subject.base_dn).to be_nil
end
end
context 'when naming contexts are available' do
it 'contains naming contexts' do
expect(subject.base_dn).to eql(base_dn)
end
end
end
describe '#schema_dn' do
before(:each) do
allow(subject).to receive(:search).and_return(schema_naming_context_result)
end
it 'should cache the result' do
expect(subject).to receive(:discover_schema_naming_context).and_call_original
subject.schema_dn
expect(subject).not_to receive(:discover_schema_naming_context)
subject.schema_dn
end
context 'when the response does not contain the schema_dn' do
before(:each) do
allow(subject).to receive(:search).and_return(empty_response_result)
end
it 'does not find the schema_dn' do
expect(subject.schema_dn).to be_nil
end
end
context 'when the response does contain the schema_dn' do
it 'finds the schema_dn' do
expect(subject.schema_dn).to eql(schema_dn)
end
end
end
end
-2
View File
@@ -1,5 +1,3 @@
version: '3.7'
services:
ldap:
tty: true
-2
View File
@@ -1,5 +1,3 @@
version: '3.7'
services:
samba:
tty: true