diff --git a/src/CommonLib/Helpers.cs b/src/CommonLib/Helpers.cs index 6b89ae9c9..19a51a37c 100644 --- a/src/CommonLib/Helpers.cs +++ b/src/CommonLib/Helpers.cs @@ -7,10 +7,6 @@ using System.Text.RegularExpressions; using SharpHoundCommonLib.Enums; using Microsoft.Extensions.Logging; -using System.IO; -using System.Security; -using SharpHoundCommonLib.Processors; -using Microsoft.Win32; using System.Threading.Tasks; namespace SharpHoundCommonLib { @@ -151,7 +147,7 @@ public static string DistinguishedNameToDomain(string distinguishedName) { } /// - /// Converts a domain name to a distinguished name using simple string substitution + /// Converts a domain name to a distinguished name using simple string substitution /// /// /// @@ -258,44 +254,6 @@ public static bool IsSidFiltered(string sid) { return false; } - public static RegistryResult GetRegistryKeyData(string target, string subkey, string subvalue, ILogger log) { - var data = new RegistryResult(); - - try { - var baseKey = OpenRemoteRegistry(target); - var value = baseKey.GetValue(subkey, subvalue); - data.Value = value; - - data.Collected = true; - } - catch (IOException e) { - log.LogDebug(e, "Error getting data from registry for {Target}: {RegSubKey}:{RegValue}", - target, subkey, subvalue); - data.FailureReason = "Target machine was not found or not connectable"; - } - catch (SecurityException e) { - log.LogDebug(e, "Error getting data from registry for {Target}: {RegSubKey}:{RegValue}", - target, subkey, subvalue); - data.FailureReason = "User does not have the proper permissions to perform this operation"; - } - catch (UnauthorizedAccessException e) { - log.LogDebug(e, "Error getting data from registry for {Target}: {RegSubKey}:{RegValue}", - target, subkey, subvalue); - data.FailureReason = "User does not have the necessary registry rights"; - } - catch (Exception e) { - log.LogDebug(e, "Error getting data from registry for {Target}: {RegSubKey}:{RegValue}", - target, subkey, subvalue); - data.FailureReason = e.Message; - } - - return data; - } - - public static IRegistryKey OpenRemoteRegistry(string target) { - return SHRegistryKey.Connect(RegistryHive.LocalMachine, target).GetAwaiter().GetResult(); - } - public static string[] AuthenticationOIDs = new string[] { CommonOids.ClientAuthentication, CommonOids.PKINITClientAuthentication, diff --git a/src/CommonLib/IRegistryAccessor.cs b/src/CommonLib/IRegistryAccessor.cs new file mode 100644 index 000000000..0a80174f1 --- /dev/null +++ b/src/CommonLib/IRegistryAccessor.cs @@ -0,0 +1,82 @@ +using System; +using System.IO; +using System.Security; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Win32; +using SharpHoundCommonLib.Processors; + +namespace SharpHoundCommonLib { + public interface IRegistryAccessor { + public RegistryResult GetRegistryKeyData(string target, string subkey, string subvalue); + public IRegistryKey OpenRemoteRegistry(string target); + public Task Connect(RegistryHive hive, string machineName); + } + + public class RegistryAccessor : IRegistryAccessor { + private readonly ILogger _log; + private readonly AdaptiveTimeout _adaptiveTimeout; + + public RegistryAccessor(ILogger log = null) { + _log = log ?? Logging.LogProvider.CreateLogger(nameof(RegistryAccessor)); + _adaptiveTimeout = new AdaptiveTimeout(maxTimeout: TimeSpan.FromSeconds(10), _log); + } + + public RegistryResult GetRegistryKeyData(string target, string subkey, string subvalue) { + var data = new RegistryResult(); + + try { + using (var baseKey = OpenRemoteRegistry(target)) { + var value = baseKey.GetValue(subkey, subvalue); + data.Value = value; + data.Collected = true; + } + } + catch (IOException e) { + _log.LogDebug(e, "Error getting data from registry for {Target}: {RegSubKey}:{RegValue}", + target, subkey, subvalue); + data.FailureReason = "Target machine was not found or not connectable"; + } + catch (SecurityException e) { + _log.LogDebug(e, "Error getting data from registry for {Target}: {RegSubKey}:{RegValue}", + target, subkey, subvalue); + data.FailureReason = "User does not have the proper permissions to perform this operation"; + } + catch (UnauthorizedAccessException e) { + _log.LogDebug(e, "Error getting data from registry for {Target}: {RegSubKey}:{RegValue}", + target, subkey, subvalue); + data.FailureReason = "User does not have the necessary registry rights"; + } + catch (Exception e) { + _log.LogDebug(e, "Error getting data from registry for {Target}: {RegSubKey}:{RegValue}", + target, subkey, subvalue); + data.FailureReason = e.Message; + } + + return data; + } + + public IRegistryKey OpenRemoteRegistry(string target) { + return Connect(RegistryHive.LocalMachine, target).GetAwaiter().GetResult(); + } + + /// + /// Gets a handle to a remote registry. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public async Task Connect(RegistryHive hive, string machineName) { + var remoteKey = await _adaptiveTimeout.ExecuteWithTimeout((_) => RegistryKey.OpenRemoteBaseKey(hive, machineName)); + if (remoteKey.IsSuccess) + return new SHRegistryKey(remoteKey.Value); + throw new TimeoutException($"Failed to connect to registry on {machineName}: {remoteKey.Error}"); + } + } +} \ No newline at end of file diff --git a/src/CommonLib/IRegistryKey.cs b/src/CommonLib/IRegistryKey.cs index fd266265f..510efe32c 100644 --- a/src/CommonLib/IRegistryKey.cs +++ b/src/CommonLib/IRegistryKey.cs @@ -1,18 +1,16 @@ using System; -using System.Threading.Tasks; using Microsoft.Win32; namespace SharpHoundCommonLib { - public interface IRegistryKey { + public interface IRegistryKey: IDisposable { public object GetValue(string subkey, string name); public string[] GetSubKeyNames(); } - public class SHRegistryKey : IRegistryKey, IDisposable { + public class SHRegistryKey : IRegistryKey { private readonly RegistryKey _currentKey; - private static readonly AdaptiveTimeout _adaptiveTimeout = new AdaptiveTimeout(maxTimeout: TimeSpan.FromSeconds(10), Logging.LogProvider.CreateLogger(nameof(SHRegistryKey))); - - private SHRegistryKey(RegistryKey registryKey) { + + public SHRegistryKey(RegistryKey registryKey) { _currentKey = registryKey; } @@ -23,38 +21,8 @@ public object GetValue(string subkey, string name) { public string[] GetSubKeyNames() => _currentKey.GetSubKeyNames(); - /// - /// Gets a handle to a remote registry. - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public static async Task Connect(RegistryHive hive, string machineName) { - var remoteKey = await _adaptiveTimeout.ExecuteWithTimeout((_) => RegistryKey.OpenRemoteBaseKey(hive, machineName)); - if (remoteKey.IsSuccess) - return new SHRegistryKey(remoteKey.Value); - throw new TimeoutException($"Failed to connect to registry on {machineName}: {remoteKey.Error}"); - } - public void Dispose() { _currentKey.Dispose(); } } - - public class MockRegistryKey : IRegistryKey { - public virtual object GetValue(string subkey, string name) { - //Unimplemented - return default; - } - - public virtual string[] GetSubKeyNames() { - throw new NotImplementedException(); - } - } } \ No newline at end of file diff --git a/src/CommonLib/Processors/CertAbuseProcessor.cs b/src/CommonLib/Processors/CertAbuseProcessor.cs index 86513f5af..7da4f46c5 100644 --- a/src/CommonLib/Processors/CertAbuseProcessor.cs +++ b/src/CommonLib/Processors/CertAbuseProcessor.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Security.AccessControl; using System.Security.Principal; using System.Text; @@ -8,7 +7,6 @@ using Microsoft.Extensions.Logging; using SharpHoundCommonLib.Enums; using SharpHoundCommonLib.OutputTypes; -using SharpHoundRPC; using SharpHoundRPC.Wrappers; using Encoder = Microsoft.Security.Application.Encoder; @@ -20,24 +18,29 @@ public class CertAbuseProcessor private readonly ILdapUtils _utils; 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, 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))); } /// /// This function should be called with the security data fetched from . /// The resulting ACEs will contain the owner of the CA as well as Management rights. /// - /// + /// /// /// + /// /// public async Task ProcessRegistryEnrollmentPermissions(string caName, string objectDomain, string computerName, string computerObjectId) { @@ -47,9 +50,23 @@ public async Task ProcessRegistryEnrollmentPermissions(str data.Collected = aceData.Collected; if (!aceData.Collected) { + await SendComputerStatus(new CSVComputerStatus { + Status = aceData.FailureReason, + Task = nameof(ProcessRegistryEnrollmentPermissions), + ComputerName = computerName, + ObjectId = computerObjectId, + }); + data.FailureReason = aceData.FailureReason; return data; } + + await SendComputerStatus(new CSVComputerStatus { + Status = CSVComputerStatus.StatusSuccess, + Task = nameof(ProcessRegistryEnrollmentPermissions), + ComputerName = computerName, + ObjectId = computerObjectId, + }); if (aceData.Value == null) { @@ -167,9 +184,23 @@ public async Task ProcessEAPermissions(string ret.Collected = regData.Collected; if (!ret.Collected) { + await SendComputerStatus(new CSVComputerStatus { + Status = regData.FailureReason, + Task = nameof(ProcessEAPermissions), + ComputerName = computerName, + ObjectId = computerObjectId, + }); + ret.FailureReason = regData.FailureReason; return ret; } + + await SendComputerStatus(new CSVComputerStatus { + Status = CSVComputerStatus.StatusSuccess, + Task = nameof(ProcessEAPermissions), + ComputerName = computerName, + ObjectId = computerObjectId, + }); if (regData.Value == null) { @@ -179,6 +210,12 @@ public async Task ProcessEAPermissions(string var isDomainController = await _utils.IsDomainController(computerObjectId, objectDomain); var machineSid = await GetMachineSid(computerName, computerObjectId); var descriptor = new RawSecurityDescriptor(regData.Value as byte[], 0); + + if (descriptor.DiscretionaryAcl is null) + { + return ret; + } + var enrollmentAgentRestrictions = new List(); foreach (var genericAce in descriptor.DiscretionaryAcl) { @@ -218,13 +255,12 @@ public async Task ProcessEAPermissions(string /// /// /// - [ExcludeFromCodeCoverage] private RegistryResult GetCASecurity(string target, string caName) { var regSubKey = $"SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\{caName}"; const string regValue = "Security"; - return Helpers.GetRegistryKeyData(target, regSubKey, regValue, _log); + return _registryAccessor.GetRegistryKeyData(target, regSubKey, regValue); } /// @@ -233,13 +269,12 @@ private RegistryResult GetCASecurity(string target, string caName) /// /// /// - [ExcludeFromCodeCoverage] private RegistryResult GetEnrollmentAgentRights(string target, string caName) { var regSubKey = $"SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\{caName}"; var regValue = "EnrollmentAgentRights"; - return Helpers.GetRegistryKeyData(target, regSubKey, regValue, _log); + return _registryAccessor.GetRegistryKeyData(target, regSubKey, regValue); } /// @@ -249,22 +284,36 @@ private RegistryResult GetEnrollmentAgentRights(string target, string caName) /// https://blog.keyfactor.com/hidden-dangers-certificate-subject-alternative-names-sans /// /// + /// /// - [ExcludeFromCodeCoverage] - public BoolRegistryAPIResult IsUserSpecifiesSanEnabled(string target, string caName) + public async Task IsUserSpecifiesSanEnabled(string target, string caName, string computerObjectId) { var ret = new BoolRegistryAPIResult(); - var subKey = + var regSubKey = $"SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\{caName}\\PolicyModules\\CertificateAuthority_MicrosoftDefault.Policy"; - const string subValue = "EditFlags"; - var data = Helpers.GetRegistryKeyData(target, subKey, subValue, _log); + const string regValue = "EditFlags"; + var data = _registryAccessor.GetRegistryKeyData(target, regSubKey, regValue); ret.Collected = data.Collected; if (!data.Collected) { + await SendComputerStatus(new CSVComputerStatus { + Status = data.FailureReason, + Task = nameof(IsUserSpecifiesSanEnabled), + ComputerName = target, + ObjectId = computerObjectId + }); + ret.FailureReason = data.FailureReason; return ret; } + + await SendComputerStatus(new CSVComputerStatus { + Status = CSVComputerStatus.StatusSuccess, + Task = nameof(IsUserSpecifiesSanEnabled), + ComputerName = target, + ObjectId = computerObjectId + }); if (data.Value == null) { @@ -278,28 +327,42 @@ public BoolRegistryAPIResult IsUserSpecifiesSanEnabled(string target, string caN } /// - /// This function checks a registry setting on the target host for the specified CA to see if role seperation is enabled. + /// This function checks a registry setting on the target host for the specified CA to see if role separation is enabled. /// If enabled, you cannot perform any CA actions if you have both ManageCA and ManageCertificates permissions. Only CA admins can modify the setting. /// /// https://www.itprotoday.com/security/q-how-can-i-make-sure-given-windows-account-assigned-only-single-certification-authority-ca /// /// + /// /// /// - [ExcludeFromCodeCoverage] - public BoolRegistryAPIResult RoleSeparationEnabled(string target, string caName) + public async Task IsRoleSeparationEnabled(string target, string caName, string computerObjectId) { var ret = new BoolRegistryAPIResult(); var regSubKey = $"SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\{caName}"; const string regValue = "RoleSeparationEnabled"; - var data = Helpers.GetRegistryKeyData(target, regSubKey, regValue, _log); + var data = _registryAccessor.GetRegistryKeyData(target, regSubKey, regValue); ret.Collected = data.Collected; if (!data.Collected) { + await SendComputerStatus(new CSVComputerStatus { + Status = data.FailureReason, + Task = nameof(IsRoleSeparationEnabled), + ComputerName = target, + ObjectId = computerObjectId + }); + ret.FailureReason = data.FailureReason; return ret; } + + await SendComputerStatus(new CSVComputerStatus { + Status = CSVComputerStatus.StatusSuccess, + Task = nameof(IsRoleSeparationEnabled), + ComputerName = target, + ObjectId = computerObjectId + }); if (data.Value == null) { @@ -346,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 @@ -360,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, @@ -377,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); } @@ -396,22 +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(); + 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); } @@ -420,50 +496,53 @@ 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) { if (ComputerStatusEvent is not null) await ComputerStatusEvent(status); } - } public class EnrollmentAgentRestriction diff --git a/src/CommonLib/Processors/ComputerSessionProcessor.cs b/src/CommonLib/Processors/ComputerSessionProcessor.cs index 729df1820..52c2a8c1c 100644 --- a/src/CommonLib/Processors/ComputerSessionProcessor.cs +++ b/src/CommonLib/Processors/ComputerSessionProcessor.cs @@ -24,6 +24,7 @@ public class ComputerSessionProcessor { private readonly string _localAdminPassword; private readonly AdaptiveTimeout _readUserSessionsAdaptiveTimeout; private readonly AdaptiveTimeout _readUserSessionsPriviledgedAdaptiveTimeout; + private readonly IRegistryAccessor _registryAccessor; public ComputerSessionProcessor(ILdapUtils utils, NativeMethods nativeMethods = null, ILogger log = null, string currentUserName = null, @@ -38,6 +39,7 @@ public ComputerSessionProcessor(ILdapUtils utils, _localAdminPassword = localAdminPassword; _readUserSessionsAdaptiveTimeout = new AdaptiveTimeout(maxTimeout: TimeSpan.FromMinutes(2), Logging.LogProvider.CreateLogger(nameof(ReadUserSessions))); _readUserSessionsPriviledgedAdaptiveTimeout = new AdaptiveTimeout(maxTimeout: TimeSpan.FromMinutes(2), Logging.LogProvider.CreateLogger(nameof(ReadUserSessionsPrivileged))); + _registryAccessor = new RegistryAccessor(); } public event ComputerStatusDelegate ComputerStatusEvent; @@ -293,7 +295,7 @@ public async Task ReadUserSessionsRegistry(string computerName _log.LogDebug("Running RegSessionEnum for {ObjectName}", computerName); try { - using (var key = await SHRegistryKey.Connect(RegistryHive.Users, computerName)) { + using (var key = await _registryAccessor.Connect(RegistryHive.Users, computerName)) { ret.Collected = true; await SendComputerStatus(new CSVComputerStatus { Status = CSVComputerStatus.StatusSuccess, diff --git a/src/CommonLib/Processors/DCRegistryProcessor.cs b/src/CommonLib/Processors/DCRegistryProcessor.cs index 4c229d025..2726a45a6 100644 --- a/src/CommonLib/Processors/DCRegistryProcessor.cs +++ b/src/CommonLib/Processors/DCRegistryProcessor.cs @@ -1,5 +1,4 @@ using SharpHoundCommonLib.OutputTypes; -using System; using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -9,12 +8,15 @@ namespace SharpHoundCommonLib.Processors public class DCRegistryProcessor { private readonly ILogger _log; + private readonly IRegistryAccessor _registryAccessor; + public readonly ILdapUtils _utils; public delegate Task ComputerStatusDelegate(CSVComputerStatus status); public DCRegistryProcessor(ILdapUtils utils, ILogger log = null) { _utils = utils; + _registryAccessor = new RegistryAccessor(log); _log = log ?? Logging.LogProvider.CreateLogger("DCRegProc"); } @@ -30,7 +32,7 @@ public IntRegistryAPIResult GetCertificateMappingMethods(string target) var ret = new IntRegistryAPIResult(); const string subKey = @"SYSTEM\CurrentControlSet\Control\SecurityProviders\Schannel"; const string subValue = "CertificateMappingMethods"; - var data = Helpers.GetRegistryKeyData(target, subKey, subValue, _log); + var data = _registryAccessor.GetRegistryKeyData(target, subKey, subValue); ret.Collected = data.Collected; if (!data.Collected) @@ -62,7 +64,7 @@ public IntRegistryAPIResult GetStrongCertificateBindingEnforcement(string target var ret = new IntRegistryAPIResult(); const string subKey = @"SYSTEM\CurrentControlSet\Services\Kdc"; const string subValue = "StrongCertificateBindingEnforcement"; - var data = Helpers.GetRegistryKeyData(target, subKey, subValue, _log); + var data = _registryAccessor.GetRegistryKeyData(target, subKey, subValue); ret.Collected = data.Collected; if (!data.Collected) @@ -94,7 +96,7 @@ public StrRegistryAPIResult GetVulnerableNetlogonSecurityDescriptor(string targe var ret = new StrRegistryAPIResult(); const string subKey = @"SYSTEM\CurrentControlSet\Services\Netlogon\Parameters"; const string subValue = "VulnerableChannelAllowList"; - var data = Helpers.GetRegistryKeyData(target, subKey, subValue, _log); + var data = _registryAccessor.GetRegistryKeyData(target, subKey, subValue); ret.Collected = data.Collected; if (!data.Collected) 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 17efe86fd..11c2185c1 100644 --- a/test/unit/CertAbuseProcessorTest.cs +++ b/test/unit/CertAbuseProcessorTest.cs @@ -1,141 +1,781 @@ using System; -using System.DirectoryServices; +using System.Collections.Generic; +using System.Security.AccessControl; +using System.Security.Principal; +using System.Threading; using System.Threading.Tasks; -using CommonLibTest.Facades; +using CommonLibTest.CollectionDefinitions; using Moq; -using Newtonsoft.Json; using SharpHoundCommonLib; using SharpHoundCommonLib.Processors; using Xunit; -using Xunit.Abstractions; - -namespace CommonLibTest { - //TODO: Make these tests work - public class CertAbuseProcessorTest : IDisposable { - private const string CASecurityFixture = - "AQAUhCABAAAwAQAAFAAAAEQAAAACADAAAgAAAALAFAD//wAAAQEAAAAAAAEAAAAAAsAUAP//AAABAQAAAAAABQcAAAACANwABwAAAAADGAABAAAAAQIAAAAAAAUgAAAAIAIAAAADGAACAAAAAQIAAAAAAAUgAAAAIAIAAAADJAABAAAAAQUAAAAAAAUVAAAAIE+Qun9GhKV2SBaQAAIAAAADJAACAAAAAQUAAAAAAAUVAAAAIE+Qun9GhKV2SBaQAAIAAAADJAABAAAAAQUAAAAAAAUVAAAAIE+Qun9GhKV2SBaQBwIAAAADJAACAAAAAQUAAAAAAAUVAAAAIE+Qun9GhKV2SBaQBwIAAAADFAAAAgAAAQEAAAAAAAULAAAAAQIAAAAAAAUgAAAAIAIAAAECAAAAAAAFIAAAACACAAA="; - - private readonly ITestOutputHelper _testOutputHelper; - - public CertAbuseProcessorTest(ITestOutputHelper testOutputHelper) { - _testOutputHelper = testOutputHelper; - } - - public void Dispose() { - } - - // [Fact] - // public void CertAbuseProcessor_GetCASecurity_HappyPath() - // { - // var mockProcessor = new Mock(new MockLDAPUtils(), null); - // - // var mockRegistryKey = new Mock(); - // mockRegistryKey.Setup(x => x.GetValue(It.IsAny(), It.IsAny())) - // .Returns(new byte[] { 0x20, 0x20 }); - // mockProcessor.Setup(x => x.OpenRemoteRegistry(It.IsAny())).Returns(mockRegistryKey.Object); - // - // var processor = mockProcessor.Object; - // var results = processor.GetCASecurity("testlab.local", "blah"); - // Assert.True(results.Collected); - // } - - // [Fact] - // public void CertAbuseProcessor_GetTrustedCerts_EmptyForNonRoot() - // { - // var mockUtils = new Mock(); - // mockUtils.Setup(x => x.IsForestRoot(It.IsAny())).Returns(false); - // var processor = new CertAbuseProcessor(mockUtils.Object); - // - // var results = processor.GetTrustedCerts("testlab.local"); - // Assert.Empty(results); - // } - // - // [Fact] - // public void CertAbuseProcessor_GetTrustedCerts_NullConfigPath_ReturnsEmpty() - // { - // var mockUtils = new Mock(); - // mockUtils.Setup(x => x.IsForestRoot(It.IsAny())).Returns(true); - // mockUtils.Setup(x => x.GetConfigurationPath(It.IsAny())).Returns((string)null); - // var processor = new CertAbuseProcessor(mockUtils.Object); - // - // var results = processor.GetTrustedCerts("testlab.local"); - // Assert.Empty(results); - // } - // - // [Fact] - // public void CertAbuseProcessor_GetRootCAs_EmptyForNonRoot() - // { - // var mockUtils = new Mock(); - // mockUtils.Setup(x => x.IsForestRoot(It.IsAny())).Returns(false); - // var processor = new CertAbuseProcessor(mockUtils.Object); - // - // var results = processor.GetRootCAs("testlab.local"); - // Assert.Empty(results); - // } - // - // [Fact] - // public void CertAbuseProcessor_GetRootCAs_NullConfigPath_ReturnsEmpty() - // { - // var mockUtils = new Mock(); - // mockUtils.Setup(x => x.IsForestRoot(It.IsAny())).Returns(true); - // mockUtils.Setup(x => x.GetConfigurationPath(It.IsAny())).Returns((string)null); - // var processor = new CertAbuseProcessor(mockUtils.Object); - // - // var results = processor.GetRootCAs("testlab.local"); - // Assert.Empty(results); - // } - - // [Fact] - // public async Task CertAbuseProcessor_ProcessCAPermissions_NullSecurity_ReturnsNull() - // { - // var processor = new CertAbuseProcessor(new MockLdapUtils()); - // - // var results = await processor.ProcessRegistryEnrollmentPermissions(null, "DUMPSTER.FIRE", null, "test"); - // - // Assert.Equal("Value cannot be null. (Parameter 'machineName')", results.FailureReason); - // Assert.False(results.Collected); - // Assert.Null(results.Data); - // } - - // [WindowsOnlyFact] - // public void CertAbuseProcessor_ProcessCAPermissions_ReturnsCorrectValues() - // { - // var mockUtils = new Mock(); - // var sd = new ActiveDirectorySecurityDescriptor(new ActiveDirectorySecurity()); - // mockUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(sd); - // var processor = new CertAbuseProcessor(mockUtils.Object); - // var bytes = Helpers.B64ToBytes(CASecurityFixture); - // - // var results = processor.ProcessCAPermissions(bytes, "TESTLAB.LOCAL", "test", false); - // _testOutputHelper.WriteLine(JsonConvert.SerializeObject(results, Formatting.Indented)); - // Assert.Contains(results, - // x => x.RightName == EdgeNames.Owns && x.PrincipalSID == "TESTLAB.LOCAL-S-1-5-32-544" && - // x.PrincipalType == Label.Group && !x.IsInherited); - // Assert.Contains(results, - // x => x.RightName == EdgeNames.Enroll && x.PrincipalSID == "TESTLAB.LOCAL-S-1-5-11" && - // !x.IsInherited); - // Assert.Contains(results, - // x => x.RightName == EdgeNames.ManageCA && x.PrincipalSID == "TESTLAB.LOCAL-S-1-5-32-544" && - // !x.IsInherited); - // Assert.Contains(results, - // x => x.RightName == EdgeNames.ManageCertificates && x.PrincipalSID == "TESTLAB.LOCAL-S-1-5-32-544" && - // !x.IsInherited); - // Assert.Contains(results, - // x => x.RightName == EdgeNames.ManageCA && - // x.PrincipalSID == "S-1-5-21-3130019616-2776909439-2417379446-512" && - // !x.IsInherited); - // Assert.Contains(results, - // x => x.RightName == EdgeNames.ManageCertificates && - // x.PrincipalSID == "S-1-5-21-3130019616-2776909439-2417379446-512" && - // !x.IsInherited); - // Assert.Contains(results, - // x => x.RightName == EdgeNames.ManageCA && - // x.PrincipalSID == "S-1-5-21-3130019616-2776909439-2417379446-519" && - // !x.IsInherited); - // Assert.Contains(results, - // x => x.RightName == EdgeNames.ManageCertificates && - // x.PrincipalSID == "S-1-5-21-3130019616-2776909439-2417379446-519" && - // !x.IsInherited); - // } +using SharpHoundCommonLib.Enums; +using SharpHoundCommonLib.OutputTypes; +using SharpHoundRPC.SAMRPCNative; +using SharpHoundRPC.Wrappers; + +namespace CommonLibTest +{ + [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(); + _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 + }); + + var results = await _certAbuseProcessor.IsUserSpecifiesSanEnabled(TargetName, CAName, TargetDomainSid); + + //Validate result + Assert.True(results.Collected); + Assert.Equal(expectedResult, results.Value); + Assert.Null(results.FailureReason); + + //Validate CompStatus Log + Assert.Equal(TargetName, _receivedCompStatus.ComputerName); + Assert.Equal(nameof(CertAbuseProcessor.IsUserSpecifiesSanEnabled), _receivedCompStatus.Task); + 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 + }); + + var results = await _certAbuseProcessor.IsUserSpecifiesSanEnabled(TargetName, CAName, TargetDomainSid); + + //Validate result + Assert.False(results.Collected); + Assert.Equal(FailureReason, results.FailureReason); + + //Validate CompStatus Log + Assert.Equal(TargetName, _receivedCompStatus.ComputerName); + Assert.Equal(nameof(_certAbuseProcessor.IsUserSpecifiesSanEnabled), _receivedCompStatus.Task); + 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 + }); + + var results = await _certAbuseProcessor.IsRoleSeparationEnabled(TargetName, CAName, TargetDomainSid); + + //Validate result + Assert.True(results.Collected); + Assert.Equal(expectedResult, results.Value); + Assert.Null(results.FailureReason); + + //Validate CompStatus Log + Assert.Equal(TargetName, _receivedCompStatus.ComputerName); + Assert.Equal(nameof(CertAbuseProcessor.IsRoleSeparationEnabled), _receivedCompStatus.Task); + 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 + }); + + var results = await _certAbuseProcessor.IsRoleSeparationEnabled(TargetName, CAName, TargetDomainSid); + + //Validate result + Assert.False(results.Collected); + Assert.Equal(FailureReason, results.FailureReason); + + //Validate CompStatus Log + Assert.Equal(TargetName, _receivedCompStatus.ComputerName); + Assert.Equal(nameof(_certAbuseProcessor.IsRoleSeparationEnabled), _receivedCompStatus.Task); + Assert.Equal(FailureReason, _receivedCompStatus.Status); + Assert.Equal(TargetDomainSid, _receivedCompStatus.ObjectId); + } + + [Fact] + public async Task CertAbuseProcessor_ProcessEAPermissions_ReturnsEmptyResult() { + 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 = 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(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 + }); + + var results = await _certAbuseProcessor.ProcessEAPermissions(CAName, DomainName, TargetName, TargetDomainSid); + + //Validate result + Assert.False(results.Collected); + Assert.Equal(FailureReason, results.FailureReason); + + //Validate CompStatus Log + Assert.Equal(TargetName, _receivedCompStatus.ComputerName); + Assert.Equal(nameof(_certAbuseProcessor.ProcessEAPermissions), _receivedCompStatus.Task); + Assert.Equal(FailureReason, _receivedCompStatus.Status); + Assert.Equal(TargetDomainSid, _receivedCompStatus.ObjectId); + } + + 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, + null, + null, + dacl); + + var regValue = new byte[descriptor.BinaryLength]; + descriptor.GetBinaryForm(regValue, 0); + + _mockRegistryAccessor + .Setup(ra => ra.GetRegistryKeyData( + TargetName, + subKey, + subValue)) + .Returns(new RegistryResult + { + Collected = true, + Value = regValue + }); + + _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 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(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 + }); + + var results = await _certAbuseProcessor.ProcessRegistryEnrollmentPermissions(CAName, DomainName, TargetName, TargetDomainSid); + + //Validate result + Assert.False(results.Collected); + Assert.Equal(FailureReason, results.FailureReason); + + //Validate CompStatus Log + Assert.Equal(TargetName, _receivedCompStatus.ComputerName); + Assert.Equal(nameof(_certAbuseProcessor.ProcessRegistryEnrollmentPermissions), _receivedCompStatus.Task); + 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); + Assert.Single(results.resolvedTemplates); + Assert.Contains(expectedTemplate, results.resolvedTemplates); + Assert.Single(results.unresolvedTemplates); + Assert.Contains(invalidCN, results.unresolvedTemplates); + } + + [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, + null + ); + + Assert.Equal((false, null), results); + _mockLdapUtils.VerifyNoOtherCalls(); + } + + [WindowsOnlyFact] + public async Task CertAbuseProcessor_GetRegistryPrincipal_CallsResolveIDAndType_ForDomainController() { + var expectedPrincipalType = Label.Group; + var expectedPrincipalSID = "S-1-5-21-3130019616-2776909439-2417379446-512"; + + _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, + 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(CSVComputerStatus.StatusSuccess, _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/Facades/MockLdapUtils.cs b/test/unit/Facades/MockLdapUtils.cs index 8efd0eb07..1e5f1194d 100644 --- a/test/unit/Facades/MockLdapUtils.cs +++ b/test/unit/Facades/MockLdapUtils.cs @@ -744,7 +744,7 @@ public bool GetDomain(out Domain domain) { return (!results.IsNullOrEmpty(), results); } - + public Task<(bool Success, TypedPrincipal Principal)> ResolveCertTemplateByProperty(string propValue, string propName, string domainName) { throw new NotImplementedException(); } diff --git a/test/unit/Utils.cs b/test/unit/Utils.cs index 10e9d90a0..fe5255182 100644 --- a/test/unit/Utils.cs +++ b/test/unit/Utils.cs @@ -90,4 +90,12 @@ public WindowsOnlyFact() if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) Skip = "Ignore on non-Windows platforms"; } } + + public sealed class WindowsOnlyTheory : TheoryAttribute + { + public WindowsOnlyTheory() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) Skip = "Ignore on non-Windows platforms"; + } + } } \ No newline at end of file