diff --git a/Dockerfile b/Dockerfile index 460809de..d641ee89 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM mono:6.12.0 # Install .NET SDK -ENV DOTNET_VERSION=5.0 +ENV DOTNET_VERSION=7.0 RUN curl -sSL https://dot.net/v1/dotnet-install.sh \ | bash -s -- -Channel $DOTNET_VERSION -InstallDir /usr/share/dotnet \ diff --git a/src/CommonLib/AsyncEnumerable.cs b/src/CommonLib/AsyncEnumerable.cs new file mode 100644 index 00000000..57d74c50 --- /dev/null +++ b/src/CommonLib/AsyncEnumerable.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace SharpHoundCommonLib; + +public static class AsyncEnumerable { + public static IAsyncEnumerable Empty() => EmptyAsyncEnumerable.Instance; + + private sealed class EmptyAsyncEnumerable : IAsyncEnumerable { + public static readonly EmptyAsyncEnumerable Instance = new(); + private readonly IAsyncEnumerator _enumerator = new EmptyAsyncEnumerator(); + public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = new CancellationToken()) { + return _enumerator; + } + } + + private sealed class EmptyAsyncEnumerator : IAsyncEnumerator { + public ValueTask DisposeAsync() => default; + public ValueTask MoveNextAsync() => new(false); + public T Current => default; + } +} \ No newline at end of file diff --git a/src/CommonLib/Cache.cs b/src/CommonLib/Cache.cs index ba46e968..e3ba46f2 100644 --- a/src/CommonLib/Cache.cs +++ b/src/CommonLib/Cache.cs @@ -85,11 +85,6 @@ internal static bool GetMachineSid(string key, out string value) return false; } - internal static void AddConvertedValue(string key, string value) - { - CacheInstance?.ValueToIdCache.TryAdd(key, value); - } - internal static void AddPrefixedValue(string key, string domain, string value) { CacheInstance?.ValueToIdCache.TryAdd(GetPrefixKey(key, domain), value); @@ -107,14 +102,7 @@ internal static void AddGCCache(string key, string[] value) internal static bool GetGCCache(string key, out string[] value) { - if (CacheInstance != null) return CacheInstance.GlobalCatalogCache.TryGetValue(key, out value); - value = null; - return false; - } - - internal static bool GetConvertedValue(string key, out string value) - { - if (CacheInstance != null) return CacheInstance.ValueToIdCache.TryGetValue(key, out value); + if (CacheInstance != null) return CacheInstance.GlobalCatalogCache.TryGetValue(key.ToUpper(), out value); value = null; return false; } diff --git a/src/CommonLib/ConnectionPoolManager.cs b/src/CommonLib/ConnectionPoolManager.cs new file mode 100644 index 00000000..f4f76719 --- /dev/null +++ b/src/CommonLib/ConnectionPoolManager.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Concurrent; +using System.DirectoryServices; +using System.Security.Principal; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using SharpHoundCommonLib.Processors; + +namespace SharpHoundCommonLib { + 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, ILogger log = null, PortScanner scanner = null) { + _ldapConfig = config; + _log = log ?? Logging.LogProvider.CreateLogger("ConnectionPoolManager"); + _portScanner = scanner ?? new PortScanner(); + } + + public void ReleaseConnection(LdapConnectionWrapper connectionWrapper, bool connectionFaulted = false) { + if (connectionWrapper == null) { + return; + } + //I don't think this is possible, but at least account for it + if (!_pools.TryGetValue(connectionWrapper.PoolIdentifier, out var pool)) { + _log.LogWarning("Could not find pool for {Identifier}", connectionWrapper.PoolIdentifier); + connectionWrapper.Connection.Dispose(); + return; + } + + pool.ReleaseConnection(connectionWrapper, connectionFaulted); + } + + public async Task<(bool Success, string Message)> TestDomainConnection(string identifier, bool globalCatalog) { + var (success, connection, message) = await GetLdapConnection(identifier, globalCatalog); + ReleaseConnection(connection); + return (success, message); + } + + public async Task<(bool Success, LdapConnectionWrapper ConnectionWrapper, string Message)> GetLdapConnection( + string identifier, bool globalCatalog) { + var resolved = ResolveIdentifier(identifier); + + if (!_pools.TryGetValue(resolved, out var pool)) { + pool = new LdapConnectionPool(identifier, resolved, _ldapConfig,scanner: _portScanner); + _pools.TryAdd(resolved, pool); + } + + if (globalCatalog) { + return await pool.GetGlobalCatalogConnectionAsync(); + } + return await pool.GetConnectionAsync(); + } + + public async Task<(bool Success, LdapConnectionWrapper connectionWrapper, string Message)> GetLdapConnectionForServer( + string identifier, string server, bool globalCatalog) { + var resolved = ResolveIdentifier(identifier); + + if (!_pools.TryGetValue(resolved, out var pool)) { + pool = new LdapConnectionPool(resolved, identifier, _ldapConfig,scanner: _portScanner); + _pools.TryAdd(resolved, pool); + } + + return await pool.GetConnectionForSpecificServerAsync(server, globalCatalog); + } + + private string ResolveIdentifier(string 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) { + if (Cache.GetDomainSidMapping(domainName, out domainSid)) return true; + + try { + var entry = new DirectoryEntry($"LDAP://{domainName}").ToDirectoryObject(); + if (entry.TryGetSecurityIdentifier(out var sid)) { + Cache.AddDomainSidMapping(domainName, sid); + domainSid = sid; + return true; + } + } + catch { + //we expect this to fail sometimes + } + + if (LdapUtils.GetDomain(domainName, _ldapConfig, out var domainObject)) + try { + if (domainObject.GetDirectoryEntry().ToDirectoryObject().TryGetSecurityIdentifier(out domainSid)) { + 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; + } + + public void Dispose() { + foreach (var kv in _pools) + { + kv.Value.Dispose(); + } + + _pools.Clear(); + } + } +} \ No newline at end of file diff --git a/src/CommonLib/DirectoryObjects/DirectoryEntryWrapper.cs b/src/CommonLib/DirectoryObjects/DirectoryEntryWrapper.cs new file mode 100644 index 00000000..9a608127 --- /dev/null +++ b/src/CommonLib/DirectoryObjects/DirectoryEntryWrapper.cs @@ -0,0 +1,188 @@ +using System; +using System.Collections.Generic; +using System.DirectoryServices; +using System.Security.Cryptography.X509Certificates; +using System.Security.Principal; +using System.Text; + +namespace SharpHoundCommonLib; + +public class DirectoryEntryWrapper : IDirectoryObject { + private readonly DirectoryEntry _entry; + + public DirectoryEntryWrapper(DirectoryEntry entry) { + _entry = entry; + } + + public bool TryGetDistinguishedName(out string value) { + return TryGetProperty(LDAPProperties.DistinguishedName, out value); + } + + private bool CheckCache(string propertyName) { + try { + if (!_entry.Properties.Contains(propertyName)) + _entry.RefreshCache(new[] { propertyName }); + + return _entry.Properties.Contains(propertyName); + } + catch { + return false; + } + } + + public bool TryGetProperty(string propertyName, out string value) { + value = string.Empty; + if (!CheckCache(propertyName)) { + return false; + } + + var s = _entry.Properties[propertyName].Value; + value = s switch { + string st => st, + int i => i.ToString(), + _ => null + }; + + return value != null; + } + + public bool TryGetByteProperty(string propertyName, out byte[] value) { + value = Array.Empty(); + if (!CheckCache(propertyName)) { + return false; + } + + var prop = _entry.Properties[propertyName].Value; + if (prop is not byte[] b) return false; + value = b; + return true; + } + + public bool TryGetArrayProperty(string propertyName, out string[] value) { + value = Array.Empty(); + if (!CheckCache(propertyName)) { + return false; + } + + var dest = new List(); + foreach (var val in _entry.Properties[propertyName]) { + if (val is string s) { + dest.Add(s); + } + } + + value = dest.ToArray(); + return true; + } + + public bool TryGetByteArrayProperty(string propertyName, out byte[][] value) { + value = Array.Empty(); + if (!CheckCache(propertyName)) { + return false; + } + + var raw = _entry.Properties[propertyName].Value; + if (raw is not byte[][] b) { + return false; + } + value = b; + return true; + } + + public bool TryGetIntProperty(string propertyName, out int value) { + value = 0; + if (!CheckCache(propertyName)) return false; + + if (!TryGetProperty(propertyName, out var s)) { + return false; + } + + return int.TryParse(s, out value); + } + + public bool TryGetCertificateArrayProperty(string propertyName, out X509Certificate2[] value) { + value = Array.Empty(); + if (!TryGetByteArrayProperty(propertyName, out var bytes)) { + return false; + } + + if (bytes.Length == 0) { + return true; + } + + var result = new List(); + + foreach (var b in bytes) { + try { + var cert = new X509Certificate2(b); + result.Add(cert); + } + catch { + //pass + } + } + + value = result.ToArray(); + return true; + } + + public bool TryGetSecurityIdentifier(out string securityIdentifier) { + securityIdentifier = string.Empty; + if (!CheckCache(LDAPProperties.ObjectSID)) { + return false; + } + + var raw = _entry.Properties[LDAPProperties.ObjectSID][0]; + try { + securityIdentifier = raw switch { + byte[] b => new SecurityIdentifier(b, 0).ToString(), + string st => new SecurityIdentifier(Encoding.ASCII.GetBytes(st), 0).ToString(), + _ => default + }; + + return securityIdentifier != default; + } + catch { + return false; + } + } + + public bool TryGetGuid(out string guid) { + guid = string.Empty; + if (!TryGetByteProperty(LDAPProperties.ObjectGUID, out var raw)) { + return false; + } + + try { + guid = new Guid(raw).ToString().ToUpper(); + return true; + } catch { + return false; + } + } + + public string GetProperty(string propertyName) { + CheckCache(propertyName); + return _entry.Properties[propertyName].Value as string; + } + + public byte[] GetByteProperty(string propertyName) { + CheckCache(propertyName); + return _entry.Properties[propertyName].Value as byte[]; + } + + public int PropertyCount(string propertyName) { + if (!CheckCache(propertyName)) { + return 0; + } + + var prop = _entry.Properties[propertyName]; + return prop.Count; + + } + + public IEnumerable PropertyNames() { + foreach (var property in _entry.Properties.PropertyNames) + yield return property.ToString().ToLower(); + } +} \ No newline at end of file diff --git a/src/CommonLib/DirectoryObjects/DirectoryObjectExtensions.cs b/src/CommonLib/DirectoryObjects/DirectoryObjectExtensions.cs new file mode 100644 index 00000000..170e7c33 --- /dev/null +++ b/src/CommonLib/DirectoryObjects/DirectoryObjectExtensions.cs @@ -0,0 +1,71 @@ +using System; +using System.Linq; +using SharpHoundCommonLib.Enums; + +namespace SharpHoundCommonLib.DirectoryObjects; + +public static class DirectoryObjectExtensions { + public static bool IsMSA(this IDirectoryObject directoryObject) { + if (!directoryObject.TryGetArrayProperty(LDAPProperties.ObjectClass, out var classes)) { + return false; + } + + return classes.Contains(ObjectClass.MSAClass, StringComparer.InvariantCultureIgnoreCase); + } + + public static bool IsGMSA(this IDirectoryObject directoryObject) { + if (!directoryObject.TryGetArrayProperty(LDAPProperties.ObjectClass, out var classes)) { + return false; + } + + return classes.Contains(ObjectClass.GMSAClass, StringComparer.InvariantCultureIgnoreCase); + } + + public static bool GetObjectIdentifier(this IDirectoryObject directoryObject, out string objectIdentifier) { + if (directoryObject.TryGetSecurityIdentifier(out objectIdentifier) && !string.IsNullOrWhiteSpace(objectIdentifier)) { + return true; + } + + return directoryObject.TryGetGuid(out objectIdentifier) && !string.IsNullOrWhiteSpace(objectIdentifier); + } + + public static bool GetLabel(this IDirectoryObject directoryObject, out Label type) { + type = Label.Base; + if (!directoryObject.GetObjectIdentifier(out var objectIdentifier)) { + return false; + } + + if (!directoryObject.TryGetIntProperty(LDAPProperties.Flags, out var flags)) { + flags = 0; + } + + directoryObject.TryGetDistinguishedName(out var distinguishedName); + directoryObject.TryGetProperty(LDAPProperties.SAMAccountType, out var samAccountType); + directoryObject.TryGetArrayProperty(LDAPProperties.ObjectClass, out var objectClasses); + + return LdapUtils.ResolveLabel(objectIdentifier, distinguishedName, samAccountType, objectClasses, flags, + out type); + } + + public static bool IsDeleted(this IDirectoryObject directoryObject) { + if (!directoryObject.TryGetProperty(LDAPProperties.IsDeleted, out var deleted)) { + return false; + } + + return bool.TryParse(deleted, out var isDeleted) && isDeleted; + } + + public static bool HasLAPS(this IDirectoryObject directoryObject) { + if (directoryObject.TryGetIntProperty(LDAPProperties.LAPSExpirationTime, out var lapsExpiration) && + lapsExpiration > 0) { + return true; + } + + if (directoryObject.TryGetIntProperty(LDAPProperties.LegacyLAPSExpirationTime, out var legacyLapsExpiration) && + legacyLapsExpiration > 0) { + return true; + } + + return false; + } +} \ No newline at end of file diff --git a/src/CommonLib/DirectoryObjects/IDirectoryObject.cs b/src/CommonLib/DirectoryObjects/IDirectoryObject.cs new file mode 100644 index 00000000..a3582e75 --- /dev/null +++ b/src/CommonLib/DirectoryObjects/IDirectoryObject.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; + +namespace SharpHoundCommonLib; + +public interface IDirectoryObject { + bool TryGetDistinguishedName(out string value); + bool TryGetProperty(string propertyName, out string value); + bool TryGetByteProperty(string propertyName, out byte[] value); + bool TryGetArrayProperty(string propertyName, out string[] value); + bool TryGetByteArrayProperty(string propertyName, out byte[][] value); + bool TryGetIntProperty(string propertyName, out int value); + bool TryGetCertificateArrayProperty(string propertyName, out X509Certificate2[] value); + bool TryGetSecurityIdentifier(out string securityIdentifier); + bool TryGetGuid(out string guid); + string GetProperty(string propertyName); + byte[] GetByteProperty(string propertyName); + int PropertyCount(string propertyName); + IEnumerable PropertyNames(); +} \ No newline at end of file diff --git a/src/CommonLib/DirectoryObjects/SearchResultEntryWrapper.cs b/src/CommonLib/DirectoryObjects/SearchResultEntryWrapper.cs new file mode 100644 index 00000000..0dc0a151 --- /dev/null +++ b/src/CommonLib/DirectoryObjects/SearchResultEntryWrapper.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.Generic; +using System.DirectoryServices.Protocols; +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using System.Security.Principal; + +namespace SharpHoundCommonLib; + +public class SearchResultEntryWrapper : IDirectoryObject { + private readonly SearchResultEntry _entry; + + public SearchResultEntryWrapper(SearchResultEntry entry) { + _entry = entry; + } + + public bool TryGetDistinguishedName(out string value) { + return TryGetProperty(LDAPProperties.DistinguishedName, out value) && !string.IsNullOrWhiteSpace(value); + } + + public bool TryGetProperty(string propertyName, out string value) { + value = string.Empty; + if (!_entry.Attributes.Contains(propertyName)) + return false; + + var collection = _entry.Attributes[propertyName]; + //Use GetValues to auto-convert to the proper type + var lookups = collection.GetValues(typeof(string)); + if (lookups.Length == 0) + return false; + + if (lookups[0] is not string prop || prop.Length == 0) + return false; + + value = prop; + return true; + } + + public bool TryGetByteProperty(string propertyName, out byte[] value) { + value = Array.Empty(); + if (!_entry.Attributes.Contains(propertyName)) + return false; + + var collection = _entry.Attributes[propertyName]; + var lookups = collection.GetValues(typeof(byte[])); + + if (lookups.Length == 0) + return false; + + if (lookups[0] is not byte[] bytes || bytes.Length == 0) + return false; + + value = bytes; + return true; + } + + public bool TryGetArrayProperty(string propertyName, out string[] value) { + value = Array.Empty(); + if (!_entry.Attributes.Contains(propertyName)) + return false; + + var values = _entry.Attributes[propertyName]; + var strings = values.GetValues(typeof(string)); + + if (strings.Length == 0) return true; + if (strings is not string[] result) return false; + + value = result; + return true; + } + + public bool TryGetByteArrayProperty(string propertyName, out byte[][] value) { + value = Array.Empty(); + if (!_entry.Attributes.Contains(propertyName)) + return false; + + var values = _entry.Attributes[propertyName]; + var bytes = values.GetValues(typeof(byte[])); + + if (bytes is not byte[][] result) return false; + value = result; + return true; + } + + public bool TryGetIntProperty(string propertyName, out int value) { + if (!TryGetProperty(propertyName, out var raw)) { + value = 0; + return false; + } + + return int.TryParse(raw, out value); + } + + public bool TryGetCertificateArrayProperty(string propertyName, out X509Certificate2[] value) { + value = Array.Empty(); + + if (!TryGetByteArrayProperty(propertyName, out var bytes)) { + return false; + } + + if (bytes.Length == 0) { + return true; + } + + var result = new List(); + + foreach (var b in bytes) { + try { + var cert = new X509Certificate2(b); + result.Add(cert); + } catch { + //pass + } + } + + value = result.ToArray(); + return true; + } + + public bool TryGetSecurityIdentifier(out string securityIdentifier) { + securityIdentifier = string.Empty; + if (!_entry.Attributes.Contains(LDAPProperties.ObjectSID)) return false; + + object[] s; + try { + s = _entry.Attributes[LDAPProperties.ObjectSID].GetValues(typeof(byte[])); + } catch (NotSupportedException) { + return false; + } + + if (s.Length == 0) + return false; + + if (s[0] is not byte[] sidBytes || sidBytes.Length == 0) + return false; + + try { + var sid = new SecurityIdentifier(sidBytes, 0); + securityIdentifier = sid.Value.ToUpper(); + return true; + } catch { + return false; + } + } + + public bool TryGetGuid(out string guid) { + guid = string.Empty; + if (!TryGetByteProperty(LDAPProperties.ObjectGUID, out var raw)) { + return false; + } + + try { + guid = new Guid(raw).ToString().ToUpper(); + return true; + } catch { + return false; + } + } + + public string GetProperty(string propertyName) { + if (!_entry.Attributes.Contains(propertyName)) + return null; + + var collection = _entry.Attributes[propertyName]; + //Use GetValues to auto-convert to the proper type + var lookups = collection.GetValues(typeof(string)); + if (lookups.Length == 0) + return null; + + if (lookups[0] is not string prop || prop.Length == 0) + return null; + + return prop; + } + + public byte[] GetByteProperty(string propertyName) { + if (!_entry.Attributes.Contains(propertyName)) + return null; + + var collection = _entry.Attributes[propertyName]; + var lookups = collection.GetValues(typeof(byte[])); + + if (lookups.Length == 0) + return Array.Empty(); + + if (lookups[0] is not byte[] bytes || bytes.Length == 0) + return Array.Empty(); + + return bytes; + } + + public int PropertyCount(string propertyName) { + if (!_entry.Attributes.Contains(propertyName)) return 0; + var prop = _entry.Attributes[propertyName]; + return prop.Count; + } + + public IEnumerable PropertyNames() { + if (_entry.Attributes.AttributeNames != null) + foreach (var property in _entry.Attributes.AttributeNames) + yield return property.ToString().ToLower(); + } +} \ No newline at end of file diff --git a/src/CommonLib/DomainInfo.cs b/src/CommonLib/DomainInfo.cs deleted file mode 100644 index 369d912f..00000000 --- a/src/CommonLib/DomainInfo.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.DirectoryServices.Protocols; - -namespace SharpHoundCommonLib -{ - public class DomainInfo - { - public string DomainSID { get; set; } - public string DomainFQDN { get; set; } - public string DomainSearchBase { get; set; } - public string DomainConfigurationPath { get; set; } - public string DomainNetbiosName { get; set; } - - public override string ToString() - { - return $"{nameof(DomainSID)}: {DomainSID}, {nameof(DomainFQDN)}: {DomainFQDN}, {nameof(DomainSearchBase)}: {DomainSearchBase}, {nameof(DomainConfigurationPath)}: {DomainConfigurationPath}, {nameof(DomainNetbiosName)}: {DomainNetbiosName}"; - } - } -} \ No newline at end of file diff --git a/src/CommonLib/Enums/CollectionMethods.cs b/src/CommonLib/Enums/CollectionMethod.cs similarity index 96% rename from src/CommonLib/Enums/CollectionMethods.cs rename to src/CommonLib/Enums/CollectionMethod.cs index d4ebf61e..0b5959e1 100644 --- a/src/CommonLib/Enums/CollectionMethods.cs +++ b/src/CommonLib/Enums/CollectionMethod.cs @@ -3,7 +3,7 @@ namespace SharpHoundCommonLib.Enums { [Flags] - public enum ResolvedCollectionMethod + public enum CollectionMethod { None = 0, Group = 1, diff --git a/src/CommonLib/Enums/DataTypes.cs b/src/CommonLib/Enums/DataTypes.cs deleted file mode 100644 index 4c3222c8..00000000 --- a/src/CommonLib/Enums/DataTypes.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace SharpHoundCommonLib.Enums -{ - public static class DataTypes - { - public const string Users = "users"; - public const string Groups = "groups"; - public const string Computers = "computers"; - public const string Domains = "domains"; - public const string GPOs = "gpos"; - public const string OUs = "ous"; - } -} \ No newline at end of file diff --git a/src/CommonLib/Enums/DirectoryPaths.cs b/src/CommonLib/Enums/DirectoryPaths.cs index 744639d9..878feef2 100644 --- a/src/CommonLib/Enums/DirectoryPaths.cs +++ b/src/CommonLib/Enums/DirectoryPaths.cs @@ -1,14 +1,14 @@ namespace SharpHoundCommonLib.Enums { - public class DirectoryPaths + public static class DirectoryPaths { - public const string EnterpriseCALocation = "CN=Enrollment Services,CN=Public Key Services,CN=Services,CN=Configuration"; - public const string RootCALocation = "CN=Certification Authorities,CN=Public Key Services,CN=Services,CN=Configuration"; - public const string AIACALocation = "CN=AIA,CN=Public Key Services,CN=Services,CN=Configuration"; - public const string CertTemplateLocation = "CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration"; - public const string NTAuthStoreLocation = "CN=NTAuthCertificates,CN=Public Key Services,CN=Services,CN=Configuration"; - public const string PKILocation = "CN=Public Key Services,CN=Services,CN=Configuration"; + public const string EnterpriseCALocation = "CN=Enrollment Services,CN=Public Key Services,CN=Services"; + public const string RootCALocation = "CN=Certification Authorities,CN=Public Key Services,CN=Services"; + public const string AIACALocation = "CN=AIA,CN=Public Key Services,CN=Services"; + public const string CertTemplateLocation = "CN=Certificate Templates,CN=Public Key Services,CN=Services"; + public const string NTAuthStoreLocation = "CN=NTAuthCertificates,CN=Public Key Services,CN=Services"; + public const string PKILocation = "CN=Public Key Services,CN=Services"; public const string ConfigLocation = "CN=Configuration"; - public const string OIDContainerLocation = "CN=OID,CN=Public Key Services,CN=Services,CN=Configuration"; + public const string OIDContainerLocation = "CN=OID,CN=Public Key Services,CN=Services"; } } \ No newline at end of file diff --git a/src/CommonLib/EdgeNames.cs b/src/CommonLib/Enums/EdgeNames.cs similarity index 97% rename from src/CommonLib/EdgeNames.cs rename to src/CommonLib/Enums/EdgeNames.cs index 276d5b00..b7e3b5a8 100644 --- a/src/CommonLib/EdgeNames.cs +++ b/src/CommonLib/Enums/EdgeNames.cs @@ -1,4 +1,4 @@ -namespace SharpHoundCommonLib +namespace SharpHoundCommonLib.Enums { public static class EdgeNames { diff --git a/src/CommonLib/LDAPProperties.cs b/src/CommonLib/Enums/LDAPProperties.cs similarity index 92% rename from src/CommonLib/LDAPProperties.cs rename to src/CommonLib/Enums/LDAPProperties.cs index 77197baf..8b13359b 100644 --- a/src/CommonLib/LDAPProperties.cs +++ b/src/CommonLib/Enums/LDAPProperties.cs @@ -36,7 +36,9 @@ public static class LDAPProperties public const string ServicePack = "operatingsystemservicepack"; public const string DNSHostName = "dnshostname"; public const string LAPSExpirationTime = "mslaps-passwordexpirationtime"; + public const string LAPSPassword = "mslaps-password"; public const string LegacyLAPSExpirationTime = "ms-mcs-admpwdexpirationtime"; + public const string LegacyLAPSPassword = "ms-mcs-admpwd"; public const string Members = "member"; public const string SecurityDescriptor = "ntsecuritydescriptor"; public const string SecurityIdentifier = "securityidentifier"; @@ -69,10 +71,14 @@ public static class LDAPProperties public const string CertificateTemplates = "certificatetemplates"; public const string CrossCertificatePair = "crosscertificatepair"; public const string Flags = "flags"; + public const string DefaultNamingContext = "defaultnamingcontext"; public const string RootDomainNamingContext = "rootdomainnamingcontext"; public const string ConfigurationNamingContext = "configurationnamingcontext"; + public const string SchemaNamingContext = "schemanamingcontext"; public const string NetbiosName = "netbiosName"; public const string DnsRoot = "dnsroot"; public const string ServerName = "servername"; + public const string OU = "ou"; + public const string ProfilePath = "profilepath"; } } diff --git a/src/CommonLib/Enums/LdapErrorCodes.cs b/src/CommonLib/Enums/LdapErrorCodes.cs index ff81967c..becf71c4 100644 --- a/src/CommonLib/Enums/LdapErrorCodes.cs +++ b/src/CommonLib/Enums/LdapErrorCodes.cs @@ -3,6 +3,7 @@ public enum LdapErrorCodes : int { Success = 0, + InvalidCredentials = 49, Busy = 51, ServerDown = 81, LocalError = 82, diff --git a/src/CommonLib/Enums/LdapFailureReason.cs b/src/CommonLib/Enums/LdapFailureReason.cs new file mode 100644 index 00000000..434e3a69 --- /dev/null +++ b/src/CommonLib/Enums/LdapFailureReason.cs @@ -0,0 +1,12 @@ +namespace SharpHoundCommonLib.Enums { + public enum LdapFailureReason + { + None, + NoData, + FailedBind, + FailedRequest, + FailedAuthentication, + AuthenticationException, + Unknown + } +} \ No newline at end of file diff --git a/src/CommonLib/Enums/NamingContext.cs b/src/CommonLib/Enums/NamingContext.cs new file mode 100644 index 00000000..29943cdb --- /dev/null +++ b/src/CommonLib/Enums/NamingContext.cs @@ -0,0 +1,8 @@ +namespace SharpHoundCommonLib.Enums { + public enum NamingContext + { + Default, + Configuration, + Schema, + } +} \ No newline at end of file diff --git a/src/CommonLib/Enums/ObjectClass.cs b/src/CommonLib/Enums/ObjectClass.cs new file mode 100644 index 00000000..f8bab0fc --- /dev/null +++ b/src/CommonLib/Enums/ObjectClass.cs @@ -0,0 +1,15 @@ +namespace SharpHoundCommonLib.Enums; + +public static class ObjectClass { + public const string GroupPolicyContainerClass = "groupPolicyContainer"; + public const string OrganizationalUnitClass = "organizationalUnit"; + public const string DomainClass = "domain"; + public const string ContainerClass = "container"; + public const string ConfigurationClass = "configuration"; + public const string PKICertificateTemplateClass = "pKICertificateTemplate"; + public const string PKIEnrollmentServiceClass = "pKIEnrollmentService"; + public const string CertificationAuthorityClass = "certificationAuthority"; + public const string OIDContainerClass = "msPKI-Enterprise-Oid"; + public const string GMSAClass = "msds-groupmanagedserviceaccount"; + public const string MSAClass = "msds-managedserviceaccount"; +} \ No newline at end of file diff --git a/src/CommonLib/Exceptions/LDAPQueryException.cs b/src/CommonLib/Exceptions/LDAPQueryException.cs deleted file mode 100644 index b463a51e..00000000 --- a/src/CommonLib/Exceptions/LDAPQueryException.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; - -namespace SharpHoundCommonLib.Exceptions -{ - public class LDAPQueryException : Exception - { - public LDAPQueryException() - { - } - - public LDAPQueryException(string message) : base(message) - { - } - - public LDAPQueryException(string message, Exception inner) : base(message, inner) - { - } - } -} \ No newline at end of file diff --git a/src/CommonLib/Exceptions/LdapAuthenticationException.cs b/src/CommonLib/Exceptions/LdapAuthenticationException.cs index e55413fb..ca3be839 100644 --- a/src/CommonLib/Exceptions/LdapAuthenticationException.cs +++ b/src/CommonLib/Exceptions/LdapAuthenticationException.cs @@ -3,10 +3,12 @@ namespace SharpHoundCommonLib.Exceptions { - public class LdapAuthenticationException : Exception + internal class LdapAuthenticationException : Exception { + public readonly LdapException LdapException; public LdapAuthenticationException(LdapException exception) : base("Error authenticating to LDAP", exception) { + LdapException = exception; } } } \ No newline at end of file diff --git a/src/CommonLib/Exceptions/LdapConnectionException.cs b/src/CommonLib/Exceptions/LdapConnectionException.cs index 3d56c9e5..3bce86c4 100644 --- a/src/CommonLib/Exceptions/LdapConnectionException.cs +++ b/src/CommonLib/Exceptions/LdapConnectionException.cs @@ -3,7 +3,7 @@ namespace SharpHoundCommonLib.Exceptions { - public class LdapConnectionException : Exception + internal class LdapConnectionException : Exception { public int ErrorCode { get; } public LdapConnectionException(LdapException innerException) : base("Failed during ldap connection tests", innerException) diff --git a/src/CommonLib/Exceptions/NoLdapDataException.cs b/src/CommonLib/Exceptions/NoLdapDataException.cs index 860f1694..771f99da 100644 --- a/src/CommonLib/Exceptions/NoLdapDataException.cs +++ b/src/CommonLib/Exceptions/NoLdapDataException.cs @@ -2,12 +2,10 @@ namespace SharpHoundCommonLib.Exceptions { - public class NoLdapDataException : Exception + internal class NoLdapDataException : Exception { - public int ErrorCode { get; set; } - public NoLdapDataException(int errorCode) + public NoLdapDataException() { - ErrorCode = errorCode; } } } \ No newline at end of file diff --git a/src/CommonLib/Extensions.cs b/src/CommonLib/Extensions.cs index e2c732e8..c7086a62 100644 --- a/src/CommonLib/Extensions.cs +++ b/src/CommonLib/Extensions.cs @@ -1,429 +1,178 @@ using System; using System.Collections.Generic; using System.DirectoryServices; -using System.DirectoryServices.Protocols; using System.Linq; -using System.Security.Cryptography.X509Certificates; using System.Security.Principal; -using System.Text; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using SharpHoundCommonLib.Enums; -using SharpHoundCommonLib.LDAPQueries; -using SearchScope = System.DirectoryServices.Protocols.SearchScope; namespace SharpHoundCommonLib { public static class Extensions { - private const string GMSAClass = "msds-groupmanagedserviceaccount"; - private const string MSAClass = "msds-managedserviceaccount"; private static readonly ILogger Log; static Extensions() { Log = Logging.LogProvider.CreateLogger("Extensions"); } - - internal static async Task> ToListAsync(this IAsyncEnumerable items) + + public static async Task> ToListAsync(this IAsyncEnumerable items) { + if (items == null) { + return new List(); + } var results = new List(); await foreach (var item in items .ConfigureAwait(false)) results.Add(item); return results; } - - /// - /// Helper function to print attributes of a SearchResultEntry - /// - /// - public static string PrintEntry(this SearchResultEntry searchResultEntry) + + public static async Task ToArrayAsync(this IAsyncEnumerable items) { - var sb = new StringBuilder(); - if (searchResultEntry.Attributes.AttributeNames == null) return sb.ToString(); - foreach (var propertyName in searchResultEntry.Attributes.AttributeNames) - { - var property = propertyName.ToString(); - sb.Append(property).Append("\t").Append(searchResultEntry.GetProperty(property)).Append("\n"); + if (items == null) { + return Array.Empty(); } - - return sb.ToString(); - } - - public static string LdapValue(this SecurityIdentifier s) - { - var bytes = new byte[s.BinaryLength]; - s.GetBinaryForm(bytes, 0); - - var output = $"\\{BitConverter.ToString(bytes).Replace('-', '\\')}"; - return output; - } - - public static string LdapValue(this Guid s) - { - var bytes = s.ToByteArray(); - var output = $"\\{BitConverter.ToString(bytes).Replace('-', '\\')}"; - return output; + var results = new List(); + await foreach (var item in items + .ConfigureAwait(false)) + results.Add(item); + return results.ToArray(); } - public static string GetSid(this DirectoryEntry result) - { - try - { - if (!result.Properties.Contains(LDAPProperties.ObjectSID)) - return null; - } - catch - { - return null; + public static async Task FirstOrDefaultAsync(this IAsyncEnumerable source, + CancellationToken cancellationToken = default) { + if (source == null) { + return default; } - var s = result.Properties[LDAPProperties.ObjectSID][0]; - return s switch - { - byte[] b => new SecurityIdentifier(b, 0).ToString(), - string st => new SecurityIdentifier(Encoding.ASCII.GetBytes(st), 0).ToString(), - _ => null - }; - } - - /// - /// Returns true if any computer collection methods are set - /// - /// - /// - public static bool IsComputerCollectionSet(this ResolvedCollectionMethod methods) - { - return (methods & ResolvedCollectionMethod.ComputerOnly) != 0; - } - - /// - /// Returns true if any local group collections are set - /// - /// - /// - public static bool IsLocalGroupCollectionSet(this ResolvedCollectionMethod methods) - { - return (methods & ResolvedCollectionMethod.LocalGroups) != 0; + await using (var enumerator = source.GetAsyncEnumerator(cancellationToken)) { + var first = await enumerator.MoveNextAsync() ? enumerator.Current : default; + return first; + } } + + public static async Task FirstOrDefaultAsync(this IAsyncEnumerable source, T defaultValue, + CancellationToken cancellationToken = default) { + if (source == null) { + return defaultValue; + } - /// - /// Gets the relative identifier for a SID - /// - /// - /// - public static int Rid(this SecurityIdentifier securityIdentifier) - { - var value = securityIdentifier.Value; - var rid = int.Parse(value.Substring(value.LastIndexOf("-", StringComparison.Ordinal) + 1)); - return rid; + await using (var enumerator = source.GetAsyncEnumerator(cancellationToken)) { + var first = await enumerator.MoveNextAsync() ? enumerator.Current : defaultValue; + return first; + } } - - #region SearchResultEntry - - /// - /// Gets the specified property as a string from the SearchResultEntry - /// - /// - /// The LDAP name of the property you want to get - /// The string value of the property if it exists or null - public static string GetProperty(this SearchResultEntry entry, string property) - { - if (!entry.Attributes.Contains(property)) - return null; - - var collection = entry.Attributes[property]; - //Use GetValues to auto-convert to the proper type - var lookups = collection.GetValues(typeof(string)); - if (lookups.Length == 0) - return null; - - if (lookups[0] is not string prop || prop.Length == 0) - return null; - - return prop; + + public static IAsyncEnumerable DefaultIfEmpty(this IAsyncEnumerable source, + T defaultValue, CancellationToken cancellationToken = default) { + return new DefaultIfEmptyAsyncEnumerable(source, defaultValue); } - /// - /// Get's the string representation of the "objectguid" property from the SearchResultEntry - /// - /// - /// The string representation of the object's GUID if possible, otherwise null - public static string GetGuid(this SearchResultEntry entry) - { - if (entry.Attributes.Contains(LDAPProperties.ObjectGUID)) - { - var guidBytes = entry.GetPropertyAsBytes(LDAPProperties.ObjectGUID); - - return new Guid(guidBytes).ToString().ToUpper(); + private sealed class DefaultIfEmptyAsyncEnumerable : IAsyncEnumerable { + private readonly DefaultIfEmptyAsyncEnumerator _enumerator; + public DefaultIfEmptyAsyncEnumerable(IAsyncEnumerable source, T defaultValue) { + _enumerator = new DefaultIfEmptyAsyncEnumerator(source, defaultValue); + } + public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = new CancellationToken()) { + return _enumerator; } - - return null; } - /// - /// Gets the "objectsid" property as a string from the SearchResultEntry - /// - /// - /// The string representation of the object's SID if possible, otherwise null - public static string GetSid(this SearchResultEntry entry) - { - if (!entry.Attributes.Contains(LDAPProperties.ObjectSID)) return null; + private sealed class DefaultIfEmptyAsyncEnumerator : IAsyncEnumerator { + private readonly IAsyncEnumerable _source; + private readonly T _defaultValue; + private T _current; + private bool _enumeratorDisposed; - object[] s; - try - { - s = entry.Attributes[LDAPProperties.ObjectSID].GetValues(typeof(byte[])); + private IAsyncEnumerator _enumerator; + + public DefaultIfEmptyAsyncEnumerator(IAsyncEnumerable source, T defaultValue) { + _source = source; + _defaultValue = defaultValue; } - catch (NotSupportedException) - { - return null; + + public async ValueTask DisposeAsync() { + _enumeratorDisposed = true; + if (_enumerator != null) { + await _enumerator.DisposeAsync().ConfigureAwait(false); + _enumerator = null; + } } - if (s.Length == 0) - return null; + public async ValueTask MoveNextAsync() { + if (_enumeratorDisposed) { + return false; + } + _enumerator ??= _source.GetAsyncEnumerator(); - if (s[0] is not byte[] sidBytes || sidBytes.Length == 0) - return null; + if (await _enumerator.MoveNextAsync().ConfigureAwait(false)) { + _current = _enumerator.Current; + return true; + } - try - { - var sid = new SecurityIdentifier(sidBytes, 0); - return sid.Value.ToUpper(); + _current = _defaultValue; + await DisposeAsync().ConfigureAwait(false); + return true; } - catch (ArgumentNullException) - { - return null; - } - } - - /// - /// Gets the specified property as a string array from the SearchResultEntry - /// - /// - /// The LDAP name of the property you want to get - /// The specified property as an array of strings if possible, else an empty array - public static string[] GetPropertyAsArray(this SearchResultEntry entry, string property) - { - if (!entry.Attributes.Contains(property)) - return Array.Empty(); - - var values = entry.Attributes[property]; - var strings = values.GetValues(typeof(string)); - return strings is not string[] result ? Array.Empty() : result; + public T Current => _current; } - /// - /// Gets the specified property as an array of byte arrays from the SearchResultEntry - /// Used for SIDHistory - /// - /// - /// The LDAP name of the property you want to get - /// The specified property as an array of bytes if possible, else an empty array - public static byte[][] GetPropertyAsArrayOfBytes(this SearchResultEntry entry, string property) - { - if (!entry.Attributes.Contains(property)) - return Array.Empty(); - var values = entry.Attributes[property]; - var bytes = values.GetValues(typeof(byte[])); - - return bytes is not byte[][] result ? Array.Empty() : result; - } - - /// - /// Gets the specified property as a byte array - /// - /// - /// The LDAP name of the property you want to get - /// An array of bytes if possible, else null - public static byte[] GetPropertyAsBytes(this SearchResultEntry searchResultEntry, string property) + public static string LdapValue(this SecurityIdentifier s) { - if (!searchResultEntry.Attributes.Contains(property)) - return null; - - var collection = searchResultEntry.Attributes[property]; - var lookups = collection.GetValues(typeof(byte[])); - - if (lookups.Length == 0) - return Array.Empty(); - - if (lookups[0] is not byte[] bytes || bytes.Length == 0) - return Array.Empty(); + var bytes = new byte[s.BinaryLength]; + s.GetBinaryForm(bytes, 0); - return bytes; + var output = $"\\{BitConverter.ToString(bytes).Replace('-', '\\')}"; + return output; } - /// - /// Gets the specified property as an int - /// - /// - /// - /// - /// - public static bool GetPropertyAsInt(this SearchResultEntry entry, string property, out int value) + public static string LdapValue(this Guid s) { - var prop = entry.GetProperty(property); - if (prop != null) return int.TryParse(prop, out value); - value = 0; - return false; + var bytes = s.ToByteArray(); + var output = $"\\{BitConverter.ToString(bytes).Replace('-', '\\')}"; + return output; } - + /// - /// Gets the specified property as an array of X509 certificates. + /// Returns true if any computer collection methods are set /// - /// - /// + /// /// - public static X509Certificate2[] GetPropertyAsArrayOfCertificates(this SearchResultEntry searchResultEntry, - string property) - { - if (!searchResultEntry.Attributes.Contains(property)) - return null; - - return searchResultEntry.GetPropertyAsArrayOfBytes(property).Select(x => new X509Certificate2(x)).ToArray(); - } - - - /// - /// Attempts to get the unique object identifier as used by BloodHound for the Search Result Entry. Tries to get - /// objectsid first, and then objectguid next. - /// - /// - /// String representation of the entry's object identifier or null - public static string GetObjectIdentifier(this SearchResultEntry entry) - { - return entry.GetSid() ?? entry.GetGuid(); + public static bool IsComputerCollectionSet(this CollectionMethod methods) { + const CollectionMethod test = CollectionMethod.ComputerOnly | CollectionMethod.LoggedOn; + return (methods & test) != 0; } /// - /// Checks the isDeleted LDAP property to determine if an entry has been deleted from the directory + /// Returns true if any local group collections are set /// - /// + /// /// - public static bool IsDeleted(this SearchResultEntry entry) + public static bool IsLocalGroupCollectionSet(this CollectionMethod methods) { - var deleted = entry.GetProperty(LDAPProperties.IsDeleted); - return bool.TryParse(deleted, out var isDeleted) && isDeleted; + return (methods & CollectionMethod.LocalGroups) != 0; } /// - /// Extension method to determine the BloodHound type of a SearchResultEntry using LDAP properties - /// Requires ldap properties objectsid, samaccounttype, objectclass + /// Gets the relative identifier for a SID /// - /// + /// /// - public static Label GetLabel(this SearchResultEntry entry) + public static int Rid(this SecurityIdentifier securityIdentifier) { - var objectId = entry.GetObjectIdentifier(); - - if (objectId == null) - { - Log.LogWarning("Failed to get an object identifier for {DN}", entry.DistinguishedName); - return Label.Base; - } - - if (objectId.StartsWith("S-1") && - WellKnownPrincipal.GetWellKnownPrincipal(objectId, out var commonPrincipal)) - { - Log.LogDebug("GetLabel - {ObjectID} is a WellKnownPrincipal with {Type}", objectId, - commonPrincipal.ObjectType); - return commonPrincipal.ObjectType; - } - - - var objectType = Label.Base; - var samAccountType = entry.GetProperty(LDAPProperties.SAMAccountType); - var objectClasses = entry.GetPropertyAsArray(LDAPProperties.ObjectClass); - - //Override object class for GMSA/MSA accounts - if (objectClasses != null && (objectClasses.Contains(MSAClass, StringComparer.OrdinalIgnoreCase) || - objectClasses.Contains(GMSAClass, StringComparer.OrdinalIgnoreCase))) - { - Log.LogDebug("GetLabel - {ObjectID} is an MSA/GMSA, returning User", objectId); - Cache.AddConvertedValue(entry.DistinguishedName, objectId); - Cache.AddType(objectId, objectType); - return Label.User; - } - - - //Its not a common principal. Lets use properties to figure out what it actually is - if (samAccountType != null) objectType = Helpers.SamAccountTypeToType(samAccountType); - - Log.LogDebug("GetLabel - SamAccountTypeToType returned {Label}", objectType); - if (objectType != Label.Base) - { - Cache.AddConvertedValue(entry.DistinguishedName, objectId); - Cache.AddType(objectId, objectType); - return objectType; - } - - - if (objectClasses == null) - { - Log.LogDebug("GetLabel - ObjectClasses for {ObjectID} is null", objectId); - objectType = Label.Base; - } - else - { - Log.LogDebug("GetLabel - ObjectClasses for {ObjectID}: {Classes}", objectId, - string.Join(", ", objectClasses)); - if (objectClasses.Contains(GroupPolicyContainerClass, StringComparer.InvariantCultureIgnoreCase)) - objectType = Label.GPO; - else if (objectClasses.Contains(OrganizationalUnitClass, StringComparer.InvariantCultureIgnoreCase)) - objectType = Label.OU; - else if (objectClasses.Contains(DomainClass, StringComparer.InvariantCultureIgnoreCase)) - objectType = Label.Domain; - else if (objectClasses.Contains(ContainerClass, StringComparer.InvariantCultureIgnoreCase)) - objectType = Label.Container; - else if (objectClasses.Contains(ConfigurationClass, StringComparer.InvariantCultureIgnoreCase)) - objectType = Label.Configuration; - else if (objectClasses.Contains(PKICertificateTemplateClass, StringComparer.InvariantCultureIgnoreCase)) - objectType = Label.CertTemplate; - else if (objectClasses.Contains(PKIEnrollmentServiceClass, StringComparer.InvariantCultureIgnoreCase)) - objectType = Label.EnterpriseCA; - else if (objectClasses.Contains(CertificationAuthorityClass, StringComparer.InvariantCultureIgnoreCase)) - { - if (entry.DistinguishedName.Contains(DirectoryPaths.RootCALocation)) - objectType = Label.RootCA; - else if (entry.DistinguishedName.Contains(DirectoryPaths.AIACALocation)) - objectType = Label.AIACA; - else if (entry.DistinguishedName.Contains(DirectoryPaths.NTAuthStoreLocation)) - objectType = Label.NTAuthStore; - }else if (objectClasses.Contains(OIDContainerClass, StringComparer.InvariantCultureIgnoreCase)) - { - if (entry.DistinguishedName.StartsWith(DirectoryPaths.OIDContainerLocation, - StringComparison.InvariantCultureIgnoreCase)) - objectType = Label.Container; - else - { - if (entry.GetPropertyAsInt(LDAPProperties.Flags, out var flags) && flags == 2) - { - objectType = Label.IssuancePolicy; - } - } - } - } - - Log.LogDebug("GetLabel - Final label for {ObjectID}: {Label}", objectId, objectType); - - Cache.AddConvertedValue(entry.DistinguishedName, objectId); - Cache.AddType(objectId, objectType); - return objectType; + var value = securityIdentifier.Value; + var rid = int.Parse(value.Substring(value.LastIndexOf("-", StringComparison.Ordinal) + 1)); + return rid; + } + + public static IDirectoryObject ToDirectoryObject(this DirectoryEntry entry) { + return new DirectoryEntryWrapper(entry); } - - private const string GroupPolicyContainerClass = "groupPolicyContainer"; - private const string OrganizationalUnitClass = "organizationalUnit"; - private const string DomainClass = "domain"; - private const string ContainerClass = "container"; - private const string ConfigurationClass = "configuration"; - private const string PKICertificateTemplateClass = "pKICertificateTemplate"; - private const string PKIEnrollmentServiceClass = "pKIEnrollmentService"; - private const string CertificationAuthorityClass = "certificationAuthority"; - private const string OIDContainerClass = "msPKI-Enterprise-Oid"; - - #endregion } } \ No newline at end of file diff --git a/src/CommonLib/Helpers.cs b/src/CommonLib/Helpers.cs index 7ac7cf7d..63d4c466 100644 --- a/src/CommonLib/Helpers.cs +++ b/src/CommonLib/Helpers.cs @@ -12,52 +12,45 @@ using SharpHoundCommonLib.Processors; using Microsoft.Win32; -namespace SharpHoundCommonLib -{ - public static class Helpers - { - private static readonly HashSet Groups = new() {"268435456", "268435457", "536870912", "536870913"}; - private static readonly HashSet Computers = new() {"805306369"}; - private static readonly HashSet Users = new() {"805306368"}; +namespace SharpHoundCommonLib { + public static class Helpers { + private static readonly HashSet Groups = new() { "268435456", "268435457", "536870912", "536870913" }; + private static readonly HashSet Computers = new() { "805306369" }; + private static readonly HashSet Users = new() { "805306368" }; private static readonly Regex DCReplaceRegex = new("DC=", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex SPNRegex = new(@".*\/.*", RegexOptions.Compiled); private static readonly DateTime EpochDiff = new(1970, 1, 1); - private static readonly string[] FilteredSids = - { + + private static readonly string[] FilteredSids = { "S-1-5-2", "S-1-5-3", "S-1-5-4", "S-1-5-6", "S-1-5-7", "S-1-2", "S-1-2-0", "S-1-5-18", "S-1-5-19", "S-1-5-20", "S-1-0-0", "S-1-0", "S-1-2-1" }; - public static string RemoveDistinguishedNamePrefix(string distinguishedName) - { - if (!distinguishedName.Contains(",")) - { + public static string RemoveDistinguishedNamePrefix(string distinguishedName) { + if (!distinguishedName.Contains(",")) { return ""; } - if (distinguishedName.IndexOf("DC=", StringComparison.OrdinalIgnoreCase) < 0) - { + + if (distinguishedName.IndexOf("DC=", StringComparison.OrdinalIgnoreCase) < 0) { return ""; } //Start at the first instance of a comma, and continue to loop while we still have commas. If we get -1, it means we ran out of commas. //This allows us to cleanly iterate over all indexes of commas in our DNs and find the first non-escaped one - for (var i = distinguishedName.IndexOf(','); i > -1; i = distinguishedName.IndexOf(',', i + 1)) - { + for (var i = distinguishedName.IndexOf(','); i > -1; i = distinguishedName.IndexOf(',', i + 1)) { //If theres a comma at the beginning of the DN, something screwy is going on. Just ignore it - if (i == 0) - { + if (i == 0) { continue; } //This indicates an escaped comma, which we should not use to split a DN - if (distinguishedName[i-1] == '\\') - { + if (distinguishedName[i - 1] == '\\') { continue; } - + //This is an unescaped comma, so snip our DN from this comma onwards and return this as the cleaned distinguished name - return distinguishedName.Substring(i + 1); + return distinguishedName.Substring(i + 1); } return ""; @@ -70,11 +63,9 @@ public static string RemoveDistinguishedNamePrefix(string distinguishedName) /// /// /// - public static IEnumerable SplitGPLinkProperty(string linkProp, bool filterDisabled = true) - { + public static IEnumerable SplitGPLinkProperty(string linkProp, bool filterDisabled = true) { foreach (var link in linkProp.Split(']', '[') - .Where(x => x.StartsWith("LDAP", StringComparison.OrdinalIgnoreCase))) - { + .Where(x => x.StartsWith("LDAP", StringComparison.OrdinalIgnoreCase))) { var s = link.Split(';'); var dn = s[0].Substring(s[0].IndexOf("CN=", StringComparison.OrdinalIgnoreCase)); var status = s[1]; @@ -84,8 +75,7 @@ public static IEnumerable SplitGPLinkProperty(string linkProp, boo if (status is "3" or "1") continue; - yield return new ParsedGPLink - { + yield return new ParsedGPLink { Status = status.TrimStart().TrimEnd(), DistinguishedName = dn.TrimStart().TrimEnd() }; @@ -97,8 +87,7 @@ public static IEnumerable SplitGPLinkProperty(string linkProp, boo /// /// /// Label value representing type - public static Label SamAccountTypeToType(string samAccountType) - { + public static Label SamAccountTypeToType(string samAccountType) { if (Groups.Contains(samAccountType)) return Label.Group; @@ -116,8 +105,7 @@ public static Label SamAccountTypeToType(string samAccountType) /// /// String security identifier to convert /// String representation to use in LDAP filters - public static string ConvertSidToHexSid(string sid) - { + public static string ConvertSidToHexSid(string sid) { var securityIdentifier = new SecurityIdentifier(sid); var sidBytes = new byte[securityIdentifier.BinaryLength]; securityIdentifier.GetBinaryForm(sidBytes, 0); @@ -131,8 +119,7 @@ public static string ConvertSidToHexSid(string sid) /// /// /// - public static string ConvertGuidToHexGuid(string guid) - { + public static string ConvertGuidToHexGuid(string guid) { var guidObj = new Guid(guid); var guidBytes = guidObj.ToByteArray(); var output = $"\\{BitConverter.ToString(guidBytes).Replace('-', '\\')}"; @@ -144,19 +131,15 @@ public static string ConvertGuidToHexGuid(string guid) /// /// Distinguished Name to extract domain from /// String representing the domain name of this object - public static string DistinguishedNameToDomain(string distinguishedName) - { + public static string DistinguishedNameToDomain(string distinguishedName) { int idx; - if (distinguishedName.ToUpper().Contains("DELETED OBJECTS")) - { + if (distinguishedName.ToUpper().Contains("DELETED OBJECTS")) { idx = distinguishedName.IndexOf("DC=", 3, StringComparison.Ordinal); - } - else - { + } else { idx = distinguishedName.IndexOf("DC=", - StringComparison.CurrentCultureIgnoreCase); + StringComparison.CurrentCultureIgnoreCase); } - + if (idx < 0) return null; @@ -165,13 +148,21 @@ public static string DistinguishedNameToDomain(string distinguishedName) 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 /// /// Raw service principal name /// Stripped service principal name with (hopefully) just the hostname - public static string StripServicePrincipalName(string target) - { + public static string StripServicePrincipalName(string target) { return SPNRegex.IsMatch(target) ? target.Split('/')[1].Split(':')[0] : target; } @@ -180,8 +171,7 @@ public static string StripServicePrincipalName(string target) /// /// /// - public static string Base64(string input) - { + public static string Base64(string input) { var plainBytes = Encoding.UTF8.GetBytes(input); return Convert.ToBase64String(plainBytes); } @@ -191,8 +181,7 @@ public static string Base64(string input) /// /// /// - public static long ConvertFileTimeToUnixEpoch(string ldapTime) - { + public static long ConvertFileTimeToUnixEpoch(string ldapTime) { if (ldapTime == null) return -1; @@ -202,12 +191,9 @@ public static long ConvertFileTimeToUnixEpoch(string ldapTime) long toReturn; - try - { - toReturn = (long) Math.Floor(DateTime.FromFileTimeUtc(time).Subtract(EpochDiff).TotalSeconds); - } - catch - { + try { + toReturn = (long)Math.Floor(DateTime.FromFileTimeUtc(time).Subtract(EpochDiff).TotalSeconds); + } catch { toReturn = -1; } @@ -219,15 +205,11 @@ public static long ConvertFileTimeToUnixEpoch(string ldapTime) /// /// /// - public static long ConvertTimestampToUnixEpoch(string ldapTime) - { - try - { + public static long ConvertTimestampToUnixEpoch(string ldapTime) { + try { var dt = DateTime.ParseExact(ldapTime, "yyyyMMddHHmmss.0K", CultureInfo.CurrentCulture); - return (long) dt.Subtract(EpochDiff).TotalSeconds; - } - catch - { + return (long)dt.Subtract(EpochDiff).TotalSeconds; + } catch { return 0; } } @@ -237,22 +219,20 @@ public static long ConvertTimestampToUnixEpoch(string ldapTime) /// /// /// - public static long ConvertLdapTimeToLong(string ldapTime) - { + public static long ConvertLdapTimeToLong(string ldapTime) { if (ldapTime == null) return -1; var time = long.Parse(ldapTime); return time; } - + /// /// Removes some commonly seen SIDs that have no use in the schema /// /// /// - internal static string PreProcessSID(string sid) - { + internal static string PreProcessSID(string sid) { sid = sid?.ToUpper(); if (sid != null) //Ignore Local System/Creator Owner/Principal Self @@ -260,9 +240,8 @@ internal static string PreProcessSID(string sid) return null; } - - public static bool IsSidFiltered(string sid) - { + + public static bool IsSidFiltered(string sid) { //Uppercase just in case we get a lowercase s sid = sid.ToUpper(); if (sid.StartsWith("S-1-5-80") || sid.StartsWith("S-1-5-82") || @@ -275,48 +254,37 @@ public static bool IsSidFiltered(string sid) return false; } - public static RegistryResult GetRegistryKeyData(string target, string subkey, string subvalue, ILogger log) - { + public static RegistryResult GetRegistryKeyData(string target, string subkey, string subvalue, ILogger log) { var data = new RegistryResult(); - - try - { + + try { var baseKey = OpenRemoteRegistry(target); var value = baseKey.GetValue(subkey, subvalue); data.Value = value; data.Collected = true; - } - catch (IOException e) - { - log.LogError(e, "Error getting data from registry for {Target}: {RegSubKey}:{RegValue}", + } catch (IOException e) { + log.LogDebug(e, "Error getting data from registry for {Target}: {RegSubKey}:{RegValue}", target, subkey, subvalue); data.FailureReason = "Target machine was not found or not connectable"; - } - catch (SecurityException e) - { - log.LogError(e, "Error getting data from registry for {Target}: {RegSubKey}:{RegValue}", + } catch (SecurityException e) { + log.LogDebug(e, "Error getting data from registry for {Target}: {RegSubKey}:{RegValue}", target, subkey, subvalue); data.FailureReason = "User does not have the proper permissions to perform this operation"; - } - catch (UnauthorizedAccessException e) - { - log.LogError(e, "Error getting data from registry for {Target}: {RegSubKey}:{RegValue}", + } catch (UnauthorizedAccessException e) { + log.LogDebug(e, "Error getting data from registry for {Target}: {RegSubKey}:{RegValue}", target, subkey, subvalue); data.FailureReason = "User does not have the necessary registry rights"; - } - catch (Exception e) - { - log.LogError(e, "Error getting data from registry for {Target}: {RegSubKey}:{RegValue}", + } catch (Exception e) { + log.LogDebug(e, "Error getting data from registry for {Target}: {RegSubKey}:{RegValue}", target, subkey, subvalue); data.FailureReason = e.Message; } return data; } - - public static IRegistryKey OpenRemoteRegistry(string target) - { + + public static IRegistryKey OpenRemoteRegistry(string target) { var key = new SHRegistryKey(RegistryHive.LocalMachine, target); return key; } @@ -329,8 +297,7 @@ public static IRegistryKey OpenRemoteRegistry(string target) }; } - public class ParsedGPLink - { + public class ParsedGPLink { public string DistinguishedName { get; set; } public string Status { get; set; } } diff --git a/src/CommonLib/ILDAPUtils.cs b/src/CommonLib/ILDAPUtils.cs deleted file mode 100644 index ff6a3517..00000000 --- a/src/CommonLib/ILDAPUtils.cs +++ /dev/null @@ -1,144 +0,0 @@ -using System.Collections.Generic; -using System.DirectoryServices.ActiveDirectory; -using System.DirectoryServices.Protocols; -using System.Security.Principal; -using System.Threading; -using System.Threading.Tasks; -using SharpHoundCommonLib.Enums; -using SharpHoundCommonLib.OutputTypes; -using SharpHoundRPC.Wrappers; -using Domain = System.DirectoryServices.ActiveDirectory.Domain; - -namespace SharpHoundCommonLib -{ - /// - /// Struct representing options to create an LDAP query - /// - public struct LDAPQueryOptions - { - public string Filter; - public SearchScope Scope; - public string[] Properties; - public CancellationToken CancellationToken; - public string DomainName; - public bool IncludeAcl; - public bool ShowDeleted; - public string AdsPath; - public bool GlobalCatalog; - public bool SkipCache; - public bool ThrowException; - } - - public interface ILDAPUtils - { - void SetLDAPConfig(LDAPConfig config); - bool TestLDAPConfig(string domain); - string[] GetUserGlobalCatalogMatches(string name); - TypedPrincipal ResolveIDAndType(string id, string fallbackDomain); - TypedPrincipal ResolveCertTemplateByProperty(string propValue, string propName, string containerDN, string domainName); - Label LookupSidType(string sid, string domain); - Label LookupGuidType(string guid, string domain); - string GetDomainNameFromSid(string sid); - string GetSidFromDomainName(string domainName); - string ConvertWellKnownPrincipal(string sid, string domain); - bool GetWellKnownPrincipal(string sid, string domain, out TypedPrincipal commonPrincipal); - - bool ConvertLocalWellKnownPrincipal(SecurityIdentifier sid, string computerDomainSid, string computerDomain, - out TypedPrincipal principal); - Domain GetDomain(string domainName = null); - void AddDomainController(string domainControllerSID); - IEnumerable GetWellKnownPrincipalOutput(string domain); - - /// - /// Performs Attribute Ranged Retrieval - /// https://docs.microsoft.com/en-us/windows/win32/adsi/attribute-range-retrieval - /// The function self-determines the range and internally handles the maximum step allowed by the server - /// - /// - /// - /// - IEnumerable DoRangedRetrieval(string distinguishedName, string attributeName); - - /// - /// Takes a host in most applicable forms from AD and attempts to resolve it into a SID. - /// - /// - /// - /// - Task ResolveHostToSid(string hostname, string domain); - - /// - /// Attempts to convert a bare account name (usually from session enumeration) to its corresponding ID and object type - /// - /// - /// - /// - TypedPrincipal ResolveAccountName(string name, string domain); - - /// - /// Attempts to convert a distinguishedname to its corresponding ID and object type. - /// - /// DistinguishedName - /// A TypedPrincipal object with the SID and Label - TypedPrincipal ResolveDistinguishedName(string dn); - - /// - /// Performs an LDAP query using the parameters specified by the user. - /// - /// LDAP query options - /// All LDAP search results matching the specified parameters - IEnumerable QueryLDAP(LDAPQueryOptions options); - - /// - /// Performs an LDAP query using the parameters specified by the user. - /// - /// LDAP filter - /// SearchScope to query - /// LDAP properties to fetch for each object - /// Cancellation Token - /// Include the DACL and Owner values in the NTSecurityDescriptor - /// Include deleted objects - /// Domain to query - /// ADS path to limit the query too - /// Use the global catalog instead of the regular LDAP server - /// - /// Skip the connection cache and force a new connection. You must dispose of this connection - /// yourself. - /// - /// Throw exceptions rather than logging the errors directly - /// All LDAP search results matching the specified parameters - IEnumerable QueryLDAP(string ldapFilter, SearchScope scope, - string[] props, CancellationToken cancellationToken, string domainName = null, bool includeAcl = false, - bool showDeleted = false, string adsPath = null, bool globalCatalog = false, bool skipCache = false, - bool throwException = false); - - /// - /// Performs an LDAP query using the parameters specified by the user. - /// - /// LDAP filter - /// SearchScope to query - /// LDAP properties to fetch for each object - /// Include the DACL and Owner values in the NTSecurityDescriptor - /// Include deleted objects - /// Domain to query - /// ADS path to limit the query too - /// Use the global catalog instead of the regular LDAP server - /// - /// Skip the connection cache and force a new connection. You must dispose of this connection - /// yourself. - /// - /// Throw exceptions rather than logging the errors directly - /// All LDAP search results matching the specified parameters - IEnumerable QueryLDAP(string ldapFilter, SearchScope scope, - string[] props, string domainName = null, bool includeAcl = false, bool showDeleted = false, - string adsPath = null, bool globalCatalog = false, bool skipCache = false, bool throwException = false); - - Forest GetForest(string domainName = null); - string GetConfigurationPath(string domainName); - string GetSchemaPath(string domainName); - - ActiveDirectorySecurityDescriptor MakeSecurityDescriptor(); - string BuildLdapPath(string dnPath, string domain); - bool IsDomainController(string computerObjectId, string domainName); - } -} \ No newline at end of file diff --git a/src/CommonLib/ILdapUtils.cs b/src/CommonLib/ILdapUtils.cs new file mode 100644 index 00000000..9f0bbc42 --- /dev/null +++ b/src/CommonLib/ILdapUtils.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.Security.Principal; +using System.Threading; +using System.Threading.Tasks; +using SharpHoundCommonLib.Enums; +using SharpHoundCommonLib.OutputTypes; + +namespace SharpHoundCommonLib { + public interface ILdapUtils : IDisposable { + /// + /// Performs a non-paged LDAP query. + /// + /// Parameters for the LDAP query + /// Optional cancellation token to support early exit + /// An IEnumerable containing Result objects containing Directory Objects + IAsyncEnumerable> Query(LdapQueryParameters queryParameters, + CancellationToken cancellationToken = new()); + + /// + /// Performs a LDAP query with paging support. + /// + /// Parameters for the LDAP query + /// Optional cancellation token to support early exit + /// An IEnumerable containing Result objects containing Directory Objects + IAsyncEnumerable> PagedQuery(LdapQueryParameters queryParameters, + CancellationToken cancellationToken = new()); + + /// + /// Performs a ranged retrieval operation + /// + /// The base distinguished name to search on + /// The attribute being retrieved + /// A cancellation token for early exit + /// An IEnumerable of result objects containing string results from the query + IAsyncEnumerable> RangedRetrieval(string distinguishedName, + string attributeName, CancellationToken cancellationToken = new()); + + /// + /// Attempts to resolve a SecurityIdentifier to its corresponding TypedPrincipal + /// + /// SecurityIdentifier object to resolve + /// The domain the object belongs too + /// A tuple containing success state as well as the resolved principal if successful + Task<(bool Success, TypedPrincipal Principal)> ResolveIDAndType(SecurityIdentifier securityIdentifier, + string objectDomain); + + /// + /// Attempts to resolve an object identifier to its corresponding TypedPrincipal + /// + /// String identifier for an object, usually a guid or sid + /// The domain the object belongs too + /// A tuple containing success state as well as the resolved principal if successful + Task<(bool Success, TypedPrincipal Principal)> + ResolveIDAndType(string identifier, string objectDomain); + + /// + /// Attempts to resolve a security identifier to its corresponding well known principal + /// + /// + /// + /// A tuple containing success state as well as the resolved principal if successful + Task<(bool Success, TypedPrincipal WellKnownPrincipal)> GetWellKnownPrincipal( + string securityIdentifier, string objectDomain); + + /// + /// Attempts to resolve the domain name for a security identifier. + /// + /// String security identifier for an object + /// A tuple containing success state as well as the resolved domain name if successful + Task<(bool Success, string DomainName)> GetDomainNameFromSid(string sid); + /// + /// Attempts to resolve the sid for a domain given its name + /// + /// The domain name to resolve + /// A tuple containing success state as well as the resolved domain sid if successful + Task<(bool Success, string DomainSid)> GetDomainSidFromDomainName(string domainName); + /// + /// Attempts to retrieve the Domain object for the specified domain + /// + /// The domain name to retrieve the Domain object for + /// The domain object + /// True if the domain was found, false if not + bool GetDomain(string domainName, out System.DirectoryServices.ActiveDirectory.Domain domain); + /// + /// Attempts to retrieve the Domain object for the user's current domain + /// + /// The Domain object + /// True if the domain was found, false if not + bool GetDomain(out System.DirectoryServices.ActiveDirectory.Domain domain); + + Task<(bool Success, string ForestName)> GetForest(string domain); + /// + /// Attempts to resolve an account name to its corresponding typed principal + /// + /// The account name to resolve + /// The domain to resolve the account in + /// A tuple containing success state as well as the resolved TypedPrincipal if successful + Task<(bool Success, TypedPrincipal Principal)> ResolveAccountName(string name, string domain); + /// + /// Attempts to resolve a host to its corresponding security identifier in AD + /// + /// The hostname to resolve. Will accept an IP or a hostname + /// The domain to lookup the account in + /// A tuple containing success state as well as the resolved computer sid if successful + Task<(bool Success, string SecurityIdentifier)> ResolveHostToSid(string host, string domain); + /// + /// Attempts to look up possible matches for a user in the global catalog + /// + /// The name of the account to look up + /// The domain to connect to a global catalog for + /// A tuple containing success state as well as all potential account matches in the global catalog + Task<(bool Success, string[] Sids)> GetGlobalCatalogMatches(string name, string domain); + /// + /// Attempts to resolve a certificate template by a specific property + /// + /// The value of the property being matched + /// The name of the property being matched + /// The domain to lookup the certificate template in + /// A tuple containing success state as well as the resolved certificate template if successful + Task<(bool Success, TypedPrincipal Principal)> ResolveCertTemplateByProperty(string propValue, string propName, string domainName); + /// + /// Makes a new security descriptor object. This is a testing shim + /// + /// An ActiveDirectorySecurityDescriptor object + ActiveDirectorySecurityDescriptor MakeSecurityDescriptor(); + + /// + /// Attempts to convert a local-to-computer well known principal + /// + /// The security identifier to convert + /// The sid of the computer in the domain + /// The domain of the computer + /// A tuple containing success state as well as the resolved principal if successful + Task<(bool Success, TypedPrincipal Principal)> ConvertLocalWellKnownPrincipal(SecurityIdentifier sid, + string computerDomainSid, string computerDomain); + + /// + /// Attempts to determine if a computer sid corresponds to a domain controller + /// + /// The sid of the computer being tested + /// The domain to lookup the computer + /// True if the SID is a domain controller, false if not or if the object is not found + Task IsDomainController(string computerObjectId, string domainName); + /// + /// Attempts to resolve a distinguished name to its corresponding principal + /// + /// The distinguished name to resolve + /// A tuple containing success state as well as the resolved principal sid if successful + Task<(bool Success, TypedPrincipal Principal)> ResolveDistinguishedName(string distinguishedName); + void AddDomainController(string domainControllerSID); + IAsyncEnumerable GetWellKnownPrincipalOutput(); + /// + /// Sets the ldap config for this utils instance. Will dispose if any existing ldap connections when set + /// + /// The new ldap config + void SetLdapConfig(LdapConfig config); + /// + /// Tests if a LDAP connection can be made successfully to a domain + /// + /// The domain to test + /// A tuple containing success state as well as a message if unsuccessful + Task<(bool Success, string Message)> TestLdapConnection(string domain); + /// + /// Attempts to get the distinguished name corresponding to a specific naming context for a domain + /// + /// The domain to get the context for + /// The naming context being retrieved + /// A tuple containing success state as well as the resolved distinguished name if successful + Task<(bool Success, string Path)> GetNamingContextPath(string domain, NamingContext context); + } +} \ No newline at end of file diff --git a/src/CommonLib/Impersonate.cs b/src/CommonLib/Impersonate.cs index fe222c54..f6e03aae 100644 --- a/src/CommonLib/Impersonate.cs +++ b/src/CommonLib/Impersonate.cs @@ -1,14 +1,13 @@ //credit to Phillip Allan-Harding (Twitter @phillipharding) for this library. + using System; using System.ComponentModel; using System.Runtime.InteropServices; using System.Security.Principal; using System.Xml.Linq; -namespace Impersonate -{ - public enum LogonType - { +namespace Impersonate { + public enum LogonType { LOGON32_LOGON_INTERACTIVE = 2, LOGON32_LOGON_NETWORK = 3, LOGON32_LOGON_BATCH = 4, @@ -18,36 +17,33 @@ public enum LogonType LOGON32_LOGON_NEW_CREDENTIALS = 9 // Win2K or higher }; - public enum LogonProvider - { + public enum LogonProvider { LOGON32_PROVIDER_DEFAULT = 0, LOGON32_PROVIDER_WINNT35 = 1, LOGON32_PROVIDER_WINNT40 = 2, LOGON32_PROVIDER_WINNT50 = 3 }; - public enum ImpersonationLevel - { + public enum ImpersonationLevel { SecurityAnonymous = 0, SecurityIdentification = 1, SecurityImpersonation = 2, SecurityDelegation = 3 } - class Win32NativeMethods - { + class Win32NativeMethods { [DllImport("advapi32.dll", SetLastError = true)] public static extern int LogonUser(string lpszUserName, - string lpszDomain, - string lpszPassword, - int dwLogonType, - int dwLogonProvider, - ref IntPtr phToken); + string lpszDomain, + string lpszPassword, + int dwLogonType, + int dwLogonProvider, + ref IntPtr phToken); [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)] public static extern int DuplicateToken(IntPtr hToken, - int impersonationLevel, - ref IntPtr hNewToken); + int impersonationLevel, + ref IntPtr hNewToken); [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)] public static extern bool RevertToSelf(); @@ -84,88 +80,73 @@ public static extern int DuplicateToken(IntPtr hToken, /// /// ... /// - public class Impersonator : IDisposable - { + public class Impersonator : IDisposable { private WindowsImpersonationContext _wic; - - /// - /// Begins impersonation with the given credentials, Logon type and Logon provider. - /// - /// Name of the user. - /// Name of the domain. - /// The password. - ///< param name="logonType">Type of the logon. - /// The logon provider. - public Impersonator(string userName, string domainName, string password, LogonType logonType, LogonProvider logonProvider) - { + + /// + /// Begins impersonation with the given credentials, Logon type and Logon provider. + /// + /// Name of the user. + /// Name of the domain. + /// The password. + ///< param name="logonType">Type of the logon. + /// The logon provider. + public Impersonator(string userName, string domainName, string password, LogonType logonType, + LogonProvider logonProvider) { Impersonate(userName, domainName, password, logonType, logonProvider); } - - /// - /// Begins impersonation with the given credentials. - /// - /// Name of the user. - /// Name of the domain. - /// The password. - public Impersonator(string userName, string domainName, string password) - { - Impersonate(userName, domainName, password, LogonType.LOGON32_LOGON_INTERACTIVE, LogonProvider.LOGON32_PROVIDER_DEFAULT); + + /// + /// Begins impersonation with the given credentials. + /// + /// Name of the user. + /// Name of the domain. + /// The password. + public Impersonator(string userName, string domainName, string password) { + Impersonate(userName, domainName, password, LogonType.LOGON32_LOGON_INTERACTIVE, + LogonProvider.LOGON32_PROVIDER_DEFAULT); } /// /// Initializes a new instance of the class. /// - public Impersonator() - { } + public Impersonator() { + } /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// - public void Dispose() - { + public void Dispose() { UndoImpersonation(); } - - /// - /// Impersonates the specified user account. - /// - /// Name of the user. - /// Name of the domain. - /// The password. - public void Impersonate(string userName, string domainName, string password) - { - Impersonate(userName, domainName, password, LogonType.LOGON32_LOGON_INTERACTIVE, LogonProvider.LOGON32_PROVIDER_DEFAULT); - } - - /// - /// Impersonates the specified user account. - /// - /// Name of the user. - /// Name of the domain. - /// The password. - ///< param name="logonType">Type of the logon. - /// The logon provider. - public void Impersonate(string userName, string domainName, string password, LogonType logonType, LogonProvider logonProvider) - { + + /// + /// Impersonates the specified user account. + /// + /// Name of the user. + /// Name of the domain. + /// The password. + ///< param name="logonType">Type of the logon. + /// The logon provider. + public void Impersonate(string userName, string domainName, string password, LogonType logonType = LogonType.LOGON32_LOGON_INTERACTIVE, + LogonProvider logonProvider = LogonProvider.LOGON32_PROVIDER_DEFAULT) { UndoImpersonation(); IntPtr logonToken = IntPtr.Zero; IntPtr logonTokenDuplicate = IntPtr.Zero; - try - { + try { // revert to the application pool identity, saving the identity of the current requestor _wic = WindowsIdentity.Impersonate(IntPtr.Zero); // do logon & impersonate if (Win32NativeMethods.LogonUser(userName, - domainName, - password, - (int)logonType, - (int)logonProvider, - ref logonToken) != 0) - { - if (Win32NativeMethods.DuplicateToken(logonToken, (int)ImpersonationLevel.SecurityImpersonation, ref logonTokenDuplicate) != 0) - { + domainName, + password, + (int)logonType, + (int)logonProvider, + ref logonToken) != 0) { + if (Win32NativeMethods.DuplicateToken(logonToken, (int)ImpersonationLevel.SecurityImpersonation, + ref logonTokenDuplicate) != 0) { var wi = new WindowsIdentity(logonTokenDuplicate); wi.Impersonate(); // discard the returned identity context (which is the context of the application pool) } @@ -175,8 +156,7 @@ public void Impersonate(string userName, string domainName, string password, Log else throw new Win32Exception(Marshal.GetLastWin32Error()); } - finally - { + finally { if (logonToken != IntPtr.Zero) Win32NativeMethods.CloseHandle(logonToken); @@ -188,12 +168,11 @@ public void Impersonate(string userName, string domainName, string password, Log /// /// Stops impersonation. /// - private void UndoImpersonation() - { + private void UndoImpersonation() { // restore saved requestor identity if (_wic != null) _wic.Undo(); _wic = null; } } -} +} \ No newline at end of file diff --git a/src/CommonLib/LDAPConnectionCacheKey.cs b/src/CommonLib/LDAPConnectionCacheKey.cs deleted file mode 100644 index f1f76464..00000000 --- a/src/CommonLib/LDAPConnectionCacheKey.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace SharpHoundCommonLib -{ - public class LDAPConnectionCacheKey - { - public bool GlobalCatalog { get; } - public string Domain { get; } - public string Server { get; set; } - - public LDAPConnectionCacheKey(string domain, bool globalCatalog) - { - GlobalCatalog = globalCatalog; - Domain = domain; - } - - protected bool Equals(LDAPConnectionCacheKey other) - { - return GlobalCatalog == other.GlobalCatalog && Domain == other.Domain; - } - - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((LDAPConnectionCacheKey)obj); - } - - public override int GetHashCode() - { - unchecked - { - return (GlobalCatalog.GetHashCode() * 397) ^ (Domain != null ? Domain.GetHashCode() : 0); - } - } - } -} \ No newline at end of file diff --git a/src/CommonLib/LDAPQueryParams.cs b/src/CommonLib/LDAPQueryParams.cs deleted file mode 100644 index 42d0587e..00000000 --- a/src/CommonLib/LDAPQueryParams.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.DirectoryServices.Protocols; -using SharpHoundCommonLib.Exceptions; - -namespace SharpHoundCommonLib -{ - internal class LDAPQueryParams - { - public LdapConnection Connection { get; set; } - public SearchRequest SearchRequest { get; set; } - public PageResultRequestControl PageControl { get; set; } - public LDAPQueryException Exception { get; set; } - } -} - diff --git a/src/CommonLib/LDAPUtils.cs b/src/CommonLib/LDAPUtils.cs deleted file mode 100644 index a97b3a1e..00000000 --- a/src/CommonLib/LDAPUtils.cs +++ /dev/null @@ -1,2110 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; -using System.DirectoryServices; -using System.DirectoryServices.ActiveDirectory; -using System.DirectoryServices.Protocols; -using System.Linq; -using System.Net; -using System.Net.Sockets; -using System.Security.Principal; -using System.Text; -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 LDAPUtils : ILDAPUtils - { - private const string NullCacheKey = "UNIQUENULL"; - - // The following byte stream contains the necessary message to request a NetBios name from a machine - // http://web.archive.org/web/20100409111218/http://msdn.microsoft.com/en-us/library/system.net.sockets.socket.aspx - private static readonly byte[] NameRequest = - { - 0x80, 0x94, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x20, 0x43, 0x4b, 0x41, - 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, - 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, - 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, - 0x41, 0x41, 0x41, 0x41, 0x41, 0x00, 0x00, 0x21, - 0x00, 0x01 - }; - - - private static readonly ConcurrentDictionary - SeenWellKnownPrincipals = new(); - - private static readonly ConcurrentDictionary DomainControllers = new(); - private static readonly ConcurrentDictionary CachedDomainInfo = new(StringComparer.OrdinalIgnoreCase); - - private readonly ConcurrentDictionary _domainCache = 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 ConcurrentDictionary _hostResolutionMap = new(); - private readonly ConcurrentDictionary _ldapConnections = new(); - private readonly ConcurrentDictionary _ldapRangeSizeCache = new(); - private readonly ILogger _log; - private readonly NativeMethods _nativeMethods; - private readonly ConcurrentDictionary _netbiosCache = new(); - private readonly PortScanner _portScanner; - private LDAPConfig _ldapConfig = new(); - private readonly ManualResetEvent _connectionResetEvent = new(false); - private readonly object _lockObj = new(); - - - /// - /// Creates a new instance of LDAP Utils with defaults - /// - public LDAPUtils() - { - _nativeMethods = new NativeMethods(); - _portScanner = new PortScanner(); - _log = Logging.LogProvider.CreateLogger("LDAPUtils"); - } - - /// - /// Creates a new instance of LDAP utils and allows overriding implementations - /// - /// - /// - /// - public LDAPUtils(NativeMethods nativeMethods = null, PortScanner scanner = null, ILogger log = null) - { - _nativeMethods = nativeMethods ?? new NativeMethods(); - _portScanner = scanner ?? new PortScanner(); - _log = log ?? Logging.LogProvider.CreateLogger("LDAPUtils"); - } - - /// - /// Sets the configuration for LDAP queries - /// - /// - /// - public void SetLDAPConfig(LDAPConfig config) - { - _ldapConfig = config ?? throw new Exception("LDAP Configuration can not be null"); - //Close out any existing LDAP connections to request a new incoming config - foreach (var kv in _ldapConnections) - { - kv.Value.Connection.Dispose(); - } - - _ldapConnections.Clear(); - } - - /// - /// Turns a sid into a well known principal ID. - /// - /// - /// - /// - /// True if a well known principal was identified, false if not - public bool GetWellKnownPrincipal(string sid, string domain, out TypedPrincipal commonPrincipal) - { - if (!WellKnownPrincipal.GetWellKnownPrincipal(sid, out commonPrincipal)) return false; - var tempDomain = domain ?? GetDomain()?.Name ?? "UNKNOWN"; - commonPrincipal.ObjectIdentifier = ConvertWellKnownPrincipal(sid, tempDomain); - SeenWellKnownPrincipals.TryAdd(commonPrincipal.ObjectIdentifier, new ResolvedWellKnownPrincipal - { - DomainName = domain, - WkpId = sid - }); - return true; - } - - public bool ConvertLocalWellKnownPrincipal(SecurityIdentifier sid, string computerDomainSid, - string computerDomain, out TypedPrincipal principal) - { - if (WellKnownPrincipal.GetWellKnownPrincipal(sid.Value, out var common)) - { - //The everyone and auth users principals are special and will be converted to the domain equivalent - if (sid.Value is "S-1-1-0" or "S-1-5-11") - { - GetWellKnownPrincipal(sid.Value, computerDomain, out principal); - return true; - } - - //Use the computer object id + the RID of the sid we looked up to create our new principal - principal = new TypedPrincipal - { - ObjectIdentifier = $"{computerDomainSid}-{sid.Rid()}", - ObjectType = common.ObjectType switch - { - Label.User => Label.LocalUser, - Label.Group => Label.LocalGroup, - _ => common.ObjectType - } - }; - - return true; - } - - principal = null; - return false; - } - - /// - /// Adds a SID to an internal list of domain controllers - /// - /// - public void AddDomainController(string domainControllerSID) - { - DomainControllers.TryAdd(domainControllerSID, new byte()); - } - - /// - /// Gets output objects for currently observed well known principals - /// - /// - /// - public IEnumerable GetWellKnownPrincipalOutput(string domain) - { - foreach (var wkp in SeenWellKnownPrincipals) - { - WellKnownPrincipal.GetWellKnownPrincipal(wkp.Value.WkpId, out var principal); - OutputBase output = principal.ObjectType switch - { - Label.User => new User(), - Label.Computer => new Computer(), - Label.Group => new Group(), - Label.GPO => new GPO(), - Label.Domain => new OutputTypes.Domain(), - Label.OU => new OU(), - Label.Container => new Container(), - Label.Configuration => new Container(), - _ => throw new ArgumentOutOfRangeException() - }; - - output.Properties.Add("name", $"{principal.ObjectIdentifier}@{wkp.Value.DomainName}".ToUpper()); - var domainSid = GetSidFromDomainName(wkp.Value.DomainName); - output.Properties.Add("domainsid", domainSid); - output.Properties.Add("domain", wkp.Value.DomainName.ToUpper()); - output.ObjectIdentifier = wkp.Key; - yield return output; - } - - var entdc = GetBaseEnterpriseDC(domain); - entdc.Members = DomainControllers.Select(x => new TypedPrincipal(x.Key, Label.Computer)).ToArray(); - yield return entdc; - } - - /// - /// Converts a - /// - /// - /// - /// - public string ConvertWellKnownPrincipal(string sid, string domain) - { - if (!WellKnownPrincipal.GetWellKnownPrincipal(sid, out _)) return sid; - - if (sid != "S-1-5-9") return $"{domain}-{sid}".ToUpper(); - - var forest = GetForest(domain)?.Name; - if (forest == null) _log.LogWarning("Error getting forest, ENTDC sid is likely incorrect"); - return $"{forest ?? "UNKNOWN"}-{sid}".ToUpper(); - } - - /// - /// Queries the global catalog to get potential SID matches for a username in the forest - /// - /// - /// - public string[] GetUserGlobalCatalogMatches(string name) - { - var tempName = name.ToLower(); - if (Cache.GetGCCache(tempName, out var sids)) - return sids; - - var query = new LDAPFilter().AddUsers($"samaccountname={tempName}").GetFilter(); - var results = QueryLDAP(query, SearchScope.Subtree, new[] { "objectsid" }, globalCatalog: true) - .Select(x => x.GetSid()).Where(x => x != null).ToArray(); - Cache.AddGCCache(tempName, results); - return results; - } - - /// - /// Uses an LDAP lookup to attempt to find the Label for a given SID - /// Will also convert to a well known principal ID if needed - /// - /// - /// - /// - public TypedPrincipal ResolveIDAndType(string id, string fallbackDomain) - { - //This is a duplicated SID object which is weird and makes things unhappy. Throw it out - if (id.Contains("0ACNF")) - return null; - - if (GetWellKnownPrincipal(id, fallbackDomain, out var principal)) - return principal; - - var type = id.StartsWith("S-") ? LookupSidType(id, fallbackDomain) : LookupGuidType(id, fallbackDomain); - return new TypedPrincipal(id, type); - } - - public TypedPrincipal ResolveCertTemplateByProperty(string propValue, string propertyName, string containerDN, - string domainName) - { - var filter = new LDAPFilter().AddCertificateTemplates().AddFilter(propertyName + "=" + propValue, true); - var res = QueryLDAP(filter.GetFilter(), SearchScope.OneLevel, - CommonProperties.TypeResolutionProps, adsPath: containerDN, domainName: domainName); - - if (res == null) - { - _log.LogWarning( - "Could not find certificate template with '{propertyName}:{propValue}' under {containerDN}; null result", - propertyName, propValue, containerDN); - return null; - } - - List resList = new List(res); - if (resList.Count == 0) - { - _log.LogWarning( - "Could not find certificate template with '{propertyName}:{propValue}' under {containerDN}; empty list", - propertyName, propValue, containerDN); - return null; - } - - if (resList.Count > 1) - { - _log.LogWarning( - "Found more than one certificate template with '{propertyName}:{propValue}' under {containerDN}", - propertyName, propValue, containerDN); - return null; - } - - ISearchResultEntry searchResultEntry = resList.FirstOrDefault(); - return new TypedPrincipal(searchResultEntry.GetGuid(), Label.CertTemplate); - } - - /// - /// Attempts to lookup the Label for a sid - /// - /// - /// - /// - public Label LookupSidType(string sid, string domain) - { - if (Cache.GetIDType(sid, out var type)) - return type; - - var rDomain = GetDomainNameFromSid(sid) ?? domain; - - var result = - QueryLDAP(CommonFilters.SpecificSID(sid), SearchScope.Subtree, CommonProperties.TypeResolutionProps, - rDomain) - .DefaultIfEmpty(null).FirstOrDefault(); - - type = result?.GetLabel() ?? Label.Base; - Cache.AddType(sid, type); - return type; - } - - /// - /// Attempts to lookup the Label for a GUID - /// - /// - /// - /// - public Label LookupGuidType(string guid, string domain) - { - if (Cache.GetIDType(guid, out var type)) - return type; - - var hex = Helpers.ConvertGuidToHexGuid(guid); - if (hex == null) - return Label.Base; - - var result = - QueryLDAP($"(objectguid={hex})", SearchScope.Subtree, CommonProperties.TypeResolutionProps, domain) - .DefaultIfEmpty(null).FirstOrDefault(); - - type = result?.GetLabel() ?? Label.Base; - Cache.AddType(guid, type); - return type; - } - - /// - /// Attempts to find the domain associated with a SID - /// - /// - /// - public string GetDomainNameFromSid(string sid) - { - try - { - var parsedSid = new SecurityIdentifier(sid); - var domainSid = parsedSid.AccountDomainSid?.Value.ToUpper(); - if (domainSid == null) - return null; - - _log.LogDebug("Resolving sid {DomainSid}", domainSid); - - if (Cache.GetDomainSidMapping(domainSid, out var domain)) - return domain; - - _log.LogDebug("No cache hit for {DomainSid}", domainSid); - domain = GetDomainNameFromSidLdap(domainSid); - _log.LogDebug("Resolved to {Domain}", domain); - - //Cache both to and from so we can use this later - if (domain != null) - { - Cache.AddDomainSidMapping(domainSid, domain); - Cache.AddDomainSidMapping(domain, domainSid); - } - - return domain; - } - catch - { - return null; - } - } - - /// - /// Attempts to get the SID associated with a domain name - /// - /// - /// - public string GetSidFromDomainName(string domainName) - { - var tempDomainName = NormalizeDomainName(domainName); - if (tempDomainName == null) - return null; - if (Cache.GetDomainSidMapping(tempDomainName, out var sid)) return sid; - - var domainObj = GetDomain(tempDomainName); - - if (domainObj != null) - sid = domainObj.GetDirectoryEntry().GetSid(); - else - sid = null; - - if (sid != null) - { - Cache.AddDomainSidMapping(sid, tempDomainName); - Cache.AddDomainSidMapping(tempDomainName, sid); - if (tempDomainName != domainName) - { - Cache.AddDomainSidMapping(domainName, sid); - } - } - - return sid; - } - - // Saving this code for an eventual async implementation - // public async IAsyncEnumerable DoRangedRetrievalAsync(string distinguishedName, string attributeName) - // { - // var domainName = Helpers.DistinguishedNameToDomain(distinguishedName); - // LdapConnection conn; - // try - // { - // conn = await CreateLDAPConnection(domainName, authType: _ldapConfig.AuthType); - // } - // catch - // { - // yield break; - // } - // - // if (conn == null) - // yield break; - // - // var index = 0; - // var step = 0; - // var currentRange = $"{attributeName};range={index}-*"; - // var complete = false; - // - // var searchRequest = CreateSearchRequest($"{attributeName}=*", SearchScope.Base, new[] {currentRange}, - // domainName, distinguishedName); - // - // var backoffDelay = MinBackoffDelay; - // var retryCount = 0; - // - // while (true) - // { - // DirectoryResponse searchResult; - // try - // { - // searchResult = await Task.Factory.FromAsync(conn.BeginSendRequest, conn.EndSendRequest, - // searchRequest, - // PartialResultProcessing.NoPartialResultSupport, null); - // } - // catch (LdapException le) when (le.ErrorCode == 51 && retryCount < MaxRetries) - // { - // //Allow three retries with a backoff on each one if we get a "Server is Busy" error - // retryCount++; - // await Task.Delay(backoffDelay); - // backoffDelay = TimeSpan.FromSeconds(Math.Min( - // backoffDelay.TotalSeconds * BackoffDelayMultiplier.TotalSeconds, MaxBackoffDelay.TotalSeconds)); - // continue; - // } - // catch (Exception e) - // { - // _log.LogWarning(e,"Caught exception during ranged retrieval for {DN}", distinguishedName); - // yield break; - // } - // - // if (searchResult is SearchResponse response && response.Entries.Count == 1) - // { - // var entry = response.Entries[0]; - // var attributeNames = entry?.Attributes?.AttributeNames; - // if (attributeNames != null) - // { - // foreach (string attr in attributeNames) - // { - // //Set our current range to the name of the attribute, which will tell us how far we are in "paging" - // currentRange = attr; - // //Check if the string has the * character in it. If it does, we've reached the end of this search - // complete = currentRange.IndexOf("*", 0, StringComparison.Ordinal) > 0; - // //Set our step to the number of attributes that came back. - // step = entry.Attributes[currentRange].Count; - // } - // } - // - // - // foreach (string val in entry.Attributes[currentRange].GetValues(typeof(string))) - // { - // yield return val; - // index++; - // } - // - // if (complete) yield break; - // - // currentRange = $"{attributeName};range={index}-{index + step}"; - // searchRequest.Attributes.Clear(); - // searchRequest.Attributes.Add(currentRange); - // } - // else - // { - // yield break; - // } - // } - // } - - /// - /// Performs Attribute Ranged Retrieval - /// https://docs.microsoft.com/en-us/windows/win32/adsi/attribute-range-retrieval - /// The function self-determines the range and internally handles the maximum step allowed by the server - /// - /// - /// - /// - public IEnumerable DoRangedRetrieval(string distinguishedName, string attributeName) - { - var domainName = Helpers.DistinguishedNameToDomain(distinguishedName); - var task = Task.Run(() => CreateLDAPConnectionWrapper(domainName, authType: _ldapConfig.AuthType)); - - LdapConnectionWrapper connWrapper; - - try - { - connWrapper = task.ConfigureAwait(false).GetAwaiter().GetResult(); - } - catch - { - yield break; - } - - if (connWrapper.Connection == null) - yield break; - - var conn = connWrapper.Connection; - - var index = 0; - var step = 0; - var baseString = $"{attributeName}"; - //Example search string: member;range=0-1000 - var currentRange = $"{baseString};range={index}-*"; - var complete = false; - - var searchRequest = CreateSearchRequest($"{attributeName}=*", SearchScope.Base, new[] { currentRange }, - connWrapper.DomainInfo, distinguishedName); - - if (searchRequest == null) - yield break; - - var backoffDelay = MinBackoffDelay; - var retryCount = 0; - - while (true) - { - SearchResponse response; - try - { - response = (SearchResponse)conn.SendRequest(searchRequest); - } - catch (LdapException le) when (le.ErrorCode == (int)LdapErrorCodes.Busy && retryCount < MaxRetries) - { - //Allow three retries with a backoff on each one if we get a "Server is Busy" error - retryCount++; - Thread.Sleep(backoffDelay); - backoffDelay = GetNextBackoff(retryCount); - continue; - } - catch (Exception e) - { - _log.LogError(e, "Error doing ranged retrieval for {Attribute} on {Dn}", attributeName, - distinguishedName); - yield break; - } - - //If we ever get more than one response from here, something is horribly wrong - if (response?.Entries.Count == 1) - { - var entry = response.Entries[0]; - //Process the attribute we get back to determine a few things - foreach (string attr in entry.Attributes.AttributeNames) - { - //Set our current range to the name of the attribute, which will tell us how far we are in "paging" - currentRange = attr; - //Check if the string has the * character in it. If it does, we've reached the end of this search - complete = currentRange.IndexOf("*", 0, StringComparison.Ordinal) > 0; - //Set our step to the number of attributes that came back. - step = entry.Attributes[currentRange].Count; - } - - foreach (string val in entry.Attributes[currentRange].GetValues(typeof(string))) - { - yield return val; - index++; - } - - if (complete) yield break; - - currentRange = $"{baseString};range={index}-{index + step}"; - searchRequest.Attributes.Clear(); - searchRequest.Attributes.Add(currentRange); - } - else - { - //Something went wrong here. - yield break; - } - } - } - - /// - /// Takes a host in most applicable forms from AD and attempts to resolve it into a SID. - /// - /// - /// - /// - public async Task ResolveHostToSid(string hostname, string domain) - { - var strippedHost = Helpers.StripServicePrincipalName(hostname).ToUpper().TrimEnd('$'); - if (string.IsNullOrEmpty(strippedHost)) - { - return null; - } - - if (_hostResolutionMap.TryGetValue(strippedHost, out var sid)) return sid; - - var normalDomain = NormalizeDomainName(domain); - - string tempName; - string tempDomain = null; - - //Step 1: Handle non-IP address values - if (!IPAddress.TryParse(strippedHost, out _)) - { - // Format: ABC.TESTLAB.LOCAL - if (strippedHost.Contains(".")) - { - var split = strippedHost.Split('.'); - tempName = split[0]; - tempDomain = string.Join(".", split.Skip(1).ToArray()); - } - // Format: WINDOWS - else - { - tempName = strippedHost; - tempDomain = normalDomain; - } - - // Add $ to the end of the name to match how computers are stored in AD - tempName = $"{tempName}$".ToUpper(); - var principal = ResolveAccountName(tempName, tempDomain); - sid = principal?.ObjectIdentifier; - if (sid != null) - { - _hostResolutionMap.TryAdd(strippedHost, sid); - return sid; - } - } - - //Step 2: Try NetWkstaGetInfo - //Next we'll try calling NetWkstaGetInfo in hopes of getting the NETBIOS name directly from the computer - //We'll use the hostname that we started with instead of the one from our previous step - var workstationInfo = await GetWorkstationInfo(strippedHost); - if (workstationInfo.HasValue) - { - tempName = workstationInfo.Value.ComputerName; - tempDomain = workstationInfo.Value.LanGroup; - - if (string.IsNullOrEmpty(tempDomain)) - tempDomain = normalDomain; - - if (!string.IsNullOrEmpty(tempName)) - { - //Append the $ to indicate this is a computer - tempName = $"{tempName}$".ToUpper(); - var principal = ResolveAccountName(tempName, tempDomain); - sid = principal?.ObjectIdentifier; - if (sid != null) - { - _hostResolutionMap.TryAdd(strippedHost, sid); - return sid; - } - } - } - - //Step 3: Socket magic - // Attempt to request the NETBIOS name of the computer directly - if (RequestNETBIOSNameFromComputer(strippedHost, normalDomain, out tempName)) - { - tempDomain ??= normalDomain; - tempName = $"{tempName}$".ToUpper(); - - var principal = ResolveAccountName(tempName, tempDomain); - sid = principal?.ObjectIdentifier; - if (sid != null) - { - _hostResolutionMap.TryAdd(strippedHost, sid); - return sid; - } - } - - //Try DNS resolution next - string resolvedHostname; - try - { - resolvedHostname = (await Dns.GetHostEntryAsync(strippedHost)).HostName; - } - catch - { - resolvedHostname = null; - } - - if (resolvedHostname != null) - { - var splitName = resolvedHostname.Split('.'); - tempName = $"{splitName[0]}$".ToUpper(); - tempDomain = string.Join(".", splitName.Skip(1)); - - var principal = ResolveAccountName(tempName, tempDomain); - sid = principal?.ObjectIdentifier; - if (sid != null) - { - _hostResolutionMap.TryAdd(strippedHost, sid); - return sid; - } - } - - //If we get here, everything has failed, and life is very sad. - tempName = strippedHost; - tempDomain = normalDomain; - - if (tempName.Contains(".")) - { - _hostResolutionMap.TryAdd(strippedHost, tempName); - return tempName; - } - - tempName = $"{tempName}.{tempDomain}"; - _hostResolutionMap.TryAdd(strippedHost, tempName); - return tempName; - } - - /// - /// Attempts to convert a bare account name (usually from session enumeration) to its corresponding ID and object type - /// - /// - /// - /// - public TypedPrincipal ResolveAccountName(string name, string domain) - { - if (string.IsNullOrWhiteSpace(name)) - return null; - - if (Cache.GetPrefixedValue(name, domain, out var id) && Cache.GetIDType(id, out var type)) - return new TypedPrincipal - { - ObjectIdentifier = id, - ObjectType = type - }; - - var d = NormalizeDomainName(domain); - var result = QueryLDAP($"(samaccountname={name})", SearchScope.Subtree, - CommonProperties.TypeResolutionProps, - d).DefaultIfEmpty(null).FirstOrDefault(); - - if (result == null) - { - _log.LogDebug("ResolveAccountName - unable to get result for {Name}", name); - return null; - } - - type = result.GetLabel(); - id = result.GetObjectIdentifier(); - - if (id == null) - { - _log.LogDebug("ResolveAccountName - could not retrieve ID on {DN} for {Name}", result.DistinguishedName, - name); - return null; - } - - Cache.AddPrefixedValue(name, domain, id); - Cache.AddType(id, type); - - id = ConvertWellKnownPrincipal(id, domain); - - return new TypedPrincipal - { - ObjectIdentifier = id, - ObjectType = type - }; - } - - /// - /// Attempts to convert a distinguishedname to its corresponding ID and object type. - /// - /// DistinguishedName - /// A TypedPrincipal object with the SID and Label - public TypedPrincipal ResolveDistinguishedName(string dn) - { - if (Cache.GetConvertedValue(dn, out var id) && Cache.GetIDType(id, out var type)) - return new TypedPrincipal - { - ObjectIdentifier = id, - ObjectType = type - }; - - var domain = Helpers.DistinguishedNameToDomain(dn); - var result = QueryLDAP("(objectclass=*)", SearchScope.Base, CommonProperties.TypeResolutionProps, domain, - adsPath: dn) - .DefaultIfEmpty(null).FirstOrDefault(); - - if (result == null) - { - _log.LogDebug("ResolveDistinguishedName - No result for {DN}", dn); - return null; - } - - id = result.GetObjectIdentifier(); - if (id == null) - { - _log.LogDebug("ResolveDistinguishedName - could not retrieve object identifier from {DN}", dn); - return null; - } - - if (GetWellKnownPrincipal(id, domain, out var principal)) return principal; - - type = result.GetLabel(); - - Cache.AddConvertedValue(dn, id); - Cache.AddType(id, type); - - id = ConvertWellKnownPrincipal(id, domain); - - return new TypedPrincipal - { - ObjectIdentifier = id, - ObjectType = type - }; - } - - /// - /// Queries LDAP using LDAPQueryOptions - /// - /// - /// - public IEnumerable QueryLDAP(LDAPQueryOptions options) - { - return QueryLDAP( - options.Filter, - options.Scope, - options.Properties, - options.CancellationToken, - options.DomainName, - options.IncludeAcl, - options.ShowDeleted, - options.AdsPath, - options.GlobalCatalog, - options.SkipCache, - options.ThrowException - ); - } - - /// - /// Performs an LDAP query using the parameters specified by the user. - /// - /// LDAP filter - /// SearchScope to query - /// LDAP properties to fetch for each object - /// Cancellation Token - /// Include the DACL and Owner values in the NTSecurityDescriptor - /// Include deleted objects - /// Domain to query - /// ADS path to limit the query too - /// Use the global catalog instead of the regular LDAP server - /// - /// Skip the connection cache and force a new connection. You must dispose of this connection - /// yourself. - /// - /// Throw exceptions rather than logging the errors directly - /// All LDAP search results matching the specified parameters - /// - /// Thrown when an error occurs during LDAP query (only when throwException = true) - /// - public IEnumerable QueryLDAP(string ldapFilter, SearchScope scope, - string[] props, CancellationToken cancellationToken, string domainName = null, bool includeAcl = false, - bool showDeleted = false, string adsPath = null, bool globalCatalog = false, bool skipCache = false, - bool throwException = false) - { - var queryParams = SetupLDAPQueryFilter( - ldapFilter, scope, props, includeAcl, domainName, includeAcl, adsPath, globalCatalog, skipCache); - - if (queryParams.Exception != null) - { - _log.LogWarning("Failed to setup LDAP Query Filter: {Message}", queryParams.Exception.Message); - if (throwException) - throw new LDAPQueryException("Failed to setup LDAP Query Filter", queryParams.Exception); - yield break; - } - - var conn = queryParams.Connection; - var request = queryParams.SearchRequest; - var pageControl = queryParams.PageControl; - - PageResultResponseControl pageResponse = null; - var backoffDelay = MinBackoffDelay; - var retryCount = 0; - while (true) - { - if (cancellationToken.IsCancellationRequested) - yield break; - - SearchResponse response; - try - { - _log.LogTrace("Sending LDAP request for {Filter}", ldapFilter); - response = (SearchResponse)conn.SendRequest(request); - if (response != null) - pageResponse = (PageResultResponseControl)response.Controls - .Where(x => x is PageResultResponseControl).DefaultIfEmpty(null).FirstOrDefault(); - } - catch (LdapException le) when (le.ErrorCode == (int)LdapErrorCodes.ServerDown && - retryCount < MaxRetries) - { - /*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.*/ - - //Always increment retry count - retryCount++; - - //Attempt to acquire a lock - if (Monitor.TryEnter(_lockObj)) - { - //If we've acquired the lock, we want to immediately signal our reset event so everyone else waits - _connectionResetEvent.Reset(); - try - { - //Sleep for our backoff - Thread.Sleep(backoffDelay); - //Explicitly skip the cache so we don't get the same connection back - conn = CreateNewConnection(domainName, globalCatalog, true).Connection; - if (conn == null) - { - _log.LogError( - "Unable to create replacement ldap connection for ServerDown exception. Breaking loop"); - yield break; - } - - _log.LogInformation("Created new LDAP connection after receiving ServerDown from server"); - } - finally - { - //Reset our event + release the lock - _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(); - conn = CreateNewConnection(domainName, globalCatalog).Connection; - } - - backoffDelay = GetNextBackoff(retryCount); - continue; - } - catch (LdapException le) when (le.ErrorCode == (int)LdapErrorCodes.Busy && retryCount < MaxRetries) - { - retryCount++; - backoffDelay = GetNextBackoff(retryCount); - continue; - } - catch (LdapException le) - { - if (le.ErrorCode != (int)LdapErrorCodes.LocalError) - { - if (throwException) - { - throw new LDAPQueryException( - $"LDAP Exception in Loop: {le.ErrorCode}. {le.ServerErrorMessage}. {le.Message}. Filter: {ldapFilter}. Domain: {domainName}", - le); - } - - _log.LogWarning(le, - "LDAP Exception in Loop: {ErrorCode}. {ServerErrorMessage}. {Message}. Filter: {Filter}. Domain: {Domain}", - le.ErrorCode, le.ServerErrorMessage, le.Message, ldapFilter, domainName); - } - - yield break; - } - catch (Exception e) - { - _log.LogWarning(e, "Exception in LDAP loop for {Filter} and {Domain}", ldapFilter, domainName); - if (throwException) - throw new LDAPQueryException($"Exception in LDAP loop for {ldapFilter} and {domainName}", e); - - yield break; - } - - if (cancellationToken.IsCancellationRequested) - yield break; - - if (response == null || pageResponse == null) - continue; - - foreach (SearchResultEntry entry in response.Entries) - { - if (cancellationToken.IsCancellationRequested) - yield break; - - yield return new SearchResultEntryWrapper(entry, this); - } - - if (pageResponse.Cookie.Length == 0 || response.Entries.Count == 0 || - cancellationToken.IsCancellationRequested) - yield break; - - pageControl.Cookie = pageResponse.Cookie; - } - } - - private LdapConnectionWrapper CreateNewConnection(string domainName = null, bool globalCatalog = false, - bool skipCache = false) - { - var task = Task.Run(() => CreateLDAPConnectionWrapper(domainName, skipCache, _ldapConfig.AuthType, globalCatalog)); - - try - { - return task.ConfigureAwait(false).GetAwaiter().GetResult(); - } - catch - { - return null; - } - } - - /// - /// Performs an LDAP query using the parameters specified by the user. - /// - /// LDAP filter - /// SearchScope to query - /// LDAP properties to fetch for each object - /// Include the DACL and Owner values in the NTSecurityDescriptor - /// Include deleted objects - /// Domain to query - /// ADS path to limit the query too - /// Use the global catalog instead of the regular LDAP server - /// - /// Skip the connection cache and force a new connection. You must dispose of this connection - /// yourself. - /// - /// Throw exceptions rather than logging the errors directly - /// All LDAP search results matching the specified parameters - /// - /// Thrown when an error occurs during LDAP query (only when throwException = true) - /// - public virtual IEnumerable QueryLDAP(string ldapFilter, SearchScope scope, - string[] props, string domainName = null, bool includeAcl = false, bool showDeleted = false, - string adsPath = null, bool globalCatalog = false, bool skipCache = false, bool throwException = false) - { - return QueryLDAP(ldapFilter, scope, props, new CancellationToken(), domainName, includeAcl, showDeleted, - adsPath, globalCatalog, skipCache, throwException); - } - - private static TimeSpan GetNextBackoff(int retryCount) - { - return TimeSpan.FromSeconds(Math.Min( - MinBackoffDelay.TotalSeconds * Math.Pow(BackoffDelayMultiplier, retryCount), - MaxBackoffDelay.TotalSeconds)); - } - - /// - /// Gets the forest associated with a domain. - /// If no domain is provided, defaults to current domain - /// - /// - /// - public virtual Forest GetForest(string domainName = null) - { - try - { - if (domainName == null && _ldapConfig.Username == null) - return Forest.GetCurrentForest(); - - var domain = GetDomain(domainName); - return domain?.Forest; - } - catch - { - return null; - } - } - - /// - /// Creates a new ActiveDirectorySecurityDescriptor - /// Function created for testing purposes - /// - /// - public ActiveDirectorySecurityDescriptor MakeSecurityDescriptor() - { - return new ActiveDirectorySecurityDescriptor(new ActiveDirectorySecurity()); - } - - public string BuildLdapPath(string dnPath, string domainName) - { - //Check our cached info for a fast check - if (CachedDomainInfo.TryGetValue(domainName, out var info)) - { - return $"{dnPath},{info.DomainSearchBase}"; - } - var domain = GetDomain(domainName)?.Name; - if (domain == null) - return null; - - var adPath = $"{dnPath},DC={domain.Replace(".", ",DC=")}"; - return adPath; - } - - /// - /// Tests the current LDAP config to ensure its valid by pulling a domain object - /// - /// True if connection was successful, else false - public bool TestLDAPConfig(string domain) - { - var filter = new LDAPFilter(); - filter.AddDomains(); - - _log.LogTrace("Testing LDAP connection for domain {Domain}", domain); - var result = QueryLDAP(filter.GetFilter(), SearchScope.Subtree, CommonProperties.ObjectID, domain, - throwException: true) - .DefaultIfEmpty(null).FirstOrDefault(); - _log.LogTrace("Result object from LDAP connection test is {DN}", result?.DistinguishedName ?? "null"); - return result != null; - } - - /// - /// Gets the domain object associated with the specified domain name. - /// Defaults to current domain if none specified - /// - /// - /// - public virtual Domain GetDomain(string domainName = null) - { - var cacheKey = domainName ?? NullCacheKey; - if (_domainCache.TryGetValue(cacheKey, out var domain)) return domain; - - 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); - } - catch (Exception e) - { - _log.LogDebug(e, "GetDomain call failed at {StackTrace}", new StackFrame()); - domain = null; - } - - _domainCache.TryAdd(cacheKey, domain); - return domain; - } - - /// - /// Setup LDAP query for filter - /// - /// LDAP filter - /// SearchScope to query - /// LDAP properties to fetch for each object - /// Include the DACL and Owner values in the NTSecurityDescriptor - /// Domain to query - /// Include deleted objects - /// ADS path to limit the query too - /// Use the global catalog instead of the regular LDAP server - /// - /// Skip the connection cache and force a new connection. You must dispose of this connection - /// yourself. - /// - /// Tuple of LdapConnection, SearchRequest, PageResultRequestControl and LDAPQueryException - // ReSharper disable once MemberCanBePrivate.Global - internal LDAPQueryParams SetupLDAPQueryFilter( - string ldapFilter, - SearchScope scope, string[] props, bool includeAcl = false, string domainName = null, - bool showDeleted = false, - string adsPath = null, bool globalCatalog = false, bool skipCache = false) - { - _log.LogTrace("Creating ldap connection for {Target} with filter {Filter}", - globalCatalog ? "Global Catalog" : "DC", ldapFilter); - var task = Task.Run(() => CreateLDAPConnectionWrapper(domainName, skipCache, _ldapConfig.AuthType, globalCatalog)); - - var queryParams = new LDAPQueryParams(); - - LdapConnectionWrapper connWrapper; - try - { - connWrapper = task.ConfigureAwait(false).GetAwaiter().GetResult(); - } - catch (NoLdapDataException) - { - var errorString = - $"Successfully connected via LDAP to {domainName ?? "Default Domain"} but no data received. This is most likely due to permissions or using kerberos authentication across trusts."; - queryParams.Exception = new LDAPQueryException(errorString, null); - return queryParams; - } - catch (LdapAuthenticationException e) - { - var errorString = - $"Failed to connect via LDAP to {domainName ?? "Default Domain"}: Authentication is invalid"; - queryParams.Exception = new LDAPQueryException(errorString, e.InnerException); - return queryParams; - } - catch (LdapConnectionException e) - { - var errorString = - $"Failed to connect via LDAP to {domainName ?? "Default Domain"}: {e.InnerException.Message} (Code: {e.ErrorCode}"; - queryParams.Exception = new LDAPQueryException(errorString, e.InnerException); - return queryParams; - } - - var conn = connWrapper.Connection; - - //If we get a null connection, something went wrong, but we don't have an error to go with it for whatever reason - if (conn == null) - { - var errorString = - $"LDAP connection is null for filter {ldapFilter} and domain {domainName ?? "Default Domain"}"; - queryParams.Exception = new LDAPQueryException(errorString); - return queryParams; - } - - SearchRequest request; - - try - { - request = CreateSearchRequest(ldapFilter, scope, props, connWrapper.DomainInfo, adsPath, showDeleted); - } - catch (LDAPQueryException ldapQueryException) - { - queryParams.Exception = ldapQueryException; - return queryParams; - } - - if (request == null) - { - var errorString = - $"Search request is null for filter {ldapFilter} and domain {domainName ?? "Default Domain"}"; - queryParams.Exception = new LDAPQueryException(errorString); - return queryParams; - } - - var pageControl = new PageResultRequestControl(500); - request.Controls.Add(pageControl); - - if (includeAcl) - request.Controls.Add(new SecurityDescriptorFlagControl - { - SecurityMasks = SecurityMasks.Dacl | SecurityMasks.Owner - }); - - queryParams.Connection = conn; - queryParams.SearchRequest = request; - queryParams.PageControl = pageControl; - - return queryParams; - } - - private Group GetBaseEnterpriseDC(string domain) - { - var forest = GetForest(domain)?.Name; - if (forest == null) _log.LogWarning("Error getting forest, ENTDC sid is likely incorrect"); - var g = new Group { ObjectIdentifier = $"{forest}-S-1-5-9".ToUpper() }; - g.Properties.Add("name", $"ENTERPRISE DOMAIN CONTROLLERS@{forest ?? "UNKNOWN"}".ToUpper()); - g.Properties.Add("domainsid", GetSidFromDomainName(forest)); - g.Properties.Add("domain", forest); - return g; - } - - /// - /// Updates the config for querying LDAP - /// - /// - public void UpdateLDAPConfig(LDAPConfig config) - { - _ldapConfig = config; - } - - private string GetDomainNameFromSidLdap(string sid) - { - var hexSid = Helpers.ConvertSidToHexSid(sid); - - if (hexSid == null) - return null; - - //Search using objectsid first - var result = - QueryLDAP($"(&(objectclass=domain)(objectsid={hexSid}))", SearchScope.Subtree, - new[] { "distinguishedname" }, globalCatalog: true).DefaultIfEmpty(null).FirstOrDefault(); - - if (result != null) - { - var domainName = Helpers.DistinguishedNameToDomain(result.DistinguishedName); - return domainName; - } - - //Try trusteddomain objects with the securityidentifier attribute - result = - QueryLDAP($"(&(objectclass=trusteddomain)(securityidentifier={sid}))", SearchScope.Subtree, - new[] { "cn" }, globalCatalog: true).DefaultIfEmpty(null).FirstOrDefault(); - - if (result != null) - { - var domainName = result.GetProperty(LDAPProperties.CanonicalName); - return domainName; - } - - //We didn't find anything so just return null - return null; - } - - /// - /// Uses a socket and a set of bytes to request the NETBIOS name from a remote computer - /// - /// - /// - /// - /// - private static bool RequestNETBIOSNameFromComputer(string server, string domain, out string netbios) - { - var receiveBuffer = new byte[1024]; - var requestSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); - try - { - //Set receive timeout to 1 second - requestSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, 1000); - EndPoint remoteEndpoint; - - //We need to create an endpoint to bind too. If its an IP, just use that. - if (IPAddress.TryParse(server, out var parsedAddress)) - remoteEndpoint = new IPEndPoint(parsedAddress, 137); - else - //If its not an IP, we're going to try and resolve it from DNS - try - { - IPAddress address; - if (server.Contains(".")) - address = Dns - .GetHostAddresses(server).First(x => x.AddressFamily == AddressFamily.InterNetwork); - else - address = Dns.GetHostAddresses($"{server}.{domain}")[0]; - - if (address == null) - { - netbios = null; - return false; - } - - remoteEndpoint = new IPEndPoint(address, 137); - } - catch - { - //Failed to resolve an IP, so return null - netbios = null; - return false; - } - - var originEndpoint = new IPEndPoint(IPAddress.Any, 0); - requestSocket.Bind(originEndpoint); - - try - { - requestSocket.SendTo(NameRequest, remoteEndpoint); - var receivedByteCount = requestSocket.ReceiveFrom(receiveBuffer, ref remoteEndpoint); - if (receivedByteCount >= 90) - { - netbios = new ASCIIEncoding().GetString(receiveBuffer, 57, 16).Trim('\0', ' '); - return true; - } - - netbios = null; - return false; - } - catch (SocketException) - { - netbios = null; - return false; - } - } - finally - { - //Make sure we close the socket if its open - requestSocket.Close(); - } - } - - /// - /// Calls the NetWkstaGetInfo API on a hostname - /// - /// - /// - private async Task GetWorkstationInfo(string hostname) - { - if (!await _portScanner.CheckPort(hostname)) - return null; - - var result = NetAPIMethods.NetWkstaGetInfo(hostname); - if (result.IsSuccess) return result.Value; - - return null; - } - - /// - /// Creates a SearchRequest object for use in querying LDAP. - /// - /// LDAP filter - /// SearchScope to query - /// LDAP properties to fetch for each object - /// Domain info object which is created alongside the LDAP connection - /// ADS path to limit the query too - /// Include deleted objects in results - /// A built SearchRequest - private SearchRequest CreateSearchRequest(string filter, SearchScope scope, string[] attributes, - DomainInfo domainInfo, string adsPath = null, bool showDeleted = false) - { - var adPath = adsPath?.Replace("LDAP://", "") ?? domainInfo.DomainSearchBase; - - var request = new SearchRequest(adPath, filter, scope, attributes); - request.Controls.Add(new SearchOptionsControl(SearchOption.DomainScope)); - if (showDeleted) - request.Controls.Add(new ShowDeletedControl()); - - return request; - } - - private LdapConnection CreateConnectionHelper(string directoryIdentifier, bool ssl, AuthType authType, 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) }; - SetupLdapConnection(connection, true, authType); - return connection; - } - - private static void CheckAndThrowException(LdapException ldapException) - { - //A null error code with success false indicates that we successfully created a connection but got no data back, this is generally because our AuthType isn't compatible. - //AuthType Kerberos will only work across trusts in very specific scenarios. Alternatively, we don't have read rights. - //Throw this exception for clients to handle - if (ldapException.ErrorCode is (int)LdapErrorCodes.KerberosAuthType or (int)ResultCode.InsufficientAccessRights) - { - throw new NoLdapDataException(ldapException.ErrorCode); - } - - //We shouldn't ever hit this in theory, but we'll error out if its the case - if (ldapException.ErrorCode is (int)ResultCode.InappropriateAuthentication) - { - throw new LdapAuthenticationException(ldapException); - } - - //Any other error we dont have specific ways to handle - if (ldapException.ErrorCode != (int)ResultCode.Unavailable && ldapException.ErrorCode != (int)ResultCode.Busy) - { - throw new LdapConnectionException(ldapException); - } - } - - private string ResolveDomainToFullName(string domain) - { - if (string.IsNullOrEmpty(domain)) - { - return GetDomain()?.Name.ToUpper().Trim(); - } - - if (CachedDomainInfo.TryGetValue(domain.ToUpper(), out var info)) - { - return info.DomainFQDN; - } - - return GetDomain(domain)?.Name.ToUpper().Trim(); - } - - /// - /// Creates an LDAP connection with appropriate options based off the ldap configuration. Caches connections - /// - /// The domain to connect too - /// Skip the connection cache - /// Auth type to use. Defaults to Kerberos. Use Negotiate for netonly/cross trust(forest) scenarios - /// Use global catalog or not - /// A connected LDAP connection or null - - private async Task CreateLDAPConnectionWrapper(string domainName = null, bool skipCache = false, - AuthType authType = AuthType.Kerberos, bool globalCatalog = false) - { - // Step 1: If domain passed in is non-null, skip this step - // - Call GetDomain with a null domain to get the user's current domain - // Step 2: Take domain passed in to the function or resolved from step 1 - // - Try an ldap connection on SSL - // - If ServerUnavailable - Try an ldap connection on non-SSL - // Step 3: Pass the domain to GetDomain to resolve to a better name (potentially) - // - If we get a better name, repeat step 2 with the new name - // Step 4: - // - Use GetDomain to get a domain object along with a list of domain controllers - // - Try the primary domain controller on both ssl/non-ssl - // - Loop over domain controllers and try each on ssl/non-ssl - - //If a server has been manually specified, we should never get past this block for opsec reasons - if (_ldapConfig.Server != null) - { - if (!skipCache) - { - if (GetCachedConnection(_ldapConfig.Server, globalCatalog, out var conn)) - { - return conn; - } - } - - var singleServerConn = CreateLDAPConnection(_ldapConfig.Server, authType, globalCatalog); - if (singleServerConn == null) return new LdapConnectionWrapper() - { - Connection = null, - DomainInfo = null - }; - var cacheKey = new LDAPConnectionCacheKey(_ldapConfig.Server, globalCatalog); - _ldapConnections.AddOrUpdate(cacheKey, singleServerConn, (_, ldapConnection) => - { - ldapConnection.Connection.Dispose(); - return singleServerConn; - }); - return singleServerConn; - } - - //Take the incoming domain name and Upper/Trim it. If the name is null, we'll have to use GetDomain to figure out the user's domain context - var domain = domainName?.ToUpper().Trim() ?? ResolveDomainToFullName(domainName); - - //If our domain is STILL null, we're not going to get anything reliable, so exit out - if (domain == null) - { - return new LdapConnectionWrapper - { - Connection = null, - DomainInfo = null - }; - } - - if (!skipCache) - { - if (GetCachedConnection(domain, globalCatalog, out var conn)) - { - return conn; - } - } - - var connectionWrapper = CreateLDAPConnection(domain, authType, globalCatalog); - //If our connection isn't null, it means we have a good connection - if (connectionWrapper != null) - { - var cacheKey = new LDAPConnectionCacheKey(domain, globalCatalog); - _ldapConnections.AddOrUpdate(cacheKey, connectionWrapper, (_, ldapConnection) => - { - ldapConnection.Connection.Dispose(); - return connectionWrapper; - }); - return connectionWrapper; - } - - //If our incoming domain name wasn't null, try to re-resolve the name for a better potential match and then retry - if (domainName != null) - { - var newDomain = ResolveDomainToFullName(domainName); - if (!string.IsNullOrEmpty(newDomain) && !newDomain.Equals(domain, StringComparison.OrdinalIgnoreCase)) - { - //Set our domain name to the newly resolved value for future steps - domain = newDomain; - if (!skipCache) - { - //Check our cache again, maybe the new name works - if (GetCachedConnection(domain, globalCatalog, out var conn)) - { - return conn; - } - } - - connectionWrapper = CreateLDAPConnection(domain, authType, globalCatalog); - //If our connection isn't null, it means we have a good connection - if (connectionWrapper != null) - { - var cacheKey = new LDAPConnectionCacheKey(domain, globalCatalog); - _ldapConnections.AddOrUpdate(cacheKey, connectionWrapper, (_, ldapConnection) => - { - ldapConnection.Connection.Dispose(); - return connectionWrapper; - }); - return connectionWrapper; - } - } - } - - //Next step, look for domain controllers - var domainObj = GetDomain(domain); - if (domainObj?.Name == null) - { - return null; - } - - //Start with the PDC of the domain and see if we can connect - var pdc = domainObj.PdcRoleOwner.Name; - connectionWrapper = await CreateLDAPConnectionWithPortCheck(pdc, authType, globalCatalog); - if (connectionWrapper != null) - { - var cacheKey = new LDAPConnectionCacheKey(domain, globalCatalog); - _ldapConnections.AddOrUpdate(cacheKey, connectionWrapper, (_, ldapConnection) => - { - ldapConnection.Connection.Dispose(); - return connectionWrapper; - }); - return connectionWrapper; - } - - //Loop over all other domain controllers and see if we can make a good connection to any - foreach (DomainController dc in domainObj.DomainControllers) - { - connectionWrapper = await CreateLDAPConnectionWithPortCheck(dc.Name, authType, globalCatalog); - if (connectionWrapper != null) - { - var cacheKey = new LDAPConnectionCacheKey(domain, globalCatalog); - _ldapConnections.AddOrUpdate(cacheKey, connectionWrapper, (_, ldapConnection) => - { - ldapConnection.Connection.Dispose(); - return connectionWrapper; - }); - return connectionWrapper; - } - } - - return new LdapConnectionWrapper() - { - Connection = null, - DomainInfo = null - }; - } - - private bool GetCachedConnection(string domain, bool globalCatalog, out LdapConnectionWrapper connectionWrapper) - { - var domainName = domain; - if (CachedDomainInfo.TryGetValue(domain.ToUpper(), out var resolved)) - { - domainName = resolved.DomainFQDN; - } - var key = new LDAPConnectionCacheKey(domainName, globalCatalog); - return _ldapConnections.TryGetValue(key, out connectionWrapper); - } - - private async Task CreateLDAPConnectionWithPortCheck(string target, AuthType authType, bool globalCatalog) - { - if (globalCatalog) - { - if (await _portScanner.CheckPort(target, _ldapConfig.GetGCPort(true)) || (!_ldapConfig.ForceSSL && - await _portScanner.CheckPort(target, _ldapConfig.GetGCPort(false)))) - { - return CreateLDAPConnection(target, authType, true); - - } - } - else - { - if (await _portScanner.CheckPort(target, _ldapConfig.GetPort(true)) || (!_ldapConfig.ForceSSL && await _portScanner.CheckPort(target, _ldapConfig.GetPort(false)))) - { - return CreateLDAPConnection(target, authType, false); - } - } - - return null; - } - - - private LdapConnectionWrapper CreateLDAPConnection(string target, AuthType authType, bool globalCatalog) - { - //Lets build a new connection - //Always try SSL first - var connection = CreateConnectionHelper(target, true, authType, globalCatalog); - var connectionResult = TestConnection(connection); - DomainInfo info; - - if (connectionResult.Success) - { - var domain = connectionResult.DomainInfo.DomainFQDN; - if (!CachedDomainInfo.ContainsKey(domain)) - { - var baseDomainInfo = connectionResult.DomainInfo; - baseDomainInfo.DomainSID = GetDomainSidFromConnection(connection, baseDomainInfo); - baseDomainInfo.DomainNetbiosName = GetDomainNetbiosName(connection, baseDomainInfo); - _log.LogInformation("Got info for domain: {info}", baseDomainInfo); - CachedDomainInfo.TryAdd(baseDomainInfo.DomainFQDN, baseDomainInfo); - CachedDomainInfo.TryAdd(baseDomainInfo.DomainNetbiosName, baseDomainInfo); - CachedDomainInfo.TryAdd(baseDomainInfo.DomainSID, baseDomainInfo); - if (!string.IsNullOrEmpty(baseDomainInfo.DomainSID)) - { - Cache.AddDomainSidMapping(baseDomainInfo.DomainFQDN, baseDomainInfo.DomainSID); - Cache.AddDomainSidMapping(baseDomainInfo.DomainSID, baseDomainInfo.DomainFQDN); - if (!string.IsNullOrEmpty(baseDomainInfo.DomainNetbiosName)) - { - Cache.AddDomainSidMapping(baseDomainInfo.DomainNetbiosName, baseDomainInfo.DomainSID); - } - } - - if (!string.IsNullOrEmpty(baseDomainInfo.DomainNetbiosName)) - { - _netbiosCache.TryAdd(baseDomainInfo.DomainFQDN, baseDomainInfo.DomainNetbiosName); - } - - info = baseDomainInfo; - } - else - { - CachedDomainInfo.TryGetValue(domain, out info); - } - return new LdapConnectionWrapper - { - Connection = connection, - DomainInfo = info - }; - } - - CheckAndThrowException(connectionResult.Exception); - - //If we're not allowing fallbacks to LDAP from LDAPS, just return here - if (_ldapConfig.ForceSSL) - { - return null; - } - //If we get to this point, it means we have an unsuccessful connection, but our error code doesn't indicate an outright failure - //Try a new connection without SSL - connection = CreateConnectionHelper(target, false, authType, globalCatalog); - - connectionResult = TestConnection(connection); - - if (connectionResult.Success) - { - var domain = connectionResult.DomainInfo.DomainFQDN; - if (!CachedDomainInfo.ContainsKey(domain.ToUpper())) - { - var baseDomainInfo = connectionResult.DomainInfo; - baseDomainInfo.DomainSID = GetDomainSidFromConnection(connection, baseDomainInfo); - baseDomainInfo.DomainNetbiosName = GetDomainNetbiosName(connection, baseDomainInfo); - CachedDomainInfo.TryAdd(baseDomainInfo.DomainFQDN, baseDomainInfo); - CachedDomainInfo.TryAdd(baseDomainInfo.DomainNetbiosName, baseDomainInfo); - CachedDomainInfo.TryAdd(baseDomainInfo.DomainSID, baseDomainInfo); - - if (!string.IsNullOrEmpty(baseDomainInfo.DomainSID)) - { - Cache.AddDomainSidMapping(baseDomainInfo.DomainFQDN, baseDomainInfo.DomainSID); - } - - if (!string.IsNullOrEmpty(baseDomainInfo.DomainNetbiosName)) - { - Cache.AddDomainSidMapping(baseDomainInfo.DomainNetbiosName, baseDomainInfo.DomainSID); - } - - info = baseDomainInfo; - }else - { - CachedDomainInfo.TryGetValue(domain, out info); - } - return new LdapConnectionWrapper - { - Connection = connection, - DomainInfo = info - }; - } - - CheckAndThrowException(connectionResult.Exception); - return null; - } - - private LdapConnectionTestResult TestConnection(LdapConnection connection) - { - try - { - //Attempt an initial bind. If this fails, likely auth is invalid, or its not a valid target - connection.Bind(); - } - catch (LdapException e) - { - connection.Dispose(); - return new LdapConnectionTestResult(false, e, null, null); - } - - try - { - //Do an initial search request to get the rootDSE - //This ldap filter is equivalent to (objectclass=*) - 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) - { - connection.Dispose(); - return new LdapConnectionTestResult(false, null, null, null); - } - - if (response.Entries.Count == 0) - { - connection.Dispose(); - return new LdapConnectionTestResult(false, new LdapException((int)LdapErrorCodes.KerberosAuthType), null, null); - } - - var entry = response.Entries[0]; - var baseDN = entry.GetProperty(LDAPProperties.RootDomainNamingContext).ToUpper().Trim(); - var configurationDN = entry.GetProperty(LDAPProperties.ConfigurationNamingContext).ToUpper().Trim(); - var domainname = Helpers.DistinguishedNameToDomain(baseDN).ToUpper().Trim(); - var servername = entry.GetProperty(LDAPProperties.ServerName); - var compName = servername.Substring(0, servername.IndexOf(',')).Substring(3).Trim(); - var fullServerName = $"{compName}.{domainname}".ToUpper().Trim(); - - return new LdapConnectionTestResult(true, null, new DomainInfo - { - DomainConfigurationPath = configurationDN, - DomainSearchBase = baseDN, - DomainFQDN = domainname - }, fullServerName); - } - catch (LdapException e) - { - try - { - connection.Dispose(); - } - catch - { - //pass - } - return new LdapConnectionTestResult(false, e, null, null); - } - } - - public class LdapConnectionTestResult - { - public bool Success { get; set; } - public LdapException Exception { get; set; } - public DomainInfo DomainInfo { get; set; } - public string ServerName { get; set; } - - public LdapConnectionTestResult(bool success, LdapException e, DomainInfo info, string server) - { - Success = success; - Exception = e; - DomainInfo = info; - ServerName = server; - } - } - - private string GetDomainNetbiosName(LdapConnection connection, DomainInfo info) - { - try - { - var searchRequest = new SearchRequest($"CN=Partitions,{info.DomainConfigurationPath}", - "(&(nETBIOSName=*)(dnsRoot=*))", - SearchScope.Subtree, new[] { LDAPProperties.NetbiosName, LDAPProperties.DnsRoot }); - - var response = (SearchResponse)connection.SendRequest(searchRequest); - if (response == null || response.Entries.Count == 0) - { - return ""; - } - - foreach (SearchResultEntry entry in response.Entries) - { - var root = entry.GetProperty(LDAPProperties.DnsRoot); - var netbios = entry.GetProperty(LDAPProperties.NetbiosName); - _log.LogInformation(root); - _log.LogInformation(netbios); - - if (root.ToUpper().Equals(info.DomainFQDN)) - { - return netbios.ToUpper(); - } - } - - return ""; - } - catch (LdapException e) - { - _log.LogWarning("Failed grabbing netbios name from ldap for {domain}: {e}", info.DomainFQDN, e); - return ""; - } - } - - private string GetDomainSidFromConnection(LdapConnection connection, DomainInfo info) - { - 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(info.DomainSearchBase, - "(&(objectCategory=computer)(userAccountControl:1.2.840.113556.1.4.803:=8192))", - SearchScope.Subtree, new[] { "objectsid"}); - - var response = (SearchResponse)connection.SendRequest(searchRequest); - if (response == null || response.Entries.Count == 0) - { - return ""; - } - - var entry = response.Entries[0]; - var sid = entry.GetSid(); - return sid.Substring(0, sid.LastIndexOf('-')).ToUpper(); - } - catch (LdapException) - { - _log.LogWarning("Failed grabbing domainsid from ldap for {domain}", info.DomainFQDN); - return ""; - } - } - - private void SetupLdapConnection(LdapConnection connection, bool ssl, AuthType authType) - { - //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; - } - - if (_ldapConfig.DisableSigning) - { - connection.SessionOptions.Sealing = false; - connection.SessionOptions.Signing = false; - } - - if (_ldapConfig.DisableCertVerification) - connection.SessionOptions.VerifyServerCertificate = (_, _) => true; - - if (_ldapConfig.Username != null) - { - var cred = new NetworkCredential(_ldapConfig.Username, _ldapConfig.Password); - connection.Credential = cred; - } - - connection.AuthType = authType; - } - - /// - /// Normalizes a domain name to its full DNS name - /// - /// - /// - internal string NormalizeDomainName(string domain) - { - if (domain == null) - return null; - - var resolved = domain; - - if (resolved.Contains(".")) - return domain.ToUpper(); - - resolved = ResolveDomainNetbiosToDns(domain) ?? domain; - - return resolved.ToUpper(); - } - - /// - /// Turns a domain Netbios name into its FQDN using the DsGetDcName function (TESTLAB -> TESTLAB.LOCAL) - /// - /// - /// - internal string ResolveDomainNetbiosToDns(string domainName) - { - var key = domainName.ToUpper(); - if (_netbiosCache.TryGetValue(key, out var flatName)) - return flatName; - - var domain = GetDomain(domainName); - if (domain != null) - { - _netbiosCache.TryAdd(key, domain.Name); - return domain.Name; - } - - var computerName = _ldapConfig.Server; - - var dci = _nativeMethods.CallDsGetDcName(computerName, domainName); - if (dci.IsSuccess) - { - flatName = dci.Value.DomainName; - _netbiosCache.TryAdd(key, flatName); - return flatName; - } - - return domainName.ToUpper(); - } - - /// - /// Gets the range retrieval limit for a domain - /// - /// - /// - /// - public int GetDomainRangeSize(string domainName = null, int defaultRangeSize = 750) - { - var domainPath = DomainNameToDistinguishedName(domainName); - //Default to a page size of 750 for safety - if (domainPath == null) - { - _log.LogDebug("Unable to resolve domain {Domain} to distinguishedname to get page size", - domainName ?? "current domain"); - return defaultRangeSize; - } - - if (_ldapRangeSizeCache.TryGetValue(domainPath.ToUpper(), out var parsedPageSize)) - { - return parsedPageSize; - } - - var configPath = CommonPaths.CreateDNPath(CommonPaths.QueryPolicyPath, domainPath); - var enumerable = QueryLDAP("(objectclass=*)", SearchScope.Base, null, adsPath: configPath); - var config = enumerable.DefaultIfEmpty(null).FirstOrDefault(); - var pageSize = config?.GetArrayProperty(LDAPProperties.LdapAdminLimits) - .FirstOrDefault(x => x.StartsWith("MaxPageSize", StringComparison.OrdinalIgnoreCase)); - if (pageSize == null) - { - _log.LogDebug("No LDAPAdminLimits object found for {Domain}", domainName); - _ldapRangeSizeCache.TryAdd(domainPath.ToUpper(), defaultRangeSize); - return defaultRangeSize; - } - - if (int.TryParse(pageSize.Split('=').Last(), out parsedPageSize)) - { - _ldapRangeSizeCache.TryAdd(domainPath.ToUpper(), parsedPageSize); - _log.LogInformation("Found page size {PageSize} for {Domain}", parsedPageSize, - domainName ?? "current domain"); - return parsedPageSize; - } - - _log.LogDebug("Failed to parse pagesize for {Domain}, returning default", domainName ?? "current domain"); - - _ldapRangeSizeCache.TryAdd(domainPath.ToUpper(), defaultRangeSize); - return defaultRangeSize; - } - - private string DomainNameToDistinguishedName(string domain) - { - var resolvedDomain = GetDomain(domain)?.Name ?? domain; - return resolvedDomain == null ? null : $"DC={resolvedDomain.Replace(".", ",DC=")}"; - } - - private class ResolvedWellKnownPrincipal - { - public string DomainName { get; set; } - public string WkpId { get; set; } - } - - public string GetConfigurationPath(string domainName = null) - { - string path = domainName == null - ? "LDAP://RootDSE" - : $"LDAP://{NormalizeDomainName(domainName)}/RootDSE"; - - DirectoryEntry rootDse; - if (_ldapConfig.Username != null) - rootDse = new DirectoryEntry(path, _ldapConfig.Username, _ldapConfig.Password); - else - rootDse = new DirectoryEntry(path); - - return $"{rootDse.Properties["configurationNamingContext"]?[0]}"; - } - - public string GetSchemaPath(string domainName) - { - string path = domainName == null - ? "LDAP://RootDSE" - : $"LDAP://{NormalizeDomainName(domainName)}/RootDSE"; - - DirectoryEntry rootDse; - if (_ldapConfig.Username != null) - rootDse = new DirectoryEntry(path, _ldapConfig.Username, _ldapConfig.Password); - else - rootDse = new DirectoryEntry(path); - - return $"{rootDse.Properties["schemaNamingContext"]?[0]}"; - } - - public bool IsDomainController(string computerObjectId, string domainName) - { - var filter = new LDAPFilter().AddFilter(LDAPProperties.ObjectSID + "=" + computerObjectId, true) - .AddFilter(CommonFilters.DomainControllers, true); - var res = QueryLDAP(filter.GetFilter(), SearchScope.Subtree, - CommonProperties.ObjectID, domainName: domainName); - if (res.Count() > 0) - return true; - return false; - } - } -} \ No newline at end of file diff --git a/src/CommonLib/LDAPConfig.cs b/src/CommonLib/LdapConfig.cs similarity index 82% rename from src/CommonLib/LDAPConfig.cs rename to src/CommonLib/LdapConfig.cs index e8bb26e4..e59bae71 100644 --- a/src/CommonLib/LDAPConfig.cs +++ b/src/CommonLib/LdapConfig.cs @@ -2,12 +2,13 @@ namespace SharpHoundCommonLib { - public class LDAPConfig + public class LdapConfig { public string Username { get; set; } = null; public string Password { get; set; } = null; public string Server { get; set; } = null; public int Port { get; set; } = 0; + public int SSLPort { get; set; } = 0; public bool ForceSSL { get; set; } = false; public bool DisableSigning { get; set; } = false; public bool DisableCertVerification { get; set; } = false; @@ -16,7 +17,10 @@ public class LDAPConfig //Returns the port for connecting to LDAP. Will always respect a user's overridden config over anything else public int GetPort(bool ssl) { - if (Port != 0) + if (ssl && SSLPort != 0) { + return SSLPort; + } + if (!ssl && Port != 0) { return Port; } diff --git a/src/CommonLib/LdapConnectionPool.cs b/src/CommonLib/LdapConnectionPool.cs new file mode 100644 index 00000000..69f44da7 --- /dev/null +++ b/src/CommonLib/LdapConnectionPool.cs @@ -0,0 +1,367 @@ +using System; +using System.Collections.Concurrent; +using System.DirectoryServices.ActiveDirectory; +using System.DirectoryServices.Protocols; +using System.Net; +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; + +namespace SharpHoundCommonLib { + public class LdapConnectionPool : IDisposable{ + private readonly ConcurrentBag _connections; + private readonly ConcurrentBag _globalCatalogConnection; + private readonly SemaphoreSlim _semaphore; + private readonly string _identifier; + private readonly string _poolIdentifier; + private readonly LdapConfig _ldapConfig; + private readonly ILogger _log; + private readonly PortScanner _portScanner; + private readonly NativeMethods _nativeMethods; + + public LdapConnectionPool(string identifier, string poolIdentifier, LdapConfig config, int maxConnections = 10, PortScanner scanner = null, NativeMethods nativeMethods = null, ILogger log = null) { + _connections = new ConcurrentBag(); + _globalCatalogConnection = new ConcurrentBag(); + _semaphore = new SemaphoreSlim(maxConnections, maxConnections); + _identifier = identifier; + _poolIdentifier = poolIdentifier; + _ldapConfig = config; + _log = log ?? Logging.LogProvider.CreateLogger("LdapConnectionPool"); + _portScanner = scanner ?? new PortScanner(); + _nativeMethods = nativeMethods ?? new NativeMethods(); + } + + public async Task<(bool Success, LdapConnectionWrapper ConnectionWrapper, string Message)> GetConnectionAsync() { + await _semaphore.WaitAsync(); + 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; + } + + return (true, connectionWrapper, null); + } + + public async Task<(bool Success, LdapConnectionWrapper connectionWrapper, string Message)> + GetConnectionForSpecificServerAsync(string server, bool globalCatalog) { + await _semaphore.WaitAsync(); + + 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, LdapConnectionWrapper ConnectionWrapper, string Message)> GetGlobalCatalogConnectionAsync() { + await _semaphore.WaitAsync(); + 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); + } + + connectionWrapper = connection; + } + + return (true, connectionWrapper, null); + } + + public void ReleaseConnection(LdapConnectionWrapper connectionWrapper, bool connectionFaulted = false) { + _semaphore.Release(); + if (!connectionFaulted) { + if (connectionWrapper.GlobalCatalog) { + _globalCatalogConnection.Add(connectionWrapper); + } + else { + _connections.Add(connectionWrapper); + } + } + else { + connectionWrapper.Connection.Dispose(); + } + } + + public void Dispose() { + while (_connections.TryTake(out var wrapper)) { + wrapper.Connection.Dispose(); + } + } + + private async Task<(bool Success, LdapConnectionWrapper Connection, string Message)> CreateNewConnection(bool globalCatalog = false) { + try { + if (!string.IsNullOrWhiteSpace(_ldapConfig.Server)) { + return CreateNewConnectionForServer(_ldapConfig.Server, globalCatalog); + } + + if (CreateLdapConnection(_identifier.ToUpper().Trim(), globalCatalog, out var connectionWrapper)) { + _log.LogDebug("Successfully created ldap connection for domain: {Domain} using strategy 1. SSL: {SSl}", _identifier, connectionWrapper.Connection.SessionOptions.SecureSocketLayer); + return (true, connectionWrapper, ""); + } + + string tempDomainName; + + var dsGetDcNameResult = _nativeMethods.CallDsGetDcName(null, _identifier, + (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 (!tempDomainName.Equals(_identifier, StringComparison.OrdinalIgnoreCase) && + CreateLdapConnection(tempDomainName, globalCatalog, out connectionWrapper)) { + _log.LogDebug( + "Successfully created ldap connection for domain: {Domain} using strategy 2 with name {NewName}", + _identifier, tempDomainName); + 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}", + _identifier, server); + return (true, result.connection, ""); + } + } + + if (!LdapUtils.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}", + _identifier); + return (false, null, "Unable to get domain object for further strategies"); + } + tempDomainName = domainObject.Name.ToUpper().Trim(); + + if (!tempDomainName.Equals(_identifier, StringComparison.OrdinalIgnoreCase) && + CreateLdapConnection(tempDomainName, globalCatalog, out connectionWrapper)) { + _log.LogDebug( + "Successfully created ldap connection for domain: {Domain} using strategy 4 with name {NewName}", + _identifier, tempDomainName); + 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}", + _identifier, primaryDomainController); + return (true, portConnectionResult.connection, ""); + } + + foreach (DomainController dc in domainObject.DomainControllers) { + portConnectionResult = + await CreateLDAPConnectionWithPortCheck(dc.Name, globalCatalog); + if (portConnectionResult.success) { + _log.LogDebug( + "Successfully created ldap connection for domain: {Domain} using strategy 6 with to pdc {Server}", + _identifier, primaryDomainController); + return (true, portConnectionResult.connection, ""); + } + } + } catch (Exception e) { + _log.LogInformation(e, "We will not be able to connect to domain {Domain} by any strategy, leaving it.", _identifier); + } + + return (false, null, "All attempted connections failed"); + } + + private (bool Success, LdapConnectionWrapper Connection, string Message ) CreateNewConnectionForServer(string identifier, bool globalCatalog = false) { + if (CreateLdapConnection(identifier, globalCatalog, out var serverConnection)) { + return (true, serverConnection, ""); + } + + return (false, null, $"Failed to create ldap connection for {identifier}"); + } + + private bool CreateLdapConnection(string target, bool globalCatalog, + out LdapConnectionWrapper connection) { + var baseConnection = CreateBaseConnection(target, true, globalCatalog); + if (TestLdapConnection(baseConnection, out var result)) { + connection = new LdapConnectionWrapper(baseConnection, result.SearchResultEntry, globalCatalog, _poolIdentifier); + 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, out result)) { + connection = new LdapConnectionWrapper(baseConnection, result.SearchResultEntry, globalCatalog, _poolIdentifier); + return true; + } + + try { + baseConnection.Dispose(); + } + catch { + //this is just in case + } + + connection = null; + return false; + } + + private LdapConnection CreateBaseConnection(string directoryIdentifier, bool ssl, + bool globalCatalog) { + _log.LogDebug("Creating connection for identifier {Identifier}", directoryIdentifier); + 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; + + if (_ldapConfig.DisableSigning || ssl) { + connection.SessionOptions.Signing = false; + connection.SessionOptions.Sealing = false; + } + else { + connection.SessionOptions.Signing = true; + connection.SessionOptions.Sealing = true; + } + + 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 ldap connection object to test + /// The results fo the connection test + /// True if connection was successful, false otherwise + /// 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, out LdapConnectionTestResult testResult) { + testResult = new LdapConnectionTestResult(); + 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); + } + + testResult.Message = e.Message; + testResult.ErrorCode = e.ErrorCode; + return false; + } + catch (Exception e) { + testResult.Message = e.Message; + 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 + */ + testResult.Message = e.Message; + testResult.ErrorCode = e.ErrorCode; + 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 + */ + + connection.Dispose(); + throw new NoLdapDataException(); + } + + testResult.SearchResultEntry = new SearchResultEntryWrapper(response.Entries[0]); + testResult.Message = ""; + return true; + } + + private class LdapConnectionTestResult { + public string Message { get; set; } + public IDirectoryObject SearchResultEntry { get; set; } + public int ErrorCode { get; set; } + } + + private async Task<(bool success, LdapConnectionWrapper 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 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; + } + } +} \ No newline at end of file diff --git a/src/CommonLib/LdapConnectionWrapper.cs b/src/CommonLib/LdapConnectionWrapper.cs index 031b32c9..17314518 100644 --- a/src/CommonLib/LdapConnectionWrapper.cs +++ b/src/CommonLib/LdapConnectionWrapper.cs @@ -1,10 +1,97 @@ +using System; using System.DirectoryServices.Protocols; +using SharpHoundCommonLib.Enums; -namespace SharpHoundCommonLib -{ - public class LdapConnectionWrapper - { - public LdapConnection Connection; - public DomainInfo DomainInfo; +namespace SharpHoundCommonLib { + public class LdapConnectionWrapper { + public LdapConnection Connection { get; private set; } + private readonly IDirectoryObject _rootDseEntry; + private string _domainSearchBase; + private string _configurationSearchBase; + private string _schemaSearchBase; + private string _server; + private string Guid { get; set; } + public readonly bool GlobalCatalog; + public readonly string PoolIdentifier; + + public LdapConnectionWrapper(LdapConnection connection, IDirectoryObject rootDseEntry, bool globalCatalog, + string poolIdentifier) { + Connection = connection; + _rootDseEntry = rootDseEntry; + Guid = new Guid().ToString(); + GlobalCatalog = globalCatalog; + PoolIdentifier = poolIdentifier; + } + + public string GetServer() { + if (_server != null) { + return _server; + } + + _server = _rootDseEntry.GetProperty(LDAPProperties.DNSHostName); + return _server; + } + + public bool GetSearchBase(NamingContext context, out string searchBase) { + searchBase = GetSavedContext(context); + if (searchBase != null) { + return true; + } + + searchBase = context switch { + NamingContext.Default => _rootDseEntry.GetProperty(LDAPProperties.DefaultNamingContext), + NamingContext.Configuration => + _rootDseEntry.GetProperty(LDAPProperties.ConfigurationNamingContext), + NamingContext.Schema => _rootDseEntry.GetProperty(LDAPProperties.SchemaNamingContext), + _ => throw new ArgumentOutOfRangeException(nameof(context), context, null) + }; + + if (searchBase != null) { + SaveContext(context, searchBase); + return true; + } + + return false; + } + + private string GetSavedContext(NamingContext context) { + return context switch { + NamingContext.Configuration => _configurationSearchBase, + NamingContext.Default => _domainSearchBase, + NamingContext.Schema => _schemaSearchBase, + _ => throw new ArgumentOutOfRangeException(nameof(context), context, null) + }; + } + + public void SaveContext(NamingContext context, string searchBase) { + switch (context) { + case NamingContext.Default: + _domainSearchBase = searchBase; + break; + case NamingContext.Configuration: + _configurationSearchBase = searchBase; + break; + case NamingContext.Schema: + _schemaSearchBase = searchBase; + break; + default: + throw new ArgumentOutOfRangeException(nameof(context), context, null); + } + } + + protected bool Equals(LdapConnectionWrapper other) { + return Guid == other.Guid; + } + + public override bool Equals(object obj) { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((LdapConnectionWrapper)obj); + } + + public override int GetHashCode() { + return (Guid != null ? Guid.GetHashCode() : 0); + } } } \ No newline at end of file diff --git a/src/CommonLib/LdapProducerQueryGenerator.cs b/src/CommonLib/LdapProducerQueryGenerator.cs new file mode 100644 index 00000000..ec38f28c --- /dev/null +++ b/src/CommonLib/LdapProducerQueryGenerator.cs @@ -0,0 +1,151 @@ +using System.Collections.Generic; +using System.Linq; +using SharpHoundCommonLib.Enums; +using SharpHoundCommonLib.LDAPQueries; + +namespace SharpHoundCommonLib; + +public class LdapProducerQueryGenerator { + public static GeneratedLdapParameters GenerateDefaultPartitionParameters(CollectionMethod methods) { + var filter = new LdapFilter(); + var properties = new List(); + + properties.AddRange(CommonProperties.BaseQueryProps); + properties.AddRange(CommonProperties.TypeResolutionProps); + + if (methods.HasFlag(CollectionMethod.ObjectProps) || methods.HasFlag(CollectionMethod.ACL) || methods.HasFlag(CollectionMethod.Container)) { + filter = filter.AddComputers().AddDomains().AddUsers().AddContainers().AddGPOs().AddOUs().AddGroups(); + + if (methods.HasFlag(CollectionMethod.Container)) { + properties.AddRange(CommonProperties.ContainerProps); + } + + if (methods.HasFlag(CollectionMethod.Group)) { + properties.AddRange(CommonProperties.GroupResolutionProps); + } + + if (methods.HasFlag(CollectionMethod.ACL)) { + properties.AddRange(CommonProperties.ACLProps); + } + + if (methods.IsComputerCollectionSet()) { + properties.AddRange(CommonProperties.ComputerMethodProps); + } + + if (methods.HasFlag(CollectionMethod.Trusts)) { + properties.AddRange(CommonProperties.DomainTrustProps); + } + + if (methods.HasFlag(CollectionMethod.GPOLocalGroup)) + properties.AddRange(CommonProperties.GPOLocalGroupProps); + + if (methods.HasFlag(CollectionMethod.SPNTargets)) + properties.AddRange(CommonProperties.SPNTargetProps); + + if (methods.HasFlag(CollectionMethod.DCRegistry)) + properties.AddRange(CommonProperties.ComputerMethodProps); + + if (methods.HasFlag(CollectionMethod.SPNTargets)) { + properties.AddRange(CommonProperties.SPNTargetProps); + } + + return new GeneratedLdapParameters { + Filter = filter, + Attributes = properties.Distinct().ToArray() + }; + } + + if (methods.HasFlag(CollectionMethod.Group)) { + filter = filter.AddGroups(); + properties.AddRange(CommonProperties.GroupResolutionProps); + } + + if (methods.IsComputerCollectionSet()) { + filter = filter.AddComputers(); + properties.AddRange(CommonProperties.ComputerMethodProps); + } + + if (methods.HasFlag(CollectionMethod.Trusts)) { + filter = filter.AddDomains(); + properties.AddRange(CommonProperties.ComputerMethodProps); + } + + if (methods.HasFlag(CollectionMethod.SPNTargets)) { + filter = filter.AddUsers(CommonFilters.NeedsSPN); + properties.AddRange(CommonProperties.SPNTargetProps); + } + + if (methods.HasFlag(CollectionMethod.GPOLocalGroup)) { + filter = filter.AddOUs(); + properties.AddRange(CommonProperties.GPOLocalGroupProps); + } + + if (methods.HasFlag(CollectionMethod.DCRegistry)) { + filter = filter.AddComputers(CommonFilters.DomainControllers); + properties.AddRange(CommonProperties.ComputerMethodProps); + } + + return new GeneratedLdapParameters { + Filter = filter, + Attributes = properties.Distinct().ToArray() + }; + } + + public static GeneratedLdapParameters GenerateConfigurationPartitionParameters(CollectionMethod methods) { + var filter = new LdapFilter(); + var properties = new List(); + + properties.AddRange(CommonProperties.BaseQueryProps); + properties.AddRange(CommonProperties.TypeResolutionProps); + + if (methods.HasFlag(CollectionMethod.ACL) || methods.HasFlag(CollectionMethod.ObjectProps) || + methods.HasFlag(CollectionMethod.Container) || methods.HasFlag(CollectionMethod.CertServices)) { + filter = filter.AddContainers().AddConfiguration().AddCertificateTemplates().AddCertificateAuthorities() + .AddEnterpriseCertificationAuthorities().AddIssuancePolicies(); + + if (methods.HasFlag(CollectionMethod.ObjectProps)) + { + properties.AddRange(CommonProperties.ObjectPropsProps); + } + + if (methods.HasFlag(CollectionMethod.ACL)) { + properties.AddRange(CommonProperties.ACLProps); + } + + if (methods.HasFlag(CollectionMethod.Container)) { + properties.AddRange(CommonProperties.ContainerProps); + } + + if (methods.HasFlag(CollectionMethod.CertServices)) { + properties.AddRange(CommonProperties.CertAbuseProps); + properties.AddRange(CommonProperties.ObjectPropsProps); + properties.AddRange(CommonProperties.ContainerProps); + properties.AddRange(CommonProperties.ACLProps); + } + + if (methods.HasFlag(CollectionMethod.CARegistry)) { + properties.AddRange(CommonProperties.CertAbuseProps); + } + + return new GeneratedLdapParameters { + Filter = filter, + Attributes = properties.Distinct().ToArray() + }; + } + + if (methods.HasFlag(CollectionMethod.CARegistry)) { + filter = filter.AddEnterpriseCertificationAuthorities(); + properties.AddRange(CommonProperties.CertAbuseProps); + } + + return new GeneratedLdapParameters { + Filter = filter, + Attributes = properties.Distinct().ToArray() + }; + } +} + +public class GeneratedLdapParameters { + public string[] Attributes { get; set; } + public LdapFilter Filter { get; set; } +} \ No newline at end of file diff --git a/src/CommonLib/LDAPQueries/CommonFilters.cs b/src/CommonLib/LdapQueries/CommonFilters.cs similarity index 100% rename from src/CommonLib/LDAPQueries/CommonFilters.cs rename to src/CommonLib/LdapQueries/CommonFilters.cs diff --git a/src/CommonLib/LDAPQueries/CommonPaths.cs b/src/CommonLib/LdapQueries/CommonPaths.cs similarity index 100% rename from src/CommonLib/LDAPQueries/CommonPaths.cs rename to src/CommonLib/LdapQueries/CommonPaths.cs diff --git a/src/CommonLib/LDAPQueries/CommonProperties.cs b/src/CommonLib/LdapQueries/CommonProperties.cs similarity index 93% rename from src/CommonLib/LDAPQueries/CommonProperties.cs rename to src/CommonLib/LdapQueries/CommonProperties.cs index c1644aac..54a59549 100644 --- a/src/CommonLib/LDAPQueries/CommonProperties.cs +++ b/src/CommonLib/LdapQueries/CommonProperties.cs @@ -5,7 +5,8 @@ public static class CommonProperties public static readonly string[] TypeResolutionProps = { LDAPProperties.SAMAccountType, LDAPProperties.ObjectSID, LDAPProperties.ObjectGUID, - LDAPProperties.ObjectClass, LDAPProperties.SAMAccountName, LDAPProperties.GroupMSAMembership + LDAPProperties.ObjectClass, LDAPProperties.SAMAccountName, LDAPProperties.GroupMSAMembership, + LDAPProperties.Flags }; public static readonly string[] ObjectID = { LDAPProperties.ObjectSID, LDAPProperties.ObjectGUID }; @@ -29,7 +30,8 @@ public static class CommonProperties public static readonly string[] ComputerMethodProps = { LDAPProperties.SAMAccountName, LDAPProperties.DistinguishedName, LDAPProperties.DNSHostName, - LDAPProperties.SAMAccountType, LDAPProperties.OperatingSystem, LDAPProperties.PasswordLastSet + LDAPProperties.SAMAccountType, LDAPProperties.OperatingSystem, LDAPProperties.PasswordLastSet, + LDAPProperties.LastLogonTimestamp }; public static readonly string[] ACLProps = @@ -86,5 +88,9 @@ public static class CommonProperties LDAPProperties.CertificateApplicationPolicy, LDAPProperties.CertificatePolicy, LDAPProperties.IssuancePolicies, LDAPProperties.CrossCertificatePair, LDAPProperties.ApplicationPolicies, LDAPProperties.PKIPrivateKeyFlag, LDAPProperties.OIDGroupLink }; + + public static readonly string[] StealthProperties = { + LDAPProperties.HomeDirectory, LDAPProperties.ScriptPath, LDAPProperties.ProfilePath + }; } } \ No newline at end of file diff --git a/src/CommonLib/LDAPQueries/LDAPFilter.cs b/src/CommonLib/LdapQueries/LdapFilter.cs similarity index 88% rename from src/CommonLib/LDAPQueries/LDAPFilter.cs rename to src/CommonLib/LdapQueries/LdapFilter.cs index 44402c7f..58bb4fbb 100644 --- a/src/CommonLib/LDAPQueries/LDAPFilter.cs +++ b/src/CommonLib/LdapQueries/LdapFilter.cs @@ -6,7 +6,7 @@ namespace SharpHoundCommonLib.LDAPQueries /// /// A class used to more easily build LDAP filters based on the common filters used by SharpHound /// - public class LDAPFilter + public class LdapFilter { private readonly List _filterParts = new(); private readonly List _mandatory = new(); @@ -49,7 +49,7 @@ private static string BuildString(string baseFilter, params string[] conditions) /// /// /// - public LDAPFilter AddAllObjects(params string[] conditions) + public LdapFilter AddAllObjects(params string[] conditions) { _filterParts.Add(BuildString("(objectclass=*)", conditions)); @@ -61,7 +61,7 @@ public LDAPFilter AddAllObjects(params string[] conditions) /// /// /// - public LDAPFilter AddUsers(params string[] conditions) + public LdapFilter AddUsers(params string[] conditions) { _filterParts.Add(BuildString("(samaccounttype=805306368)", conditions)); @@ -73,7 +73,7 @@ public LDAPFilter AddUsers(params string[] conditions) /// /// /// - public LDAPFilter AddGroups(params string[] conditions) + public LdapFilter AddGroups(params string[] conditions) { _filterParts.Add(BuildString( "(|(samaccounttype=268435456)(samaccounttype=268435457)(samaccounttype=536870912)(samaccounttype=536870913))", @@ -87,7 +87,7 @@ public LDAPFilter AddGroups(params string[] conditions) /// /// /// - public LDAPFilter AddPrimaryGroups(params string[] conditions) + public LdapFilter AddPrimaryGroups(params string[] conditions) { _filterParts.Add(BuildString("(primarygroupid=*)", conditions)); @@ -99,7 +99,7 @@ public LDAPFilter AddPrimaryGroups(params string[] conditions) /// /// /// - public LDAPFilter AddGPOs(params string[] conditions) + public LdapFilter AddGPOs(params string[] conditions) { _filterParts.Add(BuildString("(&(objectcategory=groupPolicyContainer)(flags=*))", conditions)); @@ -111,7 +111,7 @@ public LDAPFilter AddGPOs(params string[] conditions) /// /// /// - public LDAPFilter AddOUs(params string[] conditions) + public LdapFilter AddOUs(params string[] conditions) { _filterParts.Add(BuildString("(objectcategory=organizationalUnit)", conditions)); @@ -123,7 +123,7 @@ public LDAPFilter AddOUs(params string[] conditions) /// /// /// - public LDAPFilter AddDomains(params string[] conditions) + public LdapFilter AddDomains(params string[] conditions) { _filterParts.Add(BuildString("(objectclass=domain)", conditions)); @@ -135,7 +135,7 @@ public LDAPFilter AddDomains(params string[] conditions) /// /// /// - public LDAPFilter AddContainers(params string[] conditions) + public LdapFilter AddContainers(params string[] conditions) { _filterParts.Add(BuildString("(objectClass=container)", conditions)); @@ -147,7 +147,7 @@ public LDAPFilter AddContainers(params string[] conditions) /// /// /// - public LDAPFilter AddConfiguration(params string[] conditions) + public LdapFilter AddConfiguration(params string[] conditions) { _filterParts.Add(BuildString("(objectClass=configuration)", conditions)); @@ -161,7 +161,7 @@ public LDAPFilter AddConfiguration(params string[] conditions) /// /// /// - public LDAPFilter AddComputers(params string[] conditions) + public LdapFilter AddComputers(params string[] conditions) { _filterParts.Add(BuildString("(samaccounttype=805306369)", conditions)); return this; @@ -172,7 +172,7 @@ public LDAPFilter AddComputers(params string[] conditions) /// /// /// - public LDAPFilter AddCertificateTemplates(params string[] conditions) + public LdapFilter AddCertificateTemplates(params string[] conditions) { _filterParts.Add(BuildString("(objectclass=pKICertificateTemplate)", conditions)); return this; @@ -183,7 +183,7 @@ public LDAPFilter AddCertificateTemplates(params string[] conditions) /// /// /// - public LDAPFilter AddCertificateAuthorities(params string[] conditions) + public LdapFilter AddCertificateAuthorities(params string[] conditions) { _filterParts.Add(BuildString("(|(objectClass=certificationAuthority)(objectClass=pkiEnrollmentService))", conditions)); @@ -195,7 +195,7 @@ public LDAPFilter AddCertificateAuthorities(params string[] conditions) /// /// /// - public LDAPFilter AddEnterpriseCertificationAuthorities(params string[] conditions) + public LdapFilter AddEnterpriseCertificationAuthorities(params string[] conditions) { _filterParts.Add(BuildString("(objectCategory=pKIEnrollmentService)", conditions)); return this; @@ -206,7 +206,7 @@ public LDAPFilter AddEnterpriseCertificationAuthorities(params string[] conditio /// /// /// - public LDAPFilter AddIssuancePolicies(params string[] conditions) + public LdapFilter AddIssuancePolicies(params string[] conditions) { _filterParts.Add(BuildString("(objectClass=msPKI-Enterprise-Oid)", conditions)); return this; @@ -217,7 +217,7 @@ public LDAPFilter AddIssuancePolicies(params string[] conditions) /// /// /// - public LDAPFilter AddSchemaID(params string[] conditions) + public LdapFilter AddSchemaID(params string[] conditions) { _filterParts.Add(BuildString("(schemaidguid=*)", conditions)); return this; @@ -228,7 +228,7 @@ public LDAPFilter AddSchemaID(params string[] conditions) /// /// /// - public LDAPFilter AddComputersNoMSAs(params string[] conditions) + public LdapFilter AddComputersNoMSAs(params string[] conditions) { _filterParts.Add(BuildString("(&(samaccounttype=805306369)(!(objectclass=msDS-GroupManagedServiceAccount))(!(objectclass=msDS-ManagedServiceAccount)))", conditions)); return this; @@ -240,7 +240,7 @@ public LDAPFilter AddComputersNoMSAs(params string[] conditions) /// LDAP Filter to add to query /// If true, filter will be AND otherwise OR /// - public LDAPFilter AddFilter(string filter, bool enforce) + public LdapFilter AddFilter(string filter, bool enforce) { if (enforce) _mandatory.Add(FixFilter(filter)); @@ -277,7 +277,7 @@ public string GetFilter() public IEnumerable GetFilterList() { - return _filterParts; + return _filterParts.Distinct(); } } } \ No newline at end of file diff --git a/src/CommonLib/LdapQueryParameters.cs b/src/CommonLib/LdapQueryParameters.cs new file mode 100644 index 00000000..e4f099d9 --- /dev/null +++ b/src/CommonLib/LdapQueryParameters.cs @@ -0,0 +1,41 @@ +using System; +using System.DirectoryServices.Protocols; +using SharpHoundCommonLib.Enums; + +namespace SharpHoundCommonLib { + public class LdapQueryParameters + { + 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(); + public string DomainName { get; set; } + public bool GlobalCatalog { get; set; } + public bool IncludeSecurityDescriptor { get; set; } = false; + public bool IncludeDeleted { get; set; } = false; + + public string SearchBase { + get => _searchBase; + set { + _relativeSearchBase = null; + _searchBase = value; + } + } + + public string RelativeSearchBase { + get => _relativeSearchBase; + set { + _relativeSearchBase = value; + _searchBase = null; + } + } + + public NamingContext NamingContext { get; set; } = NamingContext.Default; + + public string GetQueryInfo() + { + return $"Query Information - Filter: {LDAPFilter}, Domain: {DomainName}, GlobalCatalog: {GlobalCatalog}, ADSPath: {SearchBase}"; + } + } +} \ No newline at end of file diff --git a/src/CommonLib/LdapQuerySetupResult.cs b/src/CommonLib/LdapQuerySetupResult.cs new file mode 100644 index 00000000..8a2f0e73 --- /dev/null +++ b/src/CommonLib/LdapQuerySetupResult.cs @@ -0,0 +1,12 @@ +using System.DirectoryServices; +using System.DirectoryServices.Protocols; + +namespace SharpHoundCommonLib { + public class LdapQuerySetupResult { + public LdapConnectionWrapper ConnectionWrapper { get; set; } + public SearchRequest SearchRequest { get; set; } + public string Server { get; set; } + public bool Success { get; set; } + public string Message { get; set; } + } +} \ No newline at end of file diff --git a/src/CommonLib/LdapResult.cs b/src/CommonLib/LdapResult.cs new file mode 100644 index 00000000..f565bbcf --- /dev/null +++ b/src/CommonLib/LdapResult.cs @@ -0,0 +1,30 @@ +using System; + +namespace SharpHoundCommonLib { + public class LdapResult : Result + { + public string QueryInfo { get; set; } + public int ErrorCode { get; set; } + + protected LdapResult(T value, bool success, string error, string queryInfo, int errorCode) : base(value, success, error) { + QueryInfo = queryInfo; + ErrorCode = errorCode; + } + + public new static LdapResult Ok(T value) { + return new LdapResult(value, true, string.Empty, null, 0); + } + + public new static LdapResult Fail() { + return new LdapResult(default, false, string.Empty, null, 0); + } + + public static LdapResult Fail(string message, LdapQueryParameters queryInfo) { + return new LdapResult(default, false, message, queryInfo.GetQueryInfo(), 0); + } + + public static LdapResult Fail(string message, LdapQueryParameters queryInfo, int errorCode) { + return new LdapResult(default, false, message, queryInfo.GetQueryInfo(), errorCode); + } + } +} \ No newline at end of file diff --git a/src/CommonLib/LdapUtils.cs b/src/CommonLib/LdapUtils.cs new file mode 100644 index 00000000..a7e0bf63 --- /dev/null +++ b/src/CommonLib/LdapUtils.cs @@ -0,0 +1,1697 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.DirectoryServices; +using System.DirectoryServices.AccountManagement; +using System.DirectoryServices.ActiveDirectory; +using System.DirectoryServices.Protocols; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Runtime.CompilerServices; +using System.Security.Principal; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using SharpHoundCommonLib.DirectoryObjects; +using SharpHoundCommonLib.Enums; +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 LdapUtils : ILdapUtils { + //This cache is indexed by domain sid + private readonly ConcurrentDictionary _dcInfoCache = new(); + private static readonly ConcurrentDictionary DomainCache = new(); + private static readonly ConcurrentDictionary DomainControllers = new(); + + private static readonly ConcurrentDictionary DomainToForestCache = + new(StringComparer.OrdinalIgnoreCase); + + private static readonly ConcurrentDictionary + SeenWellKnownPrincipals = new(); + + private readonly ConcurrentDictionary + _hostResolutionMap = new(StringComparer.OrdinalIgnoreCase); + + private readonly ConcurrentDictionary _distinguishedNameCache = + new(StringComparer.OrdinalIgnoreCase); + + private readonly ILogger _log; + private readonly PortScanner _portScanner; + private readonly NativeMethods _nativeMethods; + private readonly string _nullCacheKey = Guid.NewGuid().ToString(); + private static readonly Regex SIDRegex = new(@"^(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 static readonly byte[] NameRequest = { + 0x80, 0x94, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x20, 0x43, 0x4b, 0x41, + 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, + 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, + 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, + 0x41, 0x41, 0x41, 0x41, 0x41, 0x00, 0x00, 0x21, + 0x00, 0x01 + }; + + private class ResolvedWellKnownPrincipal { + public string DomainName { get; set; } + public string WkpId { get; set; } + } + + public LdapUtils() { + _nativeMethods = new NativeMethods(); + _portScanner = new PortScanner(); + _log = Logging.LogProvider.CreateLogger("LDAPUtils"); + _connectionPool = new ConnectionPoolManager(_ldapConfig, _log); + } + + public LdapUtils(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 async IAsyncEnumerable> RangedRetrieval(string distinguishedName, + string attributeName, [EnumeratorCancellation] CancellationToken cancellationToken = new()) { + var domain = Helpers.DistinguishedNameToDomain(distinguishedName); + + var connectionResult = await _connectionPool.GetLdapConnection(domain, false); + if (!connectionResult.Success) { + yield return Result.Fail(connectionResult.Message); + yield break; + } + + var index = 0; + var step = 0; + + //Start by using * as our upper index, which will automatically give us the range size + var currentRange = $"{attributeName};range={index}-*"; + var complete = false; + + var queryParameters = new LdapQueryParameters { + DomainName = domain, + LDAPFilter = $"{attributeName}=*", + Attributes = new[] { currentRange }, + SearchScope = SearchScope.Base, + SearchBase = distinguishedName + }; + var connectionWrapper = connectionResult.ConnectionWrapper; + + if (!CreateSearchRequest(queryParameters, connectionWrapper, out var searchRequest)) { + _connectionPool.ReleaseConnection(connectionWrapper); + yield return Result.Fail("Failed to create search request"); + yield break; + } + + var queryRetryCount = 0; + var busyRetryCount = 0; + + LdapResult tempResult = null; + + while (!cancellationToken.IsCancellationRequested) { + SearchResponse response = null; + try { + response = (SearchResponse)connectionWrapper.Connection.SendRequest(searchRequest); + } catch (LdapException le) when (le.ErrorCode == (int)ResultCode.Busy && busyRetryCount < MaxRetries) { + busyRetryCount++; + var backoffDelay = GetNextBackoff(busyRetryCount); + await Task.Delay(backoffDelay, cancellationToken); + } catch (LdapException le) when (le.ErrorCode == (int)LdapErrorCodes.ServerDown && + queryRetryCount < MaxRetries) { + 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(domain, + false); + if (success) { + _log.LogDebug( + "RangedRetrieval - 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( + "RangedRetrieval - Failed to get a new connection after ServerDown for path {Path}", + distinguishedName); + tempResult = + LdapResult.Fail( + "RangedRetrieval - Failed to get a new connection after ServerDown.", + queryParameters, le.ErrorCode); + } + } + } catch (LdapException le) { + tempResult = LdapResult.Fail( + $"Caught unrecoverable ldap exception: {le.Message} (ServerMessage: {le.ServerErrorMessage}) (ErrorCode: {le.ErrorCode})", + queryParameters, le.ErrorCode); + } catch (Exception e) { + tempResult = + LdapResult.Fail($"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 + //We handle connection release in the relevant exception blocks + if (tempResult != null) { + if (tempResult.ErrorCode == (int)LdapErrorCodes.ServerDown) { + _connectionPool.ReleaseConnection(connectionWrapper, true); + } else { + _connectionPool.ReleaseConnection(connectionWrapper); + } + + yield return tempResult; + yield break; + } + + if (response?.Entries.Count == 1) { + var entry = response.Entries[0]; + //We dont know the name of our attribute, but there should only be one, so we're safe to just use a loop here + foreach (string attr in entry.Attributes.AttributeNames) { + currentRange = attr; + complete = currentRange.IndexOf("*", 0, StringComparison.OrdinalIgnoreCase) > 0; + step = entry.Attributes[currentRange].Count; + } + + foreach (string dn in entry.Attributes[currentRange].GetValues(typeof(string))) { + yield return Result.Ok(dn); + index++; + } + + if (complete) { + _connectionPool.ReleaseConnection(connectionWrapper); + yield break; + } + + currentRange = $"{attributeName};range={index}-{index + step}"; + searchRequest.Attributes.Clear(); + searchRequest.Attributes.Add(currentRange); + } else { + //I dont know what can cause a RR to have multiple entries, but its nothing good. Break out + _connectionPool.ReleaseConnection(connectionWrapper); + yield break; + } + } + + _connectionPool.ReleaseConnection(connectionWrapper); + } + + 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) { + _connectionPool.ReleaseConnection(connectionWrapper); + yield break; + } + + var queryRetryCount = 0; + var busyRetryCount = 0; + LdapResult tempResult = null; + var querySuccess = false; + SearchResponse response = null; + while (!cancellationToken.IsCancellationRequested) { + 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($"Query - 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) { + if (tempResult.ErrorCode == (int)LdapErrorCodes.ServerDown) { + _connectionPool.ReleaseConnection(connectionWrapper, true); + } else { + _connectionPool.ReleaseConnection(connectionWrapper); + } + + yield return tempResult; + yield break; + } + + //If we've successfully made our query, break out of the while loop + if (querySuccess) { + break; + } + } + + _connectionPool.ReleaseConnection(connectionWrapper); + foreach (SearchResultEntry entry in response.Entries) { + yield return LdapResult.Ok(new SearchResultEntryWrapper(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 (!cancellationToken.IsCancellationRequested) { + 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.Controls + .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()); + _connectionPool.ReleaseConnection(connectionWrapper, true); + 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()); + tempResult = + LdapResult.Fail("Failed to get a new connection after serverdown", + queryParameters, le.ErrorCode); + } + } + } 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, le.ErrorCode); + } catch (Exception e) { + tempResult = + LdapResult.Fail($"PagedQuery - Caught unrecoverable exception: {e.Message}", + queryParameters); + } + + if (tempResult != null) { + if (tempResult.ErrorCode == (int)LdapErrorCodes.ServerDown) { + _connectionPool.ReleaseConnection(connectionWrapper, true); + } else { + _connectionPool.ReleaseConnection(connectionWrapper); + } + + yield return tempResult; + yield break; + } + + if (cancellationToken.IsCancellationRequested) { + _connectionPool.ReleaseConnection(connectionWrapper); + 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 (SearchResultEntry entry in response.Entries) { + if (cancellationToken.IsCancellationRequested) { + _connectionPool.ReleaseConnection(connectionWrapper); + yield break; + } + + yield return LdapResult.Ok(new SearchResultEntryWrapper(entry)); + } + + if (pageResponse.Cookie.Length == 0 || response.Entries.Count == 0 || + cancellationToken.IsCancellationRequested) { + _connectionPool.ReleaseConnection(connectionWrapper); + yield break; + } + + pageControl.Cookie = pageResponse.Cookie; + } + } + + public async Task<(bool Success, TypedPrincipal Principal)> ResolveIDAndType( + SecurityIdentifier securityIdentifier, + string objectDomain) { + return await ResolveIDAndType(securityIdentifier.Value, objectDomain); + } + + public async Task<(bool Success, TypedPrincipal Principal)> + ResolveIDAndType(string identifier, string objectDomain) { + if (identifier.Contains("0ACNF")) { + return (false, new TypedPrincipal(identifier, Label.Base)); + } + + if (await GetWellKnownPrincipal(identifier, objectDomain) is (true, var principal)) { + return (true, principal); + } + + if (identifier.StartsWith("S-")) { + var result = await LookupSidType(identifier, objectDomain); + return (result.Success, new TypedPrincipal(identifier, result.Type)); + } + + var (success, type) = await LookupGuidType(identifier, objectDomain); + return (success, new TypedPrincipal(identifier, type)); + } + + private async Task<(bool Success, Label Type)> LookupSidType(string sid, string domain) { + if (Cache.GetIDType(sid, out var type)) { + return (true, type); + } + + var tempDomain = domain; + + if (await GetDomainNameFromSid(sid) is (true, var domainName)) { + tempDomain = domainName; + } + + var result = await Query(new LdapQueryParameters() { + DomainName = tempDomain, + LDAPFilter = CommonFilters.SpecificSID(sid), + Attributes = CommonProperties.TypeResolutionProps + }).DefaultIfEmpty(LdapResult.Fail()).FirstOrDefaultAsync(); + + if (result.IsSuccess) { + if (result.Value.GetLabel(out type)) { + Cache.AddType(sid, type); + return (true, type); + } + } + + try { + var entry = CreateDirectoryEntry($"LDAP://"); + if (entry.GetLabel(out type)) { + Cache.AddType(sid, type); + return (true, type); + } + } catch { + //pass + } + + using (var ctx = new PrincipalContext(ContextType.Domain)) { + try { + var principal = Principal.FindByIdentity(ctx, IdentityType.Sid, sid); + if (principal != null) { + var entry = ((DirectoryEntry)principal.GetUnderlyingObject()).ToDirectoryObject(); + if (entry.GetLabel(out type)) { + Cache.AddType(sid, type); + return (true, type); + } + } + } catch { + //pass + } + } + + return (false, Label.Base); + } + + private async Task<(bool Success, Label type)> LookupGuidType(string guid, string domain) { + if (Cache.GetIDType(guid, out var type)) { + return (true, type); + } + + var result = await Query(new LdapQueryParameters() { + DomainName = domain, + LDAPFilter = CommonFilters.SpecificGUID(guid), + Attributes = CommonProperties.TypeResolutionProps + }).DefaultIfEmpty(LdapResult.Fail()).FirstOrDefaultAsync(); + + if (result.IsSuccess && result.Value.GetLabel(out type)) { + Cache.AddType(guid, type); + return (true, type); + } + + try { + var entry = CreateDirectoryEntry($"LDAP://"); + if (entry.GetLabel(out type)) { + Cache.AddType(guid, type); + return (true, type); + } + } catch { + //pass + } + + using (var ctx = new PrincipalContext(ContextType.Domain)) { + try { + var principal = Principal.FindByIdentity(ctx, IdentityType.Guid, guid); + if (principal != null) { + var entry = ((DirectoryEntry)principal.GetUnderlyingObject()).ToDirectoryObject(); + if (entry.GetLabel(out type)) { + Cache.AddType(guid, type); + return (true, type); + } + } + } catch { + //pass + } + } + + return (false, Label.Base); + } + + 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"); + } + + public virtual 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)) { + try { + var forestName = domainObject.Forest.Name.ToUpper(); + DomainToForestCache.TryAdd(domain, forestName); + return (true, forestName); + } catch { + //pass + } + } + + 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).DefaultIfEmpty(LdapResult.Fail()).FirstOrDefaultAsync(); + if (result.IsSuccess && + result.Value.TryGetProperty(LDAPProperties.RootDomainNamingContext, out var rootNamingContext)) { + return (true, Helpers.DistinguishedNameToDomain(rootNamingContext).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, + LdapConnectionWrapper 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); + + if (!string.IsNullOrWhiteSpace(queryParameters.RelativeSearchBase)) { + basePath = $"{queryParameters.RelativeSearchBase},{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, connectionWrapper, out var searchRequest)) { + result.Success = false; + result.Message = "Failed to create search request"; + _connectionPool.ReleaseConnection(connectionWrapper); + return result; + } + + result.Server = connectionWrapper.GetServer(); + result.Success = true; + result.SearchRequest = searchRequest; + result.ConnectionWrapper = connectionWrapper; + return result; + } + + private 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 = CreateDirectoryEntry($"LDAP://"); + if (entry.TryGetDistinguishedName(out var 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); + } + + using (var ctx = new PrincipalContext(ContextType.Domain)) { + try { + var principal = Principal.FindByIdentity(ctx, IdentityType.Sid, sid); + if (principal != null) { + var dn = principal.DistinguishedName; + if (!string.IsNullOrWhiteSpace(dn)) { + Cache.AddDomainSidMapping(domainSid, Helpers.DistinguishedNameToDomain(dn)); + return (true, Helpers.DistinguishedNameToDomain(dn)); + } + } + } catch { + //pass + } + } + + 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() + }).DefaultIfEmpty(LdapResult.Fail()).FirstOrDefaultAsync(); + + if (result.IsSuccess && result.Value.TryGetDistinguishedName(out var distinguishedName)) { + return (true, Helpers.DistinguishedNameToDomain(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() + }).DefaultIfEmpty(LdapResult.Fail()).FirstOrDefaultAsync(); + + if (result.IsSuccess && result.Value.TryGetDistinguishedName(out distinguishedName)) { + return (true, Helpers.DistinguishedNameToDomain(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() + }).DefaultIfEmpty(LdapResult.Fail()).FirstOrDefaultAsync(); + + if (result.IsSuccess && result.Value.TryGetDistinguishedName(out distinguishedName)) { + return (true, Helpers.DistinguishedNameToDomain(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 = CreateDirectoryEntry($"LDAP://{domainName}"); + //Force load objectsid into the object cache + if (entry.TryGetSecurityIdentifier(out var sid)) { + Cache.AddDomainSidMapping(domainName, sid); + domainSid = sid; + return (true, domainSid); + } + } catch { + //we expect this to fail sometimes + } + + if (GetDomain(domainName, out var domainObject)) + try { + var entry = domainObject.GetDirectoryEntry().ToDirectoryObject(); + if (entry.TryGetSecurityIdentifier(out domainSid)) { + 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() + }).DefaultIfEmpty(LdapResult.Fail()).FirstOrDefaultAsync(); + + if (result.IsSuccess && result.Value.TryGetSecurityIdentifier(out var securityIdentifier)) { + domainSid = new SecurityIdentifier(securityIdentifier).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) { + Logging.Logger.LogDebug("Static GetDomain call failed for domain {DomainName}: {Error}", domainName, + e.Message); + 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; + } + } + + public async Task<(bool Success, TypedPrincipal Principal)> ResolveAccountName(string name, string domain) { + if (string.IsNullOrWhiteSpace(name)) { + return (false, null); + } + + if (Cache.GetPrefixedValue(name, domain, out var id) && Cache.GetIDType(id, out var type)) + return (true, new TypedPrincipal { + ObjectIdentifier = id, + ObjectType = type + }); + + var result = await Query(new LdapQueryParameters() { + DomainName = domain, + Attributes = CommonProperties.TypeResolutionProps, + LDAPFilter = $"(samaccountname={name})" + }).DefaultIfEmpty(LdapResult.Fail()).FirstOrDefaultAsync(); + + if (result.IsSuccess && result.Value.GetObjectIdentifier(out id)) { + result.Value.GetLabel(out type); + Cache.AddPrefixedValue(name, domain, id); + Cache.AddType(id, type); + + var (tempID, _) = await GetWellKnownPrincipalObjectIdentifier(id, domain); + return (true, new TypedPrincipal(tempID, type)); + } + + return (false, null); + } + + public async Task<(bool Success, string SecurityIdentifier)> ResolveHostToSid(string host, string domain) { + //Remove SPN prefixes from the host name so we're working with a clean name + var strippedHost = Helpers.StripServicePrincipalName(host).ToUpper().TrimEnd('$'); + if (string.IsNullOrEmpty(strippedHost)) { + return (false, string.Empty); + } + + if (_hostResolutionMap.TryGetValue(strippedHost, out var sid)) return (true, sid); + + //Immediately start with NetWkstaGetInfo as it's our most reliable indicator if successful + if (await GetWorkstationInfo(strippedHost) is (true, var workstationInfo)) { + var tempName = workstationInfo.ComputerName; + var tempDomain = workstationInfo.LanGroup; + + if (string.IsNullOrWhiteSpace(tempDomain)) { + tempDomain = domain; + } + + if (!string.IsNullOrWhiteSpace(tempName)) { + tempName = $"{tempName}$".ToUpper(); + if (await ResolveAccountName(tempName, tempDomain) is (true, var principal)) { + _hostResolutionMap.TryAdd(strippedHost, principal.ObjectIdentifier); + return (true, principal.ObjectIdentifier); + } + } + } + + //Try some socket magic to get the NETBIOS name + if (RequestNETBIOSNameFromComputer(strippedHost, domain, out var netBiosName)) { + if (!string.IsNullOrWhiteSpace(netBiosName)) { + var result = await ResolveAccountName($"{netBiosName}$", domain); + if (result.Success) { + _hostResolutionMap.TryAdd(strippedHost, result.Principal.ObjectIdentifier); + return (true, result.Principal.ObjectIdentifier); + } + } + } + + //Start by handling non-IP address names + if (!IPAddress.TryParse(strippedHost, out _)) { + //PRIMARY.TESTLAB.LOCAL + if (strippedHost.Contains(".")) { + var split = strippedHost.Split('.'); + var name = split[0]; + var result = await ResolveAccountName($"{name}$", domain); + if (result.Success) { + _hostResolutionMap.TryAdd(strippedHost, result.Principal.ObjectIdentifier); + return (true, result.Principal.ObjectIdentifier); + } + + var tempDomain = string.Join(".", split.Skip(1).ToArray()); + result = await ResolveAccountName($"{name}$", tempDomain); + if (result.Success) { + _hostResolutionMap.TryAdd(strippedHost, result.Principal.ObjectIdentifier); + return (true, result.Principal.ObjectIdentifier); + } + } else { + //Format: WIN10 (probably a netbios name) + var result = await ResolveAccountName($"{strippedHost}$", domain); + if (result.Success) { + _hostResolutionMap.TryAdd(strippedHost, result.Principal.ObjectIdentifier); + return (true, result.Principal.ObjectIdentifier); + } + } + } + + try { + var resolvedHostname = (await Dns.GetHostEntryAsync(strippedHost)).HostName; + var split = resolvedHostname.Split('.'); + var name = split[0]; + var result = await ResolveAccountName($"{name}$", domain); + if (result.Success) { + _hostResolutionMap.TryAdd(strippedHost, result.Principal.ObjectIdentifier); + return (true, result.Principal.ObjectIdentifier); + } + + var tempDomain = string.Join(".", split.Skip(1).ToArray()); + result = await ResolveAccountName($"{name}$", tempDomain); + if (result.Success) { + _hostResolutionMap.TryAdd(strippedHost, result.Principal.ObjectIdentifier); + return (true, result.Principal.ObjectIdentifier); + } + } catch { + //pass + } + + return (false, ""); + } + + /// + /// Calls the NetWkstaGetInfo API on a hostname + /// + /// + /// + private async Task<(bool Success, NetAPIStructs.WorkstationInfo100 Info)> GetWorkstationInfo(string hostname) { + if (!await _portScanner.CheckPort(hostname)) + return (false, default); + + var result = _nativeMethods.CallNetWkstaGetInfo(hostname); + if (result.IsSuccess) return (true, result.Value); + + return (false, default); + } + + public async Task<(bool Success, string[] Sids)> GetGlobalCatalogMatches(string name, string domain) { + if (Cache.GetGCCache(name, out var matches)) { + return (true, matches); + } + + var sids = new List(); + + await foreach (var result in Query(new LdapQueryParameters { + DomainName = domain, + Attributes = new[] { LDAPProperties.ObjectSID }, + GlobalCatalog = true, + LDAPFilter = new LdapFilter().AddUsers($"(samaccountname={name})").GetFilter() + })) { + if (result.IsSuccess && result.Value.TryGetSecurityIdentifier(out var sid)) { + if (await GetWellKnownPrincipal(sid, domain) is (true, var principal)) { + sids.Add(principal.ObjectIdentifier); + } else { + sids.Add(sid); + } + } else { + return (false, Array.Empty()); + } + } + + Cache.AddGCCache(name, sids.ToArray()); + return (true, sids.ToArray()); + } + + public async Task<(bool Success, TypedPrincipal Principal)> ResolveCertTemplateByProperty(string propertyValue, + string propertyName, string domainName) { + var filter = new LdapFilter().AddCertificateTemplates() + .AddFilter($"({propertyName}={propertyValue})", true); + var result = await Query(new LdapQueryParameters { + DomainName = domainName, + Attributes = CommonProperties.TypeResolutionProps, + SearchScope = SearchScope.OneLevel, + NamingContext = NamingContext.Configuration, + RelativeSearchBase = DirectoryPaths.CertTemplateLocation, + LDAPFilter = filter.GetFilter(), + }).DefaultIfEmpty(LdapResult.Fail()).FirstOrDefaultAsync(); + + if (!result.IsSuccess) { + _log.LogWarning( + "Could not find certificate template with {PropertyName}:{PropertyValue}: {Error}", + propertyName, propertyValue, result.Error); + return (false, null); + } + + if (result.Value.TryGetGuid(out var guid)) { + return (true, new TypedPrincipal(guid, Label.CertTemplate)); + } + + return (false, default); + } + + /// + /// Uses a socket and a set of bytes to request the NETBIOS name from a remote computer + /// + /// + /// + /// + /// + private static bool RequestNETBIOSNameFromComputer(string server, string domain, out string netbios) { + var receiveBuffer = new byte[1024]; + var requestSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + try { + //Set receive timeout to 1 second + requestSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, 1000); + EndPoint remoteEndpoint; + + //We need to create an endpoint to bind too. If its an IP, just use that. + if (IPAddress.TryParse(server, out var parsedAddress)) + remoteEndpoint = new IPEndPoint(parsedAddress, 137); + else + //If its not an IP, we're going to try and resolve it from DNS + try { + IPAddress address; + if (server.Contains(".")) + address = Dns + .GetHostAddresses(server).First(x => x.AddressFamily == AddressFamily.InterNetwork); + else + address = Dns.GetHostAddresses($"{server}.{domain}")[0]; + + if (address == null) { + netbios = null; + return false; + } + + remoteEndpoint = new IPEndPoint(address, 137); + } catch { + //Failed to resolve an IP, so return null + netbios = null; + return false; + } + + var originEndpoint = new IPEndPoint(IPAddress.Any, 0); + requestSocket.Bind(originEndpoint); + + try { + requestSocket.SendTo(NameRequest, remoteEndpoint); + var receivedByteCount = requestSocket.ReceiveFrom(receiveBuffer, ref remoteEndpoint); + if (receivedByteCount >= 90) { + netbios = new ASCIIEncoding().GetString(receiveBuffer, 57, 16).Trim('\0', ' '); + return true; + } + + netbios = null; + return false; + } catch (SocketException) { + netbios = null; + return false; + } + } finally { + //Make sure we close the socket if its open + requestSocket.Close(); + } + } + + /// + /// Created for testing purposes + /// + /// + public ActiveDirectorySecurityDescriptor MakeSecurityDescriptor() { + return new ActiveDirectorySecurityDescriptor(new ActiveDirectorySecurity()); + } + + public async Task<(bool Success, TypedPrincipal Principal)> ConvertLocalWellKnownPrincipal( + SecurityIdentifier sid, + string computerDomainSid, string computerDomain) { + if (!WellKnownPrincipal.GetWellKnownPrincipal(sid.Value, out var common)) return (false, null); + //The "Everyone" and "Authenticated Users" principals are special and will be converted to the domain equivalent + if (sid.Value is "S-1-1-0" or "S-1-5-11") { + return await GetWellKnownPrincipal(sid.Value, computerDomain); + } + + //Use the computer object id + the RID of the sid we looked up to create our new principal + var principal = new TypedPrincipal { + ObjectIdentifier = $"{computerDomainSid}-{sid.Rid()}", + ObjectType = common.ObjectType switch { + Label.User => Label.LocalUser, + Label.Group => Label.LocalGroup, + _ => common.ObjectType + } + }; + + return (true, principal); + } + + public async Task IsDomainController(string computerObjectId, string domainName) { + if (DomainControllers.ContainsKey(computerObjectId)) { + return true; + } + var resDomain = await GetDomainNameFromSid(domainName) is (false, var tempDomain) ? tempDomain : domainName; + var filter = new LdapFilter().AddFilter(CommonFilters.SpecificSID(computerObjectId), true) + .AddFilter(CommonFilters.DomainControllers, true); + var result = await Query(new LdapQueryParameters() { + DomainName = resDomain, + Attributes = CommonProperties.ObjectID, + LDAPFilter = filter.GetFilter(), + }).DefaultIfEmpty(LdapResult.Fail()).FirstOrDefaultAsync(); + if (result.IsSuccess) { + DomainControllers.TryAdd(computerObjectId, new byte()); + } + return result.IsSuccess; + } + + public async Task<(bool Success, TypedPrincipal Principal)> ResolveDistinguishedName(string distinguishedName) { + if (_distinguishedNameCache.TryGetValue(distinguishedName, out var principal)) { + return (true, principal); + } + + var domain = Helpers.DistinguishedNameToDomain(distinguishedName); + var result = await Query(new LdapQueryParameters { + DomainName = domain, + Attributes = CommonProperties.TypeResolutionProps, + SearchBase = distinguishedName, + SearchScope = SearchScope.Base, + LDAPFilter = new LdapFilter().AddAllObjects().GetFilter() + }).DefaultIfEmpty(LdapResult.Fail()).FirstOrDefaultAsync(); + + if (result.IsSuccess && result.Value.GetObjectIdentifier(out var id)) { + var entry = result.Value; + + if (await GetWellKnownPrincipal(id, domain) is (true, var wellKnownPrincipal)) { + _distinguishedNameCache.TryAdd(distinguishedName, wellKnownPrincipal); + return (true, wellKnownPrincipal); + } + + entry.GetLabel(out var type); + principal = new TypedPrincipal(id, type); + _distinguishedNameCache.TryAdd(distinguishedName, principal); + return (true, principal); + } + + using (var ctx = new PrincipalContext(ContextType.Domain)) { + try { + var lookupPrincipal = + Principal.FindByIdentity(ctx, IdentityType.DistinguishedName, distinguishedName); + if (lookupPrincipal != null) { + var entry = ((DirectoryEntry)lookupPrincipal.GetUnderlyingObject()).ToDirectoryObject(); + if (entry.GetObjectIdentifier(out var identifier) && entry.GetLabel(out var label)) { + if (await GetWellKnownPrincipal(identifier, domain) is (true, var wellKnownPrincipal)) { + _distinguishedNameCache.TryAdd(distinguishedName, wellKnownPrincipal); + return (true, wellKnownPrincipal); + } + + principal = new TypedPrincipal(identifier, label); + _distinguishedNameCache.TryAdd(distinguishedName, principal); + return (true, new TypedPrincipal(identifier, label)); + } + } + + return (false, default); + } catch { + return (false, default); + } + } + } + + public void AddDomainController(string domainControllerSID) { + DomainControllers.TryAdd(domainControllerSID, new byte()); + } + + public async IAsyncEnumerable GetWellKnownPrincipalOutput() { + foreach (var wkp in SeenWellKnownPrincipals) { + WellKnownPrincipal.GetWellKnownPrincipal(wkp.Value.WkpId, out var principal); + OutputBase output = principal.ObjectType switch { + Label.User => new User(), + Label.Computer => new Computer(), + Label.Group => new OutputTypes.Group(), + Label.GPO => new GPO(), + Label.Domain => new OutputTypes.Domain(), + Label.OU => new OU(), + Label.Container => new Container(), + Label.Configuration => new Container(), + _ => throw new ArgumentOutOfRangeException() + }; + + output.Properties.Add("name", $"{principal.ObjectIdentifier}@{wkp.Value.DomainName}".ToUpper()); + if (await GetDomainSidFromDomainName(wkp.Value.DomainName) is (true, var sid)) { + output.Properties.Add("domainsid", sid); + } + + output.Properties.Add("domain", wkp.Value.DomainName.ToUpper()); + output.ObjectIdentifier = wkp.Key; + yield return output; + } + } + + public void SetLdapConfig(LdapConfig config) { + _ldapConfig = config; + _connectionPool.Dispose(); + _connectionPool = new ConnectionPoolManager(_ldapConfig, scanner: _portScanner); + } + + public Task<(bool Success, string Message)> TestLdapConnection(string domain) { + return _connectionPool.TestDomainConnection(domain, false); + } + + public async Task<(bool Success, string Path)> GetNamingContextPath(string domain, NamingContext context) { + if (await _connectionPool.GetLdapConnection(domain, false) is (true, var wrapper, _)) { + _connectionPool.ReleaseConnection(wrapper); + if (wrapper.GetSearchBase(context, out var searchBase)) { + return (true, searchBase); + } + } + + var property = context switch { + NamingContext.Default => LDAPProperties.DefaultNamingContext, + NamingContext.Configuration => LDAPProperties.ConfigurationNamingContext, + NamingContext.Schema => LDAPProperties.SchemaNamingContext, + _ => throw new ArgumentOutOfRangeException(nameof(context), context, null) + }; + + try { + var entry = CreateDirectoryEntry($"LDAP://{domain}/RootDSE"); + if (entry.TryGetProperty(property, out var searchBase)) { + return (true, searchBase); + } + } catch { + //pass + } + + if (GetDomain(domain, out var domainObj)) { + try { + var entry = domainObj.GetDirectoryEntry().ToDirectoryObject(); + if (entry.TryGetProperty(property, out var searchBase)) { + return (true, searchBase); + } + } catch { + //pass + } + + var name = domainObj.Name; + if (!string.IsNullOrWhiteSpace(name)) { + var tempPath = Helpers.DomainNameToDistinguishedName(name); + + var searchBase = context switch { + NamingContext.Configuration => $"CN=Configuration,{tempPath}", + NamingContext.Schema => $"CN=Schema,CN=Configuration,{tempPath}", + NamingContext.Default => tempPath, + _ => throw new ArgumentOutOfRangeException() + }; + + return (true, searchBase); + } + } + + return (false, default); + } + + private IDirectoryObject CreateDirectoryEntry(string path) { + if (_ldapConfig.Username != null) { + return new DirectoryEntry(path, _ldapConfig.Username, _ldapConfig.Password).ToDirectoryObject(); + } + + return new DirectoryEntry(path).ToDirectoryObject(); + } + + public void Dispose() { + _connectionPool?.Dispose(); + } + + internal static bool ResolveLabel(string objectIdentifier, string distinguishedName, string samAccountType, + string[] objectClasses, int flags, out Label type) { + type = Label.Base; + if (objectIdentifier != null && + WellKnownPrincipal.GetWellKnownPrincipal(objectIdentifier, out var principal)) { + type = principal.ObjectType; + return true; + } + + //Override GMSA/MSA account to treat them as users for the graph + if (objectClasses != null && + (objectClasses.Contains(ObjectClass.MSAClass, StringComparer.OrdinalIgnoreCase) || + objectClasses.Contains(ObjectClass.GMSAClass, StringComparer.OrdinalIgnoreCase))) { + type = Label.User; + return true; + } + + if (samAccountType != null) { + var objectType = Helpers.SamAccountTypeToType(samAccountType); + if (objectType != Label.Base) { + type = objectType; + return true; + } + } + + if (objectClasses == null || objectClasses.Length == 0) { + type = Label.Base; + return false; + } + + if (objectClasses.Contains(ObjectClass.GroupPolicyContainerClass, StringComparer.OrdinalIgnoreCase)) + type = Label.GPO; + else if (objectClasses.Contains(ObjectClass.OrganizationalUnitClass, StringComparer.OrdinalIgnoreCase)) + type = Label.OU; + else if (objectClasses.Contains(ObjectClass.DomainClass, StringComparer.OrdinalIgnoreCase)) + type = Label.Domain; + else if (objectClasses.Contains(ObjectClass.ContainerClass, StringComparer.OrdinalIgnoreCase)) + type = Label.Container; + else if (objectClasses.Contains(ObjectClass.ConfigurationClass, StringComparer.OrdinalIgnoreCase)) + type = Label.Configuration; + else if (objectClasses.Contains(ObjectClass.PKICertificateTemplateClass, StringComparer.OrdinalIgnoreCase)) + type = Label.CertTemplate; + else if (objectClasses.Contains(ObjectClass.PKIEnrollmentServiceClass, StringComparer.OrdinalIgnoreCase)) + type = Label.EnterpriseCA; + else if (objectClasses.Contains(ObjectClass.CertificationAuthorityClass, + StringComparer.OrdinalIgnoreCase)) { + if (distinguishedName.IndexOf(DirectoryPaths.RootCALocation, StringComparison.OrdinalIgnoreCase) > 0) + type = Label.RootCA; + if (distinguishedName.IndexOf(DirectoryPaths.AIACALocation, StringComparison.OrdinalIgnoreCase) > 0) + type = Label.AIACA; + if (distinguishedName.IndexOf(DirectoryPaths.NTAuthStoreLocation, StringComparison.OrdinalIgnoreCase) > + 0) + type = Label.NTAuthStore; + } else if (objectClasses.Contains(ObjectClass.OIDContainerClass, StringComparer.OrdinalIgnoreCase)) { + if (distinguishedName.StartsWith(DirectoryPaths.OIDContainerLocation, + StringComparison.OrdinalIgnoreCase)) + type = Label.Container; + else if (flags == 2) { + type = Label.IssuancePolicy; + } + } + + return type != Label.Base; + } + + public static async Task<(bool Success, ResolvedSearchResult ResolvedResult)> ResolveSearchResult( + IDirectoryObject directoryObject, ILdapUtils utils) { + if (!directoryObject.GetObjectIdentifier(out var objectIdentifier)) { + return (false, default); + } + + var res = new ResolvedSearchResult { + ObjectId = objectIdentifier + }; + + //If the object is deleted, we can short circuit the rest of this logic as we don't really care about anything else + if (directoryObject.IsDeleted()) { + res.Deleted = true; + return (true, res); + } + + if (directoryObject.TryGetIntProperty(LDAPProperties.UserAccountControl, out var rawUac)) { + var flags = (UacFlags)rawUac; + if (flags.HasFlag(UacFlags.ServerTrustAccount)) { + res.IsDomainController = true; + utils.AddDomainController(objectIdentifier); + } + } + + string domain; + + if (directoryObject.TryGetDistinguishedName(out var distinguishedName)) { + domain = Helpers.DistinguishedNameToDomain(distinguishedName); + } else { + if (objectIdentifier.StartsWith("S-1-5") && + await utils.GetDomainNameFromSid(objectIdentifier) is (true, var domainName)) { + domain = domainName; + } else { + return (false, default); + } + } + + string domainSid; + var match = SIDRegex.Match(objectIdentifier); + if (match.Success) { + domainSid = match.Groups[1].Value; + } else if (await utils.GetDomainSidFromDomainName(domain) is (true, var sid)) { + domainSid = sid; + } else { + Logging.Logger.LogWarning("Failed to resolve domain sid for object {Identifier}", objectIdentifier); + domainSid = null; + } + + res.Domain = domain; + res.DomainSid = domainSid; + + if (WellKnownPrincipal.GetWellKnownPrincipal(objectIdentifier, out var wellKnownPrincipal)) { + res.DisplayName = $"{wellKnownPrincipal.ObjectIdentifier}@{domain}"; + res.ObjectType = wellKnownPrincipal.ObjectType; + if (await utils.GetWellKnownPrincipal(objectIdentifier, domain) is (true, var convertedPrincipal)) { + res.ObjectId = convertedPrincipal.ObjectIdentifier; + } + + return (true, res); + } + + if (!directoryObject.GetLabel(out var label)) { + if (await utils.ResolveIDAndType(objectIdentifier, domain) is (true, var typedPrincipal)) { + label = typedPrincipal.ObjectType; + } + } + + if (directoryObject.IsMSA() || directoryObject.IsGMSA()) { + label = Label.User; + } + + res.ObjectType = label; + + directoryObject.TryGetProperty(LDAPProperties.SAMAccountName, out var samAccountName); + + switch (label) { + case Label.User: + case Label.Group: + case Label.Base: + res.DisplayName = $"{samAccountName}@{domain}"; + break; + case Label.Computer: { + var shortName = samAccountName?.TrimEnd('$'); + if (directoryObject.TryGetProperty(LDAPProperties.DNSHostName, out var dns)) { + res.DisplayName = dns; + } else if (!string.IsNullOrWhiteSpace(shortName)) { + res.DisplayName = $"{shortName}.{domain}"; + } else if (directoryObject.TryGetProperty(LDAPProperties.CanonicalName, + out var canonicalName)) { + res.DisplayName = $"{canonicalName}.{domain}"; + } else if (directoryObject.TryGetProperty(LDAPProperties.Name, out var name)) { + res.DisplayName = $"{name}.{domain}"; + } else { + res.DisplayName = $"UNKNOWN.{domain}"; + } + + break; + } + case Label.GPO: + case Label.IssuancePolicy: { + if (directoryObject.TryGetProperty(LDAPProperties.DisplayName, out var displayName)) { + res.DisplayName = $"{displayName}@{domain}"; + } else if (directoryObject.TryGetProperty(LDAPProperties.CanonicalName, + out var canonicalName)) { + res.DisplayName = $"{canonicalName}@{domain}"; + } else { + res.DisplayName = $"UNKNOWN@{domain}"; + } + + break; + } + case Label.Domain: + res.DisplayName = domain; + break; + case Label.OU: { + if (directoryObject.TryGetProperty(LDAPProperties.Name, out var name)) { + res.DisplayName = $"{name}@{domain}"; + } else if (directoryObject.TryGetProperty(LDAPProperties.OU, out var ou)) { + res.DisplayName = $"{ou}@{domain}"; + } else { + res.DisplayName = $"UNKNOWN@{domain}"; + } + + break; + } + case Label.Container: { + if (directoryObject.TryGetProperty(LDAPProperties.Name, out var name)) { + res.DisplayName = $"{name}@{domain}"; + } else if (directoryObject.TryGetProperty(LDAPProperties.CanonicalName, + out var canonicalName)) { + res.DisplayName = $"{canonicalName}@{domain}"; + } else { + res.DisplayName = $"UNKNOWN@{domain}"; + } + + break; + } + case Label.Configuration: + case Label.RootCA: + case Label.AIACA: + case Label.NTAuthStore: + case Label.EnterpriseCA: + case Label.CertTemplate: { + if (directoryObject.TryGetProperty(LDAPProperties.Name, out var name)) { + res.DisplayName = $"{name}@{domain}"; + } else { + res.DisplayName = $"UNKNOWN@{domain}"; + } + + break; + } + default: + throw new ArgumentOutOfRangeException(); + } + + res.DisplayName = res.DisplayName.ToUpper(); + return (true, res); + } + } +} \ No newline at end of file diff --git a/src/CommonLib/Logging.cs b/src/CommonLib/Logging/Logging.cs similarity index 100% rename from src/CommonLib/Logging.cs rename to src/CommonLib/Logging/Logging.cs diff --git a/src/CommonLib/NoOpLogger.cs b/src/CommonLib/Logging/NoOpLogger.cs similarity index 100% rename from src/CommonLib/NoOpLogger.cs rename to src/CommonLib/Logging/NoOpLogger.cs diff --git a/src/CommonLib/PassThroughLogger.cs b/src/CommonLib/Logging/PassThroughLogger.cs similarity index 100% rename from src/CommonLib/PassThroughLogger.cs rename to src/CommonLib/Logging/PassThroughLogger.cs diff --git a/src/CommonLib/NativeMethods.cs b/src/CommonLib/NativeMethods.cs index fb0c4c69..96d2e5a5 100644 --- a/src/CommonLib/NativeMethods.cs +++ b/src/CommonLib/NativeMethods.cs @@ -32,9 +32,13 @@ public virtual NetAPIResult> NetWkstaUserEn } public virtual NetAPIResult CallDsGetDcName(string computerName, - string domainName) + string domainName, uint flags) { - return NetAPIMethods.DsGetDcName(computerName, domainName); + return NetAPIMethods.DsGetDcName(computerName, domainName, flags); + } + + public virtual NetAPIResult CallNetWkstaGetInfo(string serverName) { + return NetAPIMethods.NetWkstaGetInfo(serverName); } } } \ No newline at end of file diff --git a/src/CommonLib/OutputTypes/ACE.cs b/src/CommonLib/OutputTypes/ACE.cs index 649442b5..9d62d88b 100644 --- a/src/CommonLib/OutputTypes/ACE.cs +++ b/src/CommonLib/OutputTypes/ACE.cs @@ -1,39 +1,33 @@ using SharpHoundCommonLib.Enums; -namespace SharpHoundCommonLib.OutputTypes -{ - public class ACE - { +namespace SharpHoundCommonLib.OutputTypes { + public class ACE { public string PrincipalSID { get; set; } public Label PrincipalType { get; set; } public string RightName { get; set; } public bool IsInherited { get; set; } + public string InheritanceHash { get; set; } - public override string ToString() - { + public override string ToString() { return $"{PrincipalType} {PrincipalSID} - {RightName} {(IsInherited ? "" : "Not")} Inherited"; } - protected bool Equals(ACE other) - { + protected bool Equals(ACE other) { return PrincipalSID == other.PrincipalSID && PrincipalType == other.PrincipalType && RightName == other.RightName && IsInherited == other.IsInherited; } - public override bool Equals(object obj) - { + public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; if (obj.GetType() != GetType()) return false; - return Equals((ACE) obj); + return Equals((ACE)obj); } - public override int GetHashCode() - { - unchecked - { + public override int GetHashCode() { + unchecked { var hashCode = PrincipalSID != null ? PrincipalSID.GetHashCode() : 0; - hashCode = (hashCode * 397) ^ (int) PrincipalType; + hashCode = (hashCode * 397) ^ (int)PrincipalType; hashCode = (hashCode * 397) ^ (RightName != null ? RightName.GetHashCode() : 0); hashCode = (hashCode * 397) ^ IsInherited.GetHashCode(); return hashCode; diff --git a/src/CommonLib/OutputTypes/Computer.cs b/src/CommonLib/OutputTypes/Computer.cs index a39a2903..2fbcfcf5 100644 --- a/src/CommonLib/OutputTypes/Computer.cs +++ b/src/CommonLib/OutputTypes/Computer.cs @@ -35,7 +35,7 @@ public class ComputerStatus public string Error { get; set; } public static string NonWindowsOS => "NonWindowsOS"; - public static string OldPwd => "PwdLastSetOutOfRange"; + public static string NotActive => "NotActive"; public static string PortNotOpen => "PortNotOpen"; public static string Success => "Success"; diff --git a/src/CommonLib/OutputTypes/Container.cs b/src/CommonLib/OutputTypes/Container.cs index 09f8d95b..d72cfe27 100644 --- a/src/CommonLib/OutputTypes/Container.cs +++ b/src/CommonLib/OutputTypes/Container.cs @@ -5,5 +5,6 @@ namespace SharpHoundCommonLib.OutputTypes public class Container : OutputBase { public TypedPrincipal[] ChildObjects { get; set; } = Array.Empty(); + public string[] InheritanceHashes { get; set; } = Array.Empty(); } } \ No newline at end of file diff --git a/src/CommonLib/OutputTypes/Domain.cs b/src/CommonLib/OutputTypes/Domain.cs index 0bad9c4c..d6621fe3 100644 --- a/src/CommonLib/OutputTypes/Domain.cs +++ b/src/CommonLib/OutputTypes/Domain.cs @@ -8,5 +8,7 @@ public class Domain : OutputBase public TypedPrincipal[] ChildObjects { get; set; } = Array.Empty(); public DomainTrust[] Trusts { get; set; } = Array.Empty(); public GPLink[] Links { get; set; } = Array.Empty(); + public string[] InheritanceHashes { get; set; } = Array.Empty(); + public string ForestRootIdentifier { get; set; } } } \ No newline at end of file diff --git a/src/CommonLib/OutputTypes/MetaTag.cs b/src/CommonLib/OutputTypes/MetaTag.cs index c11a4653..91158035 100644 --- a/src/CommonLib/OutputTypes/MetaTag.cs +++ b/src/CommonLib/OutputTypes/MetaTag.cs @@ -9,5 +9,6 @@ public class MetaTag [DataMember(Name = "type")] public string DataType { get; set; } [DataMember(Name = "count")] public long Count { get; set; } [DataMember(Name = "version")] public int Version { get; set; } + [DataMember(Name = "collectorversion")] public string CollectorVersion { get; set; } } } \ No newline at end of file diff --git a/src/CommonLib/OutputTypes/OU.cs b/src/CommonLib/OutputTypes/OU.cs index 2bb3e55f..87f1f9d8 100644 --- a/src/CommonLib/OutputTypes/OU.cs +++ b/src/CommonLib/OutputTypes/OU.cs @@ -7,5 +7,6 @@ public class OU : OutputBase public ResultingGPOChanges GPOChanges = new(); public GPLink[] Links { get; set; } = Array.Empty(); public TypedPrincipal[] ChildObjects { get; set; } = Array.Empty(); + public string[] InheritanceHashes { get; set; } = Array.Empty(); } } \ No newline at end of file diff --git a/src/CommonLib/Processors/ACLProcessor.cs b/src/CommonLib/Processors/ACLProcessor.cs index 815b30bc..9eb72744 100644 --- a/src/CommonLib/Processors/ACLProcessor.cs +++ b/src/CommonLib/Processors/ACLProcessor.cs @@ -3,88 +3,75 @@ using System.Collections.Generic; using System.DirectoryServices; using System.Security.AccessControl; +using System.Security.Cryptography; using System.Security.Principal; +using System.Text; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using SharpHoundCommonLib.DirectoryObjects; using SharpHoundCommonLib.Enums; using SharpHoundCommonLib.OutputTypes; -using SearchScope = System.DirectoryServices.Protocols.SearchScope; -namespace SharpHoundCommonLib.Processors -{ - public class ACLProcessor - { +namespace SharpHoundCommonLib.Processors { + public class ACLProcessor { private static readonly Dictionary BaseGuids; private static readonly ConcurrentDictionary GuidMap = new(); - private static bool _isCacheBuilt; private readonly ILogger _log; - private readonly ILDAPUtils _utils; + private readonly ILdapUtils _utils; + private static readonly HashSet BuiltDomainCaches = new(StringComparer.OrdinalIgnoreCase); - static ACLProcessor() - { + static ACLProcessor() { //Create a dictionary with the base GUIDs of each object type - BaseGuids = new Dictionary - { - {Label.User, "bf967aba-0de6-11d0-a285-00aa003049e2"}, - {Label.Computer, "bf967a86-0de6-11d0-a285-00aa003049e2"}, - {Label.Group, "bf967a9c-0de6-11d0-a285-00aa003049e2"}, - {Label.Domain, "19195a5a-6da0-11d0-afd3-00c04fd930c9"}, - {Label.GPO, "f30e3bc2-9ff0-11d1-b603-0000f80367c1"}, - {Label.OU, "bf967aa5-0de6-11d0-a285-00aa003049e2"}, - {Label.Container, "bf967a8b-0de6-11d0-a285-00aa003049e2"}, - {Label.Configuration, "bf967a87-0de6-11d0-a285-00aa003049e2"}, - {Label.RootCA, "3fdfee50-47f4-11d1-a9c3-0000f80367c1"}, - {Label.AIACA, "3fdfee50-47f4-11d1-a9c3-0000f80367c1"}, - {Label.EnterpriseCA, "ee4aa692-3bba-11d2-90cc-00c04fd91ab1"}, - {Label.NTAuthStore, "3fdfee50-47f4-11d1-a9c3-0000f80367c1"}, - {Label.CertTemplate, "e5209ca2-3bba-11d2-90cc-00c04fd91ab1"}, - {Label.IssuancePolicy, "37cfd85c-6719-4ad8-8f9e-8678ba627563"} + BaseGuids = new Dictionary { + { Label.User, "bf967aba-0de6-11d0-a285-00aa003049e2" }, + { Label.Computer, "bf967a86-0de6-11d0-a285-00aa003049e2" }, + { Label.Group, "bf967a9c-0de6-11d0-a285-00aa003049e2" }, + { Label.Domain, "19195a5a-6da0-11d0-afd3-00c04fd930c9" }, + { Label.GPO, "f30e3bc2-9ff0-11d1-b603-0000f80367c1" }, + { Label.OU, "bf967aa5-0de6-11d0-a285-00aa003049e2" }, + { Label.Container, "bf967a8b-0de6-11d0-a285-00aa003049e2" }, + { Label.Configuration, "bf967a87-0de6-11d0-a285-00aa003049e2" }, + { Label.RootCA, "3fdfee50-47f4-11d1-a9c3-0000f80367c1" }, + { Label.AIACA, "3fdfee50-47f4-11d1-a9c3-0000f80367c1" }, + { Label.EnterpriseCA, "ee4aa692-3bba-11d2-90cc-00c04fd91ab1" }, + { Label.NTAuthStore, "3fdfee50-47f4-11d1-a9c3-0000f80367c1" }, + { Label.CertTemplate, "e5209ca2-3bba-11d2-90cc-00c04fd91ab1" }, + { Label.IssuancePolicy, "37cfd85c-6719-4ad8-8f9e-8678ba627563" } }; } - public ACLProcessor(ILDAPUtils utils, bool noGuidCache = false, ILogger log = null, string domain = null) - { + public ACLProcessor(ILdapUtils utils, ILogger log = null) { _utils = utils; _log = log ?? Logging.LogProvider.CreateLogger("ACLProc"); - if (!noGuidCache) - BuildGUIDCache(domain); } /// /// Builds a mapping of GUID -> Name for LDAP rights. Used for rights that are created using an extended schema such as /// LAPS /// - private void BuildGUIDCache(string domain) - { - if (_isCacheBuilt) - return; - - var forest = _utils.GetForest(domain); - if (forest == null) - { - _log.LogError("BuildGUIDCache - Unable to resolve forest"); - return; - } - - var schema = forest.Schema.Name; - if (string.IsNullOrEmpty(schema)) - { - _log.LogError("BuildGUIDCache - Schema string is null or empty"); - return; - } + private async Task BuildGuidCache(string domain) { + BuiltDomainCaches.Add(domain); + await foreach (var result in _utils.Query(new LdapQueryParameters { + DomainName = domain, + LDAPFilter = "(schemaIDGUID=*)", + NamingContext = NamingContext.Schema, + Attributes = new[] { LDAPProperties.SchemaIDGUID, LDAPProperties.Name }, + })) { + if (result.IsSuccess) { + if (!result.Value.TryGetProperty(LDAPProperties.Name, out var name) || + !result.Value.TryGetGuid(out var guid)) { + continue; + } - _log.LogTrace("Requesting schema from {Schema}", schema); - - foreach (var entry in _utils.QueryLDAP("(schemaIDGUID=*)", SearchScope.Subtree, - new[] {LDAPProperties.SchemaIDGUID, LDAPProperties.Name}, adsPath: schema)) - { - var name = entry.GetProperty(LDAPProperties.Name)?.ToLower(); - var guid = new Guid(entry.GetByteProperty(LDAPProperties.SchemaIDGUID)).ToString(); - GuidMap.TryAdd(guid, name); + name = name.ToLower(); + if (name is LDAPProperties.LAPSPassword or LDAPProperties.LegacyLAPSPassword) { + _log.LogDebug("Found GUID for ACL Right {Name}: {Guid} in domain {Domain}", name, guid, domain); + GuidMap.TryAdd(guid, name); + } + } else { + _log.LogDebug("Error while building GUID cache for {Domain}: {Message}", domain, result.Error); + } } - - _log.LogTrace("BuildGUIDCache - Successfully grabbed schema"); - - _isCacheBuilt = true; } /// @@ -92,10 +79,12 @@ private void BuildGUIDCache(string domain) /// /// /// - public bool IsACLProtected(ISearchResultEntry entry) - { - var ntsd = entry.GetByteProperty(LDAPProperties.SecurityDescriptor); - return IsACLProtected(ntsd); + public bool IsACLProtected(IDirectoryObject entry) { + if (entry.TryGetByteProperty(LDAPProperties.SecurityDescriptor, out var ntSecurityDescriptor)) { + return IsACLProtected(ntSecurityDescriptor); + } + + return false; } /// @@ -103,8 +92,7 @@ public bool IsACLProtected(ISearchResultEntry entry) /// /// /// - public bool IsACLProtected(byte[] ntSecurityDescriptor) - { + public bool IsACLProtected(byte[] ntSecurityDescriptor) { if (ntSecurityDescriptor == null) return false; @@ -120,9 +108,11 @@ public bool IsACLProtected(byte[] ntSecurityDescriptor) /// /// /// - public IEnumerable ProcessACL(ResolvedSearchResult result, ISearchResultEntry searchResult) - { - var descriptor = searchResult.GetByteProperty(LDAPProperties.SecurityDescriptor); + public IAsyncEnumerable ProcessACL(ResolvedSearchResult result, IDirectoryObject searchResult) { + if (!searchResult.TryGetByteProperty(LDAPProperties.SecurityDescriptor, out var descriptor)) { + return AsyncEnumerable.Empty(); + } + var domain = result.Domain; var type = result.ObjectType; var hasLaps = searchResult.HasLAPS(); @@ -131,9 +121,92 @@ public IEnumerable ProcessACL(ResolvedSearchResult result, ISearchResultEnt return ProcessACL(descriptor, domain, type, hasLaps, name); } + internal static string CalculateInheritanceHash(string identityReference, ActiveDirectoryRights rights, + string aceType, string inheritedObjectType) { + var hash = identityReference + rights + aceType + inheritedObjectType; + /* + * We're using MD5 because its fast and this data isn't cryptographically important. + * Additionally, the chances of a collision in our data size is miniscule and irrelevant. + */ + using (var md5 = MD5.Create()) { + var bytes = md5.ComputeHash(Encoding.UTF8.GetBytes(hash)); + var builder = new StringBuilder(); + foreach (var b in bytes) { + builder.Append(b.ToString("x2")); + } + + return builder.ToString(); + } + } + /// - /// Read's the ntSecurityDescriptor from a SearchResultEntry and processes the ACEs in the ACL, filtering out ACEs that - /// BloodHound is not interested in + /// Helper function to get inherited ACE hashes using CommonLib types + /// + /// + /// + /// + public IEnumerable GetInheritedAceHashes(IDirectoryObject directoryObject, + ResolvedSearchResult resolvedSearchResult) { + if (directoryObject.TryGetByteProperty(LDAPProperties.SecurityDescriptor, out var value)) { + return GetInheritedAceHashes(value, resolvedSearchResult.DisplayName); + } + + return Array.Empty(); + } + + /// + /// Gets the hashes for all aces that are pushing inheritance down the tree for later comparison + /// + /// + /// + /// + public IEnumerable GetInheritedAceHashes(byte[] ntSecurityDescriptor, string objectName = "") { + if (ntSecurityDescriptor == null) { + yield break; + } + + _log.LogDebug("Processing Inherited ACE hashes for {Name}", objectName); + var descriptor = _utils.MakeSecurityDescriptor(); + try { + descriptor.SetSecurityDescriptorBinaryForm(ntSecurityDescriptor); + } catch (OverflowException) { + _log.LogWarning( + "Security descriptor on object {Name} exceeds maximum allowable length. Unable to process", + objectName); + yield break; + } + + foreach (var ace in descriptor.GetAccessRules(true, true, typeof(SecurityIdentifier))) { + //Skip all null/deny/inherited aces + if (ace == null || ace.AccessControlType() == AccessControlType.Deny || ace.IsInherited()) { + continue; + } + + var ir = ace.IdentityReference(); + var principalSid = Helpers.PreProcessSID(ir); + + //Skip aces for filtered principals + if (principalSid == null) { + continue; + } + + var iFlags = ace.InheritanceFlags; + if (iFlags == InheritanceFlags.None) { + continue; + } + + var aceRights = ace.ActiveDirectoryRights(); + //Lowercase this just in case. As far as I know it should always come back that way anyways, but better safe than sorry + var aceType = ace.ObjectType().ToString().ToLower(); + var inheritanceType = ace.InheritedObjectType(); + + yield return CalculateInheritanceHash(ir, aceRights, aceType, inheritanceType); + } + } + + /// + /// Read's a raw ntSecurityDescriptor and processes the ACEs in the ACL, filtering out ACEs that + /// BloodHound is not interested in as well as principals we don't care about /// /// /// @@ -141,99 +214,94 @@ public IEnumerable ProcessACL(ResolvedSearchResult result, ISearchResultEnt /// /// /// - public IEnumerable ProcessACL(byte[] ntSecurityDescriptor, string objectDomain, + public async IAsyncEnumerable ProcessACL(byte[] ntSecurityDescriptor, string objectDomain, Label objectType, - bool hasLaps, string objectName = "") - { - if (ntSecurityDescriptor == null) - { + bool hasLaps, string objectName = "") { + if (!BuiltDomainCaches.Contains(objectDomain)) { + await BuildGuidCache(objectDomain); + } + + if (ntSecurityDescriptor == null) { _log.LogDebug("Security Descriptor is null for {Name}", objectName); yield break; } var descriptor = _utils.MakeSecurityDescriptor(); - try - { + try { descriptor.SetSecurityDescriptorBinaryForm(ntSecurityDescriptor); - } - catch (OverflowException) - { + } catch (OverflowException) { _log.LogWarning( "Security descriptor on object {Name} exceeds maximum allowable length. Unable to process", objectName); yield break; } - + + _log.LogDebug("Processing ACL for {ObjectName}", objectName); var ownerSid = Helpers.PreProcessSID(descriptor.GetOwner(typeof(SecurityIdentifier))); - if (ownerSid != null) - { - var resolvedOwner = _utils.ResolveIDAndType(ownerSid, objectDomain); - if (resolvedOwner != null) - yield return new ACE - { + if (ownerSid != null) { + if (await _utils.ResolveIDAndType(ownerSid, objectDomain) is (true, var resolvedOwner)) { + yield return new ACE { PrincipalType = resolvedOwner.ObjectType, PrincipalSID = resolvedOwner.ObjectIdentifier, RightName = EdgeNames.Owns, IsInherited = false }; - } - else - { - _log.LogDebug("Owner is null for {Name}", objectName); - } - - foreach (var ace in descriptor.GetAccessRules(true, true, typeof(SecurityIdentifier))) - { - if (ace == null) - { - _log.LogTrace("Skipping null ACE for {Name}", objectName); - continue; - } - - if (ace.AccessControlType() == AccessControlType.Deny) - { - _log.LogTrace("Skipping deny ACE for {Name}", objectName); - continue; + } else { + _log.LogTrace("Failed to resolve owner for {Name}", objectName); + yield return new ACE { + PrincipalType = Label.Base, + PrincipalSID = ownerSid, + RightName = EdgeNames.Owns, + IsInherited = false + }; } - - if (!ace.IsAceInheritedFrom(BaseGuids[objectType])) - { - _log.LogTrace("Skipping ACE with unmatched GUID/inheritance for {Name}", objectName); + } + + foreach (var ace in descriptor.GetAccessRules(true, true, typeof(SecurityIdentifier))) { + if (ace == null || ace.AccessControlType() == AccessControlType.Deny || !ace.IsAceInheritedFrom(BaseGuids[objectType])) { continue; } var ir = ace.IdentityReference(); var principalSid = Helpers.PreProcessSID(ir); - if (principalSid == null) - { - _log.LogTrace("Pre-Process excluded SID {SID} on {Name}", ir ?? "null", objectName); + //Preprocess returns null if this is an ignored sid + if (principalSid == null) { continue; } - var resolvedPrincipal = _utils.ResolveIDAndType(principalSid, objectDomain); + var (success, resolvedPrincipal) = await _utils.ResolveIDAndType(principalSid, objectDomain); + if (!success) { + _log.LogTrace("Failed to resolve type for principal {Sid} on ACE for {Object}", principalSid, objectName); + resolvedPrincipal.ObjectIdentifier = principalSid; + resolvedPrincipal.ObjectType = Label.Base; + } var aceRights = ace.ActiveDirectoryRights(); //Lowercase this just in case. As far as I know it should always come back that way anyways, but better safe than sorry var aceType = ace.ObjectType().ToString().ToLower(); var inherited = ace.IsInherited(); + var aceInheritanceHash = ""; + if (inherited) { + aceInheritanceHash = CalculateInheritanceHash(ir, aceRights, aceType, ace.InheritedObjectType()); + } + GuidMap.TryGetValue(aceType, out var mappedGuid); _log.LogTrace("Processing ACE with rights {Rights} and guid {GUID} on object {Name}", aceRights, aceType, objectName); //GenericAll applies to every object - if (aceRights.HasFlag(ActiveDirectoryRights.GenericAll)) - { + if (aceRights.HasFlag(ActiveDirectoryRights.GenericAll)) { if (aceType is ACEGuids.AllGuid or "") - yield return new ACE - { + yield return new ACE { PrincipalType = resolvedPrincipal.ObjectType, PrincipalSID = resolvedPrincipal.ObjectIdentifier, IsInherited = inherited, - RightName = EdgeNames.GenericAll + RightName = EdgeNames.GenericAll, + InheritanceHash = aceInheritanceHash }; //This is a special case. If we don't continue here, every other ACE will match because GenericAll includes all other permissions continue; @@ -241,21 +309,21 @@ public IEnumerable ProcessACL(byte[] ntSecurityDescriptor, string objectDom //WriteDACL and WriteOwner are always useful no matter what the object type is as well because they enable all other attacks if (aceRights.HasFlag(ActiveDirectoryRights.WriteDacl)) - yield return new ACE - { + yield return new ACE { PrincipalType = resolvedPrincipal.ObjectType, PrincipalSID = resolvedPrincipal.ObjectIdentifier, IsInherited = inherited, - RightName = EdgeNames.WriteDacl + RightName = EdgeNames.WriteDacl, + InheritanceHash = aceInheritanceHash }; if (aceRights.HasFlag(ActiveDirectoryRights.WriteOwner)) - yield return new ACE - { + yield return new ACE { PrincipalType = resolvedPrincipal.ObjectType, PrincipalSID = resolvedPrincipal.ObjectIdentifier, IsInherited = inherited, - RightName = EdgeNames.WriteOwner + RightName = EdgeNames.WriteOwner, + InheritanceHash = aceInheritanceHash }; //Cool ACE courtesy of @rookuu. Allows a principal to add itself to a group and no one else @@ -263,238 +331,226 @@ public IEnumerable ProcessACL(byte[] ntSecurityDescriptor, string objectDom !aceRights.HasFlag(ActiveDirectoryRights.WriteProperty) && !aceRights.HasFlag(ActiveDirectoryRights.GenericWrite) && objectType == Label.Group && aceType == ACEGuids.WriteMember) - yield return new ACE - { + yield return new ACE { PrincipalType = resolvedPrincipal.ObjectType, PrincipalSID = resolvedPrincipal.ObjectIdentifier, IsInherited = inherited, - RightName = EdgeNames.AddSelf + RightName = EdgeNames.AddSelf, + InheritanceHash = aceInheritanceHash }; //Process object type specific ACEs. Extended rights apply to users, domains, computers, and cert templates - if (aceRights.HasFlag(ActiveDirectoryRights.ExtendedRight)) - { - if (objectType == Label.Domain) - { + if (aceRights.HasFlag(ActiveDirectoryRights.ExtendedRight)) { + if (objectType == Label.Domain) { if (aceType == ACEGuids.DSReplicationGetChanges) - yield return new ACE - { + yield return new ACE { PrincipalType = resolvedPrincipal.ObjectType, PrincipalSID = resolvedPrincipal.ObjectIdentifier, IsInherited = inherited, - RightName = EdgeNames.GetChanges + RightName = EdgeNames.GetChanges, + InheritanceHash = aceInheritanceHash }; else if (aceType == ACEGuids.DSReplicationGetChangesAll) - yield return new ACE - { + yield return new ACE { PrincipalType = resolvedPrincipal.ObjectType, PrincipalSID = resolvedPrincipal.ObjectIdentifier, IsInherited = inherited, - RightName = EdgeNames.GetChangesAll + RightName = EdgeNames.GetChangesAll, + InheritanceHash = aceInheritanceHash }; else if (aceType == ACEGuids.DSReplicationGetChangesInFilteredSet) - yield return new ACE - { + yield return new ACE { PrincipalType = resolvedPrincipal.ObjectType, PrincipalSID = resolvedPrincipal.ObjectIdentifier, IsInherited = inherited, - RightName = EdgeNames.GetChangesInFilteredSet + RightName = EdgeNames.GetChangesInFilteredSet, + InheritanceHash = aceInheritanceHash }; else if (aceType is ACEGuids.AllGuid or "") - yield return new ACE - { + yield return new ACE { PrincipalType = resolvedPrincipal.ObjectType, PrincipalSID = resolvedPrincipal.ObjectIdentifier, IsInherited = inherited, - RightName = EdgeNames.AllExtendedRights + RightName = EdgeNames.AllExtendedRights, + InheritanceHash = aceInheritanceHash }; - } - else if (objectType == Label.User) - { + } else if (objectType == Label.User) { if (aceType == ACEGuids.UserForceChangePassword) - yield return new ACE - { + yield return new ACE { PrincipalType = resolvedPrincipal.ObjectType, PrincipalSID = resolvedPrincipal.ObjectIdentifier, IsInherited = inherited, - RightName = EdgeNames.ForceChangePassword + RightName = EdgeNames.ForceChangePassword, + InheritanceHash = aceInheritanceHash }; else if (aceType is ACEGuids.AllGuid or "") - yield return new ACE - { + yield return new ACE { PrincipalType = resolvedPrincipal.ObjectType, PrincipalSID = resolvedPrincipal.ObjectIdentifier, IsInherited = inherited, - RightName = EdgeNames.AllExtendedRights + RightName = EdgeNames.AllExtendedRights, + InheritanceHash = aceInheritanceHash }; - } - else if (objectType == Label.Computer) - { + } else if (objectType == Label.Computer) { //ReadLAPSPassword is only applicable if the computer actually has LAPS. Check the world readable property ms-mcs-admpwdexpirationtime - if (hasLaps) - { + if (hasLaps) { if (aceType is ACEGuids.AllGuid or "") - yield return new ACE - { + yield return new ACE { PrincipalType = resolvedPrincipal.ObjectType, PrincipalSID = resolvedPrincipal.ObjectIdentifier, IsInherited = inherited, - RightName = EdgeNames.AllExtendedRights + RightName = EdgeNames.AllExtendedRights, + InheritanceHash = aceInheritanceHash }; - else if (mappedGuid is "ms-mcs-admpwd") - yield return new ACE - { + else if (mappedGuid is LDAPProperties.LegacyLAPSPassword or LDAPProperties.LAPSPassword) + yield return new ACE { PrincipalType = resolvedPrincipal.ObjectType, PrincipalSID = resolvedPrincipal.ObjectIdentifier, IsInherited = inherited, - RightName = EdgeNames.ReadLAPSPassword + RightName = EdgeNames.ReadLAPSPassword, + InheritanceHash = aceInheritanceHash }; } - } - else if (objectType == Label.CertTemplate) - { + } else if (objectType == Label.CertTemplate) { if (aceType is ACEGuids.AllGuid or "") - yield return new ACE - { + yield return new ACE { PrincipalType = resolvedPrincipal.ObjectType, PrincipalSID = resolvedPrincipal.ObjectIdentifier, IsInherited = inherited, - RightName = EdgeNames.AllExtendedRights + RightName = EdgeNames.AllExtendedRights, + InheritanceHash = aceInheritanceHash }; else if (aceType is ACEGuids.Enroll) - yield return new ACE - { + yield return new ACE { PrincipalType = resolvedPrincipal.ObjectType, PrincipalSID = resolvedPrincipal.ObjectIdentifier, IsInherited = inherited, - RightName = EdgeNames.Enroll + RightName = EdgeNames.Enroll, + InheritanceHash = aceInheritanceHash }; } } //GenericWrite encapsulates WriteProperty, so process them in tandem to avoid duplicate edges if (aceRights.HasFlag(ActiveDirectoryRights.GenericWrite) || - aceRights.HasFlag(ActiveDirectoryRights.WriteProperty)) - { - if (objectType is Label.User - or Label.Group - or Label.Computer - or Label.GPO - or Label.CertTemplate - or Label.RootCA - or Label.EnterpriseCA - or Label.AIACA - or Label.NTAuthStore + aceRights.HasFlag(ActiveDirectoryRights.WriteProperty)) { + if (objectType is Label.User + or Label.Group + or Label.Computer + or Label.GPO + or Label.CertTemplate + or Label.RootCA + or Label.EnterpriseCA + or Label.AIACA + or Label.NTAuthStore or Label.IssuancePolicy) if (aceType is ACEGuids.AllGuid or "") - yield return new ACE - { + yield return new ACE { PrincipalType = resolvedPrincipal.ObjectType, PrincipalSID = resolvedPrincipal.ObjectIdentifier, IsInherited = inherited, - RightName = EdgeNames.GenericWrite + RightName = EdgeNames.GenericWrite, + InheritanceHash = aceInheritanceHash }; if (objectType == Label.User && aceType == ACEGuids.WriteSPN) - yield return new ACE - { + yield return new ACE { PrincipalType = resolvedPrincipal.ObjectType, PrincipalSID = resolvedPrincipal.ObjectIdentifier, IsInherited = inherited, - RightName = EdgeNames.WriteSPN + RightName = EdgeNames.WriteSPN, + InheritanceHash = aceInheritanceHash }; else if (objectType == Label.Computer && aceType == ACEGuids.WriteAllowedToAct) - yield return new ACE - { + yield return new ACE { PrincipalType = resolvedPrincipal.ObjectType, PrincipalSID = resolvedPrincipal.ObjectIdentifier, IsInherited = inherited, - RightName = EdgeNames.AddAllowedToAct + RightName = EdgeNames.AddAllowedToAct, + InheritanceHash = aceInheritanceHash }; else if (objectType == Label.Computer && aceType == ACEGuids.UserAccountRestrictions) - yield return new ACE - { + yield return new ACE { PrincipalType = resolvedPrincipal.ObjectType, PrincipalSID = resolvedPrincipal.ObjectIdentifier, IsInherited = inherited, - RightName = EdgeNames.WriteAccountRestrictions + RightName = EdgeNames.WriteAccountRestrictions, + InheritanceHash = aceInheritanceHash }; else if (objectType == Label.Group && aceType == ACEGuids.WriteMember) - yield return new ACE - { + yield return new ACE { PrincipalType = resolvedPrincipal.ObjectType, PrincipalSID = resolvedPrincipal.ObjectIdentifier, IsInherited = inherited, - RightName = EdgeNames.AddMember + RightName = EdgeNames.AddMember, + InheritanceHash = aceInheritanceHash }; else if (objectType is Label.User or Label.Computer && aceType == ACEGuids.AddKeyPrincipal) - yield return new ACE - { + yield return new ACE { PrincipalType = resolvedPrincipal.ObjectType, PrincipalSID = resolvedPrincipal.ObjectIdentifier, IsInherited = inherited, - RightName = EdgeNames.AddKeyCredentialLink + RightName = EdgeNames.AddKeyCredentialLink, + InheritanceHash = aceInheritanceHash }; - else if (objectType is Label.CertTemplate) - { + else if (objectType is Label.CertTemplate) { if (aceType == ACEGuids.PKIEnrollmentFlag) - yield return new ACE - { + yield return new ACE { PrincipalType = resolvedPrincipal.ObjectType, PrincipalSID = resolvedPrincipal.ObjectIdentifier, IsInherited = inherited, - RightName = EdgeNames.WritePKIEnrollmentFlag + RightName = EdgeNames.WritePKIEnrollmentFlag, + InheritanceHash = aceInheritanceHash }; else if (aceType == ACEGuids.PKINameFlag) - yield return new ACE - { + yield return new ACE { PrincipalType = resolvedPrincipal.ObjectType, PrincipalSID = resolvedPrincipal.ObjectIdentifier, IsInherited = inherited, - RightName = EdgeNames.WritePKINameFlag + RightName = EdgeNames.WritePKINameFlag, + InheritanceHash = aceInheritanceHash }; } } // EnterpriseCA rights - if (objectType == Label.EnterpriseCA) - { + if (objectType == Label.EnterpriseCA) { if (aceType is ACEGuids.Enroll) - yield return new ACE - { + yield return new ACE { PrincipalType = resolvedPrincipal.ObjectType, PrincipalSID = resolvedPrincipal.ObjectIdentifier, IsInherited = inherited, - RightName = EdgeNames.Enroll + RightName = EdgeNames.Enroll, + InheritanceHash = aceInheritanceHash }; var cARights = (CertificationAuthorityRights)aceRights; // TODO: These if statements are also present in ProcessRegistryEnrollmentPermissions. Move to shared location. if ((cARights & CertificationAuthorityRights.ManageCA) != 0) - yield return new ACE - { + yield return new ACE { PrincipalType = resolvedPrincipal.ObjectType, PrincipalSID = resolvedPrincipal.ObjectIdentifier, IsInherited = inherited, - RightName = EdgeNames.ManageCA + RightName = EdgeNames.ManageCA, + InheritanceHash = aceInheritanceHash }; if ((cARights & CertificationAuthorityRights.ManageCertificates) != 0) - yield return new ACE - { + yield return new ACE { PrincipalType = resolvedPrincipal.ObjectType, PrincipalSID = resolvedPrincipal.ObjectIdentifier, IsInherited = inherited, - RightName = EdgeNames.ManageCertificates + RightName = EdgeNames.ManageCertificates, + InheritanceHash = aceInheritanceHash }; if ((cARights & CertificationAuthorityRights.Enroll) != 0) - yield return new ACE - { + yield return new ACE { PrincipalType = resolvedPrincipal.ObjectType, PrincipalSID = resolvedPrincipal.ObjectIdentifier, IsInherited = inherited, - RightName = EdgeNames.Enroll + RightName = EdgeNames.Enroll, + InheritanceHash = aceInheritanceHash }; } } @@ -506,10 +562,12 @@ or Label.NTAuthStore /// /// /// - public IEnumerable ProcessGMSAReaders(ResolvedSearchResult resolvedSearchResult, - ISearchResultEntry searchResultEntry) - { - var descriptor = searchResultEntry.GetByteProperty(LDAPProperties.GroupMSAMembership); + public IAsyncEnumerable ProcessGMSAReaders(ResolvedSearchResult resolvedSearchResult, + IDirectoryObject searchResultEntry) { + if (!searchResultEntry.TryGetByteProperty(LDAPProperties.GroupMSAMembership, out var descriptor)) { + return AsyncEnumerable.Empty(); + } + var domain = resolvedSearchResult.Domain; var name = resolvedSearchResult.DisplayName; @@ -522,8 +580,7 @@ public IEnumerable ProcessGMSAReaders(ResolvedSearchResult resolvedSearchRe /// /// /// - public IEnumerable ProcessGMSAReaders(byte[] groupMSAMembership, string objectDomain) - { + public IAsyncEnumerable ProcessGMSAReaders(byte[] groupMSAMembership, string objectDomain) { return ProcessGMSAReaders(groupMSAMembership, "", objectDomain); } @@ -535,61 +592,45 @@ public IEnumerable ProcessGMSAReaders(byte[] groupMSAMembership, string obj /// /// /// - public IEnumerable ProcessGMSAReaders(byte[] groupMSAMembership, string objectName, string objectDomain) - { - if (groupMSAMembership == null) - { + public async IAsyncEnumerable ProcessGMSAReaders(byte[] groupMSAMembership, string objectName, + string objectDomain) { + if (groupMSAMembership == null) { _log.LogDebug("GMSA bytes are null for {Name}", objectName); yield break; } var descriptor = _utils.MakeSecurityDescriptor(); - try - { + try { descriptor.SetSecurityDescriptorBinaryForm(groupMSAMembership); - } - catch (OverflowException) - { + } catch (OverflowException) { _log.LogWarning("GMSA ACL length on object {Name} exceeds allowable length. Unable to process", objectName); + yield break; } - - - foreach (var ace in descriptor.GetAccessRules(true, true, typeof(SecurityIdentifier))) - { - if (ace == null) - { - _log.LogTrace("Skipping null GMSA ACE for {Name}", objectName); - continue; - } - - if (ace.AccessControlType() == AccessControlType.Deny) - { - _log.LogTrace("Skipping deny GMSA ACE for {Name}", objectName); + + _log.LogDebug("Processing GMSA Readers for {ObjectName}", objectName); + foreach (var ace in descriptor.GetAccessRules(true, true, typeof(SecurityIdentifier))) { + if (ace == null || ace.AccessControlType() == AccessControlType.Deny) { continue; } var ir = ace.IdentityReference(); var principalSid = Helpers.PreProcessSID(ir); - if (principalSid == null) - { - _log.LogTrace("Pre-Process excluded SID {SID} on {Name}", ir ?? "null", objectName); + if (principalSid == null) { continue; } _log.LogTrace("Processing GMSA ACE with principal {Principal}", principalSid); - var resolvedPrincipal = _utils.ResolveIDAndType(principalSid, objectDomain); - - if (resolvedPrincipal != null) - yield return new ACE - { + if (await _utils.ResolveIDAndType(principalSid, objectDomain) is (true, var resolvedPrincipal)) { + yield return new ACE { RightName = EdgeNames.ReadGMSAPassword, PrincipalType = resolvedPrincipal.ObjectType, PrincipalSID = resolvedPrincipal.ObjectIdentifier, IsInherited = ace.IsInherited() }; + } } } } diff --git a/src/CommonLib/Processors/CertAbuseProcessor.cs b/src/CommonLib/Processors/CertAbuseProcessor.cs index faa93851..667a4218 100644 --- a/src/CommonLib/Processors/CertAbuseProcessor.cs +++ b/src/CommonLib/Processors/CertAbuseProcessor.cs @@ -17,12 +17,12 @@ namespace SharpHoundCommonLib.Processors public class CertAbuseProcessor { private readonly ILogger _log; - public readonly ILDAPUtils _utils; + private readonly ILdapUtils _utils; public delegate Task ComputerStatusDelegate(CSVComputerStatus status); public event ComputerStatusDelegate ComputerStatusEvent; - public CertAbuseProcessor(ILDAPUtils utils, ILogger log = null) + public CertAbuseProcessor(ILdapUtils utils, ILogger log = null) { _utils = utils; _log = log ?? Logging.LogProvider.CreateLogger("CAProc"); @@ -57,16 +57,15 @@ public async Task ProcessRegistryEnrollmentPermissions(str descriptor.SetSecurityDescriptorBinaryForm(aceData.Value as byte[], AccessControlSections.All); var ownerSid = Helpers.PreProcessSID(descriptor.GetOwner(typeof(SecurityIdentifier))); - var computerDomain = _utils.GetDomainNameFromSid(computerObjectId); - var isDomainController = _utils.IsDomainController(computerObjectId, computerDomain); - var machineSid = await GetMachineSid(computerName, computerObjectId, computerDomain, isDomainController); + var isDomainController = await _utils.IsDomainController(computerObjectId, objectDomain); + var machineSid = await GetMachineSid(computerName, computerObjectId); var aces = new List(); - if (ownerSid != null) - { - var resolvedOwner = GetRegistryPrincipal(new SecurityIdentifier(ownerSid), computerDomain, computerName, isDomainController, computerObjectId, machineSid); - if (resolvedOwner != null) + if (ownerSid != null) { + var processed = new SecurityIdentifier(ownerSid); + if (await GetRegistryPrincipal(processed, objectDomain, computerName, + isDomainController, computerObjectId, machineSid) is (true, var resolvedOwner)) { aces.Add(new ACE { PrincipalType = resolvedOwner.ObjectType, @@ -74,6 +73,15 @@ public async Task ProcessRegistryEnrollmentPermissions(str RightName = EdgeNames.Owns, IsInherited = false }); + } else { + aces.Add(new ACE + { + PrincipalType = Label.Base, + PrincipalSID = processed.Value, + RightName = EdgeNames.Owns, + IsInherited = false + }); + } } else { @@ -92,8 +100,14 @@ public async Task ProcessRegistryEnrollmentPermissions(str if (principalSid == null) continue; - var principalDomain = _utils.GetDomainNameFromSid(principalSid) ?? objectDomain; - var resolvedPrincipal = GetRegistryPrincipal(new SecurityIdentifier(principalSid), principalDomain, computerName, isDomainController, computerObjectId, machineSid); + var (getDomainSuccess, principalDomain) = await _utils.GetDomainNameFromSid(principalSid); + if (!getDomainSuccess) { + //Fallback to computer's domain in case we cant resolve the principal domain + principalDomain = objectDomain; + } + var (resSuccess, resolvedPrincipal) = await GetRegistryPrincipal(new SecurityIdentifier(principalSid), principalDomain, computerName, isDomainController, computerObjectId, machineSid); + if (!resSuccess) + continue; var isInherited = rule.IsInherited(); var cARights = (CertificationAuthorityRights)rule.ActiveDirectoryRights(); @@ -153,16 +167,17 @@ public async Task ProcessEAPermissions(string return ret; } - var computerDomain = _utils.GetDomainNameFromSid(computerObjectId); - var isDomainController = _utils.IsDomainController(computerObjectId, computerDomain); - var machineSid = await GetMachineSid(computerName, computerObjectId, computerDomain, isDomainController); - var certTemplatesLocation = _utils.BuildLdapPath(DirectoryPaths.CertTemplateLocation, computerDomain); + var isDomainController = await _utils.IsDomainController(computerObjectId, objectDomain); + var machineSid = await GetMachineSid(computerName, computerObjectId); var descriptor = new RawSecurityDescriptor(regData.Value as byte[], 0); var enrollmentAgentRestrictions = new List(); foreach (var genericAce in descriptor.DiscretionaryAcl) { var ace = (QualifiedAce)genericAce; - enrollmentAgentRestrictions.Add(new EnrollmentAgentRestriction(ace, computerDomain, certTemplatesLocation, this, computerName, isDomainController, computerObjectId, machineSid)); + if (await CreateEnrollmentAgentRestriction(ace, objectDomain, computerName, isDomainController, + computerObjectId, machineSid) is (true, var restriction)) { + enrollmentAgentRestrictions.Add(restriction); + } } ret.Restrictions = enrollmentAgentRestrictions.ToArray(); @@ -170,17 +185,16 @@ public async Task ProcessEAPermissions(string return ret; } - public (IEnumerable resolvedTemplates, IEnumerable unresolvedTemplates) ProcessCertTemplates(string[] templates, string domainName) + public async Task<(IEnumerable resolvedTemplates, IEnumerable unresolvedTemplates)> ProcessCertTemplates(IEnumerable templates, string domainName) { var resolvedTemplates = new List(); - var unresolvedTemplates = new List(); + var unresolvedTemplates = new List(); - var certTemplatesLocation = _utils.BuildLdapPath(DirectoryPaths.CertTemplateLocation, domainName); foreach (var templateCN in templates) { - var res = _utils.ResolveCertTemplateByProperty(Encoder.LdapFilterEncode(templateCN), LDAPProperties.CanonicalName, certTemplatesLocation, domainName); - if (res != null) { - resolvedTemplates.Add(res); + var res = await _utils.ResolveCertTemplateByProperty(Encoder.LdapFilterEncode(templateCN), LDAPProperties.CanonicalName, domainName); + if (res.Success) { + resolvedTemplates.Add(res.Principal); } else { unresolvedTemplates.Add(templateCN); } @@ -289,26 +303,23 @@ public BoolRegistryAPIResult RoleSeparationEnabled(string target, string caName) return ret; } - public TypedPrincipal GetRegistryPrincipal(SecurityIdentifier sid, string computerDomain, string computerName, bool isDomainController, string computerObjectId, SecurityIdentifier machineSid) + public async Task<(bool Success, TypedPrincipal Principal)> GetRegistryPrincipal(SecurityIdentifier sid, string computerDomain, string computerName, bool isDomainController, string computerObjectId, SecurityIdentifier machineSid) { _log.LogTrace("Got principal with sid {SID} on computer {ComputerName}", sid.Value, computerName); //Check if our sid is filtered if (Helpers.IsSidFiltered(sid.Value)) - return null; + return (false, default); - if (isDomainController) - { - var result = _utils.ResolveIDAndType(sid.Value, computerDomain); - if (result != null) - return result; + if (isDomainController && + await _utils.ResolveIDAndType(sid.Value, computerDomain) is (true, var resolvedPrincipal)) { + return (true, resolvedPrincipal); } //If we get a local well known principal, we need to convert it using the computer's domain sid - if (_utils.ConvertLocalWellKnownPrincipal(sid, computerObjectId, computerDomain, out var principal)) - { - _log.LogTrace("Got Well Known Principal {SID} on computer {Computer} with type {Type}", principal.ObjectIdentifier, computerName, principal.ObjectType); - return principal; + if (await _utils.ConvertLocalWellKnownPrincipal(sid, computerObjectId, computerDomain) is + (true, var principal)) { + return (true, principal); } //If the security identifier starts with the machine sid, we need to resolve it as a local principal @@ -317,23 +328,17 @@ public TypedPrincipal GetRegistryPrincipal(SecurityIdentifier sid, string comput _log.LogTrace("Got local principal {sid} on computer {Computer}", sid.Value, computerName); // Set label to be local group. It could be a local user or alias but I'm not sure how we can confirm. Besides, it will not have any effect on the end result - var objectType = Label.LocalGroup; - // The local group sid is computer machine sid - group rid. var groupRid = sid.Rid(); var newSid = $"{computerObjectId}-{groupRid}"; - return (new TypedPrincipal - { - ObjectIdentifier = newSid, - ObjectType = objectType - }); + return (true, new TypedPrincipal(newSid, Label.LocalGroup)); } //If we get here, we most likely have a domain principal. Do a lookup - return _utils.ResolveIDAndType(sid.Value, computerDomain); + return await _utils.ResolveIDAndType(sid.Value, computerDomain); } - private async Task GetMachineSid(string computerName, string computerObjectId, string computerDomain, bool isDomainController) + private async Task GetMachineSid(string computerName, string computerObjectId) { SecurityIdentifier machineSid = null; @@ -381,73 +386,78 @@ await SendComputerStatus(new CSVComputerStatus return machineSid; } - // TODO: Copied from URA processor. Find a way to have this function in a shared spot - - - public virtual Result OpenSamServer(string computerName) - { - var result = SAMServer.OpenServer(computerName); - if (result.IsFailed) - { - return Result.Fail(result.SError); - } - - return Result.Ok(result.Value); - } - - private async Task SendComputerStatus(CSVComputerStatus status) - { - if (ComputerStatusEvent is not null) await ComputerStatusEvent(status); - } - - } - - public class EnrollmentAgentRestriction - { - public EnrollmentAgentRestriction(QualifiedAce ace, string computerDomain, string certTemplatesLocation, CertAbuseProcessor certAbuseProcessor, string computerName, bool isDomainController, string computerObjectId, SecurityIdentifier machineSid) - { + private async Task<(bool success, EnrollmentAgentRestriction restriction)> CreateEnrollmentAgentRestriction(QualifiedAce ace, string computerDomain, string computerName, bool isDomainController, string computerObjectId, SecurityIdentifier machineSid) { var targets = new List(); var index = 0; - // Access type (Allow/Deny) - AccessType = ace.AceType.ToString(); - - // Agent - Agent = certAbuseProcessor.GetRegistryPrincipal(ace.SecurityIdentifier, computerDomain, computerName, isDomainController, computerObjectId, machineSid); + var accessType = ace.AceType.ToString(); + var agent = await GetRegistryPrincipal(ace.SecurityIdentifier, computerDomain, computerName, isDomainController, + computerObjectId, machineSid); - // Targets var opaque = ace.GetOpaque(); var sidCount = BitConverter.ToUInt32(opaque, 0); index += 4; - for (var i = 0; i < sidCount; i++) - { + + for (var i = 0; i < sidCount; i++) { var sid = new SecurityIdentifier(opaque, index); - targets.Add(certAbuseProcessor.GetRegistryPrincipal(ace.SecurityIdentifier, computerDomain, computerName, isDomainController, computerObjectId, machineSid)); + if (await GetRegistryPrincipal(sid, computerDomain, computerName, isDomainController, computerObjectId, + machineSid) is (true, var regPrincipal)) { + targets.Add(regPrincipal); + } + index += sid.BinaryLength; } - Targets = targets.ToArray(); - // Template - if (index < opaque.Length) - { - AllTemplates = false; + var finalTargets = targets.ToArray(); + var allTemplates = index >= opaque.Length; + if (index < opaque.Length) { var template = Encoding.Unicode.GetString(opaque, index, opaque.Length - index - 2).Replace("\u0000", string.Empty); + if (await _utils.ResolveCertTemplateByProperty(Encoder.LdapFilterEncode(template), LDAPProperties.CanonicalName, computerDomain) is (true, var resolvedTemplate)) { + return (true, new EnrollmentAgentRestriction { + Template = resolvedTemplate, + Agent = agent.Principal, + AllTemplates = allTemplates, + AccessType = accessType, + Targets = finalTargets + }); + } - // Attempt to resolve the cert template by CN - Template = certAbuseProcessor._utils.ResolveCertTemplateByProperty(Encoder.LdapFilterEncode(template), LDAPProperties.CanonicalName, certTemplatesLocation, computerDomain); - - // Attempt to resolve the cert template by OID - if (Template == null) - { - Template = certAbuseProcessor._utils.ResolveCertTemplateByProperty(template, LDAPProperties.CertTemplateOID, certTemplatesLocation, computerDomain); + if (await _utils.ResolveCertTemplateByProperty( + Encoder.LdapFilterEncode(template), LDAPProperties.CertTemplateOID, computerDomain) is + (true, var resolvedOidTemplate)) { + return (true, new EnrollmentAgentRestriction { + Template = resolvedOidTemplate, + Agent = agent.Principal, + AllTemplates = allTemplates, + AccessType = accessType, + Targets = finalTargets + }); } } - else + + return (false, default); + } + + public virtual SharpHoundRPC.Result OpenSamServer(string computerName) + { + var result = SAMServer.OpenServer(computerName); + if (result.IsFailed) { - AllTemplates = true; + return SharpHoundRPC.Result.Fail(result.SError); } + + return SharpHoundRPC.Result.Ok(result.Value); } + private async Task SendComputerStatus(CSVComputerStatus status) + { + if (ComputerStatusEvent is not null) await ComputerStatusEvent(status); + } + + } + + public class EnrollmentAgentRestriction + { public string AccessType { get; set; } public TypedPrincipal Agent { get; set; } public TypedPrincipal[] Targets { get; set; } diff --git a/src/CommonLib/Processors/ComputerAvailability.cs b/src/CommonLib/Processors/ComputerAvailability.cs index 8159bb6c..e87103a5 100644 --- a/src/CommonLib/Processors/ComputerAvailability.cs +++ b/src/CommonLib/Processors/ComputerAvailability.cs @@ -47,13 +47,14 @@ public ComputerAvailability(PortScanner scanner, int timeout = 500, int computer /// /// /// - public Task IsComputerAvailable(ResolvedSearchResult result, ISearchResultEntry entry) + public Task IsComputerAvailable(ResolvedSearchResult result, IDirectoryObject entry) { var name = result.DisplayName; var os = entry.GetProperty(LDAPProperties.OperatingSystem); var pwdlastset = entry.GetProperty(LDAPProperties.PasswordLastSet); - - return IsComputerAvailable(name, os, pwdlastset); + var lastLogon = entry.GetProperty(LDAPProperties.LastLogonTimestamp); + + return IsComputerAvailable(name, os, pwdlastset, lastLogon); } /// @@ -65,13 +66,14 @@ public Task IsComputerAvailable(ResolvedSearchResult result, ISe /// The computer to check availability for /// The LDAP operatingsystem attribute value /// The LDAP pwdlastset attribute value + /// The LDAP lastlogontimestamp attribute value /// A ComputerStatus object that represents the availability of the computer public async Task IsComputerAvailable(string computerName, string operatingSystem, - string pwdLastSet) + string pwdLastSet, string lastLogon) { if (operatingSystem != null && !operatingSystem.StartsWith("Windows", StringComparison.OrdinalIgnoreCase)) { - _log.LogDebug("{ComputerName} is not available because operating system {OperatingSystem} is not valid", + _log.LogTrace("{ComputerName} is not available because operating system {OperatingSystem} is not valid", computerName, operatingSystem); await SendComputerStatus(new CSVComputerStatus { @@ -86,28 +88,22 @@ await SendComputerStatus(new CSVComputerStatus }; } - if (!_skipPasswordCheck) + if (!_skipPasswordCheck && !IsComputerActive(pwdLastSet, lastLogon)) { - var passwordLastSet = Helpers.ConvertLdapTimeToLong(pwdLastSet); - var threshold = DateTime.Now.AddDays(_computerExpiryDays * -1).ToFileTimeUtc(); - - if (passwordLastSet < threshold) + _log.LogTrace( + "{ComputerName} is not available because password last set and lastlogontimestamp are out of range", + computerName); + await SendComputerStatus(new CSVComputerStatus { - _log.LogDebug( - "{ComputerName} is not available because password last set {PwdLastSet} is out of range", - computerName, passwordLastSet); - await SendComputerStatus(new CSVComputerStatus - { - Status = ComputerStatus.OldPwd, - Task = "ComputerAvailability", - ComputerName = computerName - }); - return new ComputerStatus - { - Connectable = false, - Error = ComputerStatus.OldPwd - }; - } + Status = ComputerStatus.NotActive, + Task = "ComputerAvailability", + ComputerName = computerName + }); + return new ComputerStatus + { + Connectable = false, + Error = ComputerStatus.NotActive + }; } if (_skipPortScan) @@ -116,11 +112,10 @@ await SendComputerStatus(new CSVComputerStatus Connectable = true, Error = null }; - - + if (!await _scanner.CheckPort(computerName, timeout: _scanTimeout)) { - _log.LogDebug("{ComputerName} is not available because port 445 is unavailable", computerName); + _log.LogTrace("{ComputerName} is not available because port 445 is unavailable", computerName); await SendComputerStatus(new CSVComputerStatus { Status = ComputerStatus.PortNotOpen, @@ -134,7 +129,7 @@ await SendComputerStatus(new CSVComputerStatus }; } - _log.LogDebug("{ComputerName} is available for enumeration", computerName); + _log.LogTrace("{ComputerName} is available for enumeration", computerName); await SendComputerStatus(new CSVComputerStatus { @@ -150,6 +145,20 @@ await SendComputerStatus(new CSVComputerStatus }; } + /// + /// Checks if a computer's passwordlastset/lastlogontimestamp attributes are within a certain range + /// + /// + /// + /// + private bool IsComputerActive(string pwdLastSet, string lastLogonTimestamp) { + var passwordLastSet = Helpers.ConvertLdapTimeToLong(pwdLastSet); + var lastLogonTimeStamp = Helpers.ConvertLdapTimeToLong(lastLogonTimestamp); + var threshold = DateTime.Now.AddDays(_computerExpiryDays * -1).ToFileTimeUtc(); + + return passwordLastSet >= threshold || lastLogonTimeStamp >= threshold; + } + private async Task SendComputerStatus(CSVComputerStatus status) { if (ComputerStatusEvent is not null) await ComputerStatusEvent.Invoke(status); diff --git a/src/CommonLib/Processors/ComputerSessionProcessor.cs b/src/CommonLib/Processors/ComputerSessionProcessor.cs index f8028f17..e0db075e 100644 --- a/src/CommonLib/Processors/ComputerSessionProcessor.cs +++ b/src/CommonLib/Processors/ComputerSessionProcessor.cs @@ -8,24 +8,24 @@ using Microsoft.Extensions.Logging; using Microsoft.Win32; using SharpHoundCommonLib.OutputTypes; +using SharpHoundRPC.NetAPINative; -namespace SharpHoundCommonLib.Processors -{ - public class ComputerSessionProcessor - { +namespace SharpHoundCommonLib.Processors { + public class ComputerSessionProcessor { public delegate Task ComputerStatusDelegate(CSVComputerStatus status); private static readonly Regex SidRegex = new(@"S-1-5-21-[0-9]+-[0-9]+-[0-9]+-[0-9]+$", RegexOptions.Compiled); private readonly string _currentUserName; private readonly ILogger _log; private readonly NativeMethods _nativeMethods; - private readonly ILDAPUtils _utils; + private readonly ILdapUtils _utils; private readonly bool _doLocalAdminSessionEnum; private readonly string _localAdminUsername; private readonly string _localAdminPassword; - public ComputerSessionProcessor(ILDAPUtils utils, string currentUserName = null, NativeMethods nativeMethods = null, ILogger log = null, bool doLocalAdminSessionEnum = false, string localAdminUsername = null, string localAdminPassword = null) - { + public ComputerSessionProcessor(ILdapUtils utils, string currentUserName = null, + NativeMethods nativeMethods = null, ILogger log = null, bool doLocalAdminSessionEnum = false, + string localAdminUsername = null, string localAdminPassword = null) { _utils = utils; _nativeMethods = nativeMethods ?? new NativeMethods(); _currentUserName = currentUserName ?? WindowsIdentity.GetCurrent().Name.Split('\\')[1]; @@ -46,49 +46,44 @@ public ComputerSessionProcessor(ILDAPUtils utils, string currentUserName = null, /// /// public async Task ReadUserSessions(string computerName, string computerSid, - string computerDomain) - { + string computerDomain) { var ret = new SessionAPIResult(); - SharpHoundRPC.NetAPINative.NetAPIResult> result; + NetAPIResult> result; + + _log.LogDebug("Running NetSessionEnum for {ObjectName}", computerName); - if (_doLocalAdminSessionEnum) - { + if (_doLocalAdminSessionEnum) { // If we are authenticating using a local admin, we need to impersonate for this - Impersonator Impersonate; - using (Impersonate = new Impersonator(_localAdminUsername, ".", _localAdminPassword, LogonType.LOGON32_LOGON_NEW_CREDENTIALS, LogonProvider.LOGON32_PROVIDER_WINNT50)) - { + using (new Impersonator(_localAdminUsername, ".", _localAdminPassword, + LogonType.LOGON32_LOGON_NEW_CREDENTIALS, LogonProvider.LOGON32_PROVIDER_WINNT50)) { result = _nativeMethods.NetSessionEnum(computerName); } - if (result.IsFailed) - { + if (result.IsFailed) { // Fall back to default User - _log.LogDebug("NetSessionEnum failed on {ComputerName} with local admin credentials: {Status}. Fallback to default user.", computerName, result.Status); + _log.LogDebug( + "NetSessionEnum failed on {ComputerName} with local admin credentials: {Status}. Fallback to default user.", + computerName, result.Status); result = _nativeMethods.NetSessionEnum(computerName); } - } - else - { + } else { result = _nativeMethods.NetSessionEnum(computerName); } - if (result.IsFailed) - { - await SendComputerStatus(new CSVComputerStatus - { + if (result.IsFailed) { + await SendComputerStatus(new CSVComputerStatus { Status = result.Status.ToString(), Task = "NetSessionEnum", ComputerName = computerName }); - _log.LogDebug("NetSessionEnum failed on {ComputerName}: {Status}", computerName, result.Status); + _log.LogTrace("NetSessionEnum failed on {ComputerName}: {Status}", computerName, result.Status); ret.Collected = false; ret.FailureReason = result.Status.ToString(); return ret; } - _log.LogDebug("NetSessionEnum succeeded on {ComputerName}", computerName); - await SendComputerStatus(new CSVComputerStatus - { + _log.LogTrace("NetSessionEnum succeeded on {ComputerName}", computerName); + await SendComputerStatus(new CSVComputerStatus { Status = CSVComputerStatus.StatusSuccess, Task = "NetSessionEnum", ComputerName = computerName @@ -97,8 +92,7 @@ await SendComputerStatus(new CSVComputerStatus ret.Collected = true; var results = new List(); - foreach (var sesInfo in result.Value) - { + foreach (var sesInfo in result.Value) { var username = sesInfo.Username; var computerSessionName = sesInfo.ComputerName; @@ -106,18 +100,14 @@ await SendComputerStatus(new CSVComputerStatus computerSessionName, computerName); //Filter out blank/null cnames/usernames - if (string.IsNullOrWhiteSpace(computerSessionName) || string.IsNullOrWhiteSpace(username)) - { - _log.LogTrace("Skipping NetSessionEnum entry with null session/user"); + if (string.IsNullOrWhiteSpace(computerSessionName) || string.IsNullOrWhiteSpace(username)) { continue; } //Filter out blank usernames, computer accounts, the user we're doing enumeration with, and anonymous logons if (username.EndsWith("$") || username.Equals(_currentUserName, StringComparison.CurrentCultureIgnoreCase) || - username.Equals("anonymous logon", StringComparison.CurrentCultureIgnoreCase)) - { - _log.LogTrace("Skipping NetSessionEnum entry for {Username}", username); + username.Equals("anonymous logon", StringComparison.CurrentCultureIgnoreCase)) { continue; } @@ -125,35 +115,28 @@ await SendComputerStatus(new CSVComputerStatus computerSessionName = computerSessionName.TrimStart('\\'); string resolvedComputerSID = null; - //Resolve "localhost" equivalents to the computer sid if (computerSessionName is "[::1]" or "127.0.0.1") resolvedComputerSID = computerSid; - else + else if (await _utils.ResolveHostToSid(computerSessionName, computerDomain) is (true, var tempSid)) //Attempt to resolve the host name to a SID - resolvedComputerSID = await _utils.ResolveHostToSid(computerSessionName, computerDomain); + resolvedComputerSID = tempSid; //Throw out this data if we couldn't resolve it successfully. - if (resolvedComputerSID == null || !resolvedComputerSID.StartsWith("S-1")) - { - _log.LogTrace("Unable to resolve {ComputerSessionName} to real SID", computerSessionName); + if (resolvedComputerSID == null || !resolvedComputerSID.StartsWith("S-1")) { continue; } - var matches = _utils.GetUserGlobalCatalogMatches(username); - if (matches.Length > 0) - { + var (matchSuccess, sids) = await _utils.GetGlobalCatalogMatches(username, computerDomain); + if (matchSuccess) { results.AddRange( - matches.Select(s => new Session {ComputerSID = resolvedComputerSID, UserSID = s})); - } - else - { - var res = _utils.ResolveAccountName(username, computerDomain); - if (res != null) - results.Add(new Session - { + sids.Select(s => new Session { ComputerSID = resolvedComputerSID, UserSID = s })); + } else { + var res = await _utils.ResolveAccountName(username, computerDomain); + if (res.Success) + results.Add(new Session { ComputerSID = resolvedComputerSID, - UserSID = res.ObjectIdentifier + UserSID = res.Principal.ObjectIdentifier }); } } @@ -172,49 +155,45 @@ await SendComputerStatus(new CSVComputerStatus /// /// public async Task ReadUserSessionsPrivileged(string computerName, - string computerSamAccountName, string computerSid) - { + string computerSamAccountName, string computerSid) { var ret = new SessionAPIResult(); - SharpHoundRPC.NetAPINative.NetAPIResult> result; + NetAPIResult> + result; + + _log.LogDebug("Running NetWkstaUserEnum for {ObjectName}", computerName); - if (_doLocalAdminSessionEnum) - { + if (_doLocalAdminSessionEnum) { // If we are authenticating using a local admin, we need to impersonate for this - Impersonator Impersonate; - using (Impersonate = new Impersonator(_localAdminUsername, ".", _localAdminPassword, LogonType.LOGON32_LOGON_NEW_CREDENTIALS, LogonProvider.LOGON32_PROVIDER_WINNT50)) - { + using (new Impersonator(_localAdminUsername, ".", _localAdminPassword, + LogonType.LOGON32_LOGON_NEW_CREDENTIALS, LogonProvider.LOGON32_PROVIDER_WINNT50)) { result = _nativeMethods.NetWkstaUserEnum(computerName); } - if (result.IsFailed) - { + if (result.IsFailed) { // Fall back to default User - _log.LogDebug("NetWkstaUserEnum failed on {ComputerName} with local admin credentials: {Status}. Fallback to default user.", computerName, result.Status); + _log.LogDebug( + "NetWkstaUserEnum failed on {ComputerName} with local admin credentials: {Status}. Fallback to default user.", + computerName, result.Status); result = _nativeMethods.NetWkstaUserEnum(computerName); } - } - else - { + } else { result = _nativeMethods.NetWkstaUserEnum(computerName); } - if (result.IsFailed) - { - await SendComputerStatus(new CSVComputerStatus - { + if (result.IsFailed) { + await SendComputerStatus(new CSVComputerStatus { Status = result.Status.ToString(), Task = "NetWkstaUserEnum", ComputerName = computerName }); - _log.LogDebug("NetWkstaUserEnum failed on {ComputerName}: {Status}", computerName, result.Status); + _log.LogTrace("NetWkstaUserEnum failed on {ComputerName}: {Status}", computerName, result.Status); ret.Collected = false; ret.FailureReason = result.Status.ToString(); return ret; } - _log.LogDebug("NetWkstaUserEnum succeeded on {ComputerName}", computerName); - await SendComputerStatus(new CSVComputerStatus - { + _log.LogTrace("NetWkstaUserEnum succeeded on {ComputerName}", computerName); + await SendComputerStatus(new CSVComputerStatus { Status = result.Status.ToString(), Task = "NetWkstaUserEnum", ComputerName = computerName @@ -223,52 +202,31 @@ await SendComputerStatus(new CSVComputerStatus ret.Collected = true; var results = new List(); - foreach (var wkstaUserInfo in result.Value) - { + foreach (var wkstaUserInfo in result.Value) { var domain = wkstaUserInfo.LogonDomain; var username = wkstaUserInfo.Username; - - _log.LogTrace("NetWkstaUserEnum entry: {Username}@{Domain} from {ComputerName}", username, domain, - computerName); - - //These are local computer accounts. - if (domain.Equals(computerSamAccountName, StringComparison.CurrentCultureIgnoreCase)) - { - _log.LogTrace("Skipping local entry {Username}@{Domain}", username, domain); + + //If we dont have a domain or the domain has a space, ignore this object as it's unusable for us + if (string.IsNullOrWhiteSpace(domain) || domain.Contains(" ")) { continue; } - //Filter out empty usernames and computer sessions - if (string.IsNullOrWhiteSpace(username) || username.EndsWith("$", StringComparison.Ordinal)) - { - _log.LogTrace("Skipping null or computer session"); + //These are local computer accounts. + if (domain.Equals(computerSamAccountName, StringComparison.CurrentCultureIgnoreCase)) { continue; } - //If we dont have a domain, ignore this object - if (string.IsNullOrWhiteSpace(domain)) - { - _log.LogTrace("Skipping null/empty domain"); + //Filter out empty usernames and computer sessions + if (string.IsNullOrWhiteSpace(username) || username.EndsWith("$", StringComparison.Ordinal)) { continue; } - //Any domain with a space is unusable. It'll be things like NT Authority or Font Driver - if (domain.Contains(" ")) - { - _log.LogTrace("Skipping domain with space: {Domain}", domain); - continue; + if (await _utils.ResolveAccountName(username, domain) is (true, var res)) { + results.Add(res); } - - var res = _utils.ResolveAccountName(username, domain); - if (res == null) - continue; - - _log.LogTrace("Resolved NetWkstaUserEnum entry: {SID}", res.ObjectIdentifier); - results.Add(res); } - ret.Results = results.Select(x => new Session - { + ret.Results = results.Select(x => new Session { ComputerSID = computerSid, UserSID = x.ObjectIdentifier }).ToArray(); @@ -277,59 +235,57 @@ await SendComputerStatus(new CSVComputerStatus } public async Task ReadUserSessionsRegistry(string computerName, string computerDomain, - string computerSid) - { + string computerSid) { var ret = new SessionAPIResult(); + + _log.LogDebug("Running RegSessionEnum for {ObjectName}", computerName); RegistryKey key = null; - try - { + try { var task = OpenRegistryKey(computerName, RegistryHive.Users); - - if (await Task.WhenAny(task, Task.Delay(10000)) != task) - { + + if (await Task.WhenAny(task, Task.Delay(10000)) != task) { _log.LogDebug("Hit timeout on registry enum on {Server}. Abandoning registry enum", computerName); ret.Collected = false; ret.FailureReason = "Timeout"; - await SendComputerStatus(new CSVComputerStatus - { + await SendComputerStatus(new CSVComputerStatus { Status = "Timeout", Task = "RegistrySessionEnum", ComputerName = computerName }); return ret; } - + key = task.Result; ret.Collected = true; - await SendComputerStatus(new CSVComputerStatus - { + await SendComputerStatus(new CSVComputerStatus { Status = CSVComputerStatus.StatusSuccess, Task = "RegistrySessionEnum", ComputerName = computerName }); - _log.LogDebug("Registry session enum succeeded on {ComputerName}", computerName); - ret.Results = key.GetSubKeyNames() - .Where(subkey => SidRegex.IsMatch(subkey)) - .Select(x => _utils.ResolveIDAndType(x, computerDomain)) - .Where(x => x != null) - .Select(x => - new Session - { + _log.LogTrace("Registry session enum succeeded on {ComputerName}", computerName); + var results = new List(); + foreach (var subkey in key.GetSubKeyNames()) { + if (!SidRegex.IsMatch(subkey)) { + continue; + } + + if (await _utils.ResolveIDAndType(subkey, computerDomain) is (true, var principal)) { + results.Add(new Session() { ComputerSID = computerSid, - UserSID = x.ObjectIdentifier - }) - .ToArray(); + UserSID = principal.ObjectIdentifier + }); + } + } + + ret.Results = results.ToArray(); return ret; - } - catch (Exception e) - { - _log.LogDebug("Registry session enum failed on {ComputerName}: {Status}", computerName, e.Message); - await SendComputerStatus(new CSVComputerStatus - { + } catch (Exception e) { + _log.LogTrace("Registry session enum failed on {ComputerName}: {Status}", computerName, e.Message); + await SendComputerStatus(new CSVComputerStatus { Status = e.Message, Task = "RegistrySessionEnum", ComputerName = computerName @@ -337,21 +293,17 @@ await SendComputerStatus(new CSVComputerStatus ret.Collected = false; ret.FailureReason = e.Message; return ret; - } - finally - { + } finally { key?.Dispose(); } } - private Task OpenRegistryKey(string computerName, RegistryHive hive) - { + private static Task OpenRegistryKey(string computerName, RegistryHive hive) { return Task.Run(() => RegistryKey.OpenRemoteBaseKey(hive, computerName)); } - private async Task SendComputerStatus(CSVComputerStatus status) - { + private async Task SendComputerStatus(CSVComputerStatus status) { if (ComputerStatusEvent is not null) await ComputerStatusEvent.Invoke(status); } } -} +} \ No newline at end of file diff --git a/src/CommonLib/Processors/ContainerProcessor.cs b/src/CommonLib/Processors/ContainerProcessor.cs index 8e73ddc6..4539d721 100644 --- a/src/CommonLib/Processors/ContainerProcessor.cs +++ b/src/CommonLib/Processors/ContainerProcessor.cs @@ -1,7 +1,10 @@ using System; using System.Collections.Generic; using System.DirectoryServices.Protocols; +using System.Linq; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using SharpHoundCommonLib.DirectoryObjects; using SharpHoundCommonLib.Enums; using SharpHoundCommonLib.LDAPQueries; using SharpHoundCommonLib.OutputTypes; @@ -11,9 +14,9 @@ namespace SharpHoundCommonLib.Processors public class ContainerProcessor { private readonly ILogger _log; - private readonly ILDAPUtils _utils; + private readonly ILdapUtils _utils; - public ContainerProcessor(ILDAPUtils utils, ILogger log = null) + public ContainerProcessor(ILdapUtils utils, ILogger log = null) { _utils = utils; _log = log ?? Logging.LogProvider.CreateLogger("ContainerProc"); @@ -34,9 +37,14 @@ private static bool IsDistinguishedNameFiltered(string distinguishedName) /// /// /// - public TypedPrincipal GetContainingObject(ISearchResultEntry entry) + public async Task<(bool Success, TypedPrincipal principal)> GetContainingObject(IDirectoryObject entry) { - return GetContainingObject(entry.DistinguishedName); + if (entry.TryGetDistinguishedName(out var dn)) { + _log.LogTrace("Reading containing object for {DN}", dn); + return await GetContainingObject(dn); + } + + return (false, default); } /// @@ -45,21 +53,23 @@ public TypedPrincipal GetContainingObject(ISearchResultEntry entry) /// /// /// - public TypedPrincipal GetContainingObject(string distinguishedName) + public async Task<(bool Success, TypedPrincipal Principal)> GetContainingObject(string distinguishedName) { var containerDn = Helpers.RemoveDistinguishedNamePrefix(distinguishedName); + //If the container is the builtin container, we want to redirect the containing object to the domain of the object if (containerDn.StartsWith("CN=BUILTIN", StringComparison.OrdinalIgnoreCase)) { + //This is always safe var domain = Helpers.DistinguishedNameToDomain(distinguishedName); - var domainSid = _utils.GetSidFromDomainName(domain); - return new TypedPrincipal(domainSid, Label.Domain); - } + if (await _utils.GetDomainSidFromDomainName(domain) is (true, var domainSid)) { + return (true, new TypedPrincipal(domainSid, Label.Domain)); + } - if (string.IsNullOrEmpty(containerDn)) - return null; + return (false, default); + } - return _utils.ResolveDistinguishedName(containerDn); + return await _utils.ResolveDistinguishedName(containerDn); } /// @@ -68,13 +78,15 @@ public TypedPrincipal GetContainingObject(string distinguishedName) /// /// /// - public IEnumerable GetContainerChildObjects(ResolvedSearchResult result, - ISearchResultEntry entry) + public IAsyncEnumerable GetContainerChildObjects(ResolvedSearchResult result, + IDirectoryObject entry) { var name = result.DisplayName; - var dn = entry.DistinguishedName; - - return GetContainerChildObjects(dn, name); + if (entry.TryGetDistinguishedName(out var dn)) { + return GetContainerChildObjects(dn, name); + } + + return AsyncEnumerable.Empty(); } /// @@ -83,45 +95,48 @@ public IEnumerable GetContainerChildObjects(ResolvedSearchResult /// /// /// - public IEnumerable GetContainerChildObjects(string distinguishedName, string containerName = "") + public async IAsyncEnumerable GetContainerChildObjects(string distinguishedName, string containerName = "") { - var filter = new LDAPFilter().AddComputers().AddUsers().AddGroups().AddOUs().AddContainers(); + var filter = new LdapFilter().AddComputers().AddUsers().AddGroups().AddOUs().AddContainers(); filter.AddCertificateAuthorities().AddCertificateTemplates().AddEnterpriseCertificationAuthorities(); - foreach (var childEntry in _utils.QueryLDAP(filter.GetFilter(), SearchScope.OneLevel, - CommonProperties.ObjectID, Helpers.DistinguishedNameToDomain(distinguishedName), - adsPath: distinguishedName)) - { - var dn = childEntry.DistinguishedName; - if (IsDistinguishedNameFiltered(dn)) - { + await foreach (var childEntryResult in _utils.Query(new LdapQueryParameters { + DomainName = Helpers.DistinguishedNameToDomain(distinguishedName), + SearchScope = SearchScope.OneLevel, + Attributes = CommonProperties.ObjectID, + LDAPFilter = filter.GetFilter(), + SearchBase = distinguishedName + })) { + if (!childEntryResult.IsSuccess) { + _log.LogWarning("Error while getting container child objects for {DistinguishedName}: {Reason}", distinguishedName, childEntryResult.Error); + yield break; + } + + var childEntry = childEntryResult.Value; + if (!childEntry.TryGetDistinguishedName(out var dn) || IsDistinguishedNameFiltered(dn)) { _log.LogTrace("Skipping filtered child {Child} for {Container}", dn, containerName); continue; } - var id = childEntry.GetObjectIdentifier(); - if (id == null) - { - _log.LogTrace("Got null ID for {ChildDN} under {Container}", childEntry.DistinguishedName, + if (!childEntry.GetObjectIdentifier(out var id)) { + _log.LogTrace("Got null ID for {ChildDN} under {Container}", dn, containerName); continue; } - var res = _utils.ResolveIDAndType(id, Helpers.DistinguishedNameToDomain(dn)); - if (res == null) - { - _log.LogTrace("Failed to resolve principal for {ID}", id); - continue; + var res = await _utils.ResolveIDAndType(id, Helpers.DistinguishedNameToDomain(dn)); + if (res.Success) { + yield return res.Principal; } - - yield return res; } } - public IEnumerable ReadContainerGPLinks(ResolvedSearchResult result, ISearchResultEntry entry) + public IAsyncEnumerable ReadContainerGPLinks(ResolvedSearchResult result, IDirectoryObject entry) { - var links = entry.GetProperty(LDAPProperties.GPLink); + if (entry.TryGetProperty(LDAPProperties.GPLink, out var links)) { + return ReadContainerGPLinks(links); + } - return ReadContainerGPLinks(links); + return AsyncEnumerable.Empty(); } /// @@ -129,7 +144,7 @@ public IEnumerable ReadContainerGPLinks(ResolvedSearchResult result, ISe /// /// /// - public IEnumerable ReadContainerGPLinks(string gpLink) + public async IAsyncEnumerable ReadContainerGPLinks(string gpLink) { if (gpLink == null) yield break; @@ -138,19 +153,15 @@ public IEnumerable ReadContainerGPLinks(string gpLink) { var enforced = link.Status.Equals("2"); - var res = _utils.ResolveDistinguishedName(link.DistinguishedName); + var res = await _utils.ResolveDistinguishedName(link.DistinguishedName); - if (res == null) - { - _log.LogTrace("Failed to resolve DN {DN}", link.DistinguishedName); - continue; + if (res.Success) { + yield return new GPLink + { + GUID = res.Principal.ObjectIdentifier, + IsEnforced = enforced + }; } - - yield return new GPLink - { - GUID = res.ObjectIdentifier, - IsEnforced = enforced - }; } } diff --git a/src/CommonLib/Processors/DCRegistryProcessor.cs b/src/CommonLib/Processors/DCRegistryProcessor.cs index e3fb4b14..0bd5bad0 100644 --- a/src/CommonLib/Processors/DCRegistryProcessor.cs +++ b/src/CommonLib/Processors/DCRegistryProcessor.cs @@ -9,10 +9,10 @@ namespace SharpHoundCommonLib.Processors public class DCRegistryProcessor { private readonly ILogger _log; - public readonly ILDAPUtils _utils; + public readonly ILdapUtils _utils; public delegate Task ComputerStatusDelegate(CSVComputerStatus status); - public DCRegistryProcessor(ILDAPUtils utils, ILogger log = null) + public DCRegistryProcessor(ILdapUtils utils, ILogger log = null) { _utils = utils; _log = log ?? Logging.LogProvider.CreateLogger("DCRegProc"); @@ -29,7 +29,7 @@ public DCRegistryProcessor(ILDAPUtils utils, ILogger log = null) public IntRegistryAPIResult GetCertificateMappingMethods(string target) { var ret = new IntRegistryAPIResult(); - var subKey = $"SYSTEM\\CurrentControlSet\\Control\\SecurityProviders\\Schannel"; + const string subKey = @"SYSTEM\CurrentControlSet\Control\SecurityProviders\Schannel"; const string subValue = "CertificateMappingMethods"; var data = Helpers.GetRegistryKeyData(target, subKey, subValue, _log); @@ -62,7 +62,7 @@ public IntRegistryAPIResult GetCertificateMappingMethods(string target) public IntRegistryAPIResult GetStrongCertificateBindingEnforcement(string target) { var ret = new IntRegistryAPIResult(); - var subKey = $"SYSTEM\\CurrentControlSet\\Services\\Kdc"; + const string subKey = @"SYSTEM\CurrentControlSet\Services\Kdc"; const string subValue = "StrongCertificateBindingEnforcement"; var data = Helpers.GetRegistryKeyData(target, subKey, subValue, _log); diff --git a/src/CommonLib/Processors/DomainTrustProcessor.cs b/src/CommonLib/Processors/DomainTrustProcessor.cs index bb157e8e..52c89786 100644 --- a/src/CommonLib/Processors/DomainTrustProcessor.cs +++ b/src/CommonLib/Processors/DomainTrustProcessor.cs @@ -11,9 +11,9 @@ namespace SharpHoundCommonLib.Processors public class DomainTrustProcessor { private readonly ILogger _log; - private readonly ILDAPUtils _utils; + private readonly ILdapUtils _utils; - public DomainTrustProcessor(ILDAPUtils utils, ILogger log = null) + public DomainTrustProcessor(ILdapUtils utils, ILogger log = null) { _utils = utils; _log = log ?? Logging.LogProvider.CreateLogger("DomainTrustProc"); @@ -24,17 +24,23 @@ public DomainTrustProcessor(ILDAPUtils utils, ILogger log = null) /// /// /// - public IEnumerable EnumerateDomainTrusts(string domain) + public async IAsyncEnumerable EnumerateDomainTrusts(string domain) { - var query = CommonFilters.TrustedDomains; - foreach (var result in _utils.QueryLDAP(query, SearchScope.Subtree, CommonProperties.DomainTrustProps, - domain)) + _log.LogDebug("Running trust enumeration for {Domain}", domain); + await foreach (var result in _utils.Query(new LdapQueryParameters { + LDAPFilter = CommonFilters.TrustedDomains, + Attributes = CommonProperties.DomainTrustProps, + DomainName = domain + })) { + if (!result.IsSuccess) { + yield break; + } + + var entry = result.Value; var trust = new DomainTrust(); - var targetSidBytes = result.GetByteProperty(LDAPProperties.SecurityIdentifier); - if (targetSidBytes == null || targetSidBytes.Length == 0) - { - _log.LogTrace("Trust sid is null or empty for target: {Domain}", domain); + if (!entry.TryGetByteProperty(LDAPProperties.SecurityIdentifier, out var targetSidBytes) || targetSidBytes.Length == 0) { + _log.LogDebug("Trust sid is null or empty for target: {Domain}", domain); continue; } @@ -51,33 +57,26 @@ public IEnumerable EnumerateDomainTrusts(string domain) trust.TargetDomainSid = sid; - if (int.TryParse(result.GetProperty(LDAPProperties.TrustDirection), out var td)) - { - trust.TrustDirection = (TrustDirection) td; - } - else - { + if (!entry.TryGetIntProperty(LDAPProperties.TrustDirection, out var td)) { _log.LogTrace("Failed to convert trustdirection for target: {Domain}", domain); continue; } - + trust.TrustDirection = (TrustDirection) td; + TrustAttributes attributes; - if (int.TryParse(result.GetProperty(LDAPProperties.TrustAttributes), out var ta)) - { - attributes = (TrustAttributes) ta; - } - else - { + if (!entry.TryGetIntProperty(LDAPProperties.TrustAttributes, out var ta)) { _log.LogTrace("Failed to convert trustattributes for target: {Domain}", domain); continue; } + + attributes = (TrustAttributes) ta; trust.IsTransitive = !attributes.HasFlag(TrustAttributes.NonTransitive); - var name = result.GetProperty(LDAPProperties.CanonicalName)?.ToUpper(); - if (name != null) - trust.TargetDomainName = name; + if (entry.TryGetProperty(LDAPProperties.CanonicalName, out var cn)) { + trust.TargetDomainName = cn.ToUpper(); + } trust.SidFilteringEnabled = attributes.HasFlag(TrustAttributes.FilterSids); trust.TrustType = TrustAttributesToType(attributes); diff --git a/src/CommonLib/Processors/GPOLocalGroupProcessor.cs b/src/CommonLib/Processors/GPOLocalGroupProcessor.cs index fa7041be..eadf1e54 100644 --- a/src/CommonLib/Processors/GPOLocalGroupProcessor.cs +++ b/src/CommonLib/Processors/GPOLocalGroupProcessor.cs @@ -12,10 +12,8 @@ using SharpHoundCommonLib.LDAPQueries; using SharpHoundCommonLib.OutputTypes; -namespace SharpHoundCommonLib.Processors -{ - public class GPOLocalGroupProcessor - { +namespace SharpHoundCommonLib.Processors { + public class GPOLocalGroupProcessor { private static readonly Regex KeyRegex = new(@"(.+?)\s*=(.*)", RegexOptions.Compiled); private static readonly Regex MemberRegex = @@ -35,59 +33,58 @@ public class GPOLocalGroupProcessor private static readonly ConcurrentDictionary> GpoActionCache = new(); private static readonly Dictionary ValidGroupNames = - new(StringComparer.OrdinalIgnoreCase) - { - {"Administrators", LocalGroupRids.Administrators}, - {"Remote Desktop Users", LocalGroupRids.RemoteDesktopUsers}, - {"Remote Management Users", LocalGroupRids.PSRemote}, - {"Distributed COM Users", LocalGroupRids.DcomUsers} + new(StringComparer.OrdinalIgnoreCase) { + { "Administrators", LocalGroupRids.Administrators }, + { "Remote Desktop Users", LocalGroupRids.RemoteDesktopUsers }, + { "Remote Management Users", LocalGroupRids.PSRemote }, + { "Distributed COM Users", LocalGroupRids.DcomUsers } }; private readonly ILogger _log; - private readonly ILDAPUtils _utils; + private readonly ILdapUtils _utils; - public GPOLocalGroupProcessor(ILDAPUtils utils, ILogger log = null) - { + public GPOLocalGroupProcessor(ILdapUtils utils, ILogger log = null) { _utils = utils; _log = log ?? Logging.LogProvider.CreateLogger("GPOLocalGroupProc"); } - public Task ReadGPOLocalGroups(ISearchResultEntry entry) - { - var links = entry.GetProperty(LDAPProperties.GPLink); - var dn = entry.DistinguishedName; - return ReadGPOLocalGroups(links, dn); + public Task ReadGPOLocalGroups(IDirectoryObject entry) { + if (entry.TryGetProperty(LDAPProperties.GPLink, out var links) && entry.TryGetDistinguishedName(out var dn)) { + return ReadGPOLocalGroups(links, dn); + } + + return default; } - public async Task ReadGPOLocalGroups(string gpLink, string distinguishedName) - { + public async Task ReadGPOLocalGroups(string gpLink, string distinguishedName) { var ret = new ResultingGPOChanges(); //If the gplink property is null, we don't need to process anything if (gpLink == null) return ret; // First lets check if this OU actually has computers that it contains. If not, then we'll ignore it. - // Its cheaper to fetch the affected computers from LDAP first and then process the GPLinks - var options = new LDAPQueryOptions - { - Filter = new LDAPFilter().AddComputersNoMSAs().GetFilter(), - Scope = SearchScope.Subtree, - Properties = CommonProperties.ObjectSID, - AdsPath = distinguishedName - }; + // Its cheaper to fetch the affected computers from LDAP first and then process the GPLinks + var affectedComputers = new List(); + await foreach (var result in _utils.Query(new LdapQueryParameters() { + LDAPFilter = new LdapFilter().AddComputersNoMSAs().GetFilter(), + Attributes = CommonProperties.ObjectSID, + SearchBase = distinguishedName + })) { + if (!result.IsSuccess) { + break; + } - var affectedComputers = _utils.QueryLDAP(options) - .Select(x => x.GetSid()) - .Where(x => x != null) - .Select(x => new TypedPrincipal - { - ObjectIdentifier = x, - ObjectType = Label.Computer - }).ToArray(); + var entry = result.Value; + if (!entry.TryGetSecurityIdentifier(out var sid)) { + continue; + } + + affectedComputers.Add(new TypedPrincipal(sid, Label.Computer)); + } //If there's no computers then we don't care about this OU - if (affectedComputers.Length == 0) + if (affectedComputers.Count == 0) return ret; var enforced = new List(); @@ -95,8 +92,7 @@ public async Task ReadGPOLocalGroups(string gpLink, string // Split our link property up and remove disabled links foreach (var link in Helpers.SplitGPLinkProperty(gpLink)) - switch (link.Status) - { + switch (link.Status) { case "0": unenforced.Add(link.DistinguishedName); break; @@ -106,40 +102,37 @@ public async Task ReadGPOLocalGroups(string gpLink, string } //Set up our links in the correct order. - // Enforced links override unenforced, and also respect the order in which they are placed in the GPLink property + //Enforced links override unenforced, and also respect the order in which they are placed in the GPLink property var orderedLinks = new List(); orderedLinks.AddRange(unenforced); orderedLinks.AddRange(enforced); var data = new Dictionary(); - foreach (var rid in Enum.GetValues(typeof(LocalGroupRids))) data[(LocalGroupRids) rid] = new GroupResults(); + foreach (var rid in Enum.GetValues(typeof(LocalGroupRids))) data[(LocalGroupRids)rid] = new GroupResults(); - foreach (var linkDn in orderedLinks) - { - if (!GpoActionCache.TryGetValue(linkDn.ToLower(), out var actions)) - { + foreach (var linkDn in orderedLinks) { + if (!GpoActionCache.TryGetValue(linkDn.ToLower(), out var actions)) { actions = new List(); var gpoDomain = Helpers.DistinguishedNameToDomain(linkDn); + var result = await _utils.Query(new LdapQueryParameters() { + LDAPFilter = new LdapFilter().AddAllObjects().GetFilter(), + SearchScope = SearchScope.Base, + Attributes = CommonProperties.GPCFileSysPath, + SearchBase = linkDn + }).DefaultIfEmpty(LdapResult.Fail()).FirstOrDefaultAsync(); + + if (!result.IsSuccess) { + continue; + } - var opts = new LDAPQueryOptions - { - Filter = new LDAPFilter().AddAllObjects().GetFilter(), - Scope = SearchScope.Base, - Properties = CommonProperties.GPCFileSysPath, - AdsPath = linkDn - }; - var filePath = _utils.QueryLDAP(opts).FirstOrDefault()? - .GetProperty(LDAPProperties.GPCFileSYSPath); - - if (filePath == null) - { + if (!result.Value.TryGetProperty(LDAPProperties.GPCFileSYSPath, out var filePath)) { GpoActionCache.TryAdd(linkDn, actions); continue; } //Add the actions for each file. The GPO template file actions will override the XML file actions - actions.AddRange(ProcessGPOXmlFile(filePath, gpoDomain).ToList()); + await foreach (var item in ProcessGPOXmlFile(filePath, gpoDomain)) actions.Add(item); await foreach (var item in ProcessGPOTemplateFile(filePath, gpoDomain)) actions.Add(item); } @@ -154,8 +147,7 @@ public async Task ReadGPOLocalGroups(string gpLink, string var restrictedMemberSets = actions.Where(x => x.Target == GroupActionTarget.RestrictedMember) .GroupBy(x => x.TargetRid); - foreach (var set in restrictedMemberSets) - { + foreach (var set in restrictedMemberSets) { var results = data[set.Key]; var members = set.Select(x => x.ToTypedPrincipal()).ToList(); results.RestrictedMember = members; @@ -166,8 +158,7 @@ public async Task ReadGPOLocalGroups(string gpLink, string var restrictedMemberOfSets = actions.Where(x => x.Target == GroupActionTarget.RestrictedMemberOf) .GroupBy(x => x.TargetRid); - foreach (var set in restrictedMemberOfSets) - { + foreach (var set in restrictedMemberOfSets) { var results = data[set.Key]; var members = set.Select(x => x.ToTypedPrincipal()).ToList(); results.RestrictedMemberOf = members; @@ -178,15 +169,12 @@ public async Task ReadGPOLocalGroups(string gpLink, string var localGroupSets = actions.Where(x => x.Target == GroupActionTarget.LocalGroup) .GroupBy(x => x.TargetRid); - foreach (var set in localGroupSets) - { + foreach (var set in localGroupSets) { var results = data[set.Key]; - foreach (var temp in set) - { + foreach (var temp in set) { var res = temp.ToTypedPrincipal(); var newMembers = results.LocalGroups; - switch (temp.Action) - { + switch (temp.Action) { case GroupActionOperation.Add: newMembers.Add(res); break; @@ -206,12 +194,11 @@ public async Task ReadGPOLocalGroups(string gpLink, string } } - ret.AffectedComputers = affectedComputers; + ret.AffectedComputers = affectedComputers.ToArray(); //At this point, we've resolved individual add/substract methods for each linked GPO. //Now we need to actually squish them together into the resulting set of changes - foreach (var kvp in data) - { + foreach (var kvp in data) { var key = kvp.Key; var val = kvp.Value; var rm = val.RestrictedMember; @@ -226,8 +213,7 @@ public async Task ReadGPOLocalGroups(string gpLink, string var finalArr = final.Distinct().ToArray(); - switch (key) - { + switch (key) { case LocalGroupRids.Administrators: ret.LocalAdmins = finalArr; break; @@ -252,20 +238,17 @@ public async Task ReadGPOLocalGroups(string gpLink, string /// /// /// - internal async IAsyncEnumerable ProcessGPOTemplateFile(string basePath, string gpoDomain) - { + internal async IAsyncEnumerable ProcessGPOTemplateFile(string basePath, string gpoDomain) { var templatePath = Path.Combine(basePath, "MACHINE", "Microsoft", "Windows NT", "SecEdit", "GptTmpl.inf"); if (!File.Exists(templatePath)) yield break; FileStream fs; - try - { + try { fs = new FileStream(templatePath, FileMode.Open, FileAccess.Read); } - catch - { + catch { yield break; } @@ -281,8 +264,7 @@ internal async IAsyncEnumerable ProcessGPOTemplateFile(string baseP //Split our text into individual lines var memberLines = Regex.Split(memberText, @"\r\n|\r|\n"); - foreach (var memberLine in memberLines) - { + foreach (var memberLine in memberLines) { //Check if the Key regex matches (S-1-5.*_memberof=blah) var keyMatch = KeyRegex.Match(memberLine); @@ -296,53 +278,44 @@ internal async IAsyncEnumerable ProcessGPOTemplateFile(string baseP var rightMatches = MemberRightRegex.Matches(val); //If leftmatch is a success, the members of a group are being explicitly set - if (leftMatch.Success) - { + if (leftMatch.Success) { var extracted = ExtractRid.Match(leftMatch.Value); var rid = int.Parse(extracted.Groups[1].Value); if (Enum.IsDefined(typeof(LocalGroupRids), rid)) //Loop over the members in the match, and try to convert them to SIDs - foreach (var member in val.Split(',')) - { - var res = GetSid(member.Trim('*'), gpoDomain); - if (res == null) - continue; - yield return new GroupAction - { - Target = GroupActionTarget.RestrictedMember, - Action = GroupActionOperation.Add, - TargetSid = res.ObjectIdentifier, - TargetType = res.ObjectType, - TargetRid = (LocalGroupRids) rid - }; + foreach (var member in val.Split(',')) { + if (await GetSid(member.Trim('*'), gpoDomain) is (true, var res)) { + yield return new GroupAction { + Target = GroupActionTarget.RestrictedMember, + Action = GroupActionOperation.Add, + TargetSid = res.ObjectIdentifier, + TargetType = res.ObjectType, + TargetRid = (LocalGroupRids)rid + }; + } } } //If right match is a success, a group has been set as a member of one of our local groups var index = key.IndexOf("MemberOf", StringComparison.CurrentCultureIgnoreCase); - if (rightMatches.Count > 0 && index > 0) - { + if (rightMatches.Count > 0 && index > 0) { var account = key.Trim('*').Substring(0, index - 3).ToUpper(); - var res = GetSid(account, gpoDomain); - if (res == null) - continue; + if (await GetSid(account, gpoDomain) is (true, var res)) { + foreach (var match in rightMatches) { + var rid = int.Parse(ExtractRid.Match(match.ToString()).Groups[1].Value); + if (!Enum.IsDefined(typeof(LocalGroupRids), rid)) continue; - foreach (var match in rightMatches) - { - var rid = int.Parse(ExtractRid.Match(match.ToString()).Groups[1].Value); - if (!Enum.IsDefined(typeof(LocalGroupRids), rid)) continue; - - var targetGroup = (LocalGroupRids) rid; - yield return new GroupAction - { - Target = GroupActionTarget.RestrictedMemberOf, - Action = GroupActionOperation.Add, - TargetRid = targetGroup, - TargetSid = res.ObjectIdentifier, - TargetType = res.ObjectType - }; + var targetGroup = (LocalGroupRids)rid; + yield return new GroupAction { + Target = GroupActionTarget.RestrictedMemberOf, + Action = GroupActionOperation.Add, + TargetRid = targetGroup, + TargetSid = res.ObjectIdentifier, + TargetType = res.ObjectType + }; + } } } } @@ -354,21 +327,17 @@ internal async IAsyncEnumerable ProcessGPOTemplateFile(string baseP /// /// /// - private TypedPrincipal GetSid(string account, string domainName) - { - if (!account.StartsWith("S-1-", StringComparison.CurrentCulture)) - { + private async Task<(bool Success, TypedPrincipal Principal)> GetSid(string account, string domainName) { + if (!account.StartsWith("S-1-", StringComparison.CurrentCulture)) { string user; string domain; - if (account.Contains('\\')) - { + if (account.Contains('\\')) { //The account is in the format DOMAIN\\username var split = account.Split('\\'); domain = split[0]; user = split[1]; } - else - { + else { //The account is just a username, so try with the current domain domain = domainName; user = account; @@ -377,21 +346,15 @@ private TypedPrincipal GetSid(string account, string domainName) user = user.ToUpper(); //Try to resolve as a user object first - var res = _utils.ResolveAccountName(user, domain); - if (res != null) - return res; + var (success, res) = await _utils.ResolveAccountName(user, domain); + if (success) + return (true, res); - res = _utils.ResolveAccountName($"{user}$", domain); - return res; + return await _utils.ResolveAccountName($"{user}$", domain); } //The element is just a sid, so return it straight - var lType = _utils.LookupSidType(account, domainName); - return new TypedPrincipal - { - ObjectIdentifier = account, - ObjectType = lType - }; + return await _utils.ResolveIDAndType(account, domainName); } /// @@ -400,8 +363,7 @@ private TypedPrincipal GetSid(string account, string domainName) /// /// /// A list of GPO "Actions" - internal IEnumerable ProcessGPOXmlFile(string basePath, string gpoDomain) - { + internal async IAsyncEnumerable ProcessGPOXmlFile(string basePath, string gpoDomain) { var xmlPath = Path.Combine(basePath, "MACHINE", "Preferences", "Groups", "Groups.xml"); //If the file doesn't exist, then just return @@ -410,12 +372,10 @@ internal IEnumerable ProcessGPOXmlFile(string basePath, string gpoD //Create an XPathDocument to let us navigate the XML XPathDocument doc; - try - { + try { doc = new XPathDocument(xmlPath); } - catch (Exception e) - { + catch (Exception e) { _log.LogError(e, "error reading GPO XML file {File}", xmlPath); yield break; } @@ -424,20 +384,17 @@ internal IEnumerable ProcessGPOXmlFile(string basePath, string gpoD //Grab all the Groups nodes var groupsNodes = navigator.Select("/Groups"); - while (groupsNodes.MoveNext()) - { + while (groupsNodes.MoveNext()) { var current = groupsNodes.Current; //If disable is set to 1, then this Group wont apply if (current.GetAttribute("disabled", "") is "1") continue; var groupNodes = current.Select("Group"); - while (groupNodes.MoveNext()) - { + while (groupNodes.MoveNext()) { //Grab the properties for each Group node. Current path is /Groups/Group var groupProperties = groupNodes.Current.Select("Properties"); - while (groupProperties.MoveNext()) - { + while (groupProperties.MoveNext()) { var currentProperties = groupProperties.Current; var action = currentProperties.GetAttribute("action", ""); @@ -451,15 +408,13 @@ internal IEnumerable ProcessGPOXmlFile(string basePath, string gpoD //Next is to determine what group is being updated. var targetGroup = LocalGroupRids.None; - if (!string.IsNullOrWhiteSpace(groupSid)) - { + if (!string.IsNullOrWhiteSpace(groupSid)) { //Use a regex to match and attempt to extract the RID var s = ExtractRid.Match(groupSid); - if (s.Success) - { + if (s.Success) { var rid = int.Parse(s.Groups[1].Value); if (Enum.IsDefined(typeof(LocalGroupRids), rid)) - targetGroup = (LocalGroupRids) rid; + targetGroup = (LocalGroupRids)rid; } } @@ -474,16 +429,14 @@ internal IEnumerable ProcessGPOXmlFile(string basePath, string gpoD var deleteGroups = currentProperties.GetAttribute("deleteAllGroups", "") == "1"; if (deleteUsers) - yield return new GroupAction - { + yield return new GroupAction { Action = GroupActionOperation.DeleteUsers, Target = GroupActionTarget.LocalGroup, TargetRid = targetGroup }; if (deleteGroups) - yield return new GroupAction - { + yield return new GroupAction { Action = GroupActionOperation.DeleteGroups, Target = GroupActionTarget.LocalGroup, TargetRid = targetGroup @@ -491,8 +444,7 @@ internal IEnumerable ProcessGPOXmlFile(string basePath, string gpoD //Get all the actual members being added var members = currentProperties.Select("Members/Member"); - while (members.MoveNext()) - { + while (members.MoveNext()) { var memberAction = members.Current.GetAttribute("action", "") .Equals("ADD", StringComparison.OrdinalIgnoreCase) ? GroupActionOperation.Add @@ -501,55 +453,25 @@ internal IEnumerable ProcessGPOXmlFile(string basePath, string gpoD var memberName = members.Current.GetAttribute("name", ""); var memberSid = members.Current.GetAttribute("sid", ""); - var ga = new GroupAction - { + var ga = new GroupAction { Action = memberAction }; //If we have a memberSid, this is the best case scenario - if (!string.IsNullOrWhiteSpace(memberSid)) - { - var memberType = - _utils.LookupSidType(memberSid, _utils.GetDomainNameFromSid(memberSid)); - ga.Target = GroupActionTarget.LocalGroup; - ga.TargetSid = memberSid; - ga.TargetType = memberType; - ga.TargetRid = targetGroup; - - yield return ga; - continue; - } - - //If we have a memberName, we need to resolve it to a SID/Type - if (!string.IsNullOrWhiteSpace(memberName)) - { - //Check if the name is domain prefixed - if (memberName.Contains("\\")) - { - var s = memberName.Split('\\'); - var name = s[1]; - var domain = s[0]; - - var res = _utils.ResolveAccountName(name, domain); - if (res == null) - { - _log.LogWarning("Failed to resolve member {memberName}", memberName); - continue; - } + if (!string.IsNullOrWhiteSpace(memberSid)) { + if (await _utils.ResolveIDAndType(memberSid, gpoDomain) is (true, var res)) { ga.Target = GroupActionTarget.LocalGroup; - ga.TargetSid = res.ObjectIdentifier; + ga.TargetSid = memberSid; ga.TargetType = res.ObjectType; ga.TargetRid = targetGroup; + yield return ga; } - else - { - var res = _utils.ResolveAccountName(memberName, gpoDomain); - if (res == null) - { - _log.LogWarning("Failed to resolve member {memberName}", memberName); - continue; - } + } + + //If we have a memberName, we need to resolve it to a SID/Type + if (!string.IsNullOrWhiteSpace(memberName)) { + if (await GetSid(memberName, gpoDomain) is (true, var res)) { ga.Target = GroupActionTarget.LocalGroup; ga.TargetSid = res.ObjectIdentifier; ga.TargetType = res.ObjectType; @@ -566,25 +488,43 @@ internal IEnumerable ProcessGPOXmlFile(string basePath, string gpoD /// /// Represents an action from a GPO /// - internal class GroupAction - { + internal class GroupAction { internal GroupActionOperation Action { get; set; } internal GroupActionTarget Target { get; set; } internal string TargetSid { get; set; } internal Label TargetType { get; set; } internal LocalGroupRids TargetRid { get; set; } - public TypedPrincipal ToTypedPrincipal() - { - return new TypedPrincipal - { + public TypedPrincipal ToTypedPrincipal() { + return new TypedPrincipal { ObjectIdentifier = TargetSid, ObjectType = TargetType }; } - public override string ToString() - { + protected bool Equals(GroupAction other) { + return Action == other.Action && Target == other.Target && TargetSid == other.TargetSid && TargetType == other.TargetType && TargetRid == other.TargetRid; + } + + public override bool Equals(object obj) { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((GroupAction)obj); + } + + public override int GetHashCode() { + unchecked { + var hashCode = (int)Action; + hashCode = (hashCode * 397) ^ (int)Target; + hashCode = (hashCode * 397) ^ (TargetSid != null ? TargetSid.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (int)TargetType; + hashCode = (hashCode * 397) ^ (int)TargetRid; + return hashCode; + } + } + + public override string ToString() { return $"{nameof(Action)}: {Action}, {nameof(Target)}: {Target}, {nameof(TargetSid)}: {TargetSid}, {nameof(TargetType)}: {TargetType}, {nameof(TargetRid)}: {TargetRid}"; } @@ -593,30 +533,26 @@ public override string ToString() /// /// Storage for each different group type /// - public class GroupResults - { + public class GroupResults { public List LocalGroups = new(); public List RestrictedMember = new(); public List RestrictedMemberOf = new(); } - internal enum GroupActionOperation - { + internal enum GroupActionOperation { Add, Delete, DeleteUsers, DeleteGroups } - internal enum GroupActionTarget - { + internal enum GroupActionTarget { RestrictedMemberOf, RestrictedMember, LocalGroup } - internal enum LocalGroupRids - { + internal enum LocalGroupRids { None = 0, Administrators = 544, RemoteDesktopUsers = 555, diff --git a/src/CommonLib/Processors/GroupProcessor.cs b/src/CommonLib/Processors/GroupProcessor.cs index 26bad106..8df10b30 100644 --- a/src/CommonLib/Processors/GroupProcessor.cs +++ b/src/CommonLib/Processors/GroupProcessor.cs @@ -6,26 +6,23 @@ using SharpHoundCommonLib.Enums; using SharpHoundCommonLib.OutputTypes; -namespace SharpHoundCommonLib.Processors -{ - public class GroupProcessor - { +namespace SharpHoundCommonLib.Processors { + public class GroupProcessor { private readonly ILogger _log; - private readonly ILDAPUtils _utils; + private readonly ILdapUtils _utils; - public GroupProcessor(ILDAPUtils utils, ILogger log = null) - { + public GroupProcessor(ILdapUtils utils, ILogger log = null) { _utils = utils; _log = log ?? Logging.LogProvider.CreateLogger("GroupProc"); } - - public IEnumerable ReadGroupMembers(ResolvedSearchResult result, ISearchResultEntry entry) - { - var members = entry.GetArrayProperty(LDAPProperties.Members); - var name = result.DisplayName; - var dn = entry.DistinguishedName; - return ReadGroupMembers(dn, members, name); + public IAsyncEnumerable ReadGroupMembers(ResolvedSearchResult result, IDirectoryObject entry) { + if (entry.TryGetArrayProperty(LDAPProperties.Members, out var members) && + entry.TryGetDistinguishedName(out var dn)) { + return ReadGroupMembers(dn, members, result.DisplayName); + } + + return AsyncEnumerable.Empty(); } /// @@ -35,52 +32,39 @@ public IEnumerable ReadGroupMembers(ResolvedSearchResult result, /// /// /// - public IEnumerable ReadGroupMembers(string distinguishedName, string[] members, - string objectName = "") - { + public async IAsyncEnumerable ReadGroupMembers(string distinguishedName, string[] members, + string objectName = "") { + _log.LogDebug("Running Group Membership Enumeration for {ObjectName}", objectName); // If our returned array has a length of 0, one of two things is happening // The first possibility we'll look at is we need to use ranged retrieval, because AD will not return // more than a certain number of items. If we get nothing back from this, then the group is empty - if (members.Length == 0) - { - _log.LogTrace("Member property for {ObjectName} is empty, trying range retrieval", + if (members.Length == 0) { + _log.LogDebug("Member property for {ObjectName} is empty, trying range retrieval", objectName); - foreach (var member in _utils.DoRangedRetrieval(distinguishedName, "member")) - { - _log.LogTrace("Got member {DN} for {ObjectName} from ranged retrieval", member, objectName); - var res = _utils.ResolveDistinguishedName(member); + await foreach (var result in _utils.RangedRetrieval(distinguishedName, "member")) { + if (!result.IsSuccess) { + _log.LogDebug("Failure during ranged retrieval for {ObjectName}: {Message}", objectName, result.Error); + yield break; + } - if (res == null) - yield return new TypedPrincipal - { - ObjectIdentifier = member.ToUpper(), - ObjectType = Label.Base - }; - else - { - if (!Helpers.IsSidFiltered(res.ObjectIdentifier)) - yield return res; + var member = result.Value; + _log.LogTrace("Got member {DN} for {ObjectName} from ranged retrieval", member, objectName); + if (await _utils.ResolveDistinguishedName(member) is (true, var res) && + !Helpers.IsSidFiltered(res.ObjectIdentifier)) { + yield return res; + } else { + yield return new TypedPrincipal(member.ToUpper(), Label.Base); } } - } - else - { + } else { //If we're here, we just read the data directly and life is good - foreach (var member in members) - { + foreach (var member in members) { _log.LogTrace("Got member {DN} for {ObjectName}", member, objectName); - var res = _utils.ResolveDistinguishedName(member); - - if (res == null) - yield return new TypedPrincipal - { - ObjectIdentifier = member.ToUpper(), - ObjectType = Label.Base - }; - else - { - if (!Helpers.IsSidFiltered(res.ObjectIdentifier)) - yield return res; + if (await _utils.ResolveDistinguishedName(member) is (true, var res) && + !Helpers.IsSidFiltered(res.ObjectIdentifier)) { + yield return res; + } else { + yield return new TypedPrincipal(member.ToUpper(), Label.Base); } } } @@ -92,22 +76,18 @@ public IEnumerable ReadGroupMembers(string distinguishedName, st /// /// /// - public static string GetPrimaryGroupInfo(string primaryGroupId, string objectId) - { + public static string GetPrimaryGroupInfo(string primaryGroupId, string objectId) { if (primaryGroupId == null) return null; if (objectId == null) return null; - try - { + try { var domainSid = new SecurityIdentifier(objectId).AccountDomainSid.Value; var primaryGroupSid = $"{domainSid}-{primaryGroupId}"; return primaryGroupSid; - } - catch - { + } catch { return null; } } diff --git a/src/CommonLib/Processors/LDAPPropertyProcessor.cs b/src/CommonLib/Processors/LdapPropertyProcessor.cs similarity index 71% rename from src/CommonLib/Processors/LDAPPropertyProcessor.cs rename to src/CommonLib/Processors/LdapPropertyProcessor.cs index 33cd6d4d..784b091b 100644 --- a/src/CommonLib/Processors/LDAPPropertyProcessor.cs +++ b/src/CommonLib/Processors/LdapPropertyProcessor.cs @@ -11,36 +11,34 @@ using SharpHoundCommonLib.Enums; using SharpHoundCommonLib.LDAPQueries; using SharpHoundCommonLib.OutputTypes; +// ReSharper disable StringLiteralTypo -namespace SharpHoundCommonLib.Processors -{ - public class LDAPPropertyProcessor - { +namespace SharpHoundCommonLib.Processors { + public class LdapPropertyProcessor { private static readonly string[] ReservedAttributes = CommonProperties.TypeResolutionProps .Concat(CommonProperties.BaseQueryProps).Concat(CommonProperties.GroupResolutionProps) .Concat(CommonProperties.ComputerMethodProps).Concat(CommonProperties.ACLProps) .Concat(CommonProperties.ObjectPropsProps).Concat(CommonProperties.ContainerProps) .Concat(CommonProperties.SPNTargetProps).Concat(CommonProperties.DomainTrustProps) - .Concat(CommonProperties.GPOLocalGroupProps).ToArray(); + .Concat(CommonProperties.GPOLocalGroupProps).Concat(CommonProperties.CertAbuseProps).ToArray(); - private readonly ILDAPUtils _utils; + private readonly ILdapUtils _utils; - public LDAPPropertyProcessor(ILDAPUtils utils) - { + public LdapPropertyProcessor(ILdapUtils utils) { _utils = utils; } - private static Dictionary GetCommonProps(ISearchResultEntry entry) - { - return new Dictionary - { - { - "description", entry.GetProperty(LDAPProperties.Description) - }, - { - "whencreated", Helpers.ConvertTimestampToUnixEpoch(entry.GetProperty(LDAPProperties.WhenCreated)) - } - }; + private static Dictionary GetCommonProps(IDirectoryObject entry) { + var ret = new Dictionary(); + if (entry.TryGetProperty(LDAPProperties.Description, out var description)) { + ret["description"] = description; + } + + if (entry.TryGetProperty(LDAPProperties.WhenCreated, out var wc)) { + ret["whencreated"] = Helpers.ConvertTimestampToUnixEpoch(wc); + } + + return ret; } /// @@ -48,13 +46,14 @@ private static Dictionary GetCommonProps(ISearchResultEntry entr /// /// /// - public static Dictionary ReadDomainProperties(ISearchResultEntry entry) - { + public static Dictionary ReadDomainProperties(IDirectoryObject entry) { var props = GetCommonProps(entry); - if (!int.TryParse(entry.GetProperty(LDAPProperties.DomainFunctionalLevel), out var level)) level = -1; + if (!entry.TryGetIntProperty(LDAPProperties.DomainFunctionalLevel, out var functionalLevel)) { + functionalLevel = -1; + } - props.Add("functionallevel", FunctionalLevelToString(level)); + props.Add("functionallevel", FunctionalLevelToString(functionalLevel)); return props; } @@ -64,10 +63,8 @@ public static Dictionary ReadDomainProperties(ISearchResultEntry /// /// /// - public static string FunctionalLevelToString(int level) - { - var functionalLevel = level switch - { + public static string FunctionalLevelToString(int level) { + var functionalLevel = level switch { 0 => "2000 Mixed/Native", 1 => "2003 Interim", 2 => "2003", @@ -87,10 +84,10 @@ public static string FunctionalLevelToString(int level) /// /// /// - public static Dictionary ReadGPOProperties(ISearchResultEntry entry) - { + public static Dictionary ReadGPOProperties(IDirectoryObject entry) { var props = GetCommonProps(entry); - props.Add("gpcpath", entry.GetProperty(LDAPProperties.GPCFileSYSPath)?.ToUpper()); + entry.TryGetProperty(LDAPProperties.GPCFileSYSPath, out var path); + props.Add("gpcpath", path.ToUpper()); return props; } @@ -99,8 +96,7 @@ public static Dictionary ReadGPOProperties(ISearchResultEntry en /// /// /// - public static Dictionary ReadOUProperties(ISearchResultEntry entry) - { + public static Dictionary ReadOUProperties(IDirectoryObject entry) { var props = GetCommonProps(entry); return props; } @@ -110,21 +106,10 @@ public static Dictionary ReadOUProperties(ISearchResultEntry ent /// /// /// - public static Dictionary ReadGroupProperties(ISearchResultEntry entry) - { + public static Dictionary ReadGroupProperties(IDirectoryObject entry) { var props = GetCommonProps(entry); - - var ac = entry.GetProperty(LDAPProperties.AdminCount); - if (ac != null) - { - var a = int.Parse(ac); - props.Add("admincount", a != 0); - } - else - { - props.Add("admincount", false); - } - + entry.TryGetIntProperty(LDAPProperties.AdminCount, out var ac); + props.Add("admincount", ac != 0); return props; } @@ -133,27 +118,29 @@ public static Dictionary ReadGroupProperties(ISearchResultEntry /// /// /// - public static Dictionary ReadContainerProperties(ISearchResultEntry entry) - { + public static Dictionary ReadContainerProperties(IDirectoryObject entry) { var props = GetCommonProps(entry); return props; } + public Task + ReadUserProperties(IDirectoryObject entry, ResolvedSearchResult searchResult) { + return ReadUserProperties(entry, searchResult.Domain); + } + /// /// Reads specific LDAP properties related to Users /// /// + /// /// - public async Task ReadUserProperties(ISearchResultEntry entry) - { + public async Task ReadUserProperties(IDirectoryObject entry, string domain) { var userProps = new UserProperties(); var props = GetCommonProps(entry); var uacFlags = (UacFlags)0; - var uac = entry.GetProperty(LDAPProperties.UserAccountControl); - if (int.TryParse(uac, out var flag)) - { - uacFlags = (UacFlags)flag; + if (entry.TryGetIntProperty(LDAPProperties.UserAccountControl, out var uac)) { + uacFlags = (UacFlags)uac; } props.Add("sensitive", uacFlags.HasFlag(UacFlags.NotDelegated)); @@ -164,24 +151,19 @@ public async Task ReadUserProperties(ISearchResultEntry entry) props.Add("enabled", !uacFlags.HasFlag(UacFlags.AccountDisable)); props.Add("trustedtoauth", uacFlags.HasFlag(UacFlags.TrustedToAuthForDelegation)); - var domain = Helpers.DistinguishedNameToDomain(entry.DistinguishedName); - var comps = new List(); - if (uacFlags.HasFlag(UacFlags.TrustedToAuthForDelegation)) - { - var delegates = entry.GetArrayProperty(LDAPProperties.AllowedToDelegateTo); + if (uacFlags.HasFlag(UacFlags.TrustedToAuthForDelegation) && + entry.TryGetArrayProperty(LDAPProperties.AllowedToDelegateTo, out var delegates)) { props.Add("allowedtodelegate", delegates); - foreach (var d in delegates) - { + foreach (var d in delegates) { if (d == null) continue; var resolvedHost = await _utils.ResolveHostToSid(d, domain); - if (resolvedHost != null && resolvedHost.Contains("S-1")) - comps.Add(new TypedPrincipal - { - ObjectIdentifier = resolvedHost, + if (resolvedHost.Success && resolvedHost.SecurityIdentifier.Contains("S-1")) + comps.Add(new TypedPrincipal { + ObjectIdentifier = resolvedHost.SecurityIdentifier, ObjectType = Label.Computer }); } @@ -189,12 +171,24 @@ public async Task ReadUserProperties(ISearchResultEntry entry) userProps.AllowedToDelegate = comps.Distinct().ToArray(); - props.Add("lastlogon", Helpers.ConvertFileTimeToUnixEpoch(entry.GetProperty(LDAPProperties.LastLogon))); - props.Add("lastlogontimestamp", - Helpers.ConvertFileTimeToUnixEpoch(entry.GetProperty(LDAPProperties.LastLogonTimestamp))); + if (!entry.TryGetProperty(LDAPProperties.LastLogon, out var lastLogon)) { + lastLogon = null; + } + + props.Add("lastlogon", Helpers.ConvertFileTimeToUnixEpoch(lastLogon)); + + if (!entry.TryGetProperty(LDAPProperties.LastLogonTimestamp, out var lastLogonTimeStamp)) { + lastLogonTimeStamp = null; + } + + props.Add("lastlogontimestamp", Helpers.ConvertFileTimeToUnixEpoch(lastLogonTimeStamp)); + + if (!entry.TryGetProperty(LDAPProperties.PasswordLastSet, out var passwordLastSet)) { + passwordLastSet = null; + } props.Add("pwdlastset", - Helpers.ConvertFileTimeToUnixEpoch(entry.GetProperty(LDAPProperties.PasswordLastSet))); - var spn = entry.GetArrayProperty(LDAPProperties.ServicePrincipalNames); + Helpers.ConvertFileTimeToUnixEpoch(passwordLastSet)); + entry.TryGetArrayProperty(LDAPProperties.ServicePrincipalNames, out var spn); props.Add("serviceprincipalnames", spn); props.Add("hasspn", spn.Length > 0); props.Add("displayname", entry.GetProperty(LDAPProperties.DisplayName)); @@ -207,39 +201,24 @@ public async Task ReadUserProperties(ISearchResultEntry entry) props.Add("sfupassword", entry.GetProperty(LDAPProperties.MsSFU30Password)); props.Add("logonscript", entry.GetProperty(LDAPProperties.ScriptPath)); - var ac = entry.GetProperty(LDAPProperties.AdminCount); - if (ac != null) - { - if (int.TryParse(ac, out var parsed)) - props.Add("admincount", parsed != 0); - else - props.Add("admincount", false); - } - else - { - props.Add("admincount", false); - } + entry.TryGetIntProperty(LDAPProperties.AdminCount, out var ac); + props.Add("admincount", ac != 0); - var sh = entry.GetByteArrayProperty(LDAPProperties.SIDHistory); + entry.TryGetByteArrayProperty(LDAPProperties.SIDHistory, out var sh); var sidHistoryList = new List(); var sidHistoryPrincipals = new List(); - foreach (var sid in sh) - { + foreach (var sid in sh) { string sSid; - try - { + try { sSid = new SecurityIdentifier(sid, 0).Value; - } - catch - { + } catch { continue; } sidHistoryList.Add(sSid); - var res = _utils.ResolveIDAndType(sSid, domain); - - sidHistoryPrincipals.Add(res); + if (await _utils.ResolveIDAndType(sSid, domain) is (true, var res)) + sidHistoryPrincipals.Add(res); } userProps.SidHistory = sidHistoryPrincipals.Distinct().ToArray(); @@ -251,45 +230,44 @@ public async Task ReadUserProperties(ISearchResultEntry entry) return userProps; } + public Task ReadComputerProperties(IDirectoryObject entry, + ResolvedSearchResult searchResult) { + return ReadComputerProperties(entry, searchResult.Domain); + } + /// /// Reads specific LDAP properties related to Computers /// /// + /// /// - public async Task ReadComputerProperties(ISearchResultEntry entry) - { + public async Task ReadComputerProperties(IDirectoryObject entry, string domain) { var compProps = new ComputerProperties(); var props = GetCommonProps(entry); var flags = (UacFlags)0; - var uac = entry.GetProperty(LDAPProperties.UserAccountControl); - if (int.TryParse(uac, out var flag)) - { - flags = (UacFlags)flag; + if (entry.TryGetIntProperty(LDAPProperties.UserAccountControl, out var uac)) { + flags = (UacFlags)uac; } props.Add("enabled", !flags.HasFlag(UacFlags.AccountDisable)); props.Add("unconstraineddelegation", flags.HasFlag(UacFlags.TrustedForDelegation)); props.Add("trustedtoauth", flags.HasFlag(UacFlags.TrustedToAuthForDelegation)); props.Add("isdc", flags.HasFlag(UacFlags.ServerTrustAccount)); - - var domain = Helpers.DistinguishedNameToDomain(entry.DistinguishedName); - + var comps = new List(); - if (flags.HasFlag(UacFlags.TrustedToAuthForDelegation)) - { - var delegates = entry.GetArrayProperty(LDAPProperties.AllowedToDelegateTo); + if (flags.HasFlag(UacFlags.TrustedToAuthForDelegation) && + entry.TryGetArrayProperty(LDAPProperties.AllowedToDelegateTo, out var delegates)) { props.Add("allowedtodelegate", delegates); - foreach (var d in delegates) - { - var hname = d.Contains("/") ? d.Split('/')[1] : d; - hname = hname.Split(':')[0]; - var resolvedHost = await _utils.ResolveHostToSid(hname, domain); - if (resolvedHost != null && resolvedHost.Contains("S-1")) - comps.Add(new TypedPrincipal - { - ObjectIdentifier = resolvedHost, + foreach (var d in delegates) { + if (d == null) + continue; + + var resolvedHost = await _utils.ResolveHostToSid(d, domain); + if (resolvedHost.Success && resolvedHost.SecurityIdentifier.Contains("S-1")) + comps.Add(new TypedPrincipal { + ObjectIdentifier = resolvedHost.SecurityIdentifier, ObjectType = Label.Computer }); } @@ -298,15 +276,12 @@ public async Task ReadComputerProperties(ISearchResultEntry compProps.AllowedToDelegate = comps.Distinct().ToArray(); var allowedToActPrincipals = new List(); - var rawAllowedToAct = entry.GetByteProperty(LDAPProperties.AllowedToActOnBehalfOfOtherIdentity); - if (rawAllowedToAct != null) - { + if (entry.TryGetByteProperty(LDAPProperties.AllowedToActOnBehalfOfOtherIdentity, out var rawAllowedToAct)) { var sd = _utils.MakeSecurityDescriptor(); sd.SetSecurityDescriptorBinaryForm(rawAllowedToAct, AccessControlSections.Access); - foreach (var rule in sd.GetAccessRules(true, true, typeof(SecurityIdentifier))) - { - var res = _utils.ResolveIDAndType(rule.IdentityReference(), domain); - allowedToActPrincipals.Add(res); + foreach (var rule in sd.GetAccessRules(true, true, typeof(SecurityIdentifier))) { + if (await _utils.ResolveIDAndType(rule.IdentityReference(), domain) is (true, var res)) + allowedToActPrincipals.Add(res); } } @@ -317,7 +292,8 @@ public async Task ReadComputerProperties(ISearchResultEntry Helpers.ConvertFileTimeToUnixEpoch(entry.GetProperty(LDAPProperties.LastLogonTimestamp))); props.Add("pwdlastset", Helpers.ConvertFileTimeToUnixEpoch(entry.GetProperty(LDAPProperties.PasswordLastSet))); - props.Add("serviceprincipalnames", entry.GetArrayProperty(LDAPProperties.ServicePrincipalNames)); + entry.TryGetArrayProperty(LDAPProperties.ServicePrincipalNames, out var spn); + props.Add("serviceprincipalnames", spn); props.Add("email", entry.GetProperty(LDAPProperties.Email)); var os = entry.GetProperty(LDAPProperties.OperatingSystem); var sp = entry.GetProperty(LDAPProperties.ServicePack); @@ -326,41 +302,31 @@ public async Task ReadComputerProperties(ISearchResultEntry props.Add("operatingsystem", os); - var sh = entry.GetByteArrayProperty(LDAPProperties.SIDHistory); + entry.TryGetByteArrayProperty(LDAPProperties.SIDHistory, out var sh); var sidHistoryList = new List(); var sidHistoryPrincipals = new List(); - foreach (var sid in sh) - { + foreach (var sid in sh) { string sSid; - try - { + try { sSid = new SecurityIdentifier(sid, 0).Value; - } - catch - { + } catch { continue; } sidHistoryList.Add(sSid); - var res = _utils.ResolveIDAndType(sSid, domain); - - sidHistoryPrincipals.Add(res); + if (await _utils.ResolveIDAndType(sSid, domain) is (true, var res)) + sidHistoryPrincipals.Add(res); } compProps.SidHistory = sidHistoryPrincipals.ToArray(); props.Add("sidhistory", sidHistoryList.ToArray()); - var hsa = entry.GetArrayProperty(LDAPProperties.HostServiceAccount); var smsaPrincipals = new List(); - if (hsa != null) - { - foreach (var dn in hsa) - { - var resolvedPrincipal = _utils.ResolveDistinguishedName(dn); - - if (resolvedPrincipal != null) + if (entry.TryGetArrayProperty(LDAPProperties.HostServiceAccount, out var hsa)) { + foreach (var dn in hsa) { + if (await _utils.ResolveDistinguishedName(dn) is (true, var resolvedPrincipal)) smsaPrincipals.Add(resolvedPrincipal); } } @@ -377,14 +343,11 @@ public async Task ReadComputerProperties(ISearchResultEntry /// /// /// Returns a dictionary with the common properties of the RootCA - public static Dictionary ReadRootCAProperties(ISearchResultEntry entry) - { + public static Dictionary ReadRootCAProperties(IDirectoryObject entry) { var props = GetCommonProps(entry); // Certificate - var rawCertificate = entry.GetByteProperty(LDAPProperties.CACertificate); - if (rawCertificate != null) - { + if (entry.TryGetByteProperty(LDAPProperties.CACertificate, out var rawCertificate)) { var cert = new ParsedCertificate(rawCertificate); props.Add("certthumbprint", cert.Thumbprint); props.Add("certname", cert.Name); @@ -392,7 +355,7 @@ public static Dictionary ReadRootCAProperties(ISearchResultEntry props.Add("hasbasicconstraints", cert.HasBasicConstraints); props.Add("basicconstraintpathlength", cert.BasicConstraintPathLength); } - + return props; } @@ -401,19 +364,16 @@ public static Dictionary ReadRootCAProperties(ISearchResultEntry /// /// /// Returns a dictionary with the common properties and the crosscertificatepair property of the AICA - public static Dictionary ReadAIACAProperties(ISearchResultEntry entry) - { + public static Dictionary ReadAIACAProperties(IDirectoryObject entry) { var props = GetCommonProps(entry); - var crossCertificatePair = entry.GetByteArrayProperty((LDAPProperties.CrossCertificatePair)); + entry.TryGetByteArrayProperty(LDAPProperties.CrossCertificatePair, out var crossCertificatePair); var hasCrossCertificatePair = crossCertificatePair.Length > 0; props.Add("crosscertificatepair", crossCertificatePair); props.Add("hascrosscertificatepair", hasCrossCertificatePair); // Certificate - var rawCertificate = entry.GetByteProperty(LDAPProperties.CACertificate); - if (rawCertificate != null) - { + if (entry.TryGetByteProperty(LDAPProperties.CACertificate, out var rawCertificate)) { var cert = new ParsedCertificate(rawCertificate); props.Add("certthumbprint", cert.Thumbprint); props.Add("certname", cert.Name); @@ -425,17 +385,14 @@ public static Dictionary ReadAIACAProperties(ISearchResultEntry return props; } - public static Dictionary ReadEnterpriseCAProperties(ISearchResultEntry entry) - { + public static Dictionary ReadEnterpriseCAProperties(IDirectoryObject entry) { var props = GetCommonProps(entry); - if (entry.GetIntProperty("flags", out var flags)) props.Add("flags", (PKICertificateAuthorityFlags)flags); + if (entry.TryGetIntProperty("flags", out var flags)) props.Add("flags", (PKICertificateAuthorityFlags)flags); props.Add("caname", entry.GetProperty(LDAPProperties.Name)); props.Add("dnshostname", entry.GetProperty(LDAPProperties.DNSHostName)); // Certificate - var rawCertificate = entry.GetByteProperty(LDAPProperties.CACertificate); - if (rawCertificate != null) - { + if (entry.TryGetByteProperty(LDAPProperties.CACertificate, out var rawCertificate)) { var cert = new ParsedCertificate(rawCertificate); props.Add("certthumbprint", cert.Thumbprint); props.Add("certname", cert.Name); @@ -452,8 +409,7 @@ public static Dictionary ReadEnterpriseCAProperties(ISearchResul /// /// /// Returns a dictionary with the common properties of the NTAuthStore - public static Dictionary ReadNTAuthStoreProperties(ISearchResultEntry entry) - { + public static Dictionary ReadNTAuthStoreProperties(IDirectoryObject entry) { var props = GetCommonProps(entry); return props; } @@ -463,21 +419,19 @@ public static Dictionary ReadNTAuthStoreProperties(ISearchResult /// /// /// Returns a dictionary associated with the CertTemplate properties that were read - public static Dictionary ReadCertTemplateProperties(ISearchResultEntry entry) - { + public static Dictionary ReadCertTemplateProperties(IDirectoryObject entry) { var props = GetCommonProps(entry); props.Add("validityperiod", ConvertPKIPeriod(entry.GetByteProperty(LDAPProperties.PKIExpirationPeriod))); props.Add("renewalperiod", ConvertPKIPeriod(entry.GetByteProperty(LDAPProperties.PKIOverlappedPeriod))); - if (entry.GetIntProperty(LDAPProperties.TemplateSchemaVersion, out var schemaVersion)) + if (entry.TryGetIntProperty(LDAPProperties.TemplateSchemaVersion, out var schemaVersion)) props.Add("schemaversion", schemaVersion); props.Add("displayname", entry.GetProperty(LDAPProperties.DisplayName)); props.Add("oid", entry.GetProperty(LDAPProperties.CertTemplateOID)); - if (entry.GetIntProperty(LDAPProperties.PKIEnrollmentFlag, out var enrollmentFlagsRaw)) - { + if (entry.TryGetIntProperty(LDAPProperties.PKIEnrollmentFlag, out var enrollmentFlagsRaw)) { var enrollmentFlags = (PKIEnrollmentFlag)enrollmentFlagsRaw; props.Add("enrollmentflag", enrollmentFlags); @@ -485,8 +439,7 @@ public static Dictionary ReadCertTemplateProperties(ISearchResul props.Add("nosecurityextension", enrollmentFlags.HasFlag(PKIEnrollmentFlag.NO_SECURITY_EXTENSION)); } - if (entry.GetIntProperty(LDAPProperties.PKINameFlag, out var nameFlagsRaw)) - { + if (entry.TryGetIntProperty(LDAPProperties.PKINameFlag, out var nameFlagsRaw)) { var nameFlags = (PKICertificateNameFlag)nameFlagsRaw; props.Add("certificatenameflag", nameFlags); @@ -506,51 +459,51 @@ public static Dictionary ReadCertTemplateProperties(ISearchResul nameFlags.HasFlag(PKICertificateNameFlag.SUBJECT_REQUIRE_EMAIL)); } - var ekus = entry.GetArrayProperty(LDAPProperties.ExtendedKeyUsage); + entry.TryGetArrayProperty(LDAPProperties.ExtendedKeyUsage, out var ekus); props.Add("ekus", ekus); - var certificateApplicationPolicy = entry.GetArrayProperty(LDAPProperties.CertificateApplicationPolicy); + entry.TryGetArrayProperty(LDAPProperties.CertificateApplicationPolicy, out var certificateApplicationPolicy); props.Add("certificateapplicationpolicy", certificateApplicationPolicy); - var certificatePolicy = entry.GetArrayProperty(LDAPProperties.CertificatePolicy); + entry.TryGetArrayProperty(LDAPProperties.CertificatePolicy, out var certificatePolicy); props.Add("certificatepolicy", certificatePolicy); - if (entry.GetIntProperty(LDAPProperties.NumSignaturesRequired, out var authorizedSignatures)) + if (entry.TryGetIntProperty(LDAPProperties.NumSignaturesRequired, out var authorizedSignatures)) props.Add("authorizedsignatures", authorizedSignatures); var hasUseLegacyProvider = false; - if (entry.GetIntProperty(LDAPProperties.PKIPrivateKeyFlag, out var privateKeyFlagsRaw)) - { + if (entry.TryGetIntProperty(LDAPProperties.PKIPrivateKeyFlag, out var privateKeyFlagsRaw)) { var privateKeyFlags = (PKIPrivateKeyFlag)privateKeyFlagsRaw; hasUseLegacyProvider = privateKeyFlags.HasFlag(PKIPrivateKeyFlag.USE_LEGACY_PROVIDER); } - props.Add("applicationpolicies", ParseCertTemplateApplicationPolicies(entry.GetArrayProperty(LDAPProperties.ApplicationPolicies), schemaVersion, hasUseLegacyProvider)); - props.Add("issuancepolicies", entry.GetArrayProperty(LDAPProperties.IssuancePolicies)); + entry.TryGetArrayProperty(LDAPProperties.ApplicationPolicies, out var appPolicies); + + props.Add("applicationpolicies", + ParseCertTemplateApplicationPolicies(appPolicies, + schemaVersion, hasUseLegacyProvider)); + entry.TryGetArrayProperty(LDAPProperties.IssuancePolicies, out var issuancePolicies); + props.Add("issuancepolicies", issuancePolicies); // Construct effectiveekus var effectiveekus = schemaVersion == 1 & ekus.Length > 0 ? ekus : certificateApplicationPolicy; props.Add("effectiveekus", effectiveekus); // Construct authenticationenabled - var authenticationEnabled = effectiveekus.Intersect(Helpers.AuthenticationOIDs).Any() | effectiveekus.Length == 0; + var authenticationEnabled = + effectiveekus.Intersect(Helpers.AuthenticationOIDs).Any() | effectiveekus.Length == 0; props.Add("authenticationenabled", authenticationEnabled); return props; } - public IssuancePolicyProperties ReadIssuancePolicyProperties(ISearchResultEntry entry) - { + public async Task ReadIssuancePolicyProperties(IDirectoryObject entry) { var ret = new IssuancePolicyProperties(); var props = GetCommonProps(entry); props.Add("displayname", entry.GetProperty(LDAPProperties.DisplayName)); props.Add("certtemplateoid", entry.GetProperty(LDAPProperties.CertTemplateOID)); - var link = entry.GetProperty(LDAPProperties.OIDGroupLink); - if (!string.IsNullOrEmpty(link)) - { - var linkedGroup = _utils.ResolveDistinguishedName(link); - if (linkedGroup != null) - { + if (entry.TryGetProperty(LDAPProperties.OIDGroupLink, out var link)) { + if (await _utils.ResolveDistinguishedName(link) is (true, var linkedGroup)) { props.Add("oidgrouplink", linkedGroup.ObjectIdentifier); ret.GroupLink = linkedGroup; } @@ -565,44 +518,28 @@ public IssuancePolicyProperties ReadIssuancePolicyProperties(ISearchResultEntry /// format using a best guess /// /// - public Dictionary ParseAllProperties(ISearchResultEntry entry) - { + public Dictionary ParseAllProperties(IDirectoryObject entry) { var props = new Dictionary(); - - var type = typeof(LDAPProperties); - var reserved = type.GetFields(BindingFlags.Static | BindingFlags.Public).Select(x => x.GetValue(null).ToString()).ToArray(); - - foreach (var property in entry.PropertyNames()) - { + foreach (var property in entry.PropertyNames()) { if (ReservedAttributes.Contains(property, StringComparer.OrdinalIgnoreCase)) continue; - var collCount = entry.PropCount(property); + var collCount = entry.PropertyCount(property); if (collCount == 0) continue; - if (collCount == 1) - { - var testBytes = entry.GetByteProperty(property); - - if (testBytes == null || testBytes.Length == 0) continue; - + if (collCount == 1) { var testString = entry.GetProperty(property); if (!string.IsNullOrEmpty(testString)) - if (property == "badpasswordtime") + if (property.Equals("badpasswordtime", StringComparison.OrdinalIgnoreCase)) props.Add(property, Helpers.ConvertFileTimeToUnixEpoch(testString)); else props.Add(property, BestGuessConvert(testString)); - } - else - { - var arrBytes = entry.GetByteArrayProperty(property); - if (arrBytes.Length == 0) - continue; - - var arr = entry.GetArrayProperty(property); - if (arr.Length > 0) props.Add(property, arr.Select(BestGuessConvert).ToArray()); + } else { + if (entry.TryGetArrayProperty(property, out var arr) && arr.Length > 0) { + props.Add(property, arr.Select(BestGuessConvert).ToArray()); + } } } @@ -615,25 +552,23 @@ public Dictionary ParseAllProperties(ISearchResultEntry entry) /// /// /// - private static string[] ParseCertTemplateApplicationPolicies(string[] applicationPolicies, int schemaVersion, bool hasUseLegacyProvider) - { + private static string[] ParseCertTemplateApplicationPolicies(string[] applicationPolicies, int schemaVersion, + bool hasUseLegacyProvider) { if (applicationPolicies == null || applicationPolicies.Length == 0 || schemaVersion == 1 || schemaVersion == 2 - || (schemaVersion == 4 && hasUseLegacyProvider)) - { + || (schemaVersion == 4 && hasUseLegacyProvider)) { return applicationPolicies; - } - else - { + } else { // Format: "Name`Type`Value`Name`Type`Value`..." // (https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-crtd/c55ec697-be3f-4117-8316-8895e4399237) // Return the Value of Name = "msPKI-RA-Application-Policies" entries var entries = applicationPolicies[0].Split('`'); return Enumerable.Range(0, entries.Length / 3) .Select(i => entries.Skip(i * 3).Take(3).ToArray()) - .Where(parts => parts.Length == 3 && parts[0].Equals(LDAPProperties.ApplicationPolicies, StringComparison.OrdinalIgnoreCase)) + .Where(parts => parts.Length == 3 && parts[0].Equals(LDAPProperties.ApplicationPolicies, + StringComparison.OrdinalIgnoreCase)) .Select(parts => parts[2]) .ToArray(); } @@ -644,8 +579,7 @@ private static string[] ParseCertTemplateApplicationPolicies(string[] applicatio /// /// /// - private static object BestGuessConvert(string property) - { + private static object BestGuessConvert(string property) { //Parse boolean values if (bool.TryParse(property, out var boolResult)) return boolResult; @@ -668,56 +602,47 @@ private static object BestGuessConvert(string property) /// https://www.sysadmins.lv/blog-en/how-to-convert-pkiexirationperiod-and-pkioverlapperiod-active-directory-attributes.aspx /// /// Returns a string representing the time period associated with the input byte array in a human readable form - private static string ConvertPKIPeriod(byte[] bytes) - { + private static string ConvertPKIPeriod(byte[] bytes) { if (bytes == null || bytes.Length == 0) return "Unknown"; - try - { + try { Array.Reverse(bytes); var temp = BitConverter.ToString(bytes).Replace("-", ""); var value = Convert.ToInt64(temp, 16) * -.0000001; - if (value % 31536000 == 0 && value / 31536000 >= 1) - { + if (value % 31536000 == 0 && value / 31536000 >= 1) { if (value / 31536000 == 1) return "1 year"; return $"{value / 31536000} years"; } - if (value % 2592000 == 0 && value / 2592000 >= 1) - { + if (value % 2592000 == 0 && value / 2592000 >= 1) { if (value / 2592000 == 1) return "1 month"; return $"{value / 2592000} months"; } - if (value % 604800 == 0 && value / 604800 >= 1) - { + if (value % 604800 == 0 && value / 604800 >= 1) { if (value / 604800 == 1) return "1 week"; return $"{value / 604800} weeks"; } - if (value % 86400 == 0 && value / 86400 >= 1) - { + if (value % 86400 == 0 && value / 86400 >= 1) { if (value / 86400 == 1) return "1 day"; return $"{value / 86400} days"; } - if (value % 3600 == 0 && value / 3600 >= 1) - { + if (value % 3600 == 0 && value / 3600 >= 1) { if (value / 3600 == 1) return "1 hour"; return $"{value / 3600} hours"; } return ""; - } - catch (Exception) - { + } catch (Exception) { return "Unknown"; } } @@ -728,8 +653,7 @@ private static string ConvertPKIPeriod(byte[] bytes) [Flags] [SuppressMessage("ReSharper", "UnusedMember.Local")] [SuppressMessage("ReSharper", "InconsistentNaming")] - private enum IsTextUnicodeFlags - { + private enum IsTextUnicodeFlags { IS_TEXT_UNICODE_ASCII16 = 0x0001, IS_TEXT_UNICODE_REVERSE_ASCII16 = 0x0010, @@ -754,16 +678,14 @@ private enum IsTextUnicodeFlags } } - public class ParsedCertificate - { + public class ParsedCertificate { public string Thumbprint { get; set; } public string Name { get; set; } public string[] Chain { get; set; } = Array.Empty(); public bool HasBasicConstraints { get; set; } = false; public int BasicConstraintPathLength { get; set; } - public ParsedCertificate(byte[] rawCertificate) - { + public ParsedCertificate(byte[] rawCertificate) { var parsedCertificate = new X509Certificate2(rawCertificate); Thumbprint = parsedCertificate.Thumbprint; var name = parsedCertificate.FriendlyName; @@ -780,11 +702,9 @@ public ParsedCertificate(byte[] rawCertificate) // Extensions X509ExtensionCollection extensions = parsedCertificate.Extensions; List certificateExtensions = new List(); - foreach (X509Extension extension in extensions) - { + foreach (X509Extension extension in extensions) { CertificateExtension certificateExtension = new CertificateExtension(extension); - switch (certificateExtension.Oid.Value) - { + switch (certificateExtension.Oid.Value) { case CAExtensionTypes.BasicConstraints: X509BasicConstraintsExtension ext = (X509BasicConstraintsExtension)extension; HasBasicConstraints = ext.HasPathLengthConstraint; @@ -795,15 +715,13 @@ public ParsedCertificate(byte[] rawCertificate) } } - public class UserProperties - { + public class UserProperties { public Dictionary Props { get; set; } = new(); public TypedPrincipal[] AllowedToDelegate { get; set; } = Array.Empty(); public TypedPrincipal[] SidHistory { get; set; } = Array.Empty(); } - public class ComputerProperties - { + public class ComputerProperties { public Dictionary Props { get; set; } = new(); public TypedPrincipal[] AllowedToDelegate { get; set; } = Array.Empty(); public TypedPrincipal[] AllowedToAct { get; set; } = Array.Empty(); @@ -811,9 +729,8 @@ public class ComputerProperties public TypedPrincipal[] DumpSMSAPassword { get; set; } = Array.Empty(); } - public class IssuancePolicyProperties - { + public class IssuancePolicyProperties { public Dictionary Props { get; set; } = new(); public TypedPrincipal GroupLink { get; set; } = new TypedPrincipal(); } -} +} \ No newline at end of file diff --git a/src/CommonLib/Processors/LocalGroupProcessor.cs b/src/CommonLib/Processors/LocalGroupProcessor.cs index 620da44e..a1c35090 100644 --- a/src/CommonLib/Processors/LocalGroupProcessor.cs +++ b/src/CommonLib/Processors/LocalGroupProcessor.cs @@ -6,7 +6,6 @@ using Microsoft.Extensions.Logging; using SharpHoundCommonLib.Enums; using SharpHoundCommonLib.OutputTypes; -using SharpHoundRPC; using SharpHoundRPC.Shared; using SharpHoundRPC.Wrappers; @@ -16,9 +15,9 @@ public class LocalGroupProcessor { public delegate Task ComputerStatusDelegate(CSVComputerStatus status); private readonly ILogger _log; - private readonly ILDAPUtils _utils; + private readonly ILdapUtils _utils; - public LocalGroupProcessor(ILDAPUtils utils, ILogger log = null) + public LocalGroupProcessor(ILdapUtils utils, ILogger log = null) { _utils = utils; _log = log ?? Logging.LogProvider.CreateLogger("LocalGroupProcessor"); @@ -26,15 +25,15 @@ public LocalGroupProcessor(ILDAPUtils utils, ILogger log = null) public event ComputerStatusDelegate ComputerStatusEvent; - public virtual Result OpenSamServer(string computerName) + public virtual SharpHoundRPC.Result OpenSamServer(string computerName) { var result = SAMServer.OpenServer(computerName); if (result.IsFailed) { - return Result.Fail(result.SError); + return SharpHoundRPC.Result.Fail(result.SError); } - return Result.Ok(result.Value); + return SharpHoundRPC.Result.Ok(result.Value); } public IAsyncEnumerable GetLocalGroups(ResolvedSearchResult result) @@ -153,7 +152,7 @@ await SendComputerStatus(new CSVComputerStatus { _log.LogTrace("Opening alias {Alias} with RID {Rid} in domain {Domain} on computer {ComputerName}", alias.Name, alias.Rid, domainResult.Name, computerName); //Try and resolve the group name using several different criteria - var resolvedName = ResolveGroupName(alias.Name, computerName, computerObjectId, computerDomain, alias.Rid, + var resolvedName = await ResolveGroupName(alias.Name, computerName, computerObjectId, computerDomain, alias.Rid, isDomainController, domainResult.Name.Equals("builtin", StringComparison.OrdinalIgnoreCase)); @@ -219,17 +218,16 @@ await SendComputerStatus(new CSVComputerStatus if (isDomainController) { - var result = ResolveDomainControllerPrincipal(sidValue, computerDomain); + var result = await ResolveDomainControllerPrincipal(sidValue, computerDomain); if (result != null) results.Add(result); continue; } //If we get a local well known principal, we need to convert it using the computer's objectid - if (_utils.ConvertLocalWellKnownPrincipal(securityIdentifier, computerObjectId, computerDomain, out var principal)) + if (await _utils.ConvertLocalWellKnownPrincipal(securityIdentifier, computerObjectId, computerDomain) is (true, var principal)) { //If the principal is null, it means we hit a weird edge case, but this is a local well known principal - if (principal != null) - results.Add(principal); + results.Add(principal); continue; } @@ -295,8 +293,8 @@ await SendComputerStatus(new CSVComputerStatus } //If we get here, we most likely have a domain principal in a local group - var resolvedPrincipal = _utils.ResolveIDAndType(sidValue, computerDomain); - if (resolvedPrincipal != null) results.Add(resolvedPrincipal); + var resolvedPrincipal = await _utils.ResolveIDAndType(sidValue, computerDomain); + if (resolvedPrincipal.Success) results.Add(resolvedPrincipal.Principal); } ret.Collected = true; @@ -307,15 +305,15 @@ await SendComputerStatus(new CSVComputerStatus } } - private TypedPrincipal ResolveDomainControllerPrincipal(string sid, string computerDomain) + private async Task ResolveDomainControllerPrincipal(string sid, string computerDomain) { //If the server is a domain controller and we have a well known group, use the domain value - if (_utils.GetWellKnownPrincipal(sid, computerDomain, out var wellKnown)) + if (await _utils.GetWellKnownPrincipal(sid, computerDomain) is (true, var wellKnown)) return wellKnown; - return _utils.ResolveIDAndType(sid, computerDomain); + return (await _utils.ResolveIDAndType(sid, computerDomain)).Principal; } - private NamedPrincipal ResolveGroupName(string baseName, string computerName, string computerDomainSid, + private async Task ResolveGroupName(string baseName, string computerName, string computerDomainSid, string domainName, int groupRid, bool isDc, bool isBuiltIn) { if (isDc) @@ -323,12 +321,12 @@ private NamedPrincipal ResolveGroupName(string baseName, string computerName, st if (isBuiltIn) { //If this is the builtin group on the DC, the groups correspond to the domain well known groups - _utils.GetWellKnownPrincipal($"S-1-5-32-{groupRid}".ToUpper(), domainName, out var principal); - return new NamedPrincipal - { - ObjectId = principal.ObjectIdentifier, - PrincipalName = "IGNOREME" - }; + if (await _utils.GetWellKnownPrincipal($"S-1-5-32-{groupRid}".ToUpper(), domainName) is (true, var principal)) + return new NamedPrincipal + { + ObjectId = principal.ObjectIdentifier, + PrincipalName = "IGNOREME" + }; } if (computerDomainSid == null) diff --git a/src/CommonLib/Processors/RegistryResult.cs b/src/CommonLib/Processors/RegistryResult.cs index 205e2346..18ec9d9d 100644 --- a/src/CommonLib/Processors/RegistryResult.cs +++ b/src/CommonLib/Processors/RegistryResult.cs @@ -1,9 +1,7 @@ using SharpHoundCommonLib.OutputTypes; -namespace SharpHoundCommonLib.Processors -{ - public class RegistryResult : APIResult - { +namespace SharpHoundCommonLib.Processors { + public class RegistryResult : APIResult { public object Value { get; set; } } } \ No newline at end of file diff --git a/src/CommonLib/Processors/SPNProcessors.cs b/src/CommonLib/Processors/SPNProcessors.cs index 0796232f..b05221c2 100644 --- a/src/CommonLib/Processors/SPNProcessors.cs +++ b/src/CommonLib/Processors/SPNProcessors.cs @@ -1,55 +1,48 @@ using System.Collections.Generic; +using System.Linq; using Microsoft.Extensions.Logging; +using SharpHoundCommonLib.Enums; using SharpHoundCommonLib.OutputTypes; -namespace SharpHoundCommonLib.Processors -{ - public class SPNProcessors - { +namespace SharpHoundCommonLib.Processors { + public class SPNProcessors { private const string MSSQLSPNString = "mssqlsvc"; private readonly ILogger _log; - private readonly ILDAPUtils _utils; + private readonly ILdapUtils _utils; - public SPNProcessors(ILDAPUtils utils, ILogger log = null) - { + public SPNProcessors(ILdapUtils utils, ILogger log = null) { _utils = utils; _log = log ?? Logging.LogProvider.CreateLogger("SPNProc"); } public IAsyncEnumerable ReadSPNTargets(ResolvedSearchResult result, - ISearchResultEntry entry) - { - var members = entry.GetArrayProperty(LDAPProperties.ServicePrincipalNames); - var name = result.DisplayName; - var dn = entry.DistinguishedName; + IDirectoryObject entry) { + if (entry.TryGetArrayProperty(LDAPProperties.ServicePrincipalNames, out var members)) { + return ReadSPNTargets(members, result.Domain, result.DisplayName); + } - return ReadSPNTargets(members, dn, name); + return AsyncEnumerable.Empty(); } public async IAsyncEnumerable ReadSPNTargets(string[] servicePrincipalNames, - string distinguishedName, string objectName = "") - { - if (servicePrincipalNames.Length == 0) - { + string domainName, string objectName = "") { + if (servicePrincipalNames.Length == 0) { _log.LogTrace("SPN Array is empty for {Name}", objectName); yield break; } + + _log.LogDebug("Processing SPN targets for {ObjectName}", objectName); - var domain = Helpers.DistinguishedNameToDomain(distinguishedName); - - foreach (var spn in servicePrincipalNames) - { + foreach (var spn in servicePrincipalNames) { //This SPN format isn't useful for us right now (username@domain) - if (spn.Contains("@")) - { + if (spn.Contains("@")) { _log.LogTrace("Skipping spn without @ {SPN} for {Name}", spn, objectName); continue; } _log.LogTrace("Processing SPN {SPN} for {Name}", spn, objectName); - if (spn.ToLower().Contains(MSSQLSPNString)) - { + if (spn.ToLower().Contains(MSSQLSPNString)) { _log.LogTrace("Matched SQL SPN {SPN} for {Name}", spn, objectName); var port = 1433; @@ -57,15 +50,14 @@ public async IAsyncEnumerable ReadSPNTargets(string[] servicePrinc if (!int.TryParse(spn.Split(':')[1], out port)) port = 1433; - var host = await _utils.ResolveHostToSid(spn, domain); - _log.LogTrace("Resolved {SPN} to {Hostname}", spn, host); - if (host != null && host.StartsWith("S-1-")) - yield return new SPNPrivilege - { + if (await _utils.ResolveHostToSid(spn, domainName) is (true, var host) && host.StartsWith("S-1")) { + _log.LogTrace("Resolved {SPN} to {Hostname}", spn, host); + yield return new SPNPrivilege { ComputerSID = host, Port = port, Service = EdgeNames.SQLAdmin }; + } } } } diff --git a/src/CommonLib/Processors/UserRightsAssignmentProcessor.cs b/src/CommonLib/Processors/UserRightsAssignmentProcessor.cs index cedb73d0..404427ee 100644 --- a/src/CommonLib/Processors/UserRightsAssignmentProcessor.cs +++ b/src/CommonLib/Processors/UserRightsAssignmentProcessor.cs @@ -4,7 +4,6 @@ using Microsoft.Extensions.Logging; using SharpHoundCommonLib.Enums; using SharpHoundCommonLib.OutputTypes; -using SharpHoundRPC; using SharpHoundRPC.Shared; using SharpHoundRPC.Wrappers; @@ -15,9 +14,9 @@ public class UserRightsAssignmentProcessor public delegate Task ComputerStatusDelegate(CSVComputerStatus status); private readonly ILogger _log; - private readonly ILDAPUtils _utils; + private readonly ILdapUtils _utils; - public UserRightsAssignmentProcessor(ILDAPUtils utils, ILogger log = null) + public UserRightsAssignmentProcessor(ILdapUtils utils, ILogger log = null) { _utils = utils; _log = log ?? Logging.LogProvider.CreateLogger("UserRightsAssignmentProcessor"); @@ -25,12 +24,12 @@ public UserRightsAssignmentProcessor(ILDAPUtils utils, ILogger log = null) public event ComputerStatusDelegate ComputerStatusEvent; - public virtual Result OpenLSAPolicy(string computerName) + public virtual SharpHoundRPC.Result OpenLSAPolicy(string computerName) { var result = LSAPolicy.OpenPolicy(computerName); - if (result.IsFailed) return Result.Fail(result.SError); + if (result.IsFailed) return SharpHoundRPC.Result.Fail(result.SError); - return Result.Ok(result.Value); + return SharpHoundRPC.Result.Ok(result.Value); } public IAsyncEnumerable GetUserRightsAssignments(ResolvedSearchResult result, @@ -53,15 +52,15 @@ public async IAsyncEnumerable GetUserRightsAssign string computerObjectId, string computerDomain, bool isDomainController, string[] desiredPrivileges = null) { var policyOpenResult = OpenLSAPolicy(computerName); - if (policyOpenResult.IsFailed) + if (!policyOpenResult.IsSuccess) { _log.LogDebug("LSAOpenPolicy failed on {ComputerName} with status {Status}", computerName, - policyOpenResult.SError); + policyOpenResult.Error); await SendComputerStatus(new CSVComputerStatus { Task = "LSAOpenPolicy", ComputerName = computerName, - Status = policyOpenResult.SError + Status = policyOpenResult.Error }); yield break; } @@ -109,7 +108,7 @@ await SendComputerStatus(new CSVComputerStatus { _log.LogDebug( "LSAEnumerateAccountsWithUserRight failed on {ComputerName} with status {Status} for privilege {Privilege}", - computerName, policyOpenResult.SError, privilege); + computerName, policyOpenResult.Error, privilege); await SendComputerStatus(new CSVComputerStatus { ComputerName = computerName, @@ -142,14 +141,14 @@ await SendComputerStatus(new CSVComputerStatus if (isDomainController) { - var result = ResolveDomainControllerPrincipal(sid.Value, computerDomain); + var result = await ResolveDomainControllerPrincipal(sid.Value, computerDomain); if (result != null) resolved.Add(result); continue; } //If we get a local well known principal, we need to convert it using the computer's domain sid - if (_utils.ConvertLocalWellKnownPrincipal(sid, computerObjectId, computerDomain, out var principal)) + if (await _utils.ConvertLocalWellKnownPrincipal(sid, computerObjectId, computerDomain) is (true, var principal)) { _log.LogTrace("Got Well Known Principal {SID} on computer {Computer} for privilege {Privilege} and type {Type}", principal.ObjectIdentifier, computerName, privilege, principal.ObjectType); resolved.Add(principal); @@ -191,8 +190,8 @@ await SendComputerStatus(new CSVComputerStatus } //If we get here, we most likely have a domain principal in a local group. Do a lookup - var resolvedPrincipal = _utils.ResolveIDAndType(sid.Value, computerDomain); - if (resolvedPrincipal != null) resolved.Add(resolvedPrincipal); + var resolvedPrincipal = await _utils.ResolveIDAndType(sid.Value, computerDomain); + if (resolvedPrincipal.Success) resolved.Add(resolvedPrincipal.Principal); } ret.Collected = true; @@ -202,13 +201,14 @@ await SendComputerStatus(new CSVComputerStatus } } - private TypedPrincipal ResolveDomainControllerPrincipal(string sid, string computerDomain) + private async Task ResolveDomainControllerPrincipal(string sid, string computerDomain) { //If the server is a domain controller and we have a well known group, use the domain value - if (_utils.GetWellKnownPrincipal(sid, computerDomain, out var wellKnown)) + if (await _utils.GetWellKnownPrincipal(sid, computerDomain) is (true, var wellKnown)) return wellKnown; //Otherwise, do a domain lookup - return _utils.ResolveIDAndType(sid, computerDomain); + var domainPrinciple = await _utils.ResolveIDAndType(sid, computerDomain); + return domainPrinciple.Principal; } diff --git a/src/CommonLib/Result.cs b/src/CommonLib/Result.cs new file mode 100644 index 00000000..c73cfec3 --- /dev/null +++ b/src/CommonLib/Result.cs @@ -0,0 +1,41 @@ +namespace SharpHoundCommonLib { + public class Result : Result { + public T Value { get; set; } + + protected Result(T value, bool success, string error) : base(success, error) { + Value = value; + } + + public new 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 => string.IsNullOrWhiteSpace(Error) && Success; + private 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/SearchResultEntryWrapper.cs b/src/CommonLib/SearchResultEntryWrapper.cs deleted file mode 100644 index ef681ba8..00000000 --- a/src/CommonLib/SearchResultEntryWrapper.cs +++ /dev/null @@ -1,281 +0,0 @@ -using System; -using System.Collections.Generic; -using System.DirectoryServices.Protocols; -using System.Linq; -using System.Security.Cryptography.X509Certificates; -using System.Security.Principal; -using Microsoft.Extensions.Logging; -using SharpHoundCommonLib.Enums; - -namespace SharpHoundCommonLib -{ - public interface ISearchResultEntry - { - string DistinguishedName { get; } - ResolvedSearchResult ResolveBloodHoundInfo(); - string GetProperty(string propertyName); - byte[] GetByteProperty(string propertyName); - string[] GetArrayProperty(string propertyName); - byte[][] GetByteArrayProperty(string propertyName); - bool GetIntProperty(string propertyName, out int value); - X509Certificate2[] GetCertificateArrayProperty(string propertyName); - string GetObjectIdentifier(); - bool IsDeleted(); - Label GetLabel(); - string GetSid(); - string GetGuid(); - int PropCount(string prop); - IEnumerable PropertyNames(); - bool IsMSA(); - bool IsGMSA(); - bool HasLAPS(); - } - - public class SearchResultEntryWrapper : ISearchResultEntry - { - private const string GMSAClass = "msds-groupmanagedserviceaccount"; - private const string MSAClass = "msds-managedserviceaccount"; - private readonly SearchResultEntry _entry; - private readonly ILogger _log; - private readonly ILDAPUtils _utils; - - public SearchResultEntryWrapper(SearchResultEntry entry, ILDAPUtils utils = null, ILogger log = null) - { - _entry = entry; - _utils = utils ?? new LDAPUtils(); - _log = log ?? Logging.LogProvider.CreateLogger("SearchResultWrapper"); - } - - public string DistinguishedName => _entry.DistinguishedName; - - public ResolvedSearchResult ResolveBloodHoundInfo() - { - var res = new ResolvedSearchResult(); - - var objectId = GetObjectIdentifier(); - if (objectId == null) - { - _log.LogWarning("ObjectIdentifier is null for {DN}", DistinguishedName); - return null; - } - - var uac = _entry.GetProperty(LDAPProperties.UserAccountControl); - if (int.TryParse(uac, out var flag)) - { - var flags = (UacFlags) flag; - if (flags.HasFlag(UacFlags.ServerTrustAccount)) - { - _log.LogTrace("Marked {SID} as a domain controller", objectId); - res.IsDomainController = true; - _utils.AddDomainController(objectId); - } - } - - //Try to resolve the domain - var distinguishedName = DistinguishedName; - string itemDomain; - if (distinguishedName == null) - { - if (objectId.StartsWith("S-1-")) - { - itemDomain = _utils.GetDomainNameFromSid(objectId); - } - else - { - _log.LogWarning("Failed to resolve domain for {ObjectID}", objectId); - return null; - } - } - else - { - itemDomain = Helpers.DistinguishedNameToDomain(distinguishedName); - } - - _log.LogTrace("Resolved domain for {SID} to {Domain}", objectId, itemDomain); - - res.ObjectId = objectId; - res.Domain = itemDomain; - if (IsDeleted()) - { - res.Deleted = IsDeleted(); - _log.LogTrace("{SID} is tombstoned, skipping rest of resolution", objectId); - return res; - } - - if (WellKnownPrincipal.GetWellKnownPrincipal(objectId, out var wkPrincipal)) - { - res.DomainSid = _utils.GetSidFromDomainName(itemDomain); - res.DisplayName = $"{wkPrincipal.ObjectIdentifier}@{itemDomain}"; - res.ObjectType = wkPrincipal.ObjectType; - res.ObjectId = _utils.ConvertWellKnownPrincipal(objectId, itemDomain); - - _log.LogTrace("Resolved {DN} to wkp {ObjectID}", DistinguishedName, res.ObjectId); - return res; - } - - if (objectId.StartsWith("S-1-")) - try - { - res.DomainSid = new SecurityIdentifier(objectId).AccountDomainSid.Value; - } - catch - { - res.DomainSid = _utils.GetSidFromDomainName(itemDomain); - } - else - res.DomainSid = _utils.GetSidFromDomainName(itemDomain); - - var samAccountName = GetProperty(LDAPProperties.SAMAccountName); - - var itemType = GetLabel(); - res.ObjectType = itemType; - - if (IsGMSA() || IsMSA()) - { - res.ObjectType = Label.User; - itemType = Label.User; - } - - _log.LogTrace("Resolved type for {SID} to {Label}", objectId, itemType); - - switch (itemType) - { - case Label.User: - case Label.Group: - case Label.Base: - res.DisplayName = $"{samAccountName}@{itemDomain}"; - break; - case Label.Computer: - { - var shortName = samAccountName?.TrimEnd('$'); - var dns = GetProperty(LDAPProperties.DNSHostName); - var cn = GetProperty(LDAPProperties.CanonicalName); - - if (dns != null) - res.DisplayName = dns; - else if (shortName == null && cn == null) - res.DisplayName = $"UNKNOWN.{itemDomain}"; - else if (shortName != null) - res.DisplayName = $"{shortName}.{itemDomain}"; - else - res.DisplayName = $"{cn}.{itemDomain}"; - - break; - } - case Label.GPO: - case Label.IssuancePolicy: - { - var cn = GetProperty(LDAPProperties.CanonicalName); - var displayName = GetProperty(LDAPProperties.DisplayName); - res.DisplayName = string.IsNullOrEmpty(displayName) ? $"{cn}@{itemDomain}" : $"{GetProperty(LDAPProperties.DisplayName)}@{itemDomain}"; - break; - } - case Label.Domain: - res.DisplayName = itemDomain; - break; - case Label.OU: - case Label.Container: - case Label.Configuration: - case Label.RootCA: - case Label.AIACA: - case Label.NTAuthStore: - case Label.EnterpriseCA: - case Label.CertTemplate: - res.DisplayName = $"{GetProperty(LDAPProperties.Name)}@{itemDomain}"; - break; - default: - throw new ArgumentOutOfRangeException(); - } - - return res; - } - - public string GetProperty(string propertyName) - { - return _entry.GetProperty(propertyName); - } - - public byte[] GetByteProperty(string propertyName) - { - return _entry.GetPropertyAsBytes(propertyName); - } - - public string[] GetArrayProperty(string propertyName) - { - return _entry.GetPropertyAsArray(propertyName); - } - - public byte[][] GetByteArrayProperty(string propertyName) - { - return _entry.GetPropertyAsArrayOfBytes(propertyName); - } - - public bool GetIntProperty(string propertyName, out int value) - { - return _entry.GetPropertyAsInt(propertyName, out value); - } - - public X509Certificate2[] GetCertificateArrayProperty(string propertyName) - { - return _entry.GetPropertyAsArrayOfCertificates(propertyName); - } - - public string GetObjectIdentifier() - { - return _entry.GetObjectIdentifier(); - } - - public bool IsDeleted() - { - return _entry.IsDeleted(); - } - - public Label GetLabel() - { - return _entry.GetLabel(); - } - - public string GetSid() - { - return _entry.GetSid(); - } - - public string GetGuid() - { - return _entry.GetGuid(); - } - - public int PropCount(string prop) - { - var coll = _entry.Attributes[prop]; - return coll.Count; - } - - public IEnumerable PropertyNames() - { - foreach (var property in _entry.Attributes.AttributeNames) yield return property.ToString().ToLower(); - } - - public bool IsMSA() - { - var classes = GetArrayProperty(LDAPProperties.ObjectClass); - return classes.Contains(MSAClass, StringComparer.InvariantCultureIgnoreCase); - } - - public bool IsGMSA() - { - var classes = GetArrayProperty(LDAPProperties.ObjectClass); - return classes.Contains(GMSAClass, StringComparer.InvariantCultureIgnoreCase); - } - - public bool HasLAPS() - { - return GetProperty(LDAPProperties.LAPSExpirationTime) != null || GetProperty(LDAPProperties.LegacyLAPSExpirationTime) != null; - } - - public SearchResultEntry GetEntry() - { - return _entry; - } - } -} diff --git a/src/CommonLib/SecurityDescriptor.cs b/src/CommonLib/SecurityDescriptor.cs index 73ef33f9..3ef96569 100644 --- a/src/CommonLib/SecurityDescriptor.cs +++ b/src/CommonLib/SecurityDescriptor.cs @@ -15,6 +15,8 @@ public ActiveDirectoryRuleDescriptor(ActiveDirectoryAccessRule inner) _inner = inner; } + public virtual InheritanceFlags InheritanceFlags => _inner.InheritanceFlags; + public virtual AccessControlType AccessControlType() { return _inner.AccessControlType; @@ -30,6 +32,10 @@ public virtual bool IsInherited() return _inner.IsInherited; } + public virtual string InheritedObjectType() { + return _inner.InheritedObjectType.ToString(); + } + public virtual bool IsAceInheritedFrom(string guid) { //Check if the ace is inherited diff --git a/src/CommonLib/SharpHoundCommonLib.csproj b/src/CommonLib/SharpHoundCommonLib.csproj index 25ffa95a..3becbab7 100644 --- a/src/CommonLib/SharpHoundCommonLib.csproj +++ b/src/CommonLib/SharpHoundCommonLib.csproj @@ -3,13 +3,13 @@ net462 library SharpHoundCommon - 11 + default Rohan Vazarkar SpecterOps Common library for C# BloodHound enumeration tasks GPL-3.0-only https://github.com/BloodHoundAD/SharpHoundCommon - 3.2.0-rc1 + 4.0.0 SharpHoundCommonLib SharpHoundCommonLib @@ -19,11 +19,12 @@ - - + + + diff --git a/src/CommonLib/WellKnownPrincipal.cs b/src/CommonLib/WellKnownPrincipal.cs index 1d1ae363..46574c0f 100644 --- a/src/CommonLib/WellKnownPrincipal.cs +++ b/src/CommonLib/WellKnownPrincipal.cs @@ -44,7 +44,7 @@ public static bool GetWellKnownPrincipal(string sid, out TypedPrincipal commonPr "S-1-5-13" => new TypedPrincipal("Terminal Server Users", Label.Group), "S-1-5-14" => new TypedPrincipal("Remote Interactive Logon", Label.Group), "S-1-5-15" => new TypedPrincipal("This Organization", Label.Group), - "S-1-5-17" => new TypedPrincipal("IUSR", Label.Group), + "S-1-5-17" => new TypedPrincipal("IUSR", Label.User), "S-1-5-18" => new TypedPrincipal("Local System", Label.User), "S-1-5-19" => new TypedPrincipal("Local Service", Label.User), "S-1-5-20" => new TypedPrincipal("Network Service", Label.User), @@ -88,4 +88,4 @@ public static bool GetWellKnownPrincipal(string sid, out TypedPrincipal commonPr return commonPrincipal != null; } } -} \ No newline at end of file +} diff --git a/src/SharpHoundRPC/NetAPINative/NetAPIMethods.cs b/src/SharpHoundRPC/NetAPINative/NetAPIMethods.cs index a2dafc8f..0a6c5093 100644 --- a/src/SharpHoundRPC/NetAPINative/NetAPIMethods.cs +++ b/src/SharpHoundRPC/NetAPINative/NetAPIMethods.cs @@ -82,11 +82,9 @@ private static extern NetAPIEnums.NetAPIStatus NetWkstaGetInfo( out NetAPIPointer bufPtr); public static NetAPIResult DsGetDcName(string computerName, - string domainName) + string domainName, uint flags) { - var result = DsGetDcName(computerName, domainName, null, null, - (uint) (NetAPIEnums.DSGETDCNAME_FLAGS.DS_IS_FLAT_NAME | - NetAPIEnums.DSGETDCNAME_FLAGS.DS_RETURN_DNS_NAME), out var buffer); + var result = DsGetDcName(computerName, domainName, null, null, flags, out var buffer); if (result != NetAPIEnums.NetAPIStatus.Success) return result; return buffer.GetData(); diff --git a/test/unit/ACLProcessorTest.cs b/test/unit/ACLProcessorTest.cs index 0aa6c1df..f002a389 100644 --- a/test/unit/ACLProcessorTest.cs +++ b/test/unit/ACLProcessorTest.cs @@ -1,8 +1,11 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.DirectoryServices; using System.Linq; using System.Security.AccessControl; +using System.Threading; +using System.Threading.Tasks; using CommonLibTest.Facades; using Moq; using Newtonsoft.Json; @@ -13,10 +16,9 @@ using Xunit; using Xunit.Abstractions; -namespace CommonLibTest -{ - public class ACLProcessorTest : IDisposable - { +namespace CommonLibTest { + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] + public class ACLProcessorTest : IDisposable { private const string ProtectedUserNTSecurityDescriptor = "AQAEnIgEAAAAAAAAAAAAABQAAAAEAHQEGAAAAAUAPAAQAAAAAwAAAABCFkzAINARp2gAqgBuBSkUzChINxS8RZsHrW8BXl8oAQIAAAAAAAUgAAAAKgIAAAUAPAAQAAAAAwAAAABCFkzAINARp2gAqgBuBSm6epa/5g3QEaKFAKoAMEniAQIAAAAAAAUgAAAAKgIAAAUAPAAQAAAAAwAAABAgIF+ledARkCAAwE/C1M8UzChINxS8RZsHrW8BXl8oAQIAAAAAAAUgAAAAKgIAAAUAPAAQAAAAAwAAABAgIF+ledARkCAAwE/C1M+6epa/5g3QEaKFAKoAMEniAQIAAAAAAAUgAAAAKgIAAAUAPAAQAAAAAwAAAEDCCrypedARkCAAwE/C1M8UzChINxS8RZsHrW8BXl8oAQIAAAAAAAUgAAAAKgIAAAUAPAAQAAAAAwAAAEDCCrypedARkCAAwE/C1M+6epa/5g3QEaKFAKoAMEniAQIAAAAAAAUgAAAAKgIAAAUAPAAQAAAAAwAAAEIvulmiedARkCAAwE/C088UzChINxS8RZsHrW8BXl8oAQIAAAAAAAUgAAAAKgIAAAUAPAAQAAAAAwAAAEIvulmiedARkCAAwE/C08+6epa/5g3QEaKFAKoAMEniAQIAAAAAAAUgAAAAKgIAAAUAPAAQAAAAAwAAAPiIcAPhCtIRtCIAoMlo+TkUzChINxS8RZsHrW8BXl8oAQIAAAAAAAUgAAAAKgIAAAUAPAAQAAAAAwAAAPiIcAPhCtIRtCIAoMlo+Tm6epa/5g3QEaKFAKoAMEniAQIAAAAAAAUgAAAAKgIAAAUAOAAwAAAAAQAAAH96lr/mDdARooUAqgAwSeIBBQAAAAAABRUAAAAgT5C6f0aEpXZIFpAFAgAABQAsABAAAAABAAAAHbGpRq5gWkC36P+KWNRW0gECAAAAAAAFIAAAADACAAAFACwAMAAAAAEAAAAcmrZtIpTREa69AAD4A2fBAQIAAAAAAAUgAAAAMQIAAAUALAAwAAAAAQAAAGK8BVjJvShEpeKFag9MGF4BAgAAAAAABSAAAAAxAgAABQAsAJQAAgACAAAAFMwoSDcUvEWbB61vAV5fKAECAAAAAAAFIAAAACoCAAAFACwAlAACAAIAAAC6epa/5g3QEaKFAKoAMEniAQIAAAAAAAUgAAAAKgIAAAUAKAAAAQAAAQAAAFMacqsvHtARmBkAqgBAUpsBAQAAAAAAAQAAAAAFACgAAAEAAAEAAABTGnKrLx7QEZgZAKoAQFKbAQEAAAAAAAUKAAAABQIoADABAAABAAAA3kfmkW/ZcEuVV9Y/9PPM2AEBAAAAAAAFCgAAAAAAJAC/AQ4AAQUAAAAAAAUVAAAAIE+Qun9GhKV2SBaQAAIAAAAAJAC/AQ4AAQUAAAAAAAUVAAAAIE+Qun9GhKV2SBaQBwIAAAAAGAC/AQ8AAQIAAAAAAAUgAAAAIAIAAAAAFACUAAIAAQEAAAAAAAULAAAAAAAUAP8BDwABAQAAAAAABRIAAAABBQAAAAAABRUAAAAgT5C6f0aEpXZIFpAAAgAA"; @@ -34,44 +36,42 @@ public class ACLProcessorTest : IDisposable private readonly string _testDomainName; private readonly ITestOutputHelper _testOutputHelper; - public ACLProcessorTest(ITestOutputHelper testOutputHelper) - { + public ACLProcessorTest(ITestOutputHelper testOutputHelper) { _testOutputHelper = testOutputHelper; _testDomainName = "TESTLAB.LOCAL"; - _baseProcessor = new ACLProcessor(new LDAPUtils()); + _baseProcessor = new ACLProcessor(new LdapUtils()); } - public void Dispose() - { + public void Dispose() { } [Fact] - public void SanityCheck() - { + public void SanityCheck() { Assert.True(true); } [Fact] - public void ACLProcessor_IsACLProtected_NullNTSD_ReturnsFalse() - { - var processor = new ACLProcessor(new MockLDAPUtils(), true); + public void ACLProcessor_IsACLProtected_NullNTSD_ReturnsFalse() { + var processor = new ACLProcessor(new MockLdapUtils()); var result = processor.IsACLProtected((byte[])null); Assert.False(result); } [WindowsOnlyFact] - public void ACLProcessor_TestKnownDataAddMember() - { - var mockLdapUtils = new MockLDAPUtils(); - var mockUtils = new Mock(); + public async Task ACLProcessor_TestKnownDataAddMember() { + var mockLdapUtils = new MockLdapUtils(); + var mockUtils = new Mock(); + var mockData = new[] { LdapResult.Fail() }; + mockUtils.Setup(x => x.Query(It.IsAny(), It.IsAny())) + .Returns(mockData.ToAsyncEnumerable()); mockUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) .Returns((string a, string b) => mockLdapUtils.ResolveIDAndType(a, b)); var sd = new ActiveDirectorySecurityDescriptor(new ActiveDirectorySecurity()); mockUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(sd); - var processor = new ACLProcessor(mockUtils.Object, true); - var bytes = Helpers.B64ToBytes(AddMemberSecurityDescriptor); - var result = processor.ProcessACL(bytes, "TESTLAB.LOCAL", Label.Group, false); + var processor = new ACLProcessor(mockUtils.Object); + var bytes = Utils.B64ToBytes(AddMemberSecurityDescriptor); + var result = await processor.ProcessACL(bytes, "TESTLAB.LOCAL", Label.Group, false).ToArrayAsync(); _testOutputHelper.WriteLine(JsonConvert.SerializeObject(result)); @@ -84,49 +84,45 @@ public void ACLProcessor_TestKnownDataAddMember() } [Fact] - public void ACLProcessor_IsACLProtected_ReturnsTrue() - { - var mockLDAPUtils = new Mock(); + public void ACLProcessor_IsACLProtected_ReturnsTrue() { + var mockLDAPUtils = new Mock(); var mockSecurityDescriptor = new Mock(MockBehavior.Loose, null); mockSecurityDescriptor.Setup(x => x.AreAccessRulesProtected()).Returns(true); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); - var processor = new ACLProcessor(mockLDAPUtils.Object, true); - var bytes = Helpers.B64ToBytes(ProtectedUserNTSecurityDescriptor); + var processor = new ACLProcessor(mockLDAPUtils.Object); + var bytes = Utils.B64ToBytes(ProtectedUserNTSecurityDescriptor); var result = processor.IsACLProtected(bytes); Assert.True(result); } [Fact] - public void ACLProcessor_IsACLProtected_ReturnsFalse() - { - var mockLDAPUtils = new Mock(); + public void ACLProcessor_IsACLProtected_ReturnsFalse() { + var mockLDAPUtils = new Mock(); var mockSecurityDescriptor = new Mock(MockBehavior.Loose, null); mockSecurityDescriptor.Setup(m => m.AreAccessRulesProtected()).Returns(false); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); - var processor = new ACLProcessor(mockLDAPUtils.Object, true); - var bytes = Helpers.B64ToBytes(UnProtectedUserNtSecurityDescriptor); + var processor = new ACLProcessor(mockLDAPUtils.Object); + var bytes = Utils.B64ToBytes(UnProtectedUserNtSecurityDescriptor); var result = processor.IsACLProtected(bytes); Assert.False(result); } [Fact] - public void ACLProcessor_ProcessGMSAReaders_NullNTSD_ReturnsNothing() - { - var test = _baseProcessor.ProcessGMSAReaders(null, ""); + public async Task ACLProcessor_ProcessGMSAReaders_NullNTSD_ReturnsNothing() { + var test = await _baseProcessor.ProcessGMSAReaders(null, "").ToArrayAsync(); Assert.Empty(test); } [Fact] - public void ACLProcess_ProcessGMSAReaders_YieldsCorrectAce() - { - var expectedRightName = "ReadGMSAPassword"; + public async Task ACLProcess_ProcessGMSAReaders_YieldsCorrectAce() { + var expectedRightName = EdgeNames.ReadGMSAPassword; var expectedSID = "S-1-5-21-3130019616-2776909439-2417379446-500"; var expectedPrincipalType = Label.User; var expectedInheritance = false; - var mockLDAPUtils = new Mock(); + var mockLDAPUtils = new Mock(); var mockSecurityDescriptor = new Mock(MockBehavior.Loose, null); var mockRule = new Mock(MockBehavior.Loose, null); @@ -134,18 +130,17 @@ public void ACLProcess_ProcessGMSAReaders_YieldsCorrectAce() mockRule.Setup(x => x.IsAceInheritedFrom(It.IsAny())).Returns(true); mockRule.Setup(x => x.IdentityReference()).Returns(expectedSID); - var collection = new List(); - collection.Add(mockRule.Object); + var collection = new List { mockRule.Object }; mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(collection); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) - .Returns(new TypedPrincipal(expectedSID, expectedPrincipalType)); + .ReturnsAsync((true, new TypedPrincipal(expectedSID, expectedPrincipalType))); - var processor = new ACLProcessor(mockLDAPUtils.Object, true); - var bytes = Helpers.B64ToBytes(GMSAProperty); - var result = processor.ProcessGMSAReaders(bytes, _testDomainName).ToArray(); + var processor = new ACLProcessor(mockLDAPUtils.Object); + var bytes = Utils.B64ToBytes(GMSAProperty); + var result = await processor.ProcessGMSAReaders(bytes, _testDomainName).ToArrayAsync(); Assert.Single(result); var actual = result.First(); @@ -157,28 +152,25 @@ public void ACLProcess_ProcessGMSAReaders_YieldsCorrectAce() } [Fact] - public void ACLProcessor_ProcessGMSAReaders_Null_ACE() - { - var mockLDAPUtils = new Mock(); + public async Task ACLProcessor_ProcessGMSAReaders_Null_ACE() { + var mockLDAPUtils = new Mock(); var mockSecurityDescriptor = new Mock(MockBehavior.Loose, null); - var collection = new List(); - collection.Add(null); + var collection = new List { null }; mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(collection); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); - var processor = new ACLProcessor(mockLDAPUtils.Object, true); - var bytes = Helpers.B64ToBytes(GMSAProperty); - var result = processor.ProcessGMSAReaders(bytes, _testDomainName).ToArray(); + var processor = new ACLProcessor(mockLDAPUtils.Object); + var bytes = Utils.B64ToBytes(GMSAProperty); + var result = await processor.ProcessGMSAReaders(bytes, _testDomainName).ToArrayAsync(); Assert.Empty(result); } [Fact] - public void ACLProcessor_ProcessGMSAReaders_Deny_ACE() - { - var mockLDAPUtils = new Mock(); + public async Task ACLProcessor_ProcessGMSAReaders_Deny_ACE() { + var mockLDAPUtils = new Mock(); var mockSecurityDescriptor = new Mock(MockBehavior.Loose, null); var mockRule = new Mock(MockBehavior.Loose, null); var collection = new List(); @@ -190,17 +182,16 @@ public void ACLProcessor_ProcessGMSAReaders_Deny_ACE() .Returns(collection); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); - var processor = new ACLProcessor(mockLDAPUtils.Object, true); - var bytes = Helpers.B64ToBytes(GMSAProperty); - var result = processor.ProcessGMSAReaders(bytes, _testDomainName).ToArray(); + var processor = new ACLProcessor(mockLDAPUtils.Object); + var bytes = Utils.B64ToBytes(GMSAProperty); + var result = await processor.ProcessGMSAReaders(bytes, _testDomainName).ToArrayAsync(); Assert.Empty(result); } [Fact] - public void ACLProcessor_ProcessGMSAReaders_Null_PrincipalID() - { - var mockLDAPUtils = new Mock(); + public async Task ACLProcessor_ProcessGMSAReaders_Null_PrincipalID() { + var mockLDAPUtils = new Mock(); var mockSecurityDescriptor = new Mock(MockBehavior.Loose, null); var mockRule = new Mock(MockBehavior.Loose, null); var collection = new List(); @@ -213,29 +204,27 @@ public void ACLProcessor_ProcessGMSAReaders_Null_PrincipalID() .Returns(collection); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); - var processor = new ACLProcessor(mockLDAPUtils.Object, true); - var bytes = Helpers.B64ToBytes(GMSAProperty); - var result = processor.ProcessGMSAReaders(bytes, _testDomainName).ToArray(); + var processor = new ACLProcessor(mockLDAPUtils.Object); + var bytes = Utils.B64ToBytes(GMSAProperty); + var result = await processor.ProcessGMSAReaders(bytes, _testDomainName).ToArrayAsync(); Assert.Empty(result); } [Fact] - public void ACLProcessor_ProcessACL_Null_NTSecurityDescriptor() - { - var processor = new ACLProcessor(new MockLDAPUtils(), true); - var result = processor.ProcessACL(null, _testDomainName, Label.User, false).ToArray(); + public async Task ACLProcessor_ProcessACL_Null_NTSecurityDescriptor() { + var processor = new ACLProcessor(new MockLdapUtils()); + var result = await processor.ProcessACL(null, _testDomainName, Label.User, false).ToArrayAsync(); Assert.Empty(result); } [Fact] - public void ACLProcessor_ProcessACL_Yields_Owns_ACE() - { + public async Task ACLProcessor_ProcessACL_Yields_Owns_ACE() { var expectedSID = "S-1-5-21-3130019616-2776909439-2417379446-512"; var expectedPrincipalType = Label.Group; - var mockLDAPUtils = new Mock(); + var mockLDAPUtils = new Mock(); var mockSecurityDescriptor = new Mock(MockBehavior.Loose, null); var collection = new List(); @@ -244,24 +233,27 @@ public void ACLProcessor_ProcessACL_Yields_Owns_ACE() mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns(expectedSID); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) - .Returns(new TypedPrincipal(expectedSID, expectedPrincipalType)); + .ReturnsAsync((true, new TypedPrincipal(expectedSID, expectedPrincipalType))); + + var mockData = new[] { LdapResult.Fail() }; + mockLDAPUtils.Setup(x => x.Query(It.IsAny(), It.IsAny())) + .Returns(mockData.ToAsyncEnumerable()); - var processor = new ACLProcessor(mockLDAPUtils.Object, true); - var bytes = Helpers.B64ToBytes(UnProtectedUserNtSecurityDescriptor); - var result = processor.ProcessACL(bytes, _testDomainName, Label.User, false).ToArray(); + var processor = new ACLProcessor(mockLDAPUtils.Object); + var bytes = Utils.B64ToBytes(UnProtectedUserNtSecurityDescriptor); + var result = await processor.ProcessACL(bytes, _testDomainName, Label.User, false).ToArrayAsync(); Assert.Single(result); var actual = result.First(); Assert.Equal(actual.PrincipalSID, expectedSID); Assert.Equal(actual.PrincipalType, expectedPrincipalType); - Assert.Equal(actual.IsInherited, false); + Assert.False(actual.IsInherited); Assert.Equal(actual.RightName, EdgeNames.Owns); } [Fact] - public void ACLProcessor_ProcessACL_Null_SID() - { - var mockLDAPUtils = new Mock(); + public async Task ACLProcessor_ProcessACL_Null_SID() { + var mockLDAPUtils = new Mock(); var mockSecurityDescriptor = new Mock(MockBehavior.Loose, null); var collection = new List(); @@ -270,37 +262,34 @@ public void ACLProcessor_ProcessACL_Null_SID() mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); - var processor = new ACLProcessor(mockLDAPUtils.Object, true); - var bytes = Helpers.B64ToBytes(UnProtectedUserNtSecurityDescriptor); - var result = processor.ProcessACL(bytes, _testDomainName, Label.User, false).ToArray(); + var processor = new ACLProcessor(mockLDAPUtils.Object); + var bytes = Utils.B64ToBytes(UnProtectedUserNtSecurityDescriptor); + var result = await processor.ProcessACL(bytes, _testDomainName, Label.User, false).ToArrayAsync(); Assert.Empty(result); } [Fact] - public void ACLProcessor_ProcessACL_Null_ACE() - { - var mockLDAPUtils = new Mock(); + public async Task ACLProcessor_ProcessACL_Null_ACE() { + var mockLDAPUtils = new Mock(); var mockSecurityDescriptor = new Mock(MockBehavior.Loose, null); - var collection = new List(); - collection.Add(null); + var collection = new List { null }; mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(collection); mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); - var processor = new ACLProcessor(mockLDAPUtils.Object, true); - var bytes = Helpers.B64ToBytes(UnProtectedUserNtSecurityDescriptor); - var result = processor.ProcessACL(bytes, _testDomainName, Label.User, false).ToArray(); + var processor = new ACLProcessor(mockLDAPUtils.Object); + var bytes = Utils.B64ToBytes(UnProtectedUserNtSecurityDescriptor); + var result = await processor.ProcessACL(bytes, _testDomainName, Label.User, false).ToArrayAsync(); Assert.Empty(result); } [Fact] - public void ACLProcessor_ProcessACL_Deny_ACE() - { - var mockLDAPUtils = new Mock(); + public async Task ACLProcessor_ProcessACL_Deny_ACE() { + var mockLDAPUtils = new Mock(); var mockSecurityDescriptor = new Mock(MockBehavior.Loose, null); var mockRule = new Mock(MockBehavior.Loose, null); var collection = new List(); @@ -312,17 +301,16 @@ public void ACLProcessor_ProcessACL_Deny_ACE() mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); - var processor = new ACLProcessor(mockLDAPUtils.Object, true); - var bytes = Helpers.B64ToBytes(UnProtectedUserNtSecurityDescriptor); - var result = processor.ProcessACL(bytes, _testDomainName, Label.User, false).ToArray(); + var processor = new ACLProcessor(mockLDAPUtils.Object); + var bytes = Utils.B64ToBytes(UnProtectedUserNtSecurityDescriptor); + var result = await processor.ProcessACL(bytes, _testDomainName, Label.User, false).ToArrayAsync(); Assert.Empty(result); } [Fact] - public void ACLProcessor_ProcessACL_Unmatched_Inheritance_ACE() - { - var mockLDAPUtils = new Mock(); + public async Task ACLProcessor_ProcessACL_Unmatched_Inheritance_ACE() { + var mockLDAPUtils = new Mock(); var mockSecurityDescriptor = new Mock(MockBehavior.Loose, null); var mockRule = new Mock(MockBehavior.Loose, null); var collection = new List(); @@ -335,17 +323,16 @@ public void ACLProcessor_ProcessACL_Unmatched_Inheritance_ACE() mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); - var processor = new ACLProcessor(mockLDAPUtils.Object, true); - var bytes = Helpers.B64ToBytes(UnProtectedUserNtSecurityDescriptor); - var result = processor.ProcessACL(bytes, _testDomainName, Label.User, false).ToArray(); + var processor = new ACLProcessor(mockLDAPUtils.Object); + var bytes = Utils.B64ToBytes(UnProtectedUserNtSecurityDescriptor); + var result = await processor.ProcessACL(bytes, _testDomainName, Label.User, false).ToArrayAsync(); Assert.Empty(result); } [Fact] - public void ACLProcessor_ProcessACL_Null_SID_ACE() - { - var mockLDAPUtils = new Mock(); + public async Task ACLProcessor_ProcessACL_Null_SID_ACE() { + var mockLDAPUtils = new Mock(); var mockSecurityDescriptor = new Mock(MockBehavior.Loose, null); var mockRule = new Mock(MockBehavior.Loose, null); var collection = new List(); @@ -359,21 +346,20 @@ public void ACLProcessor_ProcessACL_Null_SID_ACE() mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); - var processor = new ACLProcessor(mockLDAPUtils.Object, true); - var bytes = Helpers.B64ToBytes(UnProtectedUserNtSecurityDescriptor); - var result = processor.ProcessACL(bytes, _testDomainName, Label.User, false).ToArray(); + var processor = new ACLProcessor(mockLDAPUtils.Object); + var bytes = Utils.B64ToBytes(UnProtectedUserNtSecurityDescriptor); + var result = await processor.ProcessACL(bytes, _testDomainName, Label.User, false).ToArrayAsync(); Assert.Empty(result); } [Fact] - public void ACLProcessor_ProcessACL_GenericAll_Unmatched_Guid() - { + public async Task ACLProcessor_ProcessACL_GenericAll_Unmatched_Guid() { var expectedPrincipalType = Label.Group; var expectedPrincipalSID = "S-1-5-21-3130019616-2776909439-2417379446-512"; var unmatchedGuid = new Guid("583991c8-629d-4a07-8a70-74d19d22ac9c"); - var mockLDAPUtils = new Mock(); + var mockLDAPUtils = new Mock(); var mockSecurityDescriptor = new Mock(MockBehavior.Loose, null); var mockRule = new Mock(MockBehavior.Loose, null); var collection = new List(); @@ -389,22 +375,24 @@ public void ACLProcessor_ProcessACL_GenericAll_Unmatched_Guid() mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) - .Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType)); + .ReturnsAsync((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType))); + var mockData = new[] { LdapResult.Fail() }; + mockLDAPUtils.Setup(x => x.Query(It.IsAny(), It.IsAny())) + .Returns(mockData.ToAsyncEnumerable()); - var processor = new ACLProcessor(mockLDAPUtils.Object, true); - var bytes = Helpers.B64ToBytes(UnProtectedUserNtSecurityDescriptor); - var result = processor.ProcessACL(bytes, _testDomainName, Label.User, false).ToArray(); + var processor = new ACLProcessor(mockLDAPUtils.Object); + var bytes = Utils.B64ToBytes(UnProtectedUserNtSecurityDescriptor); + var result = await processor.ProcessACL(bytes, _testDomainName, Label.User, false).ToArrayAsync(); Assert.Empty(result); } [Fact] - public void ACLProcessor_ProcessACL_GenericAll() - { + public async Task ACLProcessor_ProcessACL_GenericAll() { var expectedPrincipalType = Label.Group; var expectedPrincipalSID = "S-1-5-21-3130019616-2776909439-2417379446-512"; - var mockLDAPUtils = new Mock(); + var mockLDAPUtils = new Mock(); var mockSecurityDescriptor = new Mock(MockBehavior.Loose, null); var mockRule = new Mock(MockBehavior.Loose, null); var collection = new List(); @@ -420,28 +408,30 @@ public void ACLProcessor_ProcessACL_GenericAll() mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) - .Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType)); + .ReturnsAsync((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType))); + var mockData = new[] { LdapResult.Fail() }; + mockLDAPUtils.Setup(x => x.Query(It.IsAny(), It.IsAny())) + .Returns(mockData.ToAsyncEnumerable()); - var processor = new ACLProcessor(mockLDAPUtils.Object, true); - var bytes = Helpers.B64ToBytes(UnProtectedUserNtSecurityDescriptor); - var result = processor.ProcessACL(bytes, _testDomainName, Label.User, false).ToArray(); + var processor = new ACLProcessor(mockLDAPUtils.Object); + var bytes = Utils.B64ToBytes(UnProtectedUserNtSecurityDescriptor); + var result = await processor.ProcessACL(bytes, _testDomainName, Label.User, false).ToArrayAsync(); Assert.Single(result); var actual = result.First(); Assert.Equal(actual.PrincipalType, expectedPrincipalType); Assert.Equal(actual.PrincipalSID, expectedPrincipalSID); - Assert.Equal(actual.IsInherited, false); + Assert.False(actual.IsInherited); Assert.Equal(actual.RightName, EdgeNames.GenericAll); } [Fact] - public void ACLProcessor_ProcessACL_WriteDacl() - { + public async Task ACLProcessor_ProcessACL_WriteDacl() { var expectedPrincipalType = Label.Group; var expectedPrincipalSID = "S-1-5-21-3130019616-2776909439-2417379446-512"; var expectedRightName = ActiveDirectoryRights.WriteDacl; - var mockLDAPUtils = new Mock(); + var mockLDAPUtils = new Mock(); var mockSecurityDescriptor = new Mock(MockBehavior.Loose, null); var mockRule = new Mock(MockBehavior.Loose, null); var collection = new List(); @@ -457,28 +447,30 @@ public void ACLProcessor_ProcessACL_WriteDacl() mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) - .Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType)); + .ReturnsAsync((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType))); + var mockData = new[] { LdapResult.Fail() }; + mockLDAPUtils.Setup(x => x.Query(It.IsAny(), It.IsAny())) + .Returns(mockData.ToAsyncEnumerable()); - var processor = new ACLProcessor(mockLDAPUtils.Object, true); - var bytes = Helpers.B64ToBytes(UnProtectedUserNtSecurityDescriptor); - var result = processor.ProcessACL(bytes, _testDomainName, Label.User, false).ToArray(); + var processor = new ACLProcessor(mockLDAPUtils.Object); + var bytes = Utils.B64ToBytes(UnProtectedUserNtSecurityDescriptor); + var result = await processor.ProcessACL(bytes, _testDomainName, Label.User, false).ToArrayAsync(); Assert.Single(result); var actual = result.First(); Assert.Equal(actual.PrincipalType, expectedPrincipalType); Assert.Equal(actual.PrincipalSID, expectedPrincipalSID); - Assert.Equal(actual.IsInherited, false); + Assert.False(actual.IsInherited); Assert.Equal(actual.RightName, expectedRightName.ToString()); } [Fact] - public void ACLProcessor_ProcessACL_WriteOwner() - { + public async Task ACLProcessor_ProcessACL_WriteOwner() { var expectedPrincipalType = Label.Group; var expectedPrincipalSID = "S-1-5-21-3130019616-2776909439-2417379446-512"; var expectedRightName = ActiveDirectoryRights.WriteOwner; - var mockLDAPUtils = new Mock(); + var mockLDAPUtils = new Mock(); var mockSecurityDescriptor = new Mock(MockBehavior.Loose, null); var mockRule = new Mock(MockBehavior.Loose, null); var collection = new List(); @@ -494,28 +486,30 @@ public void ACLProcessor_ProcessACL_WriteOwner() mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) - .Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType)); + .ReturnsAsync((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType))); + var mockData = new[] { LdapResult.Fail() }; + mockLDAPUtils.Setup(x => x.Query(It.IsAny(), It.IsAny())) + .Returns(mockData.ToAsyncEnumerable()); - var processor = new ACLProcessor(mockLDAPUtils.Object, true); - var bytes = Helpers.B64ToBytes(UnProtectedUserNtSecurityDescriptor); - var result = processor.ProcessACL(bytes, _testDomainName, Label.User, false).ToArray(); + var processor = new ACLProcessor(mockLDAPUtils.Object); + var bytes = Utils.B64ToBytes(UnProtectedUserNtSecurityDescriptor); + var result = await processor.ProcessACL(bytes, _testDomainName, Label.User, false).ToArrayAsync(); Assert.Single(result); var actual = result.First(); Assert.Equal(actual.PrincipalType, expectedPrincipalType); Assert.Equal(actual.PrincipalSID, expectedPrincipalSID); - Assert.Equal(actual.IsInherited, false); + Assert.False(actual.IsInherited); Assert.Equal(actual.RightName, expectedRightName.ToString()); } [Fact] - public void ACLProcessor_ProcessACL_Self() - { + public async Task ACLProcessor_ProcessACL_Self() { var expectedPrincipalType = Label.Group; var expectedPrincipalSID = "S-1-5-21-3130019616-2776909439-2417379446-512"; var expectedRightName = EdgeNames.AddSelf; - var mockLDAPUtils = new Mock(); + var mockLDAPUtils = new Mock(); var mockSecurityDescriptor = new Mock(MockBehavior.Loose, null); var mockRule = new Mock(MockBehavior.Loose, null); var collection = new List(); @@ -531,27 +525,29 @@ public void ACLProcessor_ProcessACL_Self() mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) - .Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType)); + .ReturnsAsync((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType))); + var mockData = new[] { LdapResult.Fail() }; + mockLDAPUtils.Setup(x => x.Query(It.IsAny(), It.IsAny())) + .Returns(mockData.ToAsyncEnumerable()); - var processor = new ACLProcessor(mockLDAPUtils.Object, true); - var bytes = Helpers.B64ToBytes(AddMemberSecurityDescriptor); - var result = processor.ProcessACL(bytes, _testDomainName, Label.Group, false).ToArray(); + var processor = new ACLProcessor(mockLDAPUtils.Object); + var bytes = Utils.B64ToBytes(AddMemberSecurityDescriptor); + var result = await processor.ProcessACL(bytes, _testDomainName, Label.Group, false).ToArrayAsync(); Assert.Single(result); var actual = result.First(); Assert.Equal(actual.PrincipalType, expectedPrincipalType); Assert.Equal(actual.PrincipalSID, expectedPrincipalSID); - Assert.Equal(actual.IsInherited, false); + Assert.False(actual.IsInherited); Assert.Equal(actual.RightName, expectedRightName); } [Fact] - public void ACLProcessor_ProcessACL_ExtendedRight_Domain_Unmatched() - { + public async Task ACLProcessor_ProcessACL_ExtendedRight_Domain_Unmatched() { var expectedPrincipalType = Label.Group; var expectedPrincipalSID = "S-1-5-21-3130019616-2776909439-2417379446-512"; - var mockLDAPUtils = new Mock(); + var mockLDAPUtils = new Mock(); var mockSecurityDescriptor = new Mock(MockBehavior.Loose, null); var mockRule = new Mock(MockBehavior.Loose, null); var collection = new List(); @@ -567,23 +563,25 @@ public void ACLProcessor_ProcessACL_ExtendedRight_Domain_Unmatched() mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) - .Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType)); + .ReturnsAsync((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType))); + var mockData = new[] { LdapResult.Fail() }; + mockLDAPUtils.Setup(x => x.Query(It.IsAny(), It.IsAny())) + .Returns(mockData.ToAsyncEnumerable()); - var processor = new ACLProcessor(mockLDAPUtils.Object, true); - var bytes = Helpers.B64ToBytes(UnProtectedUserNtSecurityDescriptor); - var result = processor.ProcessACL(bytes, _testDomainName, Label.Domain, false).ToArray(); + var processor = new ACLProcessor(mockLDAPUtils.Object); + var bytes = Utils.B64ToBytes(UnProtectedUserNtSecurityDescriptor); + var result = await processor.ProcessACL(bytes, _testDomainName, Label.Domain, false).ToArrayAsync(); Assert.Empty(result); } [Fact] - public void ACLProcessor_ProcessACL_ExtendedRight_Domain_DSReplicationGetChanges() - { + public async Task ACLProcessor_ProcessACL_ExtendedRight_Domain_DSReplicationGetChanges() { var expectedPrincipalType = Label.Group; var expectedPrincipalSID = "S-1-5-21-3130019616-2776909439-2417379446-512"; var expectedRightName = EdgeNames.GetChanges; - var mockLDAPUtils = new Mock(); + var mockLDAPUtils = new Mock(); var mockSecurityDescriptor = new Mock(MockBehavior.Loose, null); var mockRule = new Mock(MockBehavior.Loose, null); var collection = new List(); @@ -599,28 +597,29 @@ public void ACLProcessor_ProcessACL_ExtendedRight_Domain_DSReplicationGetChanges mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) - .Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType)); + .ReturnsAsync((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType))); + mockLDAPUtils.Setup(x => x.Query(It.IsAny(), It.IsAny())) + .Returns(Array.Empty>().ToAsyncEnumerable); - var processor = new ACLProcessor(mockLDAPUtils.Object, true); - var bytes = Helpers.B64ToBytes(UnProtectedUserNtSecurityDescriptor); - var result = processor.ProcessACL(bytes, _testDomainName, Label.Domain, false).ToArray(); + var processor = new ACLProcessor(mockLDAPUtils.Object); + var bytes = Utils.B64ToBytes(UnProtectedUserNtSecurityDescriptor); + var result = await processor.ProcessACL(bytes, _testDomainName, Label.Domain, false).ToArrayAsync(); Assert.Single(result); var actual = result.First(); Assert.Equal(actual.PrincipalType, expectedPrincipalType); Assert.Equal(actual.PrincipalSID, expectedPrincipalSID); - Assert.Equal(actual.IsInherited, false); + Assert.False(actual.IsInherited); Assert.Equal(actual.RightName, expectedRightName); } [Fact] - public void ACLProcessor_ProcessACL_ExtendedRight_Domain_All() - { + public async Task ACLProcessor_ProcessACL_ExtendedRight_Domain_All() { var expectedPrincipalType = Label.Group; var expectedPrincipalSID = "S-1-5-21-3130019616-2776909439-2417379446-512"; var expectedRightName = EdgeNames.AllExtendedRights; - var mockLDAPUtils = new Mock(); + var mockLDAPUtils = new Mock(); var mockSecurityDescriptor = new Mock(MockBehavior.Loose, null); var mockRule = new Mock(MockBehavior.Loose, null); var collection = new List(); @@ -636,28 +635,29 @@ public void ACLProcessor_ProcessACL_ExtendedRight_Domain_All() mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) - .Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType)); + .ReturnsAsync((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType))); + mockLDAPUtils.Setup(x => x.Query(It.IsAny(), It.IsAny())) + .Returns(Array.Empty>().ToAsyncEnumerable); - var processor = new ACLProcessor(mockLDAPUtils.Object, true); - var bytes = Helpers.B64ToBytes(UnProtectedUserNtSecurityDescriptor); - var result = processor.ProcessACL(bytes, _testDomainName, Label.Domain, false).ToArray(); + var processor = new ACLProcessor(mockLDAPUtils.Object); + var bytes = Utils.B64ToBytes(UnProtectedUserNtSecurityDescriptor); + var result = await processor.ProcessACL(bytes, _testDomainName, Label.Domain, false).ToArrayAsync(); Assert.Single(result); var actual = result.First(); Assert.Equal(actual.PrincipalType, expectedPrincipalType); Assert.Equal(actual.PrincipalSID, expectedPrincipalSID); - Assert.Equal(actual.IsInherited, false); + Assert.False(actual.IsInherited); Assert.Equal(actual.RightName, expectedRightName); } [Fact] - public void ACLProcessor_ProcessACL_ExtendedRight_Domain_DSReplicationGetChangesAll() - { + public async Task ACLProcessor_ProcessACL_ExtendedRight_Domain_DSReplicationGetChangesAll() { var expectedPrincipalType = Label.Group; var expectedPrincipalSID = "S-1-5-21-3130019616-2776909439-2417379446-512"; var expectedRightName = EdgeNames.GetChangesAll; - var mockLDAPUtils = new Mock(); + var mockLDAPUtils = new Mock(); var mockSecurityDescriptor = new Mock(MockBehavior.Loose, null); var mockRule = new Mock(MockBehavior.Loose, null); var collection = new List(); @@ -673,29 +673,32 @@ public void ACLProcessor_ProcessACL_ExtendedRight_Domain_DSReplicationGetChanges mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) - .Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType)); + .ReturnsAsync((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType))); + var mockData = new[] { LdapResult.Fail() }; + mockLDAPUtils.Setup(x => x.Query(It.IsAny(), It.IsAny())) + .Returns(mockData.ToAsyncEnumerable()); + mockLDAPUtils.Setup(x => x.Query(It.IsAny(), It.IsAny())) + .Returns(Array.Empty>().ToAsyncEnumerable); - var processor = new ACLProcessor(mockLDAPUtils.Object, true); - var bytes = Helpers.B64ToBytes(UnProtectedUserNtSecurityDescriptor); - var result = processor.ProcessACL(bytes, _testDomainName, Label.Domain, false).ToArray(); + var processor = new ACLProcessor(mockLDAPUtils.Object); + var bytes = Utils.B64ToBytes(UnProtectedUserNtSecurityDescriptor); + var result = await processor.ProcessACL(bytes, _testDomainName, Label.Domain, false).ToArrayAsync(); Assert.Single(result); var actual = result.First(); Assert.Equal(actual.PrincipalType, expectedPrincipalType); Assert.Equal(actual.PrincipalSID, expectedPrincipalSID); - Assert.Equal(actual.IsInherited, false); + Assert.False(actual.IsInherited); Assert.Equal(actual.RightName, expectedRightName); } [Fact] - public void ACLProcessor_ProcessACL_ExtendedRight_User_Unmatched() - { + public async Task ACLProcessor_ProcessACL_ExtendedRight_User_Unmatched() { var expectedPrincipalType = Label.Group; var expectedPrincipalSID = "S-1-5-21-3130019616-2776909439-2417379446-512"; - var expectedRightName = EdgeNames.GetChangesAll; var unmatchedGuid = new Guid("583991c8-629d-4a07-8a70-74d19d22ac9c"); - var mockLDAPUtils = new Mock(); + var mockLDAPUtils = new Mock(); var mockSecurityDescriptor = new Mock(MockBehavior.Loose, null); var mockRule = new Mock(MockBehavior.Loose, null); var collection = new List(); @@ -711,23 +714,24 @@ public void ACLProcessor_ProcessACL_ExtendedRight_User_Unmatched() mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) - .Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType)); + .ReturnsAsync((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType))); + mockLDAPUtils.Setup(x => x.Query(It.IsAny(), It.IsAny())) + .Returns(Array.Empty>().ToAsyncEnumerable); - var processor = new ACLProcessor(mockLDAPUtils.Object, true); - var bytes = Helpers.B64ToBytes(UnProtectedUserNtSecurityDescriptor); - var result = processor.ProcessACL(bytes, _testDomainName, Label.User, false).ToArray(); + var processor = new ACLProcessor(mockLDAPUtils.Object); + var bytes = Utils.B64ToBytes(UnProtectedUserNtSecurityDescriptor); + var result = await processor.ProcessACL(bytes, _testDomainName, Label.User, false).ToArrayAsync(); Assert.Empty(result); } [Fact] - public void ACLProcessor_ProcessACL_ExtendedRight_User_UserForceChangePassword() - { + public async Task ACLProcessor_ProcessACL_ExtendedRight_User_UserForceChangePassword() { var expectedPrincipalType = Label.Group; var expectedPrincipalSID = "S-1-5-21-3130019616-2776909439-2417379446-512"; var expectedRightName = EdgeNames.ForceChangePassword; - var mockLDAPUtils = new Mock(); + var mockLDAPUtils = new Mock(); var mockSecurityDescriptor = new Mock(MockBehavior.Loose, null); var mockRule = new Mock(MockBehavior.Loose, null); var collection = new List(); @@ -743,28 +747,29 @@ public void ACLProcessor_ProcessACL_ExtendedRight_User_UserForceChangePassword() mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) - .Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType)); + .ReturnsAsync((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType))); + mockLDAPUtils.Setup(x => x.Query(It.IsAny(), It.IsAny())) + .Returns(Array.Empty>().ToAsyncEnumerable); - var processor = new ACLProcessor(mockLDAPUtils.Object, true); - var bytes = Helpers.B64ToBytes(UnProtectedUserNtSecurityDescriptor); - var result = processor.ProcessACL(bytes, _testDomainName, Label.User, false).ToArray(); + var processor = new ACLProcessor(mockLDAPUtils.Object); + var bytes = Utils.B64ToBytes(UnProtectedUserNtSecurityDescriptor); + var result = await processor.ProcessACL(bytes, _testDomainName, Label.User, false).ToArrayAsync(); Assert.Single(result); var actual = result.First(); Assert.Equal(actual.PrincipalType, expectedPrincipalType); Assert.Equal(actual.PrincipalSID, expectedPrincipalSID); - Assert.Equal(actual.IsInherited, false); + Assert.False(actual.IsInherited); Assert.Equal(actual.RightName, expectedRightName); } [Fact] - public void ACLProcessor_ProcessACL_ExtendedRight_User_All() - { + public async Task ACLProcessor_ProcessACL_ExtendedRight_User_All() { var expectedPrincipalType = Label.Group; var expectedPrincipalSID = "S-1-5-21-3130019616-2776909439-2417379446-512"; var expectedRightName = EdgeNames.AllExtendedRights; - var mockLDAPUtils = new Mock(); + var mockLDAPUtils = new Mock(); var mockSecurityDescriptor = new Mock(MockBehavior.Loose, null); var mockRule = new Mock(MockBehavior.Loose, null); var collection = new List(); @@ -780,28 +785,28 @@ public void ACLProcessor_ProcessACL_ExtendedRight_User_All() mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) - .Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType)); + .ReturnsAsync((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType))); + mockLDAPUtils.Setup(x => x.Query(It.IsAny(), It.IsAny())) + .Returns(Array.Empty>().ToAsyncEnumerable); - var processor = new ACLProcessor(mockLDAPUtils.Object, true); - var bytes = Helpers.B64ToBytes(UnProtectedUserNtSecurityDescriptor); - var result = processor.ProcessACL(bytes, _testDomainName, Label.User, false).ToArray(); + var processor = new ACLProcessor(mockLDAPUtils.Object); + var bytes = Utils.B64ToBytes(UnProtectedUserNtSecurityDescriptor); + var result = await processor.ProcessACL(bytes, _testDomainName, Label.User, false).ToArrayAsync(); Assert.Single(result); var actual = result.First(); Assert.Equal(actual.PrincipalType, expectedPrincipalType); Assert.Equal(actual.PrincipalSID, expectedPrincipalSID); - Assert.Equal(actual.IsInherited, false); + Assert.False(actual.IsInherited); Assert.Equal(actual.RightName, expectedRightName); } [Fact] - public void ACLProcessor_ProcessACL_ExtendedRight_Computer_NoLAPS() - { + public async Task ACLProcessor_ProcessACL_ExtendedRight_Computer_NoLAPS() { var expectedPrincipalType = Label.Group; var expectedPrincipalSID = "S-1-5-21-3130019616-2776909439-2417379446-512"; - var expectedRightName = EdgeNames.AllExtendedRights; - var mockLDAPUtils = new Mock(); + var mockLDAPUtils = new Mock(); var mockSecurityDescriptor = new Mock(MockBehavior.Loose, null); var mockRule = new Mock(MockBehavior.Loose, null); var collection = new List(); @@ -817,23 +822,24 @@ public void ACLProcessor_ProcessACL_ExtendedRight_Computer_NoLAPS() mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) - .Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType)); + .ReturnsAsync((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType))); + mockLDAPUtils.Setup(x => x.Query(It.IsAny(), It.IsAny())) + .Returns(Array.Empty>().ToAsyncEnumerable); - var processor = new ACLProcessor(mockLDAPUtils.Object, true); - var bytes = Helpers.B64ToBytes(UnProtectedUserNtSecurityDescriptor); - var result = processor.ProcessACL(bytes, _testDomainName, Label.Computer, false).ToArray(); + var processor = new ACLProcessor(mockLDAPUtils.Object); + var bytes = Utils.B64ToBytes(UnProtectedUserNtSecurityDescriptor); + var result = await processor.ProcessACL(bytes, _testDomainName, Label.Computer, false).ToArrayAsync(); Assert.Empty(result); } [Fact] - public void ACLProcessor_ProcessACL_ExtendedRight_Computer_All() - { + public async Task ACLProcessor_ProcessACL_ExtendedRight_Computer_All() { var expectedPrincipalType = Label.Group; var expectedPrincipalSID = "S-1-5-21-3130019616-2776909439-2417379446-512"; var expectedRightName = EdgeNames.AllExtendedRights; - var mockLDAPUtils = new Mock(); + var mockLDAPUtils = new Mock(); var mockSecurityDescriptor = new Mock(MockBehavior.Loose, null); var mockRule = new Mock(MockBehavior.Loose, null); var collection = new List(); @@ -849,33 +855,28 @@ public void ACLProcessor_ProcessACL_ExtendedRight_Computer_All() mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) - .Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType)); + .ReturnsAsync((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType))); + mockLDAPUtils.Setup(x => x.Query(It.IsAny(), It.IsAny())) + .Returns(Array.Empty>().ToAsyncEnumerable); - var processor = new ACLProcessor(mockLDAPUtils.Object, true); - var bytes = Helpers.B64ToBytes(UnProtectedUserNtSecurityDescriptor); - var result = processor.ProcessACL(bytes, _testDomainName, Label.Computer, true).ToArray(); + var processor = new ACLProcessor(mockLDAPUtils.Object); + var bytes = Utils.B64ToBytes(UnProtectedUserNtSecurityDescriptor); + var result = await processor.ProcessACL(bytes, _testDomainName, Label.Computer, true).ToArrayAsync(); Assert.Single(result); var actual = result.First(); Assert.Equal(actual.PrincipalType, expectedPrincipalType); Assert.Equal(actual.PrincipalSID, expectedPrincipalSID); - Assert.Equal(actual.IsInherited, false); + Assert.False(actual.IsInherited); Assert.Equal(actual.RightName, expectedRightName); } - [Fact(Skip = "Need to populate cache to reach this case")] - public void ACLProcessor_ProcessACL_ExtendedRight_Computer_MappedGuid() - { - } - [Fact] - public void ACLProcessor_ProcessACL_GenericWrite_Unmatched() - { + public async Task ACLProcessor_ProcessACL_GenericWrite_Unmatched() { var expectedPrincipalType = Label.Group; var expectedPrincipalSID = "S-1-5-21-3130019616-2776909439-2417379446-512"; - var expectedRightName = EdgeNames.AllExtendedRights; - var mockLDAPUtils = new Mock(); + var mockLDAPUtils = new Mock(); var mockSecurityDescriptor = new Mock(MockBehavior.Loose, null); var mockRule = new Mock(MockBehavior.Loose, null); var collection = new List(); @@ -891,23 +892,24 @@ public void ACLProcessor_ProcessACL_GenericWrite_Unmatched() mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) - .Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType)); + .ReturnsAsync((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType))); + mockLDAPUtils.Setup(x => x.Query(It.IsAny(), It.IsAny())) + .Returns(Array.Empty>().ToAsyncEnumerable); - var processor = new ACLProcessor(mockLDAPUtils.Object, true); - var bytes = Helpers.B64ToBytes(UnProtectedUserNtSecurityDescriptor); - var result = processor.ProcessACL(bytes, _testDomainName, Label.Container, true).ToArray(); + var processor = new ACLProcessor(mockLDAPUtils.Object); + var bytes = Utils.B64ToBytes(UnProtectedUserNtSecurityDescriptor); + var result = await processor.ProcessACL(bytes, _testDomainName, Label.Container, true).ToArrayAsync(); Assert.Empty(result); } [Fact] - public void ACLProcessor_ProcessACL_GenericWrite_User_All() - { + public async Task ACLProcessor_ProcessACL_GenericWrite_User_All() { var expectedPrincipalType = Label.Group; var expectedPrincipalSID = "S-1-5-21-3130019616-2776909439-2417379446-512"; var expectedRightName = EdgeNames.GenericWrite; - var mockLDAPUtils = new Mock(); + var mockLDAPUtils = new Mock(); var mockSecurityDescriptor = new Mock(MockBehavior.Loose, null); var mockRule = new Mock(MockBehavior.Loose, null); var collection = new List(); @@ -923,28 +925,29 @@ public void ACLProcessor_ProcessACL_GenericWrite_User_All() mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) - .Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType)); + .ReturnsAsync((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType))); + mockLDAPUtils.Setup(x => x.Query(It.IsAny(), It.IsAny())) + .Returns(Array.Empty>().ToAsyncEnumerable); - var processor = new ACLProcessor(mockLDAPUtils.Object, true); - var bytes = Helpers.B64ToBytes(UnProtectedUserNtSecurityDescriptor); - var result = processor.ProcessACL(bytes, _testDomainName, Label.User, true).ToArray(); + var processor = new ACLProcessor(mockLDAPUtils.Object); + var bytes = Utils.B64ToBytes(UnProtectedUserNtSecurityDescriptor); + var result = await processor.ProcessACL(bytes, _testDomainName, Label.User, true).ToArrayAsync(); Assert.Single(result); var actual = result.First(); Assert.Equal(actual.PrincipalType, expectedPrincipalType); Assert.Equal(actual.PrincipalSID, expectedPrincipalSID); - Assert.Equal(actual.IsInherited, false); + Assert.False(actual.IsInherited); Assert.Equal(actual.RightName, expectedRightName); } [Fact] - public void ACLProcessor_ProcessACL_GenericWrite_User_WriteMember() - { + public async Task ACLProcessor_ProcessACL_GenericWrite_User_WriteMember() { var expectedPrincipalType = Label.Group; var expectedPrincipalSID = "S-1-5-21-3130019616-2776909439-2417379446-512"; var expectedRightName = EdgeNames.AddMember; - var mockLDAPUtils = new Mock(); + var mockLDAPUtils = new Mock(); var mockSecurityDescriptor = new Mock(MockBehavior.Loose, null); var mockRule = new Mock(MockBehavior.Loose, null); var collection = new List(); @@ -960,11 +963,13 @@ public void ACLProcessor_ProcessACL_GenericWrite_User_WriteMember() mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) - .Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType)); + .ReturnsAsync((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType))); + mockLDAPUtils.Setup(x => x.Query(It.IsAny(), It.IsAny())) + .Returns(Array.Empty>().ToAsyncEnumerable); - var processor = new ACLProcessor(mockLDAPUtils.Object, true); - var bytes = Helpers.B64ToBytes(AddMemberSecurityDescriptor); - var result = processor.ProcessACL(bytes, _testDomainName, Label.Group, true).ToArray(); + var processor = new ACLProcessor(mockLDAPUtils.Object); + var bytes = Utils.B64ToBytes(AddMemberSecurityDescriptor); + var result = await processor.ProcessACL(bytes, _testDomainName, Label.Group, true).ToArrayAsync(); _testOutputHelper.WriteLine(JsonConvert.SerializeObject(result)); @@ -972,18 +977,17 @@ public void ACLProcessor_ProcessACL_GenericWrite_User_WriteMember() var actual = result.First(); Assert.Equal(actual.PrincipalType, expectedPrincipalType); Assert.Equal(actual.PrincipalSID, expectedPrincipalSID); - Assert.Equal(actual.IsInherited, false); + Assert.False(actual.IsInherited); Assert.Equal(actual.RightName, expectedRightName); } [Fact] - public void ACLProcessor_ProcessACL_GenericWrite_Computer_WriteAllowedToAct() - { + public async Task ACLProcessor_ProcessACL_GenericWrite_Computer_WriteAllowedToAct() { var expectedPrincipalType = Label.Group; var expectedPrincipalSID = "S-1-5-21-3130019616-2776909439-2417379446-512"; var expectedRightName = EdgeNames.AddAllowedToAct; - var mockLDAPUtils = new Mock(); + var mockLDAPUtils = new Mock(); var mockSecurityDescriptor = new Mock(MockBehavior.Loose, null); var mockRule = new Mock(MockBehavior.Loose, null); var collection = new List(); @@ -999,18 +1003,95 @@ public void ACLProcessor_ProcessACL_GenericWrite_Computer_WriteAllowedToAct() mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) - .Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType)); + .ReturnsAsync((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType))); + mockLDAPUtils.Setup(x => x.Query(It.IsAny(), It.IsAny())) + .Returns(Array.Empty>().ToAsyncEnumerable); - var processor = new ACLProcessor(mockLDAPUtils.Object, true); - var bytes = Helpers.B64ToBytes(UnProtectedUserNtSecurityDescriptor); - var result = processor.ProcessACL(bytes, _testDomainName, Label.Computer, true).ToArray(); + var processor = new ACLProcessor(mockLDAPUtils.Object); + var bytes = Utils.B64ToBytes(UnProtectedUserNtSecurityDescriptor); + var result = await processor.ProcessACL(bytes, _testDomainName, Label.Computer, true).ToArrayAsync(); Assert.Single(result); var actual = result.First(); Assert.Equal(actual.PrincipalType, expectedPrincipalType); Assert.Equal(actual.PrincipalSID, expectedPrincipalSID); - Assert.Equal(actual.IsInherited, false); + Assert.False(actual.IsInherited); Assert.Equal(actual.RightName, expectedRightName); } + + [Fact] + public void GetInheritedAceHashes_NullSD_Empty() { + var proc = new ACLProcessor(new MockLdapUtils()); + var result = proc.GetInheritedAceHashes(null).ToArray(); + Assert.Empty(result); + } + + [Fact] + public void GetInheritedAceHashes_HappyPath() { + var mockLDAPUtils = new Mock(); + var mockSecurityDescriptor = new Mock(MockBehavior.Loose, null); + var mockRule = new Mock(MockBehavior.Loose, null); + const string expectedPrincipalSID = "S-1-5-21-3130019616-2776909439-2417379446-512"; + var collection = new List(); + mockRule.Setup(x => x.AccessControlType()).Returns(AccessControlType.Allow); + mockRule.Setup(x => x.IsAceInheritedFrom(It.IsAny())).Returns(true); + mockRule.Setup(x => x.IdentityReference()).Returns(expectedPrincipalSID); + mockRule.Setup(x => x.ActiveDirectoryRights()).Returns(ActiveDirectoryRights.GenericWrite); + mockRule.Setup(x => x.ObjectType()).Returns(new Guid(ACEGuids.WriteAllowedToAct)); + mockRule.Setup(x => x.IsInherited()).Returns(true); + mockRule.Setup(x => x.InheritanceFlags).Returns(InheritanceFlags.ContainerInherit); + collection.Add(mockRule.Object); + mockRule = new Mock(MockBehavior.Loose, null); + mockRule.Setup(x => x.AccessControlType()).Returns(AccessControlType.Allow); + mockRule.Setup(x => x.IsAceInheritedFrom(It.IsAny())).Returns(true); + mockRule.Setup(x => x.IdentityReference()).Returns(expectedPrincipalSID); + mockRule.Setup(x => x.ActiveDirectoryRights()).Returns(ActiveDirectoryRights.GenericWrite); + mockRule.Setup(x => x.ObjectType()).Returns(new Guid(ACEGuids.WriteAllowedToAct)); + mockRule.Setup(x => x.IsInherited()).Returns(false); + mockRule.Setup(x => x.InheritanceFlags).Returns(InheritanceFlags.ContainerInherit); + collection.Add(mockRule.Object); + mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(collection); + mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); + var processor = new ACLProcessor(mockLDAPUtils.Object); + var result = processor.GetInheritedAceHashes(Array.Empty()).ToArray(); + Assert.Single(result); + } + + [Fact] + public void Test_ACLInheritanceHashSame() { + const string expectedPrincipalSID = "S-1-5-21-3130019616-2776909439-2417379446-512"; + var g = new Guid().ToString(); + var result1 = ACLProcessor.CalculateInheritanceHash(expectedPrincipalSID, + ActiveDirectoryRights.GenericWrite, new Guid(ACEGuids.WriteAllowedToAct).ToString(), g); + var result2 = ACLProcessor.CalculateInheritanceHash(expectedPrincipalSID, + ActiveDirectoryRights.GenericWrite, new Guid(ACEGuids.WriteAllowedToAct).ToString(), g); + + Assert.Equal(result1, result2); + } + + [Fact] + public void Test_ACLProcessor_IsACLProtected_Protected() { + var mockLDAPUtils = new Mock(); + var mockSecurityDescriptor = new Mock(MockBehavior.Loose, null); + mockSecurityDescriptor.Setup(x => x.AreAccessRulesProtected()).Returns(true); + mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); + + var processor = new ACLProcessor(mockLDAPUtils.Object); + var result = processor.IsACLProtected(Array.Empty()); + Assert.True(result); + } + + [Fact] + public void Test_ACLProcessor_IsACLProtected_NotProtected() { + var mockLDAPUtils = new Mock(); + var mockSecurityDescriptor = new Mock(MockBehavior.Loose, null); + mockSecurityDescriptor.Setup(x => x.AreAccessRulesProtected()).Returns(false); + mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); + + var processor = new ACLProcessor(mockLDAPUtils.Object); + var result = processor.IsACLProtected(Array.Empty()); + Assert.False(result); + } } } \ No newline at end of file diff --git a/test/unit/AsyncEnumerableTests.cs b/test/unit/AsyncEnumerableTests.cs new file mode 100644 index 00000000..33664030 --- /dev/null +++ b/test/unit/AsyncEnumerableTests.cs @@ -0,0 +1,92 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using SharpHoundCommonLib; +using Xunit; + +namespace CommonLibTest; + +public class AsyncEnumerableTests { + [Fact] + public async Task AsyncEnumerable_DefaultIfEmpty_Empty() { + var enumerable = AsyncEnumerable.Empty().DefaultIfEmpty(1); + var e = enumerable.GetAsyncEnumerator(); + var res = await e.MoveNextAsync(); + Assert.True(res); + Assert.Equal(1, e.Current); + Assert.False(await e.MoveNextAsync()); + } + + [Fact] + public async Task AsyncEnumerable_FirstOrDefault() { + var enumerable = AsyncEnumerable.Empty(); + var res = await enumerable.FirstOrDefaultAsync(); + Assert.Equal(0, res); + } + + [Fact] + public async Task AsyncEnumerable_FirstOrDefault_WithDefault() { + var enumerable = AsyncEnumerable.Empty(); + var res = await enumerable.FirstOrDefaultAsync(10); + Assert.Equal(10, res); + } + + [Fact] + public async Task AsyncEnumerable_CombinedOperators() { + var enumerable = AsyncEnumerable.Empty(); + var res = await enumerable.DefaultIfEmpty("abc").FirstOrDefaultAsync(); + Assert.Equal("abc", res); + } + + [Fact] + public async Task AsyncEnumerable_ToAsyncEnumerable() { + var collection = new[] { + "a", "b", "c" + }; + + var test = collection.ToAsyncEnumerable(); + + var index = 0; + await foreach (var item in test) { + Assert.Equal(collection[index], item); + index++; + } + } + + [Fact] + public async Task AsyncEnumerable_FirstOrDefaultFunction() { + var test = await TestFunc().FirstOrDefaultAsync(); + Assert.Equal("a", test); + } + + [Fact] + public async Task AsyncEnumerable_CombinedFunction() { + var test = await TestFunc().DefaultIfEmpty("d").FirstOrDefaultAsync(); + Assert.Equal("a", test); + } + + [Fact] + public async Task AsyncEnumerable_FirstOrDefaultEmptyFunction() { + var test = await EmptyFunc().FirstOrDefaultAsync(); + Assert.Null(test); + } + + [Fact] + public async Task AsyncEnumerable_CombinedEmptyFunction() { + var test = await EmptyFunc().DefaultIfEmpty("d").FirstOrDefaultAsync(); + Assert.Equal("d", test); + } + + private async IAsyncEnumerable TestFunc() { + var collection = new[] { + "a", "b", "c" + }; + + foreach (var i in collection) { + yield return i; + } + } + + private async IAsyncEnumerable EmptyFunc() { + yield break; + } +} \ No newline at end of file diff --git a/test/unit/CertAbuseProcessorTest.cs b/test/unit/CertAbuseProcessorTest.cs index 6ecf8a0c..3a5ce818 100644 --- a/test/unit/CertAbuseProcessorTest.cs +++ b/test/unit/CertAbuseProcessorTest.cs @@ -1,30 +1,19 @@ using System; -using System.DirectoryServices; -using CommonLibTest.Facades; -using Moq; -using Newtonsoft.Json; -using SharpHoundCommonLib; -using SharpHoundCommonLib.Enums; -using SharpHoundCommonLib.Processors; -using Xunit; using Xunit.Abstractions; -namespace CommonLibTest -{ - public class CertAbuseProcessorTest : IDisposable - { +namespace CommonLibTest { + //TODO: Make these tests work + public class CertAbuseProcessorTest : IDisposable { private const string CASecurityFixture = "AQAUhCABAAAwAQAAFAAAAEQAAAACADAAAgAAAALAFAD//wAAAQEAAAAAAAEAAAAAAsAUAP//AAABAQAAAAAABQcAAAACANwABwAAAAADGAABAAAAAQIAAAAAAAUgAAAAIAIAAAADGAACAAAAAQIAAAAAAAUgAAAAIAIAAAADJAABAAAAAQUAAAAAAAUVAAAAIE+Qun9GhKV2SBaQAAIAAAADJAACAAAAAQUAAAAAAAUVAAAAIE+Qun9GhKV2SBaQAAIAAAADJAABAAAAAQUAAAAAAAUVAAAAIE+Qun9GhKV2SBaQBwIAAAADJAACAAAAAQUAAAAAAAUVAAAAIE+Qun9GhKV2SBaQBwIAAAADFAAAAgAAAQEAAAAAAAULAAAAAQIAAAAAAAUgAAAAIAIAAAECAAAAAAAFIAAAACACAAA="; private readonly ITestOutputHelper _testOutputHelper; - public CertAbuseProcessorTest(ITestOutputHelper testOutputHelper) - { + public CertAbuseProcessorTest(ITestOutputHelper testOutputHelper) { _testOutputHelper = testOutputHelper; } - public void Dispose() - { + public void Dispose() { } // [Fact] diff --git a/test/unit/CommonLibHelperTests.cs b/test/unit/CommonLibHelperTests.cs index db95033d..2c054eed 100644 --- a/test/unit/CommonLibHelperTests.cs +++ b/test/unit/CommonLibHelperTests.cs @@ -1,38 +1,35 @@ using System; using System.Text; +using SharpHoundCommonLib; using SharpHoundCommonLib.Enums; using Xunit; -namespace CommonLibTest -{ - public class CommonLibHelperTest - { +namespace CommonLibTest { + public class CommonLibHelperTest { [Fact] - public void RemoveDistinguishedNamePrefix_ExpectedResult() - { + public void RemoveDistinguishedNamePrefix_ExpectedResult() { var dn = "CN=Jeff Smith,OU=Sales,DC=Fabrikam,DC=COM"; - var result = SharpHoundCommonLib.Helpers.RemoveDistinguishedNamePrefix(dn); + var result = Helpers.RemoveDistinguishedNamePrefix(dn); Assert.Equal("OU=Sales,DC=Fabrikam,DC=COM", result); - result = SharpHoundCommonLib.Helpers.RemoveDistinguishedNamePrefix( + result = Helpers.RemoveDistinguishedNamePrefix( "CN=Administrator,CN=Users,DC=testlab,DC=local"); Assert.Equal("CN=Users,DC=testlab,DC=local", result); - result = SharpHoundCommonLib.Helpers.RemoveDistinguishedNamePrefix( + result = Helpers.RemoveDistinguishedNamePrefix( "CN=Litware,OU=Docs\\, Adatum,DC=Fabrikam,DC=COM"); Assert.Equal("OU=Docs\\, Adatum,DC=Fabrikam,DC=COM", result); - result = SharpHoundCommonLib.Helpers.RemoveDistinguishedNamePrefix( + result = Helpers.RemoveDistinguishedNamePrefix( "OU=Test\\, OU,OU=Test,DC=Fabrikam,DC=COM"); Assert.Equal("OU=Test,DC=Fabrikam,DC=COM", result); } [Fact] - public void SplitGPLinkProperty_ValidPropFilterEnabled_ExpectedResult() - { + public void SplitGPLinkProperty_ValidPropFilterEnabled_ExpectedResult() { var isPropFilterEnabled = false; //TODO: Ari, proper test string? var testGPLinkProperty = "[LDAP:/o=foo/ou=foo Group (ABC123)/cn=foouser (blah)123; SIP:foouser@example.co.uk; smtp:foouser@sub1.example.co.uk; smtp:foouser@sub2.example.co.uk; SMTP:foouser@example.co.uk][]"; - var res = SharpHoundCommonLib.Helpers.SplitGPLinkProperty(testGPLinkProperty, isPropFilterEnabled); + var res = Helpers.SplitGPLinkProperty(testGPLinkProperty, isPropFilterEnabled); foreach (var parsedGPLink in res) Assert.Equal("cn=foouser (blah)123", parsedGPLink.DistinguishedName); @@ -40,14 +37,13 @@ public void SplitGPLinkProperty_ValidPropFilterEnabled_ExpectedResult() } [Fact] - public void SplitGPLinkProperty_ValidPropFilterDisabled_ExpectedResult() - { + public void SplitGPLinkProperty_ValidPropFilterDisabled_ExpectedResult() { var isPropFilterEnabled = false; //TODO: Ari, proper test string? var testGPLinkProperty = "[LDAP:/o=foo/ou=foo Group (ABC123)/cn=foouser (blah)123; SIP:foouser@example.co.uk; smtp:foouser@sub1.example.co.uk; smtp:foouser@sub2.example.co.uk; SMTP:foouser@example.co.uk][]"; - var res = SharpHoundCommonLib.Helpers.SplitGPLinkProperty(testGPLinkProperty, isPropFilterEnabled); + var res = Helpers.SplitGPLinkProperty(testGPLinkProperty, isPropFilterEnabled); foreach (var parsedGPLink in res) Assert.Equal("cn=foouser (blah)123", parsedGPLink.DistinguishedName); @@ -56,14 +52,13 @@ public void SplitGPLinkProperty_ValidPropFilterDisabled_ExpectedResult() /// [Fact] - public void SplitGPLinkProperty_PropWithUnsupportedDelimiter_FilterEnabled_ExpectedResult() - { + public void SplitGPLinkProperty_PropWithUnsupportedDelimiter_FilterEnabled_ExpectedResult() { var isPropFilterEnabled = true; //TODO: Ari, proper test string? var testGPLinkProperty = "[LDAP:/o=foo/ou=foo Group (ABC123)/cn=foouser (blah)123; DC=somedomainName; SIP:foouser@example.co.uk; smtp:foouser@sub1.example.co.uk; smtp:foouser@sub2.example.co.uk; SMTP:foouser@example.co.uk][]"; - var res = SharpHoundCommonLib.Helpers.SplitGPLinkProperty(testGPLinkProperty, isPropFilterEnabled); + var res = Helpers.SplitGPLinkProperty(testGPLinkProperty, isPropFilterEnabled); foreach (var parsedGPLink in res) Assert.Equal("cn=foouser (blah)123", parsedGPLink.DistinguishedName); @@ -71,20 +66,17 @@ public void SplitGPLinkProperty_PropWithUnsupportedDelimiter_FilterEnabled_Expec } [Fact] - public void SplitGPLinkProperty_InValidPropFilterDisabled_ExpectedResult() - { + public void SplitGPLinkProperty_InValidPropFilterDisabled_ExpectedResult() { var isPropFilterEnabled = false; //TODO: Ari, proper test string? var testGPLinkProperty = "/*obviously wrong data*/"; - var res = SharpHoundCommonLib.Helpers.SplitGPLinkProperty(testGPLinkProperty, isPropFilterEnabled); + var res = Helpers.SplitGPLinkProperty(testGPLinkProperty, isPropFilterEnabled); Assert.Empty(res); } [Fact] - public void SamAccountTypeToType_ValidString_CorrectLabel() - { - var accountTypeLookup = new (string accountType, Label label)[] - { + public void SamAccountTypeToType_ValidString_CorrectLabel() { + var accountTypeLookup = new (string accountType, Label label)[] { (accountType: "268435456", label: Label.Group), (accountType: "268435457", label: Label.Group), (accountType: "536870912", label: Label.Group), @@ -93,17 +85,15 @@ public void SamAccountTypeToType_ValidString_CorrectLabel() (accountType: "805306368", Label.User) }; - foreach (var e in accountTypeLookup) - { - var result = SharpHoundCommonLib.Helpers.SamAccountTypeToType(e.accountType); + foreach (var e in accountTypeLookup) { + var result = Helpers.SamAccountTypeToType(e.accountType); Assert.Equal(result, e.label); } } [Fact] - public void SamAccountTypeToType_InValidString_CorrectLabel() - { - var result = SharpHoundCommonLib.Helpers.SamAccountTypeToType("nonsense_^&^^&(*^*^*&(&^&(^*AAAA"); + public void SamAccountTypeToType_InValidString_CorrectLabel() { + var result = Helpers.SamAccountTypeToType("nonsense_^&^^&(*^*^*&(&^&(^*AAAA"); Assert.Equal(Label.Base, result); } @@ -120,12 +110,11 @@ public void SamAccountTypeToType_InValidString_CorrectLabel() // } [Fact] - public void ConvertGuidToHexGuid_ValidStringGuid_ValidHex() - { + public void ConvertGuidToHexGuid_ValidStringGuid_ValidHex() { // Atmoic conversion test. Add as many variants as needed to increase confidence. var guid = Guid.NewGuid(); - var hexString = SharpHoundCommonLib.Helpers.ConvertGuidToHexGuid(guid.ToString()); + var hexString = Helpers.ConvertGuidToHexGuid(guid.ToString()); // We recreate part of thr operation here to test parity. If you the function under test changes this code then the test may fail and indicate drift in the code away from expected behavior. var output = $"\\{BitConverter.ToString(guid.ToByteArray()).Replace('-', '\\')}"; @@ -133,108 +122,113 @@ public void ConvertGuidToHexGuid_ValidStringGuid_ValidHex() } [Fact] - public void DistinguishedNameToDomain_ValidDistinguishedName_ExpectedDomainValue() - { + public void DistinguishedNameToDomain_ValidDistinguishedName_ExpectedDomainValue() { var expected = "FABRIKAM.COM"; var actual = - SharpHoundCommonLib.Helpers.DistinguishedNameToDomain("CN=Jeff Smith,OU=Sales,DC=Fabrikam,DC=COM"); + Helpers.DistinguishedNameToDomain("CN=Jeff Smith,OU=Sales,DC=Fabrikam,DC=COM"); Assert.Equal(expected, actual); } [Fact] - public void DistinguishedNameToDomain_InValidDistinguishedName_ReturnsNull() - { + public void DistinguishedNameToDomain_InValidDistinguishedName_ReturnsNull() { var testDCQuery = "[LDAP:/o=foo/ou=foo Group (ABC123)/cn=foouser (blah)123; DX=wjatvar][]"; - var actual = SharpHoundCommonLib.Helpers.DistinguishedNameToDomain(testDCQuery); + var actual = Helpers.DistinguishedNameToDomain(testDCQuery); Assert.Null(actual); } [Fact] - public void StripServicePrincipalName_ValidServicePrincipal_ExpectedHostName() - { + public void StripServicePrincipalName_ValidServicePrincipal_ExpectedHostName() { var testString = "www/WEB-SERVER-01.adsec.local"; var expected = "WEB-SERVER-01.adsec.local"; - var actual = SharpHoundCommonLib.Helpers.StripServicePrincipalName(testString); + var actual = Helpers.StripServicePrincipalName(testString); Assert.Equal(expected, actual); } [Fact] - public void StripServicePrincipalName_InValidServicePrincipal_ExpectedHostName() - { + public void StripServicePrincipalName_InValidServicePrincipal_ExpectedHostName() { var testString = "234234f___bb4::fadfs"; var expected = "234234f___bb4::fadfs"; - var actual = SharpHoundCommonLib.Helpers.StripServicePrincipalName(testString); + var actual = Helpers.StripServicePrincipalName(testString); Assert.Equal(expected, actual); } [Fact] - public void StripServicePrincipalName_EmptyHost_Valid() - { + public void StripServicePrincipalName_EmptyHost_Valid() { var testString = "MSSQLSvc/:1433"; var expected = ""; - var actual = SharpHoundCommonLib.Helpers.StripServicePrincipalName(testString); + var actual = Helpers.StripServicePrincipalName(testString); Assert.Equal(expected, actual); } [Fact] - public void B64ToBytes_String_ValidBase64String() - { + public void B64ToBytes_String_ValidBase64String() { var testString = "obviously nonsense"; var exampleBytes = Encoding.UTF8.GetBytes(testString); var compareString = Convert.ToBase64String(exampleBytes); - var result = SharpHoundCommonLib.Helpers.Base64(testString); + var result = Helpers.Base64(testString); Assert.Equal(compareString, result); } [Fact] - public void ConvertFileTimeToUnixEpoch_ValidFileTime_ValidUnixEpoch() - { + public void ConvertFileTimeToUnixEpoch_ValidFileTime_ValidUnixEpoch() { var testFileTime = "132260149842749745"; - var result = SharpHoundCommonLib.Helpers.ConvertFileTimeToUnixEpoch(testFileTime); + var result = Helpers.ConvertFileTimeToUnixEpoch(testFileTime); var expected = 1581541384; Assert.Equal(expected, result); } [Fact] - public void ConvertFileTimeToUnixEpoch_Null_NegativeOne() - { - var result = SharpHoundCommonLib.Helpers.ConvertFileTimeToUnixEpoch(null); + public void ConvertFileTimeToUnixEpoch_Null_NegativeOne() { + var result = Helpers.ConvertFileTimeToUnixEpoch(null); Assert.Equal(-1, result); } - [WindowsOnlyFact] - public void ConvertFileTimeToUnixEpoch_WrongFormat_FortmatException() - { + [Fact] + public void ConvertFileTimeToUnixEpoch_WrongFormat_FortmatException() { Exception ex = - Assert.Throws(() => SharpHoundCommonLib.Helpers.ConvertFileTimeToUnixEpoch("asdsf")); - Assert.Equal("Input string was not in a correct format.", ex.Message); + Assert.Throws(() => Helpers.ConvertFileTimeToUnixEpoch("asdsf")); + Assert.Equal("The input string 'asdsf' was not in a correct format.", ex.Message); } [Fact] - public void ConvertFileTimeToUnixEpoch_BadInput_CastExceptionReturnsNegativeOne() - { - var result = SharpHoundCommonLib.Helpers.ConvertFileTimeToUnixEpoch("-3242432"); + public void ConvertFileTimeToUnixEpoch_BadInput_CastExceptionReturnsNegativeOne() { + var result = Helpers.ConvertFileTimeToUnixEpoch("-3242432"); Assert.Equal(-1, result); } [Fact] - public void ConvertTimestampToUnixEpoch_ValidTimestamp_ValidUnixEpoch() - { + public void ConvertTimestampToUnixEpoch_ValidTimestamp_ValidUnixEpoch() { var d = DateTime.Parse("2021-06-21T00:00:00"); var result = - SharpHoundCommonLib.Helpers.ConvertFileTimeToUnixEpoch(d.ToFileTimeUtc().ToString()); // get the epoch + Helpers.ConvertFileTimeToUnixEpoch(d.ToFileTimeUtc().ToString()); // get the epoch var dateTimeOffset = DateTimeOffset.FromUnixTimeSeconds(result); // create an offset from the epoch var testDate = dateTimeOffset.UtcDateTime; Assert.Equal(d.ToUniversalTime().Date, testDate); } - [WindowsOnlyFact] - public void ConvertTimestampToUnixEpoch_InvalidTimestamp_FormatException() - { + [Fact] + public void ConvertTimestampToUnixEpoch_InvalidTimestamp_FormatException() { Exception ex = Assert.Throws(() => - SharpHoundCommonLib.Helpers.ConvertFileTimeToUnixEpoch("-201adsfasf12180244")); - Assert.Equal("Input string was not in a correct format.", ex.Message); + Helpers.ConvertFileTimeToUnixEpoch("-201adsfasf12180244")); + Assert.Equal("The input string '-201adsfasf12180244' was not in a correct format.", ex.Message); + } + + [Fact] + public void DistinguishedNameToDomain_RegularObject_CorrectDomain() { + var result = Helpers.DistinguishedNameToDomain( + "CN=Account Operators,CN=Builtin,DC=testlab,DC=local"); + Assert.Equal("TESTLAB.LOCAL", result); + + result = Helpers.DistinguishedNameToDomain("DC=testlab,DC=local"); + Assert.Equal("TESTLAB.LOCAL", result); + } + + [Fact] + public void DistinguishedNameToDomain_DeletedObjects_CorrectDomain() { + var result = Helpers.DistinguishedNameToDomain( + @"DC=..Deleted-_msdcs.testlab.local\0ADEL:af1f072f-28d7-4b86-9b87-a408bfc9cb0d,CN=Deleted Objects,DC=testlab,DC=local"); + Assert.Equal("TESTLAB.LOCAL", result); } } } \ No newline at end of file diff --git a/test/unit/CommonLibTest.csproj b/test/unit/CommonLibTest.csproj index 11591e28..717d5c93 100644 --- a/test/unit/CommonLibTest.csproj +++ b/test/unit/CommonLibTest.csproj @@ -1,7 +1,7 @@ - net5.0 + net7.0 false true ..\..\docfx\coverage\ @@ -16,14 +16,14 @@ - + - + - - - + + + diff --git a/test/unit/ComputerAvailabilityTests.cs b/test/unit/ComputerAvailabilityTests.cs index 96fbda24..2a4dd9e7 100644 --- a/test/unit/ComputerAvailabilityTests.cs +++ b/test/unit/ComputerAvailabilityTests.cs @@ -36,7 +36,7 @@ public void Dispose() public async Task ComputerAvailability_IsComputerAvailable_BadOperatingSystem_ReturnsFalse() { var processor = new ComputerAvailability(); - var test = await processor.IsComputerAvailable("test", "Linux Mint 1.0", "132682398326125518"); + var test = await processor.IsComputerAvailable("test", "Linux Mint 1.0", "132682398326125518", "132682398326125518"); Assert.False(test.Connectable); Assert.Equal(ComputerStatus.NonWindowsOS, test.Error); @@ -50,10 +50,10 @@ public async Task ComputerAvailability_IsComputerAvailable_OldPwdLastSet_Returns //Create a date 91 days ago. Our threshold for pwdlastset is 90 days var n = DateTime.Now.AddDays(-91) - new DateTime(1601, 01, 01, 0, 0, 0, DateTimeKind.Utc); - var test = await processor.IsComputerAvailable("test", "Windows 10 Enterprise", n.Ticks.ToString()); + var test = await processor.IsComputerAvailable("test", "Windows 10 Enterprise", n.Ticks.ToString(), n.Ticks.ToString()); Assert.False(test.Connectable); - Assert.Equal(ComputerStatus.OldPwd, test.Error); + Assert.Equal(ComputerStatus.NotActive, test.Error); } [Fact] @@ -64,7 +64,7 @@ public async Task ComputerAvailability_IsComputerAvailable_PortClosed_ReturnsFal //Create a date 5 days ago var n = DateTime.Now.AddDays(-5) - new DateTime(1601, 01, 01, 0, 0, 0, DateTimeKind.Utc); - var test = await processor.IsComputerAvailable("test", "Windows 10 Enterprise", n.Ticks.ToString()); + var test = await processor.IsComputerAvailable("test", "Windows 10 Enterprise", n.Ticks.ToString(), n.Ticks.ToString()); Assert.False(test.Connectable); Assert.Equal(ComputerStatus.PortNotOpen, test.Error); @@ -78,7 +78,7 @@ public async Task ComputerAvailability_IsComputerAvailable_PortOpen_ReturnsTrue( //Create a date 5 days ago var n = DateTime.Now.AddDays(-5) - new DateTime(1601, 01, 01, 0, 0, 0, DateTimeKind.Utc); - var test = await processor.IsComputerAvailable("test", "Windows 10 Enterprise", n.Ticks.ToString()); + var test = await processor.IsComputerAvailable("test", "Windows 10 Enterprise", n.Ticks.ToString(), n.Ticks.ToString()); Assert.True(test.Connectable); } diff --git a/test/unit/ComputerSessionProcessorTest.cs b/test/unit/ComputerSessionProcessorTest.cs index f5935810..58f009df 100644 --- a/test/unit/ComputerSessionProcessorTest.cs +++ b/test/unit/ComputerSessionProcessorTest.cs @@ -34,7 +34,7 @@ public void Dispose() #endregion - [WindowsOnlyFact] + [Fact] public async Task ComputerSessionProcessor_ReadUserSessions_FilteringWorks() { var mockNativeMethods = new Mock(); @@ -47,13 +47,13 @@ public async Task ComputerSessionProcessor_ReadUserSessions_FilteringWorks() }; mockNativeMethods.Setup(x => x.NetSessionEnum(It.IsAny())).Returns(apiResult); - var processor = new ComputerSessionProcessor(new MockLDAPUtils(), "dfm", mockNativeMethods.Object); + var processor = new ComputerSessionProcessor(new MockLdapUtils(), "dfm", mockNativeMethods.Object); var result = await processor.ReadUserSessions("win10", _computerSid, _computerDomain); Assert.True(result.Collected); Assert.Empty(result.Results); } - [WindowsOnlyFact] + [Fact] public async Task ComputerSessionProcessor_ReadUserSessions_ResolvesHost() { var mockNativeMethods = new Mock(); @@ -72,13 +72,13 @@ public async Task ComputerSessionProcessor_ReadUserSessions_ResolvesHost() } }; - var processor = new ComputerSessionProcessor(new MockLDAPUtils(), "dfm", mockNativeMethods.Object); + var processor = new ComputerSessionProcessor(new MockLdapUtils(), "dfm", mockNativeMethods.Object); var result = await processor.ReadUserSessions("win10", _computerSid, _computerDomain); Assert.True(result.Collected); Assert.Equal(expected, result.Results); } - [WindowsOnlyFact] + [Fact] public async Task ComputerSessionProcessor_ReadUserSessions_ResolvesLocalHostEquivalent() { var mockNativeMethods = new Mock(); @@ -97,13 +97,13 @@ public async Task ComputerSessionProcessor_ReadUserSessions_ResolvesLocalHostEqu } }; - var processor = new ComputerSessionProcessor(new MockLDAPUtils(), "dfm", mockNativeMethods.Object); + var processor = new ComputerSessionProcessor(new MockLdapUtils(), "dfm", mockNativeMethods.Object); var result = await processor.ReadUserSessions("win10", _computerSid, _computerDomain); Assert.True(result.Collected); Assert.Equal(expected, result.Results); } - [WindowsOnlyFact] + [Fact] public async Task ComputerSessionProcessor_ReadUserSessions_MultipleMatches_AddsAll() { var mockNativeMethods = new Mock(); @@ -127,13 +127,13 @@ public async Task ComputerSessionProcessor_ReadUserSessions_MultipleMatches_Adds } }; - var processor = new ComputerSessionProcessor(new MockLDAPUtils(), "dfm", mockNativeMethods.Object); + var processor = new ComputerSessionProcessor(new MockLdapUtils(), "dfm", mockNativeMethods.Object); var result = await processor.ReadUserSessions("win10", _computerSid, _computerDomain); Assert.True(result.Collected); Assert.Equal(expected, result.Results); } - [WindowsOnlyFact] + [Fact] public async Task ComputerSessionProcessor_ReadUserSessions_NoGCMatch_TriesResolve() { var mockNativeMethods = new Mock(); @@ -152,39 +152,39 @@ public async Task ComputerSessionProcessor_ReadUserSessions_NoGCMatch_TriesResol } }; - var processor = new ComputerSessionProcessor(new MockLDAPUtils(), "dfm", mockNativeMethods.Object); + var processor = new ComputerSessionProcessor(new MockLdapUtils(), "dfm", mockNativeMethods.Object); var result = await processor.ReadUserSessions("win10", _computerSid, _computerDomain); Assert.True(result.Collected); Assert.Equal(expected, result.Results); } - [WindowsOnlyFact] + [Fact] public async Task ComputerSessionProcessor_ReadUserSessions_ComputerAccessDenied_Handled() { var mockNativeMethods = new Mock(); //mockNativeMethods.Setup(x => x.CallSamConnect(ref It.Ref.IsAny, out It.Ref.IsAny, It.IsAny(), ref It.Ref.IsAny)).Returns(NativeMethods.NtStatus.StatusAccessDenied); mockNativeMethods.Setup(x => x.NetSessionEnum(It.IsAny())) .Returns(NetAPIEnums.NetAPIStatus.ErrorAccessDenied); - var processor = new ComputerSessionProcessor(new MockLDAPUtils(), "dfm", mockNativeMethods.Object); + var processor = new ComputerSessionProcessor(new MockLdapUtils(), "dfm", mockNativeMethods.Object); var test = await processor.ReadUserSessions("test", "test", "test"); Assert.False(test.Collected); Assert.Equal(NetAPIEnums.NetAPIStatus.ErrorAccessDenied.ToString(), test.FailureReason); } - [WindowsOnlyFact] + [Fact] public async Task ComputerSessionProcessor_ReadUserSessionsPrivileged_ComputerAccessDenied_ExceptionCaught() { var mockNativeMethods = new Mock(); //mockNativeMethods.Setup(x => x.CallSamConnect(ref It.Ref.IsAny, out It.Ref.IsAny, It.IsAny(), ref It.Ref.IsAny)).Returns(NativeMethods.NtStatus.StatusAccessDenied); mockNativeMethods.Setup(x => x.NetWkstaUserEnum(It.IsAny())) .Returns(NetAPIEnums.NetAPIStatus.ErrorAccessDenied); - var processor = new ComputerSessionProcessor(new MockLDAPUtils(), "dfm", mockNativeMethods.Object); + var processor = new ComputerSessionProcessor(new MockLdapUtils(), "dfm", mockNativeMethods.Object); var test = await processor.ReadUserSessionsPrivileged("test", "test", "test"); Assert.False(test.Collected); Assert.Equal(NetAPIEnums.NetAPIStatus.ErrorAccessDenied.ToString(), test.FailureReason); } - [WindowsOnlyFact] + [Fact] public async Task ComputerSessionProcessor_ReadUserSessionsPrivileged_FilteringWorks() { var mockNativeMethods = new Mock(); @@ -220,7 +220,7 @@ public async Task ComputerSessionProcessor_ReadUserSessionsPrivileged_FilteringW } }; - var processor = new ComputerSessionProcessor(new MockLDAPUtils(), nativeMethods: mockNativeMethods.Object); + var processor = new ComputerSessionProcessor(new MockLdapUtils(), nativeMethods: mockNativeMethods.Object, currentUserName:"ADMINISTRATOR"); var test = await processor.ReadUserSessionsPrivileged("WIN10.TESTLAB.LOCAL", samAccountName, _computerSid); Assert.True(test.Collected); _testOutputHelper.WriteLine(JsonConvert.SerializeObject(test.Results)); diff --git a/test/unit/ContainerProcessorTest.cs b/test/unit/ContainerProcessorTest.cs index 35592b14..a4111bca 100644 --- a/test/unit/ContainerProcessorTest.cs +++ b/test/unit/ContainerProcessorTest.cs @@ -1,8 +1,11 @@ using System; using System.DirectoryServices.Protocols; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using CommonLibTest.Facades; using Moq; +using SharpHoundCommonLib; using SharpHoundCommonLib.Enums; using SharpHoundCommonLib.OutputTypes; using SharpHoundCommonLib.Processors; @@ -28,29 +31,29 @@ public void Dispose() } [Fact] - public void ContainerProcessor_ReadContainerGPLinks_IgnoresNull() + public async Task ContainerProcessor_ReadContainerGPLinks_IgnoresNull() { - var processor = new ContainerProcessor(new MockLDAPUtils()); - var test = processor.ReadContainerGPLinks(null); + var processor = new ContainerProcessor(new MockLdapUtils()); + var test = await processor.ReadContainerGPLinks(null).ToArrayAsync(); Assert.Empty(test); } [Fact] - public void ContainerProcessor_ReadContainerGPLinks_UnresolvedGPLink_IsIgnored() + public async Task ContainerProcessor_ReadContainerGPLinks_UnresolvedGPLink_IsIgnored() { - var processor = new ContainerProcessor(new MockLDAPUtils()); + var processor = new ContainerProcessor(new MockLdapUtils()); //GPLink that doesn't exist const string s = "[LDAP://cn={94DD0260-38B5-497E-8876-ABCDEFG},cn=policies,cn=system,DC=testlab,DC=local;0]"; - var test = processor.ReadContainerGPLinks(s); + var test = await processor.ReadContainerGPLinks(s).ToArrayAsync(); Assert.Empty(test); } [Fact] - public void ContainerProcessor_ReadContainerGPLinks_ReturnsCorrectValues() + public async Task ContainerProcessor_ReadContainerGPLinks_ReturnsCorrectValues() { - var processor = new ContainerProcessor(new MockLDAPUtils()); - var test = processor.ReadContainerGPLinks(_testGpLinkString).ToArray(); + var processor = new ContainerProcessor(new MockLdapUtils()); + var test = await processor.ReadContainerGPLinks(_testGpLinkString).ToArrayAsync(); var expected = new GPLink[] { @@ -76,34 +79,32 @@ public void ContainerProcessor_ReadContainerGPLinks_ReturnsCorrectValues() } [Fact] - public void ContainerProcessor_GetContainerChildObjects_ReturnsCorrectData() + public async Task ContainerProcessor_GetContainerChildObjects_ReturnsCorrectData() { - var mock = new Mock(); + var mock = new Mock(); - var searchResults = new MockSearchResultEntry[] + var searchResults = new[] { //These first 4 should be filtered by our DN filters - new( + LdapResult.Ok(new MockDirectoryObject( "CN=7868d4c8-ac41-4e05-b401-776280e8e9f1,CN=Operations,CN=DomainUpdates,CN=System,DC=testlab,DC=local" - , null, null, Label.Base), - new("CN=Microsoft,CN=Program Data,DC=testlab,DC=local", null, null, Label.Base), - new("CN=Operations,CN=DomainUpdates,CN=System,DC=testlab,DC=local", null, null, Label.Base), - new("CN=User,CN={C52F168C-CD05-4487-B405-564934DA8EFF},CN=Policies,CN=System,DC=testlab,DC=local", null, - null, Label.Base), + , null, null,null)), + LdapResult.Ok(new MockDirectoryObject("CN=Microsoft,CN=Program Data,DC=testlab,DC=local", null, null,null)), + LdapResult.Ok(new MockDirectoryObject("CN=Operations,CN=DomainUpdates,CN=System,DC=testlab,DC=local", null, null,null)), + LdapResult.Ok(new MockDirectoryObject("CN=User,CN={C52F168C-CD05-4487-B405-564934DA8EFF},CN=Policies,CN=System,DC=testlab,DC=local", null, + null,null)), //This is a real object in our mock - new("CN=Users,DC=testlab,DC=local", null, "ECAD920E-8EB1-4E31-A80E-DD36367F81F4", Label.Container), + LdapResult.Ok(new MockDirectoryObject("CN=Users,DC=testlab,DC=local", null, "","ECAD920E-8EB1-4E31-A80E-DD36367F81F4")), //This object does not exist in our mock - new("CN=Users,DC=testlab,DC=local", null, "ECAD920E-8EB1-4E31-A80E-DD36367F81FD", Label.Container), + LdapResult.Ok(new MockDirectoryObject("CN=Users,DC=testlab,DC=local", null, "","ECAD920E-8EB1-4E31-A80E-DD36367F81FD")), //Test null objectid - new("CN=Users,DC=testlab,DC=local", null, null, Label.Container) + LdapResult.Ok(new MockDirectoryObject("CN=Users,DC=testlab,DC=local", null, null, "")) }; - mock.Setup(x => x.QueryLDAP(It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny())).Returns(searchResults); + mock.Setup(x => x.Query(It.IsAny(), It.IsAny())).Returns(searchResults.ToAsyncEnumerable); var processor = new ContainerProcessor(mock.Object); - var test = processor.GetContainerChildObjects(_testGpLinkString).ToArray(); + var test = await processor.GetContainerChildObjects(_testGpLinkString).ToArrayAsync(); var expected = new TypedPrincipal[] { @@ -114,8 +115,9 @@ public void ContainerProcessor_GetContainerChildObjects_ReturnsCorrectData() } }; - Assert.Single(test); Assert.Equal(expected, test); + Assert.Single(test); + } [Fact] @@ -131,32 +133,35 @@ public void ContainerProcessor_ReadBlocksInheritance_ReturnsCorrectValues() } [Fact] - public void ContainerProcessor_GetContainingObject_ExpectedResult() + public async Task ContainerProcessor_GetContainingObject_ExpectedResult() { - var utils = new MockLDAPUtils(); + var utils = new MockLdapUtils(); var proc = new ContainerProcessor(utils); - var result = proc.GetContainingObject("OU=TESTOU,DC=TESTLAB,DC=LOCAL"); + var (success, result) = await proc.GetContainingObject("OU=TESTOU,DC=TESTLAB,DC=LOCAL"); Assert.Equal(Label.Domain, result.ObjectType); Assert.Equal("S-1-5-21-3130019616-2776909439-2417379446", result.ObjectIdentifier); + Assert.True(success); - result = proc.GetContainingObject("CN=PRIMARY,OU=DOMAIN CONTROLLERS,DC=TESTLAB,DC=LOCAL"); + (success, result) = await proc.GetContainingObject("CN=PRIMARY,OU=DOMAIN CONTROLLERS,DC=TESTLAB,DC=LOCAL"); Assert.Equal(Label.OU, result.ObjectType); Assert.Equal("0DE400CD-2FF3-46E0-8A26-2C917B403C65", result.ObjectIdentifier); + Assert.True(success); - result = proc.GetContainingObject("CN=ADMINISTRATORS,CN=BUILTIN,DC=TESTLAB,DC=LOCAL"); + (success, result) = await proc.GetContainingObject("CN=ADMINISTRATORS,CN=BUILTIN,DC=TESTLAB,DC=LOCAL"); Assert.Equal(Label.Domain, result.ObjectType); Assert.Equal("S-1-5-21-3130019616-2776909439-2417379446", result.ObjectIdentifier); + Assert.True(success); } [Fact] - public void ContainerProcessor_GetContainingObject_BadDN_ReturnsNull() + public async Task ContainerProcessor_GetContainingObject_BadDN_ReturnsNull() { - var utils = new MockLDAPUtils(); + var utils = new MockLdapUtils(); var proc = new ContainerProcessor(utils); - var result = proc.GetContainingObject("abc123"); - Assert.Equal(null, result); + var (success, result) = await proc.GetContainingObject("abc123"); + Assert.False(success); } } } \ No newline at end of file diff --git a/test/unit/DirectoryObjectTests.cs b/test/unit/DirectoryObjectTests.cs new file mode 100644 index 00000000..9c3f930f --- /dev/null +++ b/test/unit/DirectoryObjectTests.cs @@ -0,0 +1,320 @@ +using System; +using System.Collections.Generic; +using System.Security.Principal; +using CommonLibTest.Facades; +using SharpHoundCommonLib; +using SharpHoundCommonLib.DirectoryObjects; +using SharpHoundCommonLib.Enums; +using Xunit; + +namespace CommonLibTest { + public class DirectoryObjectTests { + [Fact] + public void Test_GetLabelIssuanceOIDObjects() { + var attribs = new Dictionary { + { LDAPProperties.ObjectClass, new[] { "msPKI-Enterprise-Oid" } }, + { LDAPProperties.Flags, "2" } + }; + + var mock = new MockDirectoryObject("CN=Test,CN=OID,CN=Public Key Services,CN=Services,CN=Configuration", + attribs, "S-1-5-21-3130019616-2776909439-2417379446-500", ""); + + var success = mock.GetLabel(out var label); + Assert.True(success); + Assert.Equal(Label.IssuancePolicy, label); + + mock = new MockDirectoryObject("CN=OID,CN=Public Key Services,CN=Services,CN=Configuration", + attribs, "S-1-5-21-3130019616-2776909439-2417379446-500", ""); + success = mock.GetLabel(out label); + Assert.True(success); + Assert.Equal(Label.Container, label); + } + + [Fact] + public void Test_HasLaps() { + var attribs = new Dictionary { + { LDAPProperties.LegacyLAPSExpirationTime, 12345 }, + }; + + var mock = new MockDirectoryObject("abc", attribs, "", ""); + Assert.True(mock.HasLAPS()); + + mock.Properties = new Dictionary { + { LDAPProperties.LAPSExpirationTime, 12345 }, + }; + + Assert.True(mock.HasLAPS()); + + mock.Properties = new Dictionary { + { LDAPProperties.Flags, 0 } + }; + + Assert.False(mock.HasLAPS()); + } + + [Fact] + public void Test_IsDeleted() { + var attribs = new Dictionary { + { LDAPProperties.IsDeleted, "true" }, + }; + + var mock = new MockDirectoryObject("abc", attribs, "", ""); + Assert.True(mock.IsDeleted()); + + mock.Properties = new Dictionary { + { LDAPProperties.IsDeleted, false }, + }; + Assert.False(mock.IsDeleted()); + + mock.Properties = new Dictionary(); + Assert.False(mock.IsDeleted()); + } + + [Fact] + public void Test_IsMSA() { + var attribs = new Dictionary { + { LDAPProperties.ObjectClass, new[] { "top", "msds-managedserviceaccount" } }, + }; + + var mock = new MockDirectoryObject("abc", attribs, "", ""); + Assert.True(mock.IsMSA()); + Assert.False(mock.IsGMSA()); + + mock.Properties = new Dictionary { + { LDAPProperties.ObjectClass, new[] { "top" } }, + }; + + Assert.False(mock.IsMSA()); + Assert.False(mock.IsGMSA()); + + mock.Properties = new Dictionary(); + Assert.False(mock.IsGMSA()); + Assert.False(mock.IsMSA()); + } + + [Fact] + public void Test_IsGMSA() { + var attribs = new Dictionary { + { LDAPProperties.ObjectClass, new[] { "top", "msds-groupmanagedserviceaccount" } }, + }; + + var mock = new MockDirectoryObject("abc", attribs, "", ""); + Assert.True(mock.IsGMSA()); + Assert.False(mock.IsMSA()); + + mock.Properties = new Dictionary { + { LDAPProperties.ObjectClass, new[] { "top" } }, + }; + + Assert.False(mock.IsGMSA()); + Assert.False(mock.IsMSA()); + + mock.Properties = new Dictionary(); + Assert.False(mock.IsGMSA()); + Assert.False(mock.IsMSA()); + } + + [Fact] + public void Test_GetLabel_BadObjectID() { + var attribs = new Dictionary { + { LDAPProperties.ObjectClass, new[] { "top", "msds-groupmanagedserviceaccount" } }, + }; + + var mock = new MockDirectoryObject("abc", attribs, "", ""); + Assert.False(mock.GetLabel(out var label)); + Assert.Equal(Label.Base, label); + } + + [Fact] + public void Test_GetLabel_WellKnownAdministratorsObject() { + var attribs = new Dictionary() { + { LDAPProperties.ObjectClass, new[] { "top" } }, + { LDAPProperties.Flags, "2" }, + { LDAPProperties.SAMAccountType, "805306368" } + }; + + var mock = new MockDirectoryObject("CN=Administrators,CN=BuiltIn,DC=Testlab,DC=Local", attribs, + "S-1-5-32-544", new Guid().ToString()); + + Assert.True(mock.GetLabel(out var label)); + Assert.Equal(Label.Group, label); + } + + [Fact] + public void Test_GetLabel_Computer_Objects() { + var attribs = new Dictionary { + { LDAPProperties.ObjectClass, new[] { "top", "msds-groupmanagedserviceaccount" } }, + { LDAPProperties.Flags, "2" }, + { LDAPProperties.SAMAccountType, "805306369" } + }; + + var mock = new MockDirectoryObject("abc", attribs, + "123456", new Guid().ToString()); + + Assert.True(mock.GetLabel(out var label)); + Assert.Equal(Label.User, label); + + mock.Properties = new Dictionary { + { LDAPProperties.ObjectClass, new[] { "top", "msds-managedserviceaccount" } }, + { LDAPProperties.Flags, "2" }, + { LDAPProperties.SAMAccountType, "805306369" } + }; + + Assert.True(mock.GetLabel(out label)); + Assert.Equal(Label.User, label); + + mock.Properties = new Dictionary { + { LDAPProperties.ObjectClass, new[] { "top", "computer" } }, + { LDAPProperties.Flags, "2" }, + { LDAPProperties.SAMAccountType, "805306369" } + }; + + Assert.True(mock.GetLabel(out label)); + Assert.Equal(Label.Computer, label); + } + + [Fact] + public void Test_GetLabel_UserObjects() { + var attribs = new Dictionary { + { LDAPProperties.ObjectClass, new[] { "top", "person" } }, + { LDAPProperties.SAMAccountType, "805306368" } + }; + + var mock = new MockDirectoryObject("abc", attribs, + "123456", new Guid().ToString()); + Assert.True(mock.GetLabel(out var label)); + Assert.Equal(Label.User, label); + } + + [Fact] + public void Test_GetLabel_GPOObjects() { + var attribs = new Dictionary { + { LDAPProperties.ObjectClass, new[] { "top", ObjectClass.GroupPolicyContainerClass } }, + }; + + var mock = new MockDirectoryObject("abc", attribs, + "123456", new Guid().ToString()); + Assert.True(mock.GetLabel(out var label)); + Assert.Equal(Label.GPO, label); + } + + [Fact] + public void Test_GetLabel_GroupObjects() { + var attribs = new Dictionary { + { LDAPProperties.ObjectClass, new[] { "top" } }, + { LDAPProperties.SAMAccountType, "268435456" } + }; + + var mock = new MockDirectoryObject("abc", attribs, + "123456", new Guid().ToString()); + Assert.True(mock.GetLabel(out var label)); + Assert.Equal(Label.Group, label); + + mock.Properties = new Dictionary { + { LDAPProperties.ObjectClass, new[] { "top" } }, + { LDAPProperties.SAMAccountType, "268435457" } + }; + + Assert.True(mock.GetLabel(out label)); + Assert.Equal(Label.Group, label); + } + + [Fact] + public void Test_GetLabel_DomainObjects() { + var attribs = new Dictionary { + { LDAPProperties.ObjectClass, new[] { "top", ObjectClass.DomainClass } }, + }; + + var mock = new MockDirectoryObject("abc", attribs, + "123456", new Guid().ToString()); + Assert.True(mock.GetLabel(out var label)); + Assert.Equal(Label.Domain, label); + } + + [Fact] + public void Test_GetLabel_ContainerObjects() { + var attribs = new Dictionary { + { LDAPProperties.ObjectClass, new[] { "top", ObjectClass.ContainerClass } }, + }; + + var mock = new MockDirectoryObject("abc", attribs, + "123456", new Guid().ToString()); + Assert.True(mock.GetLabel(out var label)); + Assert.Equal(Label.Container, label); + } + + [Fact] + public void Test_GetLabel_ConfigurationObjects() { + var attribs = new Dictionary { + { LDAPProperties.ObjectClass, new[] { "top", ObjectClass.ConfigurationClass } }, + }; + + var mock = new MockDirectoryObject("abc", attribs, + "123456", new Guid().ToString()); + Assert.True(mock.GetLabel(out var label)); + Assert.Equal(Label.Configuration, label); + } + + [Fact] + public void Test_GetLabel_CertTemplateObjects() { + var attribs = new Dictionary { + { LDAPProperties.ObjectClass, new[] { "top", ObjectClass.PKICertificateTemplateClass } }, + }; + + var mock = new MockDirectoryObject("abc", attribs, + "123456", new Guid().ToString()); + Assert.True(mock.GetLabel(out var label)); + Assert.Equal(Label.CertTemplate, label); + } + + [Fact] + public void Test_GetLabel_EnterpriseCAObjects() { + var attribs = new Dictionary { + { LDAPProperties.ObjectClass, new[] { "top", ObjectClass.PKIEnrollmentServiceClass } }, + }; + + var mock = new MockDirectoryObject("abc", attribs, + "123456", new Guid().ToString()); + Assert.True(mock.GetLabel(out var label)); + Assert.Equal(Label.EnterpriseCA, label); + } + + [Fact] + public void Test_GetLabel_CertificationAuthorityObjects() { + var attribs = new Dictionary { + { LDAPProperties.ObjectClass, new[] { "top", ObjectClass.CertificationAuthorityClass } }, + }; + + var mock = new MockDirectoryObject($"CN=Test,{DirectoryPaths.RootCALocation.ToUpper()},DC=Testlab,DC=local", + attribs, + "123456", new Guid().ToString()); + Assert.True(mock.GetLabel(out var label)); + Assert.Equal(Label.RootCA, label); + + mock.DistinguishedName = $"CN=Test,{DirectoryPaths.AIACALocation.ToUpper()},DC=Testlab,DC=local"; + Assert.True(mock.GetLabel(out label)); + Assert.Equal(Label.AIACA, label); + + mock.DistinguishedName = $"CN=Test,{DirectoryPaths.NTAuthStoreLocation.ToUpper()},DC=Testlab,DC=local"; + Assert.True(mock.GetLabel(out label)); + Assert.Equal(Label.NTAuthStore, label); + } + + [Fact] + public void Test_GetLabel_NoLabel() { + var attribs = new Dictionary { + { LDAPProperties.ObjectClass, new[] { "top" } }, + }; + + var mock = new MockDirectoryObject($"CN=Test,{DirectoryPaths.RootCALocation.ToUpper()},DC=Testlab,DC=local", + attribs, + "123456", new Guid().ToString()); + Assert.False(mock.GetLabel(out var label)); + Assert.Equal(Label.Base, label); + + mock.Properties = new Dictionary(); + Assert.False(mock.GetLabel(out label)); + Assert.Equal(Label.Base, label); + } + } +} \ No newline at end of file diff --git a/test/unit/DomainTrustProcessorTest.cs b/test/unit/DomainTrustProcessorTest.cs index ad0dc5b2..c31326d0 100644 --- a/test/unit/DomainTrustProcessorTest.cs +++ b/test/unit/DomainTrustProcessorTest.cs @@ -2,8 +2,11 @@ using System.Collections.Generic; using System.DirectoryServices.Protocols; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using CommonLibTest.Facades; using Moq; +using SharpHoundCommonLib; using SharpHoundCommonLib.Enums; using SharpHoundCommonLib.Processors; using Xunit; @@ -21,27 +24,25 @@ public DomainTrustProcessorTest(ITestOutputHelper testOutputHelper) } [WindowsOnlyFact] - public void DomainTrustProcessor_EnumerateDomainTrusts_HappyPath() + public async Task DomainTrustProcessor_EnumerateDomainTrusts_HappyPath() { - var mockUtils = new Mock(); + var mockUtils = new Mock(); var searchResults = new[] { - new MockSearchResultEntry("CN\u003dexternal.local,CN\u003dSystem,DC\u003dtestlab,DC\u003dlocal", + LdapResult.Ok(new MockDirectoryObject("CN\u003dexternal.local,CN\u003dSystem,DC\u003dtestlab,DC\u003dlocal", new Dictionary { {"trustdirection", "3"}, {"trusttype", "2"}, {"trustattributes", 0x24.ToString()}, {"cn", "external.local"}, - {"securityidentifier", Helpers.B64ToBytes("AQQAAAAAAAUVAAAA7JjftxhaHTnafGWh")} - }, "", Label.Domain) + {"securityidentifier", Utils.B64ToBytes("AQQAAAAAAAUVAAAA7JjftxhaHTnafGWh")} + }, "","")) }; - mockUtils.Setup(x => x.QueryLDAP(It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny())).Returns(searchResults); + mockUtils.Setup(x => x.Query(It.IsAny(), It.IsAny())).Returns(searchResults.ToAsyncEnumerable); var processor = new DomainTrustProcessor(mockUtils.Object); - var test = processor.EnumerateDomainTrusts("testlab.local").ToArray(); + var test = await processor.EnumerateDomainTrusts("testlab.local").ToArrayAsync(); Assert.Single(test); var trust = test.First(); Assert.Equal(TrustDirection.Bidirectional, trust.TrustDirection); @@ -53,12 +54,12 @@ public void DomainTrustProcessor_EnumerateDomainTrusts_HappyPath() } [Fact] - public void DomainTrustProcessor_EnumerateDomainTrusts_SadPaths() + public async Task DomainTrustProcessor_EnumerateDomainTrusts_SadPaths() { - var mockUtils = new Mock(); + var mockUtils = new Mock(); var searchResults = new[] { - new MockSearchResultEntry("CN\u003dexternal.local,CN\u003dSystem,DC\u003dtestlab,DC\u003dlocal", + LdapResult.Ok(new MockDirectoryObject("CN\u003dexternal.local,CN\u003dSystem,DC\u003dtestlab,DC\u003dlocal", new Dictionary { {"trustdirection", "3"}, @@ -66,39 +67,37 @@ public void DomainTrustProcessor_EnumerateDomainTrusts_SadPaths() {"trustattributes", 0x24.ToString()}, {"cn", "external.local"}, {"securityIdentifier", Array.Empty()} - }, "", Label.Domain), - new MockSearchResultEntry("CN\u003dexternal.local,CN\u003dSystem,DC\u003dtestlab,DC\u003dlocal", + }, "","")), + LdapResult.Ok(new MockDirectoryObject("CN\u003dexternal.local,CN\u003dSystem,DC\u003dtestlab,DC\u003dlocal", new Dictionary { {"trustdirection", "3"}, {"trusttype", "2"}, {"trustattributes", 0x24.ToString()}, {"cn", "external.local"}, - {"securityIdentifier", Helpers.B64ToBytes("QQQAAAAAAAUVAAAA7JjftxhaHTnafGWh")} - }, "", Label.Domain), - new MockSearchResultEntry("CN\u003dexternal.local,CN\u003dSystem,DC\u003dtestlab,DC\u003dlocal", + {"securityIdentifier", Utils.B64ToBytes("QQQAAAAAAAUVAAAA7JjftxhaHTnafGWh")} + }, "","")), + LdapResult.Ok(new MockDirectoryObject("CN\u003dexternal.local,CN\u003dSystem,DC\u003dtestlab,DC\u003dlocal", new Dictionary { {"trusttype", "2"}, {"trustattributes", 0x24.ToString()}, {"cn", "external.local"}, - {"securityIdentifier", Helpers.B64ToBytes("AQQAAAAAAAUVAAAA7JjftxhaHTnafGWh")} - }, "", Label.Domain), - new MockSearchResultEntry("CN\u003dexternal.local,CN\u003dSystem,DC\u003dtestlab,DC\u003dlocal", + {"securityIdentifier", Utils.B64ToBytes("AQQAAAAAAAUVAAAA7JjftxhaHTnafGWh")} + }, "","")), + LdapResult.Ok(new MockDirectoryObject("CN\u003dexternal.local,CN\u003dSystem,DC\u003dtestlab,DC\u003dlocal", new Dictionary { {"trustdirection", "3"}, {"trusttype", "2"}, {"cn", "external.local"}, - {"securityIdentifier", Helpers.B64ToBytes("AQQAAAAAAAUVAAAA7JjftxhaHTnafGWh")} - }, "", Label.Domain) + {"securityIdentifier", Utils.B64ToBytes("AQQAAAAAAAUVAAAA7JjftxhaHTnafGWh")} + }, "","")) }; - mockUtils.Setup(x => x.QueryLDAP(It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny())).Returns(searchResults); + mockUtils.Setup(x => x.Query(It.IsAny(), It.IsAny())).Returns(searchResults.ToAsyncEnumerable); var processor = new DomainTrustProcessor(mockUtils.Object); - var test = processor.EnumerateDomainTrusts("testlab.local"); + var test = await processor.EnumerateDomainTrusts("testlab.local").ToArrayAsync(); Assert.Empty(test); } diff --git a/test/unit/Facades/LSAMocks/DCMocks/MockDCLSAPolicy.cs b/test/unit/Facades/LSAMocks/DCMocks/MockDCLSAPolicy.cs index c6f3c3c5..bdc2fb8f 100644 --- a/test/unit/Facades/LSAMocks/DCMocks/MockDCLSAPolicy.cs +++ b/test/unit/Facades/LSAMocks/DCMocks/MockDCLSAPolicy.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Security.Principal; using SharpHoundRPC; using SharpHoundRPC.Shared; @@ -7,6 +8,7 @@ namespace CommonLibTest.Facades.LSAMocks.DCMocks { + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] public class MockDCLSAPolicy : ILSAPolicy { public Result<(string Name, string Sid)> GetLocalDomainInformation() diff --git a/test/unit/Facades/LSAMocks/WorkstationMocks/MockWorkstationLSAPolicy.cs b/test/unit/Facades/LSAMocks/WorkstationMocks/MockWorkstationLSAPolicy.cs index 95424ea2..924d7575 100644 --- a/test/unit/Facades/LSAMocks/WorkstationMocks/MockWorkstationLSAPolicy.cs +++ b/test/unit/Facades/LSAMocks/WorkstationMocks/MockWorkstationLSAPolicy.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Security.Principal; using SharpHoundRPC; using SharpHoundRPC.Shared; @@ -7,6 +8,7 @@ namespace CommonLibTest.Facades.LSAMocks.WorkstationMocks { + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] public class MockWorkstationLSAPolicy : ILSAPolicy { public Result<(string Name, string Sid)> GetLocalDomainInformation() diff --git a/test/unit/Facades/MockDirectoryObject.cs b/test/unit/Facades/MockDirectoryObject.cs new file mode 100644 index 00000000..db8d69fc --- /dev/null +++ b/test/unit/Facades/MockDirectoryObject.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using SharpHoundCommonLib; +using SharpHoundCommonLib.Enums; + +namespace CommonLibTest.Facades; + +public class MockDirectoryObject : IDirectoryObject { + private readonly string _objectSID; + private readonly string _objectGuid; + public IDictionary Properties { get; set; } + public string DistinguishedName { get; set; } + + public MockDirectoryObject(string distinguishedName, IDictionary properties, string sid = "", string guid = "") { + DistinguishedName = distinguishedName; + Properties = properties; + _objectSID = sid; + _objectGuid = guid; + } + + public bool TryGetDistinguishedName(out string value) { + value = DistinguishedName; + return !string.IsNullOrWhiteSpace(DistinguishedName); + } + + public bool TryGetProperty(string propertyName, out string value) { + if (!Properties.Contains(propertyName)) { + value = default; + return false; + } + + var temp = Properties[propertyName]; + + switch (temp) { + case string s: + value = s; + return true; + case int i: + value = i.ToString(); + return true; + default: + value = default; + return false; + } + } + + public bool TryGetByteProperty(string propertyName, out byte[] value) { + if (!Properties.Contains(propertyName)) { + value = default; + return false; + } + + switch (Properties[propertyName]) { + case string prop: + value = Encoding.ASCII.GetBytes(prop); + return true; + case byte[] b: + value = b; + return true; + default: + value = default; + return false; + } + } + + public bool TryGetArrayProperty(string propertyName, out string[] value) { + if (!Properties.Contains(propertyName)) { + value = Array.Empty(); + return false; + } + + var temp = Properties[propertyName]; + if (temp.IsArray()) { + value = temp as string[]; + return true; + } + + value = Array.Empty(); + return false; + } + + public bool TryGetByteArrayProperty(string propertyName, out byte[][] value) { + if (!Properties.Contains(propertyName)) { + value = Array.Empty(); + return false; + } + + if (Properties[propertyName] is byte[][] b) { + value = b; + return true; + } + + value = default; + return false; + } + + public bool TryGetIntProperty(string propertyName, out int value) { + if (!Properties.Contains(propertyName)) { + value = default; + return false; + } + + switch (Properties[propertyName]) { + case int i: + value = i; + return true; + case string s when int.TryParse(s, out var val): + value = val; + return true; + default: + value = 0; + return false; + } + } + + public bool TryGetCertificateArrayProperty(string propertyName, out X509Certificate2[] value) { + if (!TryGetByteArrayProperty(propertyName, out var b)) { + value = Array.Empty(); + return false; + } + + value = b.Select(x => new X509Certificate2(x)).ToArray(); + return true; + } + + public bool TryGetSecurityIdentifier(out string securityIdentifier) { + securityIdentifier = _objectSID; + return true; + } + + public bool TryGetGuid(out string guid) { + guid = _objectGuid; + return true; + } + + public string GetProperty(string propertyName) { + return Properties[propertyName] as string; + } + + public byte[] GetByteProperty(string propertyName) { + return Properties[propertyName] as byte[]; + } + + public int PropertyCount(string propertyName) { + if (!Properties.Contains(propertyName)) { + return 0; + } + + var property = Properties[propertyName]; + if (property.IsArray()) + { + var cast = property as string[]; + return cast?.Length ?? 0; + } + + return 1; + } + + public IEnumerable PropertyNames() { + foreach (var property in Properties.Keys) yield return property.ToString().ToLower(); + } +} \ No newline at end of file diff --git a/test/unit/Facades/MockLDAPUtils.cs b/test/unit/Facades/MockLdapUtils.cs similarity index 94% rename from test/unit/Facades/MockLDAPUtils.cs rename to test/unit/Facades/MockLdapUtils.cs index f03fea3f..6b854a57 100644 --- a/test/unit/Facades/MockLDAPUtils.cs +++ b/test/unit/Facades/MockLdapUtils.cs @@ -3,58 +3,54 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.DirectoryServices.ActiveDirectory; -using System.DirectoryServices.Protocols; using System.Linq; using System.Security.Principal; using System.Threading; using System.Threading.Tasks; +using Castle.Core.Internal; using Moq; using SharpHoundCommonLib; using SharpHoundCommonLib.Enums; using SharpHoundCommonLib.OutputTypes; -using SharpHoundRPC.Wrappers; using Domain = System.DirectoryServices.ActiveDirectory.Domain; +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously namespace CommonLibTest.Facades { - public class MockLDAPUtils : ILDAPUtils + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] + public class MockLdapUtils : ILdapUtils { private readonly ConcurrentDictionary _domainControllers = new(); private readonly Forest _forest; private readonly ConcurrentDictionary _seenWellKnownPrincipals = new(); - public MockLDAPUtils() + public MockLdapUtils() { _forest = MockableForest.Construct("FOREST.LOCAL"); } - public void SetLDAPConfig(LDAPConfig config) - { + public virtual IAsyncEnumerable> Query(LdapQueryParameters queryParameters, + CancellationToken cancellationToken = new CancellationToken()) { throw new NotImplementedException(); } - public bool TestLDAPConfig(string domain) - { - return true; + public virtual IAsyncEnumerable> PagedQuery(LdapQueryParameters queryParameters, + CancellationToken cancellationToken = new CancellationToken()) { + throw new NotImplementedException(); } - public string[] GetUserGlobalCatalogMatches(string name) - { - name = name.ToLower(); - return name switch - { - "dfm" => new[] { "S-1-5-21-3130019616-2776909439-2417379446-1105" }, - "administrator" => new[] - {"S-1-5-21-3130019616-2776909439-2417379446-500", "S-1-5-21-3084884204-958224920-2707782874-500"}, - "admin" => new[] { "S-1-5-21-3130019616-2776909439-2417379446-2116" }, - _ => Array.Empty() - }; + public virtual IAsyncEnumerable> RangedRetrieval(string distinguishedName, string attributeName, + CancellationToken cancellationToken = new CancellationToken()) { + throw new NotImplementedException(); } - public TypedPrincipal ResolveIDAndType(string id, string fallbackDomain) - { - id = id?.ToUpper(); - if (GetWellKnownPrincipal(id, fallbackDomain, out var principal)) return principal; + public Task<(bool Success, TypedPrincipal Principal)> ResolveIDAndType(SecurityIdentifier securityIdentifier, string objectDomain) { + return ResolveIDAndType(securityIdentifier.Value, objectDomain); + } + + public async Task<(bool Success, TypedPrincipal Principal)> ResolveIDAndType(string identifier, string objectDomain) { + var id = identifier.ToUpper(); + if (await GetWellKnownPrincipal(id, objectDomain) is (true, var principal)) return (true, principal); principal = id switch { @@ -670,132 +666,57 @@ public TypedPrincipal ResolveIDAndType(string id, string fallbackDomain) _ => null }; - return principal; + return (principal != null, principal); } - public Label LookupSidType(string sid, string domain) - { - var result = ResolveIDAndType(sid, domain); - return result.ObjectType; + public async Task<(bool Success, TypedPrincipal WellKnownPrincipal)> GetWellKnownPrincipal(string securityIdentifier, string objectDomain) { + if (!WellKnownPrincipal.GetWellKnownPrincipal(securityIdentifier, out var commonPrincipal)) return (false, default); + commonPrincipal.ObjectIdentifier = await ConvertWellKnownPrincipal(securityIdentifier, objectDomain); + _seenWellKnownPrincipals.TryAdd(commonPrincipal.ObjectIdentifier, securityIdentifier); + return (true, commonPrincipal); } - public Label LookupGuidType(string guid, string domain) - { - var result = ResolveIDAndType(guid, domain); - return result.ObjectType; - } + async Task<(bool Success, string DomainName)> ILdapUtils.GetDomainNameFromSid(string sid) { + if (sid.StartsWith("S-1-5-21-3130019616-2776909439-2417379446", StringComparison.OrdinalIgnoreCase)) { + return (true, "TESTLAB.LOCAL"); + } - public string GetDomainNameFromSid(string sid) - { - throw new NotImplementedException(); + return (false, default); } - public string GetSidFromDomainName(string domainName) - { + public async Task<(bool Success, string DomainSid)> GetDomainSidFromDomainName(string domainName) { if (domainName.Equals("TESTLAB.LOCAL", StringComparison.OrdinalIgnoreCase)) { - return "S-1-5-21-3130019616-2776909439-2417379446"; + return (true, "S-1-5-21-3130019616-2776909439-2417379446"); } - return null; - } - - public string ConvertWellKnownPrincipal(string sid, string domain) - { - if (!WellKnownPrincipal.GetWellKnownPrincipal(sid, out _)) return sid; - - if (sid != "S-1-5-9") return $"{domain}-{sid}".ToUpper(); - - var forest = GetForest(domain)?.Name; - return $"{forest}-{sid}".ToUpper(); + return (false, default); } - public bool GetWellKnownPrincipal(string sid, string domain, out TypedPrincipal commonPrincipal) - { - if (!WellKnownPrincipal.GetWellKnownPrincipal(sid, out commonPrincipal)) return false; - commonPrincipal.ObjectIdentifier = ConvertWellKnownPrincipal(sid, domain); - _seenWellKnownPrincipals.TryAdd(commonPrincipal.ObjectIdentifier, sid); - return true; - } - - public bool ConvertLocalWellKnownPrincipal(SecurityIdentifier sid, string computerDomainSid, string computerDomain, - out TypedPrincipal principal) - { - if (WellKnownPrincipal.GetWellKnownPrincipal(sid.Value, out var common)) - { - //The everyone and auth users principals are special and will be converted to the domain equivalent - if (sid.Value is "S-1-1-0" or "S-1-5-11") - { - GetWellKnownPrincipal(sid.Value, computerDomain, out principal); - return true; - } - - //Use the computer object id + the RID of the sid we looked up to create our new principal - principal = new TypedPrincipal - { - ObjectIdentifier = $"{computerDomainSid}-{sid.Rid()}", - ObjectType = common.ObjectType switch - { - Label.User => Label.LocalUser, - Label.Group => Label.LocalGroup, - _ => common.ObjectType - } - }; - - return true; - } - - principal = null; - return false; - } - - public void AddDomainController(string domainControllerSID) - { - _domainControllers.TryAdd(domainControllerSID, new byte()); - } - - public Domain GetDomain(string domainName = null) - { + public bool GetDomain(string domainName, out Domain domain) { throw new NotImplementedException(); } - public IEnumerable GetWellKnownPrincipalOutput(string domain) - { - foreach (var wkp in _seenWellKnownPrincipals) - { - WellKnownPrincipal.GetWellKnownPrincipal(wkp.Value, out var principal); - OutputBase output = principal.ObjectType switch - { - Label.User => new User(), - Label.Computer => new Computer(), - Label.Group => new Group(), - Label.GPO => new GPO(), - Label.Domain => new SharpHoundCommonLib.OutputTypes.Domain(), - Label.OU => new OU(), - Label.Container => new Container(), - _ => throw new ArgumentOutOfRangeException() - }; - - output.Properties.Add("name", principal.ObjectIdentifier); - output.ObjectIdentifier = wkp.Key; - yield return output; - } - - var entdc = GetBaseEnterpriseDC(); - entdc.Members = _domainControllers.Select(x => new TypedPrincipal(x.Key, Label.Computer)).ToArray(); - yield return entdc; + public bool GetDomain(out Domain domain) { + throw new NotImplementedException(); } - public virtual IEnumerable DoRangedRetrieval(string distinguishedName, string attributeName) - { - throw new NotImplementedException(); + public async Task<(bool Success, TypedPrincipal Principal)> ResolveAccountName(string name, string domain) { + var res = name.ToUpper() switch { + "ADMINISTRATOR" => new TypedPrincipal( + "S-1-5-21-3130019616-2776909439-2417379446-500", Label.User), + "DFM" => new TypedPrincipal( + "S-1-5-21-3130019616-2776909439-2417379446-1105", Label.User), + "TEST" => new TypedPrincipal("S-1-5-21-3130019616-2776909439-2417379446-1106", Label.User), + _ => null + }; + return (res != null, res); } -#pragma warning disable CS1998 - public async Task ResolveHostToSid(string hostname, string domain) - { - var h = SharpHoundCommonLib.Helpers.StripServicePrincipalName(hostname); - return h.ToUpper() switch + public async Task<(bool Success, string SecurityIdentifier)> ResolveHostToSid(string host, string domain) { + var h = SharpHoundCommonLib.Helpers.StripServicePrincipalName(host); + + var resolved = h.ToUpper() switch { "192.168.1.1" => "S-1-5-21-3130019616-2776909439-2417379446-1104", "PRIMARY" => "S-1-5-21-3130019616-2776909439-2417379446-1001", @@ -804,27 +725,40 @@ public async Task ResolveHostToSid(string hostname, string domain) "WIN10.TESTLAB.LOCAL" => "S-1-5-21-3130019616-2776909439-2417379446-1104", _ => null }; + + return (resolved != null, resolved); } -#pragma warning restore CS1998 -#pragma warning disable CS1998 - public TypedPrincipal ResolveAccountName(string name, string domain) - { - return name.ToUpper() switch + public async Task<(bool Success, string[] Sids)> GetGlobalCatalogMatches(string name, string domain) { + name = name.ToLower(); + var results = name switch { - "ADMINISTRATOR" => new TypedPrincipal( - "S-1-5-21-3130019616-2776909439-2417379446-500", Label.User), - "DFM" => new TypedPrincipal( - "S-1-5-21-3130019616-2776909439-2417379446-1105", Label.User), - "TEST" => new TypedPrincipal("S-1-5-21-3130019616-2776909439-2417379446-1106", Label.User), - _ => null + "dfm" => new[] { "S-1-5-21-3130019616-2776909439-2417379446-1105" }, + "administrator" => new[] + {"S-1-5-21-3130019616-2776909439-2417379446-500", "S-1-5-21-3084884204-958224920-2707782874-500"}, + "admin" => new[] { "S-1-5-21-3130019616-2776909439-2417379446-2116" }, + _ => Array.Empty() }; + + return (!results.IsNullOrEmpty(), results); + } + + public Task<(bool Success, TypedPrincipal Principal)> ResolveCertTemplateByProperty(string propValue, string propName, string domainName) { + throw new NotImplementedException(); } -#pragma warning restore CS1998 - public TypedPrincipal ResolveDistinguishedName(string dn) + public async Task ConvertWellKnownPrincipal(string sid, string domain) { - return dn.ToUpper() switch + if (!WellKnownPrincipal.GetWellKnownPrincipal(sid, out _)) return sid; + + if (sid != "S-1-5-9") return $"{domain}-{sid}".ToUpper(); + + var (success, forest) = await GetForest(domain); + return $"{forest}-{sid}".ToUpper(); + } + + public async Task<(bool Success, TypedPrincipal Principal)> ResolveDistinguishedName(string distinguishedName) { + var result = distinguishedName.ToUpper() switch { "CN=REPLICATOR,CN=BUILTIN,DC=TESTLAB,DC=LOCAL" => new TypedPrincipal("TESTLAB.LOCAL-S-1-5-32-552", Label.Group), @@ -1057,32 +991,70 @@ public TypedPrincipal ResolveDistinguishedName(string dn) "CN=ENTERPRISE KEY ADMINS,CN=USERS,DC=ESC10,DC=LOCAL" => new TypedPrincipal("S-1-5-21-3662707843-2053279151-3839588741-527", Label.Group), _ => null, }; + + return (result != null, result); } - public virtual IEnumerable QueryLDAP(LDAPQueryOptions options) + public void AddDomainController(string domainControllerSID) { + _domainControllers.TryAdd(domainControllerSID, new byte()); + } + + public IAsyncEnumerable GetWellKnownPrincipalOutput() { + throw new NotImplementedException(); + } + + public void SetLdapConfig(LdapConfig config) { + throw new NotImplementedException(); + } + + public Task<(bool Success, string Message)> TestLdapConnection(string domain) { throw new NotImplementedException(); } - public virtual IEnumerable QueryLDAP(string ldapFilter, SearchScope scope, string[] props, - CancellationToken cancellationToken, - string domainName = null, bool includeAcl = false, bool showDeleted = false, string adsPath = null, - bool globalCatalog = false, bool skipCache = false, bool throwException = false) + public Task<(bool Success, string Path)> GetNamingContextPath(string domain, NamingContext context) { + throw new NotImplementedException(); + } + + public Domain GetDomain(string domainName = null) { throw new NotImplementedException(); } - public virtual IEnumerable QueryLDAP(string ldapFilter, SearchScope scope, string[] props, - string domainName = null, - bool includeAcl = false, bool showDeleted = false, string adsPath = null, bool globalCatalog = false, - bool skipCache = false, bool throwException = false) + public IEnumerable GetWellKnownPrincipalOutput(string domain) { + foreach (var wkp in _seenWellKnownPrincipals) + { + WellKnownPrincipal.GetWellKnownPrincipal(wkp.Value, out var principal); + OutputBase output = principal.ObjectType switch + { + Label.User => new User(), + Label.Computer => new Computer(), + Label.Group => new Group(), + Label.GPO => new GPO(), + Label.Domain => new SharpHoundCommonLib.OutputTypes.Domain(), + Label.OU => new OU(), + Label.Container => new Container(), + _ => throw new ArgumentOutOfRangeException() + }; + + output.Properties.Add("name", principal.ObjectIdentifier); + output.ObjectIdentifier = wkp.Key; + yield return output; + } + + var entdc = GetBaseEnterpriseDC(); + entdc.Members = _domainControllers.Select(x => new TypedPrincipal(x.Key, Label.Computer)).ToArray(); + yield return entdc; + } + + Task ILdapUtils.IsDomainController(string computerObjectId, string domainName) { throw new NotImplementedException(); } - public Forest GetForest(string domainName = null) + public async Task<(bool Success, string ForestName)> GetForest(string domainName = null) { - return _forest; + return (true, _forest.Name); } public ActiveDirectorySecurityDescriptor MakeSecurityDescriptor() @@ -1091,6 +1063,33 @@ public ActiveDirectorySecurityDescriptor MakeSecurityDescriptor() return mockSecurityDescriptor.Object; } + public async Task<(bool Success, TypedPrincipal Principal)> ConvertLocalWellKnownPrincipal(SecurityIdentifier sid, string computerDomainSid, string computerDomain) { + if (WellKnownPrincipal.GetWellKnownPrincipal(sid.Value, out var common)) + { + //The everyone and auth users principals are special and will be converted to the domain equivalent + if (sid.Value is "S-1-1-0" or "S-1-5-11") + { + return await GetWellKnownPrincipal(sid.Value, computerDomain); + } + + //Use the computer object id + the RID of the sid we looked up to create our new principal + var principal = new TypedPrincipal + { + ObjectIdentifier = $"{computerDomainSid}-{sid.Rid()}", + ObjectType = common.ObjectType switch + { + Label.User => Label.LocalUser, + Label.Group => Label.LocalGroup, + _ => common.ObjectType + } + }; + + return (true, principal); + } + + return (false, default); + } + public string BuildLdapPath(string dnPath, string domain) { throw new NotImplementedException(); @@ -1102,12 +1101,7 @@ private Group GetBaseEnterpriseDC() g.Properties.Add("name", "ENTERPRISE DOMAIN CONTROLLERS@TESTLAB.LOCAL".ToUpper()); return g; } - - public TypedPrincipal ResolveCertTemplateByCN(string cn, string containerDN, string domainName) - { - throw new NotImplementedException(); - } - + public string GetConfigurationPath(string domainName) { throw new NotImplementedException(); @@ -1118,14 +1112,13 @@ public string GetSchemaPath(string domainName) throw new NotImplementedException(); } - TypedPrincipal ILDAPUtils.ResolveCertTemplateByProperty(string propValue, string propName, string containerDN, string domainName) + public bool IsDomainController(string computerObjectId, string domainName) { throw new NotImplementedException(); } - public bool IsDomainController(string computerObjectId, string domainName) - { - throw new NotImplementedException(); + public void Dispose() { + } } } \ No newline at end of file diff --git a/test/unit/Facades/MockSearchResultEntry.cs b/test/unit/Facades/MockSearchResultEntry.cs deleted file mode 100644 index 63f7cc97..00000000 --- a/test/unit/Facades/MockSearchResultEntry.cs +++ /dev/null @@ -1,141 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Security.Cryptography.X509Certificates; -using System.Text; -using SharpHoundCommonLib; -using SharpHoundCommonLib.Enums; - -namespace CommonLibTest.Facades -{ - public class MockSearchResultEntry : ISearchResultEntry - { - private readonly string _objectId; - private readonly Label _objectType; - private readonly IDictionary _properties; - - public MockSearchResultEntry(string distinguishedName, IDictionary properties, string objectId, - Label objectType) - { - DistinguishedName = distinguishedName; - _properties = properties; - _objectId = objectId; - _objectType = objectType; - } - - public string DistinguishedName { get; } - - public ResolvedSearchResult ResolveBloodHoundInfo() - { - throw new NotImplementedException(); - } - - public string GetProperty(string propertyName) - { - return _properties[propertyName] as string; - } - - public byte[] GetByteProperty(string propertyName) - { - if (!_properties.Contains(propertyName)) - return null; - - if (_properties[propertyName] is string prop) - { - return Encoding.ASCII.GetBytes(prop); - } - - return _properties[propertyName] as byte[]; - } - - public string[] GetArrayProperty(string propertyName) - { - if (!_properties.Contains(propertyName)) - return Array.Empty(); - - var value = _properties[propertyName]; - if (value.IsArray()) - return value as string[]; - - return new [] { (value ?? "").ToString() }; - } - - public byte[][] GetByteArrayProperty(string propertyName) - { - if (!_properties.Contains(propertyName)) - return Array.Empty(); - - var property = _properties[propertyName] as byte[][]; - return property; - } - - public bool GetIntProperty(string propertyName, out int value) - { - value = _properties[propertyName] is int ? (int)_properties[propertyName] : 0; - return true; - } - - public X509Certificate2[] GetCertificateArrayProperty(string propertyName) - { - return GetByteArrayProperty(propertyName).Select(x => new X509Certificate2(x)).ToArray(); - } - - public string GetObjectIdentifier() - { - return _objectId; - } - - public bool IsDeleted() - { - throw new NotImplementedException(); - } - - public Label GetLabel() - { - return _objectType; - } - - public string GetSid() - { - return _objectId; - } - - public string GetGuid() - { - return _objectId; - } - - public int PropCount(string prop) - { - var property = _properties[prop]; - if (property.IsArray()) - { - var cast = property as string[]; - return cast?.Length ?? 0; - } - - return 1; - } - - public IEnumerable PropertyNames() - { - foreach (var property in _properties.Keys) yield return property.ToString().ToLower(); - } - - public bool IsMSA() - { - throw new NotImplementedException(); - } - - public bool IsGMSA() - { - throw new NotImplementedException(); - } - - public bool HasLAPS() - { - throw new NotImplementedException(); - } - } -} \ No newline at end of file diff --git a/test/unit/Facades/MockableDomain.cs b/test/unit/Facades/MockableDomain.cs index 059918d1..c28bd093 100644 --- a/test/unit/Facades/MockableDomain.cs +++ b/test/unit/Facades/MockableDomain.cs @@ -1,7 +1,9 @@ -using System.DirectoryServices.ActiveDirectory; +using System.Diagnostics.CodeAnalysis; +using System.DirectoryServices.ActiveDirectory; namespace CommonLibTest.Facades { + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] public class MockableDomain { public static Domain Construct(string domainName) diff --git a/test/unit/Facades/MockableForest.cs b/test/unit/Facades/MockableForest.cs index 9893e633..4c421ef2 100644 --- a/test/unit/Facades/MockableForest.cs +++ b/test/unit/Facades/MockableForest.cs @@ -1,7 +1,9 @@ -using System.DirectoryServices.ActiveDirectory; +using System.Diagnostics.CodeAnalysis; +using System.DirectoryServices.ActiveDirectory; namespace CommonLibTest.Facades { + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] public class MockableForest { public static Forest Construct(string forestDnsName) diff --git a/test/unit/Facades/SAMMocks/DCMocks/MockDCAliasAdministrators.cs b/test/unit/Facades/SAMMocks/DCMocks/MockDCAliasAdministrators.cs index 15a18ddc..80f3db1a 100644 --- a/test/unit/Facades/SAMMocks/DCMocks/MockDCAliasAdministrators.cs +++ b/test/unit/Facades/SAMMocks/DCMocks/MockDCAliasAdministrators.cs @@ -1,10 +1,12 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Security.Principal; using SharpHoundRPC; using SharpHoundRPC.Wrappers; namespace CommonLibTest.Facades { + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] public class MockDCAliasAdministrators : ISAMAlias { public Result> GetMembers() diff --git a/test/unit/Facades/SAMMocks/DCMocks/MockDCAliasUsers.cs b/test/unit/Facades/SAMMocks/DCMocks/MockDCAliasUsers.cs index b15c0e59..5b0cb58c 100644 --- a/test/unit/Facades/SAMMocks/DCMocks/MockDCAliasUsers.cs +++ b/test/unit/Facades/SAMMocks/DCMocks/MockDCAliasUsers.cs @@ -1,10 +1,12 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Security.Principal; using SharpHoundRPC; using SharpHoundRPC.Wrappers; namespace CommonLibTest.Facades { + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] public class MockDCAliasUsers : ISAMAlias { public Result> GetMembers() diff --git a/test/unit/Facades/SAMMocks/DCMocks/MockDCSAMServer.cs b/test/unit/Facades/SAMMocks/DCMocks/MockDCSAMServer.cs index 5e090c19..bf0e5352 100644 --- a/test/unit/Facades/SAMMocks/DCMocks/MockDCSAMServer.cs +++ b/test/unit/Facades/SAMMocks/DCMocks/MockDCSAMServer.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Security.Principal; using SharpHoundRPC; using SharpHoundRPC.SAMRPCNative; @@ -8,6 +9,7 @@ namespace CommonLibTest.Facades { + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] public class MockDCSAMServer : ISAMServer { public bool IsNull { get; } diff --git a/test/unit/Facades/SAMMocks/WorkstationMocks/MockWorkstationAliasAdministrators.cs b/test/unit/Facades/SAMMocks/WorkstationMocks/MockWorkstationAliasAdministrators.cs index 2c4d50a5..d9e1dcea 100644 --- a/test/unit/Facades/SAMMocks/WorkstationMocks/MockWorkstationAliasAdministrators.cs +++ b/test/unit/Facades/SAMMocks/WorkstationMocks/MockWorkstationAliasAdministrators.cs @@ -1,10 +1,12 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Security.Principal; using SharpHoundRPC; using SharpHoundRPC.Wrappers; namespace CommonLibTest.Facades { + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] public class MockWorkstationAliasAdministrators : ISAMAlias { public Result> GetMembers() diff --git a/test/unit/Facades/SAMMocks/WorkstationMocks/MockWorkstationAliasRDP.cs b/test/unit/Facades/SAMMocks/WorkstationMocks/MockWorkstationAliasRDP.cs index 11fe80ae..e6647307 100644 --- a/test/unit/Facades/SAMMocks/WorkstationMocks/MockWorkstationAliasRDP.cs +++ b/test/unit/Facades/SAMMocks/WorkstationMocks/MockWorkstationAliasRDP.cs @@ -1,10 +1,12 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Security.Principal; using SharpHoundRPC; using SharpHoundRPC.Wrappers; namespace CommonLibTest.Facades { + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] public class MockWorkstationAliasRDP : ISAMAlias { public Result> GetMembers() diff --git a/test/unit/Facades/SAMMocks/WorkstationMocks/MockWorkstationAliasTestGroup.cs b/test/unit/Facades/SAMMocks/WorkstationMocks/MockWorkstationAliasTestGroup.cs index 8eaeb8fc..09164c6b 100644 --- a/test/unit/Facades/SAMMocks/WorkstationMocks/MockWorkstationAliasTestGroup.cs +++ b/test/unit/Facades/SAMMocks/WorkstationMocks/MockWorkstationAliasTestGroup.cs @@ -1,10 +1,12 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Security.Principal; using SharpHoundRPC; using SharpHoundRPC.Wrappers; namespace CommonLibTest.Facades { + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] public class MockWorkstationAliasTestGroup : ISAMAlias { public Result> GetMembers() diff --git a/test/unit/Facades/SAMMocks/WorkstationMocks/MockWorkstationSAMServer.cs b/test/unit/Facades/SAMMocks/WorkstationMocks/MockWorkstationSAMServer.cs index 8f076b19..f89827bc 100644 --- a/test/unit/Facades/SAMMocks/WorkstationMocks/MockWorkstationSAMServer.cs +++ b/test/unit/Facades/SAMMocks/WorkstationMocks/MockWorkstationSAMServer.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Security.Principal; using SharpHoundRPC; using SharpHoundRPC.SAMRPCNative; @@ -8,6 +9,7 @@ namespace CommonLibTest.Facades { + [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] public class MockWorkstationSAMServer : ISAMServer { public bool IsNull { get; } @@ -23,7 +25,7 @@ public class MockWorkstationSAMServer : ISAMServer public Result LookupDomain(string name) { - throw new System.NotImplementedException(); + throw new NotImplementedException(); } public Result GetMachineSid(string testName = null) diff --git a/test/unit/GPOLocalGroupProcessorTest.cs b/test/unit/GPOLocalGroupProcessorTest.cs index 5a0393ee..84643074 100644 --- a/test/unit/GPOLocalGroupProcessorTest.cs +++ b/test/unit/GPOLocalGroupProcessorTest.cs @@ -1,9 +1,11 @@ +using System; using System.Collections.Generic; using System.DirectoryServices.Protocols; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using CommonLibTest.Facades; using Moq; using SharpHoundCommonLib; using SharpHoundCommonLib.Enums; @@ -13,10 +15,8 @@ using Xunit; using Xunit.Abstractions; -namespace CommonLibTest -{ - public class GPOLocalGroupProcessorTest - { +namespace CommonLibTest { + public class GPOLocalGroupProcessorTest { private readonly string GpttmplInfContent = @"[Unicode] Unicode=yes [Version] @@ -88,15 +88,13 @@ [Group Membership] private ITestOutputHelper _testOutputHelper; - public GPOLocalGroupProcessorTest(ITestOutputHelper testOutputHelper) - { + public GPOLocalGroupProcessorTest(ITestOutputHelper testOutputHelper) { _testOutputHelper = testOutputHelper; } - [Fact(Skip = "")] - public async Task GPOLocalGroupProcessor_ReadGPOLocalGroups_Null_GPLink() - { - var mockLDAPUtils = new Mock(); + [Fact] + public async Task GPOLocalGroupProcessor_ReadGPOLocalGroups_Null_GPLink() { + var mockLDAPUtils = new Mock(); var processor = new GPOLocalGroupProcessor(mockLDAPUtils.Object); var result = await processor.ReadGPOLocalGroups(null, null); @@ -108,23 +106,11 @@ public async Task GPOLocalGroupProcessor_ReadGPOLocalGroups_Null_GPLink() Assert.Empty(result.PSRemoteUsers); } - [Fact(Skip = "")] - public async Task GPOLocalGroupProcessor_ReadGPOLocalGroups_AffectedComputers_0() - { - var mockLDAPUtils = new Mock(); - mockLDAPUtils.Setup(x => x.QueryLDAP( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny() - )).Returns(new List()); + [Fact] + public async Task GPOLocalGroupProcessor_ReadGPOLocalGroups_AffectedComputers_0() { + var mockLDAPUtils = new Mock(); + mockLDAPUtils.Setup(x => x.Query(It.IsAny(), It.IsAny())) + .Returns(Array.Empty>().ToAsyncEnumerable); var processor = new GPOLocalGroupProcessor(mockLDAPUtils.Object); var result = await processor.ReadGPOLocalGroups("teapot", null); @@ -136,22 +122,27 @@ public async Task GPOLocalGroupProcessor_ReadGPOLocalGroups_AffectedComputers_0( Assert.Empty(result.PSRemoteUsers); } - [Fact(Skip = "")] - public async Task GPOLocalGroupProcessor_ReadGPOLocalGroups_Null_Gpcfilesyspath() - { - var mockLDAPUtils = new Mock(); - var mockSearchResultEntry = new Mock(); - mockSearchResultEntry.Setup(x => x.GetSid()).Returns("teapot"); - var mockSearchResults = new List(); - mockSearchResults.Add(mockSearchResultEntry.Object); - mockLDAPUtils.Setup(x => x.QueryLDAP(new LDAPQueryOptions - { - Filter = "(&(samaccounttype=805306369)(!(objectclass=msDS-GroupManagedServiceAccount))(!(objectclass=msDS-ManagedServiceAccount)))", - Scope = SearchScope.Subtree, - Properties = CommonProperties.ObjectSID, - AdsPath = null - })) - .Returns(mockSearchResults.ToArray()); + [Fact] + public async Task GPOLocalGroupProcessor_ReadGPOLocalGroups_Null_Gpcfilesyspath() { + var mockLDAPUtils = new Mock(); + var mockSearchResultEntry = new Mock(); + var sid = "teapot"; + mockSearchResultEntry.Setup(x => x.TryGetSecurityIdentifier(out sid)).Returns(true); + var mockResult = LdapResult.Ok(mockSearchResultEntry.Object); + var mockSearchResults = new List> { mockResult }; + mockLDAPUtils + .Setup(x => x.Query( + It.Is(y => + y.LDAPFilter.Equals(new LdapFilter().AddComputersNoMSAs().GetFilter()) && + y.Attributes.Equals(CommonProperties.ObjectSID)), + It.IsAny())).Returns(mockSearchResults.ToAsyncEnumerable); + + mockLDAPUtils + .Setup(x => x.Query( + It.Is(y => + y.LDAPFilter.Equals(new LdapFilter().AddAllObjects().GetFilter())), + It.IsAny())) + .Returns(Array.Empty>().ToAsyncEnumerable); var processor = new GPOLocalGroupProcessor(mockLDAPUtils.Object); var testGPLinkProperty = @@ -166,9 +157,8 @@ public async Task GPOLocalGroupProcessor_ReadGPOLocalGroups_Null_Gpcfilesyspath( } [Fact] - public async Task GPOLocalGroupProcessor_ReadGPOLocalGroups() - { - var mockLDAPUtils = new Mock(MockBehavior.Strict); + public async Task GPOLocalGroupProcessor_ReadGPOLocalGroups() { + var mockLDAPUtils = new Mock(MockBehavior.Loose); var gpcFileSysPath = Path.GetTempPath(); var groupsXmlPath = Path.Join(gpcFileSysPath, "MACHINE", "Preferences", "Groups", "Groups.xml"); @@ -177,19 +167,21 @@ public async Task GPOLocalGroupProcessor_ReadGPOLocalGroups() Directory.CreateDirectory(Path.GetDirectoryName(groupsXmlPath)); File.WriteAllText(groupsXmlPath, GroupXmlContent); - var mockComputerEntry = new Mock(); - mockComputerEntry.Setup(x => x.GetSid()).Returns("teapot"); - var mockComputerResults = new List(); - mockComputerResults.Add(mockComputerEntry.Object); + var mockComputerEntry = new Mock(); + var sid = "teapot"; + mockComputerEntry.Setup(x => x.TryGetSecurityIdentifier(out sid)).Returns(true); + var mockComputerResults = new List>(); + mockComputerResults.Add(LdapResult.Ok(mockComputerEntry.Object)); - var mockGCPFileSysPathEntry = new Mock(); - mockGCPFileSysPathEntry.Setup(x => x.GetProperty(It.IsAny())).Returns(gpcFileSysPath); - var mockGCPFileSysPathResults = new List(); - mockGCPFileSysPathResults.Add(mockGCPFileSysPathEntry.Object); + var mockGCPFileSysPathEntry = new Mock(); + mockGCPFileSysPathEntry.Setup(x => x.TryGetProperty(It.IsAny(), out gpcFileSysPath)).Returns(true); + var mockGCPFileSysPathResults = new List> + { LdapResult.Ok(mockGCPFileSysPathEntry.Object) }; - mockLDAPUtils.SetupSequence(x => x.QueryLDAP(It.IsAny())) - .Returns(mockComputerResults.ToArray()) - .Returns(mockGCPFileSysPathResults.ToArray()); + mockLDAPUtils.SetupSequence(x => x.Query(It.IsAny(), It.IsAny())) + .Returns(mockComputerResults.ToAsyncEnumerable) + .Returns(mockGCPFileSysPathResults.ToAsyncEnumerable) + .Returns(Array.Empty>().ToAsyncEnumerable); var processor = new GPOLocalGroupProcessor(mockLDAPUtils.Object); @@ -206,21 +198,19 @@ public async Task GPOLocalGroupProcessor_ReadGPOLocalGroups() } [Fact] - public async Task GPOLocalGroupProcess_ProcessGPOXMLFile_NoFile() - { - var mockLDAPUtils = new Mock(); + public async Task GPOLocalGroupProcess_ProcessGPOXMLFile_NoFile() { + var mockLDAPUtils = new Mock(); var processor = new GPOLocalGroupProcessor(mockLDAPUtils.Object); var gpcFileSysPath = Path.Join(Path.GetTempPath(), "made", "up", "path"); - var actual = processor.ProcessGPOXmlFile(gpcFileSysPath, "somedomain").ToList(); + var actual = await processor.ProcessGPOXmlFile(gpcFileSysPath, "somedomain").ToArrayAsync(); Assert.NotNull(actual); Assert.Empty(actual); } [Fact] - public async Task GPOLocalGroupProcess_ProcessGPOXMLFile_Disabled() - { - var mockLDAPUtils = new Mock(); + public async Task GPOLocalGroupProcess_ProcessGPOXMLFile_Disabled() { + var mockLDAPUtils = new Mock(); var gpcFileSysPath = Path.GetTempPath(); var groupsXmlPath = Path.Join(gpcFileSysPath, "MACHINE", "Preferences", "Groups", "Groups.xml"); @@ -229,17 +219,16 @@ public async Task GPOLocalGroupProcess_ProcessGPOXMLFile_Disabled() var processor = new GPOLocalGroupProcessor(mockLDAPUtils.Object); - var actual = processor.ProcessGPOXmlFile(gpcFileSysPath, "somedomain").ToList(); + var actual = await processor.ProcessGPOXmlFile(gpcFileSysPath, "somedomain").ToArrayAsync(); Assert.NotNull(actual); Assert.Empty(actual); } [Fact] - public async Task GPOLocalGroupProcessor_ProcessGPOXMLFile() - { - var mockLDAPUtils = new Mock(); + public async Task GPOLocalGroupProcessor_ProcessGPOXMLFile() { + var mockLDAPUtils = new Mock(); mockLDAPUtils.Setup(x => x.ResolveAccountName(It.IsAny(), It.IsAny())) - .Returns(new TypedPrincipal("S-1-5-21-3130019616-2776909439-2417379446-513", Label.User)); + .ReturnsAsync((true, new TypedPrincipal("S-1-5-21-3130019616-2776909439-2417379446-513", Label.User))); var gpcFileSysPath = Path.GetTempPath(); var groupsXmlPath = Path.Join(gpcFileSysPath, "MACHINE", "Preferences", "Groups", "Groups.xml"); @@ -247,16 +236,15 @@ public async Task GPOLocalGroupProcessor_ProcessGPOXMLFile() File.WriteAllText(groupsXmlPath, GroupXmlContent); var processor = new GPOLocalGroupProcessor(mockLDAPUtils.Object); - var actual = processor.ProcessGPOXmlFile(gpcFileSysPath, "somedomain").ToList(); + var actual = await processor.ProcessGPOXmlFile(gpcFileSysPath, "somedomain").ToArrayAsync(); Assert.NotNull(actual); Assert.NotEmpty(actual); } [Fact] - public async Task GPOLocalGroupProcess_ProcessGPOTemplateFile_NoFile() - { - var mockLDAPUtils = new Mock(); + public async Task GPOLocalGroupProcess_ProcessGPOTemplateFile_NoFile() { + var mockLDAPUtils = new Mock(); var processor = new GPOLocalGroupProcessor(mockLDAPUtils.Object); var gpcFileSysPath = Path.Join(Path.GetTempPath(), "made", "up", "path"); @@ -266,9 +254,8 @@ public async Task GPOLocalGroupProcess_ProcessGPOTemplateFile_NoFile() } [Fact] - public async Task GPOLocalGroupProcess_ProcessGPOTemplateFile_NoMatch() - { - var mockLDAPUtils = new Mock(); + public async Task GPOLocalGroupProcess_ProcessGPOTemplateFile_NoMatch() { + var mockLDAPUtils = new Mock(); var gpcFileSysPath = Path.GetTempPath(); var gptTmplPath = Path.Join(gpcFileSysPath, "MACHINE", "Microsoft", "Windows NT", "SecEdit", "GptTmpl.inf"); @@ -283,16 +270,15 @@ public async Task GPOLocalGroupProcess_ProcessGPOTemplateFile_NoMatch() } [Fact] - public async Task GPOLocalGroupProcess_ProcessGPOTemplateFile_NullSID() - { - var mockLDAPUtils = new Mock(); + public async Task GPOLocalGroupProcess_ProcessGPOTemplateFile_NullSID() { + var mockLDAPUtils = new MockLdapUtils(); var gpcFileSysPath = Path.GetTempPath(); var gptTmplPath = Path.Join(gpcFileSysPath, "MACHINE", "Microsoft", "Windows NT", "SecEdit", "GptTmpl.inf"); Directory.CreateDirectory(Path.GetDirectoryName(gptTmplPath)); File.WriteAllText(gptTmplPath, GpttmplInfContent); - var processor = new GPOLocalGroupProcessor(mockLDAPUtils.Object); + var processor = new GPOLocalGroupProcessor(mockLDAPUtils); var actual = await processor.ProcessGPOTemplateFile(gpcFileSysPath, "somedomain").ToListAsync(); Assert.NotNull(actual); @@ -300,11 +286,10 @@ public async Task GPOLocalGroupProcess_ProcessGPOTemplateFile_NullSID() } [Fact] - public async Task GPOLocalGroupProcess_ProcessGPOTemplateFile() - { - var mockLDAPUtils = new Mock(); + public async Task GPOLocalGroupProcess_ProcessGPOTemplateFile() { + var mockLDAPUtils = new Mock(); mockLDAPUtils.Setup(x => x.ResolveAccountName(It.IsAny(), It.IsAny())) - .Returns(new TypedPrincipal("S-1-5-21-3130019616-2776909439-2417379446-513", Label.User)); + .ReturnsAsync((true, new TypedPrincipal("S-1-5-21-3130019616-2776909439-2417379446-513", Label.User))); var gpcFileSysPath = Path.GetTempPath(); var gptTmplPath = Path.Join(gpcFileSysPath, "MACHINE", "Microsoft", "Windows NT", "SecEdit", "GptTmpl.inf"); @@ -315,12 +300,19 @@ public async Task GPOLocalGroupProcess_ProcessGPOTemplateFile() var actual = await processor.ProcessGPOTemplateFile(gpcFileSysPath, "somedomain").ToListAsync(); Assert.NotNull(actual); - // Assert.Empty(actual); + Assert.NotEmpty(actual); + var expected = new GPOLocalGroupProcessor.GroupAction() { + Action = GPOLocalGroupProcessor.GroupActionOperation.Add, + Target = GPOLocalGroupProcessor.GroupActionTarget.RestrictedMember, + TargetSid = "S-1-5-21-3130019616-2776909439-2417379446-513", + TargetRid = GPOLocalGroupProcessor.LocalGroupRids.Administrators, + TargetType = Label.User + }; + Assert.Contains(expected, actual); } [Fact] - public void GPOLocalGroupProcess_GroupAction() - { + public void GPOLocalGroupProcess_GroupAction() { var ga = new GPOLocalGroupProcessor.GroupAction(); var tp = ga.ToTypedPrincipal(); var str = ga.ToString(); diff --git a/test/unit/GroupProcessorTest.cs b/test/unit/GroupProcessorTest.cs index 17f4810b..c8f621e7 100644 --- a/test/unit/GroupProcessorTest.cs +++ b/test/unit/GroupProcessorTest.cs @@ -1,5 +1,7 @@ using System; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using CommonLibTest.Facades; using Moq; using SharpHoundCommonLib; @@ -13,8 +15,14 @@ namespace CommonLibTest { public class GroupProcessorTest { - private readonly string _testDomainName; - + private readonly Result[] _testMembershipReturn = + { + Result.Ok("CN=Domain Admins,CN=Users,DC=testlab,DC=local"), + Result.Ok("CN=Enterprise Admins,CN=Users,DC=testlab,DC=local"), + Result.Ok("CN=Administrator,CN=Users,DC=testlab,DC=local"), + Result.Ok("CN=NonExistent,CN=Users,DC=testlab,DC=local") + }; + private readonly string[] _testMembership = { "CN=Domain Admins,CN=Users,DC=testlab,DC=local", @@ -22,6 +30,7 @@ public class GroupProcessorTest "CN=Administrator,CN=Users,DC=testlab,DC=local", "CN=NonExistent,CN=Users,DC=testlab,DC=local" }; + private readonly ITestOutputHelper _testOutputHelper; private GroupProcessor _baseProcessor; @@ -29,8 +38,7 @@ public class GroupProcessorTest public GroupProcessorTest(ITestOutputHelper testOutputHelper) { _testOutputHelper = testOutputHelper; - _testDomainName = "TESTLAB.LOCAL"; - _baseProcessor = new GroupProcessor(new LDAPUtils()); + _baseProcessor = new GroupProcessor(new LdapUtils()); } [Fact] @@ -55,9 +63,9 @@ public void GroupProcessor_GetPrimaryGroupInfo_BadSID_ReturnsNull() } [Fact] - public void GroupProcessor_ReadGroupMembers_EmptyMembers_DoesRangedRetrieval() + public async Task GroupProcessor_ReadGroupMembers_EmptyMembers_DoesRangedRetrieval() { - var mockUtils = new Mock(); + var mockUtils = new Mock(); var expected = new TypedPrincipal[] { new() @@ -81,20 +89,20 @@ public void GroupProcessor_ReadGroupMembers_EmptyMembers_DoesRangedRetrieval() ObjectType = Label.Base } }; - mockUtils.Setup(x => x.DoRangedRetrieval(It.IsAny(), It.IsAny())).Returns(_testMembership); + mockUtils.Setup(x => x.RangedRetrieval(It.IsAny(), It.IsAny(), It.IsAny())).Returns(_testMembershipReturn.ToAsyncEnumerable()); var processor = new GroupProcessor(mockUtils.Object); - var results = processor - .ReadGroupMembers("CN=Administrators,CN=Builtin,DC=testlab,DC=local", Array.Empty()).ToArray(); + var results = await processor + .ReadGroupMembers("CN=Administrators,CN=Builtin,DC=testlab,DC=local", Array.Empty()).ToArrayAsync(); foreach (var t in results) _testOutputHelper.WriteLine(t.ToString()); Assert.Equal(4, results.Length); Assert.Equal(expected, results); } [WindowsOnlyFact] - public void GroupProcessor_ReadGroupMembers_ReturnsCorrectMembers() + public async Task GroupProcessor_ReadGroupMembers_ReturnsCorrectMembers() { - var utils = new MockLDAPUtils(); + var utils = new MockLdapUtils(); var processor = new GroupProcessor(utils); var expected = new TypedPrincipal[] { @@ -120,8 +128,8 @@ public void GroupProcessor_ReadGroupMembers_ReturnsCorrectMembers() } }; - var results = processor - .ReadGroupMembers("CN=Administrators,CN=Builtin,DC=testlab,DC=local", _testMembership).ToArray(); + var results = await processor + .ReadGroupMembers("CN=Administrators,CN=Builtin,DC=testlab,DC=local", _testMembership).ToArrayAsync(); foreach (var t in results) _testOutputHelper.WriteLine(t.ToString()); Assert.Equal(4, results.Length); Assert.Equal(expected, results); diff --git a/test/unit/Helpers.cs b/test/unit/Helpers.cs deleted file mode 100644 index fa3f1482..00000000 --- a/test/unit/Helpers.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Xunit; - -namespace CommonLibTest -{ - public class Helpers - { - internal static byte[] B64ToBytes(string base64) - { - return Convert.FromBase64String(base64); - } - - internal static string B64ToString(string base64) - { - var b = B64ToBytes(base64); - return Encoding.UTF8.GetString(b); - } - } - - internal static class Extensions - { - internal static async Task ToArrayAsync(this IAsyncEnumerable items, - CancellationToken cancellationToken = default) - { - var results = new List(); - await foreach (var item in items.WithCancellation(cancellationToken) - .ConfigureAwait(false)) - results.Add(item); - return results.ToArray(); - } - - internal static bool IsArray(this object obj) - { - var valueType = obj?.GetType(); - if (valueType == null) - return false; - return valueType.IsArray; - } - } - - public sealed class WindowsOnlyFact : FactAttribute - { - public WindowsOnlyFact() - { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) Skip = "Ignore on non-Windows platforms"; - } - } -} \ No newline at end of file diff --git a/test/unit/LDAPFilterTest.cs b/test/unit/LDAPFilterTest.cs index 7c534486..ad9f0f8a 100644 --- a/test/unit/LDAPFilterTest.cs +++ b/test/unit/LDAPFilterTest.cs @@ -25,7 +25,7 @@ public void Dispose() [Fact] public void LDAPFilter_CreateNewFilter_FilterNotNull() { - var test = new LDAPFilter(); + var test = new LdapFilter(); Assert.NotNull(test); } @@ -36,7 +36,7 @@ public void LDAPFilter_CreateNewFilter_FilterNotNull() [Fact] public void LDAPFilter_GroupFilter_FilterCorrect() { - var test = new LDAPFilter(); + var test = new LdapFilter(); test.AddGroups(); var filter = test.GetFilter(); _testOutputHelper.WriteLine(filter); @@ -48,7 +48,7 @@ public void LDAPFilter_GroupFilter_FilterCorrect() [Fact] public void LDAPFilter_GroupFilter_ExtraFilter_FilterCorrect() { - var test = new LDAPFilter(); + var test = new LdapFilter(); test.AddGroups("objectclass=*"); var filter = test.GetFilter(); _testOutputHelper.WriteLine(filter); @@ -60,7 +60,7 @@ public void LDAPFilter_GroupFilter_ExtraFilter_FilterCorrect() [Fact] public void LDAPFilter_GetFilterList() { - var test = new LDAPFilter().AddUsers().AddComputers(); + var test = new LdapFilter().AddUsers().AddComputers(); IEnumerable filters = test.GetFilterList(); int i = 0; diff --git a/test/unit/LDAPUtilsTest.cs b/test/unit/LDAPUtilsTest.cs index aecedd97..c69e4bae 100644 --- a/test/unit/LDAPUtilsTest.cs +++ b/test/unit/LDAPUtilsTest.cs @@ -1,33 +1,28 @@ using System; using System.Collections.Generic; using System.DirectoryServices.ActiveDirectory; -using System.DirectoryServices.Protocols; -using System.Threading; +using System.Threading.Tasks; using CommonLibTest.Facades; using Moq; using SharpHoundCommonLib; using SharpHoundCommonLib.Enums; -using SharpHoundCommonLib.Exceptions; using Xunit; using Xunit.Abstractions; -namespace CommonLibTest -{ - public class LDAPUtilsTest : IDisposable - { +namespace CommonLibTest { + public class LDAPUtilsTest : IDisposable { private readonly string _testDomainName; private readonly string _testForestName; private readonly ITestOutputHelper _testOutputHelper; - private readonly ILDAPUtils _utils; + private readonly ILdapUtils _utils; #region Constructor(s) - public LDAPUtilsTest(ITestOutputHelper testOutputHelper) - { + public LDAPUtilsTest(ITestOutputHelper testOutputHelper) { _testOutputHelper = testOutputHelper; _testForestName = "PARENT.LOCAL"; _testDomainName = "TESTLAB.LOCAL"; - _utils = new LDAPUtils(); + _utils = new LdapUtils(); // This runs once per test. } @@ -35,246 +30,197 @@ public LDAPUtilsTest(ITestOutputHelper testOutputHelper) #region IDispose Implementation - public void Dispose() - { + public void Dispose() { // Tear down (called once per test) } #endregion [Fact] - public void SanityCheck() - { + public void SanityCheck() { Assert.True(true); } - #region Private Members - - #endregion - - #region Creation - /// /// [Fact] - public void GetUserGlobalCatalogMatches_Garbage_ReturnsNull() - { - var test = _utils.GetUserGlobalCatalogMatches("foo"); + public async Task GetUserGlobalCatalogMatches_Garbage_ReturnsNull() { + var test = await _utils.GetGlobalCatalogMatches("foo", "bar"); _testOutputHelper.WriteLine(test.ToString()); - Assert.NotNull(test); - Assert.Empty(test); + Assert.True(test.Success); + Assert.Empty(test.Sids); } [Fact] - public void ResolveIDAndType_DuplicateSid_ReturnsNull() - { - var test = _utils.ResolveIDAndType("ABC0ACNF", null); - Assert.Null(test); + public async Task ResolveIDAndType_DuplicateSid_ReturnsNull() { + var test = await _utils.ResolveIDAndType("ABC0ACNF", null); + Assert.False(test.Success); } [Fact] - public void ResolveIDAndType_WellKnownAdministrators_ReturnsConvertedSID() - { - var test = _utils.ResolveIDAndType("S-1-5-32-544", "TESTLAB.LOCAL"); - Assert.NotNull(test); - Assert.Equal(Label.Group, test.ObjectType); - Assert.Equal("TESTLAB.LOCAL-S-1-5-32-544", test.ObjectIdentifier); - } - - [WindowsOnlyFact] - public void GetWellKnownPrincipal_EnterpriseDomainControllers_ReturnsCorrectedSID() - { - var mock = new Mock(); - var mockForest = MockableForest.Construct(_testForestName); - mock.Setup(x => x.GetForest(It.IsAny())).Returns(mockForest); - var result = mock.Object.GetWellKnownPrincipal("S-1-5-9", null, out var typedPrincipal); - Assert.True(result); - Assert.Equal($"{_testForestName}-S-1-5-9", typedPrincipal.ObjectIdentifier); - Assert.Equal(Label.Group, typedPrincipal.ObjectType); + public async void ResolveIDAndType_WellKnownAdministrators_ReturnsConvertedSID() { + var test = await _utils.ResolveIDAndType("S-1-5-32-544", "TESTLAB.LOCAL"); + Assert.True(test.Success); + Assert.NotNull(test.Principal); + Assert.Equal(Label.Group, test.Principal.ObjectType); + Assert.Equal("TESTLAB.LOCAL-S-1-5-32-544", test.Principal.ObjectIdentifier); } [Fact] - public void BuildLdapPath_BadDomain_ReturnsNull() + public async void GetWellKnownPrincipal_EnterpriseDomainControllers_ReturnsCorrectedSID() { - var mock = new Mock(); - //var mockDomain = MockableDomain.Construct("TESTLAB.LOCAL"); - mock.Setup(x => x.GetDomain(It.IsAny())) - .Returns((Domain)null); - var result = mock.Object.BuildLdapPath("TEST", "ABC"); - Assert.Null(result); - } - - [WindowsOnlyFact] - public void BuildLdapPath_HappyPath() - { - var mock = new Mock(); - var mockDomain = MockableDomain.Construct("TESTLAB.LOCAL"); - mock.Setup(x => x.GetDomain(It.IsAny())) - .Returns(mockDomain); - var result = mock.Object.BuildLdapPath(DirectoryPaths.PKILocation, "ABC"); - Assert.NotNull(result); - Assert.Equal("CN=Public Key Services,CN=Services,CN=Configuration,DC=TESTLAB,DC=LOCAL", result); + var mock = new Mock(); + mock.Setup(x => x.GetForest(It.IsAny())).ReturnsAsync((true, _testForestName)); + var result = await mock.Object.GetWellKnownPrincipal("S-1-5-9", null); + Assert.True(result.Success); + Assert.Equal($"{_testForestName}-S-1-5-9", result.WellKnownPrincipal.ObjectIdentifier); + Assert.Equal(Label.Group, result.WellKnownPrincipal.ObjectType); } [Fact] - public void GetWellKnownPrincipal_NonWellKnown_ReturnsNull() - { - var result = _utils.GetWellKnownPrincipal("S-1-5-21-123456-78910", _testDomainName, out var typedPrincipal); - Assert.False(result); - Assert.Null(typedPrincipal); + public async void GetWellKnownPrincipal_NonWellKnown_ReturnsNull() { + var result = await _utils.GetWellKnownPrincipal("S-1-5-21-123456-78910", _testDomainName); + Assert.False(result.Success); + Assert.Null(result.WellKnownPrincipal); } [Fact] - public void GetWellKnownPrincipal_WithDomain_ConvertsSID() - { + public async void GetWellKnownPrincipal_WithDomain_ConvertsSID() { var result = - _utils.GetWellKnownPrincipal("S-1-5-32-544", _testDomainName, out var typedPrincipal); - Assert.True(result); - Assert.Equal(Label.Group, typedPrincipal.ObjectType); - Assert.Equal($"{_testDomainName}-S-1-5-32-544", typedPrincipal.ObjectIdentifier); + await _utils.GetWellKnownPrincipal("S-1-5-32-544", _testDomainName); + Assert.True(result.Success); + Assert.Equal(Label.Group, result.WellKnownPrincipal.ObjectType); + Assert.Equal($"{_testDomainName}-S-1-5-32-544", result.WellKnownPrincipal.ObjectIdentifier); } [Fact] - public void DistinguishedNameToDomain_RegularObject_CorrectDomain() - { - var result = SharpHoundCommonLib.Helpers.DistinguishedNameToDomain( - "CN=Account Operators,CN=Builtin,DC=testlab,DC=local"); - Assert.Equal("TESTLAB.LOCAL", result); - - result = SharpHoundCommonLib.Helpers.DistinguishedNameToDomain("DC=testlab,DC=local"); - Assert.Equal("TESTLAB.LOCAL", result); - } + public async Task Test_ResolveSearchResult_BadObjectID() { + var utils = new MockLdapUtils(); + var attribs = new Dictionary { + { LDAPProperties.ObjectClass, new[] { "top", "person" } }, + { LDAPProperties.SAMAccountType, "805306368" } + }; - [Fact] - public void GetDomainRangeSize_BadDomain_ReturnsDefault() - { - var mock = new Mock(); - mock.Setup(x => x.GetDomain(It.IsAny())).Returns((Domain)null); - var result = mock.Object.GetDomainRangeSize(); - Assert.Equal(750, result); + var mock = new MockDirectoryObject("abc", attribs, + "", ""); + var (success, _) = await LdapUtils.ResolveSearchResult(mock, utils); + Assert.False(success); } [Fact] - public void GetDomainRangeSize_RespectsDefaultParam() - { - var mock = new Mock(); - mock.Setup(x => x.GetDomain(It.IsAny())).Returns((Domain)null); - - var result = mock.Object.GetDomainRangeSize(null, 1000); - Assert.Equal(1000, result); - } - - [WindowsOnlyFact] - public void GetDomainRangeSize_NoLdapEntry_ReturnsDefault() - { - var mock = new Mock(); - var mockDomain = MockableDomain.Construct("testlab.local"); - mock.Setup(x => x.GetDomain(It.IsAny())).Returns(mockDomain); - mock.Setup(x => x.QueryLDAP(It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny())).Returns(new List()); - - var result = mock.Object.GetDomainRangeSize(); - Assert.Equal(750, result); - } - - [WindowsOnlyFact] - public void GetDomainRangeSize_ExpectedResults() - { - var mock = new Mock(); - var mockDomain = MockableDomain.Construct("testlab.local"); - mock.Setup(x => x.GetDomain(It.IsAny())).Returns(mockDomain); - var searchResult = new MockSearchResultEntry("CN=Default Query Policy,CN=Query-Policies,CN=Directory Service,CN=Windows NT,CN=Services,CN=Configuration,DC=testlab,DC=local", new Dictionary - { - {"ldapadminlimits", new[] - { - "MaxPageSize=1250" - }}, - }, "abc123", Label.Base); + public async Task Test_ResolveSearchResult_DeletedObject() { + var utils = new MockLdapUtils(); + var attribs = new Dictionary { + { LDAPProperties.IsDeleted, "true" }, + }; - mock.Setup(x => x.QueryLDAP(It.IsAny(), It.IsAny(), null, - It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny())).Returns(new List { searchResult }); - var result = mock.Object.GetDomainRangeSize(); - Assert.Equal(1250, result); - } + var guid = new Guid().ToString(); - [Fact] - public void DistinguishedNameToDomain_DeletedObjects_CorrectDomain() - { - var result = SharpHoundCommonLib.Helpers.DistinguishedNameToDomain( - @"DC=..Deleted-_msdcs.testlab.local\0ADEL:af1f072f-28d7-4b86-9b87-a408bfc9cb0d,CN=Deleted Objects,DC=testlab,DC=local"); - Assert.Equal("TESTLAB.LOCAL", result); + var mock = new MockDirectoryObject("abc", attribs, + "", guid); + var (success, resolved) = await LdapUtils.ResolveSearchResult(mock, utils); + Assert.True(success); + Assert.Equal(guid, resolved.ObjectId); + Assert.True(resolved.Deleted); } [Fact] - public void QueryLDAP_With_Exception() - { - var options = new LDAPQueryOptions - { - ThrowException = true + public async Task Test_ResolveSearchResult_DCObject() { + var utils = new MockLdapUtils(); + var attribs = new Dictionary { + { LDAPProperties.SAMAccountType, "805306369" }, { + LDAPProperties.UserAccountControl, + ((int)(UacFlags.ServerTrustAccount | UacFlags.WorkstationTrustAccount)).ToString() + }, + { LDAPProperties.DNSHostName, "primary.testlab.local" } }; - - Assert.Throws( - () => - { - foreach (var sre in _utils.QueryLDAP(null, new SearchScope(), null, new CancellationToken(), null, - false, false, null, false, false, true)) - { - // We shouldn't reach this anyway, and all we care about is if exceptions are bubbling - } - }); - - Assert.Throws( - () => - { - foreach (var sre in _utils.QueryLDAP(options)) - { - // We shouldn't reach this anyway, and all we care about is if exceptions are bubbling - } - }); + var guid = new Guid().ToString(); + const string sid = "S-1-5-21-3130019616-2776909439-2417379446-1001"; + const string dn = "CN=PRIMARY,OU=DOMAIN CONTROLLERS,DC=TESTLAB,DC=LOCAL"; + + var mock = new MockDirectoryObject(dn, attribs, sid, guid); + + var (success, result) = await LdapUtils.ResolveSearchResult(mock, utils); + Assert.True(success); + Assert.Equal(sid, result.ObjectId); + Assert.Equal(Label.Computer, result.ObjectType); + Assert.True(result.IsDomainController); + Assert.Equal("PRIMARY.TESTLAB.LOCAL", result.DisplayName); + Assert.Equal("S-1-5-21-3130019616-2776909439-2417379446", result.DomainSid); + Assert.Equal("TESTLAB.LOCAL", result.Domain); + Assert.False(result.Deleted); + + mock.DistinguishedName = ""; + + (success, result) = await LdapUtils.ResolveSearchResult(mock, utils); + Assert.True(success); + Assert.Equal(sid, result.ObjectId); + Assert.Equal(Label.Computer, result.ObjectType); + Assert.True(result.IsDomainController); + Assert.Equal("PRIMARY.TESTLAB.LOCAL", result.DisplayName); + Assert.Equal("S-1-5-21-3130019616-2776909439-2417379446", result.DomainSid); + Assert.Equal("TESTLAB.LOCAL", result.Domain); + Assert.False(result.Deleted); + + mock.Properties.Remove(LDAPProperties.DNSHostName); + mock.Properties[LDAPProperties.CanonicalName] = "PRIMARY"; + (success, result) = await LdapUtils.ResolveSearchResult(mock, utils); + Assert.True(success); + Assert.Equal(sid, result.ObjectId); + Assert.Equal(Label.Computer, result.ObjectType); + Assert.True(result.IsDomainController); + Assert.Equal("PRIMARY.TESTLAB.LOCAL", result.DisplayName); + Assert.Equal("S-1-5-21-3130019616-2776909439-2417379446", result.DomainSid); + Assert.Equal("TESTLAB.LOCAL", result.Domain); + Assert.False(result.Deleted); + + mock.Properties.Remove(LDAPProperties.CanonicalName); + mock.Properties[LDAPProperties.Name] = "PRIMARY"; + (success, result) = await LdapUtils.ResolveSearchResult(mock, utils); + Assert.True(success); + Assert.Equal(sid, result.ObjectId); + Assert.Equal(Label.Computer, result.ObjectType); + Assert.True(result.IsDomainController); + Assert.Equal("PRIMARY.TESTLAB.LOCAL", result.DisplayName); + Assert.Equal("S-1-5-21-3130019616-2776909439-2417379446", result.DomainSid); + Assert.Equal("TESTLAB.LOCAL", result.Domain); + Assert.False(result.Deleted); + + mock.Properties.Remove(LDAPProperties.Name); + (success, result) = await LdapUtils.ResolveSearchResult(mock, utils); + Assert.True(success); + Assert.Equal(sid, result.ObjectId); + Assert.Equal(Label.Computer, result.ObjectType); + Assert.True(result.IsDomainController); + Assert.Equal("UNKNOWN.TESTLAB.LOCAL", result.DisplayName); + Assert.Equal("S-1-5-21-3130019616-2776909439-2417379446", result.DomainSid); + Assert.Equal("TESTLAB.LOCAL", result.Domain); + Assert.False(result.Deleted); } [Fact] - public void QueryLDAP_Without_Exception() - { - Exception exception; - - var options = new LDAPQueryOptions - { - ThrowException = false + public async Task Test_ResolveSearchResult_MSAGMSA() { + var utils = new MockLdapUtils(); + var attribs = new Dictionary { + { LDAPProperties.ObjectClass, new[] { "top", ObjectClass.MSAClass } }, + { LDAPProperties.SAMAccountType, "805306369" }, + { LDAPProperties.SAMAccountName, "TESTMSA$" } }; - exception = Record.Exception( - () => - { - foreach (var sre in _utils.QueryLDAP(null, new SearchScope(), null, new CancellationToken())) - { - // We shouldn't reach this anyway, and all we care about is if exceptions are bubbling - } - }); - Assert.Null(exception); - - exception = Record.Exception( - () => - { - foreach (var sre in _utils.QueryLDAP(options)) - { - // We shouldn't reach this anyway, and all we care about is if exceptions are bubbling - } - }); - Assert.Null(exception); - } - - #endregion + const string sid = "S-1-5-21-3130019616-2776909439-2417379446-2105"; + const string dn = "CN=TESTMSA,CN=MANAGED SERVICE ACCOUNTS,DC=TESTLAB,DC=LOCAL"; + var guid = new Guid().ToString(); - #region Structural + var mock = new MockDirectoryObject(dn, attribs, sid, guid); - #endregion - - - #region Behavioral - - #endregion + var (success, result) = await LdapUtils.ResolveSearchResult(mock, utils); + Assert.True(success); + Assert.Equal(sid, result.ObjectId); + Assert.Equal(Label.User, result.ObjectType); + Assert.Equal("TESTMSA$@TESTLAB.LOCAL", result.DisplayName); + Assert.Equal("S-1-5-21-3130019616-2776909439-2417379446", result.DomainSid); + Assert.Equal("TESTLAB.LOCAL", result.Domain); + Assert.False(result.Deleted); + } } } \ No newline at end of file diff --git a/test/unit/LDAPPropertyTests.cs b/test/unit/LdapPropertyTests.cs similarity index 75% rename from test/unit/LDAPPropertyTests.cs rename to test/unit/LdapPropertyTests.cs index e405a18a..30c35cd4 100644 --- a/test/unit/LDAPPropertyTests.cs +++ b/test/unit/LdapPropertyTests.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using CommonLibTest.Facades; using SharpHoundCommonLib; @@ -8,14 +10,15 @@ using SharpHoundCommonLib.Processors; using Xunit; using Xunit.Abstractions; +// ReSharper disable StringLiteralTypo namespace CommonLibTest { - public class LDAPPropertyTests + public class LdapPropertyTests { private readonly ITestOutputHelper _testOutputHelper; - public LDAPPropertyTests(ITestOutputHelper testOutputHelper) + public LdapPropertyTests(ITestOutputHelper testOutputHelper) { _testOutputHelper = testOutputHelper; } @@ -23,13 +26,13 @@ public LDAPPropertyTests(ITestOutputHelper testOutputHelper) [Fact] public void LDAPPropertyProcessor_ReadDomainProperties_TestGoodData() { - var mock = new MockSearchResultEntry("DC\u003dtestlab,DC\u003dlocal", new Dictionary + var mock = new MockDirectoryObject("DC\u003dtestlab,DC\u003dlocal", new Dictionary { {"description", "TESTLAB Domain"}, {"msds-behavior-version", "6"} - }, "S-1-5-21-3130019616-2776909439-2417379446", Label.Domain); + }, "S-1-5-21-3130019616-2776909439-2417379446",""); - var test = LDAPPropertyProcessor.ReadDomainProperties(mock); + var test = LdapPropertyProcessor.ReadDomainProperties(mock); Assert.Contains("functionallevel", test.Keys); Assert.Equal("2012 R2", test["functionallevel"] as string); Assert.Contains("description", test.Keys); @@ -39,12 +42,12 @@ public void LDAPPropertyProcessor_ReadDomainProperties_TestGoodData() [Fact] public void LDAPPropertyProcessor_ReadDomainProperties_TestBadFunctionalLevel() { - var mock = new MockSearchResultEntry("DC\u003dtestlab,DC\u003dlocal", new Dictionary + var mock = new MockDirectoryObject("DC\u003dtestlab,DC\u003dlocal", new Dictionary { {"msds-behavior-version", "a"} - }, "S-1-5-21-3130019616-2776909439-2417379446", Label.Domain); + }, "S-1-5-21-3130019616-2776909439-2417379446",""); - var test = LDAPPropertyProcessor.ReadDomainProperties(mock); + var test = LdapPropertyProcessor.ReadDomainProperties(mock); Assert.Contains("functionallevel", test.Keys); Assert.Equal("Unknown", test["functionallevel"] as string); } @@ -66,25 +69,25 @@ public void LDAPPropertyProcessor_FunctionalLevelToString_TestFunctionalLevels() }; foreach (var (key, value) in expected) - Assert.Equal(value, LDAPPropertyProcessor.FunctionalLevelToString(key)); + Assert.Equal(value, LdapPropertyProcessor.FunctionalLevelToString(key)); } [Fact] public void LDAPPropertyProcessor_ReadGPOProperties_TestGoodData() { - var mock = new MockSearchResultEntry( + var mock = new MockDirectoryObject( "CN\u003d{94DD0260-38B5-497E-8876-10E7A96E80D0},CN\u003dPolicies,CN\u003dSystem,DC\u003dtestlab,DC\u003dlocal", new Dictionary { { "gpcfilesyspath", - Helpers.B64ToString( + Utils.B64ToString( "XFx0ZXN0bGFiLmxvY2FsXFN5c1ZvbFx0ZXN0bGFiLmxvY2FsXFBvbGljaWVzXHs5NEREMDI2MC0zOEI1LTQ5N0UtODg3Ni0xMEU3QTk2RTgwRDB9") }, {"description", "Test"} - }, "S-1-5-21-3130019616-2776909439-2417379446", Label.GPO); + }, "S-1-5-21-3130019616-2776909439-2417379446",""); - var test = LDAPPropertyProcessor.ReadGPOProperties(mock); + var test = LdapPropertyProcessor.ReadGPOProperties(mock); Assert.Contains("description", test.Keys); Assert.Equal("Test", test["description"] as string); @@ -96,13 +99,13 @@ public void LDAPPropertyProcessor_ReadGPOProperties_TestGoodData() [Fact] public void LDAPPropertyProcessor_ReadOUProperties_TestGoodData() { - var mock = new MockSearchResultEntry("OU\u003dTestOU,DC\u003dtestlab,DC\u003dlocal", + var mock = new MockDirectoryObject("OU\u003dTestOU,DC\u003dtestlab,DC\u003dlocal", new Dictionary { {"description", "Test"} - }, "2A374493-816A-4193-BEFD-D2F4132C6DCA", Label.OU); + },"", "2A374493-816A-4193-BEFD-D2F4132C6DCA"); - var test = LDAPPropertyProcessor.ReadOUProperties(mock); + var test = LdapPropertyProcessor.ReadOUProperties(mock); Assert.Contains("description", test.Keys); Assert.Equal("Test", test["description"] as string); } @@ -110,14 +113,14 @@ public void LDAPPropertyProcessor_ReadOUProperties_TestGoodData() [Fact] public void LDAPPropertyProcessor_ReadGroupProperties_TestGoodData() { - var mock = new MockSearchResultEntry("CN\u003dDomain Admins,CN\u003dUsers,DC\u003dtestlab,DC\u003dlocal", + var mock = new MockDirectoryObject("CN\u003dDomain Admins,CN\u003dUsers,DC\u003dtestlab,DC\u003dlocal", new Dictionary { {"description", "Test"}, {"admincount", "1"} - }, "S-1-5-21-3130019616-2776909439-2417379446-512", Label.Group); + }, "S-1-5-21-3130019616-2776909439-2417379446-512",""); - var test = LDAPPropertyProcessor.ReadGroupProperties(mock); + var test = LdapPropertyProcessor.ReadGroupProperties(mock); Assert.Contains("description", test.Keys); Assert.Equal("Test", test["description"] as string); Assert.Contains("admincount", test.Keys); @@ -127,14 +130,14 @@ public void LDAPPropertyProcessor_ReadGroupProperties_TestGoodData() [Fact] public void LDAPPropertyProcessor_ReadGroupProperties_TestGoodData_FalseAdminCount() { - var mock = new MockSearchResultEntry("CN\u003dDomain Admins,CN\u003dUsers,DC\u003dtestlab,DC\u003dlocal", + var mock = new MockDirectoryObject("CN\u003dDomain Admins,CN\u003dUsers,DC\u003dtestlab,DC\u003dlocal", new Dictionary { {"description", "Test"}, {"admincount", "0"} - }, "S-1-5-21-3130019616-2776909439-2417379446-512", Label.Group); + }, "S-1-5-21-3130019616-2776909439-2417379446-512",""); - var test = LDAPPropertyProcessor.ReadGroupProperties(mock); + var test = LdapPropertyProcessor.ReadGroupProperties(mock); Assert.Contains("description", test.Keys); Assert.Equal("Test", test["description"] as string); Assert.Contains("admincount", test.Keys); @@ -144,13 +147,13 @@ public void LDAPPropertyProcessor_ReadGroupProperties_TestGoodData_FalseAdminCou [Fact] public void LDAPPropertyProcessor_ReadGroupProperties_NullAdminCount() { - var mock = new MockSearchResultEntry("CN\u003dDomain Admins,CN\u003dUsers,DC\u003dtestlab,DC\u003dlocal", + var mock = new MockDirectoryObject("CN\u003dDomain Admins,CN\u003dUsers,DC\u003dtestlab,DC\u003dlocal", new Dictionary { {"description", "Test"} - }, "S-1-5-21-3130019616-2776909439-2417379446-512", Label.Group); + }, "S-1-5-21-3130019616-2776909439-2417379446-512",""); - var test = LDAPPropertyProcessor.ReadGroupProperties(mock); + var test = LdapPropertyProcessor.ReadGroupProperties(mock); Assert.Contains("description", test.Keys); Assert.Equal("Test", test["description"] as string); Assert.Contains("admincount", test.Keys); @@ -160,13 +163,13 @@ public void LDAPPropertyProcessor_ReadGroupProperties_NullAdminCount() [Fact] public async Task LDAPPropertyProcessor_ReadUserProperties_TestTrustedToAuth() { - var mock = new MockSearchResultEntry("CN\u003ddfm,CN\u003dUsers,DC\u003dtestlab,DC\u003dlocal", + var mock = new MockDirectoryObject("CN\u003ddfm,CN\u003dUsers,DC\u003dtestlab,DC\u003dlocal", new Dictionary { {"description", "Test"}, {"useraccountcontrol", 0x1000000.ToString()}, - {"lastlogon", "132673011142753043"}, - {"lastlogontimestamp", "132670318095676525"}, + {LDAPProperties.LastLogon, "132673011142753043"}, + {LDAPProperties.LastLogonTimestamp, "132670318095676525"}, {"homedirectory", @"\\win10\testdir"}, { "serviceprincipalname", new[] @@ -178,7 +181,7 @@ public async Task LDAPPropertyProcessor_ReadUserProperties_TestTrustedToAuth() { "sidhistory", new[] { - Helpers.B64ToBytes("AQUAAAAAAAUVAAAAIE+Qun9GhKV2SBaQUQQAAA==") + Utils.B64ToBytes("AQUAAAAAAAUVAAAAIE+Qun9GhKV2SBaQUQQAAA==") } }, {"pwdlastset", "132131667346106691"}, @@ -189,10 +192,10 @@ public async Task LDAPPropertyProcessor_ReadUserProperties_TestTrustedToAuth() "rdpman/win10" } } - }, "S-1-5-21-3130019616-2776909439-2417379446-1101", Label.User); + }, "S-1-5-21-3130019616-2776909439-2417379446-1101", ""); - var processor = new LDAPPropertyProcessor(new MockLDAPUtils()); - var test = await processor.ReadUserProperties(mock); + var processor = new LdapPropertyProcessor(new MockLdapUtils()); + var test = await processor.ReadUserProperties(mock, "testlab.local"); var props = test.Props; var keys = props.Keys; @@ -223,7 +226,7 @@ public async Task LDAPPropertyProcessor_ReadUserProperties_TestTrustedToAuth() [Fact] public async Task LDAPPropertyProcessor_ReadUserProperties_NullAdminCount() { - var mock = new MockSearchResultEntry("CN\u003ddfm,CN\u003dUsers,DC\u003dtestlab,DC\u003dlocal", + var mock = new MockDirectoryObject("CN\u003ddfm,CN\u003dUsers,DC\u003dtestlab,DC\u003dlocal", new Dictionary { {"description", "Test"}, @@ -240,14 +243,14 @@ public async Task LDAPPropertyProcessor_ReadUserProperties_NullAdminCount() { "sidhistory", new[] { - Helpers.B64ToBytes("AQUAAAAAAAUVAAAAIE+Qun9GhKV2SBaQUQQAAA==") + Utils.B64ToBytes("AQUAAAAAAAUVAAAAIE+Qun9GhKV2SBaQUQQAAA==") } }, {"pwdlastset", "132131667346106691"} - }, "S-1-5-21-3130019616-2776909439-2417379446-1101", Label.User); + }, "S-1-5-21-3130019616-2776909439-2417379446-1101",""); - var processor = new LDAPPropertyProcessor(new MockLDAPUtils()); - var test = await processor.ReadUserProperties(mock); + var processor = new LdapPropertyProcessor(new MockLdapUtils()); + var test = await processor.ReadUserProperties(mock, "testlab.local"); var props = test.Props; var keys = props.Keys; Assert.Contains("admincount", keys); @@ -257,7 +260,7 @@ public async Task LDAPPropertyProcessor_ReadUserProperties_NullAdminCount() [WindowsOnlyFact] public async Task LDAPPropertyProcessor_ReadUserProperties_HappyPath() { - var mock = new MockSearchResultEntry("CN\u003ddfm,CN\u003dUsers,DC\u003dtestlab,DC\u003dlocal", + var mock = new MockDirectoryObject("CN\u003ddfm,CN\u003dUsers,DC\u003dtestlab,DC\u003dlocal", new Dictionary { {"description", "Test"}, @@ -276,14 +279,14 @@ public async Task LDAPPropertyProcessor_ReadUserProperties_HappyPath() { "sidhistory", new[] { - Helpers.B64ToBytes("AQUAAAAAAAUVAAAAIE+Qun9GhKV2SBaQUQQAAA==") + Utils.B64ToBytes("AQUAAAAAAAUVAAAAIE+Qun9GhKV2SBaQUQQAAA==") } }, {"pwdlastset", "132131667346106691"} - }, "S-1-5-21-3130019616-2776909439-2417379446-1101", Label.User); + }, "S-1-5-21-3130019616-2776909439-2417379446-1101",""); - var processor = new LDAPPropertyProcessor(new MockLDAPUtils()); - var test = await processor.ReadUserProperties(mock); + var processor = new LdapPropertyProcessor(new MockLdapUtils()); + var test = await processor.ReadUserProperties(mock, "testlab.local"); var props = test.Props; var keys = props.Keys; @@ -339,7 +342,7 @@ public async Task LDAPPropertyProcessor_ReadUserProperties_HappyPath() [Fact] public async Task LDAPPropertyProcessor_ReadUserProperties_TestBadPaths() { - var mock = new MockSearchResultEntry("CN\u003ddfm,CN\u003dUsers,DC\u003dtestlab,DC\u003dlocal", + var mock = new MockDirectoryObject("CN\u003ddfm,CN\u003dUsers,DC\u003dtestlab,DC\u003dlocal", new Dictionary { {"description", "Test"}, @@ -361,10 +364,10 @@ public async Task LDAPPropertyProcessor_ReadUserProperties_TestBadPaths() } }, {"pwdlastset", "132131667346106691"} - }, "S-1-5-21-3130019616-2776909439-2417379446-1101", Label.User); + }, "S-1-5-21-3130019616-2776909439-2417379446-1101",""); - var processor = new LDAPPropertyProcessor(new MockLDAPUtils()); - var test = await processor.ReadUserProperties(mock); + var processor = new LdapPropertyProcessor(new MockLdapUtils()); + var test = await processor.ReadUserProperties(mock, "testlab.local"); var props = test.Props; var keys = props.Keys; @@ -392,7 +395,7 @@ public async Task LDAPPropertyProcessor_ReadUserProperties_TestBadPaths() public async Task LDAPPropertyProcessor_ReadComputerProperties_HappyPath() { //TODO: Add coverage for allowedtoact - var mock = new MockSearchResultEntry("CN\u003dWIN10,OU\u003dTestOU,DC\u003dtestlab,DC\u003dlocal", + var mock = new MockDirectoryObject("CN\u003dWIN10,OU\u003dTestOU,DC\u003dtestlab,DC\u003dlocal", new Dictionary { {"description", "Test"}, @@ -406,7 +409,7 @@ public async Task LDAPPropertyProcessor_ReadComputerProperties_HappyPath() { "sidhistory", new[] { - Helpers.B64ToBytes("AQUAAAAAAAUVAAAAIE+Qun9GhKV2SBaQUQQAAA==") + Utils.B64ToBytes("AQUAAAAAAAUVAAAAIE+Qun9GhKV2SBaQUQQAAA==") } }, { @@ -429,10 +432,10 @@ public async Task LDAPPropertyProcessor_ReadComputerProperties_HappyPath() "HOST/WIN10.testlab.local" } } - }, "S-1-5-21-3130019616-2776909439-2417379446-1101", Label.Computer); + }, "S-1-5-21-3130019616-2776909439-2417379446-1101",""); - var processor = new LDAPPropertyProcessor(new MockLDAPUtils()); - var test = await processor.ReadComputerProperties(mock); + var processor = new LdapPropertyProcessor(new MockLdapUtils()); + var test = await processor.ReadComputerProperties(mock, "testlab.local"); var props = test.Props; var keys = props.Keys; @@ -490,7 +493,7 @@ public async Task LDAPPropertyProcessor_ReadComputerProperties_HappyPath() [Fact] public async Task LDAPPropertyProcessor_ReadComputerProperties_TestBadPaths() { - var mock = new MockSearchResultEntry("CN\u003dWIN10,OU\u003dTestOU,DC\u003dtestlab,DC\u003dlocal", + var mock = new MockDirectoryObject("CN\u003dWIN10,OU\u003dTestOU,DC\u003dtestlab,DC\u003dlocal", new Dictionary { {"description", "Test"}, @@ -525,10 +528,10 @@ public async Task LDAPPropertyProcessor_ReadComputerProperties_TestBadPaths() "HOST/WIN10.testlab.local" } } - }, "S-1-5-21-3130019616-2776909439-2417379446-1101", Label.Computer); + }, "S-1-5-21-3130019616-2776909439-2417379446-1101", ""); - var processor = new LDAPPropertyProcessor(new MockLDAPUtils()); - var test = await processor.ReadComputerProperties(mock); + var processor = new LdapPropertyProcessor(new MockLdapUtils()); + var test = await processor.ReadComputerProperties(mock, "testlab.local"); var props = test.Props; var keys = props.Keys; @@ -546,7 +549,7 @@ public async Task LDAPPropertyProcessor_ReadComputerProperties_TestBadPaths() [Fact] public async Task LDAPPropertyProcessor_ReadComputerProperties_TestDumpSMSAPassword() { - var mock = new MockSearchResultEntry("CN\u003dWIN10,OU\u003dTestOU,DC\u003dtestlab,DC\u003dlocal", + var mock = new MockDirectoryObject("CN\u003dWIN10,OU\u003dTestOU,DC\u003dtestlab,DC\u003dlocal", new Dictionary { {"description", "Test"}, @@ -559,7 +562,7 @@ public async Task LDAPPropertyProcessor_ReadComputerProperties_TestDumpSMSAPassw { "sidhistory", new[] { - Helpers.B64ToBytes("AQUAAAAAAAUVAAAAIE+Qun9GhKV2SBaQUQQAAA==") + Utils.B64ToBytes("AQUAAAAAAAUVAAAAIE+Qun9GhKV2SBaQUQQAAA==") } }, { @@ -589,10 +592,10 @@ public async Task LDAPPropertyProcessor_ReadComputerProperties_TestDumpSMSAPassw "CN=krbtgt,CN=Users,DC=testlab,DC=local" } } - }, "S-1-5-21-3130019616-2776909439-2417379446-1101", Label.Computer); + }, "S-1-5-21-3130019616-2776909439-2417379446-1101", ""); - var processor = new LDAPPropertyProcessor(new MockLDAPUtils()); - var test = await processor.ReadComputerProperties(mock); + var processor = new LdapPropertyProcessor(new MockLdapUtils()); + var test = await processor.ReadComputerProperties(mock, "testlab.local"); var expected = new TypedPrincipal[] { @@ -617,7 +620,7 @@ public async Task LDAPPropertyProcessor_ReadComputerProperties_TestDumpSMSAPassw [Fact] public void LDAPPropertyProcessor_ReadRootCAProperties() { - var mock = new MockSearchResultEntry( + var mock = new MockDirectoryObject( "CN\u003dDUMPSTER-DC01-CA,CN\u003dCERTIFICATION AUTHORITIES,CN\u003dPUBLIC KEY SERVICES,CN\u003dSERVICES,CN\u003dCONFIGURATION,DC\u003dDUMPSTER,DC\u003dFIRE", new Dictionary { @@ -626,9 +629,9 @@ public void LDAPPropertyProcessor_ReadRootCAProperties() {"name", "DUMPSTER-DC01-CA@DUMPSTER.FIRE"}, {"domainsid", "S-1-5-21-2697957641-2271029196-387917394"}, {"whencreated", 1683986131}, - }, "2F9F3630-F46A-49BF-B186-6629994EBCF9", Label.RootCA); + }, "","2F9F3630-F46A-49BF-B186-6629994EBCF9"); - var test = LDAPPropertyProcessor.ReadRootCAProperties(mock); + var test = LdapPropertyProcessor.ReadRootCAProperties(mock); var keys = test.Keys; //These are not common properties @@ -636,14 +639,17 @@ public void LDAPPropertyProcessor_ReadRootCAProperties() Assert.DoesNotContain("name", keys); Assert.DoesNotContain("domainsid", keys); - Assert.Contains("description", keys); Assert.Contains("whencreated", keys); } [Fact] - public void LDAPPropertyProcessor_ReadAIACAProperties() - { - var mock = new MockSearchResultEntry( + public void LDAPPropertyProcessor_ReadAIACAProperties() { + var ecdsa = ECDsa.Create(); + var req = new CertificateRequest("cn=foobar", ecdsa, HashAlgorithmName.SHA256); + var cert = req.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(5)); + + var bytes = cert.Export(X509ContentType.Cert, "abc"); + var mock = new MockDirectoryObject( "CN\u003dDUMPSTER-DC01-CA,CN\u003dAIA,CN\u003dPUBLIC KEY SERVICES,CN\u003dSERVICES,CN\u003dCONFIGURATION,DC\u003dDUMPSTER,DC\u003dFIRE", new Dictionary { @@ -653,9 +659,10 @@ public void LDAPPropertyProcessor_ReadAIACAProperties() {"domainsid", "S-1-5-21-2697957641-2271029196-387917394"}, {"whencreated", 1683986131}, {"hascrosscertificatepair", true}, - }, "2F9F3630-F46A-49BF-B186-6629994EBCF9", Label.AIACA); + {LDAPProperties.CACertificate, bytes} + }, "","2F9F3630-F46A-49BF-B186-6629994EBCF9"); - var test = LDAPPropertyProcessor.ReadAIACAProperties(mock); + var test = LdapPropertyProcessor.ReadAIACAProperties(mock); var keys = test.Keys; //These are not common properties @@ -663,15 +670,19 @@ public void LDAPPropertyProcessor_ReadAIACAProperties() Assert.DoesNotContain("name", keys); Assert.DoesNotContain("domainsid", keys); - Assert.Contains("description", keys); Assert.Contains("whencreated", keys); Assert.Contains("crosscertificatepair", keys); + Assert.Contains("certthumbprint", keys); + Assert.Contains("certname", keys); + Assert.Contains("certchain", keys); + Assert.Contains("hasbasicconstraints", keys); + Assert.Contains("basicconstraintpathlength", keys); } [Fact] public void LDAPPropertyProcessor_ReadNTAuthStoreProperties() { - var mock = new MockSearchResultEntry("CN\u003dNTAUTHCERTIFICATES,CN\u003dPUBLIC KEY SERVICES,CN\u003dSERVICES,CN\u003dCONFIGURATION,DC\u003dDUMPSTER,DC\u003dFIRE", + var mock = new MockDirectoryObject("CN\u003dNTAUTHCERTIFICATES,CN\u003dPUBLIC KEY SERVICES,CN\u003dSERVICES,CN\u003dCONFIGURATION,DC\u003dDUMPSTER,DC\u003dFIRE", new Dictionary { {"description", null}, @@ -679,9 +690,9 @@ public void LDAPPropertyProcessor_ReadNTAuthStoreProperties() {"name", "NTAUTHCERTIFICATES@DUMPSTER.FIRE"}, {"domainsid", "S-1-5-21-2697957641-2271029196-387917394"}, {"whencreated", 1683986131}, - }, "2F9F3630-F46A-49BF-B186-6629994EBCF9", Label.NTAuthStore); + }, "","2F9F3630-F46A-49BF-B186-6629994EBCF9"); - var test = LDAPPropertyProcessor.ReadNTAuthStoreProperties(mock); + var test = LdapPropertyProcessor.ReadNTAuthStoreProperties(mock); var keys = test.Keys; //These are not common properties @@ -689,14 +700,13 @@ public void LDAPPropertyProcessor_ReadNTAuthStoreProperties() Assert.DoesNotContain("name", keys); Assert.DoesNotContain("domainsid", keys); - Assert.Contains("description", keys); Assert.Contains("whencreated", keys); } [Fact] public void LDAPPropertyProcessor_ReadCertTemplateProperties() { - var mock = new MockSearchResultEntry("CN\u003dWORKSTATION,CN\u003dCERTIFICATE TEMPLATES,CN\u003dPUBLIC KEY SERVICES,CN\u003dSERVICES,CN\u003dCONFIGURATION,DC\u003dEXTERNAL,DC\u003dLOCAL", + var mock = new MockDirectoryObject("CN\u003dWORKSTATION,CN\u003dCERTIFICATE TEMPLATES,CN\u003dPUBLIC KEY SERVICES,CN\u003dSERVICES,CN\u003dCONFIGURATION,DC\u003dEXTERNAL,DC\u003dLOCAL", new Dictionary { {"domain", "EXTERNAL.LOCAL"}, @@ -706,32 +716,33 @@ public void LDAPPropertyProcessor_ReadCertTemplateProperties() {"whencreated", 1683986183}, {"validityperiod", 31536000}, {"renewalperiod", 3628800}, - {"schemaversion", 2}, + {LDAPProperties.TemplateSchemaVersion, 2}, {"displayname", "Workstation Authentication"}, {"oid", "1.3.6.1.4.1.311.21.8.4571196.1884641.3293620.10686285.12068043.134.1.30"}, - {"enrollmentflag", 32}, + {LDAPProperties.PKIEnrollmentFlag, 32}, {"requiresmanagerapproval", false}, - {"certificatenameflag", 0x8000000}, + {LDAPProperties.PKINameFlag, 0x8000000}, {"ekus", new[] - {"1.3.6.1.5.5.7.3.2"} + {"1.3.6.1.5.5.7.3.2"} }, {LDAPProperties.CertificateApplicationPolicy, new[] - {"1.3.6.1.5.5.7.3.2"} + {"1.3.6.1.5.5.7.3.2"} }, {LDAPProperties.CertificatePolicy, new[] {"1.3.6.1.5.5.7.3.2"} }, - {"authorizedsignatures", 1}, + {LDAPProperties.NumSignaturesRequired, 1}, {"applicationpolicies", new[] - { "1.3.6.1.4.1.311.20.2.1"} + { "1.3.6.1.4.1.311.20.2.1"} }, {"issuancepolicies", new[] - {"1.3.6.1.4.1.311.21.8.4571196.1884641.3293620.10686285.12068043.134.1.400", - "1.3.6.1.4.1.311.21.8.4571196.1884641.3293620.10686285.12068043.134.1.402"} + {"1.3.6.1.4.1.311.21.8.4571196.1884641.3293620.10686285.12068043.134.1.400", + "1.3.6.1.4.1.311.21.8.4571196.1884641.3293620.10686285.12068043.134.1.402"} }, - }, "2F9F3630-F46A-49BF-B186-6629994EBCF9", Label.CertTemplate); + {LDAPProperties.PKIPrivateKeyFlag, 256}, + }, "","2F9F3630-F46A-49BF-B186-6629994EBCF9"); - var test = LDAPPropertyProcessor.ReadCertTemplateProperties(mock); + var test = LdapPropertyProcessor.ReadCertTemplateProperties(mock); var keys = test.Keys; //These are not common properties @@ -739,7 +750,6 @@ public void LDAPPropertyProcessor_ReadCertTemplateProperties() Assert.DoesNotContain("name", keys); Assert.DoesNotContain("domainsid", keys); - Assert.Contains("description", keys); Assert.Contains("whencreated", keys); Assert.Contains("validityperiod", keys); Assert.Contains("renewalperiod", keys); @@ -769,74 +779,66 @@ public void LDAPPropertyProcessor_ReadCertTemplateProperties() Assert.Contains("issuancepolicies", keys); } - + [Fact] - public void LDAPPropertyProcessor_ReadIssuancePolicyProperties() + public async Task LDAPPropertyProcessor_ReadIssuancePolicyProperties() { - var mock = new MockSearchResultEntry("CN\u003d6250993.11BB1AB25A8A65E9FCDF709FCDD5FBC6,CN\u003dOID,CN\u003dPUBLIC KEY SERVICES,CN\u003dSERVICES,CN\u003dCONFIGURATION,DC\u003dESC10,DC\u003dLOCAL", + var mock = new MockDirectoryObject("CN\u003d6250993.11BB1AB25A8A65E9FCDF709FCDD5FBC6,CN\u003dOID,CN\u003dPUBLIC KEY SERVICES,CN\u003dSERVICES,CN\u003dCONFIGURATION,DC\u003dESC10,DC\u003dLOCAL", new Dictionary { - {"domain", "ESC10.LOCAL"}, - {"name", "KEYADMINSOID@ESC10.LOCAL"}, - {"domainsid", "S-1-5-21-3662707843-2053279151-3839588741"}, - {"description", null}, - {"whencreated", 1712567279}, - {"displayname", "KeyAdminsOID"}, - {"certtemplateoid", "1.3.6.1.4.1.311.21.8.4571196.1884641.3293620.10686285.12068043.134.1.30"}, - {"msds-oidtogrouplink", "CN=ENTERPRISE KEY ADMINS,CN=USERS,DC=ESC10,DC=LOCAL"} + {LDAPProperties.Description, null}, + {LDAPProperties.WhenCreated, 1712567279}, + {LDAPProperties.DisplayName, "KeyAdminsOID"}, + {LDAPProperties.CertTemplateOID, "1.3.6.1.4.1.311.21.8.4571196.1884641.3293620.10686285.12068043.134.1.30"}, + {LDAPProperties.OIDGroupLink, "CN=ENTERPRISE KEY ADMINS,CN=USERS,DC=ESC10,DC=LOCAL"} , - }, "1E5311A8-E949-4E02-8E08-234ED63200DE", Label.IssuancePolicy); - - var mockLDAPUtils = new MockLDAPUtils(); - var ldapPropertyProcessor = new LDAPPropertyProcessor(mockLDAPUtils); - - - var test = ldapPropertyProcessor.ReadIssuancePolicyProperties(mock); + }, "","1E5311A8-E949-4E02-8E08-234ED63200DE"); + + var mockLDAPUtils = new MockLdapUtils(); + var ldapPropertyProcessor = new LdapPropertyProcessor(mockLDAPUtils); + + + var test = await ldapPropertyProcessor.ReadIssuancePolicyProperties(mock); var keys = test.Props.Keys; - + //These are not common properties Assert.DoesNotContain("domain", keys); Assert.DoesNotContain("name", keys); Assert.DoesNotContain("domainsid", keys); - - Assert.Contains("description", keys); + Assert.Contains("whencreated", keys); Assert.Contains("displayname", keys); Assert.Contains("certtemplateoid", keys); Assert.Contains("oidgrouplink", keys); } - + [Fact] - public void LDAPPropertyProcessor_ReadIssuancePolicyProperties_NoOIDGroupLink() + public async Task LDAPPropertyProcessor_ReadIssuancePolicyProperties_NoOIDGroupLink() { - var mock = new MockSearchResultEntry("CN\u003d6250993.11BB1AB25A8A65E9FCDF709FCDD5FBC6,CN\u003dOID,CN\u003dPUBLIC KEY SERVICES,CN\u003dSERVICES,CN\u003dCONFIGURATION,DC\u003dESC10,DC\u003dLOCAL", + var mock = new MockDirectoryObject("CN\u003d6250993.11BB1AB25A8A65E9FCDF709FCDD5FBC6,CN\u003dOID,CN\u003dPUBLIC KEY SERVICES,CN\u003dSERVICES,CN\u003dCONFIGURATION,DC\u003dESC10,DC\u003dLOCAL", new Dictionary { - {"domain", "ESC10.LOCAL"}, - {"name", "KEYADMINSOID@ESC10.LOCAL"}, - {"domainsid", "S-1-5-21-3662707843-2053279151-3839588741"}, - {"description", null}, - {"whencreated", 1712567279}, - {"displayname", "KeyAdminsOID"}, - {"certtemplateoid", "1.3.6.1.4.1.311.21.8.4571196.1884641.3293620.10686285.12068043.134.1.30"}, - {"msds-oidtogrouplink", null} + {LDAPProperties.Description, null}, + {LDAPProperties.WhenCreated, 1712567279}, + {LDAPProperties.DisplayName, "KeyAdminsOID"}, + {LDAPProperties.CertTemplateOID, "1.3.6.1.4.1.311.21.8.4571196.1884641.3293620.10686285.12068043.134.1.30"}, + {LDAPProperties.OIDGroupLink, null} , - }, "1E5311A8-E949-4E02-8E08-234ED63200DE", Label.IssuancePolicy); - - var mockLDAPUtils = new MockLDAPUtils(); - var ldapPropertyProcessor = new LDAPPropertyProcessor(mockLDAPUtils); - - - var test = ldapPropertyProcessor.ReadIssuancePolicyProperties(mock); + }, "","1E5311A8-E949-4E02-8E08-234ED63200DE"); + + var mockLDAPUtils = new MockLdapUtils(); + var ldapPropertyProcessor = new LdapPropertyProcessor(mockLDAPUtils); + + var test = await ldapPropertyProcessor.ReadIssuancePolicyProperties(mock); var keys = test.Props.Keys; - + //These are not common properties Assert.DoesNotContain("domain", keys); Assert.DoesNotContain("name", keys); Assert.DoesNotContain("domainsid", keys); Assert.DoesNotContain("oidgrouplink", keys); - - Assert.Contains("description", keys); + + //Assert.Contains("description", keys); Assert.Contains("whencreated", keys); Assert.Contains("displayname", keys); Assert.Contains("certtemplateoid", keys); @@ -845,7 +847,7 @@ public void LDAPPropertyProcessor_ReadIssuancePolicyProperties_NoOIDGroupLink() [Fact] public void LDAPPropertyProcessor_ParseAllProperties() { - var mock = new MockSearchResultEntry("CN\u003dNTAUTHCERTIFICATES,CN\u003dPUBLIC KEY SERVICES,CN\u003dSERVICES,CN\u003dCONFIGURATION,DC\u003dDUMPSTER,DC\u003dFIRE", + var mock = new MockDirectoryObject("CN\u003dNTAUTHCERTIFICATES,CN\u003dPUBLIC KEY SERVICES,CN\u003dSERVICES,CN\u003dCONFIGURATION,DC\u003dDUMPSTER,DC\u003dFIRE", new Dictionary { {"description", null}, @@ -853,9 +855,9 @@ public void LDAPPropertyProcessor_ParseAllProperties() {"name", "NTAUTHCERTIFICATES@DUMPSTER.FIRE"}, {"domainsid", "S-1-5-21-2697957641-2271029196-387917394"}, {"whencreated", 1683986131}, - }, "2F9F3630-F46A-49BF-B186-6629994EBCF9", Label.NTAuthStore); + }, "","2F9F3630-F46A-49BF-B186-6629994EBCF9"); - var processor = new LDAPPropertyProcessor(new MockLDAPUtils()); + var processor = new LdapPropertyProcessor(new MockLdapUtils()); var props = processor.ParseAllProperties(mock); var keys = props.Keys; @@ -871,11 +873,11 @@ public void LDAPPropertyProcessor_ParseAllProperties() [Fact] public void LDAPPropertyProcessor_ParseAllProperties_NoProperties() { - var mock = new MockSearchResultEntry("CN\u003dNTAUTHCERTIFICATES,CN\u003dPUBLIC KEY SERVICES,CN\u003dSERVICES,CN\u003dCONFIGURATION,DC\u003dDUMPSTER,DC\u003dFIRE", + var mock = new MockDirectoryObject("CN\u003dNTAUTHCERTIFICATES,CN\u003dPUBLIC KEY SERVICES,CN\u003dSERVICES,CN\u003dCONFIGURATION,DC\u003dDUMPSTER,DC\u003dFIRE", new Dictionary - { }, "2F9F3630-F46A-49BF-B186-6629994EBCF9", Label.NTAuthStore); + { }, "","2F9F3630-F46A-49BF-B186-6629994EBCF9"); - var processor = new LDAPPropertyProcessor(new MockLDAPUtils()); + var processor = new LdapPropertyProcessor(new MockLdapUtils()); var props = processor.ParseAllProperties(mock); var keys = props.Keys; @@ -886,11 +888,11 @@ public void LDAPPropertyProcessor_ParseAllProperties_NoProperties() [Fact] public void LDAPPropertyProcessor_ParseAllProperties_CollectionCountOne_NullString() { - var mock = new MockSearchResultEntry("CN\u003dNTAUTHCERTIFICATES,CN\u003dPUBLIC KEY SERVICES,CN\u003dSERVICES,CN\u003dCONFIGURATION,DC\u003dDUMPSTER,DC\u003dFIRE", + var mock = new MockDirectoryObject("CN\u003dNTAUTHCERTIFICATES,CN\u003dPUBLIC KEY SERVICES,CN\u003dSERVICES,CN\u003dCONFIGURATION,DC\u003dDUMPSTER,DC\u003dFIRE", new Dictionary - {{"domainsid", null} }, "2F9F3630-F46A-49BF-B186-6629994EBCF9", Label.NTAuthStore); + {{"domainsid", null} }, "","2F9F3630-F46A-49BF-B186-6629994EBCF9"); - var processor = new LDAPPropertyProcessor(new MockLDAPUtils()); + var processor = new LdapPropertyProcessor(new MockLdapUtils()); var props = processor.ParseAllProperties(mock); var keys = props.Keys; @@ -900,11 +902,11 @@ public void LDAPPropertyProcessor_ParseAllProperties_CollectionCountOne_NullStri [Fact] public void LDAPPropertyProcessor_ParseAllProperties_CollectionCountOne_BadPasswordTime() { - var mock = new MockSearchResultEntry("CN\u003dNTAUTHCERTIFICATES,CN\u003dPUBLIC KEY SERVICES,CN\u003dSERVICES,CN\u003dCONFIGURATION,DC\u003dDUMPSTER,DC\u003dFIRE", + var mock = new MockDirectoryObject("CN\u003dNTAUTHCERTIFICATES,CN\u003dPUBLIC KEY SERVICES,CN\u003dSERVICES,CN\u003dCONFIGURATION,DC\u003dDUMPSTER,DC\u003dFIRE", new Dictionary - {{"badpasswordtime", "130435290000000000"} }, "2F9F3630-F46A-49BF-B186-6629994EBCF9", Label.NTAuthStore); + {{"badpasswordtime", "130435290000000000"} }, "","2F9F3630-F46A-49BF-B186-6629994EBCF9"); - var processor = new LDAPPropertyProcessor(new MockLDAPUtils()); + var processor = new LdapPropertyProcessor(new MockLdapUtils()); var props = processor.ParseAllProperties(mock); var keys = props.Keys; @@ -915,11 +917,11 @@ public void LDAPPropertyProcessor_ParseAllProperties_CollectionCountOne_BadPassw [Fact] public void LDAPPropertyProcessor_ParseAllProperties_CollectionCountOne_NotBadPasswordTime() { - var mock = new MockSearchResultEntry("CN\u003dNTAUTHCERTIFICATES,CN\u003dPUBLIC KEY SERVICES,CN\u003dSERVICES,CN\u003dCONFIGURATION,DC\u003dDUMPSTER,DC\u003dFIRE", + var mock = new MockDirectoryObject("CN\u003dNTAUTHCERTIFICATES,CN\u003dPUBLIC KEY SERVICES,CN\u003dSERVICES,CN\u003dCONFIGURATION,DC\u003dDUMPSTER,DC\u003dFIRE", new Dictionary - {{"domainsid", "S-1-5-21-2697957641-2271029196-387917394"}}, "2F9F3630-F46A-49BF-B186-6629994EBCF9", Label.NTAuthStore); + {{"domainsid", "S-1-5-21-2697957641-2271029196-387917394"}}, "","2F9F3630-F46A-49BF-B186-6629994EBCF9"); - var processor = new LDAPPropertyProcessor(new MockLDAPUtils()); + var processor = new LdapPropertyProcessor(new MockLdapUtils()); var props = processor.ParseAllProperties(mock); var keys = props.Keys; diff --git a/test/unit/LocalGroupProcessorTest.cs b/test/unit/LocalGroupProcessorTest.cs index 0310fb73..e12cf85b 100644 --- a/test/unit/LocalGroupProcessorTest.cs +++ b/test/unit/LocalGroupProcessorTest.cs @@ -27,7 +27,7 @@ public void Dispose() [WindowsOnlyFact] public async Task LocalGroupProcessor_TestWorkstation() { - var mockProcessor = new Mock(new MockLDAPUtils(), null); + var mockProcessor = new Mock(new MockLdapUtils(), null); var mockSamServer = new MockWorkstationSAMServer(); mockProcessor.Setup(x => x.OpenSamServer(It.IsAny())).Returns(mockSamServer); var processor = mockProcessor.Object; @@ -58,7 +58,7 @@ public async Task LocalGroupProcessor_TestWorkstation() [WindowsOnlyFact] public async Task LocalGroupProcessor_TestDomainController() { - var mockProcessor = new Mock(new MockLDAPUtils(), null); + var mockProcessor = new Mock(new MockLdapUtils(), null); var mockSamServer = new MockDCSAMServer(); mockProcessor.Setup(x => x.OpenSamServer(It.IsAny())).Returns(mockSamServer); var processor = mockProcessor.Object; @@ -76,15 +76,17 @@ public async Task LocalGroupProcessor_TestDomainController() [Fact] public async Task LocalGroupProcessor_ResolveGroupName_NonDC() { - var mockUtils = new Mock(); + var mockUtils = new Mock(); var proc = new LocalGroupProcessor(mockUtils.Object); - var result = TestPrivateMethod.InstanceMethod(proc, "ResolveGroupName", + var resultTask = TestPrivateMethod.InstanceMethod>(proc, "ResolveGroupName", new object[] { "ADMINISTRATORS", "WIN10.TESTLAB.LOCAL", "S-1-5-32-123-123-500", "TESTLAB.LOCAL", 544, false, false }); + var result = await resultTask; + Assert.Equal("ADMINISTRATORS@WIN10.TESTLAB.LOCAL", result.PrincipalName); ; Assert.Equal("S-1-5-32-123-123-500-544", result.ObjectId); @@ -93,15 +95,17 @@ public async Task LocalGroupProcessor_ResolveGroupName_NonDC() [Fact] public async Task LocalGroupProcessor_ResolveGroupName_DC() { - var mockUtils = new Mock(); + var mockUtils = new Mock(); var proc = new LocalGroupProcessor(mockUtils.Object); - var result = TestPrivateMethod.InstanceMethod(proc, "ResolveGroupName", + var resultTask = TestPrivateMethod.InstanceMethod>(proc, "ResolveGroupName", new object[] { "ADMINISTRATORS", "PRIMARY.TESTLAB.LOCAL", "S-1-5-32-123-123-1000", "TESTLAB.LOCAL", 544, true, true }); + var result = await resultTask; + Assert.Equal("IGNOREME", result.PrincipalName); ; Assert.Equal("TESTLAB.LOCAL-S-1-5-32-544", result.ObjectId); diff --git a/test/unit/SPNProcessorsTest.cs b/test/unit/SPNProcessorsTest.cs index 0ba7411b..fd124202 100644 --- a/test/unit/SPNProcessorsTest.cs +++ b/test/unit/SPNProcessorsTest.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using CommonLibTest.Facades; using SharpHoundCommonLib; +using SharpHoundCommonLib.Enums; using SharpHoundCommonLib.OutputTypes; using SharpHoundCommonLib.Processors; using Xunit; @@ -13,7 +14,7 @@ public class SPNProcessorsTest [Fact] public async Task ReadSPNTargets_SPNLengthZero_YieldBreak() { - var processor = new SPNProcessors(new MockLDAPUtils()); + var processor = new SPNProcessors(new MockLdapUtils()); var servicePrincipalNames = Array.Empty(); const string distinguishedName = "cn=policies,cn=system,DC=testlab,DC=local"; await foreach (var spn in processor.ReadSPNTargets(servicePrincipalNames, distinguishedName)) @@ -23,7 +24,7 @@ public async Task ReadSPNTargets_SPNLengthZero_YieldBreak() [Fact] public async Task ReadSPNTargets_NoPortSupplied_ParsedCorrectly() { - var processor = new SPNProcessors(new MockLDAPUtils()); + var processor = new SPNProcessors(new MockLdapUtils()); string[] servicePrincipalNames = {"MSSQLSvc/PRIMARY.TESTLAB.LOCAL"}; const string distinguishedName = "cn=policies,cn=system,DC=testlab,DC=local"; @@ -44,7 +45,7 @@ public async Task ReadSPNTargets_NoPortSupplied_ParsedCorrectly() [Fact] public async Task ReadSPNTargets_BadPortSupplied_ParsedCorrectly() { - var processor = new SPNProcessors(new MockLDAPUtils()); + var processor = new SPNProcessors(new MockLdapUtils()); string[] servicePrincipalNames = {"MSSQLSvc/PRIMARY.TESTLAB.LOCAL:abcd"}; const string distinguishedName = "cn=policies,cn=system,DC=testlab,DC=local"; @@ -65,7 +66,7 @@ public async Task ReadSPNTargets_BadPortSupplied_ParsedCorrectly() [Fact] public async void ReadSPNTargets_SuppliedPort_ParsedCorrectly() { - var processor = new SPNProcessors(new MockLDAPUtils()); + var processor = new SPNProcessors(new MockLdapUtils()); string[] servicePrincipalNames = {"MSSQLSvc/PRIMARY.TESTLAB.LOCAL:2345"}; const string distinguishedName = "cn=policies,cn=system,DC=testlab,DC=local"; @@ -86,7 +87,7 @@ public async void ReadSPNTargets_SuppliedPort_ParsedCorrectly() [Fact] public async void ReadSPNTargets_MissingMssqlSvc_NotRead() { - var processor = new SPNProcessors(new MockLDAPUtils()); + var processor = new SPNProcessors(new MockLdapUtils()); string[] servicePrincipalNames = {"myhost.redmond.microsoft.com:1433"}; const string distinguishedName = "CN=Jeff Smith,OU=Sales,DC=Fabrikam,DC=COM"; await foreach (var spn in processor.ReadSPNTargets(servicePrincipalNames, distinguishedName)) @@ -96,7 +97,7 @@ public async void ReadSPNTargets_MissingMssqlSvc_NotRead() [Fact] public async void ReadSPNTargets_SPNWithAddressSign_NotRead() { - var processor = new SPNProcessors(new MockLDAPUtils()); + var processor = new SPNProcessors(new MockLdapUtils()); string[] servicePrincipalNames = {"MSSQLSvc/myhost.redmond.microsoft.com:1433 user@domain"}; const string distinguishedName = "CN=Jeff Smith,OU=Sales,DC=Fabrikam,DC=COM"; await foreach (var spn in processor.ReadSPNTargets(servicePrincipalNames, distinguishedName)) diff --git a/test/unit/SearchResultEntryTests.cs b/test/unit/SearchResultEntryTests.cs deleted file mode 100644 index 8eb59319..00000000 --- a/test/unit/SearchResultEntryTests.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Collections.Generic; -using System.Security.Principal; -using CommonLibTest.Facades; -using SharpHoundCommonLib; -using SharpHoundCommonLib.Enums; -using Xunit; - -namespace CommonLibTest -{ - public class SearchResultEntryTests - { - [WindowsOnlyFact] - public void Test_GetLabelIssuanceOIDObjects() - { - var sid = new SecurityIdentifier("S-1-5-21-3130019616-2776909439-2417379446-500"); - var bsid = new byte[sid.BinaryLength]; - sid.GetBinaryForm(bsid, 0); - var attribs = new Dictionary - { - { "objectsid", bsid}, - { "objectclass", "msPKI-Enterprise-Oid" }, - { "flags", "2" } - }; - - var sre = MockableSearchResultEntry.Construct(attribs, "CN=Test,CN=OID,CN=Public Key Services,CN=Services,CN=Configuration"); - Assert.Equal(Label.IssuancePolicy, sre.GetLabel()); - - sre = MockableSearchResultEntry.Construct(attribs, "CN=OID,CN=Public Key Services,CN=Services,CN=Configuration"); - Assert.Equal(Label.Container, sre.GetLabel()); - } - } -} \ No newline at end of file diff --git a/test/unit/TestLogger.cs b/test/unit/TestLogger.cs index 8f15398f..53a10259 100644 --- a/test/unit/TestLogger.cs +++ b/test/unit/TestLogger.cs @@ -1,9 +1,11 @@ using System; +using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; using Xunit.Abstractions; namespace CommonLibTest { + [ExcludeFromCodeCoverage] public class TestLogger : ILogger { private readonly LogLevel _level; diff --git a/test/unit/UserRightsAssignmentProcessorTest.cs b/test/unit/UserRightsAssignmentProcessorTest.cs index 3bfca790..b4d4488c 100644 --- a/test/unit/UserRightsAssignmentProcessorTest.cs +++ b/test/unit/UserRightsAssignmentProcessorTest.cs @@ -24,7 +24,7 @@ public UserRightsAssignmentProcessorTest(ITestOutputHelper testOutputHelper) [WindowsOnlyFact] public async Task UserRightsAssignmentProcessor_TestWorkstation() { - var mockProcessor = new Mock(new MockLDAPUtils(), null); + var mockProcessor = new Mock(new MockLdapUtils(), null); var mockLSAPolicy = new MockWorkstationLSAPolicy(); mockProcessor.Setup(x => x.OpenLSAPolicy(It.IsAny())).Returns(mockLSAPolicy); var processor = mockProcessor.Object; @@ -46,7 +46,7 @@ public async Task UserRightsAssignmentProcessor_TestWorkstation() [WindowsOnlyFact] public async Task UserRightsAssignmentProcessor_TestDC() { - var mockProcessor = new Mock(new MockLDAPUtils(), null); + var mockProcessor = new Mock(new MockLdapUtils(), null); var mockLSAPolicy = new MockDCLSAPolicy(); mockProcessor.Setup(x => x.OpenLSAPolicy(It.IsAny())).Returns(mockLSAPolicy); var processor = mockProcessor.Object; diff --git a/test/unit/Utils.cs b/test/unit/Utils.cs new file mode 100644 index 00000000..10e9d90a --- /dev/null +++ b/test/unit/Utils.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace CommonLibTest +{ + public static class Utils + { + internal static byte[] B64ToBytes(string base64) + { + return Convert.FromBase64String(base64); + } + + internal static string B64ToString(string base64) + { + var b = B64ToBytes(base64); + return Encoding.UTF8.GetString(b); + } + } + + internal static class Extensions + { + public static async Task ToArrayAsync(this IAsyncEnumerable items) + { + var results = new List(); + await foreach (var item in items + .ConfigureAwait(false)) + results.Add(item); + return results.ToArray(); + } + + internal static bool IsArray(this object obj) + { + var valueType = obj?.GetType(); + if (valueType == null) + return false; + return valueType.IsArray; + } + + internal static IAsyncEnumerable ToAsyncEnumerable(this IEnumerable source) { + return source switch { + ICollection collection => new IAsyncEnumerableCollectionAdapter(collection), + _ => null + }; + } + + private sealed class IAsyncEnumerableCollectionAdapter : IAsyncEnumerable { + private readonly IAsyncEnumerator _enumerator; + + public IAsyncEnumerableCollectionAdapter(ICollection source) { + _enumerator = new IAsyncEnumeratorCollectionAdapter(source); + } + public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = new CancellationToken()) { + return _enumerator; + } + } + + private sealed class IAsyncEnumeratorCollectionAdapter : IAsyncEnumerator { + private readonly IEnumerable _source; + private IEnumerator _enumerator; + + public IAsyncEnumeratorCollectionAdapter(ICollection source) { + _source = source; + } + + public ValueTask DisposeAsync() { + _enumerator = null; + return ValueTask.CompletedTask; + } + + public ValueTask MoveNextAsync() { + if (_enumerator == null) { + _enumerator = _source.GetEnumerator(); + } + return ValueTask.FromResult(_enumerator.MoveNext()); + } + + public T Current => _enumerator.Current; + } + } + + public sealed class WindowsOnlyFact : FactAttribute + { + public WindowsOnlyFact() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) Skip = "Ignore on non-Windows platforms"; + } + } +} \ No newline at end of file diff --git a/test/unit/WellKnownPrincipalTest.cs b/test/unit/WellKnownPrincipalTest.cs index 73c3c419..f8043c78 100644 --- a/test/unit/WellKnownPrincipalTest.cs +++ b/test/unit/WellKnownPrincipalTest.cs @@ -6,14 +6,6 @@ namespace CommonLibTest { - public struct WKP - { - public string SID { get; set; } - public string Name { get; set; } - - public string Description { get; set; } - } - public class WellKnownPrincipalTest : IDisposable { #region Constructor(s) @@ -21,8 +13,6 @@ public class WellKnownPrincipalTest : IDisposable public WellKnownPrincipalTest(ITestOutputHelper testOutputHelper) { _testOutputHelper = testOutputHelper; - _testDomainName = "TESTLAB.LOCAL"; - _testForestName = "FOREST.LOCAL"; } #endregion @@ -88,7 +78,7 @@ private static (string sid, string name, Label label)[] GetWellKnownPrincipals() ("S-1-5-13", "Terminal Server Users", Label.Group), ("S-1-5-14", "Remote Interactive Logon", Label.Group), ("S-1-5-15", "This Organization", Label.Group), - ("S-1-5-17", "IUSR", Label.Group), + ("S-1-5-17", "IUSR", Label.User), ("S-1-5-18", "Local System", Label.User), ("S-1-5-19", "Local Service", Label.User), ("S-1-5-20", "Network Service", Label.User), @@ -131,8 +121,6 @@ private static (string sid, string name, Label label)[] GetWellKnownPrincipals() #region Private Members private readonly ITestOutputHelper _testOutputHelper; - private readonly string _testDomainName; - private readonly string _testForestName; #endregion @@ -160,4 +148,4 @@ public void GetWellKnownPrincipal_NonWellKnown_ReturnsNull() #endregion } -} \ No newline at end of file +}