Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions lib/msf/core/exploit/remote/ldap/active_directory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -156,12 +156,17 @@ def adds_query_member_groups(ldap, member_dn, base_dn: nil, inherited: true)
# @rtype [Net::LDAP::Entry,nil]
def adds_get_object_by_dn(ldap, object_dn)
object = ldap_entry_cache.get_by_dn(object_dn)
return nil if ldap_entry_cache.missing_entry?(object)
return object if object

object = ldap.search(base: object_dn, controls: [adds_build_ldap_sd_control], scope: Net::LDAP::SearchScope_BaseObject)&.first
validate_query_result!(ldap.get_operation_result.table)

ldap_entry_cache << object if object
if object
ldap_entry_cache << object
else
ldap_entry_cache.mark_missing_by_dn(object_dn)
end
object
end

Expand All @@ -173,6 +178,7 @@ def adds_get_object_by_dn(ldap, object_dn)
# @rtype [Net::LDAP::Entry,nil]
def adds_get_object_by_samaccountname(ldap, object_samaccountname)
object = ldap_entry_cache.get_by_samaccountname(object_samaccountname)
return nil if ldap_entry_cache.missing_entry?(object)
return object if object

filter = "(sAMAccountName=#{ldap_escape_filter(object_samaccountname)})"
Expand All @@ -184,7 +190,11 @@ def adds_get_object_by_samaccountname(ldap, object_samaccountname)
end
validate_query_result!(ldap.get_operation_result.table, filter)

ldap_entry_cache << object if object
if object
ldap_entry_cache << object
else
ldap_entry_cache.mark_missing_by_samaccountname(object_samaccountname)
end
object
end

Expand All @@ -197,13 +207,18 @@ def adds_get_object_by_samaccountname(ldap, object_samaccountname)
def adds_get_object_by_sid(ldap, object_sid)
object_sid = Rex::Proto::MsDtyp::MsDtypSid.new(object_sid)
object = ldap_entry_cache.get_by_sid(object_sid)
return nil if ldap_entry_cache.missing_entry?(object)
return object if object

filter = "(objectSID=#{ldap_escape_filter(object_sid.to_s)})"
object = ldap.search(base: ldap.base_dn, controls: [adds_build_ldap_sd_control], filter: filter)&.first
validate_query_result!(ldap.get_operation_result.table, filter)

ldap_entry_cache << object if object
if object
ldap_entry_cache << object
else
ldap_entry_cache.mark_missing_by_sid(object_sid)
end
object
end

Expand Down
38 changes: 34 additions & 4 deletions lib/msf/core/exploit/remote/ldap/entry_cache.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ module Msf
module Exploit::Remote::LDAP
module EntryCache
class LDAPEntryCache < LruRedux::Cache
MissingEntry = Object.new.freeze

def initialize(*args)
super
max_size = args.first || 1000
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we pass in the key word argument max_size and reference it here instead of .first?

@missing_samaccountname = LruRedux::Cache.new(max_size)
@missing_sid = LruRedux::Cache.new(max_size)
end

def <<(entry)
raise TypeError unless entry.is_a? Net::LDAP::Entry

Expand All @@ -20,17 +29,38 @@ def get_by_dn(dn)
end

def get_by_samaccountname(samaccountname)
entry = @data.values.reverse_each.find { _1[:sAMAccountName]&.first == samaccountname }
entry = @data.values.reverse_each.find { _1.is_a?(Net::LDAP::Entry) && _1[:sAMAccountName]&.first == samaccountname }
@data[entry.dn] = entry if entry # update it as recently used
entry
return entry if entry

MissingEntry if @missing_samaccountname[samaccountname]
end

def get_by_sid(sid)
sid = Rex::Proto::MsDtyp::MsDtypSid.new(sid)

