diff --git a/src/BaseContext.cs b/src/BaseContext.cs index 8c6cf51..3ab2934 100644 --- a/src/BaseContext.cs +++ b/src/BaseContext.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -64,6 +65,8 @@ public void UpdateLoopTime() CurrentLoopTime = $"{DateTime.Now:yyyyMMddHHmmss}"; } + public HashSet CollectedDomainSids { get; } = new(); + public async Task DoDelay() { if (Throttle == 0) diff --git a/src/Client/Context.cs b/src/Client/Context.cs index 4e52f96..7e24734 100644 --- a/src/Client/Context.cs +++ b/src/Client/Context.cs @@ -75,5 +75,6 @@ public interface IContext string ResolveFileName(string filename, string extension, bool addTimestamp); EnumerationDomain[] Domains { get; set; } void UpdateLoopTime(); + public HashSet CollectedDomainSids { get; } } } \ No newline at end of file diff --git a/src/Producers/BaseProducer.cs b/src/Producers/BaseProducer.cs index 08e0610..8bf7016 100644 --- a/src/Producers/BaseProducer.cs +++ b/src/Producers/BaseProducer.cs @@ -130,7 +130,7 @@ protected LDAPData CreateDefaultNCData() if ((methods & ResolvedCollectionMethod.DCRegistry) != 0) { - query = query.AddComputers(); + query = query.AddComputers(CommonFilters.DomainControllers); props.AddRange(CommonProperties.ComputerMethodProps); } } @@ -169,6 +169,9 @@ protected LDAPData CreateConfigNCData() { query = allObjectTypesQuery; props.AddRange(CommonProperties.CertAbuseProps); + props.AddRange(CommonProperties.ObjectPropsProps); + props.AddRange(CommonProperties.ContainerProps); + props.AddRange(CommonProperties.ACLProps); } if ((methods & ResolvedCollectionMethod.Container) != 0) diff --git a/src/Producers/LdapProducer.cs b/src/Producers/LdapProducer.cs index 7ee79ab..fb6027e 100644 --- a/src/Producers/LdapProducer.cs +++ b/src/Producers/LdapProducer.cs @@ -31,6 +31,11 @@ public override async Task Produce() var log = Context.Logger; var utils = Context.LDAPUtils; + if (string.IsNullOrEmpty(ldapData.Filter.GetFilter())) + { + return; + } + if (Context.Flags.CollectAllProperties) { log.LogDebug("CollectAllProperties set. Changing LDAP properties to *"); @@ -39,7 +44,7 @@ public override async Task Produce() foreach (var domain in Context.Domains) { - Context.Logger.LogInformation("Beginning LDAP search for {Domain}", domain); + Context.Logger.LogInformation("Beginning LDAP search for {Domain}", domain.Name); //Do a basic LDAP search and grab results var successfulConnect = false; try @@ -59,14 +64,7 @@ public override async Task Produce() continue; } - await OutputChannel.Writer.WriteAsync(new Domain - { - ObjectIdentifier = domain.DomainSid, - Properties = new Dictionary - { - { "collected", true }, - } - }); + Context.CollectedDomainSids.Add(domain.DomainSid); foreach (var searchResult in Context.LDAPUtils.QueryLDAP(ldapData.Filter.GetFilter(), SearchScope.Subtree, ldapData.Props.Distinct().ToArray(), cancellationToken, domain.Name, @@ -83,7 +81,6 @@ await OutputChannel.Writer.WriteAsync(new Domain Context.Logger.LogTrace("Producer wrote {DistinguishedName} to channel", searchResult.DistinguishedName); } } - } /// @@ -105,19 +102,18 @@ public override async Task ProduceConfigNC() if (!configurationNCsCollected.Contains(configAdsPath)) { Context.Logger.LogInformation("Beginning LDAP search for {Domain} Configuration NC", domain.Name); + // Ensure we only collect the Configuration NC once per forest + configurationNCsCollected.Add(configAdsPath); //Do a basic LDAP search and grab results foreach (var searchResult in Context.LDAPUtils.QueryLDAP(configNcData.Filter.GetFilter(), SearchScope.Subtree, configNcData.Props.Distinct().ToArray(), cancellationToken, domain.Name, adsPath: configAdsPath, - includeAcl: (Context.ResolvedCollectionMethods & ResolvedCollectionMethod.ACL) != 0)) + includeAcl: (Context.ResolvedCollectionMethods & ResolvedCollectionMethod.ACL) != 0 || (Context.ResolvedCollectionMethods & ResolvedCollectionMethod.CertServices) != 0)) { await Channel.Writer.WriteAsync(searchResult, cancellationToken); Context.Logger.LogTrace("Producer wrote {DistinguishedName} to channel", searchResult.DistinguishedName); } - - // Ensure we only collect the Configuration NC once per forest - configurationNCsCollected.Add(configAdsPath); } else { diff --git a/src/Runtime/CollectionTask.cs b/src/Runtime/CollectionTask.cs index bfc7f78..5bb57ff 100644 --- a/src/Runtime/CollectionTask.cs +++ b/src/Runtime/CollectionTask.cs @@ -22,6 +22,7 @@ public class CollectionTask private readonly OutputWriter _outputWriter; private readonly BaseProducer _producer; private readonly List _taskPool = new(); + private const string EnterpriseDCSuffix = "S-1-5-9"; public CollectionTask(IContext context) { @@ -82,7 +83,19 @@ internal async Task StartCollection() _log.LogInformation("Consumers finished, closing output channel"); foreach (var wkp in _context.LDAPUtils.GetWellKnownPrincipalOutput(_context.DomainName)) + { + if (!wkp.ObjectIdentifier.EndsWith(EnterpriseDCSuffix)) + { + wkp.Properties["reconcile"] = false; + } + else if (wkp is Group g && g.Members.Length == 0) + { + continue; + } + await _outputChannel.Writer.WriteAsync(wkp); + } + _outputChannel.Writer.Complete(); _compStatusChannel?.Writer.Complete(); diff --git a/src/Runtime/LDAPConsumer.cs b/src/Runtime/LDAPConsumer.cs index c298d5b..6960bd5 100644 --- a/src/Runtime/LDAPConsumer.cs +++ b/src/Runtime/LDAPConsumer.cs @@ -37,6 +37,11 @@ internal static async Task ConsumeSearchResults(Channel inpu watch.Elapsed.TotalMilliseconds, res.DisplayName); if (processed == null) continue; + + if (processed is Domain d && context.CollectedDomainSids.Contains(d.ObjectIdentifier)) + { + d.Properties.Add("collected", true); + } await outputChannel.Writer.WriteAsync(processed); } catch (Exception e) diff --git a/src/Runtime/ObjectProcessors.cs b/src/Runtime/ObjectProcessors.cs index 20881cf..e4ca533 100644 --- a/src/Runtime/ObjectProcessors.cs +++ b/src/Runtime/ObjectProcessors.cs @@ -108,8 +108,11 @@ private async Task ProcessUserObject(ISearchResultEntry entry, ret.Properties.Add("name", resolvedSearchResult.DisplayName); ret.Properties.Add("distinguishedname", entry.DistinguishedName.ToUpper()); ret.Properties.Add("domainsid", resolvedSearchResult.DomainSid); - ret.Properties.Add("highvalue", false); ret.Properties.Add("samaccountname", entry.GetProperty(LDAPProperties.SAMAccountName)); + + if (entry.IsMSA()) ret.Properties.Add("msa", true); + + if (entry.IsGMSA()) ret.Properties.Add("gmsa", true); if ((_methods & ResolvedCollectionMethod.ACL) != 0) { @@ -172,7 +175,6 @@ private async Task ProcessComputerObject(ISearchResultEntry entry, ret.Properties.Add("name", resolvedSearchResult.DisplayName); ret.Properties.Add("distinguishedname", entry.DistinguishedName.ToUpper()); ret.Properties.Add("domainsid", resolvedSearchResult.DomainSid); - ret.Properties.Add("highvalue", false); ret.Properties.Add("samaccountname", entry.GetProperty(LDAPProperties.SAMAccountName)); var hasLaps = entry.HasLAPS(); @@ -224,6 +226,7 @@ private async Task ProcessComputerObject(ISearchResultEntry entry, { await compStatusChannel.Writer.WriteAsync(availability.GetCSVStatus(resolvedSearchResult.DisplayName), _cancellationToken); + ret.Status = availability; return ret; } @@ -322,7 +325,6 @@ private Group ProcessGroupObject(ISearchResultEntry entry, ret.Properties.Add("name", resolvedSearchResult.DisplayName); ret.Properties.Add("distinguishedname", entry.DistinguishedName.ToUpper()); ret.Properties.Add("domainsid", resolvedSearchResult.DomainSid); - ret.Properties.Add("highvalue", IsHighValueGroup(resolvedSearchResult.ObjectId)); ret.Properties.Add("samaccountname", entry.GetProperty(LDAPProperties.SAMAccountName)); if ((_methods & ResolvedCollectionMethod.ACL) != 0) @@ -356,30 +358,6 @@ private Group ProcessGroupObject(ISearchResultEntry entry, return ret; } - private bool IsHighValueGroup(string objectId) - { - // TODO: replace w/ a more definitive/centralized list - var suffixes = new string[] - { - "-512", - "-516", - "-519", - "S-1-5-32-544", - "S-1-5-32-548", - "S-1-5-32-549", - "S-1-5-32-550", - "S-1-5-32-551", - }; - foreach (var suffix in suffixes) - { - if (objectId.EndsWith(suffix)) - { - return true; - } - } - return false; - } - private async Task ProcessDomainObject(ISearchResultEntry entry, ResolvedSearchResult resolvedSearchResult) { @@ -392,7 +370,6 @@ private async Task ProcessDomainObject(ISearchResultEntry entry, ret.Properties.Add("name", resolvedSearchResult.DisplayName); ret.Properties.Add("distinguishedname", entry.DistinguishedName.ToUpper()); ret.Properties.Add("domainsid", resolvedSearchResult.DomainSid); - ret.Properties.Add("highvalue", true); if ((_methods & ResolvedCollectionMethod.ACL) != 0) { @@ -440,7 +417,6 @@ private GPO ProcessGPOObject(ISearchResultEntry entry, ret.Properties.Add("name", resolvedSearchResult.DisplayName); ret.Properties.Add("distinguishedname", entry.DistinguishedName.ToUpper()); ret.Properties.Add("domainsid", resolvedSearchResult.DomainSid); - ret.Properties.Add("highvalue", false); if ((_methods & ResolvedCollectionMethod.ACL) != 0) { @@ -474,7 +450,6 @@ private async Task ProcessOUObject(ISearchResultEntry entry, ret.Properties.Add("name", resolvedSearchResult.DisplayName); ret.Properties.Add("distinguishedname", entry.DistinguishedName.ToUpper()); ret.Properties.Add("domainsid", resolvedSearchResult.DomainSid); - ret.Properties.Add("highvalue", false); if ((_methods & ResolvedCollectionMethod.ACL) != 0) { @@ -522,12 +497,11 @@ private Container ProcessContainerObject(ISearchResultEntry entry, ret.Properties.Add("name", resolvedSearchResult.DisplayName); ret.Properties.Add("distinguishedname", entry.DistinguishedName.ToUpper()); ret.Properties.Add("domainsid", resolvedSearchResult.DomainSid); - ret.Properties.Add("highvalue", false); - if ((_methods & ResolvedCollectionMethod.Container) != 0) + if ((_methods & ResolvedCollectionMethod.Container) != 0 || (_methods & ResolvedCollectionMethod.CertServices) != 0) ret.ContainedBy = _containerProcessor.GetContainingObject(entry.DistinguishedName); - if ((_methods & ResolvedCollectionMethod.ACL) != 0) + if ((_methods & ResolvedCollectionMethod.ACL) != 0 || (_methods & ResolvedCollectionMethod.CertServices) != 0) { ret.Aces = _aclProcessor.ProcessACL(resolvedSearchResult, entry) .ToArray(); @@ -535,7 +509,7 @@ private Container ProcessContainerObject(ISearchResultEntry entry, ret.Properties.Add("isaclprotected", ret.IsACLProtected); } - if ((_methods & ResolvedCollectionMethod.ObjectProps) != 0) + if ((_methods & ResolvedCollectionMethod.ObjectProps) != 0 || (_methods & ResolvedCollectionMethod.CertServices) != 0) { if (_context.Flags.CollectAllProperties) { @@ -561,21 +535,22 @@ private async Task ProcessRootCA(ISearchResultEntry entry, ResolvedSearc ret.Properties.Add("name", resolvedSearchResult.DisplayName); ret.Properties.Add("distinguishedname", entry.DistinguishedName.ToUpper()); ret.Properties.Add("domainsid", resolvedSearchResult.DomainSid); + - if ((_methods & ResolvedCollectionMethod.ACL) != 0) + if ((_methods & ResolvedCollectionMethod.ACL) != 0 || (_methods & ResolvedCollectionMethod.CertServices) != 0) { ret.Aces = _aclProcessor.ProcessACL(resolvedSearchResult, entry).ToArray(); ret.IsACLProtected = _aclProcessor.IsACLProtected(entry); ret.Properties.Add("isaclprotected", ret.IsACLProtected); } - if ((_methods & ResolvedCollectionMethod.ObjectProps) != 0) + if ((_methods & ResolvedCollectionMethod.ObjectProps) != 0 || (_methods & ResolvedCollectionMethod.CertServices) != 0) { var props = LDAPPropertyProcessor.ReadRootCAProperties(entry); ret.Properties.Merge(props); } - if ((_methods & ResolvedCollectionMethod.Container) != 0) + if ((_methods & ResolvedCollectionMethod.Container) != 0 || (_methods & ResolvedCollectionMethod.CertServices) != 0) { ret.ContainedBy = _containerProcessor.GetContainingObject(entry.DistinguishedName); } @@ -595,20 +570,20 @@ private async Task ProcessAIACA(ISearchResultEntry entry, ResolvedSearchR ret.Properties.Add("distinguishedname", entry.DistinguishedName.ToUpper()); ret.Properties.Add("domainsid", resolvedSearchResult.DomainSid); - if ((_methods & ResolvedCollectionMethod.ACL) != 0) + if ((_methods & ResolvedCollectionMethod.ACL) != 0 || (_methods & ResolvedCollectionMethod.CertServices) != 0) { ret.Aces = _aclProcessor.ProcessACL(resolvedSearchResult, entry).ToArray(); ret.IsACLProtected = _aclProcessor.IsACLProtected(entry); ret.Properties.Add("isaclprotected", ret.IsACLProtected); } - if ((_methods & ResolvedCollectionMethod.ObjectProps) != 0) + if ((_methods & ResolvedCollectionMethod.ObjectProps) != 0 || (_methods & ResolvedCollectionMethod.CertServices) != 0) { var props = LDAPPropertyProcessor.ReadAIACAProperties(entry); ret.Properties.Merge(props); } - if ((_methods & ResolvedCollectionMethod.Container) != 0) + if ((_methods & ResolvedCollectionMethod.Container) != 0 || (_methods & ResolvedCollectionMethod.CertServices) != 0) { ret.ContainedBy = _containerProcessor.GetContainingObject(entry.DistinguishedName); } @@ -628,14 +603,14 @@ private async Task ProcessEnterpriseCA(ISearchResultEntry entry, R ret.Properties.Add("distinguishedname", entry.DistinguishedName.ToUpper()); ret.Properties.Add("domainsid", resolvedSearchResult.DomainSid); - if ((_methods & ResolvedCollectionMethod.ACL) != 0) + if ((_methods & ResolvedCollectionMethod.ACL) != 0 || (_methods & ResolvedCollectionMethod.CertServices) != 0) { ret.Aces = _aclProcessor.ProcessACL(resolvedSearchResult, entry).ToArray(); ret.IsACLProtected = _aclProcessor.IsACLProtected(entry); ret.Properties.Add("isaclprotected", ret.IsACLProtected); } - if ((_methods & ResolvedCollectionMethod.ObjectProps) != 0) + if ((_methods & ResolvedCollectionMethod.ObjectProps) != 0 || (_methods & ResolvedCollectionMethod.CertServices) != 0) { var props = LDAPPropertyProcessor.ReadEnterpriseCAProperties(entry); ret.Properties.Merge(props); @@ -644,41 +619,44 @@ private async Task ProcessEnterpriseCA(ISearchResultEntry entry, R ret.EnabledCertTemplates = _certAbuseProcessor.ProcessCertTemplates(entry.GetArrayProperty(LDAPProperties.CertificateTemplates), resolvedSearchResult.Domain).ToArray(); } - if ((_methods & ResolvedCollectionMethod.Container) != 0) + if ((_methods & ResolvedCollectionMethod.Container) != 0 || (_methods & ResolvedCollectionMethod.CertServices) != 0) { ret.ContainedBy = _containerProcessor.GetContainingObject(entry.DistinguishedName); } - // Collect properties from CA server registry - bool cASecurityCollected = false; - bool enrollmentAgentRestrictionsCollected = false; - bool isUserSpecifiesSanEnabledCollected = false; - var caName = entry.GetProperty(LDAPProperties.Name); - var dnsHostName = entry.GetProperty(LDAPProperties.DNSHostName); - if ((_methods & ResolvedCollectionMethod.CARegistry) != 0 && caName != null && dnsHostName != null) + if ((_methods & ResolvedCollectionMethod.CARegistry) != 0) { - ret.HostingComputer = await _context.LDAPUtils.ResolveHostToSid(dnsHostName, resolvedSearchResult.Domain); - - CARegistryData cARegistryData = new() + // Collect properties from CA server registry + var cASecurityCollected = false; + var enrollmentAgentRestrictionsCollected = false; + var isUserSpecifiesSanEnabledCollected = false; + var caName = entry.GetProperty(LDAPProperties.Name); + var dnsHostName = entry.GetProperty(LDAPProperties.DNSHostName); + if ((_methods & ResolvedCollectionMethod.CARegistry) != 0 && caName != null && dnsHostName != null) { - IsUserSpecifiesSanEnabled = _certAbuseProcessor.IsUserSpecifiesSanEnabled(dnsHostName, caName), - EnrollmentAgentRestrictions = await _certAbuseProcessor.ProcessEAPermissions(caName, resolvedSearchResult.Domain, dnsHostName, ret.HostingComputer), + ret.HostingComputer = await _context.LDAPUtils.ResolveHostToSid(dnsHostName, resolvedSearchResult.Domain); - // The CASecurity exist in the AD object DACL and in registry of the CA server. We prefer to use the values from registry as they are the ground truth. - // If changes are made on the CA server, registry and the AD object is updated. If changes are made directly on the AD object, the CA server registry is not updated. - CASecurity = await _certAbuseProcessor.ProcessRegistryEnrollmentPermissions(caName, resolvedSearchResult.Domain, dnsHostName, ret.HostingComputer) - }; + CARegistryData cARegistryData = new() + { + IsUserSpecifiesSanEnabled = _certAbuseProcessor.IsUserSpecifiesSanEnabled(dnsHostName, caName), + EnrollmentAgentRestrictions = await _certAbuseProcessor.ProcessEAPermissions(caName, resolvedSearchResult.Domain, dnsHostName, ret.HostingComputer), + + // The CASecurity exist in the AD object DACL and in registry of the CA server. We prefer to use the values from registry as they are the ground truth. + // If changes are made on the CA server, registry and the AD object is updated. If changes are made directly on the AD object, the CA server registry is not updated. + CASecurity = await _certAbuseProcessor.ProcessRegistryEnrollmentPermissions(caName, resolvedSearchResult.Domain, dnsHostName, ret.HostingComputer) + }; + + cASecurityCollected = cARegistryData.CASecurity.Collected; + enrollmentAgentRestrictionsCollected = cARegistryData.EnrollmentAgentRestrictions.Collected; + isUserSpecifiesSanEnabledCollected = cARegistryData.IsUserSpecifiesSanEnabled.Collected; + ret.CARegistryData = cARegistryData; + } - cASecurityCollected = cARegistryData.CASecurity.Collected; - enrollmentAgentRestrictionsCollected = cARegistryData.EnrollmentAgentRestrictions.Collected; - isUserSpecifiesSanEnabledCollected = cARegistryData.IsUserSpecifiesSanEnabled.Collected; - ret.CARegistryData = cARegistryData; + ret.Properties.Add("casecuritycollected", cASecurityCollected); + ret.Properties.Add("enrollmentagentrestrictionscollected", enrollmentAgentRestrictionsCollected); + ret.Properties.Add("isuserspecifiessanenabledcollected", isUserSpecifiesSanEnabledCollected); } - - ret.Properties.Add("casecuritycollected", cASecurityCollected); - ret.Properties.Add("enrollmentagentrestrictionscollected", enrollmentAgentRestrictionsCollected); - ret.Properties.Add("isuserspecifiessanenabledcollected", isUserSpecifiesSanEnabledCollected); - + return ret; } @@ -695,14 +673,14 @@ private async Task ProcessNTAuthStore(ISearchResultEntry entry, Res ret.Properties.Add("distinguishedname", entry.DistinguishedName.ToUpper()); ret.Properties.Add("domainsid", resolvedSearchResult.DomainSid); - if ((_methods & ResolvedCollectionMethod.ACL) != 0) + if ((_methods & ResolvedCollectionMethod.ACL) != 0 || (_methods & ResolvedCollectionMethod.CertServices) != 0) { ret.Aces = _aclProcessor.ProcessACL(resolvedSearchResult, entry).ToArray(); ret.IsACLProtected = _aclProcessor.IsACLProtected(entry); ret.Properties.Add("isaclprotected", ret.IsACLProtected); } - if ((_methods & ResolvedCollectionMethod.ObjectProps) != 0) + if ((_methods & ResolvedCollectionMethod.ObjectProps) != 0 || (_methods & ResolvedCollectionMethod.CertServices) != 0) { var props = LDAPPropertyProcessor.ReadNTAuthStoreProperties(entry); @@ -715,7 +693,7 @@ private async Task ProcessNTAuthStore(ISearchResultEntry entry, Res ret.Properties.Merge(props); } - if ((_methods & ResolvedCollectionMethod.Container) != 0) + if ((_methods & ResolvedCollectionMethod.Container) != 0 || (_methods & ResolvedCollectionMethod.CertServices) != 0) { ret.ContainedBy = _containerProcessor.GetContainingObject(entry.DistinguishedName); } @@ -735,20 +713,20 @@ private async Task ProcessCertTemplate(ISearchResultEntry entry, R ret.Properties.Add("distinguishedname", entry.DistinguishedName.ToUpper()); ret.Properties.Add("domainsid", resolvedSearchResult.DomainSid); - if ((_methods & ResolvedCollectionMethod.ACL) != 0) + if ((_methods & ResolvedCollectionMethod.ACL) != 0 || (_methods & ResolvedCollectionMethod.CertServices) != 0) { ret.Aces = _aclProcessor.ProcessACL(resolvedSearchResult, entry).ToArray(); ret.IsACLProtected = _aclProcessor.IsACLProtected(entry); ret.Properties.Add("isaclprotected", ret.IsACLProtected); } - if ((_methods & ResolvedCollectionMethod.ObjectProps) != 0) + if ((_methods & ResolvedCollectionMethod.ObjectProps) != 0 || (_methods & ResolvedCollectionMethod.CertServices) != 0) { var certTemplatesProps = LDAPPropertyProcessor.ReadCertTemplateProperties(entry); ret.Properties.Merge(certTemplatesProps); } - if ((_methods & ResolvedCollectionMethod.Container) != 0) + if ((_methods & ResolvedCollectionMethod.Container) != 0 || (_methods & ResolvedCollectionMethod.CertServices) != 0) { ret.ContainedBy = _containerProcessor.GetContainingObject(entry.DistinguishedName); }