diff --git a/src/CommonLib/ConnectionPoolManager.cs b/src/CommonLib/ConnectionPoolManager.cs index a671d16e..e74526bc 100644 --- a/src/CommonLib/ConnectionPoolManager.cs +++ b/src/CommonLib/ConnectionPoolManager.cs @@ -3,6 +3,8 @@ using System.DirectoryServices; using System.Security.Principal; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using SharpHoundCommonLib.Processors; namespace SharpHoundCommonLib; @@ -10,9 +12,24 @@ public class ConnectionPoolManager : IDisposable{ private readonly ConcurrentDictionary _pools = new(); private readonly LDAPConfig _ldapConfig; private readonly string[] _translateNames = { "Administrator", "admin" }; + private readonly ConcurrentDictionary _resolvedIdentifiers = new(StringComparer.OrdinalIgnoreCase); + private readonly ILogger _log; + private readonly PortScanner _portScanner; - public ConnectionPoolManager(LDAPConfig config) { + public ConnectionPoolManager(LDAPConfig config, ILogger log = null, PortScanner scanner = null) { _ldapConfig = config; + _log = log ?? Logging.LogProvider.CreateLogger("ConnectionPoolManager"); + _portScanner = scanner ?? new PortScanner(); + } + + public void ReleaseConnection(LdapConnectionWrapperNew connectionWrapper, bool connectionFaulted = false) { + //I dont think this is possible, but at least account for it + if (!_pools.TryGetValue(connectionWrapper.PoolIdentifier, out var pool)) { + connectionWrapper.Connection.Dispose(); + return; + } + + pool.ReleaseConnection(connectionWrapper, connectionFaulted); } public async Task<(bool Success, LdapConnectionWrapperNew connectionWrapper, string Message)> GetLdapConnection( @@ -20,7 +37,7 @@ public ConnectionPoolManager(LDAPConfig config) { var resolved = ResolveIdentifier(identifier); if (!_pools.TryGetValue(identifier, out var pool)) { - pool = new LdapConnectionPool(resolved, _ldapConfig); + pool = new LdapConnectionPool(resolved, _ldapConfig,scanner: _portScanner); _pools.TryAdd(identifier, pool); } @@ -35,7 +52,7 @@ public ConnectionPoolManager(LDAPConfig config) { var resolved = ResolveIdentifier(identifier); if (!_pools.TryGetValue(identifier, out var pool)) { - pool = new LdapConnectionPool(resolved, _ldapConfig); + pool = new LdapConnectionPool(resolved, _ldapConfig,scanner: _portScanner); _pools.TryAdd(identifier, pool); } @@ -43,7 +60,18 @@ public ConnectionPoolManager(LDAPConfig config) { } private string ResolveIdentifier(string identifier) { - return GetDomainSidFromDomainName(identifier, out var sid) ? sid : identifier; + if (_resolvedIdentifiers.TryGetValue(identifier, out var resolved)) { + return resolved; + } + + + if (GetDomainSidFromDomainName(identifier, out var sid)) { + _log.LogDebug("Resolved identifier {Identifier} to {Resolved}", identifier, sid); + _resolvedIdentifiers.TryAdd(identifier, sid); + return sid; + } + + return identifier; } private bool GetDomainSidFromDomainName(string domainName, out string domainSid) { @@ -64,7 +92,7 @@ private bool GetDomainSidFromDomainName(string domainName, out string domainSid) //we expect this to fail sometimes } - if (LDAPUtilsNew.GetDomain(domainName, _ldapConfig, out var domainObject)) + if (LdapUtilsNew.GetDomain(domainName, _ldapConfig, out var domainObject)) try { domainSid = domainObject.GetDirectoryEntry().GetSid(); if (domainSid != null) { diff --git a/src/CommonLib/Extensions.cs b/src/CommonLib/Extensions.cs index 49c59269..c7c2622f 100644 --- a/src/CommonLib/Extensions.cs +++ b/src/CommonLib/Extensions.cs @@ -67,6 +67,24 @@ public static string LdapValue(this Guid s) return output; } + public static string GetProperty(this DirectoryEntry entry, string propertyName) { + try { + if (!entry.Properties.Contains(propertyName)) { + return null; + } + } + catch { + return null; + } + + var s = entry.Properties[propertyName][0]; + return s switch + { + string st => st, + _ => null + }; + } + public static string GetSid(this DirectoryEntry result) { try diff --git a/src/CommonLib/Helpers.cs b/src/CommonLib/Helpers.cs index 7ac7cf7d..76f2b0ee 100644 --- a/src/CommonLib/Helpers.cs +++ b/src/CommonLib/Helpers.cs @@ -164,6 +164,15 @@ public static string DistinguishedNameToDomain(string distinguishedName) temp = DCReplaceRegex.Replace(temp, "").Replace(",", ".").ToUpper(); return temp; } + + /// + /// Converts a domain name to a distinguished name using simple string substitution + /// + /// + /// + public static string DomainNameToDistinguishedName(string domainName) { + return $"DC={domainName.Replace(".", ",DC=")}"; + } /// /// Strips a "serviceprincipalname" entry down to just its hostname diff --git a/src/CommonLib/LDAPUtilsNew.cs b/src/CommonLib/LDAPUtilsNew.cs deleted file mode 100644 index ca183358..00000000 --- a/src/CommonLib/LDAPUtilsNew.cs +++ /dev/null @@ -1,994 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.DirectoryServices; -using System.DirectoryServices.ActiveDirectory; -using System.DirectoryServices.Protocols; -using System.Linq; -using System.Net; -using System.Runtime.CompilerServices; -using System.Security.Principal; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using SharpHoundCommonLib.Enums; -using SharpHoundCommonLib.Exceptions; -using SharpHoundCommonLib.LDAPQueries; -using SharpHoundCommonLib.Processors; -using SharpHoundRPC.NetAPINative; -using SearchScope = System.DirectoryServices.Protocols.SearchScope; -using SecurityMasks = System.DirectoryServices.Protocols.SecurityMasks; - -namespace SharpHoundCommonLib; - -public class LDAPUtilsNew { - //This cache is indexed by domain sid - private readonly ConcurrentDictionary _dcInfoCache = new(); - private readonly DCConnectionCache _ldapConnectionCache = new(); - private static readonly ConcurrentDictionary _domainCache = new(); - private readonly ILogger _log; - private readonly NativeMethods _nativeMethods; - private readonly string _nullCacheKey = Guid.NewGuid().ToString(); - private readonly PortScanner _portScanner; - private readonly string[] _translateNames = { "Administrator", "admin" }; - private readonly LDAPConfig _ldapConfig = new(); - - private static readonly TimeSpan MinBackoffDelay = TimeSpan.FromSeconds(2); - private static readonly TimeSpan MaxBackoffDelay = TimeSpan.FromSeconds(20); - private const int BackoffDelayMultiplier = 2; - private const int MaxRetries = 3; - private readonly object _lockObj = new(); - private readonly ManualResetEvent _connectionResetEvent = new(false); - - public async IAsyncEnumerable> Query(LdapQueryParameters queryParameters, - [EnumeratorCancellation] CancellationToken cancellationToken = new()) { - var setupResult = await SetupLdapQuery(queryParameters, true); - - if (!setupResult.Success) { - _log.LogInformation("PagedQuery - Failure during query setup: {Reason}\n{Info}", setupResult.Message, - queryParameters.GetQueryInfo()); - yield break; - } - - var searchRequest = setupResult.SearchRequest; - var connectionWrapper = setupResult.ConnectionWrapper; - var connection = connectionWrapper.Connection; - var serverName = setupResult.Server; - - if (serverName == null) { - _log.LogWarning("PagedQuery - Failed to get a server name for connection, retry not possible"); - } - - if (cancellationToken.IsCancellationRequested) { - yield break; - } - - var queryRetryCount = 0; - var busyRetryCount = 0; - LdapResult tempResult = null; - while (queryRetryCount < MaxRetries) { - SearchResponse response = null; - try { - _log.LogTrace("Sending ldap request - {Info}", queryParameters.GetQueryInfo()); - response = (SearchResponse)connection.SendRequest(searchRequest); - } - catch (LdapException le) when (le.ErrorCode == (int)LdapErrorCodes.ServerDown) { - /* - * If we dont have a servername, we're not going to be able to re-establish a connection here. Page cookies are only valid for the server they were generated on. Bail out. - */ - if (serverName == null) { - _log.LogError( - "Query - Received server down exception without a known servername. Unable to generate new connection\n{Info}", - queryParameters.GetQueryInfo()); - yield break; - } - - /*A ServerDown exception indicates that our connection is no longer valid for one of many reasons. - However, this function is generally called by multiple threads, so we need to be careful in recreating - the connection. Using a semaphore, we can ensure that only one thread is actually recreating the connection - while the other threads that hit the ServerDown exception simply wait. The initial caller will hold the semaphore - and do a backoff delay before trying to make a new connection which will replace the existing connection in the - _ldapConnections cache. Other threads will retrieve the new connection from the cache instead of making a new one - This minimizes overhead of new connections while still fixing our core problem.*/ - - //Increment our query retry count - queryRetryCount++; - - //Attempt to acquire a lock - if (Monitor.TryEnter(_lockObj)) { - //Signal the reset event to ensure no everyone else waits - _connectionResetEvent.Reset(); - try { - //Try up to MaxRetries time to make a new connection, ensuring we're not using the cache - for (var retryCount = 0; retryCount < MaxRetries; retryCount++) { - var backoffDelay = GetNextBackoff(retryCount); - await Task.Delay(backoffDelay, cancellationToken); - var (success, newConnectionWrapper, _) = await GetLdapConnection(queryParameters, true); - if (success) { - newConnectionWrapper.CopyContexts(connectionWrapper); - connectionWrapper.Connection.Dispose(); - connectionWrapper = newConnectionWrapper; - break; - } - - if (retryCount == MaxRetries - 1) { - _log.LogError("Query - Failed to get a new connection after ServerDown.\n{Info}", - queryParameters.GetQueryInfo()); - yield break; - } - } - }finally{ - _connectionResetEvent.Set(); - Monitor.Exit(_lockObj); - } - } - else { - //If someone else is holding the reset event, we want to just wait and then pull the newly created connection out of the cache - //This event will be released after the first entrant thread is done making a new connection - //The thread.sleep is to prevent a potential, very unlikely race - Thread.Sleep(50); - _connectionResetEvent.WaitOne(); - - //At this point, our connection reset event should be tripped, and there should be a new connection on the cache - var (success, newConnectionWrapper, _) = await GetLdapConnection(queryParameters); - if (!success) { - _log.LogError("Query - Failed to recover from ServerDown error\n{Info}", queryParameters.GetQueryInfo()); - yield break; - } - - newConnectionWrapper.CopyContexts(connectionWrapper); - connectionWrapper = newConnectionWrapper; - connection = connectionWrapper.Connection; - } - } - catch (LdapException le) when (le.ErrorCode == (int)ResultCode.Busy && busyRetryCount < MaxRetries) { - /* - * If we get a busy error, we want to do an exponential backoff, but maintain the current connection - * The expectation is that given enough time, the server should stop being busy and service our query appropriately - */ - busyRetryCount++; - var backoffDelay = GetNextBackoff(busyRetryCount); - await Task.Delay(backoffDelay, cancellationToken); - } - catch (LdapException le) { - //No point in printing local exceptions because they're literally worthless - tempResult = new LdapResult() { - Error = - $"Query - Caught unrecoverable ldap exception: {le.Message} (ServerMessage: {le.ServerErrorMessage}) (ErrorCode: {le.ErrorCode})", - QueryInfo = queryParameters.GetQueryInfo() - }; - } - catch (Exception e) { - tempResult = new LdapResult { - Error = - $"PagedQuery - Caught unrecoverable exception: {e.Message}", - QueryInfo = queryParameters.GetQueryInfo() - }; - } - - if (tempResult != null) { - yield return tempResult; - yield break; - } - } - } - - private bool SendSearchRequestWithRetryHandling(LdapConnectionWrapperNew connectionWrapper, - SearchRequest searchRequest, out SearchResponse searchResponse) { - for (var retryCount = 0; retryCount < MaxRetries; retryCount++) { - try { - searchResponse = (SearchResponse)connectionWrapper.Connection.SendRequest(searchRequest); - return true; - } - catch (LdapException le) when (le.ErrorCode == (int)LdapErrorCodes.ServerDown) { - - } - } - } - - private bool SendPagedSearchRequestWithRetryHandling(LdapConnectionWrapperNew connectionWrapper, - SearchRequest searchRequest, out SearchResponse searchResponse) { - - - - - } - - public async IAsyncEnumerable> PagedQuery(LdapQueryParameters queryParameters, - [EnumeratorCancellation] CancellationToken cancellationToken = new()) { - var setupResult = await SetupLdapQuery(queryParameters, true); - - if (!setupResult.Success) { - _log.LogInformation("PagedQuery - Failure during query setup: {Reason}\n{Info}", setupResult.Message, - queryParameters.GetQueryInfo()); - yield break; - } - - var searchRequest = setupResult.SearchRequest; - var connectionWrapper = setupResult.ConnectionWrapper; - var connection = connectionWrapper.Connection; - var serverName = setupResult.Server; - - if (serverName == null) { - _log.LogWarning("PagedQuery - Failed to get a server name for connection, retry not possible"); - } - - var pageControl = new PageResultRequestControl(500); - searchRequest.Controls.Add(pageControl); - - PageResultResponseControl pageResponse = null; - var busyRetryCount = 0; - LdapResult tempResult = null; - - while (true) { - if (cancellationToken.IsCancellationRequested) { - yield break; - } - - if (tempResult != null) { - yield return tempResult; - yield break; - } - - SearchResponse response = null; - try { - _log.LogTrace("Sending paged ldap request - {Info}", queryParameters.GetQueryInfo()); - response = (SearchResponse)connection.SendRequest(searchRequest); - if (response != null) { - pageResponse = (PageResultResponseControl)response.Controls - .Where(x => x is PageResultResponseControl).DefaultIfEmpty(null).FirstOrDefault(); - } - } - catch (LdapException le) when (le.ErrorCode == (int)LdapErrorCodes.ServerDown) { - /* - * If we dont have a servername, we're not going to be able to re-establish a connection here. Page cookies are only valid for the server they were generated on. Bail out. - */ - if (serverName == null) { - _log.LogError( - "PagedQuery - Received server down exception without a known servername. Unable to generate new connection\n{Info}", - queryParameters.GetQueryInfo()); - yield break; - } - - /* - * Paged queries will not use the cached ldap connections, as the intention is to only have 1 or a couple of these queries running at once. - * The connection logic here is simplified accordingly - */ - for (var retryCount = 0; retryCount < MaxRetries; retryCount++) { - var backoffDelay = GetNextBackoff(retryCount); - await Task.Delay(backoffDelay, cancellationToken); - if (GetLdapConnectionForServer(serverName, out var newConnectionWrapper, - queryParameters.GlobalCatalog, - true)) { - newConnectionWrapper.CopyContexts(connectionWrapper); - connectionWrapper.Connection.Dispose(); - connectionWrapper = newConnectionWrapper; - _log.LogDebug( - "PagedQuery - Successfully created new ldap connection to {Server} after ServerDown", - serverName); - break; - } - - if (retryCount == MaxRetries - 1) { - _log.LogError("PagedQuery - Failed to get a new connection after ServerDown.\n{Info}", - queryParameters.GetQueryInfo()); - yield break; - } - } - } - catch (LdapException le) when (le.ErrorCode == (int)ResultCode.Busy && busyRetryCount < MaxRetries) { - /* - * If we get a busy error, we want to do an exponential backoff, but maintain the current connection - * The expectation is that given enough time, the server should stop being busy and service our query appropriately - */ - busyRetryCount++; - var backoffDelay = GetNextBackoff(busyRetryCount); - await Task.Delay(backoffDelay, cancellationToken); - } - catch (LdapException le) { - //No point in printing local exceptions because they're literally worthless - tempResult = new LdapResult() { - Error = - $"PagedQuery - Caught unrecoverable ldap exception: {le.Message} (ServerMessage: {le.ServerErrorMessage}) (ErrorCode: {le.ErrorCode})", - QueryInfo = queryParameters.GetQueryInfo() - }; - } - catch (Exception e) { - tempResult = new LdapResult { - Error = - $"PagedQuery - Caught unrecoverable exception: {e.Message}", - QueryInfo = queryParameters.GetQueryInfo() - }; - } - - if (cancellationToken.IsCancellationRequested) { - yield break; - } - - //I'm not sure why this happens sometimes, but if we try the request again, it works sometimes, other times we get an exception - if (response == null || pageResponse == null) { - continue; - } - - foreach (ISearchResultEntry entry in response.Entries) { - if (cancellationToken.IsCancellationRequested) { - yield break; - } - - yield return new LdapResult() { - Value = entry - }; - } - - if (pageResponse.Cookie.Length == 0 || response.Entries.Count == 0 || - cancellationToken.IsCancellationRequested) - yield break; - - pageControl.Cookie = pageResponse.Cookie; - } - } - - private static TimeSpan GetNextBackoff(int retryCount) { - return TimeSpan.FromSeconds(Math.Min( - MinBackoffDelay.TotalSeconds * Math.Pow(BackoffDelayMultiplier, retryCount), - MaxBackoffDelay.TotalSeconds)); - } - - private bool CreateSearchRequest(LdapQueryParameters queryParameters, - ref LdapConnectionWrapperNew connectionWrapper, out SearchRequest searchRequest) { - string basePath; - if (!string.IsNullOrWhiteSpace(queryParameters.SearchBase)) { - basePath = queryParameters.SearchBase; - } - else if (!connectionWrapper.GetSearchBase(queryParameters.NamingContext, out basePath)) { - string tempPath; - if (CallDsGetDcName(queryParameters.DomainName, out var info) && info != null) { - tempPath = DomainNameToDistinguishedName(info.Value.DomainName); - connectionWrapper.SaveContext(queryParameters.NamingContext, basePath); - } - else if (GetDomain(queryParameters.DomainName, out var domainObject)) { - tempPath = DomainNameToDistinguishedName(domainObject.Name); - } - else { - searchRequest = null; - return false; - } - - basePath = queryParameters.NamingContext switch { - NamingContext.Configuration => $"CN=Configuration,{tempPath}", - NamingContext.Schema => $"CN=Schema,CN=Configuration,{tempPath}", - NamingContext.Default => tempPath, - _ => throw new ArgumentOutOfRangeException() - }; - - connectionWrapper.SaveContext(queryParameters.NamingContext, basePath); - } - - searchRequest = new SearchRequest(basePath, queryParameters.LDAPFilter, queryParameters.SearchScope, - queryParameters.Attributes); - searchRequest.Controls.Add(new SearchOptionsControl(SearchOption.DomainScope)); - if (queryParameters.IncludeDeleted) { - searchRequest.Controls.Add(new ShowDeletedControl()); - } - - if (queryParameters.IncludeSecurityDescriptor) { - searchRequest.Controls.Add(new SecurityDescriptorFlagControl { - SecurityMasks = SecurityMasks.Dacl | SecurityMasks.Owner - }); - } - - return true; - } - - private static string DomainNameToDistinguishedName(string domainName) { - return $"DC={domainName.Replace(".", ",DC=")}"; - } - - private bool CallDsGetDcName(string domainName, out NetAPIStructs.DomainControllerInfo? info) { - if (_dcInfoCache.TryGetValue(domainName.ToUpper().Trim(), out info)) return info != null; - - var apiResult = _nativeMethods.CallDsGetDcName(null, domainName, - (uint)(NetAPIEnums.DSGETDCNAME_FLAGS.DS_FORCE_REDISCOVERY | - NetAPIEnums.DSGETDCNAME_FLAGS.DS_RETURN_DNS_NAME | - NetAPIEnums.DSGETDCNAME_FLAGS.DS_DIRECTORY_SERVICE_REQUIRED)); - - if (apiResult.IsFailed) { - _dcInfoCache.TryAdd(domainName.ToUpper().Trim(), null); - return false; - } - - info = apiResult.Value; - return true; - } - - private bool - GetLdapConnectionForServer(string serverName, out LdapConnectionWrapperNew connectionWrapper, - bool globalCatalog = false, bool forceCreateNewConnection = false) { - if (string.IsNullOrWhiteSpace(serverName)) { - throw new ArgumentNullException(nameof(serverName)); - } - - try { - if (!forceCreateNewConnection && - GetCachedConnection(serverName, globalCatalog, out connectionWrapper)) - return true; - - if (CreateLdapConnection(serverName, globalCatalog, out connectionWrapper)) { - return true; - } - - connectionWrapper = null; - return false; - } - catch (LdapAuthenticationException e) { - _log.LogError("Error connecting to {Domain}: credentials are invalid (error code {ErrorCode})", serverName, - e.LdapException.ErrorCode); - connectionWrapper = null; - return false; - } - catch (NoLdapDataException) { - _log.LogError("No data returned for domain {Domain} during initial LDAP test.", serverName); - connectionWrapper = null; - return false; - } - } - - private async Task SetupLdapQuery(LdapQueryParameters queryParameters, - bool forceNewConnection = false) { - var result = new LdapQuerySetupResult(); - var (success, connectionWrapper, message) = await GetLdapConnection(queryParameters, forceNewConnection); - if (!success) { - result.Success = false; - result.Message = $"Unable to create a connection: {message}"; - return result; - } - - //This should never happen as far as I know, so just checking for safety - if (connectionWrapper.Connection == null) { - result.Success = false; - result.Message = $"Connection object is null"; - return result; - } - - if (!CreateSearchRequest(queryParameters, ref connectionWrapper, out var searchRequest)) { - result.Success = false; - result.Message = "Failed to create search request"; - return result; - } - - if (GetServerFromConnection(connectionWrapper.Connection, out var server)) { - result.Server = server; - } - - result.Success = true; - result.SearchRequest = searchRequest; - result.ConnectionWrapper = connectionWrapper; - return result; - } - - private Task<(bool Success, LdapConnectionWrapperNew Connection, string Message )> GetLdapConnection( - LdapQueryParameters queryParameters, bool forceCreateNewConnection = false) { - return GetLdapConnection(queryParameters.DomainName, queryParameters.GlobalCatalog, forceCreateNewConnection); - } - - private async Task<(bool Success, LdapConnectionWrapperNew Connection, string Message )> GetLdapConnection( - string domainName, bool globalCatalog = false, - bool forceCreateNewConnection = false) { - //TODO: Pull out individual strategies into single functions for readability and better logging - if (string.IsNullOrWhiteSpace(domainName)) throw new ArgumentNullException(nameof(domainName)); - - try { - /* - * If a server is explicitly set on the config, we should only test this config - */ - LdapConnectionWrapperNew connectionWrapper; - if (_ldapConfig.Server != null) { - _log.LogWarning("Server is overridden via config, creating connection to {Server}", _ldapConfig.Server); - if (!forceCreateNewConnection && - GetCachedConnection(domainName, globalCatalog, out connectionWrapper)) - return (true, connectionWrapper, ""); - - if (CreateLdapConnection(_ldapConfig.Server, globalCatalog, out var serverConnection)) { - connectionWrapper = CheckCacheConnection(serverConnection, domainName, globalCatalog, - forceCreateNewConnection); - return (true, connectionWrapper, ""); - } - - return (false, null, "Failed to connect to specified server"); - } - - if (!forceCreateNewConnection && GetCachedConnection(domainName, globalCatalog, out connectionWrapper)) - return (true, connectionWrapper, ""); - - _log.LogInformation("No cached connection found for domain {Domain}, attempting a new connection", - domainName); - - if (CreateLdapConnection(domainName.ToUpper().Trim(), globalCatalog, out connectionWrapper)) { - _log.LogDebug("Successfully created ldap connection for domain: {Domain} using strategy 1", domainName); - connectionWrapper = - CheckCacheConnection(connectionWrapper, domainName, globalCatalog, forceCreateNewConnection); - return (true, connectionWrapper, ""); - } - - string tempDomainName; - - var dsGetDcNameResult = _nativeMethods.CallDsGetDcName(null, domainName, - (uint)(NetAPIEnums.DSGETDCNAME_FLAGS.DS_FORCE_REDISCOVERY | - NetAPIEnums.DSGETDCNAME_FLAGS.DS_RETURN_DNS_NAME | - NetAPIEnums.DSGETDCNAME_FLAGS.DS_DIRECTORY_SERVICE_REQUIRED)); - if (dsGetDcNameResult.IsSuccess) { - tempDomainName = dsGetDcNameResult.Value.DomainName; - if (!forceCreateNewConnection && - GetCachedConnection(tempDomainName, globalCatalog, out connectionWrapper)) - return (true, connectionWrapper, ""); - - if (!tempDomainName.Equals(domainName, StringComparison.OrdinalIgnoreCase) && - CreateLdapConnection(tempDomainName, globalCatalog, out connectionWrapper)) { - _log.LogDebug( - "Successfully created ldap connection for domain: {Domain} using strategy 2 with name {NewName}", - domainName, tempDomainName); - connectionWrapper = CheckCacheConnection(connectionWrapper, tempDomainName, globalCatalog, - forceCreateNewConnection); - return (true, connectionWrapper, ""); - } - - var server = dsGetDcNameResult.Value.DomainControllerName.TrimStart('\\'); - - var result = - await CreateLDAPConnectionWithPortCheck(server, globalCatalog); - if (result.success) { - _log.LogDebug( - "Successfully created ldap connection for domain: {Domain} using strategy 3 to server {Server}", - domainName, server); - connectionWrapper = CheckCacheConnection(result.connection, tempDomainName, globalCatalog, - forceCreateNewConnection); - return (true, connectionWrapper, ""); - } - } - - if (!GetDomain(domainName, out var domainObject) || domainObject.Name == null) { - //If we don't get a result here, we effectively have no other ways to resolve this domain, so we'll just have to exit out - _log.LogDebug( - "Could not get domain object from GetDomain, unable to create ldap connection for domain {Domain}", - domainName); - return (false, null, "Unable to get domain object for further methods"); - } - - tempDomainName = domainObject.Name.ToUpper().Trim(); - if (!forceCreateNewConnection && - GetCachedConnection(tempDomainName, globalCatalog, out connectionWrapper)) - return (true, connectionWrapper, ""); - - if (!tempDomainName.Equals(domainName, StringComparison.OrdinalIgnoreCase) && - CreateLdapConnection(tempDomainName, globalCatalog, out connectionWrapper)) { - _log.LogDebug( - "Successfully created ldap connection for domain: {Domain} using strategy 4 with name {NewName}", - domainName, tempDomainName); - connectionWrapper = - CheckCacheConnection(connectionWrapper, tempDomainName, globalCatalog, forceCreateNewConnection); - return (true, connectionWrapper, ""); - } - - var primaryDomainController = domainObject.PdcRoleOwner.Name; - var portConnectionResult = - await CreateLDAPConnectionWithPortCheck(primaryDomainController, globalCatalog); - if (portConnectionResult.success) { - _log.LogDebug( - "Successfully created ldap connection for domain: {Domain} using strategy 5 with to pdc {Server}", - domainName, primaryDomainController); - connectionWrapper = CheckCacheConnection(portConnectionResult.connection, tempDomainName, globalCatalog, - forceCreateNewConnection); - return (true, connectionWrapper, ""); - } - - //Loop over all other domain controllers and see if we can make a good connection to any - foreach (DomainController dc in domainObject.DomainControllers) { - portConnectionResult = - await CreateLDAPConnectionWithPortCheck(primaryDomainController, globalCatalog); - if (portConnectionResult.success) { - _log.LogDebug( - "Successfully created ldap connection for domain: {Domain} using strategy 6 with to pdc {Server}", - domainName, primaryDomainController); - connectionWrapper = CheckCacheConnection(portConnectionResult.connection, tempDomainName, - globalCatalog, - forceCreateNewConnection); - return (true, connectionWrapper, ""); - } - } - - _log.LogWarning("Exhausted all potential methods of creating ldap connection to {DomainName}", domainName); - return (false, null, "All attempted connections failed"); - } - catch (LdapAuthenticationException e) { - _log.LogError("Error connecting to {Domain}: credentials are invalid (error code {ErrorCode})", domainName, - e.LdapException.ErrorCode); - return (false, null, "Invalid credentials for connection"); - } - catch (NoLdapDataException) { - _log.LogError("No data returned for domain {Domain} during initial LDAP test.", domainName); - return (false, null, "No data returned from ldap connection"); - } - } - - private async Task<(bool success, LdapConnectionWrapperNew connection)> CreateLDAPConnectionWithPortCheck( - string target, bool globalCatalog) { - if (globalCatalog) { - if (await _portScanner.CheckPort(target, _ldapConfig.GetGCPort(true)) || (!_ldapConfig.ForceSSL && - await _portScanner.CheckPort(target, _ldapConfig.GetGCPort(false)))) - return (CreateLdapConnection(target, true, out var connection), connection); - } - else { - if (await _portScanner.CheckPort(target, _ldapConfig.GetPort(true)) || (!_ldapConfig.ForceSSL && - await _portScanner.CheckPort(target, _ldapConfig.GetPort(false)))) - return (CreateLdapConnection(target, true, out var connection), connection); - } - - return (false, null); - } - - private LdapConnectionWrapperNew CheckCacheConnection(LdapConnectionWrapperNew connectionWrapper, string domainName, - bool globalCatalog, bool forceCreateNewConnection) { - string cacheIdentifier; - if (_ldapConfig.Server != null) { - cacheIdentifier = _ldapConfig.Server; - } - else { - if (!GetDomainSidFromDomainName(domainName, out cacheIdentifier)) { - //This is kinda gross, but its another way to get the correct domain sid - if (!connectionWrapper.Connection.GetNamingContextSearchBase(NamingContext.Default, - out var searchBase) || !GetDomainSidFromConnection(connectionWrapper.Connection, searchBase, - out cacheIdentifier)) { - /* - * If we get here, we couldn't resolve a domain sid, which is hella bad, but we also want to keep from creating a shitton of new connections - * Cache using the domainname and pray it all works out - */ - cacheIdentifier = domainName; - } - } - } - - if (forceCreateNewConnection) { - return _ldapConnectionCache.AddOrUpdate(cacheIdentifier, globalCatalog, connectionWrapper); - } - - return _ldapConnectionCache.TryAdd(cacheIdentifier, globalCatalog, connectionWrapper); - } - - private bool GetCachedConnection(string domain, bool globalCatalog, out LdapConnectionWrapperNew connection) { - //If server is set via our config, we'll always just use this as the cache key - if (_ldapConfig.Server != null) { - return _ldapConnectionCache.TryGet(_ldapConfig.Server, globalCatalog, out connection); - } - - if (GetDomainSidFromDomainName(domain, out var domainSid)) { - if (_ldapConnectionCache.TryGet(domainSid, globalCatalog, out connection)) { - return true; - } - } - - return _ldapConnectionCache.TryGet(domain, globalCatalog, out connection); - } - - private bool GetDomainSidFromConnection(LdapConnection connection, string searchBase, out string domainSid) { - try { - //This ldap filter searches for domain controllers - //Searches for any accounts with a UAC value inclusive of 8192 bitwise - //8192 is the flag for SERVER_TRUST_ACCOUNT, which is set only on Domain Controllers - var searchRequest = new SearchRequest(searchBase, - "(&(objectCategory=computer)(userAccountControl:1.2.840.113556.1.4.803:=8192))", - SearchScope.Subtree, "objectsid"); - - var response = (SearchResponse)connection.SendRequest(searchRequest); - if (response == null || response.Entries.Count == 0) { - domainSid = ""; - return false; - } - - var entry = response.Entries[0]; - var sid = entry.GetSid(); - domainSid = sid.Substring(0, sid.LastIndexOf('-')).ToUpper(); - return true; - } - catch (LdapException) { - _log.LogWarning("Failed grabbing domainsid from ldap for {domain}", searchBase); - domainSid = ""; - return false; - } - } - - private bool GetServerFromConnection(LdapConnection connection, out string server) { - var searchRequest = new SearchRequest("", new LDAPFilter().AddAllObjects().GetFilter(), - SearchScope.Base, null); - searchRequest.Controls.Add(new SearchOptionsControl(SearchOption.DomainScope)); - - var response = (SearchResponse)connection.SendRequest(searchRequest); - if (response?.Entries == null || response.Entries.Count == 0) { - server = ""; - return false; - } - - var entry = response.Entries[0]; - server = entry.GetProperty(LDAPProperties.DNSHostName); - return server != null; - } - - private bool CreateLdapConnection(string target, bool globalCatalog, - out LdapConnectionWrapperNew connection) { - var baseConnection = CreateBaseConnection(target, true, globalCatalog); - if (TestLdapConnection(baseConnection, target, out var entry)) { - connection = new LdapConnectionWrapperNew(baseConnection, entry); - return true; - } - - try { - baseConnection.Dispose(); - } - catch { - //this is just in case - } - - if (_ldapConfig.ForceSSL) { - connection = null; - return false; - } - - baseConnection = CreateBaseConnection(target, false, globalCatalog); - if (TestLdapConnection(baseConnection, target, out entry)) { - connection = new LdapConnectionWrapperNew(baseConnection, entry); - return true; - } - - try { - baseConnection.Dispose(); - } - catch { - //this is just in case - } - - connection = null; - return false; - } - - private LdapConnection CreateBaseConnection(string directoryIdentifier, bool ssl, - bool globalCatalog) { - var port = globalCatalog ? _ldapConfig.GetGCPort(ssl) : _ldapConfig.GetPort(ssl); - var identifier = new LdapDirectoryIdentifier(directoryIdentifier, port, false, false); - var connection = new LdapConnection(identifier) { Timeout = new TimeSpan(0, 0, 5, 0) }; - - //These options are important! - connection.SessionOptions.ProtocolVersion = 3; - //Referral chasing does not work with paged searches - connection.SessionOptions.ReferralChasing = ReferralChasingOptions.None; - if (ssl) connection.SessionOptions.SecureSocketLayer = true; - - connection.SessionOptions.Sealing = !_ldapConfig.DisableSigning; - connection.SessionOptions.Signing = !_ldapConfig.DisableSigning; - - if (_ldapConfig.DisableCertVerification) - connection.SessionOptions.VerifyServerCertificate = (_, _) => true; - - if (_ldapConfig.Username != null) { - var cred = new NetworkCredential(_ldapConfig.Username, _ldapConfig.Password); - connection.Credential = cred; - } - - connection.AuthType = _ldapConfig.AuthType; - - return connection; - } - - /// - /// Tests whether an LDAP connection is working - /// - /// - /// - /// The rootdse object for this connection if successful - /// - /// Something is wrong with the supplied credentials - /// - /// A connection "succeeded" but no data was returned. This can be related to - /// kerberos auth across trusts or just simply lack of permissions - /// - private bool TestLdapConnection(LdapConnection connection, string identifier, out ISearchResultEntry entry) { - try { - //Attempt an initial bind. If this fails, likely auth is invalid, or its not a valid target - connection.Bind(); - } - catch (LdapException e) { - //TODO: Maybe look at this and find a better way? - if (e.ErrorCode is (int)LdapErrorCodes.InvalidCredentials or (int)ResultCode.InappropriateAuthentication) { - connection.Dispose(); - throw new LdapAuthenticationException(e); - } - - entry = null; - return false; - } - catch (Exception e) { - entry = null; - return false; - } - - SearchResponse response; - try { - //Do an initial search request to get the rootDSE - //This ldap filter is equivalent to (objectclass=*) - var searchRequest = CreateSearchRequest("", new LDAPFilter().AddAllObjects().GetFilter(), - SearchScope.Base, null); - - response = (SearchResponse)connection.SendRequest(searchRequest); - } - catch (LdapException e) { - /* - * If we can't send the initial search request, its unlikely any other search requests will work so we will immediately return false - */ - _log.LogDebug(e, "TestLdapConnection failed during search request against target {Target}", identifier); - entry = null; - return false; - } - - if (response?.Entries == null || response.Entries.Count == 0) { - /* - * This can happen for one of two reasons, either we dont have permission to query AD or we're authenticating - * across external trusts with kerberos authentication without Forest Search Order properly configured. - * Either way, this connection isn't useful for us because we're not going to get data, so return false - */ - - _log.LogDebug("TestLdapConnection failed to return results against target {Target}", identifier); - connection.Dispose(); - throw new NoLdapDataException(); - } - - entry = new SearchResultEntryWrapper(response.Entries[0]); - return true; - } - - public static SearchRequest CreateSearchRequest(string distinguishedName, string ldapFilter, SearchScope searchScope, - string[] attributes) { - var searchRequest = new SearchRequest(distinguishedName, ldapFilter, - searchScope, attributes); - searchRequest.Controls.Add(new SearchOptionsControl(SearchOption.DomainScope)); - return searchRequest; - } - - public bool GetDomainSidFromDomainName(string domainName, out string domainSid) { - if (Cache.GetDomainSidMapping(domainName, out domainSid)) return true; - - try { - var entry = new DirectoryEntry($"LDAP://{domainName}"); - //Force load objectsid into the object cache - entry.RefreshCache(new[] { "objectSid" }); - var sid = entry.GetSid(); - if (sid != null) { - Cache.AddDomainSidMapping(domainName, sid); - domainSid = sid; - return true; - } - } - catch { - //we expect this to fail sometimes - } - - if (GetDomain(domainName, out var domainObject)) - try { - domainSid = domainObject.GetDirectoryEntry().GetSid(); - if (domainSid != null) { - Cache.AddDomainSidMapping(domainName, domainSid); - return true; - } - } - catch { - //we expect this to fail sometimes (not sure why, but better safe than sorry) - } - - foreach (var name in _translateNames) - try { - var account = new NTAccount(domainName, name); - var sid = (SecurityIdentifier)account.Translate(typeof(SecurityIdentifier)); - domainSid = sid.AccountDomainSid.ToString(); - Cache.AddDomainSidMapping(domainName, domainSid); - return true; - } - catch { - //We expect this to fail if the username doesn't exist in the domain - } - - return false; - } - - private string ResolveDomainCrossRef(string domainName) { - } - - /// - /// Attempts to get the Domain object representing the target domain. If null is specified for the domain name, gets - /// the user's current domain - /// - /// - /// - /// - public bool GetDomain(string domainName, out Domain domain) { - var cacheKey = domainName ?? _nullCacheKey; - if (_domainCache.TryGetValue(cacheKey, out domain)) return true; - - try { - DirectoryContext context; - if (_ldapConfig.Username != null) - context = domainName != null - ? new DirectoryContext(DirectoryContextType.Domain, domainName, _ldapConfig.Username, - _ldapConfig.Password) - : new DirectoryContext(DirectoryContextType.Domain, _ldapConfig.Username, - _ldapConfig.Password); - else - context = domainName != null - ? new DirectoryContext(DirectoryContextType.Domain, domainName) - : new DirectoryContext(DirectoryContextType.Domain); - - domain = Domain.GetDomain(context); - if (domain == null) return false; - _domainCache.TryAdd(cacheKey, domain); - return true; - } - catch (Exception e) { - _log.LogDebug(e, "GetDomain call failed for domain name {Name}", domainName); - return false; - } - } - - public static bool GetDomain(string domainName, LDAPConfig ldapConfig, out Domain domain) { - if (_domainCache.TryGetValue(domainName, out domain)) return true; - - try { - DirectoryContext context; - if (ldapConfig.Username != null) - context = domainName != null - ? new DirectoryContext(DirectoryContextType.Domain, domainName, ldapConfig.Username, - ldapConfig.Password) - : new DirectoryContext(DirectoryContextType.Domain, ldapConfig.Username, - ldapConfig.Password); - else - context = domainName != null - ? new DirectoryContext(DirectoryContextType.Domain, domainName) - : new DirectoryContext(DirectoryContextType.Domain); - - domain = Domain.GetDomain(context); - if (domain == null) return false; - _domainCache.TryAdd(domainName, domain); - return true; - } - catch (Exception e) { - return false; - } - } - - /// - /// Attempts to get the Domain object representing the target domain. If null is specified for the domain name, gets - /// the user's current domain - /// - /// - /// - /// - public bool GetDomain(out Domain domain) { - var cacheKey = _nullCacheKey; - if (_domainCache.TryGetValue(cacheKey, out domain)) return true; - - try { - var context = _ldapConfig.Username != null - ? new DirectoryContext(DirectoryContextType.Domain, _ldapConfig.Username, - _ldapConfig.Password) - : new DirectoryContext(DirectoryContextType.Domain); - - domain = Domain.GetDomain(context); - _domainCache.TryAdd(cacheKey, domain); - return true; - } - catch (Exception e) { - _log.LogDebug(e, "GetDomain call failed for blank domain"); - return false; - } - } - - private struct LdapFailure { - public LdapFailureReason FailureReason { get; set; } - public string Message { get; set; } - } -} \ No newline at end of file diff --git a/src/CommonLib/LdapConnectionPool.cs b/src/CommonLib/LdapConnectionPool.cs index 6cbc1796..23f38580 100644 --- a/src/CommonLib/LdapConnectionPool.cs +++ b/src/CommonLib/LdapConnectionPool.cs @@ -41,9 +41,11 @@ public LdapConnectionPool(string identifier, LDAPConfig config, int maxConnectio if (!_connections.TryTake(out var connectionWrapper)) { var (success, connection, message) = await CreateNewConnection(); if (!success) { + //If we didn't get a connection, immediately release the semaphore so we don't have hanging ones + _semaphore.Release(); return (false, null, message); } - + connectionWrapper = connection; } @@ -54,7 +56,13 @@ public LdapConnectionPool(string identifier, LDAPConfig config, int maxConnectio GetConnectionForSpecificServerAsync(string server, bool globalCatalog) { await _semaphore.WaitAsync(); - return CreateNewConnectionForServer(server, globalCatalog); + var result= CreateNewConnectionForServer(server, globalCatalog); + if (!result.Success) { + //If we didn't get a connection, immediately release the semaphore so we don't have hanging ones + _semaphore.Release(); + } + + return result; } public async Task<(bool Success, LdapConnectionWrapperNew connectionWrapper, string Message)> GetGlobalCatalogConnectionAsync() { @@ -62,6 +70,8 @@ public LdapConnectionPool(string identifier, LDAPConfig config, int maxConnectio if (!_globalCatalogConnection.TryTake(out var connectionWrapper)) { var (success, connection, message) = await CreateNewConnection(true); if (!success) { + //If we didn't get a connection, immediately release the semaphore so we don't have hanging ones + _semaphore.Release(); return (false, null, message); } @@ -71,8 +81,8 @@ public LdapConnectionPool(string identifier, LDAPConfig config, int maxConnectio return (true, connectionWrapper, null); } - public void ReleaseConnection(LdapConnectionWrapperNew connectionWrapper, bool returnToPool = true) { - if (returnToPool) { + public void ReleaseConnection(LdapConnectionWrapperNew connectionWrapper, bool connectionFaulted = false) { + if (!connectionFaulted) { if (connectionWrapper.GlobalCatalog) { _globalCatalogConnection.Add(connectionWrapper); } @@ -132,7 +142,7 @@ public void Dispose() { } } - if (!LDAPUtilsNew.GetDomain(_identifier, _ldapConfig, out var domainObject) || domainObject.Name == null) { + if (!LdapUtilsNew.GetDomain(_identifier, _ldapConfig, out var domainObject) || domainObject.Name == null) { //If we don't get a result here, we effectively have no other ways to resolve this domain, so we'll just have to exit out _log.LogDebug( "Could not get domain object from GetDomain, unable to create ldap connection for domain {Domain}", @@ -161,7 +171,7 @@ public void Dispose() { foreach (DomainController dc in domainObject.DomainControllers) { portConnectionResult = - await CreateLDAPConnectionWithPortCheck(primaryDomainController, globalCatalog); + await CreateLDAPConnectionWithPortCheck(dc.Name, globalCatalog); if (portConnectionResult.success) { _log.LogDebug( "Successfully created ldap connection for domain: {Domain} using strategy 6 with to pdc {Server}", @@ -283,7 +293,7 @@ private bool TestLdapConnection(LdapConnection connection, out LdapConnectionTes try { //Do an initial search request to get the rootDSE //This ldap filter is equivalent to (objectclass=*) - var searchRequest = LDAPUtilsNew.CreateSearchRequest("", new LDAPFilter().AddAllObjects().GetFilter(), + var searchRequest = LdapUtilsNew.CreateSearchRequest("", new LDAPFilter().AddAllObjects().GetFilter(), SearchScope.Base, null); response = (SearchResponse)connection.SendRequest(searchRequest); diff --git a/src/CommonLib/LdapConnectionWrapperNew.cs b/src/CommonLib/LdapConnectionWrapperNew.cs index 1c3392da..bd0e0fc1 100644 --- a/src/CommonLib/LdapConnectionWrapperNew.cs +++ b/src/CommonLib/LdapConnectionWrapperNew.cs @@ -12,8 +12,7 @@ public class LdapConnectionWrapperNew private string _configurationSearchBase; private string _schemaSearchBase; private string _server; - public string Guid { get; set; } - private const string Unknown = "UNKNOWN"; + private string Guid { get; set; } public bool GlobalCatalog; public string PoolIdentifier; @@ -33,15 +32,13 @@ public void CopyContexts(LdapConnectionWrapperNew other) { _server = other._server; } - public bool GetServer(out string server) { + public string GetServer() { if (_server != null) { - server = _server; - return true; + return _server; } _server = _searchResultEntry.GetProperty(LDAPProperties.DNSHostName); - server = _server; - return server != null; + return _server; } public bool GetSearchBase(NamingContext context, out string searchBase) diff --git a/src/CommonLib/LdapResult.cs b/src/CommonLib/LdapResult.cs index ffcedd03..68ca06c0 100644 --- a/src/CommonLib/LdapResult.cs +++ b/src/CommonLib/LdapResult.cs @@ -2,10 +2,19 @@ namespace SharpHoundCommonLib; -public class LdapResult +public class LdapResult : Result { - public T Value { get; set; } - public string Error { get; set; } - public bool IsSuccess => Error == null; public string QueryInfo { get; set; } + + protected LdapResult(T value, bool success, string error, string queryInfo) : base(value, success, error) { + QueryInfo = queryInfo; + } + + public static LdapResult Ok(T value) { + return new LdapResult(value, true, string.Empty, null); + } + + public static LdapResult Fail(string message, LdapQueryParameters queryInfo) { + return new LdapResult(default, false, message, queryInfo.GetQueryInfo()); + } } \ No newline at end of file diff --git a/src/CommonLib/LdapUtilsNew.cs b/src/CommonLib/LdapUtilsNew.cs new file mode 100644 index 00000000..467ec910 --- /dev/null +++ b/src/CommonLib/LdapUtilsNew.cs @@ -0,0 +1,756 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.DirectoryServices; +using System.DirectoryServices.ActiveDirectory; +using System.DirectoryServices.Protocols; +using System.Linq; +using System.Net; +using System.Runtime.CompilerServices; +using System.Security.Principal; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using SharpHoundCommonLib.Enums; +using SharpHoundCommonLib.Exceptions; +using SharpHoundCommonLib.LDAPQueries; +using SharpHoundCommonLib.OutputTypes; +using SharpHoundCommonLib.Processors; +using SharpHoundRPC.NetAPINative; +using Domain = System.DirectoryServices.ActiveDirectory.Domain; +using SearchScope = System.DirectoryServices.Protocols.SearchScope; +using SecurityMasks = System.DirectoryServices.Protocols.SecurityMasks; + +namespace SharpHoundCommonLib; + +public class LdapUtilsNew { + //This cache is indexed by domain sid + private readonly ConcurrentDictionary _dcInfoCache = new(); + private static readonly ConcurrentDictionary DomainCache = new(); + private static readonly ConcurrentDictionary DomainToForestCache = new(StringComparer.OrdinalIgnoreCase); + private static readonly ConcurrentDictionary + SeenWellKnownPrincipals = new(); + private readonly ILogger _log; + private readonly PortScanner _portScanner; + private readonly NativeMethods _nativeMethods; + private readonly string _nullCacheKey = Guid.NewGuid().ToString(); + private readonly Regex SidRegex = new Regex(@"^(S-\d+-\d+-\d+-\d+-\d+-\d+)-\d+$"); + + private readonly string[] _translateNames = { "Administrator", "admin" }; + private LDAPConfig _ldapConfig = new(); + + private ConnectionPoolManager _connectionPool; + + private static readonly TimeSpan MinBackoffDelay = TimeSpan.FromSeconds(2); + private static readonly TimeSpan MaxBackoffDelay = TimeSpan.FromSeconds(20); + private const int BackoffDelayMultiplier = 2; + private const int MaxRetries = 3; + + private class ResolvedWellKnownPrincipal + { + public string DomainName { get; set; } + public string WkpId { get; set; } + } + + public LdapUtilsNew() { + _nativeMethods = new NativeMethods(); + _portScanner = new PortScanner(); + _log = Logging.LogProvider.CreateLogger("LDAPUtils"); + _connectionPool = new ConnectionPoolManager(_ldapConfig); + } + + public LdapUtilsNew(NativeMethods nativeMethods = null, PortScanner scanner = null, ILogger log = null) { + _nativeMethods = nativeMethods ?? new NativeMethods(); + _portScanner = scanner ?? new PortScanner(); + _log = log ?? Logging.LogProvider.CreateLogger("LDAPUtils"); + _connectionPool = new ConnectionPoolManager(_ldapConfig, scanner:_portScanner); + } + + public void SetLDAPConfig(LDAPConfig config) { + _ldapConfig = config; + _connectionPool.Dispose(); + _connectionPool = new ConnectionPoolManager(_ldapConfig, scanner: _portScanner); + } + + public async IAsyncEnumerable> Query(LdapQueryParameters queryParameters, + [EnumeratorCancellation] CancellationToken cancellationToken = new()) { + var setupResult = await SetupLdapQuery(queryParameters); + + if (!setupResult.Success) { + _log.LogInformation("Query - Failure during query setup: {Reason}\n{Info}", setupResult.Message, + queryParameters.GetQueryInfo()); + yield break; + } + + var searchRequest = setupResult.SearchRequest; + var connectionWrapper = setupResult.ConnectionWrapper; + + if (cancellationToken.IsCancellationRequested) { + yield break; + } + + var queryRetryCount = 0; + var busyRetryCount = 0; + LdapResult tempResult = null; + var querySuccess = false; + SearchResponse response = null; + while (true) { + if (cancellationToken.IsCancellationRequested) { + yield break; + } + + try { + _log.LogTrace("Sending ldap request - {Info}", queryParameters.GetQueryInfo()); + response = (SearchResponse)connectionWrapper.Connection.SendRequest(searchRequest); + + if (response != null) { + querySuccess = true; + } + else if (queryRetryCount == MaxRetries) { + tempResult = LdapResult.Fail($"Failed to get a response after {MaxRetries} attempts", queryParameters); + }else { + queryRetryCount++; + continue; + } + } + catch (LdapException le) when (le.ErrorCode == (int)LdapErrorCodes.ServerDown && queryRetryCount < MaxRetries) { + /* + * A ServerDown exception indicates that our connection is no longer valid for one of many reasons. + * We'll want to release our connection back to the pool, but dispose it. We need a new connection, + * and because this is not a paged query, we can get this connection from anywhere. + */ + + //Increment our query retry count + queryRetryCount++; + _connectionPool.ReleaseConnection(connectionWrapper, true); + + for (var retryCount = 0; retryCount < MaxRetries; retryCount++) { + var backoffDelay = GetNextBackoff(retryCount); + await Task.Delay(backoffDelay, cancellationToken); + var (success, newConnectionWrapper, message) = await _connectionPool.GetLdapConnection(queryParameters.DomainName, queryParameters.GlobalCatalog); + if (success) { + _log.LogDebug("Query - Recovered from ServerDown successfully, connection made to {NewServer}", newConnectionWrapper.GetServer()); + connectionWrapper = newConnectionWrapper; + break; + } + + //If we hit our max retries for making a new connection, set tempResult so we can yield it after this logic + if (retryCount == MaxRetries - 1) { + _log.LogError("Query - Failed to get a new connection after ServerDown.\n{Info}", + queryParameters.GetQueryInfo()); + tempResult = + LdapResult.Fail( + "Query - Failed to get a new connection after ServerDown.", queryParameters); + } + } + } + catch (LdapException le) when (le.ErrorCode == (int)ResultCode.Busy && busyRetryCount < MaxRetries) { + /* + * If we get a busy error, we want to do an exponential backoff, but maintain the current connection + * The expectation is that given enough time, the server should stop being busy and service our query appropriately + */ + busyRetryCount++; + var backoffDelay = GetNextBackoff(busyRetryCount); + await Task.Delay(backoffDelay, cancellationToken); + } + catch (LdapException le) { + tempResult = LdapResult.Fail($"Query - Caught unrecoverable ldap exception: {le.Message} (ServerMessage: {le.ServerErrorMessage}) (ErrorCode: {le.ErrorCode})", queryParameters); + } + catch (Exception e) { + tempResult = + LdapResult.Fail($"PagedQuery - Caught unrecoverable exception: {e.Message}", + queryParameters); + } + + //If we have a tempResult set it means we hit an error we couldn't recover from, so yield that result and then break out of the function + if (tempResult != null) { + yield return tempResult; + yield break; + } + + //If we've successfully made our query, break out of the while loop + if (querySuccess) { + break; + } + } + + //TODO: Fix this with a new wrapper object + foreach (ISearchResultEntry entry in response.Entries) { + yield return LdapResult.Ok(entry); + } + } + + public async IAsyncEnumerable> PagedQuery(LdapQueryParameters queryParameters, + [EnumeratorCancellation] CancellationToken cancellationToken = new()) { + var setupResult = await SetupLdapQuery(queryParameters); + + if (!setupResult.Success) { + _log.LogInformation("PagedQuery - Failure during query setup: {Reason}\n{Info}", setupResult.Message, + queryParameters.GetQueryInfo()); + yield break; + } + + var searchRequest = setupResult.SearchRequest; + var connectionWrapper = setupResult.ConnectionWrapper; + var serverName = setupResult.Server; + + if (serverName == null) { + _log.LogWarning("PagedQuery - Failed to get a server name for connection, retry not possible"); + } + + var pageControl = new PageResultRequestControl(500); + searchRequest.Controls.Add(pageControl); + + PageResultResponseControl pageResponse = null; + var busyRetryCount = 0; + var queryRetryCount = 0; + LdapResult tempResult = null; + + while (true) { + if (cancellationToken.IsCancellationRequested) { + yield break; + } + + if (tempResult != null) { + yield return tempResult; + yield break; + } + + SearchResponse response = null; + try { + _log.LogTrace("Sending paged ldap request - {Info}", queryParameters.GetQueryInfo()); + response = (SearchResponse)connectionWrapper.Connection.SendRequest(searchRequest); + if (response != null) { + pageResponse = (PageResultResponseControl)response.Controlsdo + .Where(x => x is PageResultResponseControl).DefaultIfEmpty(null).FirstOrDefault(); + queryRetryCount = 0; + }else if (queryRetryCount == MaxRetries) { + tempResult = LdapResult.Fail($"PagedQuery - Failed to get a response after {MaxRetries} attempts", + queryParameters); + } + else { + queryRetryCount++; + } + } + catch (LdapException le) when (le.ErrorCode == (int)LdapErrorCodes.ServerDown) { + /* + * If we dont have a servername, we're not going to be able to re-establish a connection here. Page cookies are only valid for the server they were generated on. Bail out. + */ + if (serverName == null) { + _log.LogError( + "PagedQuery - Received server down exception without a known servername. Unable to generate new connection\n{Info}", + queryParameters.GetQueryInfo()); + yield break; + } + + /* + * Paged queries will not use the cached ldap connections, as the intention is to only have 1 or a couple of these queries running at once. + * The connection logic here is simplified accordingly + */ + _connectionPool.ReleaseConnection(connectionWrapper, true); + for (var retryCount = 0; retryCount < MaxRetries; retryCount++) { + var backoffDelay = GetNextBackoff(retryCount); + await Task.Delay(backoffDelay, cancellationToken); + var (success, ldapConnectionWrapperNew, message) = await _connectionPool.GetLdapConnectionForServer( + queryParameters.DomainName,serverName, queryParameters.GlobalCatalog); + + if (success) { + _log.LogDebug("PagedQuery - Recovered from ServerDown successfully"); + connectionWrapper = ldapConnectionWrapperNew; + break; + } + + if (retryCount == MaxRetries - 1) { + _log.LogError("PagedQuery - Failed to get a new connection after ServerDown.\n{Info}", + queryParameters.GetQueryInfo()); + yield break; + } + } + } + catch (LdapException le) when (le.ErrorCode == (int)ResultCode.Busy && busyRetryCount < MaxRetries) { + /* + * If we get a busy error, we want to do an exponential backoff, but maintain the current connection + * The expectation is that given enough time, the server should stop being busy and service our query appropriately + */ + busyRetryCount++; + var backoffDelay = GetNextBackoff(busyRetryCount); + await Task.Delay(backoffDelay, cancellationToken); + } + catch (LdapException le) { + tempResult = LdapResult.Fail($"PagedQuery - Caught unrecoverable ldap exception: {le.Message} (ServerMessage: {le.ServerErrorMessage}) (ErrorCode: {le.ErrorCode})", queryParameters); + } + catch (Exception e) { + tempResult = LdapResult.Fail($"PagedQuery - Caught unrecoverable exception: {e.Message}", queryParameters); + } + + if (tempResult != null) { + yield return tempResult; + yield break; + } + + if (cancellationToken.IsCancellationRequested) { + yield break; + } + + //I'm not sure why this happens sometimes, but if we try the request again, it works sometimes, other times we get an exception + if (response == null || pageResponse == null) { + continue; + } + + foreach (ISearchResultEntry entry in response.Entries) { + if (cancellationToken.IsCancellationRequested) { + yield break; + } + + yield return LdapResult.Ok(entry); + } + + if (pageResponse.Cookie.Length == 0 || response.Entries.Count == 0 || + cancellationToken.IsCancellationRequested) + yield break; + + pageControl.Cookie = pageResponse.Cookie; + } + } + + public bool ResolveIDAndType(SecurityIdentifier securityIdentifier, string objectDomain, out TypedPrincipal resolvedPrincipal) { + return ResolveIDAndType(securityIdentifier.Value, objectDomain, out resolvedPrincipal); + } + + public async Task<(bool Success, TypedPrincipal Principal)> ResolveIDAndType(string identifier, string objectDomain) { + if (identifier.Contains("0ACNF")) { + return (false, null); + } + + if (await GetWellKnownPrincipal(identifier, objectDomain) is (true, var principal)) { + return (true, principal); + } + + var type = identifier.StartsWith("S-") ? LookupSidType(id, fallbackDomain) : LookupGuidType(id, fallbackDomain); + return new TypedPrincipal(id, type); + } + + private async Task<(bool Success, Label type)> LookupSidType(string sid, string domain) { + if (Cache.GetIDType(sid, out var type)) { + return (true, type); + } + + if (await GetDomainSidFromDomainName(domain) is (true, var domainSid)) { + + } + } + + public async Task<(bool Success, TypedPrincipal wellKnownPrincipal)> GetWellKnownPrincipal(string securityIdentifier, string objectDomain) { + if (!WellKnownPrincipal.GetWellKnownPrincipal(securityIdentifier, out var wellKnownPrincipal)) { + return (false, null); + } + + var (newIdentifier, newDomain) = await GetWellKnownPrincipalObjectIdentifier(securityIdentifier, objectDomain); + + wellKnownPrincipal.ObjectIdentifier = newIdentifier; + SeenWellKnownPrincipals.TryAdd(wellKnownPrincipal.ObjectIdentifier, new ResolvedWellKnownPrincipal { + DomainName = newDomain, + WkpId = securityIdentifier + }); + + return (true, wellKnownPrincipal); + } + + private async Task<(string ObjectID, string Domain)> GetWellKnownPrincipalObjectIdentifier(string securityIdentifier, string domain) { + if (!WellKnownPrincipal.GetWellKnownPrincipal(securityIdentifier, out _)) return (securityIdentifier, string.Empty); + + if (!securityIdentifier.Equals("S-1-5-9", StringComparison.OrdinalIgnoreCase)) { + var tempDomain = domain; + if (GetDomain(tempDomain, out var domainObject) && domainObject.Name != null) { + tempDomain = domainObject.Name; + } + return ($"{tempDomain}-{securityIdentifier}".ToUpper(), tempDomain); + } + + if (await GetForest(domain) is (true, var forest)) { + return ($"{forest}-{securityIdentifier}".ToUpper(), forest); + } + + _log.LogWarning("Failed to get a forest name for domain {Domain}, unable to resolve enterprise DC sid", domain); + return ($"UNKNOWN-{securityIdentifier}", "UNKNOWN"); + } + + private async Task<(bool Success, string ForestName)> GetForest(string domain) { + if (DomainToForestCache.TryGetValue(domain, out var cachedForest)) { + return (true, cachedForest); + } + if (GetDomain(domain, out var domainObject)) { + var forestName = domainObject.Forest.Name.ToUpper(); + DomainToForestCache.TryAdd(domain, forestName); + return (true, forestName); + } + + var (success, forest) = await GetForestFromLdap(domain); + if (success) { + DomainToForestCache.TryAdd(domain, forest); + return (true, forest); + } + + return (false, null); + } + + private async Task<(bool Success, string ForestName)> GetForestFromLdap(string domain) { + var queryParameters = new LdapQueryParameters { + Attributes = new[] { LDAPProperties.RootDomainNamingContext }, + SearchScope = SearchScope.Base, + DomainName = domain, + LDAPFilter = new LDAPFilter().AddAllObjects().GetFilter(), + }; + + var result = await Query(queryParameters).FirstAsync(); + if (result.IsSuccess) { + var rdn = result.Value.GetProperty(LDAPProperties.RootDomainNamingContext); + if (!string.IsNullOrEmpty(rdn)) { + return (true, Helpers.DistinguishedNameToDomain(rdn).ToUpper()); + } + } + + return (false, null); + } + + private static TimeSpan GetNextBackoff(int retryCount) { + return TimeSpan.FromSeconds(Math.Min( + MinBackoffDelay.TotalSeconds * Math.Pow(BackoffDelayMultiplier, retryCount), + MaxBackoffDelay.TotalSeconds)); + } + + private bool CreateSearchRequest(LdapQueryParameters queryParameters, + ref LdapConnectionWrapperNew connectionWrapper, out SearchRequest searchRequest) { + string basePath; + if (!string.IsNullOrWhiteSpace(queryParameters.SearchBase)) { + basePath = queryParameters.SearchBase; + } + else if (!connectionWrapper.GetSearchBase(queryParameters.NamingContext, out basePath)) { + string tempPath; + if (CallDsGetDcName(queryParameters.DomainName, out var info) && info != null) { + tempPath = Helpers.DomainNameToDistinguishedName(info.Value.DomainName); + connectionWrapper.SaveContext(queryParameters.NamingContext, basePath); + } + else if (GetDomain(queryParameters.DomainName, out var domainObject)) { + tempPath = Helpers.DomainNameToDistinguishedName(domainObject.Name); + } + else { + searchRequest = null; + return false; + } + + basePath = queryParameters.NamingContext switch { + NamingContext.Configuration => $"CN=Configuration,{tempPath}", + NamingContext.Schema => $"CN=Schema,CN=Configuration,{tempPath}", + NamingContext.Default => tempPath, + _ => throw new ArgumentOutOfRangeException() + }; + + connectionWrapper.SaveContext(queryParameters.NamingContext, basePath); + } + + searchRequest = new SearchRequest(basePath, queryParameters.LDAPFilter, queryParameters.SearchScope, + queryParameters.Attributes); + searchRequest.Controls.Add(new SearchOptionsControl(SearchOption.DomainScope)); + if (queryParameters.IncludeDeleted) { + searchRequest.Controls.Add(new ShowDeletedControl()); + } + + if (queryParameters.IncludeSecurityDescriptor) { + searchRequest.Controls.Add(new SecurityDescriptorFlagControl { + SecurityMasks = SecurityMasks.Dacl | SecurityMasks.Owner + }); + } + + return true; + } + + + + private bool CallDsGetDcName(string domainName, out NetAPIStructs.DomainControllerInfo? info) { + if (_dcInfoCache.TryGetValue(domainName.ToUpper().Trim(), out info)) return info != null; + + var apiResult = _nativeMethods.CallDsGetDcName(null, domainName, + (uint)(NetAPIEnums.DSGETDCNAME_FLAGS.DS_FORCE_REDISCOVERY | + NetAPIEnums.DSGETDCNAME_FLAGS.DS_RETURN_DNS_NAME | + NetAPIEnums.DSGETDCNAME_FLAGS.DS_DIRECTORY_SERVICE_REQUIRED)); + + if (apiResult.IsFailed) { + _dcInfoCache.TryAdd(domainName.ToUpper().Trim(), null); + return false; + } + + info = apiResult.Value; + return true; + } + + private async Task SetupLdapQuery(LdapQueryParameters queryParameters) { + var result = new LdapQuerySetupResult(); + var (success, connectionWrapper, message) = + await _connectionPool.GetLdapConnection(queryParameters.DomainName, queryParameters.GlobalCatalog); + if (!success) { + result.Success = false; + result.Message = $"Unable to create a connection: {message}"; + return result; + } + + //This should never happen as far as I know, so just checking for safety + if (connectionWrapper.Connection == null) { + result.Success = false; + result.Message = $"Connection object is null"; + return result; + } + + if (!CreateSearchRequest(queryParameters, ref connectionWrapper, out var searchRequest)) { + result.Success = false; + result.Message = "Failed to create search request"; + return result; + } + + result.Server = connectionWrapper.GetServer(); + result.Success = true; + result.SearchRequest = searchRequest; + result.ConnectionWrapper = connectionWrapper; + return result; + } + + public static SearchRequest CreateSearchRequest(string distinguishedName, string ldapFilter, SearchScope searchScope, + string[] attributes) { + var searchRequest = new SearchRequest(distinguishedName, ldapFilter, + searchScope, attributes); + searchRequest.Controls.Add(new SearchOptionsControl(SearchOption.DomainScope)); + return searchRequest; + } + + public async Task<(bool Success, string DomainName)> GetDomainNameFromSid(string sid) { + string domainSid; + try { + domainSid = new SecurityIdentifier(sid).AccountDomainSid?.Value.ToUpper(); + } + catch { + var match = SidRegex.Match(sid); + domainSid = match.Success ? match.Groups[1].Value : null; + } + + if (domainSid == null) { + return (false, ""); + } + + if (Cache.GetDomainSidMapping(domainSid, out var domain)) { + return (true, domain); + } + + try { + var entry = new DirectoryEntry($"LDAP://"); + entry.RefreshCache(new[] { LDAPProperties.DistinguishedName }); + var dn = entry.GetProperty(LDAPProperties.DistinguishedName); + if (!string.IsNullOrEmpty(dn)) { + Cache.AddDomainSidMapping(domainSid, Helpers.DistinguishedNameToDomain(dn)); + return (true, Helpers.DistinguishedNameToDomain(dn)); + } + } + catch { + //pass + } + + if (await ConvertDomainSidToDomainNameFromLdap(sid) is (true, var domainName)) { + Cache.AddDomainSidMapping(domainSid, domainName); + return (true, domainName); + } + + return (false, string.Empty); + } + + private async Task<(bool Success, string DomainName)> ConvertDomainSidToDomainNameFromLdap(string domainSid) { + if (!GetDomain(out var domain) || domain?.Name == null) { + return (false, string.Empty); + } + + var result = await Query(new LdapQueryParameters { + DomainName = domain.Name, + Attributes = new[] { LDAPProperties.DistinguishedName }, + GlobalCatalog = true, + LDAPFilter = new LDAPFilter().AddDomains(CommonFilters.SpecificSID(domainSid)).GetFilter() + }).FirstAsync(); + + if (result.IsSuccess) { + return (true, Helpers.DistinguishedNameToDomain(result.Value.DistinguishedName)); + } + + result = await Query(new LdapQueryParameters { + DomainName = domain.Name, + Attributes = new[] { LDAPProperties.DistinguishedName }, + GlobalCatalog = true, + LDAPFilter = new LDAPFilter().AddFilter("(objectclass=trusteddomain)", true).AddFilter($"(securityidentifier={Helpers.ConvertSidToHexSid(domainSid)})", true).GetFilter() + }).FirstAsync(); + + if (result.IsSuccess) { + return (true, Helpers.DistinguishedNameToDomain(result.Value.DistinguishedName)); + } + + result = await Query(new LdapQueryParameters { + DomainName = domain.Name, + Attributes = new[] { LDAPProperties.DistinguishedName }, + LDAPFilter = new LDAPFilter().AddFilter("(objectclass=domaindns)", true).AddFilter(CommonFilters.SpecificSID(domainSid), true).GetFilter() + }).FirstAsync(); + + if (result.IsSuccess) { + return (true, Helpers.DistinguishedNameToDomain(result.Value.DistinguishedName)); + } + + return (false, string.Empty); + } + + public async Task<(bool Success, string domainSid)> GetDomainSidFromDomainName(string domainName) { + if (Cache.GetDomainSidMapping(domainName, out var domainSid)) return (true, domainSid); + + try { + var entry = new DirectoryEntry($"LDAP://{domainName}"); + //Force load objectsid into the object cache + entry.RefreshCache(new[] { "objectSid" }); + var sid = entry.GetSid(); + if (sid != null) { + Cache.AddDomainSidMapping(domainName, sid); + domainSid = sid; + return (true, domainSid); + } + } + catch { + //we expect this to fail sometimes + } + + if (GetDomain(domainName, out var domainObject)) + try { + domainSid = domainObject.GetDirectoryEntry().GetSid(); + if (domainSid != null) { + Cache.AddDomainSidMapping(domainName, domainSid); + return (true, domainSid); + } + } + catch { + //we expect this to fail sometimes (not sure why, but better safe than sorry) + } + + foreach (var name in _translateNames) + try { + var account = new NTAccount(domainName, name); + var sid = (SecurityIdentifier)account.Translate(typeof(SecurityIdentifier)); + domainSid = sid.AccountDomainSid.ToString(); + Cache.AddDomainSidMapping(domainName, domainSid); + return (true, domainSid); + } + catch { + //We expect this to fail if the username doesn't exist in the domain + } + + var result = await Query(new LdapQueryParameters() { + DomainName = domainName, + Attributes = new[] { LDAPProperties.ObjectSID }, + LDAPFilter = new LDAPFilter().AddFilter(CommonFilters.DomainControllers, true).GetFilter() + }).FirstAsync(); + + if (result.Success) { + var sid = result.Value.GetSid(); + if (!string.IsNullOrEmpty(sid)) { + domainSid = new SecurityIdentifier(sid).AccountDomainSid.Value; + Cache.AddDomainSidMapping(domainName, domainSid); + return (true, domainSid); + } + } + return (false, string.Empty); + } + + /// + /// Attempts to get the Domain object representing the target domain. If null is specified for the domain name, gets + /// the user's current domain + /// + /// + /// + /// + public bool GetDomain(string domainName, out Domain domain) { + var cacheKey = domainName ?? _nullCacheKey; + if (DomainCache.TryGetValue(cacheKey, out domain)) return true; + + try { + DirectoryContext context; + if (_ldapConfig.Username != null) + context = domainName != null + ? new DirectoryContext(DirectoryContextType.Domain, domainName, _ldapConfig.Username, + _ldapConfig.Password) + : new DirectoryContext(DirectoryContextType.Domain, _ldapConfig.Username, + _ldapConfig.Password); + else + context = domainName != null + ? new DirectoryContext(DirectoryContextType.Domain, domainName) + : new DirectoryContext(DirectoryContextType.Domain); + + domain = Domain.GetDomain(context); + if (domain == null) return false; + DomainCache.TryAdd(cacheKey, domain); + return true; + } + catch (Exception e) { + _log.LogDebug(e, "GetDomain call failed for domain name {Name}", domainName); + return false; + } + } + + public static bool GetDomain(string domainName, LDAPConfig ldapConfig, out Domain domain) { + if (DomainCache.TryGetValue(domainName, out domain)) return true; + + try { + DirectoryContext context; + if (ldapConfig.Username != null) + context = domainName != null + ? new DirectoryContext(DirectoryContextType.Domain, domainName, ldapConfig.Username, + ldapConfig.Password) + : new DirectoryContext(DirectoryContextType.Domain, ldapConfig.Username, + ldapConfig.Password); + else + context = domainName != null + ? new DirectoryContext(DirectoryContextType.Domain, domainName) + : new DirectoryContext(DirectoryContextType.Domain); + + domain = Domain.GetDomain(context); + if (domain == null) return false; + DomainCache.TryAdd(domainName, domain); + return true; + } + catch (Exception e) { + return false; + } + } + + /// + /// Attempts to get the Domain object representing the target domain. If null is specified for the domain name, gets + /// the user's current domain + /// + /// + /// + /// + public bool GetDomain(out Domain domain) { + var cacheKey = _nullCacheKey; + if (DomainCache.TryGetValue(cacheKey, out domain)) return true; + + try { + var context = _ldapConfig.Username != null + ? new DirectoryContext(DirectoryContextType.Domain, _ldapConfig.Username, + _ldapConfig.Password) + : new DirectoryContext(DirectoryContextType.Domain); + + domain = Domain.GetDomain(context); + DomainCache.TryAdd(cacheKey, domain); + return true; + } + catch (Exception e) { + _log.LogDebug(e, "GetDomain call failed for blank domain"); + return false; + } + } + + private struct LdapFailure { + public LdapFailureReason FailureReason { get; set; } + public string Message { get; set; } + } +} \ No newline at end of file diff --git a/src/CommonLib/Result.cs b/src/CommonLib/Result.cs new file mode 100644 index 00000000..b0ac9ba1 --- /dev/null +++ b/src/CommonLib/Result.cs @@ -0,0 +1,41 @@ +namespace SharpHoundCommonLib; + +public class Result : Result { + public T Value { get; set; } + + protected internal Result(T value, bool success, string error) : base(success, error) { + Value = value; + } + + public static Result Fail(string message) { + return new Result(default, false, message); + } + + public static Result Fail() { + return new Result(default, false, string.Empty); + } + + public static Result Ok(T value) { + return new Result(value, true, string.Empty); + } +} + +public class Result { + + public string Error { get; set; } + public bool IsSuccess => Error == null && Success; + public bool Success { get; set; } + + protected Result(bool success, string error) { + Success = success; + Error = error; + } + + public static Result Fail(string message) { + return new Result(false, message); + } + + public static Result Ok() { + return new Result(true, string.Empty); + } +} \ No newline at end of file diff --git a/src/CommonLib/SearchResultEntryWrapperNew.cs b/src/CommonLib/SearchResultEntryWrapperNew.cs new file mode 100644 index 00000000..be04cb3c --- /dev/null +++ b/src/CommonLib/SearchResultEntryWrapperNew.cs @@ -0,0 +1,79 @@ +using System.Collections.Generic; +using System.DirectoryServices.Protocols; +using System.Security.Cryptography.X509Certificates; +using SharpHoundCommonLib.Enums; + +namespace SharpHoundCommonLib; + +public class SearchResultEntryWrapperNew : ISearchResultEntry { + private readonly SearchResultEntry _entry; + + public string DistinguishedName => _entry.DistinguishedName; + public ResolvedSearchResult ResolveBloodHoundInfo() { + throw new System.NotImplementedException(); + } + + public string GetProperty(string propertyName) { + throw new System.NotImplementedException(); + } + + public byte[] GetByteProperty(string propertyName) { + throw new System.NotImplementedException(); + } + + public string[] GetArrayProperty(string propertyName) { + throw new System.NotImplementedException(); + } + + public byte[][] GetByteArrayProperty(string propertyName) { + throw new System.NotImplementedException(); + } + + public bool GetIntProperty(string propertyName, out int value) { + throw new System.NotImplementedException(); + } + + public X509Certificate2[] GetCertificateArrayProperty(string propertyName) { + throw new System.NotImplementedException(); + } + + public string GetObjectIdentifier() { + throw new System.NotImplementedException(); + } + + public bool IsDeleted() { + throw new System.NotImplementedException(); + } + + public Label GetLabel() { + throw new System.NotImplementedException(); + } + + public string GetSid() { + throw new System.NotImplementedException(); + } + + public string GetGuid() { + throw new System.NotImplementedException(); + } + + public int PropCount(string prop) { + throw new System.NotImplementedException(); + } + + public IEnumerable PropertyNames() { + throw new System.NotImplementedException(); + } + + public bool IsMSA() { + throw new System.NotImplementedException(); + } + + public bool IsGMSA() { + throw new System.NotImplementedException(); + } + + public bool HasLAPS() { + throw new System.NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/CommonLib/SharpHoundCommonLib.csproj b/src/CommonLib/SharpHoundCommonLib.csproj index eca94af0..c5d9fb04 100644 --- a/src/CommonLib/SharpHoundCommonLib.csproj +++ b/src/CommonLib/SharpHoundCommonLib.csproj @@ -19,8 +19,9 @@ - - + + +