Skip to content

Commit db064a4

Browse files
authored
Merge pull request #20895 from rudraditya21/fixed/ldap-entry-cache-misses
added: negative caching for LDAP lookup misses
2 parents b92da7c + 674934d commit db064a4

File tree

4 files changed

+138
-8
lines changed

4 files changed

+138
-8
lines changed

lib/msf/core/exploit/remote/ldap/active_directory.rb

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,12 +156,17 @@ def adds_query_member_groups(ldap, member_dn, base_dn: nil, inherited: true)
156156
# @rtype [Net::LDAP::Entry,nil]
157157
def adds_get_object_by_dn(ldap, object_dn)
158158
object = ldap_entry_cache.get_by_dn(object_dn)
159+
return nil if ldap_entry_cache.missing_entry?(object)
159160
return object if object
160161

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

164-
ldap_entry_cache << object if object
165+
if object
166+
ldap_entry_cache << object
167+
else
168+
ldap_entry_cache.mark_missing_by_dn(object_dn)
169+
end
165170
object
166171
end
167172

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

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

187-
ldap_entry_cache << object if object
193+
if object
194+
ldap_entry_cache << object
195+
else
196+
ldap_entry_cache.mark_missing_by_samaccountname(object_samaccountname)
197+
end
188198
object
189199
end
190200

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

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

206-
ldap_entry_cache << object if object
217+
if object
218+
ldap_entry_cache << object
219+
else
220+
ldap_entry_cache.mark_missing_by_sid(object_sid)
221+
end
207222
object
208223
end
209224

lib/msf/core/exploit/remote/ldap/entry_cache.rb

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ module Msf
99
module Exploit::Remote::LDAP
1010
module EntryCache
1111
class LDAPEntryCache < LruRedux::Cache
12+
MissingEntry = Object.new.freeze
13+
14+
def initialize(max_size: 1000)
15+
super(max_size)
16+
@missing_samaccountname = LruRedux::Cache.new(max_size)
17+
@missing_sid = LruRedux::Cache.new(max_size)
18+
end
19+
1220
def <<(entry)
1321
raise TypeError unless entry.is_a? Net::LDAP::Entry
1422

@@ -20,22 +28,43 @@ def get_by_dn(dn)
2028
end
2129

2230
def get_by_samaccountname(samaccountname)
23-
entry = @data.values.reverse_each.find { _1[:sAMAccountName]&.first == samaccountname }
31+
entry = @data.values.reverse_each.find { _1.is_a?(Net::LDAP::Entry) && _1[:sAMAccountName]&.first == samaccountname }
2432
@data[entry.dn] = entry if entry # update it as recently used
25-
entry
33+
return entry if entry
34+
35+
MissingEntry if @missing_samaccountname[samaccountname]
2636
end
2737

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

31-
entry = @data.values.reverse_each.find { _1[:objectSid]&.first == sid.to_binary_s }
41+
entry = @data.values.reverse_each.find { _1.is_a?(Net::LDAP::Entry) && _1[:objectSid]&.first == sid.to_binary_s }
3242
@data[entry.dn] = entry if entry # update it as recently used
33-
entry
43+
return entry if entry
44+
45+
MissingEntry if @missing_sid[sid.to_s]
46+
end
47+
48+
def mark_missing_by_dn(dn)
49+
self[dn] = MissingEntry
50+
end
51+
52+
def mark_missing_by_samaccountname(samaccountname)
53+
@missing_samaccountname[samaccountname] = true
54+
end
55+
56+
def mark_missing_by_sid(sid)
57+
sid = Rex::Proto::MsDtyp::MsDtypSid.new(sid)
58+
@missing_sid[sid.to_s] = true
59+
end
60+
61+
def missing_entry?(entry)
62+
entry.equal?(MissingEntry)
3463
end
3564
end
3665

3766
def ldap_entry_cache
38-
@ldap_entry_cache ||= LDAPEntryCache.new(1000)
67+
@ldap_entry_cache ||= LDAPEntryCache.new(max_size: 1000)
3968
end
4069
end
4170
end
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
require 'net/ldap'
5+
require 'msf/core/exploit/remote/ldap/active_directory'
6+
7+
RSpec.describe Msf::Exploit::Remote::LDAP::ActiveDirectory do
8+
subject(:helper) { Object.new.extend(described_class) }
9+
10+
describe '#adds_get_object_by_samaccountname' do
11+
it 'returns nil without querying LDAP when a miss is cached' do
12+
ldap = Object.new
13+
helper.ldap_entry_cache.mark_missing_by_samaccountname('missing')
14+
15+
expect(helper.adds_get_object_by_samaccountname(ldap, 'missing')).to be_nil
16+
end
17+
end
18+
19+
describe '#adds_get_object_by_sid' do
20+
it 'returns nil without querying LDAP when a miss is cached' do
21+
ldap = Object.new
22+
helper.ldap_entry_cache.mark_missing_by_sid('S-1-5-21-111111111-222222222-333333333-4444')
23+
24+
expect(helper.adds_get_object_by_sid(ldap, 'S-1-5-21-111111111-222222222-333333333-4444')).to be_nil
25+
end
26+
end
27+
28+
describe '#adds_get_object_by_dn' do
29+
it 'returns nil without querying LDAP when a miss is cached' do
30+
ldap = Object.new
31+
helper.ldap_entry_cache.mark_missing_by_dn('cn=Missing,dc=example,dc=com')
32+
33+
expect(helper.adds_get_object_by_dn(ldap, 'cn=Missing,dc=example,dc=com')).to be_nil
34+
end
35+
end
36+
end
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
require 'net/ldap'
5+
require 'msf/core/exploit/remote/ldap/entry_cache'
6+
7+
RSpec.describe Msf::Exploit::Remote::LDAP::EntryCache::LDAPEntryCache do
8+
subject(:cache) { described_class.new(max_size: 1000) }
9+
10+
let(:dn) { 'cn=User One,dc=example,dc=com' }
11+
let(:samaccountname) { 'user1' }
12+
let(:sid) { 'S-1-5-21-111111111-222222222-333333333-4444' }
13+
14+
describe '#get_by_dn' do
15+
it 'returns a missing sentinel when a DN miss is cached' do
16+
cache.mark_missing_by_dn(dn)
17+
expect(cache.get_by_dn(dn)).to eq described_class::MissingEntry
18+
end
19+
end
20+
21+
describe '#get_by_samaccountname' do
22+
it 'returns a cached entry when present' do
23+
entry = Net::LDAP::Entry.new(dn)
24+
entry[:sAMAccountName] = [samaccountname]
25+
cache << entry
26+
27+
expect(cache.get_by_samaccountname(samaccountname)).to eq entry
28+
end
29+
30+
it 'returns a missing sentinel when a sAMAccountName miss is cached' do
31+
cache.mark_missing_by_samaccountname(samaccountname)
32+
expect(cache.get_by_samaccountname(samaccountname)).to eq described_class::MissingEntry
33+
end
34+
end
35+
36+
describe '#get_by_sid' do
37+
it 'returns a cached entry when present' do
38+
entry = Net::LDAP::Entry.new(dn)
39+
entry[:objectSid] = [Rex::Proto::MsDtyp::MsDtypSid.new(sid).to_binary_s]
40+
cache << entry
41+
42+
expect(cache.get_by_sid(sid)).to eq entry
43+
end
44+
45+
it 'returns a missing sentinel when a SID miss is cached' do
46+
cache.mark_missing_by_sid(sid)
47+
expect(cache.get_by_sid(sid)).to eq described_class::MissingEntry
48+
end
49+
end
50+
end

0 commit comments

Comments
 (0)