Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
3 changes: 3 additions & 0 deletions Sharphound.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@
<!-- <Reference Include="SharpHoundCommon">-->
<!-- <HintPath>..\SharpHoundCommon\src\CommonLib\bin\Debug\net472\SharpHoundCommonLib.dll</HintPath>-->
<!-- </Reference>-->
<!-- <Reference Include="SharpHoundRPC">-->
<!-- <HintPath>..\SharpHoundCommon\src\SharpHoundRPC\bin\Debug\net472\SharpHoundRPC.dll</HintPath>-->
<!-- </Reference>-->
<Reference Include="System.DirectoryServices" />
<Reference Include="System.DirectoryServices.Protocols" />
<Reference Include="System.IO.Compression" />
Expand Down
17 changes: 10 additions & 7 deletions src/Runtime/ObjectProcessors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using SharpHoundCommonLib.Enums;
using SharpHoundCommonLib.OutputTypes;
using SharpHoundCommonLib.Processors;
using SharpHoundRPC.Registry;
using SharpHoundRPC.Wrappers;
using Container = SharpHoundCommonLib.OutputTypes.Container;
using Group = SharpHoundCommonLib.OutputTypes.Group;
Expand Down Expand Up @@ -85,6 +86,9 @@ internal void ClearEventHandlers() {
_spnProcessor.ComputerStatusEvent -= HandleCompStatusEvent;
_ldapPropertyProcessor.ComputerStatusEvent -= HandleCompStatusEvent;
_certAbuseProcessor.ComputerStatusEvent -= HandleCompStatusEvent;
foreach (var registryProcessor in _registryProcessorMap.Values) {
registryProcessor.ComputerStatusEvent -= HandleCompStatusEvent;
}
}

