Skip to content
Merged
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
71 changes: 56 additions & 15 deletions src/CommonLib/Cache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,22 +46,49 @@ private Cache()
[IgnoreDataMember] private static Cache CacheInstance { get; set; }

/// <summary>
/// Add a SID/Domain-name pair to the cache. Writes both directions (SID→Name and
/// Name→SID) so a successful resolution by any tier benefits subsequent lookups in
/// either direction. Existing entries are preserved (TryAdd semantics) — first
/// resolver wins.
/// Add a SID/Domain-name pair to the cache. The Name→SID direction is always written
/// (NetBIOS aliases are valid lookup keys). The SID→Name direction is only written
/// when the name is a DNS-shaped FQDN: a NetBIOS-keyed reverse write would poison
/// the slot for downstream consumers that depend on the SID→Name lookup yielding a
/// DNS name (LDAP base DN construction, server selection, GetDomainInfoAsync hints).
/// Existing entries are preserved (TryAdd semantics) — first resolver wins.
/// </summary>
/// <param name="key">A SID or a domain name.</param>
/// <param name="value">The corresponding domain name or SID.</param>
internal static void AddDomainSidMapping(string key, string value)
{
if (CacheInstance == null) return;
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(value)) return;
CacheInstance.SIDToDomainCache.TryAdd(key, value);
// Bidirectional: SIDs (S-…) and DNS domain names cannot collide as keys, so writing
// the reverse mapping makes a successful resolution by any tier visible to lookups
// in either direction.
CacheInstance.SIDToDomainCache.TryAdd(value, key);

var keyIsSid = LooksLikeDomainSid(key);
var valueIsSid = LooksLikeDomainSid(value);

if (keyIsSid == valueIsSid)
{
// Both look like SIDs or neither does — caller misuse or an unexpected input
// shape. Throw this data out
return;
}

var sid = keyIsSid ? key : value;
var name = keyIsSid ? value : key;

CacheInstance.SIDToDomainCache.TryAdd(name, sid);

if (LooksLikeDnsDomainName(name))
{
CacheInstance.SIDToDomainCache.TryAdd(sid, name);
}
}

private static bool LooksLikeDomainSid(string value)
{
return value != null && value.StartsWith("S-1-", StringComparison.OrdinalIgnoreCase);
}

private static bool LooksLikeDnsDomainName(string value)
{
return value != null && value.IndexOf('.') >= 0;
}

/// <summary>
Expand Down Expand Up @@ -177,19 +204,33 @@ private static void NormalizeCaseInsensitiveCaches()
if (CacheInstance == null) return;
if (CacheInstance.SIDToDomainCache != null)
{
CacheInstance.SIDToDomainCache = new ConcurrentDictionary<string, string>(
CacheInstance.SIDToDomainCache, StringComparer.OrdinalIgnoreCase);
CacheInstance.SIDToDomainCache = CopyCaseInsensitive(CacheInstance.SIDToDomainCache);
}
if (CacheInstance.GlobalCatalogCache != null)
{
CacheInstance.GlobalCatalogCache = new ConcurrentDictionary<string, string[]>(
CacheInstance.GlobalCatalogCache, StringComparer.OrdinalIgnoreCase);
CacheInstance.GlobalCatalogCache = CopyCaseInsensitive(CacheInstance.GlobalCatalogCache);
}
if (CacheInstance.ValueToIdCache != null)
{
CacheInstance.ValueToIdCache = new ConcurrentDictionary<string, string>(
CacheInstance.ValueToIdCache, StringComparer.OrdinalIgnoreCase);
CacheInstance.ValueToIdCache = CopyCaseInsensitive(CacheInstance.ValueToIdCache);
}
}

/// <summary>
/// Copies <paramref name="source"/> into a new <see cref="ConcurrentDictionary{TKey,TValue}"/> keyed
/// by <see cref="StringComparer.OrdinalIgnoreCase"/>. Entries are added with TryAdd so keys that
/// collide only by case (introduced before the case-insensitive invariant was reapplied) are
/// silently dropped — first writer wins — rather than throwing from the constructor.
/// </summary>
private static ConcurrentDictionary<string, TValue> CopyCaseInsensitive<TValue>(
ConcurrentDictionary<string, TValue> source)
{
var copy = new ConcurrentDictionary<string, TValue>(StringComparer.OrdinalIgnoreCase);
foreach (var kvp in source)
{
copy.TryAdd(kvp.Key, kvp.Value);
}
return copy;
}

