diff --git a/src/CommonLib/Processors/ACEGuids.cs b/src/CommonLib/Processors/ACEGuids.cs index ebb0e5b11..fb205d7a1 100644 --- a/src/CommonLib/Processors/ACEGuids.cs +++ b/src/CommonLib/Processors/ACEGuids.cs @@ -8,6 +8,7 @@ public class ACEGuids public const string UserForceChangePassword = "00299570-246d-11d0-a768-00aa006e0529"; public const string AllGuid = "00000000-0000-0000-0000-000000000000"; public const string WriteMember = "bf9679c0-0de6-11d0-a285-00aa003049e2"; + public const string MembershipPropertySet = "bc0ac240-79a9-11d0-9020-00c04fc2d4cf"; // property set https://learn.microsoft.com/en-us/windows/win32/adschema/r-membership public const string WriteAllowedToAct = "3f78c3e5-f79a-46bd-a0b8-9d18116ddc79"; public const string WriteSPN = "f3a64788-5306-11d1-a9c5-0000f80367c1"; public const string AddKeyPrincipal = "5b47d60f-6090-40b2-9f37-2a4de88f3063"; diff --git a/src/CommonLib/Processors/ACLProcessor.cs b/src/CommonLib/Processors/ACLProcessor.cs index 383b69aff..da3a615b4 100644 --- a/src/CommonLib/Processors/ACLProcessor.cs +++ b/src/CommonLib/Processors/ACLProcessor.cs @@ -584,7 +584,7 @@ public async IAsyncEnumerable ProcessACL(byte[] ntSecurityDescriptor, strin if (aceRights.HasFlag(ActiveDirectoryRights.Self) && !aceRights.HasFlag(ActiveDirectoryRights.WriteProperty) && !aceRights.HasFlag(ActiveDirectoryRights.GenericWrite) && objectType == Label.Group && - aceType is ACEGuids.WriteMember or ACEGuids.AllGuid) + aceType is ACEGuids.WriteMember or ACEGuids.MembershipPropertySet or ACEGuids.AllGuid) yield return new ACE { PrincipalType = resolvedPrincipal.ObjectType, PrincipalSID = resolvedPrincipal.ObjectIdentifier, @@ -786,7 +786,7 @@ or Label.NTAuthStore IsPermissionForOwnerRightsSid = isPermissionForOwnerRightsSid, IsInheritedPermissionForOwnerRightsSid = isInheritedPermissionForOwnerRightsSid, }; - else if (objectType == Label.Group && aceType == ACEGuids.WriteMember) + else if (objectType == Label.Group && (aceType is ACEGuids.WriteMember or ACEGuids.MembershipPropertySet)) yield return new ACE { PrincipalType = resolvedPrincipal.ObjectType, PrincipalSID = resolvedPrincipal.ObjectIdentifier, diff --git a/src/CommonLib/Processors/CertAbuseProcessor.cs b/src/CommonLib/Processors/CertAbuseProcessor.cs index d9e4286e9..7da4f46c5 100644 --- a/src/CommonLib/Processors/CertAbuseProcessor.cs +++ b/src/CommonLib/Processors/CertAbuseProcessor.cs @@ -19,16 +19,18 @@ public class CertAbuseProcessor private readonly AdaptiveTimeout _getMachineSidAdaptiveTimeout; private readonly AdaptiveTimeout _openSamServerAdaptiveTimeout; private readonly IRegistryAccessor _registryAccessor; + private readonly ISAMServerAccessor _samServerAccessor; public delegate Task ComputerStatusDelegate(CSVComputerStatus status); public event ComputerStatusDelegate ComputerStatusEvent; - public CertAbuseProcessor(ILdapUtils utils, IRegistryAccessor registryAccessor, ILogger log = null) { + public CertAbuseProcessor(ILdapUtils utils, IRegistryAccessor registryAccessor, ISAMServerAccessor samServerAccessor, ILogger log = null) { _utils = utils; _registryAccessor = registryAccessor; + _samServerAccessor = samServerAccessor; _log = log ?? Logging.LogProvider.CreateLogger("CAProc"); _getMachineSidAdaptiveTimeout = new AdaptiveTimeout(maxTimeout: TimeSpan.FromMinutes(2), Logging.LogProvider.CreateLogger(nameof(ISAMServer.GetMachineSid))); - _openSamServerAdaptiveTimeout = new AdaptiveTimeout(maxTimeout: TimeSpan.FromMinutes(2), Logging.LogProvider.CreateLogger(nameof(SAMServer.OpenServer))); + _openSamServerAdaptiveTimeout = new AdaptiveTimeout(maxTimeout: TimeSpan.FromMinutes(2), Logging.LogProvider.CreateLogger(nameof(ISAMServerAccessor.OpenServer))); } /// @@ -407,11 +409,11 @@ await _utils.ResolveIDAndType(sid.Value, computerDomain) is (true, var resolvedP return await _utils.ResolveIDAndType(sid.Value, computerDomain); } - private async Task GetMachineSid(string computerName, string computerObjectId) + internal async Task GetMachineSid(string computerName, string computerObjectId) { SecurityIdentifier machineSid = null; - //Try to get the machine sid for the computer if its not already cached + //Try to get the machine sid for the computer if it's not already cached if (!Cache.GetMachineSid(computerObjectId, out var tempMachineSid)) { // Open a handle to the server @@ -421,7 +423,7 @@ private async Task GetMachineSid(string computerName, string _log.LogTrace("OpenServer failed on {ComputerName}: {Error}", computerName, openServerResult.SError); await SendComputerStatus(new CSVComputerStatus { - Task = "SamConnect", + Task = nameof(OpenSamServer), ComputerName = computerName, Status = openServerResult.SError, ObjectId = computerObjectId, @@ -438,14 +440,21 @@ await SendComputerStatus(new CSVComputerStatus { Status = getMachineSidResult.SError, ComputerName = computerName, - Task = "GetMachineSid", + Task = nameof(GetMachineSid), ObjectId = computerObjectId, }); - //If we can't get a machine sid, we wont be able to make local principals with unique object ids, or differentiate local/domain objects + //If we can't get a machine sid, we won't be able to make local principals with unique object ids, or differentiate local/domain objects _log.LogWarning("Unable to get machineSid for {Computer}: {Status}", computerName, getMachineSidResult.SError); return null; } + await SendComputerStatus(new CSVComputerStatus { + Status = CSVComputerStatus.StatusSuccess, + Task = nameof(GetMachineSid), + ComputerName = computerName, + ObjectId = computerObjectId + }); + machineSid = getMachineSidResult.Value; Cache.AddMachineSid(computerObjectId, machineSid.Value); } @@ -457,26 +466,28 @@ await SendComputerStatus(new CSVComputerStatus return machineSid; } - private async Task<(bool success, EnrollmentAgentRestriction restriction)> CreateEnrollmentAgentRestriction(QualifiedAce ace, string computerDomain, string computerName, bool isDomainController, string computerObjectId, SecurityIdentifier machineSid) { + internal async Task<(bool success, EnrollmentAgentRestriction restriction)> CreateEnrollmentAgentRestriction(QualifiedAce ace, string computerDomain, string computerName, bool isDomainController, string computerObjectId, SecurityIdentifier machineSid) + { + var opaque = ace.GetOpaque(); + + if(opaque is null) + return (false, default); + var targets = new List(); var index = 0; var accessType = ace.AceType.ToString(); var agent = await GetRegistryPrincipal(ace.SecurityIdentifier, computerDomain, computerName, isDomainController, computerObjectId, machineSid); - - var opaque = ace.GetOpaque(); - - if(opaque is null) - return (false, default); var sidCount = BitConverter.ToUInt32(opaque, 0); index += 4; for (var i = 0; i < sidCount; i++) { var sid = new SecurityIdentifier(opaque, index); - if (await GetRegistryPrincipal(sid, computerDomain, computerName, isDomainController, computerObjectId, - machineSid) is (true, var regPrincipal)) { + if (await GetRegistryPrincipal(sid, computerDomain, computerName, isDomainController, computerObjectId, machineSid) + is (true, var regPrincipal)) + { targets.Add(regPrincipal); } @@ -485,43 +496,47 @@ await SendComputerStatus(new CSVComputerStatus var finalTargets = targets.ToArray(); var allTemplates = index >= opaque.Length; - if (index < opaque.Length) { - var template = Encoding.Unicode.GetString(opaque, index, opaque.Length - index - 2).Replace("\u0000", string.Empty); - if (await _utils.ResolveCertTemplateByProperty(Encoder.LdapFilterEncode(template), LDAPProperties.CanonicalName, computerDomain) is (true, var resolvedTemplate)) { - return (true, new EnrollmentAgentRestriction { - Template = resolvedTemplate, - Agent = agent.Principal, - AllTemplates = allTemplates, - AccessType = accessType, - Targets = finalTargets - }); - } - - if (await _utils.ResolveCertTemplateByProperty( - Encoder.LdapFilterEncode(template), LDAPProperties.CertTemplateOID, computerDomain) is - (true, var resolvedOidTemplate)) { - return (true, new EnrollmentAgentRestriction { - Template = resolvedOidTemplate, - Agent = agent.Principal, - AllTemplates = allTemplates, - AccessType = accessType, - Targets = finalTargets - }); - } + + if (allTemplates) { + return (true, new EnrollmentAgentRestriction { + Agent = agent.Principal, + AllTemplates = allTemplates, + AccessType = accessType, + Targets = finalTargets + }); + } + + var template = Encoding.Unicode.GetString(opaque, index, opaque.Length - index - 2).Replace("\u0000", string.Empty); + if (await _utils.ResolveCertTemplateByProperty(Encoder.LdapFilterEncode(template), LDAPProperties.CanonicalName, computerDomain) + is (true, var resolvedTemplate)) + { + return (true, new EnrollmentAgentRestriction { + Template = resolvedTemplate, + Agent = agent.Principal, + AllTemplates = allTemplates, + AccessType = accessType, + Targets = finalTargets + }); } + if (await _utils.ResolveCertTemplateByProperty(Encoder.LdapFilterEncode(template), LDAPProperties.CertTemplateOID, computerDomain) + is (true, var resolvedOidTemplate)) + { + return (true, new EnrollmentAgentRestriction { + Template = resolvedOidTemplate, + Agent = agent.Principal, + AllTemplates = allTemplates, + AccessType = accessType, + Targets = finalTargets + }); + } + return (false, default); } public virtual SharpHoundRPC.Result OpenSamServer(string computerName) { - var result = _openSamServerAdaptiveTimeout.ExecuteRPCWithTimeout((_) => SAMServer.OpenServer(computerName)).GetAwaiter().GetResult(); - if (result.IsFailed) - { - return SharpHoundRPC.Result.Fail(result.SError); - } - - return SharpHoundRPC.Result.Ok(result.Value); + return _openSamServerAdaptiveTimeout.ExecuteRPCWithTimeout((_) => _samServerAccessor.OpenServer(computerName)).GetAwaiter().GetResult(); } private async Task SendComputerStatus(CSVComputerStatus status) diff --git a/src/CommonLib/Processors/LocalGroupProcessor.cs b/src/CommonLib/Processors/LocalGroupProcessor.cs index 93454b174..c2048b04f 100644 --- a/src/CommonLib/Processors/LocalGroupProcessor.cs +++ b/src/CommonLib/Processors/LocalGroupProcessor.cs @@ -16,6 +16,7 @@ public class LocalGroupProcessor public delegate Task ComputerStatusDelegate(CSVComputerStatus status); private readonly ILogger _log; private readonly ILdapUtils _utils; + private readonly ISAMServerAccessor _samServerAccessor; private readonly AdaptiveTimeout _getMachineSidAdaptiveTimeout; private readonly AdaptiveTimeout _openSamServerAdaptiveTimeout; private readonly AdaptiveTimeout _getDomainsAdaptiveTimeout; @@ -27,9 +28,10 @@ public class LocalGroupProcessor public LocalGroupProcessor(ILdapUtils utils, ILogger log = null) { _utils = utils; + _samServerAccessor = new SAMServerAccessor(); _log = log ?? Logging.LogProvider.CreateLogger("LocalGroupProcessor"); _getMachineSidAdaptiveTimeout = new AdaptiveTimeout(maxTimeout: TimeSpan.FromMinutes(2), Logging.LogProvider.CreateLogger(nameof(ISAMServer.GetMachineSid))); - _openSamServerAdaptiveTimeout = new AdaptiveTimeout(maxTimeout: TimeSpan.FromMinutes(2), Logging.LogProvider.CreateLogger(nameof(SAMServer.OpenServer))); + _openSamServerAdaptiveTimeout = new AdaptiveTimeout(maxTimeout: TimeSpan.FromMinutes(2), Logging.LogProvider.CreateLogger(nameof(ISAMServerAccessor.OpenServer))); _getDomainsAdaptiveTimeout = new AdaptiveTimeout(maxTimeout: TimeSpan.FromMinutes(2), Logging.LogProvider.CreateLogger(nameof(ISAMServer.GetDomains))); _openDomainAdaptiveTimeout = new AdaptiveTimeout(maxTimeout: TimeSpan.FromMinutes(2), Logging.LogProvider.CreateLogger(nameof(ISAMServer.OpenDomain))); _getAliasesAdaptiveTimeout = new AdaptiveTimeout(maxTimeout: TimeSpan.FromMinutes(2), Logging.LogProvider.CreateLogger(nameof(ISAMDomain.GetAliases))); @@ -42,7 +44,7 @@ public LocalGroupProcessor(ILdapUtils utils, ILogger log = null) { public virtual SharpHoundRPC.Result OpenSamServer(string computerName) { - var result = _openSamServerAdaptiveTimeout.ExecuteRPCWithTimeout((_) => SAMServer.OpenServer(computerName)).GetAwaiter().GetResult(); + var result = _openSamServerAdaptiveTimeout.ExecuteRPCWithTimeout((_) => _samServerAccessor.OpenServer(computerName)).GetAwaiter().GetResult(); if (result.IsFailed) { return SharpHoundRPC.Result.Fail(result.SError); diff --git a/src/SharpHoundRPC/SAMRPCNative/SAMMethods.cs b/src/SharpHoundRPC/SAMRPCNative/SAMMethods.cs index e17de5b64..5fc5edc77 100644 --- a/src/SharpHoundRPC/SAMRPCNative/SAMMethods.cs +++ b/src/SharpHoundRPC/SAMRPCNative/SAMMethods.cs @@ -1,8 +1,6 @@ using System; -using System.Collections.Generic; using System.Runtime.InteropServices; using System.Security; -using System.Security.Principal; using SharpHoundRPC.Handles; using SharpHoundRPC.Shared; diff --git a/src/SharpHoundRPC/Wrappers/ISAMServerAccessor.cs b/src/SharpHoundRPC/Wrappers/ISAMServerAccessor.cs new file mode 100644 index 000000000..65851a212 --- /dev/null +++ b/src/SharpHoundRPC/Wrappers/ISAMServerAccessor.cs @@ -0,0 +1,27 @@ +using SharpHoundRPC.SAMRPCNative; + +namespace SharpHoundRPC.Wrappers +{ + public interface ISAMServerAccessor + { + Result OpenServer(string computerName, SAMEnums.SamAccessMasks requestedConnectAccess = + SAMEnums.SamAccessMasks.SamServerConnect | + SAMEnums.SamAccessMasks.SamServerEnumerateDomains | + SAMEnums.SamAccessMasks.SamServerLookupDomain); + } + + public class SAMServerAccessor : ISAMServerAccessor + { + public Result OpenServer(string computerName, SAMEnums.SamAccessMasks requestedConnectAccess = + SAMEnums.SamAccessMasks.SamServerConnect | + SAMEnums.SamAccessMasks.SamServerEnumerateDomains | + SAMEnums.SamAccessMasks.SamServerLookupDomain) + { + var (status, handle) = SAMMethods.SamConnect(computerName, requestedConnectAccess); + + return status.IsError() + ? status + : new SAMServer(handle, computerName); + } + } +} \ No newline at end of file diff --git a/src/SharpHoundRPC/Wrappers/SAMServer.cs b/src/SharpHoundRPC/Wrappers/SAMServer.cs index 872faa497..e405d1a88 100644 --- a/src/SharpHoundRPC/Wrappers/SAMServer.cs +++ b/src/SharpHoundRPC/Wrappers/SAMServer.cs @@ -22,19 +22,6 @@ public SAMServer(SAMHandle handle, string computerName) : base(handle) public string ComputerName { get; } - public static Result OpenServer(string computerName, SAMEnums.SamAccessMasks requestedConnectAccess = - SAMEnums.SamAccessMasks.SamServerConnect | - SAMEnums.SamAccessMasks - .SamServerEnumerateDomains | - SAMEnums.SamAccessMasks.SamServerLookupDomain) - { - var (status, handle) = SAMMethods.SamConnect(computerName, requestedConnectAccess); - - return status.IsError() - ? status - : new SAMServer(handle, computerName); - } - public Result> GetDomains() { var (status, rids, count) = SAMMethods.SamEnumerateDomainsInSamServer(Handle); diff --git a/test/unit/CertAbuseProcessorTest.cs b/test/unit/CertAbuseProcessorTest.cs index 663ece98d..ade1fc34c 100644 --- a/test/unit/CertAbuseProcessorTest.cs +++ b/test/unit/CertAbuseProcessorTest.cs @@ -2,56 +2,67 @@ using System.Collections.Generic; using System.Security.AccessControl; using System.Security.Principal; +using System.Threading; using System.Threading.Tasks; +using CommonLibTest.CollectionDefinitions; using Moq; using SharpHoundCommonLib; using SharpHoundCommonLib.Processors; using Xunit; -using Microsoft.Extensions.Logging; using SharpHoundCommonLib.Enums; using SharpHoundCommonLib.OutputTypes; +using SharpHoundRPC.SAMRPCNative; +using SharpHoundRPC.Wrappers; namespace CommonLibTest { - public class CertAbuseProcessorTest + [Collection(nameof(CacheTestCollectionDefinition))] + public class CertAbuseProcessorTest { private readonly Mock _mockLdapUtils; private readonly Mock _mockRegistryAccessor; + private readonly Mock _mockSAMServerAccessor; private readonly CertAbuseProcessor _certAbuseProcessor; - + private const string DomainName = "TEST.LOCAL"; private const string CAName = "TEST-CA"; private const string TargetName = "target.test.local"; private const string TargetDomainSid = "S-1-5-21-123456789-123456789-123456789"; private const string FailureReason = "Registry Lookup Failure"; - + private CSVComputerStatus _receivedCompStatus; - + public CertAbuseProcessorTest() { _mockLdapUtils = new Mock(); _mockRegistryAccessor = new Mock(); - _certAbuseProcessor = new CertAbuseProcessor(_mockLdapUtils.Object, _mockRegistryAccessor.Object); - - _certAbuseProcessor.ComputerStatusEvent += status => - { + _mockSAMServerAccessor = new Mock(); + _certAbuseProcessor = new CertAbuseProcessor(_mockLdapUtils.Object, _mockRegistryAccessor.Object, _mockSAMServerAccessor.Object); + + _certAbuseProcessor.ComputerStatusEvent += status => { _receivedCompStatus = status; return Task.CompletedTask; }; + + Cache.SetCacheInstance(Cache.CreateNewCache()); } - + [Theory] [InlineData(0x00040000, true)] [InlineData(0x00000000, false)] public async Task CertAbuseProcessor_IsUserSpecifiesSanEnabled_ReturnsResult(int editFlags, bool expectedResult) { const string subKey = $"SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\{CAName}\\PolicyModules\\CertificateAuthority_MicrosoftDefault.Policy"; const string subValue = "EditFlags"; - + _mockRegistryAccessor .Setup(ra => ra.GetRegistryKeyData( TargetName, subKey, subValue)) - .Returns(new RegistryResult { Collected = true, Value = editFlags }); + .Returns(new RegistryResult + { + Collected = true, + Value = editFlags + }); var results = await _certAbuseProcessor.IsUserSpecifiesSanEnabled(TargetName, CAName, TargetDomainSid); @@ -66,18 +77,22 @@ public async Task CertAbuseProcessor_IsUserSpecifiesSanEnabled_ReturnsResult(int Assert.Equal(CSVComputerStatus.StatusSuccess, _receivedCompStatus.Status); Assert.Equal(TargetDomainSid, _receivedCompStatus.ObjectId); } - + [Fact] public async Task CertAbuseProcessor_IsUserSpecifiesSanEnabled_HandlesFailedLookup() { const string subKey = $"SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\{CAName}\\PolicyModules\\CertificateAuthority_MicrosoftDefault.Policy"; const string subValue = "EditFlags"; - + _mockRegistryAccessor .Setup(ra => ra.GetRegistryKeyData( TargetName, subKey, subValue)) - .Returns(new RegistryResult { Collected = false, FailureReason = FailureReason }); + .Returns(new RegistryResult + { + Collected = false, + FailureReason = FailureReason + }); var results = await _certAbuseProcessor.IsUserSpecifiesSanEnabled(TargetName, CAName, TargetDomainSid); @@ -91,20 +106,24 @@ public async Task CertAbuseProcessor_IsUserSpecifiesSanEnabled_HandlesFailedLook Assert.Equal(FailureReason, _receivedCompStatus.Status); Assert.Equal(TargetDomainSid, _receivedCompStatus.ObjectId); } - + [Theory] [InlineData(1, true)] [InlineData(0, false)] public async Task CertAbuseProcessor_IsRoleSeparationEnabled_ReturnsResult(int roleSeparationEnabled, bool expectedResult) { const string subKey = $"SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\{CAName}"; const string subValue = "RoleSeparationEnabled"; - + _mockRegistryAccessor .Setup(ra => ra.GetRegistryKeyData( TargetName, subKey, subValue)) - .Returns(new RegistryResult { Collected = true, Value = roleSeparationEnabled }); + .Returns(new RegistryResult + { + Collected = true, + Value = roleSeparationEnabled + }); var results = await _certAbuseProcessor.IsRoleSeparationEnabled(TargetName, CAName, TargetDomainSid); @@ -119,18 +138,22 @@ public async Task CertAbuseProcessor_IsRoleSeparationEnabled_ReturnsResult(int r Assert.Equal(CSVComputerStatus.StatusSuccess, _receivedCompStatus.Status); Assert.Equal(TargetDomainSid, _receivedCompStatus.ObjectId); } - + [Fact] public async Task CertAbuseProcessor_IsRoleSeparationEnabled_HandlesFailedLookup() { const string subKey = $"SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\{CAName}"; const string subValue = "RoleSeparationEnabled"; - + _mockRegistryAccessor .Setup(ra => ra.GetRegistryKeyData( TargetName, subKey, subValue)) - .Returns(new RegistryResult { Collected = false, FailureReason = FailureReason }); + .Returns(new RegistryResult + { + Collected = false, + FailureReason = FailureReason + }); var results = await _certAbuseProcessor.IsRoleSeparationEnabled(TargetName, CAName, TargetDomainSid); @@ -145,89 +168,50 @@ public async Task CertAbuseProcessor_IsRoleSeparationEnabled_HandlesFailedLookup Assert.Equal(TargetDomainSid, _receivedCompStatus.ObjectId); } - public static IEnumerable ProcessEAPermissionsTestData() { - var nullOpaqueAce = new CommonAce( - AceFlags.None, - AceQualifier.AccessAllowed, - 0x0000, - new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null), - false, - null - ); - - var daclWithNullOpaque = new RawAcl(2, 1); - daclWithNullOpaque.InsertAce(0, nullOpaqueAce); - - var emptyOpaqueAce = new CommonAce( - AceFlags.None, - AceQualifier.AccessAllowed, - 0x0000, - new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null), - true, - new byte[] { 0, 0, 0, 0 } - ); - - var daclWithEmptyOpaque = new RawAcl(2, 1); - daclWithEmptyOpaque.InsertAce(0, emptyOpaqueAce); - - return new List - { - new object[] { null }, //null dacl - new object[] { new RawAcl(2, 0) }, //empty dacl - new object[] { daclWithNullOpaque }, //dacl is callback false, null opaque - new object[] { daclWithEmptyOpaque }, //dacl is callback true, empty opaque - }; - } - - [WindowsOnlyTheory] - [MemberData(nameof(ProcessEAPermissionsTestData))] - public async Task CertAbuseProcessor_ProcessEAPermissions_ReturnsEmpty(RawAcl dacl) { + [Fact] + public async Task CertAbuseProcessor_ProcessEAPermissions_ReturnsEmptyResult() { const string subKey = $"SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\{CAName}"; const string subValue = "EnrollmentAgentRights"; - - //setup binary security descriptor as registry value - var descriptor = new RawSecurityDescriptor( - ControlFlags.DiscretionaryAclPresent, - null, - null, - null, - dacl); - - var regValue = new byte[descriptor.BinaryLength]; - descriptor.GetBinaryForm(regValue, 0); _mockRegistryAccessor .Setup(ra => ra.GetRegistryKeyData( - "localhost", + TargetName, subKey, subValue)) - .Returns(new RegistryResult { Collected = true, Value = regValue }); - - var results = await _certAbuseProcessor.ProcessEAPermissions(CAName, DomainName, "localhost", TargetDomainSid); - + .Returns(new RegistryResult + { + Collected = true + }); + + var results = await _certAbuseProcessor.ProcessEAPermissions(CAName, DomainName, TargetName, TargetDomainSid); + //Validate result Assert.True(results.Collected); Assert.Empty(results.Restrictions); Assert.Null(results.FailureReason); - + //Validate CompStatus Log - Assert.Equal("localhost", _receivedCompStatus.ComputerName); + Assert.Equal(TargetName, _receivedCompStatus.ComputerName); Assert.Equal(nameof(CertAbuseProcessor.ProcessEAPermissions), _receivedCompStatus.Task); Assert.Equal(CSVComputerStatus.StatusSuccess, _receivedCompStatus.Status); Assert.Equal(TargetDomainSid, _receivedCompStatus.ObjectId); } - + [Fact] public async Task CertAbuseProcessor_ProcessEAPermissions_HandlesFailedLookup() { const string subKey = $"SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\{CAName}"; const string subValue = "EnrollmentAgentRights"; - + _mockRegistryAccessor .Setup(ra => ra.GetRegistryKeyData( TargetName, subKey, subValue)) - .Returns(new RegistryResult { Collected = false, FailureReason = FailureReason }); + .Returns(new RegistryResult + { + Collected = false, + FailureReason = FailureReason + }); var results = await _certAbuseProcessor.ProcessEAPermissions(CAName, DomainName, TargetName, TargetDomainSid); @@ -242,60 +226,101 @@ public async Task CertAbuseProcessor_ProcessEAPermissions_HandlesFailedLookup() Assert.Equal(TargetDomainSid, _receivedCompStatus.ObjectId); } - [WindowsOnlyFact] - public async Task CertAbuseProcessor_ProcessRegistryEnrollmentPermissions_ReturnsEmpty_WhenNoOwnerAndNoRules() { - const string subKey = $"SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\{CAName}"; - const string subValue = "Security"; + public static IEnumerable ProcessEAPermissionsTestData() { + return new List + { + new object[] { null }, //null dacl + new object[] { new RawAcl(2, 0) }, //empty dacl + }; + } + + [WindowsOnlyTheory] + [MemberData(nameof(ProcessEAPermissionsTestData))] + public async Task CertAbuseProcessor_ProcessEAPermissions_ReturnsEmpty(RawAcl dacl) { + var mockSamServer = new Mock(); + const string subKey = $"SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\{CAName}"; + const string subValue = "EnrollmentAgentRights"; + //setup binary security descriptor as registry value var descriptor = new RawSecurityDescriptor( ControlFlags.DiscretionaryAclPresent, - null, //owner is null null, null, - null); - - byte[] regValue = new byte[descriptor.BinaryLength]; + null, + dacl); + + var regValue = new byte[descriptor.BinaryLength]; descriptor.GetBinaryForm(regValue, 0); - + _mockRegistryAccessor .Setup(ra => ra.GetRegistryKeyData( - "localhost", + TargetName, subKey, subValue)) - .Returns(new RegistryResult { Collected = true, Value = regValue}); - - //get access rules returns empty - var mockSecurityDescriptor = new Mock(MockBehavior.Loose, null); - mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns([]); - _mockLdapUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); + .Returns(new RegistryResult + { + Collected = true, + Value = regValue + }); - var results = await _certAbuseProcessor.ProcessRegistryEnrollmentPermissions(CAName, DomainName, "localhost", TargetDomainSid); + _mockSAMServerAccessor.Setup(x => x.OpenServer(It.IsAny(), It.IsAny())) + .Returns(SharpHoundRPC.Result.Ok(mockSamServer.Object)); + mockSamServer.Setup(x => x.GetMachineSid(It.IsAny(), It.IsAny())) + .Returns(new SecurityIdentifier(TargetDomainSid)); - //Validate result + var results = await _certAbuseProcessor.ProcessEAPermissions(CAName, DomainName, TargetName, TargetDomainSid); + + //Validate result + Assert.True(results.Collected); + Assert.Empty(results.Restrictions); + Assert.Null(results.FailureReason); + } + + [Fact] + public async Task CertAbuseProcessor_ProcessRegistryEnrollmentPermissions_ReturnsEmptyResult() { + const string subKey = $"SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\{CAName}"; + const string subValue = "Security"; + + _mockRegistryAccessor + .Setup(ra => ra.GetRegistryKeyData( + TargetName, + subKey, + subValue)) + .Returns(new RegistryResult + { + Collected = true + }); + + var results = await _certAbuseProcessor.ProcessRegistryEnrollmentPermissions(CAName, DomainName, TargetName, TargetDomainSid); + + //Validate result Assert.True(results.Collected); Assert.Empty(results.Data); Assert.Null(results.FailureReason); - + //Validate CompStatus Log - Assert.Equal("localhost", _receivedCompStatus.ComputerName); + Assert.Equal(TargetName, _receivedCompStatus.ComputerName); Assert.Equal(nameof(CertAbuseProcessor.ProcessRegistryEnrollmentPermissions), _receivedCompStatus.Task); Assert.Equal(CSVComputerStatus.StatusSuccess, _receivedCompStatus.Status); Assert.Equal(TargetDomainSid, _receivedCompStatus.ObjectId); } - + [Fact] public async Task CertAbuseProcessor_ProcessRegistryEnrollmentPermissions_HandlesFailedLookup() { const string subKey = $"SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\{CAName}"; const string subValue = "Security"; - + _mockRegistryAccessor .Setup(ra => ra.GetRegistryKeyData( TargetName, subKey, subValue)) - .Returns(new RegistryResult { Collected = false, FailureReason = FailureReason }); + .Returns(new RegistryResult + { + Collected = false, + FailureReason = FailureReason + }); var results = await _certAbuseProcessor.ProcessRegistryEnrollmentPermissions(CAName, DomainName, TargetName, TargetDomainSid); @@ -309,19 +334,68 @@ public async Task CertAbuseProcessor_ProcessRegistryEnrollmentPermissions_Handle Assert.Equal(FailureReason, _receivedCompStatus.Status); Assert.Equal(TargetDomainSid, _receivedCompStatus.ObjectId); } - + + [WindowsOnlyFact] + public async Task CertAbuseProcessor_ProcessRegistryEnrollmentPermissions_ReturnsEmpty_WhenNoOwnerAndNoRules() { + var mockSecurityDescriptor = new Mock(null); + var mockSamServer = new Mock(); + + const string subKey = $"SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\{CAName}"; + const string subValue = "Security"; + + //setup binary security descriptor as registry value + var descriptor = new RawSecurityDescriptor( + ControlFlags.DiscretionaryAclPresent, + null, + null, + null, + null); + + byte[] regValue = new byte[descriptor.BinaryLength]; + descriptor.GetBinaryForm(regValue, 0); + + _mockRegistryAccessor + .Setup(ra => ra.GetRegistryKeyData( + TargetName, + subKey, + subValue)) + .Returns(new RegistryResult + { + Collected = true, + Value = regValue + }); + + _mockLdapUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); + + _mockSAMServerAccessor.Setup(x => x.OpenServer(It.IsAny(), It.IsAny())) + .Returns(SharpHoundRPC.Result.Ok(mockSamServer.Object)); + mockSamServer.Setup(x => x.GetMachineSid(It.IsAny(), It.IsAny())) + .Returns(new SecurityIdentifier(TargetDomainSid)); + + //get access rules returns empty + mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns([]); + + var results = await _certAbuseProcessor.ProcessRegistryEnrollmentPermissions(CAName, DomainName, TargetName, TargetDomainSid); + + //Validate result + Assert.True(results.Collected); + Assert.Empty(results.Data); + Assert.Null(results.FailureReason); + } + [Fact] public async Task CertAbuseProcessor_ProcessCertTemplates_ReturnsResolvedAndUnresolvedTemplates() { const string validCN = "ValidCN"; const string invalidCN = "InvalidCN"; - + _mockLdapUtils .Setup(x => x.ResolveCertTemplateByProperty(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync((string cn, string _, string _) => cn == validCN ? (true, new TypedPrincipal("test guid", Label.CertTemplate)) : (false, null)); - + var results = await _certAbuseProcessor.ProcessCertTemplates([validCN, invalidCN], DomainName); var expectedTemplate = new TypedPrincipal("test guid", Label.CertTemplate); @@ -330,46 +404,378 @@ public async Task CertAbuseProcessor_ProcessCertTemplates_ReturnsResolvedAndUnre Assert.Single(results.unresolvedTemplates); Assert.Contains(invalidCN, results.unresolvedTemplates); } - - [WindowsOnlyTheory] - [InlineData("S-1-5-80")] - [InlineData("S-1-5-82")] - [InlineData("S-1-5-90")] - [InlineData("S-1-5-96")] - public async Task CertAbuseProcessor_GetRegistryPrincipal_ReturnsFalseForFilteredSID(string sidValue) { - var sid = new SecurityIdentifier(sidValue); - + + [WindowsOnlyFact] + public async Task CertAbuseProcessor_GetRegistryPrincipal_ReturnsFalseForFilteredSID() { + var sid = new SecurityIdentifier("S-1-5-3"); + var results = await _certAbuseProcessor.GetRegistryPrincipal( sid, DomainName, TargetName, true, TargetDomainSid, - new SecurityIdentifier("S-1-5-18") + null ); Assert.Equal((false, null), results); + _mockLdapUtils.VerifyNoOtherCalls(); } - + [WindowsOnlyFact] - public async Task CertAbuseProcessor_GetRegistryPrincipal_ResolvedDomainController_ReturnsTrue() { + public async Task CertAbuseProcessor_GetRegistryPrincipal_CallsResolveIDAndType_ForDomainController() { var expectedPrincipalType = Label.Group; var expectedPrincipalSID = "S-1-5-21-3130019616-2776909439-2417379446-512"; - var sid = new SecurityIdentifier(expectedPrincipalSID); - + _mockLdapUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) .ReturnsAsync((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType))); - + + var sid = new SecurityIdentifier(expectedPrincipalSID); + var results = await _certAbuseProcessor.GetRegistryPrincipal( sid, DomainName, TargetName, true, TargetDomainSid, - new SecurityIdentifier("S-1-5-18") + null + ); + + Assert.Equal((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType)), results); + + _mockLdapUtils.Verify( + x => x.ResolveIDAndType(It.IsAny(), It.IsAny()), + Times.Once); + + _mockLdapUtils.VerifyNoOtherCalls(); + } + + [WindowsOnlyFact] + public async Task CertAbuseProcessor_GetRegistryPrincipal_CallsConvertLocalWellKnownPrincipal_ForNonDomainController() { + var expectedPrincipalType = Label.Group; + var expectedPrincipalSID = "S-1-5-21-3130019616-2776909439-2417379446-512"; + + _mockLdapUtils.Setup(x => x.ConvertLocalWellKnownPrincipal(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType))); + + var sid = new SecurityIdentifier(expectedPrincipalSID); + + var results = await _certAbuseProcessor.GetRegistryPrincipal( + sid, + DomainName, + TargetName, + false, + TargetDomainSid, + null + ); + + Assert.Equal((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType)), results); + + _mockLdapUtils.Verify( + x => x.ConvertLocalWellKnownPrincipal(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Once); + _mockLdapUtils.VerifyNoOtherCalls(); + } + + [WindowsOnlyFact] + public async Task CertAbuseProcessor_GetRegistryPrincipal_ResolvesToLocalPrincipal_ForLocalSID() { + var expectedPrincipalType = Label.LocalGroup; + var expectedPrincipalSID = $"{TargetDomainSid}-123"; + + _mockLdapUtils.Setup(x => x.ConvertLocalWellKnownPrincipal(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((false, null)); + + var sid = new SecurityIdentifier(expectedPrincipalSID); + + var results = await _certAbuseProcessor.GetRegistryPrincipal( + sid, + DomainName, + TargetName, + false, + TargetDomainSid, + new SecurityIdentifier(TargetDomainSid) + ); + + Assert.Equal((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType)), results); + + _mockLdapUtils.Verify( + x => x.ConvertLocalWellKnownPrincipal(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Once); + _mockLdapUtils.VerifyNoOtherCalls(); + } + + [WindowsOnlyFact] + public async Task CertAbuseProcessor_GetRegistryPrincipal_ResolvesToDomainPrincipal() { + var expectedPrincipalType = Label.Group; + var expectedPrincipalSID = "S-1-5-21-3130019616-2776909439-2417379446-512"; + + _mockLdapUtils.Setup(x => x.ConvertLocalWellKnownPrincipal(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((false, null)); + _mockLdapUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) + .ReturnsAsync((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType))); + + var sid = new SecurityIdentifier(expectedPrincipalSID); + + var results = await _certAbuseProcessor.GetRegistryPrincipal( + sid, + DomainName, + TargetName, + false, + TargetDomainSid, + null ); Assert.Equal((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType)), results); + + _mockLdapUtils.Verify( + x => x.ConvertLocalWellKnownPrincipal(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Once); + _mockLdapUtils.Verify( + x => x.ResolveIDAndType(It.IsAny(), It.IsAny()), + Times.Once); + _mockLdapUtils.VerifyNoOtherCalls(); + } + + [Fact] + public void CertAbuseProcessor_OpenSamServer_CallsOpenServer_Failure() { + _mockSAMServerAccessor.Setup(x => x.OpenServer(It.IsAny(), It.IsAny())) + .Returns(SharpHoundRPC.Result.Fail("Connection Failed")); + + var result = _certAbuseProcessor.OpenSamServer(TargetName); + + Assert.True(result.IsFailed); + Assert.Null(result.Value); + } + + [Fact] + public void CertAbuseProcessor_OpenSamServer_CallsOpenServer_Success() { + _mockSAMServerAccessor.Setup(x => x.OpenServer(It.IsAny(), It.IsAny())) + .Returns(SharpHoundRPC.Result.Ok(new SAMServer(null, "TestServer"))); + + var result = _certAbuseProcessor.OpenSamServer(TargetName); + + Assert.True(result.IsSuccess); + Assert.IsType(result.Value); + } + + [WindowsOnlyFact] + public async Task CertAbuseProcessor_GetMachineSid_ReturnsCachedValue() { + Cache.AddMachineSid(TargetDomainSid, TargetDomainSid); + + var result = await _certAbuseProcessor.GetMachineSid(TargetName, TargetDomainSid); + + Assert.Equal(TargetDomainSid, result.Value); + Assert.Null(_receivedCompStatus); + } + + [Fact] + public async Task CertAbuseProcessor_GetMachineSid_OpenSAMFailure_ReturnsNull() { + var error = "Connection Failed"; + _mockSAMServerAccessor.Setup(x => x.OpenServer(It.IsAny(), It.IsAny())) + .Returns(SharpHoundRPC.Result.Fail(error)); + + var result = await _certAbuseProcessor.GetMachineSid(TargetName, TargetDomainSid); + + //Validate result + Assert.Null(result); + + //Validate CompStatus Log + Assert.Equal(TargetName, _receivedCompStatus.ComputerName); + Assert.Equal(nameof(_certAbuseProcessor.OpenSamServer), _receivedCompStatus.Task); + Assert.Equal(error, _receivedCompStatus.Status); + Assert.Equal(TargetDomainSid, _receivedCompStatus.ObjectId); + } + + [WindowsOnlyFact] + public async Task CertAbuseProcessor_GetMachineSid_GetMachineSidFailure_ReturnsNull() { + var mockSamServer = new Mock(); + var error = "Sid Lookup Failed"; + + _mockSAMServerAccessor.Setup(x => x.OpenServer(It.IsAny(), It.IsAny())) + .Returns(SharpHoundRPC.Result.Ok(mockSamServer.Object)); + mockSamServer.Setup(x => x.GetMachineSid(It.IsAny(), It.IsAny())) + .Returns(SharpHoundRPC.Result.Fail(error)); + + var result = await _certAbuseProcessor.GetMachineSid(TargetName, TargetDomainSid); + + //Validate result + Assert.Null(result); + + //Validate CompStatus Log + Assert.Equal(TargetName, _receivedCompStatus.ComputerName); + Assert.Equal(nameof(_certAbuseProcessor.GetMachineSid), _receivedCompStatus.Task); + Assert.Equal(error, _receivedCompStatus.Status); + Assert.Equal(TargetDomainSid, _receivedCompStatus.ObjectId); + } + + [WindowsOnlyFact] + public async Task CertAbuseProcessor_GetMachineSid_ReturnsSid() { + var mockSamServer = new Mock(); + + _mockSAMServerAccessor.Setup(x => x.OpenServer(It.IsAny(), It.IsAny())) + .Returns(SharpHoundRPC.Result.Ok(mockSamServer.Object)); + mockSamServer.Setup(x => x.GetMachineSid(It.IsAny(), It.IsAny())) + .Returns(new SecurityIdentifier(TargetDomainSid)); + + var result = await _certAbuseProcessor.GetMachineSid(TargetName, TargetDomainSid); + + //Validate result + Assert.Equal(TargetDomainSid, result.Value); + + //Validate CompStatus Log + Assert.Equal(TargetName, _receivedCompStatus.ComputerName); + Assert.Equal(nameof(_certAbuseProcessor.GetMachineSid), _receivedCompStatus.Task); + Assert.Equal(ComputerStatus.Success, _receivedCompStatus.Status); + Assert.Equal(TargetDomainSid, _receivedCompStatus.ObjectId); + } + + [WindowsOnlyFact] + public async Task CertAbuseProcessor_CreateEnrollmentAgentRestriction_NullOpaque_ReturnsFalse() { + var nullOpaqueAce = new CommonAce( + AceFlags.None, + AceQualifier.AccessAllowed, + 0x0000, + new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null), + false, + null + ); + var sid = new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null); + + var result = await _certAbuseProcessor.CreateEnrollmentAgentRestriction(nullOpaqueAce, TargetName, DomainName, false, TargetDomainSid, sid); + + //Validate result + Assert.False(result.success); + Assert.Null(result.restriction); + _mockLdapUtils.VerifyNoOtherCalls(); + } + + [WindowsOnlyFact] + public async Task CertAbuseProcessor_CreateEnrollmentAgentRestriction_UnresolvedTemplate_ReturnsFalse() { + var emptyOpaqueAce = new CommonAce( + AceFlags.None, + AceQualifier.AccessAllowed, + 0x0000, + new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null), + true, + new byte[] + { + 1, 0, 0, 0, //sid count + 1, 0, 0, 0, 0, 0, 0, 0, //target sid + 0, 0, 0, 0 //template + } + ); + var sid = new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null); + + _mockLdapUtils.Setup(x => x.ResolveCertTemplateByProperty(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((false, null)); + + var result = await _certAbuseProcessor.CreateEnrollmentAgentRestriction(emptyOpaqueAce, TargetName, DomainName, false, TargetDomainSid, sid); + + //Validate result + Assert.False(result.success); + Assert.Null(result.restriction); + } + + [WindowsOnlyFact] + public async Task CertAbuseProcessor_CreateEnrollmentAgentRestriction_NoTemplate_ReturnsAllTemplates() { + var emptyOpaqueAce = new CommonAce( + AceFlags.None, + AceQualifier.AccessAllowed, + 0x0000, + new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null), + true, + new byte[] + { + 2, 0, 0, 0, //sid count + 1, 0, 0, 0, 0, 0, 0, 1, //target sid S-1-1 + 1, 0, 0, 0, 0, 0, 0, 3 //target sid S-1-3 + } + ); + var sid = new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null); + + _mockLdapUtils.Setup(x => x.ResolveIDAndType("S-1-1", It.IsAny())) + .ReturnsAsync((true, new TypedPrincipal("S-1-1", Label.User))); + _mockLdapUtils.Setup(x => x.ResolveIDAndType("S-1-3", It.IsAny())) + .ReturnsAsync((true, new TypedPrincipal("S-1-3", Label.User))); + + var result = await _certAbuseProcessor.CreateEnrollmentAgentRestriction(emptyOpaqueAce, nameof(Label.User), DomainName, false, TargetDomainSid, sid); + + //Validate result + Assert.True(result.success); + Assert.True(result.restriction.AllTemplates); + Assert.Null(result.restriction.Template); + Assert.Equal(2, result.restriction.Targets.Length); + Assert.Contains(result.restriction.Targets, t => t.ObjectIdentifier == "S-1-1"); + Assert.Contains(result.restriction.Targets, t => t.ObjectIdentifier == "S-1-3"); + } + + [WindowsOnlyFact] + public async Task CertAbuseProcessor_CreateEnrollmentAgentRestriction_WithCanonicalName_ReturnsTemplate() { + var expectedPrincipalType = Label.CertTemplate; + var templateOID = "E4B7F0B1-27E5-4C0F-A5C9-641A67171D05"; + + var emptyOpaqueAce = new CommonAce( + AceFlags.None, + AceQualifier.AccessAllowed, + 0x0000, + new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null), + true, + new byte[] + { + 1, 0, 0, 0, //sid count + 1, 0, 0, 0, 0, 0, 0, 0, //target sid + 77, 0, 97, 0, 99, 0, 104, 0, 105, 0, 110, 0, 101, 0, 0, 0 //Computer Template + } + ); + var sid = new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null); + + _mockLdapUtils.Setup(x => x.ResolveCertTemplateByProperty(It.IsAny(), LDAPProperties.CanonicalName, It.IsAny())) + .ReturnsAsync((true, new TypedPrincipal(templateOID, expectedPrincipalType))); + + var result = await _certAbuseProcessor.CreateEnrollmentAgentRestriction(emptyOpaqueAce, TargetName, DomainName, false, TargetDomainSid, sid); + + Assert.True(result.success); + Assert.False(result.restriction.AllTemplates); + Assert.NotNull(result.restriction.Template); + _mockLdapUtils.Verify( + x => x.ResolveCertTemplateByProperty(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Once); + } + + [WindowsOnlyFact] + public async Task CertAbuseProcessor_CreateEnrollmentAgentRestriction_WithCertTemplateOID_ReturnsTemplate() { + var expectedPrincipalType = Label.CertTemplate; + var templateOID = "E4B7F0B1-27E5-4C0F-A5C9-641A67171D05"; + + var emptyOpaqueAce = new CommonAce( + AceFlags.None, + AceQualifier.AccessAllowed, + 0x0000, + new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null), + true, + new byte[] + { + 1, 0, 0, 0, //sid count + 1, 0, 0, 0, 0, 0, 0, 0, //target sid + 77, 0, 97, 0, 99, 0, 104, 0, 105, 0, 110, 0, 101, 0, 0, 0 //Computer Template + } + ); + var sid = new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null); + + _mockLdapUtils.Setup(x => x.ResolveCertTemplateByProperty(It.IsAny(), LDAPProperties.CanonicalName, It.IsAny())) + .ReturnsAsync((false, null)); + _mockLdapUtils.Setup(x => x.ResolveCertTemplateByProperty(It.IsAny(), LDAPProperties.CertTemplateOID, It.IsAny())) + .ReturnsAsync((true, new TypedPrincipal(templateOID, expectedPrincipalType))); + + var result = await _certAbuseProcessor.CreateEnrollmentAgentRestriction(emptyOpaqueAce, TargetName, DomainName, false, TargetDomainSid, sid); + + //Validate result + Assert.True(result.success); + Assert.False(result.restriction.AllTemplates); + Assert.NotNull(result.restriction.Template); + _mockLdapUtils.Verify( + x => x.ResolveCertTemplateByProperty(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Exactly(2)); } } } \ No newline at end of file diff --git a/test/unit/CollectionDefinitions/CacheTestsCollectionDefinition.cs b/test/unit/CollectionDefinitions/CacheTestsCollectionDefinition.cs new file mode 100644 index 000000000..c03cbc593 --- /dev/null +++ b/test/unit/CollectionDefinitions/CacheTestsCollectionDefinition.cs @@ -0,0 +1,9 @@ +using Xunit; + +namespace CommonLibTest.CollectionDefinitions; + +/// +/// Test that use cache cannot run in parallel, they can have flaky behavior +/// +[CollectionDefinition(nameof(CacheTestCollectionDefinition), DisableParallelization = true)] +public class CacheTestCollectionDefinition{} diff --git a/test/unit/CommonLibHelperTests.cs b/test/unit/CommonLibHelperTests.cs index e2cfe24f3..0cad80e12 100644 --- a/test/unit/CommonLibHelperTests.cs +++ b/test/unit/CommonLibHelperTests.cs @@ -1,4 +1,5 @@ using System; +using System.Security.Principal; using System.Text; using System.Threading.Tasks; using SharpHoundCommonLib; @@ -293,5 +294,82 @@ public async Task RetryOnException_SucceedsOnLastAttempt() { Assert.True(success); } + + [Fact] + public void DomainNameToDistinguishedName_DotsBecomeDcComponents() + { + var result = Helpers.DomainNameToDistinguishedName("test.local"); + Assert.Equal("DC=test,DC=local", result); + } + + [Theory] + [InlineData("S-1-5-32-544", "\\01\\02\\00\\00\\00\\00\\00\\05\\20\\00\\00\\00\\20\\02\\00\\00")] + public void ConvertSidToHexSid_ValidSid_MatchesSecurityIdentifierBinaryForm(string sid, string expectedHexSid) + { + // Arrange & Act + var actual = Helpers.ConvertSidToHexSid(sid); + + // Assert + Assert.Equal(expectedHexSid, actual); + return; + + static string BuildExpectedHexSid(string sid) + { + var securityIdentifier = new SecurityIdentifier(sid); + var sidBytes = new byte[securityIdentifier.BinaryLength]; + securityIdentifier.GetBinaryForm(sidBytes, 0); + return $"\\{BitConverter.ToString(sidBytes).Replace('-', '\\')}"; + } + } + + [Fact] + public void ConvertSidToHexSid_InvalidSid_Throws() + { + Assert.ThrowsAny(() => Helpers.ConvertSidToHexSid("NOT-A-SID")); + } + + [Theory] + [InlineData("s-1-5-18")] + [InlineData("S-1-5-18")] + public void IsSidFiltered_FilteredWellKnownSidsCaseInsensitive_ReturnsTrue(string sid) + { + Assert.True(Helpers.IsSidFiltered(sid)); + } + + [Theory] + [InlineData("S-1-5-80-1234567890")] + [InlineData("S-1-5-82-1234567890")] + [InlineData("S-1-5-90-0")] + [InlineData("S-1-5-96-0")] + public void IsSidFiltered_FilteredPrefixes_ReturnsTrue(string sid) + { + Assert.True(Helpers.IsSidFiltered(sid)); + } + + [Theory] + [InlineData("S-1-5-21-1234567890")] + [InlineData("S-1-5-21")] + public void IsSidFiltered_ReturnsFalse(string sid) + { + Assert.False(Helpers.IsSidFiltered(sid)); + } + + [Fact] + public void ConvertLdapTimeToLong_Null_ReturnsMinusOne() + { + Assert.Equal(-1, Helpers.ConvertLdapTimeToLong(null)); + } + + [Fact] + public void ConvertLdapTimeToLong_InvalidNumber_ThrowsFormatException() + { + Assert.Throws(() => Helpers.ConvertLdapTimeToLong("not-a-number")); + } + + [Fact] + public void ConvertLdapTimeToLong_ValidNumber_Parses() + { + Assert.Equal(123456789L, Helpers.ConvertLdapTimeToLong("123456789")); + } } } \ No newline at end of file diff --git a/test/unit/CommonLibTests.cs b/test/unit/CommonLibTests.cs new file mode 100644 index 000000000..8f1f595e0 --- /dev/null +++ b/test/unit/CommonLibTests.cs @@ -0,0 +1,91 @@ +using System; +using System.Reflection; +using CommonLibTest.CollectionDefinitions; +using Microsoft.Extensions.Logging; +using Moq; +using SharpHoundCommonLib; +using Xunit; + +namespace CommonLibTest; + +[Collection(nameof(CacheTestCollectionDefinition))] +public class CommonLibTests +{ + + public CommonLibTests() + { + ResetCommonLibState(); + } + + [Fact] + public void InitializeCommonLib_FirstCallWithoutCache_CreatesAndSetsCacheInstance() + { + // Arrange & Act + CommonLib.InitializeCommonLib(); + + // Assert + var cache = Cache.GetCacheInstance(); + Assert.NotNull(cache); + Assert.NotNull(cache.IdToTypeCache); + Assert.NotNull(cache.ValueToIdCache); + Assert.NotNull(cache.GlobalCatalogCache); + Assert.NotNull(cache.MachineSidCache); + Assert.NotNull(cache.SIDToDomainCache); + } + + [Fact] + public void InitializeCommonLib_UsesProvidedInstance() + { + // Arrange + var provided = Cache.CreateNewCache(); + + // Act + CommonLib.InitializeCommonLib(cache: provided); + + // Assert + Assert.Same(provided, Cache.GetCacheInstance()); + } + + [Fact] + public void InitializeCommonLib_2Calls_LogsWarningAndDoesNotReplaceCache() + { + // Arrange + var cache1 = Cache.CreateNewCache(); + CommonLib.InitializeCommonLib(cache: cache1); + + var cache2 = Cache.CreateNewCache(); + var logger = new Mock(); + + // Act + CommonLib.InitializeCommonLib(logger.Object, cache2); + + // Assert + Assert.Same(cache1, Cache.GetCacheInstance()); // cache1 should be then one used since lib was already initialized + + logger.Verify(x => x.Log( + LogLevel.Warning, + It.IsAny(), + It.Is((v, _) => + v.ToString() != null && + v.ToString().Contains("already initialized", StringComparison.InvariantCultureIgnoreCase)), + It.IsAny(), + It.IsAny>()), + Times.Once()); + } + + private static void ResetCommonLibState() + { + // Reset CommonLib._initialized (private static) + var commonLibType = typeof(CommonLib); + var initializedField = commonLibType.GetField("_initialized", + BindingFlags.Static | BindingFlags.NonPublic); + + if (initializedField == null) + throw new InvalidOperationException("CommonLib _initialized field not found"); + + initializedField.SetValue(null, false); + + // Reset cache singleton so tests don't leak state into each other + Cache.SetCacheInstance(null); + } +} \ No newline at end of file diff --git a/test/unit/UserRightsAssignmentProcessorTest.cs b/test/unit/UserRightsAssignmentProcessorTest.cs index 459c06bf4..14fbc9f8c 100644 --- a/test/unit/UserRightsAssignmentProcessorTest.cs +++ b/test/unit/UserRightsAssignmentProcessorTest.cs @@ -1,146 +1,151 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using CommonLibTest.Facades; -using CommonLibTest.Facades.LSAMocks.DCMocks; -using CommonLibTest.Facades.LSAMocks.WorkstationMocks; -using Moq; -using Newtonsoft.Json; -using SharpHoundCommonLib; -using SharpHoundCommonLib.Enums; -using SharpHoundCommonLib.Processors; -using SharpHoundRPC; -using Xunit; -using Xunit.Abstractions; - -namespace CommonLibTest -{ - public class UserRightsAssignmentProcessorTest - { - private readonly ITestOutputHelper _testOutputHelper; - - public UserRightsAssignmentProcessorTest(ITestOutputHelper testOutputHelper) - { - _testOutputHelper = testOutputHelper; - } - - [WindowsOnlyFact] - public async Task UserRightsAssignmentProcessor_TestWorkstation() - { - var mockProcessor = new Mock(new MockLdapUtils(), null); - var mockLSAPolicy = new MockWorkstationLSAPolicy(); - mockProcessor.Setup(x => x.OpenLSAPolicy(It.IsAny())).Returns(mockLSAPolicy); - var processor = mockProcessor.Object; - var machineDomainSid = $"{Consts.MockDomainSid}-1001"; - var results = await processor.GetUserRightsAssignments("win10.testlab.local", machineDomainSid, "testlab.local", false) - .ToArrayAsync(); - - var privilege = results[0]; - Assert.Equal(LSAPrivileges.RemoteInteractiveLogon, privilege.Privilege); - Assert.Equal(3, results[0].Results.Length); - var adminResult = privilege.Results.First(x => x.ObjectIdentifier.EndsWith("-544")); - Assert.Equal($"{machineDomainSid}-544", adminResult.ObjectIdentifier); - Assert.Equal(Label.LocalGroup, adminResult.ObjectType); - var rdpResult = privilege.Results.First(x => x.ObjectIdentifier.EndsWith("-555")); - Assert.Equal($"{machineDomainSid}-555", rdpResult.ObjectIdentifier); - Assert.Equal(Label.LocalGroup, rdpResult.ObjectType); - } - - [WindowsOnlyFact] - public async Task UserRightsAssignmentProcessor_TestDC() - { - var mockProcessor = new Mock(new MockLdapUtils(), null); - var mockLSAPolicy = new MockDCLSAPolicy(); - mockProcessor.Setup(x => x.OpenLSAPolicy(It.IsAny())).Returns(mockLSAPolicy); - var processor = mockProcessor.Object; - var machineDomainSid = $"{Consts.MockDomainSid}-1000"; - var results = await processor.GetUserRightsAssignments("primary.testlab.local", machineDomainSid, "testlab.local", true) - .ToArrayAsync(); - - var privilege = results[0]; - _testOutputHelper.WriteLine(JsonConvert.SerializeObject(privilege)); - Assert.Equal(LSAPrivileges.RemoteInteractiveLogon, privilege.Privilege); - Assert.Single(results[0].Results); - var adminResult = privilege.Results.First(x => x.ObjectIdentifier.EndsWith("-544")); - Assert.Equal("TESTLAB.LOCAL-S-1-5-32-544", adminResult.ObjectIdentifier); - Assert.Equal(Label.Group, adminResult.ObjectType); - } - - // Obsolete by AdaptiveTimeout - // [Fact] - // public async Task UserRightsAssignmentProcessor_TestTimeout() { - // var mockProcessor = new Mock(new MockLdapUtils(), null); - // mockProcessor.Setup(x => x.OpenLSAPolicy(It.IsAny())).Returns(()=> { - // Task.Delay(100).Wait(); - // return NtStatus.StatusAccessDenied; - // }); - // var processor = mockProcessor.Object; - // var machineDomainSid = $"{Consts.MockDomainSid}-1000"; - // var receivedStatus = new List(); - // processor.ComputerStatusEvent += status => { - // receivedStatus.Add(status); - // return Task.CompletedTask; - // }; - // var results = await processor.GetUserRightsAssignments("primary.testlab.local", machineDomainSid, "testlab.local", true, null) - // .ToArrayAsync(); - // Assert.Empty(results); - // Assert.Single(receivedStatus); - // var status = receivedStatus[0]; - // Assert.Equal("Timeout", status.Status); - // } - - [WindowsOnlyFact] - public async Task UserRightsAssignmentProcessor_TestGetLocalDomainInformationFail() - { - var mockProcessor = new Mock(new MockLdapUtils(), null); - var mockLSAPolicy = new MockFailLSAPolicy_GetLocalDomainInformation(); - mockProcessor.Setup(x => x.OpenLSAPolicy(It.IsAny())).Returns(()=> { - Task.Delay(100).Wait(); - return NtStatus.StatusAccessDenied; - }); - mockProcessor.Setup(x => x.OpenLSAPolicy(It.IsAny())).Returns(mockLSAPolicy); - var processor = mockProcessor.Object; - var machineDomainSid = $"{Consts.MockDomainSid}-1001"; - var receivedStatus = new List(); - processor.ComputerStatusEvent += async status => { - receivedStatus.Add(status); - }; - var results = await processor.GetUserRightsAssignments("win10.testlab.local", machineDomainSid, "testlab.local", false) - .ToArrayAsync(); - - Assert.Empty(results); - Assert.Single(receivedStatus); - var status = receivedStatus[0]; - Assert.Equal("StatusAccessDenied", status.Status); - Assert.Equal("LSAGetMachineSID", status.Task); - } - - [WindowsOnlyFact] - public async Task UserRightsAssignmentProcessor_TestGetResolvedPrincipalsWithPrivilegeFail() - { - var mockProcessor = new Mock(new MockLdapUtils(), null); - var mockLSAPolicy = new MockFailLSAPolicy_GetResolvedPrincipalsWithPrivilege(); - mockProcessor.Setup(x => x.OpenLSAPolicy(It.IsAny())).Returns(mockLSAPolicy); - var processor = mockProcessor.Object; - var machineDomainSid = $"{Consts.MockDomainSid}-1001"; - var receivedStatus = new List(); - processor.ComputerStatusEvent += async status => { - receivedStatus.Add(status); - }; - var results = await processor.GetUserRightsAssignments("win10.testlab.local", machineDomainSid, "testlab.local", false) - .ToArrayAsync(); - - Assert.Single(results); - - var result = results[0]; - Assert.False(result.Collected); - Assert.Equal("LSAEnumerateAccountsWithUserRights returned StatusAccessDenied", result.FailureReason); - Assert.Single(receivedStatus); - var status = receivedStatus[0]; - Assert.Equal("StatusAccessDenied", status.Status); - Assert.Equal("LSAEnumerateAccountsWithUserRight", status.Task); - } - } +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using CommonLibTest.CollectionDefinitions; +using CommonLibTest.Facades; +using CommonLibTest.Facades.LSAMocks.DCMocks; +using CommonLibTest.Facades.LSAMocks.WorkstationMocks; +using Moq; +using Newtonsoft.Json; +using SharpHoundCommonLib; +using SharpHoundCommonLib.Enums; +using SharpHoundCommonLib.Processors; +using SharpHoundRPC; +using Xunit; +using Xunit.Abstractions; + +namespace CommonLibTest +{ + [Collection(nameof(CacheTestCollectionDefinition))] + public class UserRightsAssignmentProcessorTest + { + private readonly ITestOutputHelper _testOutputHelper; + + public UserRightsAssignmentProcessorTest(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + + //reseting cache + Cache.SetCacheInstance(null); + } + + [WindowsOnlyFact] + public async Task UserRightsAssignmentProcessor_TestWorkstation() + { + var mockProcessor = new Mock(new MockLdapUtils(), null); + var mockLSAPolicy = new MockWorkstationLSAPolicy(); + mockProcessor.Setup(x => x.OpenLSAPolicy(It.IsAny())).Returns(mockLSAPolicy); + var processor = mockProcessor.Object; + var machineDomainSid = $"{Consts.MockDomainSid}-1001"; + var results = await processor.GetUserRightsAssignments("win10.testlab.local", machineDomainSid, "testlab.local", false) + .ToArrayAsync(); + + var privilege = results[0]; + Assert.Equal(LSAPrivileges.RemoteInteractiveLogon, privilege.Privilege); + Assert.Equal(3, results[0].Results.Length); + var adminResult = privilege.Results.First(x => x.ObjectIdentifier.EndsWith("-544")); + Assert.Equal($"{machineDomainSid}-544", adminResult.ObjectIdentifier); + Assert.Equal(Label.LocalGroup, adminResult.ObjectType); + var rdpResult = privilege.Results.First(x => x.ObjectIdentifier.EndsWith("-555")); + Assert.Equal($"{machineDomainSid}-555", rdpResult.ObjectIdentifier); + Assert.Equal(Label.LocalGroup, rdpResult.ObjectType); + } + + [WindowsOnlyFact] + public async Task UserRightsAssignmentProcessor_TestDC() + { + var mockProcessor = new Mock(new MockLdapUtils(), null); + var mockLSAPolicy = new MockDCLSAPolicy(); + mockProcessor.Setup(x => x.OpenLSAPolicy(It.IsAny())).Returns(mockLSAPolicy); + var processor = mockProcessor.Object; + var machineDomainSid = $"{Consts.MockDomainSid}-1000"; + var results = await processor.GetUserRightsAssignments("primary.testlab.local", machineDomainSid, "testlab.local", true) + .ToArrayAsync(); + + var privilege = results[0]; + _testOutputHelper.WriteLine(JsonConvert.SerializeObject(privilege)); + Assert.Equal(LSAPrivileges.RemoteInteractiveLogon, privilege.Privilege); + Assert.Single(results[0].Results); + var adminResult = privilege.Results.First(x => x.ObjectIdentifier.EndsWith("-544")); + Assert.Equal("TESTLAB.LOCAL-S-1-5-32-544", adminResult.ObjectIdentifier); + Assert.Equal(Label.Group, adminResult.ObjectType); + } + + // Obsolete by AdaptiveTimeout + // [Fact] + // public async Task UserRightsAssignmentProcessor_TestTimeout() { + // var mockProcessor = new Mock(new MockLdapUtils(), null); + // mockProcessor.Setup(x => x.OpenLSAPolicy(It.IsAny())).Returns(()=> { + // Task.Delay(100).Wait(); + // return NtStatus.StatusAccessDenied; + // }); + // var processor = mockProcessor.Object; + // var machineDomainSid = $"{Consts.MockDomainSid}-1000"; + // var receivedStatus = new List(); + // processor.ComputerStatusEvent += status => { + // receivedStatus.Add(status); + // return Task.CompletedTask; + // }; + // var results = await processor.GetUserRightsAssignments("primary.testlab.local", machineDomainSid, "testlab.local", true, null) + // .ToArrayAsync(); + // Assert.Empty(results); + // Assert.Single(receivedStatus); + // var status = receivedStatus[0]; + // Assert.Equal("Timeout", status.Status); + // } + + [WindowsOnlyFact] + public async Task UserRightsAssignmentProcessor_TestGetLocalDomainInformationFail() + { + var mockProcessor = new Mock(new MockLdapUtils(), null); + var mockLSAPolicy = new MockFailLSAPolicy_GetLocalDomainInformation(); + mockProcessor.Setup(x => x.OpenLSAPolicy(It.IsAny())).Returns(()=> { + Task.Delay(100).Wait(); + return NtStatus.StatusAccessDenied; + }); + mockProcessor.Setup(x => x.OpenLSAPolicy(It.IsAny())).Returns(mockLSAPolicy); + var processor = mockProcessor.Object; + var machineDomainSid = $"{Consts.MockDomainSid}-1001"; + var receivedStatus = new List(); + processor.ComputerStatusEvent += async status => { + receivedStatus.Add(status); + }; + var results = await processor.GetUserRightsAssignments("win10.testlab.local", machineDomainSid, "testlab.local", false) + .ToArrayAsync(); + + Assert.Empty(results); + Assert.Single(receivedStatus); + var status = receivedStatus[0]; + Assert.Equal("StatusAccessDenied", status.Status); + Assert.Equal("LSAGetMachineSID", status.Task); + } + + [WindowsOnlyFact] + public async Task UserRightsAssignmentProcessor_TestGetResolvedPrincipalsWithPrivilegeFail() + { + var mockProcessor = new Mock(new MockLdapUtils(), null); + var mockLSAPolicy = new MockFailLSAPolicy_GetResolvedPrincipalsWithPrivilege(); + mockProcessor.Setup(x => x.OpenLSAPolicy(It.IsAny())).Returns(mockLSAPolicy); + var processor = mockProcessor.Object; + var machineDomainSid = $"{Consts.MockDomainSid}-1001"; + var receivedStatus = new List(); + processor.ComputerStatusEvent += async status => { + receivedStatus.Add(status); + }; + var results = await processor.GetUserRightsAssignments("win10.testlab.local", machineDomainSid, "testlab.local", false) + .ToArrayAsync(); + + Assert.Single(results); + + var result = results[0]; + Assert.False(result.Collected); + Assert.Equal("LSAEnumerateAccountsWithUserRights returned StatusAccessDenied", result.FailureReason); + Assert.Single(receivedStatus); + var status = receivedStatus[0]; + Assert.Equal("StatusAccessDenied", status.Status); + Assert.Equal("LSAEnumerateAccountsWithUserRight", status.Task); + } + } } \ No newline at end of file