private async Task HandleCompStatusEvent(CSVComputerStatus status) {
Expand Down Expand Up @@ -372,13 +376,12 @@ await HandleCompStatusEvent(new CSVComputerStatus {

if (_methods.HasFlag(CollectionMethod.NTLMRegistry)) {
await _context.DoDelay();
if (_registryProcessorMap.TryGetValue(resolvedSearchResult.DomainSid, out var processor)) {
ret.NTLMRegistryData = await processor.ReadRegistrySettings(resolvedSearchResult.DisplayName);
} else {
var newProcessor = new RegistryProcessor(null, resolvedSearchResult.Domain);
_registryProcessorMap.TryAdd(resolvedSearchResult.DomainSid, newProcessor);
ret.NTLMRegistryData = await newProcessor.ReadRegistrySettings(resolvedSearchResult.DisplayName);
}
var processor = _registryProcessorMap.GetOrAdd(resolvedSearchResult.DomainSid, _ => {
var newProcessor = new RegistryProcessor(null, new StrategyExecutor(), resolvedSearchResult.Domain);
newProcessor.ComputerStatusEvent += HandleCompStatusEvent;
return newProcessor;
});
Comment on lines +379 to +383
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

git ls-files | grep -E "ObjectProcessors|RegistryProcessor"

Repository: SpecterOps/SharpHound

Length of output: 96


🏁 Script executed:

head -50 src/Runtime/ObjectProcessors.cs | cat -n

Repository: SpecterOps/SharpHound

Length of output: 2901


🏁 Script executed:

sed -n '35,55p' src/Runtime/ObjectProcessors.cs | cat -n

Repository: SpecterOps/SharpHound

Length of output: 1728


🏁 Script executed:

sed -n '370,395p' src/Runtime/ObjectProcessors.cs | cat -n

Repository: SpecterOps/SharpHound

Length of output: 1664


🏁 Script executed:

sed -n '85,100p' src/Runtime/ObjectProcessors.cs | cat -n

Repository: SpecterOps/SharpHound

Length of output: 998


🏁 Script executed:

rg -n "class RegistryProcessor" --type=cs

Repository: SpecterOps/SharpHound

Length of output: 47


🏁 Script executed:

rg -n "class StrategyExecutor" --type=cs

Repository: SpecterOps/SharpHound

Length of output: 47


🏁 Script executed:

rg -n "public RegistryProcessor" --type=cs -A 5

Repository: SpecterOps/SharpHound

Length of output: 47


🏁 Script executed:

fd -type f -name "*.cs" | xargs rg -l "class RegistryProcessor" 2>/dev/null

Repository: SpecterOps/SharpHound

Length of output: 237


🏁 Script executed:

fd -type f -name "*.cs" | xargs rg -l "class StrategyExecutor" 2>/dev/null

Repository: SpecterOps/SharpHound

Length of output: 237


🏁 Script executed:

find . -name "*.csproj" -o -name "*.sln" | head -5

Repository: SpecterOps/SharpHound

Length of output: 101


🏁 Script executed:

cat .csproj 2>/dev/null || cat *.csproj 2>/dev/null | head -100

Repository: SpecterOps/SharpHound

Length of output: 2726


🏁 Script executed:

git ls-files | grep -i "csproj\|packages"

Repository: SpecterOps/SharpHound

Length of output: 82


🏁 Script executed:

rg -n "RegistryProcessor" --type-add 'src:*.cs'

Repository: SpecterOps/SharpHound

Length of output: 980


🏁 Script executed:

rg -n "StrategyExecutor" --type-add 'src:*.cs'

Repository: SpecterOps/SharpHound

Length of output: 221


🏁 Script executed:

rg -n "GetOrAdd" --type-add 'src:*.cs'

Repository: SpecterOps/SharpHound

Length of output: 202


🏁 Script executed:

rg -n "ClearEventHandlers" --type-add 'src:*.cs'

Repository: SpecterOps/SharpHound

Length of output: 297


🏁 Script executed:

rg -n "_registryProcessorMap" --type-add 'src:*.cs'

Repository: SpecterOps/SharpHound

Length of output: 454


🏁 Script executed:

sed -n '375,390p' src/Runtime/ObjectProcessors.cs | cat -n

Repository: SpecterOps/SharpHound

Length of output: 971


🏁 Script executed:

sed -n '77,95p' src/Runtime/ObjectProcessors.cs | cat -n

Repository: SpecterOps/SharpHound

Length of output: 1253


GetOrAdd factory may run multiple times for the same key, leaking processors with dangling event subscriptions.

ConcurrentDictionary.GetOrAdd(key, factory) does not guarantee the factory executes only once per key under concurrent access. If two threads hit this for the same DomainSid simultaneously, both may create a RegistryProcessor and subscribe to ComputerStatusEvent, but only one is stored. The discarded instance retains its subscription — it is never unsubscribed in ClearEventHandlers (which only iterates the map) and may produce duplicate status events.

Wrap the value in Lazy<T> so the actual construction + subscription happens exactly once:

Proposed fix using Lazy<T>

Change the field declaration (line 44):

-private readonly ConcurrentDictionary<string, RegistryProcessor> _registryProcessorMap = new();
+private readonly ConcurrentDictionary<string, Lazy<RegistryProcessor>> _registryProcessorMap = new();

Update the GetOrAdd call (lines 379–384):

-var processor = _registryProcessorMap.GetOrAdd(resolvedSearchResult.DomainSid, _ => {
-    var newProcessor = new RegistryProcessor(null, new StrategyExecutor(), resolvedSearchResult.Domain);
-    newProcessor.ComputerStatusEvent += HandleCompStatusEvent;
-    return newProcessor;
-});
-ret.NTLMRegistryData = await processor.ReadRegistrySettings(resolvedSearchResult.DisplayName);
+var processor = _registryProcessorMap.GetOrAdd(resolvedSearchResult.DomainSid, _ =>
+    new Lazy<RegistryProcessor>(() => {
+        var newProcessor = new RegistryProcessor(null, new StrategyExecutor(), resolvedSearchResult.Domain);
+        newProcessor.ComputerStatusEvent += HandleCompStatusEvent;
+        return newProcessor;
+    })).Value;
+ret.NTLMRegistryData = await processor.ReadRegistrySettings(resolvedSearchResult.DisplayName);

Update ClearEventHandlers (lines 89–91):

-foreach (var registryProcessor in _registryProcessorMap.Values) {
-    registryProcessor.ComputerStatusEvent -= HandleCompStatusEvent;
+foreach (var lazy in _registryProcessorMap.Values) {
+    if (lazy.IsValueCreated) {
+        lazy.Value.ComputerStatusEvent -= HandleCompStatusEvent;
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Runtime/ObjectProcessors.cs` around lines 379 - 383, The current use of
_registryProcessorMap.GetOrAdd with a factory can create multiple
RegistryProcessor instances that subscribe to ComputerStatusEvent and leak if
discarded; change the dictionary to store Lazy<RegistryProcessor> instead,
update the GetOrAdd call to add a new Lazy that constructs the RegistryProcessor
and subscribes to ComputerStatusEvent inside the Lazy factory (using
resolvedSearchResult.Domain and DomainSid), and update any code that reads
processors to use .Value (e.g., when accessing the processor) as well as modify
ClearEventHandlers to iterate the stored Lazy values and unsubscribe from each
Lazy.Value.ComputerStatusEvent (or otherwise access Value once) so that
construction and subscription happen exactly once per key.

ret.NTLMRegistryData = await processor.ReadRegistrySettings(resolvedSearchResult.DisplayName);
}

if (_methods.HasFlag(CollectionMethod.WebClientService)) {
Expand Down