/// <summary>
Expand Down
7 changes: 7 additions & 0 deletions src/CommonLib/ConnectionPoolManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,13 @@ private string ResolveIdentifier(string identifier) {
.GetAwaiter().GetResult();
if (infoOk && !string.IsNullOrEmpty(info?.DomainSid)) {
Cache.AddDomainSidMapping(domainName, info.DomainSid);
// Also seed the canonical FQDN keyed write so the SID->Name slot is populated
// even when the caller passed a NetBIOS alias. AddDomainSidMapping gates the
// SID->Name direction on the name being DNS-shaped.
if (!string.IsNullOrEmpty(info.Name) &&
!string.Equals(domainName, info.Name, StringComparison.OrdinalIgnoreCase)) {
Cache.AddDomainSidMapping(info.Name, info.DomainSid);
}
return (true, info.DomainSid);
}

Expand Down
10 changes: 7 additions & 3 deletions src/CommonLib/LdapConnectionPool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,18 @@ internal class LdapConnectionPool : IDisposable {
private static readonly TimeSpan MaxBackoffDelay = TimeSpan.FromSeconds(20);
private const int BackoffDelayMultiplier = 2;
private const int MaxRetries = 3;
private static readonly ConcurrentDictionary<string, NetAPIStructs.DomainControllerInfo?> DCInfoCache = new();
private static readonly ConcurrentDictionary<string, NetAPIStructs.DomainControllerInfo?> DCInfoCache = new(StringComparer.OrdinalIgnoreCase);

// Metrics
private readonly IMetricRouter _metric;

// Tracks domains we know we've determined we shouldn't try to connect to
// Tracks domains we know we've determined we shouldn't try to connect to.
private static readonly ConcurrentHashSet ExcludedDomains = new(StringComparer.OrdinalIgnoreCase);

// Drops every exclusion record. Called from LdapUtils.ResetUtils so a fresh enumeration
// pass after a configuration change isn't shadowed by stale exclusion state.
internal static void ClearExclusions() => ExcludedDomains.Clear();

public LdapConnectionPool(string identifier, string poolIdentifier, LdapConfig config,
IPortScanner scanner = null, NativeMethods nativeMethods = null, ILogger log = null, IMetricRouter metric = null) {
_connections = [];
Expand Down Expand Up @@ -693,7 +697,7 @@ private static TimeSpan GetNextBackoff(int retryCount) {
string tempPath;
if (CallDsGetDcName(queryParameters.DomainName, out var info) && info != null) {
tempPath = Helpers.DomainNameToDistinguishedName(info.Value.DomainName);
connectionWrapper.SaveContext(queryParameters.NamingContext, basePath);
connectionWrapper.SaveContext(queryParameters.NamingContext, tempPath);
}
else {
// Controlled replacement for LdapUtils.GetDomain + DomainNameToDistinguishedName.
Expand Down
71 changes: 36 additions & 35 deletions src/CommonLib/LdapQueryParameters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,46 +3,47 @@
using System.Threading;
using SharpHoundCommonLib.Enums;

namespace SharpHoundCommonLib {
public class LdapQueryParameters {
private static int _queryIDIndex;
private string _searchBase;
private string _relativeSearchBase;
public string LDAPFilter { get; set; }
public SearchScope SearchScope { get; set; } = SearchScope.Subtree;
public string[] Attributes { get; set; } = Array.Empty<string>();
public string DomainName { get; set; }
public bool GlobalCatalog { get; set; }
public bool IncludeSecurityDescriptor { get; set; } = false;
public bool IncludeDeleted { get; set; } = false;
private int QueryID { get; }

public LdapQueryParameters() {
QueryID = _queryIDIndex;
Interlocked.Increment(ref _queryIDIndex);
}
namespace SharpHoundCommonLib;

public class LdapQueryParameters {
private static int _queryIDIndex;
private string _relativeSearchBase;
private string _searchBase;

public LdapQueryParameters() {
QueryID = _queryIDIndex;
Interlocked.Increment(ref _queryIDIndex);
}

public string SearchBase {
get => _searchBase;
set {
_relativeSearchBase = null;
_searchBase = value;
}
public string LDAPFilter { get; set; }
public SearchScope SearchScope { get; set; } = SearchScope.Subtree;
public string[] Attributes { get; set; } = Array.Empty<string>();
public string DomainName { get; set; }
public bool GlobalCatalog { get; set; }
public bool IncludeSecurityDescriptor { get; set; } = false;
public bool IncludeDeleted { get; set; } = false;
private int QueryID { get; }

public string SearchBase {
get => _searchBase;
set {
_relativeSearchBase = null;
_searchBase = value;
}
}

public string RelativeSearchBase {
get => _relativeSearchBase;
set {
_relativeSearchBase = value;
_searchBase = null;
}
public string RelativeSearchBase {
get => _relativeSearchBase;
set {
_relativeSearchBase = value;
_searchBase = null;
}
}

public NamingContext NamingContext { get; set; } = NamingContext.Default;
public NamingContext NamingContext { get; set; } = NamingContext.Default;

public string GetQueryInfo()
{
return $"Query Information - Filter: {LDAPFilter}, Domain: {DomainName}, GlobalCatalog: {GlobalCatalog}, ADSPath: {SearchBase}, ID: {QueryID}";
}
public string GetQueryInfo() {
return
$"Query Information - Filter: {LDAPFilter}, Domain: {DomainName}, GlobalCatalog: {GlobalCatalog}, ADSPath: {SearchBase}, ID: {QueryID}";
}
}
Loading
Loading