diff --git a/src/CommonLib/Enums/LDAPProperties.cs b/src/CommonLib/Enums/LDAPProperties.cs index 0b202e46..25bd4a8b 100644 --- a/src/CommonLib/Enums/LDAPProperties.cs +++ b/src/CommonLib/Enums/LDAPProperties.cs @@ -36,7 +36,8 @@ 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 LAPSPlaintextPassword = "ms-laps-password"; + public const string LAPSEncryptedPassword = "ms-laps-encryptedpassword"; public const string LegacyLAPSExpirationTime = "ms-mcs-admpwdexpirationtime"; public const string LegacyLAPSPassword = "ms-mcs-admpwd"; public const string Members = "member"; diff --git a/src/CommonLib/LdapProducerQueryGenerator.cs b/src/CommonLib/LdapProducerQueryGenerator.cs index 1fc7ea73..0d6580f8 100644 --- a/src/CommonLib/LdapProducerQueryGenerator.cs +++ b/src/CommonLib/LdapProducerQueryGenerator.cs @@ -59,11 +59,6 @@ public static GeneratedLdapParameters GenerateDefaultPartitionParameters(Collect }; } - if (methods.HasFlag(CollectionMethod.Group)) { - filter = filter.AddGroups(); - properties.AddRange(CommonProperties.GroupResolutionProps); - } - if (methods.IsComputerCollectionSet()) { filter = filter.AddComputers(); properties.AddRange(CommonProperties.ComputerMethodProps); @@ -89,6 +84,11 @@ public static GeneratedLdapParameters GenerateDefaultPartitionParameters(Collect properties.AddRange(CommonProperties.ComputerMethodProps); } + if (methods.HasFlag(CollectionMethod.Group)) { + filter = filter.AddGroups(); + properties.AddRange(CommonProperties.GroupResolutionProps); + } + return new GeneratedLdapParameters { Filter = filter, Attributes = properties.Distinct().ToArray() diff --git a/src/CommonLib/LdapUtils.cs b/src/CommonLib/LdapUtils.cs index 17293d3b..a9166aff 100644 --- a/src/CommonLib/LdapUtils.cs +++ b/src/CommonLib/LdapUtils.cs @@ -168,8 +168,8 @@ public IAsyncEnumerable> PagedQuery(LdapQueryParame //pass } - using (var ctx = new PrincipalContext(ContextType.Domain)) { - try { + try { + using (var ctx = new PrincipalContext(ContextType.Domain)) { var principal = Principal.FindByIdentity(ctx, IdentityType.Sid, sid); if (principal != null) { var entry = ((DirectoryEntry)principal.GetUnderlyingObject()).ToDirectoryObject(); @@ -178,10 +178,11 @@ public IAsyncEnumerable> PagedQuery(LdapQueryParame return (true, type); } } - } catch { - //pass } + } catch { + //pass } + return (false, Label.Base); } @@ -212,8 +213,8 @@ public IAsyncEnumerable> PagedQuery(LdapQueryParame //pass } - using (var ctx = new PrincipalContext(ContextType.Domain)) { - try { + try { + using (var ctx = new PrincipalContext(ContextType.Domain)) { var principal = Principal.FindByIdentity(ctx, IdentityType.Guid, guid); if (principal != null) { var entry = ((DirectoryEntry)principal.GetUnderlyingObject()).ToDirectoryObject(); @@ -222,10 +223,11 @@ public IAsyncEnumerable> PagedQuery(LdapQueryParame return (true, type); } } - } catch { - //pass } + } catch { + //pass } + return (false, Label.Base); } @@ -313,7 +315,7 @@ public IAsyncEnumerable> PagedQuery(LdapQueryParame return (false, null); } - public async Task<(bool Success, string DomainName)> GetDomainNameFromSid(string sid) { + public virtual async Task<(bool Success, string DomainName)> GetDomainNameFromSid(string sid) { string domainSid; try { domainSid = new SecurityIdentifier(sid).AccountDomainSid?.Value.ToUpper(); @@ -345,8 +347,8 @@ public IAsyncEnumerable> PagedQuery(LdapQueryParame return (true, domainName); } - using (var ctx = new PrincipalContext(ContextType.Domain)) { - try { + try { + using (var ctx = new PrincipalContext(ContextType.Domain)) { var principal = Principal.FindByIdentity(ctx, IdentityType.Sid, sid); if (principal != null) { var dn = principal.DistinguishedName; @@ -355,10 +357,11 @@ public IAsyncEnumerable> PagedQuery(LdapQueryParame return (true, Helpers.DistinguishedNameToDomain(dn)); } } - } catch { - //pass } + } catch { + //pass } + return (false, string.Empty); } @@ -405,7 +408,7 @@ public IAsyncEnumerable> PagedQuery(LdapQueryParame return (false, string.Empty); } - public async Task<(bool Success, string DomainSid)> GetDomainSidFromDomainName(string domainName) { + public virtual async Task<(bool Success, string DomainSid)> GetDomainSidFromDomainName(string domainName) { if (Cache.GetDomainSidMapping(domainName, out var domainSid)) return (true, domainSid); try { @@ -877,8 +880,8 @@ public async Task IsDomainController(string computerObjectId, string domai return (true, principal); } - using (var ctx = new PrincipalContext(ContextType.Domain)) { - try { + try { + using (var ctx = new PrincipalContext(ContextType.Domain)) { var lookupPrincipal = Principal.FindByIdentity(ctx, IdentityType.DistinguishedName, distinguishedName); if (lookupPrincipal != null) { @@ -895,12 +898,13 @@ public async Task IsDomainController(string computerObjectId, string domai } } - return (false, default); - } catch { - _unresolvablePrincipals.Add(distinguishedName); return (false, default); } + } catch { + _unresolvablePrincipals.Add(distinguishedName); + return (false, default); } + } public async Task<(bool Success, string DSHeuristics)> GetDSHueristics(string domain, string dn) { @@ -934,7 +938,7 @@ public async IAsyncEnumerable GetWellKnownPrincipalOutput() { OutputBase output = principal.ObjectType switch { Label.User => new User(), Label.Computer => new Computer(), - Label.Group => new OutputTypes.Group(), + Label.Group => new Group(), Label.GPO => new GPO(), Label.Domain => new OutputTypes.Domain(), Label.OU => new OU(), @@ -957,7 +961,7 @@ public async IAsyncEnumerable GetWellKnownPrincipalOutput() { yield return entdc; } } - + private async IAsyncEnumerable GetEnterpriseDCGroups() { var grouped = new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase); var forestSidToName = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); @@ -968,7 +972,7 @@ await GetForest(domainName) is (true, var forestName) && await GetDomainSidFromDomainName(forestName) is (true, var forestDomainSid)) { forestSidToName.TryAdd(forestDomainSid, forestName); if (!grouped.ContainsKey(forestDomainSid)) { - grouped[forestDomainSid] = new List(); + grouped[forestDomainSid] = []; } foreach (var k in domainSid) { @@ -978,10 +982,13 @@ await GetDomainSidFromDomainName(forestName) is (true, var forestDomainSid)) { } foreach (var f in grouped) { - var group = new Group { ObjectIdentifier = $"{f.Key}-S-1-5-9" }; - group.Properties.Add("name", $"ENTERPRISE DOMAIN CONTROLLERS@{forestSidToName[f.Key]}".ToUpper()); + if (!forestSidToName.TryGetValue(f.Key, out var forestName)) { + continue; + } + var group = new Group { ObjectIdentifier = $"{forestName}-S-1-5-9" }; + group.Properties.Add("name", $"ENTERPRISE DOMAIN CONTROLLERS@{forestName}".ToUpper()); group.Properties.Add("domainsid", f.Key); - group.Properties.Add("domain", forestSidToName[f.Key]); + group.Properties.Add("domain", forestName); group.Members = f.Value.Select(x => new TypedPrincipal(x, Label.Computer)).ToArray(); yield return group; } diff --git a/src/CommonLib/OutputTypes/Computer.cs b/src/CommonLib/OutputTypes/Computer.cs index 2fbcfcf5..879013d5 100644 --- a/src/CommonLib/OutputTypes/Computer.cs +++ b/src/CommonLib/OutputTypes/Computer.cs @@ -20,6 +20,7 @@ public class Computer : OutputBase public DCRegistryData DCRegistryData { get; set; } = new(); public ComputerStatus Status { get; set; } public bool IsDC { get; set; } + public bool UnconstrainedDelegation { get; set; } public string DomainSID { get; set; } } diff --git a/src/CommonLib/OutputTypes/DomainTrust.cs b/src/CommonLib/OutputTypes/DomainTrust.cs index 9c62a82c..446df81d 100644 --- a/src/CommonLib/OutputTypes/DomainTrust.cs +++ b/src/CommonLib/OutputTypes/DomainTrust.cs @@ -9,7 +9,7 @@ public class DomainTrust public bool IsTransitive { get; set; } public bool SidFilteringEnabled { get; set; } public bool TGTDelegationEnabled { get; set; } - public string TrustAttributes { get; set; } + public long TrustAttributes { get; set; } public TrustDirection TrustDirection { get; set; } public TrustType TrustType { get; set; } } diff --git a/src/CommonLib/OutputTypes/User.cs b/src/CommonLib/OutputTypes/User.cs index 387d1f0f..7de2ce95 100644 --- a/src/CommonLib/OutputTypes/User.cs +++ b/src/CommonLib/OutputTypes/User.cs @@ -8,5 +8,7 @@ public class User : OutputBase public string PrimaryGroupSID { get; set; } public TypedPrincipal[] HasSIDHistory { get; set; } = Array.Empty(); public SPNPrivilege[] SPNTargets { get; set; } = Array.Empty(); + public bool UnconstrainedDelegation { get; set; } + public string DomainSID { get; set; } } } \ No newline at end of file diff --git a/src/CommonLib/Processors/ACLProcessor.cs b/src/CommonLib/Processors/ACLProcessor.cs index af69068a..45af8136 100644 --- a/src/CommonLib/Processors/ACLProcessor.cs +++ b/src/CommonLib/Processors/ACLProcessor.cs @@ -15,10 +15,11 @@ namespace SharpHoundCommonLib.Processors { public class ACLProcessor { private static readonly Dictionary BaseGuids; - private static readonly ConcurrentDictionary GuidMap = new(); + private readonly ConcurrentDictionary _guidMap = new(); private readonly ILogger _log; private readonly ILdapUtils _utils; - private static readonly HashSet BuiltDomainCaches = new(StringComparer.OrdinalIgnoreCase); + private readonly ConcurrentHashSet _builtDomainCaches = new(StringComparer.OrdinalIgnoreCase); + private readonly object _lock = new(); static ACLProcessor() { //Create a dictionary with the base GUIDs of each object type @@ -50,8 +51,16 @@ public ACLProcessor(ILdapUtils utils, ILogger log = null) { /// LAPS /// private async Task BuildGuidCache(string domain) { - BuiltDomainCaches.Add(domain); - await foreach (var result in _utils.Query(new LdapQueryParameters { + lock (_lock) { + if (_builtDomainCaches.Contains(domain)) { + return; + } + + _builtDomainCaches.Add(domain); + } + + _log.LogInformation("Building GUID Cache for {Domain}", domain); + await foreach (var result in _utils.PagedQuery(new LdapQueryParameters { DomainName = domain, LDAPFilter = "(schemaIDGUID=*)", NamingContext = NamingContext.Schema, @@ -59,19 +68,31 @@ private async Task BuildGuidCache(string domain) { })) { if (result.IsSuccess) { if (!result.Value.TryGetProperty(LDAPProperties.Name, out var name) || - !result.Value.TryGetGuid(out var guid)) { + !result.Value.TryGetByteProperty(LDAPProperties.SchemaIDGUID, out var schemaGuid)) { continue; } 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); + + string guid; + try + { + guid = new Guid(schemaGuid).ToString(); + } + catch + { + continue; + } + + if (name is LDAPProperties.LAPSPlaintextPassword or LDAPProperties.LAPSEncryptedPassword or LDAPProperties.LegacyLAPSPassword) { + _log.LogInformation("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); } } + } /// @@ -125,17 +146,21 @@ internal static string CalculateInheritanceHash(string identityReference, Active string aceType, string inheritedObjectType) { var hash = identityReference + rights + aceType + inheritedObjectType; /* - * We're using MD5 because its fast and this data isn't cryptographically important. + * We're using SHA1 because its fast and this data isn't cryptographically important. * Additionally, the chances of a collision in our data size is miniscule and irrelevant. + * We cannot use MD5 as it is not FIPS compliant and environments can enforce this setting */ - 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")); + try + { + using (var sha1 = SHA1.Create()) + { + var bytes = sha1.ComputeHash(Encoding.UTF8.GetBytes(hash)); + return BitConverter.ToString(bytes).Replace("-", string.Empty).ToUpper(); } - - return builder.ToString(); + } + catch + { + return ""; } } @@ -199,8 +224,12 @@ public IEnumerable GetInheritedAceHashes(byte[] ntSecurityDescriptor, st //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); + + var hash = CalculateInheritanceHash(ir, aceRights, aceType, inheritanceType); + if (!string.IsNullOrEmpty(hash)) + { + yield return hash; + } } } @@ -217,9 +246,7 @@ public IEnumerable GetInheritedAceHashes(byte[] ntSecurityDescriptor, st public async IAsyncEnumerable ProcessACL(byte[] ntSecurityDescriptor, string objectDomain, Label objectType, bool hasLaps, string objectName = "") { - if (!BuiltDomainCaches.Contains(objectDomain)) { - await BuildGuidCache(objectDomain); - } + await BuildGuidCache(objectDomain); if (ntSecurityDescriptor == null) { _log.LogDebug("Security Descriptor is null for {Name}", objectName); @@ -245,7 +272,8 @@ public async IAsyncEnumerable ProcessACL(byte[] ntSecurityDescriptor, strin PrincipalType = resolvedOwner.ObjectType, PrincipalSID = resolvedOwner.ObjectIdentifier, RightName = EdgeNames.Owns, - IsInherited = false + IsInherited = false, + InheritanceHash = "" }; } else { _log.LogTrace("Failed to resolve owner for {Name}", objectName); @@ -253,7 +281,8 @@ public async IAsyncEnumerable ProcessACL(byte[] ntSecurityDescriptor, strin PrincipalType = Label.Base, PrincipalSID = ownerSid, RightName = EdgeNames.Owns, - IsInherited = false + IsInherited = false, + InheritanceHash = "" }; } } @@ -288,8 +317,6 @@ public async IAsyncEnumerable ProcessACL(byte[] ntSecurityDescriptor, strin 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); @@ -402,14 +429,23 @@ public async IAsyncEnumerable ProcessACL(byte[] ntSecurityDescriptor, strin RightName = EdgeNames.AllExtendedRights, InheritanceHash = aceInheritanceHash }; - else if (mappedGuid is LDAPProperties.LegacyLAPSPassword or LDAPProperties.LAPSPassword) - yield return new ACE { - PrincipalType = resolvedPrincipal.ObjectType, - PrincipalSID = resolvedPrincipal.ObjectIdentifier, - IsInherited = inherited, - RightName = EdgeNames.ReadLAPSPassword, - InheritanceHash = aceInheritanceHash - }; + else if (_guidMap.TryGetValue(aceType, out var lapsAttribute)) + { + // Compare the retrieved attribute name against LDAPProperties values + if (lapsAttribute == LDAPProperties.LegacyLAPSPassword || + lapsAttribute == LDAPProperties.LAPSPlaintextPassword || + lapsAttribute == LDAPProperties.LAPSEncryptedPassword) + { + yield return new ACE + { + PrincipalType = resolvedPrincipal.ObjectType, + PrincipalSID = resolvedPrincipal.ObjectIdentifier, + IsInherited = inherited, + RightName = EdgeNames.ReadLAPSPassword, + InheritanceHash = aceInheritanceHash + }; + } + } } } else if (objectType == Label.CertTemplate) { if (aceType is ACEGuids.AllGuid or "") diff --git a/src/CommonLib/Processors/DomainTrustProcessor.cs b/src/CommonLib/Processors/DomainTrustProcessor.cs index 1df373ec..42ab5862 100644 --- a/src/CommonLib/Processors/DomainTrustProcessor.cs +++ b/src/CommonLib/Processors/DomainTrustProcessor.cs @@ -71,7 +71,7 @@ public async IAsyncEnumerable EnumerateDomainTrusts(string domain) continue; } - trust.TrustAttributes = ta.ToString(); + trust.TrustAttributes = ta; attributes = (TrustAttributes) ta; trust.IsTransitive = !attributes.HasFlag(TrustAttributes.NonTransitive); @@ -86,8 +86,9 @@ public async IAsyncEnumerable EnumerateDomainTrusts(string domain) trust.TGTDelegationEnabled = !attributes.HasFlag(TrustAttributes.QuarantinedDomain) && - (attributes.HasFlag(TrustAttributes.CrossOrganizationEnableTGTDelegation) - || !attributes.HasFlag(TrustAttributes.CrossOrganizationNoTGTDelegation)); + (attributes.HasFlag(TrustAttributes.WithinForest) || + attributes.HasFlag(TrustAttributes.CrossOrganizationEnableTGTDelegation)); + trust.TrustType = TrustAttributesToType(attributes); yield return trust; @@ -111,4 +112,4 @@ public static TrustType TrustAttributesToType(TrustAttributes attributes) return trustType; } } -} \ No newline at end of file +} diff --git a/src/CommonLib/Processors/LdapPropertyProcessor.cs b/src/CommonLib/Processors/LdapPropertyProcessor.cs index 8577f0fe..852619f7 100644 --- a/src/CommonLib/Processors/LdapPropertyProcessor.cs +++ b/src/CommonLib/Processors/LdapPropertyProcessor.cs @@ -57,17 +57,33 @@ private static Dictionary GetCommonProps(IDirectoryObject entry) /// /// /// - public async Task> ReadDomainProperties(IDirectoryObject entry, string domain) - { + public async Task> ReadDomainProperties(IDirectoryObject entry, string domain) { var props = GetCommonProps(entry); + if (entry.TryGetProperty(LDAPProperties.ExpirePasswordsOnSmartCardOnlyAccounts, out var expirePassword) && + bool.TryParse(expirePassword, out var expirePasswordBool)) { + props.Add("expirepasswordsonsmartcardonlyaccounts", expirePasswordBool); + } + + if (entry.TryGetLongProperty(LDAPProperties.MachineAccountQuota, out var machineAccountQuota)) { + props.Add("machineaccountquota", machineAccountQuota); + } - props.Add("expirepasswordsonsmartcardonlyaccounts", entry.GetProperty(LDAPProperties.ExpirePasswordsOnSmartCardOnlyAccounts)); - props.Add("machineaccountquota", entry.GetProperty(LDAPProperties.MachineAccountQuota)); - props.Add("minpwdlength", entry.GetProperty(LDAPProperties.MinPwdLength)); - props.Add("pwdproperties", entry.GetProperty(LDAPProperties.PwdProperties)); - props.Add("pwdhistorylength", entry.GetProperty(LDAPProperties.PwdHistoryLength)); - props.Add("lockoutthreshold", entry.GetProperty(LDAPProperties.LockoutThreshold)); + if (entry.TryGetLongProperty(LDAPProperties.MinPwdLength, out var minPwdLength)) { + props.Add("minpwdlength", minPwdLength); + } + + if (entry.TryGetLongProperty(LDAPProperties.PwdProperties, out var pwdProperties)) { + props.Add("pwdproperties", pwdProperties); + } + + if (entry.TryGetLongProperty(LDAPProperties.PwdHistoryLength, out var pwdHistoryLength)) { + props.Add("pwdhistorylength", pwdHistoryLength); + } + + if (entry.TryGetLongProperty(LDAPProperties.LockoutThreshold, out var lockoutThreshold)) { + props.Add("lockoutthreshold", lockoutThreshold); + } if (entry.TryGetLongProperty(LDAPProperties.MinPwdAge, out var minpwdage)) { var duration = ConvertNanoDuration(minpwdage); @@ -75,27 +91,32 @@ public async Task> ReadDomainProperties(IDirectoryObj props.Add("minpwdage", duration); } } + if (entry.TryGetLongProperty(LDAPProperties.MaxPwdAge, out var maxpwdage)) { var duration = ConvertNanoDuration(maxpwdage); if (duration != null) { props.Add("maxpwdage", duration); } } + if (entry.TryGetLongProperty(LDAPProperties.LockoutDuration, out var lockoutduration)) { var duration = ConvertNanoDuration(lockoutduration); if (duration != null) { props.Add("lockoutduration", duration); } } + if (entry.TryGetLongProperty(LDAPProperties.LockOutObservationWindow, out var lockoutobservationwindow)) { var duration = ConvertNanoDuration(lockoutobservationwindow); if (duration != null) { props.Add("lockoutobservationwindow", lockoutobservationwindow); } } + if (!entry.TryGetLongProperty(LDAPProperties.DomainFunctionalLevel, out var functionalLevel)) { functionalLevel = -1; } + props.Add("functionallevel", FunctionalLevelToString((int)functionalLevel)); var dn = entry.GetProperty(LDAPProperties.DistinguishedName); @@ -206,6 +227,8 @@ public async Task ReadUserProperties(IDirectoryObject entry, str props.Add("passwordcantchange", uacFlags.HasFlag(UacFlags.PasswordCantChange)); props.Add("passwordexpired", uacFlags.HasFlag(UacFlags.PasswordExpired)); + userProps.UnconstrainedDelegation = uacFlags.HasFlag(UacFlags.TrustedForDelegation); + var comps = new List(); if (uacFlags.HasFlag(UacFlags.TrustedToAuthForDelegation) && entry.TryGetArrayProperty(LDAPProperties.AllowedToDelegateTo, out var delegates)) { @@ -321,6 +344,8 @@ public async Task ReadComputerProperties(IDirectoryObject en props.Add("lockedout", flags.HasFlag(UacFlags.Lockout)); props.Add("passwordexpired", flags.HasFlag(UacFlags.PasswordExpired)); + compProps.UnconstrainedDelegation = flags.HasFlag(UacFlags.TrustedForDelegation); + var encryptionTypes = ConvertEncryptionTypes(entry.GetProperty(LDAPProperties.SupportedEncryptionTypes)); props.Add("supportedencryptiontypes", encryptionTypes); @@ -619,7 +644,7 @@ public Dictionary ParseAllProperties(IDirectoryObject entry) { if (testBytes == null || testBytes.Length == 0) { continue; } - + // SIDs try { var sid = new SecurityIdentifier(testBytes, 0); @@ -703,8 +728,7 @@ private static object BestGuessConvert(string value) { return value; } - private static List ConvertEncryptionTypes(string encryptionTypes) - { + private static List ConvertEncryptionTypes(string encryptionTypes) { if (encryptionTypes == null) { return null; } @@ -715,36 +739,36 @@ private static List ConvertEncryptionTypes(string encryptionTypes) supportedEncryptionTypes.Add("Not defined"); } - if ((encryptionTypesInt & KerberosEncryptionTypes.DES_CBC_CRC) == KerberosEncryptionTypes.DES_CBC_CRC) - { + if ((encryptionTypesInt & KerberosEncryptionTypes.DES_CBC_CRC) == KerberosEncryptionTypes.DES_CBC_CRC) { supportedEncryptionTypes.Add("DES-CBC-CRC"); } - if ((encryptionTypesInt & KerberosEncryptionTypes.DES_CBC_MD5) == KerberosEncryptionTypes.DES_CBC_MD5) - { + + if ((encryptionTypesInt & KerberosEncryptionTypes.DES_CBC_MD5) == KerberosEncryptionTypes.DES_CBC_MD5) { supportedEncryptionTypes.Add("DES-CBC-MD5"); } - if ((encryptionTypesInt & KerberosEncryptionTypes.RC4_HMAC_MD5) == KerberosEncryptionTypes.RC4_HMAC_MD5) - { + + if ((encryptionTypesInt & KerberosEncryptionTypes.RC4_HMAC_MD5) == KerberosEncryptionTypes.RC4_HMAC_MD5) { supportedEncryptionTypes.Add("RC4-HMAC-MD5"); } - if ((encryptionTypesInt & KerberosEncryptionTypes.AES128_CTS_HMAC_SHA1_96) == KerberosEncryptionTypes.AES128_CTS_HMAC_SHA1_96) - { + + if ((encryptionTypesInt & KerberosEncryptionTypes.AES128_CTS_HMAC_SHA1_96) == + KerberosEncryptionTypes.AES128_CTS_HMAC_SHA1_96) { supportedEncryptionTypes.Add("AES128-CTS-HMAC-SHA1-96"); } - if ((encryptionTypesInt & KerberosEncryptionTypes.AES256_CTS_HMAC_SHA1_96) == KerberosEncryptionTypes.AES256_CTS_HMAC_SHA1_96) - { + + if ((encryptionTypesInt & KerberosEncryptionTypes.AES256_CTS_HMAC_SHA1_96) == + KerberosEncryptionTypes.AES256_CTS_HMAC_SHA1_96) { supportedEncryptionTypes.Add("AES256-CTS-HMAC-SHA1-96"); } return supportedEncryptionTypes; } - private static string ConvertNanoDuration(long duration) - { + private static string ConvertNanoDuration(long duration) { // In case duration is long.MinValue, Math.Abs will overflow. Value represents Forever or Never if (duration == long.MinValue) { return "Forever"; - // And if the value is positive, it indicates an error code + // And if the value is positive, it indicates an error code } else if (duration > 0) { return null; } @@ -757,20 +781,19 @@ private static string ConvertNanoDuration(long duration) List timeComponents = new List(); // Add each time component if it's greater than zero - if (durationSpan.Days > 0) - { + if (durationSpan.Days > 0) { timeComponents.Add($"{durationSpan.Days} {(durationSpan.Days == 1 ? "day" : "days")}"); } - if (durationSpan.Hours > 0) - { + + if (durationSpan.Hours > 0) { timeComponents.Add($"{durationSpan.Hours} {(durationSpan.Hours == 1 ? "hour" : "hours")}"); } - if (durationSpan.Minutes > 0) - { + + if (durationSpan.Minutes > 0) { timeComponents.Add($"{durationSpan.Minutes} {(durationSpan.Minutes == 1 ? "minute" : "minutes")}"); } - if (durationSpan.Seconds > 0) - { + + if (durationSpan.Seconds > 0) { timeComponents.Add($"{durationSpan.Seconds} {(durationSpan.Seconds == 1 ? "second" : "seconds")}"); } @@ -884,10 +907,12 @@ public ParsedCertificate(byte[] rawCertificate) { foreach (var cert in chain.ChainElements) temp.Add(cert.Certificate.Thumbprint); Chain = temp.ToArray(); } catch (Exception e) { - Logging.LogProvider.CreateLogger("ParsedCertificate").LogWarning(e, "Failed to read certificate chain for certificate {Name} with Algo {Algorithm}", name, parsedCertificate.SignatureAlgorithm.FriendlyName); + Logging.LogProvider.CreateLogger("ParsedCertificate").LogWarning(e, + "Failed to read certificate chain for certificate {Name} with Algo {Algorithm}", name, + parsedCertificate.SignatureAlgorithm.FriendlyName); Chain = Array.Empty(); } - + // Extensions var extensions = parsedCertificate.Extensions; @@ -908,6 +933,7 @@ public class UserProperties { public Dictionary Props { get; set; } = new(); public TypedPrincipal[] AllowedToDelegate { get; set; } = Array.Empty(); public TypedPrincipal[] SidHistory { get; set; } = Array.Empty(); + public bool UnconstrainedDelegation { get; set; } } public class ComputerProperties { @@ -916,6 +942,7 @@ public class ComputerProperties { public TypedPrincipal[] AllowedToAct { get; set; } = Array.Empty(); public TypedPrincipal[] SidHistory { get; set; } = Array.Empty(); public TypedPrincipal[] DumpSMSAPassword { get; set; } = Array.Empty(); + public bool UnconstrainedDelegation { get; set; } } public class IssuancePolicyProperties { diff --git a/src/CommonLib/SharpHoundCommonLib.csproj b/src/CommonLib/SharpHoundCommonLib.csproj index 85cffc92..fd5d7aa0 100644 --- a/src/CommonLib/SharpHoundCommonLib.csproj +++ b/src/CommonLib/SharpHoundCommonLib.csproj @@ -9,7 +9,7 @@ Common library for C# BloodHound enumeration tasks GPL-3.0-only https://github.com/BloodHoundAD/SharpHoundCommon - 4.0.7 + 4.0.9 SharpHoundCommonLib SharpHoundCommonLib diff --git a/test/unit/ACLProcessorTest.cs b/test/unit/ACLProcessorTest.cs index f002a389..5b367205 100644 --- a/test/unit/ACLProcessorTest.cs +++ b/test/unit/ACLProcessorTest.cs @@ -62,7 +62,7 @@ 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())) + mockUtils.Setup(x => x.PagedQuery(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)); @@ -212,8 +212,13 @@ public async Task ACLProcessor_ProcessGMSAReaders_Null_PrincipalID() { } [Fact] - public async Task ACLProcessor_ProcessACL_Null_NTSecurityDescriptor() { - var processor = new ACLProcessor(new MockLdapUtils()); + public async Task ACLProcessor_ProcessACL_Null_NTSecurityDescriptor() + { + var mock = new Mock(); + mock.Setup(x => x.PagedQuery(It.IsAny(), It.IsAny())) + .Returns(AsyncEnumerable.Empty>()); + var processor = new ACLProcessor(mock.Object); + var result = await processor.ProcessACL(null, _testDomainName, Label.User, false).ToArrayAsync(); Assert.Empty(result); @@ -236,7 +241,7 @@ public async Task ACLProcessor_ProcessACL_Yields_Owns_ACE() { .ReturnsAsync((true, new TypedPrincipal(expectedSID, expectedPrincipalType))); var mockData = new[] { LdapResult.Fail() }; - mockLDAPUtils.Setup(x => x.Query(It.IsAny(), It.IsAny())) + mockLDAPUtils.Setup(x => x.PagedQuery(It.IsAny(), It.IsAny())) .Returns(mockData.ToAsyncEnumerable()); var processor = new ACLProcessor(mockLDAPUtils.Object); @@ -261,6 +266,8 @@ public async Task ACLProcessor_ProcessACL_Null_SID() { .Returns(collection); mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); + mockLDAPUtils.Setup(x => x.PagedQuery(It.IsAny(), It.IsAny())) + .Returns(AsyncEnumerable.Empty>()); var processor = new ACLProcessor(mockLDAPUtils.Object); var bytes = Utils.B64ToBytes(UnProtectedUserNtSecurityDescriptor); @@ -279,6 +286,8 @@ public async Task ACLProcessor_ProcessACL_Null_ACE() { .Returns(collection); mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); + mockLDAPUtils.Setup(x => x.PagedQuery(It.IsAny(), It.IsAny())) + .Returns(AsyncEnumerable.Empty>()); var processor = new ACLProcessor(mockLDAPUtils.Object); var bytes = Utils.B64ToBytes(UnProtectedUserNtSecurityDescriptor); @@ -300,6 +309,8 @@ public async Task ACLProcessor_ProcessACL_Deny_ACE() { .Returns(collection); mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); + mockLDAPUtils.Setup(x => x.PagedQuery(It.IsAny(), It.IsAny())) + .Returns(AsyncEnumerable.Empty>()); var processor = new ACLProcessor(mockLDAPUtils.Object); var bytes = Utils.B64ToBytes(UnProtectedUserNtSecurityDescriptor); @@ -322,6 +333,8 @@ public async Task ACLProcessor_ProcessACL_Unmatched_Inheritance_ACE() { .Returns(collection); mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); + mockLDAPUtils.Setup(x => x.PagedQuery(It.IsAny(), It.IsAny())) + .Returns(AsyncEnumerable.Empty>()); var processor = new ACLProcessor(mockLDAPUtils.Object); var bytes = Utils.B64ToBytes(UnProtectedUserNtSecurityDescriptor); @@ -345,6 +358,8 @@ public async Task ACLProcessor_ProcessACL_Null_SID_ACE() { .Returns(collection); mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); + mockLDAPUtils.Setup(x => x.PagedQuery(It.IsAny(), It.IsAny())) + .Returns(AsyncEnumerable.Empty>()); var processor = new ACLProcessor(mockLDAPUtils.Object); var bytes = Utils.B64ToBytes(UnProtectedUserNtSecurityDescriptor); @@ -377,7 +392,7 @@ public async Task ACLProcessor_ProcessACL_GenericAll_Unmatched_Guid() { mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) .ReturnsAsync((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType))); var mockData = new[] { LdapResult.Fail() }; - mockLDAPUtils.Setup(x => x.Query(It.IsAny(), It.IsAny())) + mockLDAPUtils.Setup(x => x.PagedQuery(It.IsAny(), It.IsAny())) .Returns(mockData.ToAsyncEnumerable()); var processor = new ACLProcessor(mockLDAPUtils.Object); @@ -410,7 +425,7 @@ public async Task ACLProcessor_ProcessACL_GenericAll() { mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) .ReturnsAsync((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType))); var mockData = new[] { LdapResult.Fail() }; - mockLDAPUtils.Setup(x => x.Query(It.IsAny(), It.IsAny())) + mockLDAPUtils.Setup(x => x.PagedQuery(It.IsAny(), It.IsAny())) .Returns(mockData.ToAsyncEnumerable()); var processor = new ACLProcessor(mockLDAPUtils.Object); @@ -449,7 +464,7 @@ public async Task ACLProcessor_ProcessACL_WriteDacl() { mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) .ReturnsAsync((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType))); var mockData = new[] { LdapResult.Fail() }; - mockLDAPUtils.Setup(x => x.Query(It.IsAny(), It.IsAny())) + mockLDAPUtils.Setup(x => x.PagedQuery(It.IsAny(), It.IsAny())) .Returns(mockData.ToAsyncEnumerable()); var processor = new ACLProcessor(mockLDAPUtils.Object); @@ -488,7 +503,7 @@ public async Task ACLProcessor_ProcessACL_WriteOwner() { mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) .ReturnsAsync((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType))); var mockData = new[] { LdapResult.Fail() }; - mockLDAPUtils.Setup(x => x.Query(It.IsAny(), It.IsAny())) + mockLDAPUtils.Setup(x => x.PagedQuery(It.IsAny(), It.IsAny())) .Returns(mockData.ToAsyncEnumerable()); var processor = new ACLProcessor(mockLDAPUtils.Object); @@ -527,7 +542,7 @@ public async Task ACLProcessor_ProcessACL_Self() { mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) .ReturnsAsync((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType))); var mockData = new[] { LdapResult.Fail() }; - mockLDAPUtils.Setup(x => x.Query(It.IsAny(), It.IsAny())) + mockLDAPUtils.Setup(x => x.PagedQuery(It.IsAny(), It.IsAny())) .Returns(mockData.ToAsyncEnumerable()); var processor = new ACLProcessor(mockLDAPUtils.Object); @@ -565,7 +580,7 @@ public async Task ACLProcessor_ProcessACL_ExtendedRight_Domain_Unmatched() { mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) .ReturnsAsync((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType))); var mockData = new[] { LdapResult.Fail() }; - mockLDAPUtils.Setup(x => x.Query(It.IsAny(), It.IsAny())) + mockLDAPUtils.Setup(x => x.PagedQuery(It.IsAny(), It.IsAny())) .Returns(mockData.ToAsyncEnumerable()); var processor = new ACLProcessor(mockLDAPUtils.Object); @@ -598,7 +613,7 @@ public async Task ACLProcessor_ProcessACL_ExtendedRight_Domain_DSReplicationGetC mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) .ReturnsAsync((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType))); - mockLDAPUtils.Setup(x => x.Query(It.IsAny(), It.IsAny())) + mockLDAPUtils.Setup(x => x.PagedQuery(It.IsAny(), It.IsAny())) .Returns(Array.Empty>().ToAsyncEnumerable); var processor = new ACLProcessor(mockLDAPUtils.Object); @@ -636,7 +651,7 @@ public async Task ACLProcessor_ProcessACL_ExtendedRight_Domain_All() { mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) .ReturnsAsync((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType))); - mockLDAPUtils.Setup(x => x.Query(It.IsAny(), It.IsAny())) + mockLDAPUtils.Setup(x => x.PagedQuery(It.IsAny(), It.IsAny())) .Returns(Array.Empty>().ToAsyncEnumerable); var processor = new ACLProcessor(mockLDAPUtils.Object); @@ -675,9 +690,9 @@ public async Task ACLProcessor_ProcessACL_ExtendedRight_Domain_DSReplicationGetC mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) .ReturnsAsync((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType))); var mockData = new[] { LdapResult.Fail() }; - mockLDAPUtils.Setup(x => x.Query(It.IsAny(), It.IsAny())) + mockLDAPUtils.Setup(x => x.PagedQuery(It.IsAny(), It.IsAny())) .Returns(mockData.ToAsyncEnumerable()); - mockLDAPUtils.Setup(x => x.Query(It.IsAny(), It.IsAny())) + mockLDAPUtils.Setup(x => x.PagedQuery(It.IsAny(), It.IsAny())) .Returns(Array.Empty>().ToAsyncEnumerable); var processor = new ACLProcessor(mockLDAPUtils.Object); @@ -715,7 +730,7 @@ public async Task ACLProcessor_ProcessACL_ExtendedRight_User_Unmatched() { mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) .ReturnsAsync((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType))); - mockLDAPUtils.Setup(x => x.Query(It.IsAny(), It.IsAny())) + mockLDAPUtils.Setup(x => x.PagedQuery(It.IsAny(), It.IsAny())) .Returns(Array.Empty>().ToAsyncEnumerable); var processor = new ACLProcessor(mockLDAPUtils.Object); @@ -748,7 +763,7 @@ public async Task ACLProcessor_ProcessACL_ExtendedRight_User_UserForceChangePass mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) .ReturnsAsync((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType))); - mockLDAPUtils.Setup(x => x.Query(It.IsAny(), It.IsAny())) + mockLDAPUtils.Setup(x => x.PagedQuery(It.IsAny(), It.IsAny())) .Returns(Array.Empty>().ToAsyncEnumerable); var processor = new ACLProcessor(mockLDAPUtils.Object); @@ -786,7 +801,7 @@ public async Task ACLProcessor_ProcessACL_ExtendedRight_User_All() { mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) .ReturnsAsync((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType))); - mockLDAPUtils.Setup(x => x.Query(It.IsAny(), It.IsAny())) + mockLDAPUtils.Setup(x => x.PagedQuery(It.IsAny(), It.IsAny())) .Returns(Array.Empty>().ToAsyncEnumerable); var processor = new ACLProcessor(mockLDAPUtils.Object); @@ -823,7 +838,7 @@ public async Task ACLProcessor_ProcessACL_ExtendedRight_Computer_NoLAPS() { mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) .ReturnsAsync((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType))); - mockLDAPUtils.Setup(x => x.Query(It.IsAny(), It.IsAny())) + mockLDAPUtils.Setup(x => x.PagedQuery(It.IsAny(), It.IsAny())) .Returns(Array.Empty>().ToAsyncEnumerable); var processor = new ACLProcessor(mockLDAPUtils.Object); @@ -856,7 +871,7 @@ public async Task ACLProcessor_ProcessACL_ExtendedRight_Computer_All() { mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) .ReturnsAsync((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType))); - mockLDAPUtils.Setup(x => x.Query(It.IsAny(), It.IsAny())) + mockLDAPUtils.Setup(x => x.PagedQuery(It.IsAny(), It.IsAny())) .Returns(Array.Empty>().ToAsyncEnumerable); var processor = new ACLProcessor(mockLDAPUtils.Object); @@ -893,7 +908,7 @@ public async Task ACLProcessor_ProcessACL_GenericWrite_Unmatched() { mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) .ReturnsAsync((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType))); - mockLDAPUtils.Setup(x => x.Query(It.IsAny(), It.IsAny())) + mockLDAPUtils.Setup(x => x.PagedQuery(It.IsAny(), It.IsAny())) .Returns(Array.Empty>().ToAsyncEnumerable); var processor = new ACLProcessor(mockLDAPUtils.Object); @@ -926,7 +941,7 @@ public async Task ACLProcessor_ProcessACL_GenericWrite_User_All() { mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) .ReturnsAsync((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType))); - mockLDAPUtils.Setup(x => x.Query(It.IsAny(), It.IsAny())) + mockLDAPUtils.Setup(x => x.PagedQuery(It.IsAny(), It.IsAny())) .Returns(Array.Empty>().ToAsyncEnumerable); var processor = new ACLProcessor(mockLDAPUtils.Object); @@ -964,7 +979,7 @@ public async Task ACLProcessor_ProcessACL_GenericWrite_User_WriteMember() { mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) .ReturnsAsync((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType))); - mockLDAPUtils.Setup(x => x.Query(It.IsAny(), It.IsAny())) + mockLDAPUtils.Setup(x => x.PagedQuery(It.IsAny(), It.IsAny())) .Returns(Array.Empty>().ToAsyncEnumerable); var processor = new ACLProcessor(mockLDAPUtils.Object); @@ -1004,7 +1019,7 @@ public async Task ACLProcessor_ProcessACL_GenericWrite_Computer_WriteAllowedToAc mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) .ReturnsAsync((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType))); - mockLDAPUtils.Setup(x => x.Query(It.IsAny(), It.IsAny())) + mockLDAPUtils.Setup(x => x.PagedQuery(It.IsAny(), It.IsAny())) .Returns(Array.Empty>().ToAsyncEnumerable); var processor = new ACLProcessor(mockLDAPUtils.Object); @@ -1018,6 +1033,57 @@ public async Task ACLProcessor_ProcessACL_GenericWrite_Computer_WriteAllowedToAc Assert.False(actual.IsInherited); Assert.Equal(actual.RightName, expectedRightName); } + + [Fact] + public async Task ACLProcessor_ProcessACL_LAPS_Computer() { + var expectedPrincipalType = Label.Group; + var expectedPrincipalSID = "S-1-5-21-3130019616-2776909439-2417379446-512"; + var expectedRightName = EdgeNames.ReadLAPSPassword; + + var mockLDAPUtils = new Mock(); + var mockSecurityDescriptor = new Mock(MockBehavior.Loose, null); + var mockRule = new Mock(MockBehavior.Loose, null); + 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.ExtendedRight); + var lapsGuid = Guid.NewGuid(); + mockRule.Setup(x => x.ObjectType()).Returns(lapsGuid); + collection.Add(mockRule.Object); + + 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); + mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) + .ReturnsAsync((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType))); + + //Return a directory object from pagedquery for the schemaid to simulate LAPS + var searchResults = new[] + { + LdapResult.Ok(new MockDirectoryObject( + "abc123" + , new Dictionary() + { + {LDAPProperties.SchemaIDGUID, lapsGuid.ToByteArray()}, + {LDAPProperties.Name, LDAPProperties.LegacyLAPSPassword} + }, null,null)), + }; + mockLDAPUtils.Setup(x => x.PagedQuery(It.IsAny(), It.IsAny())) + .Returns(searchResults.ToAsyncEnumerable); + + 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.False(actual.IsInherited); + Assert.Equal(actual.RightName, expectedRightName); + } [Fact] public void GetInheritedAceHashes_NullSD_Empty() { diff --git a/test/unit/CommonLibTest.csproj b/test/unit/CommonLibTest.csproj index 717d5c93..cd4ca2f3 100644 --- a/test/unit/CommonLibTest.csproj +++ b/test/unit/CommonLibTest.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 false true ..\..\docfx\coverage\ diff --git a/test/unit/LDAPUtilsTest.cs b/test/unit/LDAPUtilsTest.cs index 2b08cc3d..0641aa6b 100644 --- a/test/unit/LDAPUtilsTest.cs +++ b/test/unit/LDAPUtilsTest.cs @@ -6,6 +6,7 @@ using Moq; using SharpHoundCommonLib; using SharpHoundCommonLib.Enums; +using SharpHoundCommonLib.OutputTypes; using Xunit; using Xunit.Abstractions; @@ -231,5 +232,33 @@ public async Task Test_ResolveHostToSid_BlankHost() { var (success, sid) = await utils.ResolveHostToSid(spn, ""); Assert.False(success); } + + [WindowsOnlyFact] + public async Task EnterpriseDomainControllersGroup_CorrectValues() { + var utilsMock = new Mock(); + + //We're going to say TESTLAB.LOCAL is forest root, and SECONDARY is a child domain underneath TESTLAB.LOCAL + + utilsMock.Setup(x => x.GetDomainNameFromSid("S-1-5-21-3130019616-2776909439-2417379446")) + .ReturnsAsync((true, "TESTLAB.LOCAL")); + utilsMock.Setup(x => x.GetDomainNameFromSid("S-1-5-21-3130019616-2776909439-2417379447")) + .ReturnsAsync((true, "SECONDARY.TESTLAB.LOCAL")); + + utilsMock.Setup(x => x.GetForest("TESTLAB.LOCAL")).ReturnsAsync((true, "TESTLAB.LOCAL")); + utilsMock.Setup(x => x.GetForest("SECONDARY.TESTLAB.LOCAL")).ReturnsAsync((true, "TESTLAB.LOCAL")); + + utilsMock.Setup(x => x.GetDomainSidFromDomainName("TESTLAB.LOCAL")).ReturnsAsync((true, "S-1-5-21-3130019616-2776909439-2417379446")); + + var utils = utilsMock.Object; + utils.AddDomainController("S-1-5-21-3130019616-2776909439-2417379446-2105"); + utils.AddDomainController("S-1-5-21-3130019616-2776909439-2417379446-2106"); + utils.AddDomainController("S-1-5-21-3130019616-2776909439-2417379447-2105"); + + var result = await utils.GetWellKnownPrincipalOutput().ToArrayAsync(); + Assert.Single(result); + var entDCGroup = result[0] as Group; + Assert.Equal("TESTLAB.LOCAL-S-1-5-9", entDCGroup.ObjectIdentifier); + Assert.Equal(3, entDCGroup.Members.Length); + } } } \ No newline at end of file