From 4bfb80a467a0541b1bbd35e7dcd100e6fbfe35c3 Mon Sep 17 00:00:00 2001 From: JonasBK Date: Mon, 4 Aug 2025 13:13:06 +0200 Subject: [PATCH] Add WriteAltSecurityIdentities and WritePublicInformation --- src/CommonLib/Enums/EdgeNames.cs | 2 + src/CommonLib/Processors/ACEGuids.cs | 3 +- src/CommonLib/Processors/ACLProcessor.cs | 21 ++++ test/unit/ACLProcessorTest.cs | 152 +++++++++++++++++++++++ 4 files changed, 177 insertions(+), 1 deletion(-) diff --git a/src/CommonLib/Enums/EdgeNames.cs b/src/CommonLib/Enums/EdgeNames.cs index 3cd346729..ed0bd4af9 100644 --- a/src/CommonLib/Enums/EdgeNames.cs +++ b/src/CommonLib/Enums/EdgeNames.cs @@ -22,6 +22,8 @@ public static class EdgeNames public const string SQLAdmin = "SQLAdmin"; public const string WriteAccountRestrictions = "WriteAccountRestrictions"; public const string WriteGPLink = "WriteGPLink"; + public const string WriteAltSecurityIdentities = "WriteAltSecurityIdentities"; + public const string WritePublicInformation = "WritePublicInformation"; //CertAbuse edges public const string WritePKIEnrollmentFlag = "WritePKIEnrollmentFlag"; diff --git a/src/CommonLib/Processors/ACEGuids.cs b/src/CommonLib/Processors/ACEGuids.cs index ebb0e5b11..b32df4faa 100644 --- a/src/CommonLib/Processors/ACEGuids.cs +++ b/src/CommonLib/Processors/ACEGuids.cs @@ -14,7 +14,8 @@ public class ACEGuids public const string UserAccountRestrictions = "4c164200-20c0-11d0-a768-00aa006e0529"; public const string WriteGPLink = "f30e3bbe-9ff0-11d1-b603-0000f80367c1"; public const string WriteTitle = "bf967a55-0de6-11d0-a285-00aa003049e2"; // Not an edge, just used for testing - + public const string WriteAltSecurityIdentities = "00fbf30c-91fe-11d1-aebc-0000f80367c1"; + public const string WritePublicInformation = "e48d0154-bcf8-11d1-8702-00c04fb96050"; //Cert abuse ACEs public const string PKINameFlag = "ea1dddc4-60ff-416e-8cc0-17cee534bce7"; diff --git a/src/CommonLib/Processors/ACLProcessor.cs b/src/CommonLib/Processors/ACLProcessor.cs index 383b69aff..d0d643b70 100644 --- a/src/CommonLib/Processors/ACLProcessor.cs +++ b/src/CommonLib/Processors/ACLProcessor.cs @@ -806,6 +806,27 @@ or Label.NTAuthStore IsPermissionForOwnerRightsSid = isPermissionForOwnerRightsSid, IsInheritedPermissionForOwnerRightsSid = isInheritedPermissionForOwnerRightsSid, }; + else if (objectType is Label.User or Label.Computer && aceType == ACEGuids.WriteAltSecurityIdentities) + yield return new ACE { + PrincipalType = resolvedPrincipal.ObjectType, + PrincipalSID = resolvedPrincipal.ObjectIdentifier, + IsInherited = inherited, + RightName = EdgeNames.WriteAltSecurityIdentities, + InheritanceHash = aceInheritanceHash, + IsPermissionForOwnerRightsSid = isPermissionForOwnerRightsSid, + IsInheritedPermissionForOwnerRightsSid = isInheritedPermissionForOwnerRightsSid, + }; + else if (objectType is Label.User or Label.Computer && aceType == ACEGuids.WritePublicInformation) + yield return new ACE + { + PrincipalType = resolvedPrincipal.ObjectType, + PrincipalSID = resolvedPrincipal.ObjectIdentifier, + IsInherited = inherited, + RightName = EdgeNames.WritePublicInformation, + InheritanceHash = aceInheritanceHash, + IsPermissionForOwnerRightsSid = isPermissionForOwnerRightsSid, + IsInheritedPermissionForOwnerRightsSid = isInheritedPermissionForOwnerRightsSid, + }; else if (objectType is Label.CertTemplate) { if (aceType == ACEGuids.PKIEnrollmentFlag) yield return new ACE { diff --git a/test/unit/ACLProcessorTest.cs b/test/unit/ACLProcessorTest.cs index 7f7677c52..16cf57f82 100644 --- a/test/unit/ACLProcessorTest.cs +++ b/test/unit/ACLProcessorTest.cs @@ -2129,5 +2129,157 @@ public async Task ACLProcessor_ProcessACL_EnterpriseCA_Enroll() Assert.False(actual.IsInherited); Assert.Equal(actual.RightName, expectedRightName); } + + [Fact] + public async Task ACLProcessor_ProcessACL_GenericWrite_User_WriteAltSecurityIdentities() { + var expectedPrincipalType = Label.User; + var expectedPrincipalSID = "S-1-5-21-3130019616-2776909439-2417379446-512"; + var expectedRightName = EdgeNames.WriteAltSecurityIdentities; + + 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.GenericWrite); + mockRule.Setup(x => x.ObjectType()).Returns(new Guid(ACEGuids.WriteAltSecurityIdentities)); + 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))); + mockLDAPUtils.Setup(x => x.PagedQuery(It.IsAny(), It.IsAny())) + .Returns(Array.Empty>().ToAsyncEnumerable); + + var processor = new ACLProcessor(mockLDAPUtils.Object); + var bytes = Utils.B64ToBytes(UnProtectedUserNtSecurityDescriptor); + var result = await processor.ProcessACL(bytes, _testDomainName, Label.User, true).ToArrayAsync(); + + Assert.Single(result); + var actual = result.First(); + Assert.Equal(actual.PrincipalType, expectedPrincipalType); + Assert.Equal(actual.PrincipalSID, expectedPrincipalSID); + Assert.False(actual.IsInherited); + Assert.Equal(actual.RightName, expectedRightName); + } + + [Fact] + public async Task ACLProcessor_ProcessACL_GenericWrite_Computer_WriteAltSecurityIdentities() { + var expectedPrincipalType = Label.Computer; + var expectedPrincipalSID = "S-1-5-21-3130019616-2776909439-2417379446-512"; + var expectedRightName = EdgeNames.WriteAltSecurityIdentities; + + 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.GenericWrite); + mockRule.Setup(x => x.ObjectType()).Returns(new Guid(ACEGuids.WriteAltSecurityIdentities)); + 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))); + mockLDAPUtils.Setup(x => x.PagedQuery(It.IsAny(), It.IsAny())) + .Returns(Array.Empty>().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 async Task ACLProcessor_ProcessACL_GenericWrite_User_WritePublicInformation() { + var expectedPrincipalType = Label.User; + var expectedPrincipalSID = "S-1-5-21-3130019616-2776909439-2417379446-512"; + var expectedRightName = EdgeNames.WritePublicInformation; + + 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.GenericWrite); + mockRule.Setup(x => x.ObjectType()).Returns(new Guid(ACEGuids.WritePublicInformation)); + 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))); + mockLDAPUtils.Setup(x => x.PagedQuery(It.IsAny(), It.IsAny())) + .Returns(Array.Empty>().ToAsyncEnumerable); + + var processor = new ACLProcessor(mockLDAPUtils.Object); + var bytes = Utils.B64ToBytes(UnProtectedUserNtSecurityDescriptor); + var result = await processor.ProcessACL(bytes, _testDomainName, Label.User, true).ToArrayAsync(); + + Assert.Single(result); + var actual = result.First(); + Assert.Equal(actual.PrincipalType, expectedPrincipalType); + Assert.Equal(actual.PrincipalSID, expectedPrincipalSID); + Assert.False(actual.IsInherited); + Assert.Equal(actual.RightName, expectedRightName); + } + + [Fact] + public async Task ACLProcessor_ProcessACL_GenericWrite_Computer_WritePublicInformation() { + var expectedPrincipalType = Label.Computer; + var expectedPrincipalSID = "S-1-5-21-3130019616-2776909439-2417379446-512"; + var expectedRightName = EdgeNames.WritePublicInformation; + + 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.GenericWrite); + mockRule.Setup(x => x.ObjectType()).Returns(new Guid(ACEGuids.WritePublicInformation)); + 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))); + mockLDAPUtils.Setup(x => x.PagedQuery(It.IsAny(), It.IsAny())) + .Returns(Array.Empty>().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); + } } } \ No newline at end of file