From 48e136df5c66a89d42af34a7721ea2ccb0f3d381 Mon Sep 17 00:00:00 2001 From: Rohan Vazarkar Date: Tue, 15 Oct 2024 11:43:33 -0400 Subject: [PATCH 1/9] Incorrect collection of ACE GUIDS from Schema (#169) * fix: laps guids not being properly processed from schema * chore: fix tests, add test for laps aces * chore: fix comment * chore: bump to 8.0 as 7.0 is deprecated * chore: comment out test for now * fix: change from static to instance so tests work * chore: fix tests --- src/CommonLib/Processors/ACLProcessor.cs | 29 ++++-- test/unit/ACLProcessorTest.cs | 112 ++++++++++++++++++----- test/unit/CommonLibTest.csproj | 2 +- 3 files changed, 110 insertions(+), 33 deletions(-) diff --git a/src/CommonLib/Processors/ACLProcessor.cs b/src/CommonLib/Processors/ACLProcessor.cs index af69068a..7afbb9d5 100644 --- a/src/CommonLib/Processors/ACLProcessor.cs +++ b/src/CommonLib/Processors/ACLProcessor.cs @@ -15,10 +15,10 @@ 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); static ACLProcessor() { //Create a dictionary with the base GUIDs of each object type @@ -50,8 +50,8 @@ 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 { + _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,14 +59,24 @@ 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(); + string guid; + try + { + guid = new Guid(schemaGuid).ToString(); + } + catch + { + continue; + } + 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); + _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); @@ -217,7 +227,8 @@ public IEnumerable GetInheritedAceHashes(byte[] ntSecurityDescriptor, st public async IAsyncEnumerable ProcessACL(byte[] ntSecurityDescriptor, string objectDomain, Label objectType, bool hasLaps, string objectName = "") { - if (!BuiltDomainCaches.Contains(objectDomain)) { + if (!_builtDomainCaches.Contains(objectDomain)) { + _builtDomainCaches.Add(objectDomain); await BuildGuidCache(objectDomain); } @@ -288,7 +299,7 @@ public async IAsyncEnumerable ProcessACL(byte[] ntSecurityDescriptor, strin aceInheritanceHash = CalculateInheritanceHash(ir, aceRights, aceType, ace.InheritedObjectType()); } - GuidMap.TryGetValue(aceType, out var mappedGuid); + _guidMap.TryGetValue(aceType, out var mappedGuid); _log.LogTrace("Processing ACE with rights {Rights} and guid {GUID} on object {Name}", aceRights, aceType, objectName); 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\ From 37ba5167082dc49f64ac7052796b2c86d22bad36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20B=C3=BClow=20Knudsen?= <12843299+JonasBK@users.noreply.github.com> Date: Tue, 15 Oct 2024 20:38:23 +0200 Subject: [PATCH 2/9] fix TGTDelegationEnabled (#168) * fix TGTDelegationEnabled CrossOrganizationNoTGTDelegation being false does not enable TGT delegation. That is what my testing shows. It seems like this flag became meaningless after TGT delegation was disabled by default in 2019. --- src/CommonLib/Processors/DomainTrustProcessor.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/CommonLib/Processors/DomainTrustProcessor.cs b/src/CommonLib/Processors/DomainTrustProcessor.cs index 1df373ec..16681f9d 100644 --- a/src/CommonLib/Processors/DomainTrustProcessor.cs +++ b/src/CommonLib/Processors/DomainTrustProcessor.cs @@ -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 +} From 2343b28f8a222f34378d14027b60789eb4973845 Mon Sep 17 00:00:00 2001 From: Rohan Vazarkar Date: Thu, 17 Oct 2024 15:05:30 -0400 Subject: [PATCH 3/9] fix: switch to SHA1 and add a try/catch block on acl inheritance hashing (#172) * fix: switch to MD5 and add a try/catch block on acl inheritance hashing * chore: add missing inheritancehash value for owns * chore: switch to bitconverter for perf --- src/CommonLib/Processors/ACLProcessor.cs | 34 +++++++++++++++--------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/src/CommonLib/Processors/ACLProcessor.cs b/src/CommonLib/Processors/ACLProcessor.cs index 7afbb9d5..cec3692b 100644 --- a/src/CommonLib/Processors/ACLProcessor.cs +++ b/src/CommonLib/Processors/ACLProcessor.cs @@ -135,17 +135,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 ""; } } @@ -209,8 +213,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; + } } } @@ -256,7 +264,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); @@ -264,7 +273,8 @@ public async IAsyncEnumerable ProcessACL(byte[] ntSecurityDescriptor, strin PrincipalType = Label.Base, PrincipalSID = ownerSid, RightName = EdgeNames.Owns, - IsInherited = false + IsInherited = false, + InheritanceHash = "" }; } } From 1d9e9c070d18e5519e7e0331d1c659d34718d6b6 Mon Sep 17 00:00:00 2001 From: spyr0 <78267628+spyr0-sec@users.noreply.github.com> Date: Fri, 18 Oct 2024 16:09:10 +0100 Subject: [PATCH 4/9] Fixed LAPS attributes (#167) * Update LDAPProperties.cs Updated LAPS password attributes * Update ACLProcessor.cs Updated logic to create ReadLAPSPassword edges based on updated LAPS password attributes * Update ACLProcessor.cs Updated logic to pull GUIDs for new LAPS password attributes * Update LDAPProperties.cs Corrected new LAPS password expiry attribute * Fixed ReadLAPSPassword Logic --------- Co-authored-by: Rohan Vazarkar --- src/CommonLib/Enums/LDAPProperties.cs | 3 ++- src/CommonLib/Processors/ACLProcessor.cs | 30 +++++++++++++++--------- 2 files changed, 21 insertions(+), 12 deletions(-) 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/Processors/ACLProcessor.cs b/src/CommonLib/Processors/ACLProcessor.cs index cec3692b..34aff91f 100644 --- a/src/CommonLib/Processors/ACLProcessor.cs +++ b/src/CommonLib/Processors/ACLProcessor.cs @@ -64,6 +64,7 @@ private async Task BuildGuidCache(string domain) { } name = name.ToLower(); + string guid; try { @@ -74,7 +75,7 @@ private async Task BuildGuidCache(string domain) { continue; } - if (name is LDAPProperties.LAPSPassword or LDAPProperties.LegacyLAPSPassword) { + 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); } @@ -309,8 +310,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); @@ -423,14 +422,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 "") From 859d470e73f2187bd4b0cdfc9ed234b756cf8439 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20B=C3=BClow=20Knudsen?= <12843299+JonasBK@users.noreply.github.com> Date: Fri, 18 Oct 2024 17:46:31 +0200 Subject: [PATCH 5/9] prep for CoerceToTGT (#171) --- src/CommonLib/OutputTypes/Computer.cs | 1 + src/CommonLib/OutputTypes/User.cs | 2 ++ src/CommonLib/Processors/LdapPropertyProcessor.cs | 6 ++++++ 3 files changed, 9 insertions(+) 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/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/LdapPropertyProcessor.cs b/src/CommonLib/Processors/LdapPropertyProcessor.cs index 8577f0fe..c12764b1 100644 --- a/src/CommonLib/Processors/LdapPropertyProcessor.cs +++ b/src/CommonLib/Processors/LdapPropertyProcessor.cs @@ -206,6 +206,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 +323,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); @@ -908,6 +912,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 +921,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 { From 276237fe7eb4d72255e8d666baf947a0ebe59e87 Mon Sep 17 00:00:00 2001 From: Rohan Vazarkar Date: Fri, 18 Oct 2024 11:54:59 -0400 Subject: [PATCH 6/9] chore: version bump (#170) --- src/CommonLib/SharpHoundCommonLib.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CommonLib/SharpHoundCommonLib.csproj b/src/CommonLib/SharpHoundCommonLib.csproj index 85cffc92..6c88c8e8 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.8 SharpHoundCommonLib SharpHoundCommonLib From 9e03f0c76e75e78d8a91b5411610067247bbe7a1 Mon Sep 17 00:00:00 2001 From: Rohan Vazarkar Date: Mon, 4 Nov 2024 09:52:43 -0500 Subject: [PATCH 7/9] Fix properties with incorrect types (#173) * wip: fix bad data types in sh * chore: revert formatting to k&r * chore: fix more formatting * fix: elevate try/catch on principalcontext calls to fix exceptions https://github.com/BloodHoundAD/SharpHound/issues/120 * chore: add lock on buildguidcache --- src/CommonLib/LdapUtils.cs | 38 ++++---- src/CommonLib/OutputTypes/DomainTrust.cs | 2 +- src/CommonLib/Processors/ACLProcessor.cs | 15 +++- .../Processors/DomainTrustProcessor.cs | 2 +- .../Processors/LdapPropertyProcessor.cs | 89 ++++++++++++------- 5 files changed, 89 insertions(+), 57 deletions(-) diff --git a/src/CommonLib/LdapUtils.cs b/src/CommonLib/LdapUtils.cs index 17293d3b..e29defc7 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); } @@ -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); } @@ -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) { 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/Processors/ACLProcessor.cs b/src/CommonLib/Processors/ACLProcessor.cs index 34aff91f..45af8136 100644 --- a/src/CommonLib/Processors/ACLProcessor.cs +++ b/src/CommonLib/Processors/ACLProcessor.cs @@ -19,6 +19,7 @@ public class ACLProcessor { private readonly ILogger _log; private readonly ILdapUtils _utils; 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,6 +51,14 @@ public ACLProcessor(ILdapUtils utils, ILogger log = null) { /// LAPS /// private async Task BuildGuidCache(string domain) { + 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, @@ -83,6 +92,7 @@ private async Task BuildGuidCache(string domain) { _log.LogDebug("Error while building GUID cache for {Domain}: {Message}", domain, result.Error); } } + } /// @@ -236,10 +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)) { - _builtDomainCaches.Add(objectDomain); - await BuildGuidCache(objectDomain); - } + await BuildGuidCache(objectDomain); if (ntSecurityDescriptor == null) { _log.LogDebug("Security Descriptor is null for {Name}", objectName); diff --git a/src/CommonLib/Processors/DomainTrustProcessor.cs b/src/CommonLib/Processors/DomainTrustProcessor.cs index 16681f9d..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); diff --git a/src/CommonLib/Processors/LdapPropertyProcessor.cs b/src/CommonLib/Processors/LdapPropertyProcessor.cs index c12764b1..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); + } + + 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); + } - 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.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); @@ -623,7 +644,7 @@ public Dictionary ParseAllProperties(IDirectoryObject entry) { if (testBytes == null || testBytes.Length == 0) { continue; } - + // SIDs try { var sid = new SecurityIdentifier(testBytes, 0); @@ -707,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; } @@ -719,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; } @@ -761,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")}"); } @@ -888,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; From 812d105da8e3920683bc9dbcbba5ea4921c41e7f Mon Sep 17 00:00:00 2001 From: Rohan Vazarkar Date: Fri, 15 Nov 2024 11:52:18 -0500 Subject: [PATCH 8/9] fix: make enterprise domain controllers group spit ou the correct sid (#174) * fix: make enterprise domain controllers group spit ou the correct sid Closes: https://specterops.atlassian.net/issues/BED-4846 * chore: fix test --- src/CommonLib/LdapUtils.cs | 19 +++++++++++-------- test/unit/LDAPUtilsTest.cs | 29 +++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/src/CommonLib/LdapUtils.cs b/src/CommonLib/LdapUtils.cs index e29defc7..a9166aff 100644 --- a/src/CommonLib/LdapUtils.cs +++ b/src/CommonLib/LdapUtils.cs @@ -315,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(); @@ -408,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 { @@ -938,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(), @@ -961,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); @@ -972,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) { @@ -982,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/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 From c953260325cbfd335ed2e9726cfe28d4b16357c8 Mon Sep 17 00:00:00 2001 From: Rohan Vazarkar Date: Thu, 21 Nov 2024 11:29:46 -0500 Subject: [PATCH 9/9] chore: move group collection to end of our ldap queries (#175) * chore: move group collection to end of our ldap queries * chore: version bump --- src/CommonLib/LdapProducerQueryGenerator.cs | 10 +++++----- src/CommonLib/SharpHoundCommonLib.csproj | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) 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/SharpHoundCommonLib.csproj b/src/CommonLib/SharpHoundCommonLib.csproj index 6c88c8e8..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.8 + 4.0.9 SharpHoundCommonLib SharpHoundCommonLib