entry = @data.values.reverse_each.find { _1[:objectSid]&.first == sid.to_binary_s }
entry = @data.values.reverse_each.find { _1.is_a?(Net::LDAP::Entry) && _1[:objectSid]&.first == sid.to_binary_s }
@data[entry.dn] = entry if entry # update it as recently used
entry
return entry if entry

MissingEntry if @missing_sid[sid.to_s]
end

def mark_missing_by_dn(dn)
self[dn] = MissingEntry
end

def mark_missing_by_samaccountname(samaccountname)
@missing_samaccountname[samaccountname] = true
end

def mark_missing_by_sid(sid)
sid = Rex::Proto::MsDtyp::MsDtypSid.new(sid)
@missing_sid[sid.to_s] = true
end

def missing_entry?(entry)
entry.equal?(MissingEntry)
end
end

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

require 'spec_helper'
require 'net/ldap'
require 'msf/core/exploit/remote/ldap/active_directory'

RSpec.describe Msf::Exploit::Remote::LDAP::ActiveDirectory do
subject(:helper) { Object.new.extend(described_class) }

describe '#adds_get_object_by_samaccountname' do
it 'returns nil without querying LDAP when a miss is cached' do
ldap = Object.new
helper.ldap_entry_cache.mark_missing_by_samaccountname('missing')

expect(helper.adds_get_object_by_samaccountname(ldap, 'missing')).to be_nil
end
end

describe '#adds_get_object_by_sid' do
it 'returns nil without querying LDAP when a miss is cached' do
ldap = Object.new
helper.ldap_entry_cache.mark_missing_by_sid('S-1-5-21-111111111-222222222-333333333-4444')

expect(helper.adds_get_object_by_sid(ldap, 'S-1-5-21-111111111-222222222-333333333-4444')).to be_nil
end
end

describe '#adds_get_object_by_dn' do
it 'returns nil without querying LDAP when a miss is cached' do
ldap = Object.new
helper.ldap_entry_cache.mark_missing_by_dn('cn=Missing,dc=example,dc=com')

expect(helper.adds_get_object_by_dn(ldap, 'cn=Missing,dc=example,dc=com')).to be_nil
end
end
end
50 changes: 50 additions & 0 deletions spec/lib/msf/core/exploit/remote/ldap/entry_cache_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# frozen_string_literal: true

require 'spec_helper'
require 'net/ldap'
require 'msf/core/exploit/remote/ldap/entry_cache'

RSpec.describe Msf::Exploit::Remote::LDAP::EntryCache::LDAPEntryCache do
subject(:cache) { described_class.new(1000) }

let(:dn) { 'cn=User One,dc=example,dc=com' }
let(:samaccountname) { 'user1' }
let(:sid) { 'S-1-5-21-111111111-222222222-333333333-4444' }

describe '#get_by_dn' do
it 'returns a missing sentinel when a DN miss is cached' do
cache.mark_missing_by_dn(dn)
expect(cache.get_by_dn(dn)).to eq described_class::MissingEntry
end
end

describe '#get_by_samaccountname' do
it 'returns a cached entry when present' do
entry = Net::LDAP::Entry.new(dn)
entry[:sAMAccountName] = [samaccountname]
cache << entry

expect(cache.get_by_samaccountname(samaccountname)).to eq entry
end

it 'returns a missing sentinel when a sAMAccountName miss is cached' do
cache.mark_missing_by_samaccountname(samaccountname)
expect(cache.get_by_samaccountname(samaccountname)).to eq described_class::MissingEntry
end
end

describe '#get_by_sid' do
it 'returns a cached entry when present' do
entry = Net::LDAP::Entry.new(dn)
entry[:objectSid] = [Rex::Proto::MsDtyp::MsDtypSid.new(sid).to_binary_s]
cache << entry

expect(cache.get_by_sid(sid)).to eq entry
end

it 'returns a missing sentinel when a SID miss is cached' do
cache.mark_missing_by_sid(sid)
expect(cache.get_by_sid(sid)).to eq described_class::MissingEntry
end
end
end
Loading