diff --git a/Sharphound.csproj b/Sharphound.csproj index 368deb0..476cb39 100644 --- a/Sharphound.csproj +++ b/Sharphound.csproj @@ -6,8 +6,8 @@ latest full favicon.ico - 2.4.1 - 2.4.1 + 2.5.1 + 2.5.1 SpecterOps SharpHound SharpHound @@ -21,19 +21,19 @@ all - - + + - - + + - + - + diff --git a/src/BaseContext.cs b/src/BaseContext.cs index 3ab2934..2497b74 100644 --- a/src/BaseContext.cs +++ b/src/BaseContext.cs @@ -22,18 +22,18 @@ public sealed class BaseContext : IDisposable, IContext private bool disposedValue; - public BaseContext(ILogger logger, LDAPConfig ldapConfig, Flags flags) + public BaseContext(ILogger logger, LdapConfig ldapConfig, Flags flags) { Logger = logger; Flags = flags; - LDAPUtils = new LDAPUtils(); - LDAPUtils.SetLDAPConfig(ldapConfig); + LDAPUtils = new LdapUtils(); + LDAPUtils.SetLdapConfig(ldapConfig); CancellationTokenSource = new CancellationTokenSource(); } public bool IsFaulted { get; set; } - public ResolvedCollectionMethod ResolvedCollectionMethods { get; set; } + public CollectionMethod ResolvedCollectionMethods { get; set; } public string LdapFilter { get; set; } public string SearchBase { get; set; } public string DomainName { get; set; } @@ -56,7 +56,7 @@ public BaseContext(ILogger logger, LDAPConfig ldapConfig, Flags flags) public int Jitter { get; set; } public int PortScanTimeout { get; set; } = 500; public CancellationTokenSource CancellationTokenSource { get; set; } - public ILDAPUtils LDAPUtils { get; set; } + public ILdapUtils LDAPUtils { get; set; } public Task CollectionTask { get; set; } public Flags Flags { get; set; } @@ -90,12 +90,12 @@ public string GetCachePath() return path; } - public ResolvedCollectionMethod SetupMethodsForLoop() + public CollectionMethod SetupMethodsForLoop() { var original = ResolvedCollectionMethods; - const ResolvedCollectionMethod computerCollectionMethods = - ResolvedCollectionMethod.LocalGroups | ResolvedCollectionMethod.LoggedOn | - ResolvedCollectionMethod.Session; + const CollectionMethod computerCollectionMethods = + CollectionMethod.LocalGroups | CollectionMethod.LoggedOn | + CollectionMethod.Session; return original & computerCollectionMethods; } diff --git a/src/Client/Context.cs b/src/Client/Context.cs index 7e24734..ceda178 100644 --- a/src/Client/Context.cs +++ b/src/Client/Context.cs @@ -47,7 +47,7 @@ public interface IContext CancellationTokenSource CancellationTokenSource { get; set; } ILogger Logger { get; set; } - ILDAPUtils LDAPUtils { get; set; } + ILdapUtils LDAPUtils { get; set; } string OutputPrefix { get; set; } string OutputDirectory { get; set; } @@ -62,7 +62,7 @@ public interface IContext public string LocalAdminPassword { get; set; } - ResolvedCollectionMethod ResolvedCollectionMethods { get; set; } + CollectionMethod ResolvedCollectionMethods { get; set; } /// /// Does throttle and jitter for computer requests @@ -71,7 +71,7 @@ public interface IContext Task DoDelay(); string GetCachePath(); - ResolvedCollectionMethod SetupMethodsForLoop(); + CollectionMethod SetupMethodsForLoop(); string ResolveFileName(string filename, string extension, bool addTimestamp); EnumerationDomain[] Domains { get; set; } void UpdateLoopTime(); diff --git a/src/Client/Flags.cs b/src/Client/Flags.cs index 4e5e39b..48e935a 100644 --- a/src/Client/Flags.cs +++ b/src/Client/Flags.cs @@ -27,5 +27,6 @@ public class Flags public bool SearchForest { get; set; } public bool RecurseDomains { get; set; } public bool DoLocalAdminSessionEnum { get; set; } + public bool ParititonLdapQueries { get; set; } } } \ No newline at end of file diff --git a/src/Client/Links.cs b/src/Client/Links.cs index ec4c7e4..131e421 100644 --- a/src/Client/Links.cs +++ b/src/Client/Links.cs @@ -9,15 +9,15 @@ namespace Sharphound.Client /// A context to be populated. public interface Links { - IContext Initialize(IContext context, LDAPConfig options); + IContext Initialize(IContext context, LdapConfig options); - IContext + Task TestConnection( T context); //Initial LDAP connection test. Search for the well known administrator SID to make sure we can connect successfully. IContext SetSessionUserName(string overrideUserName, T context); IContext InitCommonLib(T context); - IContext GetDomainsForEnumeration(T context); + Task GetDomainsForEnumeration(T context); IContext StartBaseCollectionTask(T context); Task AwaitBaseRunCompletion(T context); IContext StartLoopTimer(T context); diff --git a/src/EnumerationDomain.cs b/src/EnumerationDomain.cs index 7b211cc..c420161 100644 --- a/src/EnumerationDomain.cs +++ b/src/EnumerationDomain.cs @@ -1,9 +1,12 @@ +using System.DirectoryServices.ActiveDirectory; + namespace Sharphound { public class EnumerationDomain { public string Name { get; set; } public string DomainSid { get; set; } + public string TrustType { get; set; } } } diff --git a/src/Extensions.cs b/src/Extensions.cs index d90a3f2..4735a23 100644 --- a/src/Extensions.cs +++ b/src/Extensions.cs @@ -24,7 +24,7 @@ internal static void Merge(this Dictionary s, Dictio } } - public static string GetDNSName(this ISearchResultEntry entry, string overrideDNSName) + public static string GetDNSName(this IDirectoryObject entry, string overrideDNSName) { var shortName = entry.GetProperty("samaccountname")?.TrimEnd('$'); var dns = entry.GetProperty("dnshostname"); @@ -111,10 +111,10 @@ internal static async IAsyncEnumerable ReadAllAsync(this ChannelReader /// Removes non-computer collection methods from specified ones for looping /// /// - internal static ResolvedCollectionMethod GetLoopCollectionMethods(this ResolvedCollectionMethod methods) + internal static CollectionMethod GetLoopCollectionMethods(this CollectionMethod methods) { - const ResolvedCollectionMethod computerCollectionMethods = ResolvedCollectionMethod.LocalGroups | ResolvedCollectionMethod.LoggedOn | - ResolvedCollectionMethod.Session; + const CollectionMethod computerCollectionMethods = CollectionMethod.LocalGroups | CollectionMethod.LoggedOn | + CollectionMethod.Session; return methods & computerCollectionMethods; } } diff --git a/src/Options.cs b/src/Options.cs index 37b558f..6cd6e38 100644 --- a/src/Options.cs +++ b/src/Options.cs @@ -92,9 +92,12 @@ public class Options [Option(HelpText = "Override port for LDAP", Default = 0)] public int LDAPPort { get; set; } + + [Option(HelpText = "Override port for LDAPS", Default = 0)] + public int LDAPSSLPort { get; set; } - [Option(HelpText = "Connect to LDAP SSL instead of regular LDAP", Default = false)] - public bool SecureLDAP { get; set; } + [Option(HelpText = "Only connect to LDAP SSL, disallowing fallback", Default = false)] + public bool ForceSecureLDAP { get; set; } [Option(HelpText = "Disables certificate verification when using LDAPS", Default = false)] public bool DisableCertVerification { get; set; } @@ -136,6 +139,9 @@ public class Options [Option(HelpText = "Collect all LDAP properties from objects")] public bool CollectAllProperties { get; set; } + + [Option(HelpText = "Split the main ldap query into smaller chunks to attempt to reduce server load")] + public bool PartitionLdapQueries { get; set; } //Loop Options [Option('l', "Loop", HelpText = "Loop computer collection")] @@ -153,13 +159,13 @@ public class Options [Option('v', HelpText = "Enable verbose output", Default = (int)LogLevel.Information)] public int Verbosity { get; set; } - internal bool ResolveCollectionMethods(ILogger logger, out ResolvedCollectionMethod resolved, out bool dconly) + internal bool ResolveCollectionMethods(ILogger logger, out CollectionMethod resolved, out bool dconly) { var arr = CollectionMethods.Count() == 1 ? CollectionMethods.First().Split(',') : CollectionMethods.ToArray(); - resolved = ResolvedCollectionMethod.None; + resolved = CollectionMethod.None; dconly = false; foreach (var baseMethod in arr) @@ -177,29 +183,29 @@ internal bool ResolveCollectionMethods(ILogger logger, out ResolvedCollectionMet resolved |= option switch { - CollectionMethodOptions.Group => ResolvedCollectionMethod.Group, - CollectionMethodOptions.Session => ResolvedCollectionMethod.Session, - CollectionMethodOptions.LoggedOn => ResolvedCollectionMethod.LoggedOn, - CollectionMethodOptions.Trusts => ResolvedCollectionMethod.Trusts, - CollectionMethodOptions.ACL => ResolvedCollectionMethod.ACL, - CollectionMethodOptions.ObjectProps => ResolvedCollectionMethod.ObjectProps, - CollectionMethodOptions.RDP => ResolvedCollectionMethod.RDP, - CollectionMethodOptions.DCOM => ResolvedCollectionMethod.DCOM, - CollectionMethodOptions.LocalAdmin => ResolvedCollectionMethod.LocalAdmin, - CollectionMethodOptions.PSRemote => ResolvedCollectionMethod.PSRemote, - CollectionMethodOptions.SPNTargets => ResolvedCollectionMethod.SPNTargets, - CollectionMethodOptions.Container => ResolvedCollectionMethod.Container, - CollectionMethodOptions.GPOLocalGroup => ResolvedCollectionMethod.GPOLocalGroup, - CollectionMethodOptions.LocalGroup => ResolvedCollectionMethod.LocalGroups, - CollectionMethodOptions.UserRights => ResolvedCollectionMethod.UserRights, - CollectionMethodOptions.Default => ResolvedCollectionMethod.Default, - CollectionMethodOptions.DCOnly => ResolvedCollectionMethod.DCOnly, - CollectionMethodOptions.ComputerOnly => ResolvedCollectionMethod.ComputerOnly, - CollectionMethodOptions.CARegistry => ResolvedCollectionMethod.CARegistry, - CollectionMethodOptions.DCRegistry => ResolvedCollectionMethod.DCRegistry, - CollectionMethodOptions.CertServices => ResolvedCollectionMethod.CertServices, - CollectionMethodOptions.All => ResolvedCollectionMethod.All, - CollectionMethodOptions.None => ResolvedCollectionMethod.None, + CollectionMethodOptions.Group => CollectionMethod.Group, + CollectionMethodOptions.Session => CollectionMethod.Session, + CollectionMethodOptions.LoggedOn => CollectionMethod.LoggedOn, + CollectionMethodOptions.Trusts => CollectionMethod.Trusts, + CollectionMethodOptions.ACL => CollectionMethod.ACL, + CollectionMethodOptions.ObjectProps => CollectionMethod.ObjectProps, + CollectionMethodOptions.RDP => CollectionMethod.RDP, + CollectionMethodOptions.DCOM => CollectionMethod.DCOM, + CollectionMethodOptions.LocalAdmin => CollectionMethod.LocalAdmin, + CollectionMethodOptions.PSRemote => CollectionMethod.PSRemote, + CollectionMethodOptions.SPNTargets => CollectionMethod.SPNTargets, + CollectionMethodOptions.Container => CollectionMethod.Container, + CollectionMethodOptions.GPOLocalGroup => CollectionMethod.GPOLocalGroup, + CollectionMethodOptions.LocalGroup => CollectionMethod.LocalGroups, + CollectionMethodOptions.UserRights => CollectionMethod.UserRights, + CollectionMethodOptions.Default => CollectionMethod.Default, + CollectionMethodOptions.DCOnly => CollectionMethod.DCOnly, + CollectionMethodOptions.ComputerOnly => CollectionMethod.ComputerOnly, + CollectionMethodOptions.CARegistry => CollectionMethod.CARegistry, + CollectionMethodOptions.DCRegistry => CollectionMethod.DCRegistry, + CollectionMethodOptions.CertServices => CollectionMethod.CertServices, + CollectionMethodOptions.All => CollectionMethod.All, + CollectionMethodOptions.None => CollectionMethod.None, _ => throw new ArgumentOutOfRangeException() }; @@ -209,56 +215,56 @@ internal bool ResolveCollectionMethods(ILogger logger, out ResolvedCollectionMet if (Stealth) { var updates = new List(); - if ((resolved & ResolvedCollectionMethod.LoggedOn) != 0) + if ((resolved & CollectionMethod.LoggedOn) != 0) { - resolved ^= ResolvedCollectionMethod.LoggedOn; + resolved ^= CollectionMethod.LoggedOn; updates.Add("[-] Removed LoggedOn"); } var localGroupRemoved = false; - if ((resolved & ResolvedCollectionMethod.RDP) != 0) + if ((resolved & CollectionMethod.RDP) != 0) { localGroupRemoved = true; - resolved ^= ResolvedCollectionMethod.RDP; + resolved ^= CollectionMethod.RDP; updates.Add("[-] Removed RDP Collection"); } - if ((resolved & ResolvedCollectionMethod.DCOM) != 0) + if ((resolved & CollectionMethod.DCOM) != 0) { localGroupRemoved = true; - resolved ^= ResolvedCollectionMethod.DCOM; + resolved ^= CollectionMethod.DCOM; updates.Add("[-] Removed DCOM Collection"); } - if ((resolved & ResolvedCollectionMethod.PSRemote) != 0) + if ((resolved & CollectionMethod.PSRemote) != 0) { localGroupRemoved = true; - resolved ^= ResolvedCollectionMethod.PSRemote; + resolved ^= CollectionMethod.PSRemote; updates.Add("[-] Removed PSRemote Collection"); } - if ((resolved & ResolvedCollectionMethod.LocalAdmin) != 0) + if ((resolved & CollectionMethod.LocalAdmin) != 0) { localGroupRemoved = true; - resolved ^= ResolvedCollectionMethod.LocalAdmin; + resolved ^= CollectionMethod.LocalAdmin; updates.Add("[-] Removed LocalAdmin Collection"); } - if ((resolved & ResolvedCollectionMethod.CARegistry) != 0) + if ((resolved & CollectionMethod.CARegistry) != 0) { - resolved ^= ResolvedCollectionMethod.CARegistry; + resolved ^= CollectionMethod.CARegistry; updates.Add("[-] Removed CARegistry Collection"); } - if ((resolved & ResolvedCollectionMethod.DCRegistry) != 0) + if ((resolved & CollectionMethod.DCRegistry) != 0) { - resolved ^= ResolvedCollectionMethod.DCRegistry; + resolved ^= CollectionMethod.DCRegistry; updates.Add("[-] Removed DCRegistry Collection"); } if (localGroupRemoved) { - resolved |= ResolvedCollectionMethod.GPOLocalGroup; + resolved |= CollectionMethod.GPOLocalGroup; updates.Add("[+] Added GPOLocalGroup"); } diff --git a/src/Producers/BaseProducer.cs b/src/Producers/BaseProducer.cs index b8fd9f2..8456cbd 100644 --- a/src/Producers/BaseProducer.cs +++ b/src/Producers/BaseProducer.cs @@ -21,11 +21,11 @@ namespace Sharphound.Producers /// public abstract class BaseProducer { - protected readonly Channel Channel; + protected readonly Channel Channel; protected readonly Channel OutputChannel; protected readonly IContext Context; - protected BaseProducer(IContext context, Channel channel, Channel outputChannel) + protected BaseProducer(IContext context, Channel channel, Channel outputChannel) { Context = context; Channel = channel; @@ -35,169 +35,21 @@ protected BaseProducer(IContext context, Channel channel, Ch public abstract Task Produce(); public abstract Task ProduceConfigNC(); - protected LDAPData CreateDefaultNCData() - { - var query = new LDAPFilter(); - var props = new List(); - var data = new LDAPData(); - props.AddRange(CommonProperties.BaseQueryProps); - props.AddRange(CommonProperties.TypeResolutionProps); - - var methods = Context.ResolvedCollectionMethods; - - if ((methods & ResolvedCollectionMethod.ObjectProps) != 0 || (methods & ResolvedCollectionMethod.ACL) != 0) - { - query = query.AddComputers().AddContainers().AddUsers().AddGroups().AddDomains().AddOUs().AddGPOs(); - props.AddRange(CommonProperties.ObjectPropsProps); - - if ((methods & ResolvedCollectionMethod.Container) != 0) - props.AddRange(CommonProperties.ContainerProps); - - if ((methods & ResolvedCollectionMethod.Group) != 0) - { - props.AddRange(CommonProperties.GroupResolutionProps); - query = query.AddPrimaryGroups(); - } - - if ((methods & ResolvedCollectionMethod.ACL) != 0) props.AddRange(CommonProperties.ACLProps); - - if ((methods & ResolvedCollectionMethod.LocalAdmin) != 0 || - (methods & ResolvedCollectionMethod.DCOM) != 0 || - (methods & ResolvedCollectionMethod.PSRemote) != 0 || - (methods & ResolvedCollectionMethod.RDP) != 0 || - (methods & ResolvedCollectionMethod.LoggedOn) != 0 || - (methods & ResolvedCollectionMethod.Session) != 0 || - (methods & ResolvedCollectionMethod.ObjectProps) != 0 || - (methods & ResolvedCollectionMethod.UserRights) != 0) - - props.AddRange(CommonProperties.ComputerMethodProps); - - if ((methods & ResolvedCollectionMethod.Trusts) != 0) props.AddRange(CommonProperties.DomainTrustProps); - - if ((methods & ResolvedCollectionMethod.GPOLocalGroup) != 0) - props.AddRange(CommonProperties.GPOLocalGroupProps); - - if ((methods & ResolvedCollectionMethod.SPNTargets) != 0) - props.AddRange(CommonProperties.SPNTargetProps); - - if ((methods & ResolvedCollectionMethod.DCRegistry) != 0) - props.AddRange(CommonProperties.ComputerMethodProps); - } - else - { - if ((methods & ResolvedCollectionMethod.Container) != 0) - { - query = query.AddComputers().AddContainers().AddUsers().AddGroups().AddDomains().AddOUs().AddGPOs(); - props.AddRange(CommonProperties.ContainerProps); - } - - if ((methods & ResolvedCollectionMethod.Group) != 0) - { - query = query.AddGroups().AddPrimaryGroups(); - props.AddRange(CommonProperties.GroupResolutionProps); - } - - if ((methods & ResolvedCollectionMethod.LocalAdmin) != 0 || - (methods & ResolvedCollectionMethod.DCOM) != 0 || - (methods & ResolvedCollectionMethod.PSRemote) != 0 || - (methods & ResolvedCollectionMethod.RDP) != 0 || - (methods & ResolvedCollectionMethod.LoggedOn) != 0 || - (methods & ResolvedCollectionMethod.Session) != 0 || - (methods & ResolvedCollectionMethod.ObjectProps) != 0 || - (methods & ResolvedCollectionMethod.UserRights) != 0) - { - query = query.AddComputers(); - props.AddRange(CommonProperties.ComputerMethodProps); - } - - if ((methods & ResolvedCollectionMethod.Trusts) != 0) - { - query = query.AddDomains(); - props.AddRange(CommonProperties.DomainTrustProps); - } + protected GeneratedLdapParameters CreateDefaultNCData() { + var baseData = + LdapProducerQueryGenerator.GenerateDefaultPartitionParameters(Context.ResolvedCollectionMethods); - if ((methods & ResolvedCollectionMethod.SPNTargets) != 0) - { - query = query.AddUsers(CommonFilters.NeedsSPN); - props.AddRange(CommonProperties.SPNTargetProps); - } - - if ((methods & ResolvedCollectionMethod.GPOLocalGroup) != 0) - { - query = query.AddOUs(); - props.AddRange(CommonProperties.GPOLocalGroupProps); - } - - if ((methods & ResolvedCollectionMethod.DCRegistry) != 0) - { - query = query.AddComputers(CommonFilters.DomainControllers); - props.AddRange(CommonProperties.ComputerMethodProps); - } - } - - if (Context.LdapFilter != null) query.AddFilter(Context.LdapFilter, true); - - data.Filter = query; - data.Props = props; - return data; + if (Context.LdapFilter != null) baseData.Filter.AddFilter(Context.LdapFilter, true); + return baseData; } - protected LDAPData CreateConfigNCData() + protected GeneratedLdapParameters CreateConfigNCData() { - var query = new LDAPFilter(); - var props = new List(); - var data = new LDAPData(); - props.AddRange(CommonProperties.BaseQueryProps); - props.AddRange(CommonProperties.TypeResolutionProps); - - var methods = Context.ResolvedCollectionMethods; - var allObjectTypesQuery = new LDAPFilter().AddContainers().AddConfiguration().AddCertificateTemplates().AddCertificateAuthorities().AddEnterpriseCertificationAuthorities().AddIssuancePolicies(); - - if ((methods & ResolvedCollectionMethod.ObjectProps) != 0) - { - query = allObjectTypesQuery; - props.AddRange(CommonProperties.ObjectPropsProps); - } - - if ((methods & ResolvedCollectionMethod.ACL) != 0) - { - query = allObjectTypesQuery; - props.AddRange(CommonProperties.ACLProps); - } - - if ((methods & ResolvedCollectionMethod.CertServices) != 0) - { - query = allObjectTypesQuery; - props.AddRange(CommonProperties.CertAbuseProps); - props.AddRange(CommonProperties.ObjectPropsProps); - props.AddRange(CommonProperties.ContainerProps); - props.AddRange(CommonProperties.ACLProps); - } + var baseData = + LdapProducerQueryGenerator.GenerateConfigurationPartitionParameters(Context.ResolvedCollectionMethods); - if ((methods & ResolvedCollectionMethod.Container) != 0) - { - query = allObjectTypesQuery; - props.AddRange(CommonProperties.ContainerProps); - } - - if ((methods & ResolvedCollectionMethod.CARegistry) != 0) - { - query = query.AddEnterpriseCertificationAuthorities(); - props.AddRange(CommonProperties.CertAbuseProps); - } - - - if (Context.LdapFilter != null) query.AddFilter(Context.LdapFilter, true); - - data.Filter = query; - data.Props = props; - return data; + if (Context.LdapFilter != null) baseData.Filter.AddFilter(Context.LdapFilter, true); + return baseData; } } - - public class LDAPData - { - internal LDAPFilter Filter { get; set; } - internal IEnumerable Props { get; set; } - } } \ No newline at end of file diff --git a/src/Producers/ComputerFileProducer.cs b/src/Producers/ComputerFileProducer.cs index 7069778..b6233df 100644 --- a/src/Producers/ComputerFileProducer.cs +++ b/src/Producers/ComputerFileProducer.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Logging; using Sharphound.Client; using SharpHoundCommonLib; +using SharpHoundCommonLib.LDAPQueries; using SharpHoundCommonLib.OutputTypes; namespace Sharphound.Producers @@ -16,7 +17,7 @@ namespace Sharphound.Producers /// internal class ComputerFileProducer : BaseProducer { - public ComputerFileProducer(IContext context, Channel channel, Channel outputChannel) : base(context, channel, outputChannel) + public ComputerFileProducer(IContext context, Channel channel, Channel outputChannel) : base(context, channel, outputChannel) { } @@ -32,6 +33,24 @@ public override async Task Produce() var ldapData = CreateDefaultNCData(); + + if (Context.Flags.CollectAllProperties) + { + Context.Logger.LogDebug("CollectAllProperties set. Changing LDAP properties to *"); + ldapData.Attributes = new[] { "*" }; + } + + string domainName; + if (Context.DomainName == null) { + if (!Context.LDAPUtils.GetDomain(out var domainObj)) { + Context.Logger.LogError("No domain name specified for computer file producer and unable to resolve a domain name"); + return; + } + domainName = domainObj?.Name; + } else { + domainName = Context.DomainName; + } + try { //Open the file for reading @@ -44,9 +63,15 @@ public override async Task Produce() if (cancellationToken.IsCancellationRequested) break; string sid; - if (!computer.StartsWith("S-1-5-21")) + if (!computer.StartsWith("S-1-5-21")) { //The computer isn't a SID so try to convert it to one - sid = await Context.LDAPUtils.ResolveHostToSid(computer, Context.DomainName); + if (await Context.LDAPUtils.ResolveHostToSid(computer, domainName) is (true, var tempSid)) { + sid = tempSid; + } else { + Context.Logger.LogError("Failed to resolve host {Computer} to SID", computer); + continue; + } + } else //The computer is already a sid, so just store it off sid = computer; @@ -54,11 +79,13 @@ public override async Task Produce() try { //Convert the sid to a hex representation and find the entry in the domain - var hexSid = Helpers.ConvertSidToHexSid(sid); - var entry = Context.LDAPUtils.QueryLDAP($"(objectsid={hexSid})", SearchScope.Subtree, - ldapData.Props.ToArray(), cancellationToken, Context.DomainName, - adsPath: Context.SearchBase).DefaultIfEmpty(null).FirstOrDefault(); - if (entry == null) + var entry = await Context.LDAPUtils.Query(new LdapQueryParameters() { + LDAPFilter = CommonFilters.SpecificSID(sid), + Attributes = ldapData.Attributes, + DomainName = domainName, + SearchBase = Context.SearchBase + }, cancellationToken).FirstOrDefaultAsync(LdapResult.Fail()); + if (!entry.IsSuccess) { //We couldn't find the entry for whatever reason Context.Logger.LogWarning("Failed to resolve {computer}", computer); @@ -66,7 +93,7 @@ public override async Task Produce() } //Success! Send the computer to be processed - await Channel.Writer.WriteAsync(entry, cancellationToken); + await Channel.Writer.WriteAsync(entry.Value, cancellationToken); } catch (Exception e) { diff --git a/src/Producers/LdapProducer.cs b/src/Producers/LdapProducer.cs index fb6027e..99aea43 100644 --- a/src/Producers/LdapProducer.cs +++ b/src/Producers/LdapProducer.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.DirectoryServices.Protocols; -using System.Linq; using System.Threading.Channels; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -14,7 +12,7 @@ namespace Sharphound.Producers { public class LdapProducer : BaseProducer { - public LdapProducer(IContext context, Channel channel, Channel outputChannel) : base(context, channel, outputChannel) + public LdapProducer(IContext context, Channel channel, Channel outputChannel) : base(context, channel, outputChannel) { } @@ -39,48 +37,61 @@ public override async Task Produce() if (Context.Flags.CollectAllProperties) { log.LogDebug("CollectAllProperties set. Changing LDAP properties to *"); - ldapData.Props = new[] { "*" }; + ldapData.Attributes = new[] { "*" }; } foreach (var domain in Context.Domains) { Context.Logger.LogInformation("Beginning LDAP search for {Domain}", domain.Name); //Do a basic LDAP search and grab results - var successfulConnect = false; - try - { - log.LogInformation("Testing ldap connection to {Domain}", domain.Name); - successfulConnect = utils.TestLDAPConfig(domain.Name); - } - catch (Exception e) - { - log.LogError(e, "Unable to connect to domain {Domain}", domain.Name); - continue; - } - - if (!successfulConnect) - { - log.LogError("Successful connection made to {Domain} but no data returned", domain.Name); + if (await utils.TestLdapConnection(domain.Name) is (false, var message)) { + log.LogError("Unable to connect to domain {Domain}: {Message}", domain.Name, message); continue; } Context.CollectedDomainSids.Add(domain.DomainSid); - foreach (var searchResult in Context.LDAPUtils.QueryLDAP(ldapData.Filter.GetFilter(), SearchScope.Subtree, - ldapData.Props.Distinct().ToArray(), cancellationToken, domain.Name, - adsPath: Context.SearchBase, - includeAcl: (Context.ResolvedCollectionMethods & ResolvedCollectionMethod.ACL) != 0)) - { - var l = searchResult.DistinguishedName.ToLower(); - if (l.Contains("cn=domainupdates,cn=system")) - continue; - if (l.Contains("cn=policies,cn=system") && (l.StartsWith("cn=user") || l.StartsWith("cn=machine"))) - continue; + foreach (var filter in ldapData.Filter.GetFilterList()) { + foreach (var partitionedFilter in GetPartitionedFilter(filter)) { + await foreach (var result in Context.LDAPUtils.PagedQuery(new LdapQueryParameters() { + LDAPFilter = partitionedFilter, + Attributes = ldapData.Attributes, + DomainName = domain.Name, + SearchBase = Context.SearchBase, + IncludeSecurityDescriptor = Context.ResolvedCollectionMethods.HasFlag(CollectionMethod.ACL) + }, cancellationToken)){ + if (!result.IsSuccess) { + Context.Logger.LogError("Error during main ldap query:{Message} ({Code})", result.Error, result.ErrorCode); + break; + } + + var searchResult = result.Value; - await Channel.Writer.WriteAsync(searchResult, cancellationToken); - Context.Logger.LogTrace("Producer wrote {DistinguishedName} to channel", searchResult.DistinguishedName); + if (searchResult.TryGetDistinguishedName(out var distinguishedName)) { + var lower = distinguishedName.ToLower(); + if (lower.Contains("cn=domainupdates,cn=system")) + continue; + if (lower.Contains("cn=policies,cn=system") && (lower.StartsWith("cn=user") || lower.StartsWith("cn=machine"))) + continue; + + await Channel.Writer.WriteAsync(searchResult, cancellationToken); + Context.Logger.LogTrace("Producer wrote {DistinguishedName} to channel", distinguishedName); + } + } + } + } + } + } + + private IEnumerable GetPartitionedFilter(string originalFilter) { + if (Context.Flags.ParititonLdapQueries) { + for (var i = 0; i < 256; i++) { + yield return $"(&{originalFilter}(objectguid=\\{i.ToString("x2")}*))"; } } + else { + yield return originalFilter; + } } /// @@ -91,33 +102,63 @@ public override async Task ProduceConfigNC() { var cancellationToken = Context.CancellationTokenSource.Token; var configNcData = CreateConfigNCData(); - var configurationNCsCollected = new List(); + var configurationNCsCollected = new HashSet(StringComparer.OrdinalIgnoreCase); if (string.IsNullOrEmpty(configNcData.Filter.GetFilter())) return; foreach (var domain in Context.Domains) { - var configAdsPath = Context.LDAPUtils.GetConfigurationPath(domain.Name); - if (!configurationNCsCollected.Contains(configAdsPath)) - { + if (await Context.LDAPUtils.GetNamingContextPath(domain.Name, NamingContext.Configuration) is + (true, var path)) { + if (!configurationNCsCollected.Add(path)) { + continue; + } + 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 || (Context.ResolvedCollectionMethods & ResolvedCollectionMethod.CertServices) != 0)) - { - await Channel.Writer.WriteAsync(searchResult, cancellationToken); - Context.Logger.LogTrace("Producer wrote {DistinguishedName} to channel", searchResult.DistinguishedName); + foreach (var filter in configNcData.Filter.GetFilterList()) { + await foreach (var result in Context.LDAPUtils.PagedQuery(new LdapQueryParameters() { + LDAPFilter = filter, + Attributes = configNcData.Attributes, + DomainName = domain.Name, + SearchBase = path, + IncludeSecurityDescriptor = Context.ResolvedCollectionMethods.HasFlag(CollectionMethod.ACL) + }, cancellationToken)){ + if (!result.IsSuccess) { + Context.Logger.LogError("Error during main ldap query:{Message} ({Code})", result.Error, result.ErrorCode); + break; + } + + var searchResult = result.Value; + + if (searchResult.TryGetDistinguishedName(out var distinguishedName)) { + await Channel.Writer.WriteAsync(searchResult, cancellationToken); + Context.Logger.LogTrace("Producer wrote {DistinguishedName} to channel", distinguishedName); + } + } + } + } else { + foreach (var filter in configNcData.Filter.GetFilterList()) { + await foreach (var result in Context.LDAPUtils.PagedQuery(new LdapQueryParameters() { + LDAPFilter = filter, + Attributes = configNcData.Attributes, + DomainName = domain.Name, + IncludeSecurityDescriptor = Context.ResolvedCollectionMethods.HasFlag(CollectionMethod.ACL), + NamingContext = NamingContext.Configuration + }, cancellationToken)){ + if (!result.IsSuccess) { + Context.Logger.LogError("Error during main ldap query:{Message} ({Code})", result.Error, result.ErrorCode); + break; + } + + var searchResult = result.Value; + + if (searchResult.TryGetDistinguishedName(out var distinguishedName)) { + await Channel.Writer.WriteAsync(searchResult, cancellationToken); + Context.Logger.LogTrace("Producer wrote {DistinguishedName} to channel", distinguishedName); + } + } } - } - else - { - Context.Logger.LogTrace("Skipping already collected config NC '{path}' for domain {Domain}", configAdsPath, domain.Name); } } } diff --git a/src/Producers/StealthContext.cs b/src/Producers/StealthContext.cs index 3c47b24..6880760 100644 --- a/src/Producers/StealthContext.cs +++ b/src/Producers/StealthContext.cs @@ -5,13 +5,13 @@ namespace Sharphound.Producers { public static class StealthContext { - private static Dictionary _stealthTargetSids; + private static Dictionary _stealthTargetSids; /// /// Sets the list of stealth targets or appends to it if necessary /// /// - internal static void AddStealthTargetSids(Dictionary targets) + internal static void AddStealthTargetSids(Dictionary targets) { if (_stealthTargetSids == null) _stealthTargetSids = targets; @@ -26,7 +26,7 @@ internal static bool IsSidStealthTarget(string sid) return _stealthTargetSids.ContainsKey(sid); } - internal static IEnumerable GetSearchResultEntries() + internal static IEnumerable GetSearchResultEntries() { return _stealthTargetSids.Values; } diff --git a/src/Producers/StealthProducer.cs b/src/Producers/StealthProducer.cs index a88a30e..dafeece 100644 --- a/src/Producers/StealthProducer.cs +++ b/src/Producers/StealthProducer.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.DirectoryServices.Protocols; @@ -7,6 +8,7 @@ using Microsoft.Extensions.Logging; using Sharphound.Client; using SharpHoundCommonLib; +using SharpHoundCommonLib.Enums; using SharpHoundCommonLib.LDAPQueries; using SharpHoundCommonLib.OutputTypes; @@ -18,20 +20,20 @@ namespace Sharphound.Producers internal class StealthProducer : BaseProducer { private static bool _stealthTargetsBuilt; - private readonly IEnumerable _props; - private readonly IEnumerable _propsConfigNC; - private readonly LDAPFilter _query; - private readonly LDAPFilter _queryConfigNC; + private readonly string[] _props; + private readonly string[] _propsConfigNC; + private readonly LdapFilter _query; + private readonly LdapFilter _queryConfigNC; - public StealthProducer(IContext context, Channel channel, Channel outputChannel) : base(context, channel, outputChannel) + public StealthProducer(IContext context, Channel channel, Channel outputChannel) : base(context, channel, outputChannel) { var ldapData = CreateDefaultNCData(); _query = ldapData.Filter; - _props = ldapData.Props; + _props = ldapData.Attributes; var configNCData = CreateConfigNCData(); _queryConfigNC = configNCData.Filter; - _propsConfigNC = configNCData.Props; + _propsConfigNC = configNCData.Attributes; } /// @@ -47,26 +49,42 @@ public override async Task Produce() //OutputTasks.StartOutputTimer(context); //Output our stealth targets to the queue - foreach (var searchResult in Context.LDAPUtils.QueryLDAP(_query.GetFilter(), SearchScope.Subtree, - _props.ToArray(), cancellationToken, - Context.DomainName, adsPath: Context.SearchBase)) - await Channel.Writer.WriteAsync(searchResult, cancellationToken); + await foreach (var result in Context.LDAPUtils.PagedQuery(new LdapQueryParameters() { + LDAPFilter = _query.GetFilter(), + Attributes = _props, + SearchBase = Context.SearchBase, + DomainName = Context.DomainName + }, cancellationToken)) { + if (!result.IsSuccess) { + Context.Logger.LogError("Error in stealth producer: {Message} ({Code})", result.Error, result.ErrorCode); + break; + } + await Channel.Writer.WriteAsync(result.Value, cancellationToken); + } } public override async Task ProduceConfigNC() { + if (string.IsNullOrEmpty(_queryConfigNC.GetFilter())) + return; var cancellationToken = Context.CancellationTokenSource.Token; //If we haven't generated our stealth targets, we'll build it now if (!_stealthTargetsBuilt) BuildStealthTargets(); - - var configAdsPath = Context.LDAPUtils.GetConfigurationPath(Context.DomainName); - + //Output our stealth targets to the queue - foreach (var searchResult in Context.LDAPUtils.QueryLDAP(_queryConfigNC.GetFilter(), SearchScope.Subtree, - _propsConfigNC.ToArray(), cancellationToken, - Context.DomainName, adsPath: configAdsPath)) - await Channel.Writer.WriteAsync(searchResult, cancellationToken); + await foreach (var result in Context.LDAPUtils.PagedQuery(new LdapQueryParameters() { + LDAPFilter = _query.GetFilter(), + Attributes = _props, + DomainName = Context.DomainName, + NamingContext = NamingContext.Configuration + }, cancellationToken)) { + if (!result.IsSuccess) { + Context.Logger.LogError("Error in stealth producer: {Message} ({Code})", result.Error, result.ErrorCode); + break; + } + await Channel.Writer.WriteAsync(result.Value, cancellationToken); + } } @@ -75,68 +93,86 @@ private async void BuildStealthTargets() Context.Logger.LogInformation("Finding Stealth Targets from LDAP Properties"); var targets = await FindPathTargetSids(); - if (!Context.Flags.ExcludeDomainControllers) targets.Merge(FindDomainControllers()); + if (!Context.Flags.ExcludeDomainControllers) targets.Merge(await FindDomainControllers()); StealthContext.AddStealthTargetSids(targets); _stealthTargetsBuilt = true; } - private Dictionary FindDomainControllers() - { - return Context.LDAPUtils.QueryLDAP(CommonFilters.DomainControllers, - SearchScope.Subtree, _props.ToArray(), Context.DomainName).Where(x => x.GetSid() != null) - .ToDictionary(x => x.GetSid()); + private async Task> FindDomainControllers() { + var res = new Dictionary(); + await foreach (var result in Context.LDAPUtils.PagedQuery(new LdapQueryParameters() { + LDAPFilter = CommonFilters.DomainControllers, + Attributes = _props, + DomainName = Context.DomainName + })) { + if (!result.IsSuccess) { + break; + } + + if (!result.Value.TryGetSecurityIdentifier(out var sid)) { + continue; + } + + res.Add(sid, result.Value); + } + + return res; } /// /// Finds stealth targets using ldap properties. /// /// - private async Task> FindPathTargetSids() + private async Task> FindPathTargetSids() { var paths = new ConcurrentDictionary(); - var sids = new Dictionary(); - - var query = new LDAPFilter(); + var sids = new Dictionary(); + + //Request user objects with the "homedirectory", "scriptpath", or "profilepath" attributes + var query = new LdapFilter(); query.AddComputers("(|(homedirectory=*)(scriptpath=*)(profilepath=*))"); foreach (var domain in Context.Domains) { - //Request user objects with the "homedirectory", "scriptpath", or "profilepath" attributes - Parallel.ForEach(Context.LDAPUtils.QueryLDAP( - query.GetFilter(), - SearchScope.Subtree, - new[] { "homedirectory", "scriptpath", "profilepath" }, domain.Name), searchResult => - { - //Grab any properties that exist, filter out null values - var poss = new[] - { - searchResult.GetProperty("homedirectory"), searchResult.GetProperty("scriptpath"), - searchResult.GetProperty("profilepath") - }.Where(s => s != null); - - // Loop over each possibility, and grab the hostname from the path, adding it to a list - foreach (var s in poss) - { - var split = s.Split('\\'); - if (!(split.Length >= 3)) continue; - var path = split[2]; - paths.TryAdd(path, new byte()); + await foreach (var searchResult in Context.LDAPUtils.PagedQuery(new LdapQueryParameters() { + LDAPFilter = query.GetFilter(), + Attributes = CommonProperties.StealthProperties, + DomainName = domain.Name + })) { + if (searchResult.IsSuccess) { + var poss = new[] + { + searchResult.Value.GetProperty("homedirectory"), searchResult.Value.GetProperty("scriptpath"), + searchResult.Value.GetProperty("profilepath") + }.Where(s => s != null); + + foreach (var s in poss) + { + var split = s.Split('\\'); + if (!(split.Length >= 3)) continue; + var path = split[2]; + paths.TryAdd(path, new byte()); + } } - }); + } } - - - + // Loop over the paths we grabbed, and resolve them to sids. foreach (var path in paths.Keys) { - var sid = await Context.LDAPUtils.ResolveHostToSid(path, Context.DomainName); - - if (sid != null && sid.StartsWith("S-1-5")) - { - var searchResult = Context.LDAPUtils.QueryLDAP(CommonFilters.SpecificSID(sid), - SearchScope.Subtree, _props.ToArray()); - sids.Add(sid, searchResult.FirstOrDefault()); + if (await Context.LDAPUtils.ResolveHostToSid(path, Context.DomainName) is (true, var sid)) { + if (sid != null && sid.StartsWith("S-1-5")) { + var searchResult = await Context.LDAPUtils.Query(new LdapQueryParameters() { + LDAPFilter = CommonFilters.SpecificSID(sid), + SearchScope = SearchScope.Subtree, + Attributes = _props, + DomainName = Context.DomainName + }).FirstOrDefaultAsync(LdapResult.Fail()); + + if (searchResult.IsSuccess) { + sids.Add(sid, searchResult.Value); + } + } } } diff --git a/src/Runtime/CollectionTask.cs b/src/Runtime/CollectionTask.cs index 5bb57ff..edc5361 100644 --- a/src/Runtime/CollectionTask.cs +++ b/src/Runtime/CollectionTask.cs @@ -15,7 +15,7 @@ public class CollectionTask private readonly Channel _compStatusChannel; private readonly CompStatusWriter _compStatusWriter; private readonly IContext _context; - private readonly Channel _ldapChannel; + private readonly Channel _ldapChannel; private readonly ILogger _log; private readonly Channel _outputChannel; @@ -28,7 +28,7 @@ public CollectionTask(IContext context) { _context = context; _log = context.Logger; - _ldapChannel = Channel.CreateBounded(new BoundedChannelOptions(1000) + _ldapChannel = Channel.CreateBounded(new BoundedChannelOptions(1000) { SingleWriter = true, SingleReader = false, @@ -82,7 +82,7 @@ internal async Task StartCollection() await Task.WhenAll(_taskPool); _log.LogInformation("Consumers finished, closing output channel"); - foreach (var wkp in _context.LDAPUtils.GetWellKnownPrincipalOutput(_context.DomainName)) + await foreach (var wkp in _context.LDAPUtils.GetWellKnownPrincipalOutput()) { if (!wkp.ObjectIdentifier.EndsWith(EnterpriseDCSuffix)) { diff --git a/src/Runtime/LDAPConsumer.cs b/src/Runtime/LDAPConsumer.cs index 6960bd5..4aa4046 100644 --- a/src/Runtime/LDAPConsumer.cs +++ b/src/Runtime/LDAPConsumer.cs @@ -12,7 +12,7 @@ namespace Sharphound.Runtime { public static class LDAPConsumer { - internal static async Task ConsumeSearchResults(Channel inputChannel, + internal static async Task ConsumeSearchResults(Channel inputChannel, Channel computerStatusChannel, Channel outputChannel, IContext context, int id) { @@ -24,10 +24,9 @@ internal static async Task ConsumeSearchResults(Channel inpu await foreach (var item in inputChannel.Reader.ReadAllAsync()) try { - var res = item.ResolveBloodHoundInfo(); - - if (res == null) + if (await LdapUtils.ResolveSearchResult(item, context.LDAPUtils) is not (true, var res) || res == null) { continue; + } log.LogTrace("Consumer {ThreadID} started processing {obj}", threadId, res.DisplayName); watch.Start(); diff --git a/src/Runtime/ObjectProcessors.cs b/src/Runtime/ObjectProcessors.cs index 5a03232..4e289ad 100644 --- a/src/Runtime/ObjectProcessors.cs +++ b/src/Runtime/ObjectProcessors.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.Logging; using Sharphound.Client; using SharpHoundCommonLib; +using SharpHoundCommonLib.DirectoryObjects; using SharpHoundCommonLib.Enums; using SharpHoundCommonLib.OutputTypes; using SharpHoundCommonLib.Processors; @@ -15,10 +16,8 @@ using Group = SharpHoundCommonLib.OutputTypes.Group; using Label = SharpHoundCommonLib.Enums.Label; -namespace Sharphound.Runtime -{ - public class ObjectProcessors - { +namespace Sharphound.Runtime { + public class ObjectProcessors { private const string StatusSuccess = "Success"; private readonly ACLProcessor _aclProcessor; private readonly CertAbuseProcessor _certAbuseProcessor; @@ -30,25 +29,27 @@ public class ObjectProcessors private readonly DCRegistryProcessor _dCRegistryProcessor; private readonly DomainTrustProcessor _domainTrustProcessor; private readonly GroupProcessor _groupProcessor; - private readonly LDAPPropertyProcessor _ldapPropertyProcessor; + private readonly LdapPropertyProcessor _ldapPropertyProcessor; private readonly GPOLocalGroupProcessor _gpoLocalGroupProcessor; private readonly UserRightsAssignmentProcessor _userRightsAssignmentProcessor; private readonly LocalGroupProcessor _localGroupProcessor; private readonly ILogger _log; - private readonly ResolvedCollectionMethod _methods; + private readonly CollectionMethod _methods; private readonly SPNProcessors _spnProcessor; - public ObjectProcessors(IContext context, ILogger log) - { + public ObjectProcessors(IContext context, ILogger log) { _context = context; _aclProcessor = new ACLProcessor(context.LDAPUtils); _spnProcessor = new SPNProcessors(context.LDAPUtils); - _ldapPropertyProcessor = new LDAPPropertyProcessor(context.LDAPUtils); + _ldapPropertyProcessor = new LdapPropertyProcessor(context.LDAPUtils); _domainTrustProcessor = new DomainTrustProcessor(context.LDAPUtils); - _computerAvailability = new ComputerAvailability(context.PortScanTimeout, skipPortScan: context.Flags.SkipPortScan, skipPasswordCheck: context.Flags.SkipPasswordAgeCheck); + _computerAvailability = new ComputerAvailability(context.PortScanTimeout, + skipPortScan: context.Flags.SkipPortScan, skipPasswordCheck: context.Flags.SkipPasswordAgeCheck); _certAbuseProcessor = new CertAbuseProcessor(context.LDAPUtils); _dCRegistryProcessor = new DCRegistryProcessor(context.LDAPUtils); - _computerSessionProcessor = new ComputerSessionProcessor(context.LDAPUtils, doLocalAdminSessionEnum: context.Flags.DoLocalAdminSessionEnum, localAdminUsername: context.LocalAdminUsername, localAdminPassword: context.LocalAdminPassword); + _computerSessionProcessor = new ComputerSessionProcessor(context.LDAPUtils, + doLocalAdminSessionEnum: context.Flags.DoLocalAdminSessionEnum, + localAdminUsername: context.LocalAdminUsername, localAdminPassword: context.LocalAdminPassword); _groupProcessor = new GroupProcessor(context.LDAPUtils); _containerProcessor = new ContainerProcessor(context.LDAPUtils); _gpoLocalGroupProcessor = new GPOLocalGroupProcessor(context.LDAPUtils); @@ -59,38 +60,36 @@ public ObjectProcessors(IContext context, ILogger log) _log = log; } - internal async Task ProcessObject(ISearchResultEntry entry, - ResolvedSearchResult resolvedSearchResult, Channel compStatusChannel) - { - switch (resolvedSearchResult.ObjectType) - { + internal async Task ProcessObject(IDirectoryObject entry, + ResolvedSearchResult resolvedSearchResult, Channel compStatusChannel) { + switch (resolvedSearchResult.ObjectType) { case Label.User: return await ProcessUserObject(entry, resolvedSearchResult); case Label.Computer: return await ProcessComputerObject(entry, resolvedSearchResult, compStatusChannel); case Label.Group: - return ProcessGroupObject(entry, resolvedSearchResult); + return await ProcessGroupObject(entry, resolvedSearchResult); case Label.GPO: - return ProcessGPOObject(entry, resolvedSearchResult); + return await ProcessGPOObject(entry, resolvedSearchResult); case Label.Domain: return await ProcessDomainObject(entry, resolvedSearchResult); case Label.OU: return await ProcessOUObject(entry, resolvedSearchResult); case Label.Container: case Label.Configuration: - return ProcessContainerObject(entry, resolvedSearchResult); + return await ProcessContainerObject(entry, resolvedSearchResult); case Label.RootCA: - return ProcessRootCA(entry, resolvedSearchResult); + return await ProcessRootCA(entry, resolvedSearchResult); case Label.AIACA: - return ProcessAIACA(entry, resolvedSearchResult); + return await ProcessAIACA(entry, resolvedSearchResult); case Label.EnterpriseCA: return await ProcessEnterpriseCA(entry, resolvedSearchResult); case Label.NTAuthStore: - return ProcessNTAuthStore(entry, resolvedSearchResult); + return await ProcessNTAuthStore(entry, resolvedSearchResult); case Label.CertTemplate: - return ProcessCertTemplate(entry, resolvedSearchResult); + return await ProcessCertTemplate(entry, resolvedSearchResult); case Label.IssuancePolicy: - return ProcessIssuancePolicy(entry, resolvedSearchResult); + return await ProcessIssuancePolicy(entry, resolvedSearchResult); case Label.Base: return null; default: @@ -98,85 +97,84 @@ internal async Task ProcessObject(ISearchResultEntry entry, } } - private async Task ProcessUserObject(ISearchResultEntry entry, - ResolvedSearchResult resolvedSearchResult) - { - var ret = new User - { + private static Dictionary GetCommonProperties(IDirectoryObject entry, + ResolvedSearchResult resolvedSearchResult) { + var props = new Dictionary { + { "domain", resolvedSearchResult.Domain }, + { "name", resolvedSearchResult.DisplayName }, + }; + + if (entry.TryGetDistinguishedName(out var distinguishedName)) { + props.Add("distinguishedname", distinguishedName.ToUpper()); + } + + if (!string.IsNullOrWhiteSpace(resolvedSearchResult.DomainSid)) { + props.Add("domainsid", resolvedSearchResult.DomainSid); + } + + return props; + } + + private async Task ProcessUserObject(IDirectoryObject entry, + ResolvedSearchResult resolvedSearchResult) { + var ret = new User { ObjectIdentifier = resolvedSearchResult.ObjectId }; - ret.Properties.Add("domain", resolvedSearchResult.Domain); - ret.Properties.Add("name", resolvedSearchResult.DisplayName); - ret.Properties.Add("distinguishedname", entry.DistinguishedName.ToUpper()); - ret.Properties.Add("domainsid", resolvedSearchResult.DomainSid); + ret.Properties = new Dictionary(GetCommonProperties(entry, resolvedSearchResult)); 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) - { - var aces = _aclProcessor.ProcessACL(resolvedSearchResult, entry); + if ((_methods & CollectionMethod.ACL) != 0) { + var aces = await _aclProcessor.ProcessACL(resolvedSearchResult, entry) + .ToArrayAsync(cancellationToken: _cancellationToken); var gmsa = entry.GetByteProperty(LDAPProperties.GroupMSAMembership); - ret.Aces = aces.Concat(_aclProcessor.ProcessGMSAReaders(gmsa, resolvedSearchResult.Domain)).ToArray(); + ret.Aces = aces.Concat(await _aclProcessor.ProcessGMSAReaders(gmsa, resolvedSearchResult.Domain) + .ToArrayAsync(cancellationToken: _cancellationToken)).ToArray(); ret.IsACLProtected = _aclProcessor.IsACLProtected(entry); ret.Properties.Add("isaclprotected", ret.IsACLProtected); } - if ((_methods & ResolvedCollectionMethod.Group) != 0) - { + if ((_methods & CollectionMethod.Group) != 0) { var pg = entry.GetProperty(LDAPProperties.PrimaryGroupID); ret.PrimaryGroupSID = GroupProcessor.GetPrimaryGroupInfo(pg, resolvedSearchResult.ObjectId); } - if ((_methods & ResolvedCollectionMethod.ObjectProps) != 0) - { - var userProps = await _ldapPropertyProcessor.ReadUserProperties(entry); + if ((_methods & CollectionMethod.ObjectProps) != 0) { + var userProps = await _ldapPropertyProcessor.ReadUserProperties(entry, resolvedSearchResult); ret.Properties = ContextUtils.Merge(ret.Properties, userProps.Props); - if (_context.Flags.CollectAllProperties) - { + if (_context.Flags.CollectAllProperties) { ret.Properties = ContextUtils.Merge(_ldapPropertyProcessor.ParseAllProperties(entry), ret.Properties); } + ret.HasSIDHistory = userProps.SidHistory; ret.AllowedToDelegate = userProps.AllowedToDelegate; } - if ((_methods & ResolvedCollectionMethod.SPNTargets) != 0) - { - var spn = entry.GetArrayProperty(LDAPProperties.ServicePrincipalNames); - - var targets = new List(); - var enumerator = _spnProcessor.ReadSPNTargets(spn, entry.DistinguishedName) - .GetAsyncEnumerator(_cancellationToken); - - while (await enumerator.MoveNextAsync()) targets.Add(enumerator.Current); - - ret.SPNTargets = targets.ToArray(); + if ((_methods & CollectionMethod.SPNTargets) != 0) { + ret.SPNTargets = await _spnProcessor.ReadSPNTargets(resolvedSearchResult, entry) + .ToArrayAsync(cancellationToken: _cancellationToken); } - if ((_methods & ResolvedCollectionMethod.Container) != 0) - { - ret.ContainedBy = _containerProcessor.GetContainingObject(entry.DistinguishedName); + if ((_methods & CollectionMethod.Container) != 0) { + if (entry.TryGetDistinguishedName(out var dn) && + await _containerProcessor.GetContainingObject(dn) is (true, var container)) { + ret.ContainedBy = container; + } } return ret; } - private async Task ProcessComputerObject(ISearchResultEntry entry, - ResolvedSearchResult resolvedSearchResult, Channel compStatusChannel) - { - var ret = new Computer - { + private async Task ProcessComputerObject(IDirectoryObject entry, + ResolvedSearchResult resolvedSearchResult, Channel compStatusChannel) { + var ret = new Computer { ObjectIdentifier = resolvedSearchResult.ObjectId }; - ret.Properties.Add("domain", resolvedSearchResult.Domain); - ret.Properties.Add("name", resolvedSearchResult.DisplayName); - ret.Properties.Add("distinguishedname", entry.DistinguishedName.ToUpper()); - ret.Properties.Add("domainsid", resolvedSearchResult.DomainSid); + ret.Properties = new Dictionary(GetCommonProperties(entry, resolvedSearchResult)); ret.Properties.Add("samaccountname", entry.GetProperty(LDAPProperties.SAMAccountName)); var hasLaps = entry.HasLAPS(); @@ -184,37 +182,36 @@ private async Task ProcessComputerObject(ISearchResultEntry entry, ret.IsDC = resolvedSearchResult.IsDomainController; ret.DomainSID = resolvedSearchResult.DomainSid; - if ((_methods & ResolvedCollectionMethod.ACL) != 0) - { - ret.Aces = _aclProcessor.ProcessACL(resolvedSearchResult, entry).ToArray(); + if ((_methods & CollectionMethod.ACL) != 0) { + ret.Aces = await _aclProcessor.ProcessACL(resolvedSearchResult, entry).ToArrayAsync(_cancellationToken); ret.IsACLProtected = _aclProcessor.IsACLProtected(entry); ret.Properties.Add("isaclprotected", ret.IsACLProtected); } - if ((_methods & ResolvedCollectionMethod.Group) != 0) - { + if ((_methods & CollectionMethod.Group) != 0) { var pg = entry.GetProperty(LDAPProperties.PrimaryGroupID); ret.PrimaryGroupSID = GroupProcessor.GetPrimaryGroupInfo(pg, resolvedSearchResult.ObjectId); } - if ((_methods & ResolvedCollectionMethod.ObjectProps) != 0) - { - var computerProps = await _ldapPropertyProcessor.ReadComputerProperties(entry); + if ((_methods & CollectionMethod.ObjectProps) != 0) { + var computerProps = await _ldapPropertyProcessor.ReadComputerProperties(entry, resolvedSearchResult); ret.Properties = ContextUtils.Merge(ret.Properties, computerProps.Props); - if (_context.Flags.CollectAllProperties) - { + if (_context.Flags.CollectAllProperties) { ret.Properties = ContextUtils.Merge(_ldapPropertyProcessor.ParseAllProperties(entry), ret.Properties); } + ret.AllowedToDelegate = computerProps.AllowedToDelegate; ret.AllowedToAct = computerProps.AllowedToAct; ret.HasSIDHistory = computerProps.SidHistory; ret.DumpSMSAPassword = computerProps.DumpSMSAPassword; } - if ((_methods & ResolvedCollectionMethod.Container) != 0) - { - ret.ContainedBy = _containerProcessor.GetContainingObject(entry.DistinguishedName); + if ((_methods & CollectionMethod.Container) != 0) { + if (entry.TryGetDistinguishedName(out var dn) && + await _containerProcessor.GetContainingObject(dn) is (true, var container)) { + ret.ContainedBy = container; + } } if (!_methods.IsComputerCollectionSet()) @@ -226,8 +223,7 @@ private async Task ProcessComputerObject(ISearchResultEntry entry, var availability = await _computerAvailability.IsComputerAvailable(resolvedSearchResult, entry); - if (!availability.Connectable) - { + if (!availability.Connectable) { await compStatusChannel.Writer.WriteAsync(availability.GetCSVStatus(resolvedSearchResult.DisplayName), _cancellationToken); ret.Status = availability; @@ -236,12 +232,11 @@ await compStatusChannel.Writer.WriteAsync(availability.GetCSVStatus(resolvedSear // DCRegistry if (resolvedSearchResult.IsDomainController & - (_methods & ResolvedCollectionMethod.DCRegistry) != 0) - { - DCRegistryData dCRegistryData = new() - { + (_methods & CollectionMethod.DCRegistry) != 0) { + DCRegistryData dCRegistryData = new() { CertificateMappingMethods = _dCRegistryProcessor.GetCertificateMappingMethods(apiName), - StrongCertificateBindingEnforcement = _dCRegistryProcessor.GetStrongCertificateBindingEnforcement(apiName) + StrongCertificateBindingEnforcement = + _dCRegistryProcessor.GetStrongCertificateBindingEnforcement(apiName) }; ret.DCRegistryData = dCRegistryData; @@ -249,23 +244,20 @@ await compStatusChannel.Writer.WriteAsync(availability.GetCSVStatus(resolvedSear var samAccountName = entry.GetProperty(LDAPProperties.SAMAccountName)?.TrimEnd('$'); - if ((_methods & ResolvedCollectionMethod.Session) != 0) - { + if ((_methods & CollectionMethod.Session) != 0) { await _context.DoDelay(); var sessionResult = await _computerSessionProcessor.ReadUserSessions(apiName, resolvedSearchResult.ObjectId, resolvedSearchResult.Domain); ret.Sessions = sessionResult; if (_context.Flags.DumpComputerStatus) - await compStatusChannel.Writer.WriteAsync(new CSVComputerStatus - { + await compStatusChannel.Writer.WriteAsync(new CSVComputerStatus { Status = sessionResult.Collected ? StatusSuccess : sessionResult.FailureReason, Task = "NetSessionEnum", ComputerName = resolvedSearchResult.DisplayName }, _cancellationToken); } - if ((_methods & ResolvedCollectionMethod.LoggedOn) != 0) - { + if ((_methods & CollectionMethod.LoggedOn) != 0) { await _context.DoDelay(); var privSessionResult = await _computerSessionProcessor.ReadUserSessionsPrivileged( resolvedSearchResult.DisplayName, samAccountName, @@ -273,22 +265,19 @@ await compStatusChannel.Writer.WriteAsync(new CSVComputerStatus ret.PrivilegedSessions = privSessionResult; if (_context.Flags.DumpComputerStatus) - await compStatusChannel.Writer.WriteAsync(new CSVComputerStatus - { + await compStatusChannel.Writer.WriteAsync(new CSVComputerStatus { Status = privSessionResult.Collected ? StatusSuccess : privSessionResult.FailureReason, Task = "NetWkstaUserEnum", ComputerName = resolvedSearchResult.DisplayName }, _cancellationToken); - if (!_context.Flags.NoRegistryLoggedOn) - { + if (!_context.Flags.NoRegistryLoggedOn) { await _context.DoDelay(); var registrySessionResult = await _computerSessionProcessor.ReadUserSessionsRegistry(apiName, resolvedSearchResult.Domain, resolvedSearchResult.ObjectId); ret.RegistrySessions = registrySessionResult; if (_context.Flags.DumpComputerStatus) - await compStatusChannel.Writer.WriteAsync(new CSVComputerStatus - { + await compStatusChannel.Writer.WriteAsync(new CSVComputerStatus { Status = privSessionResult.Collected ? StatusSuccess : privSessionResult.FailureReason, Task = "RegistrySessions", ComputerName = resolvedSearchResult.DisplayName @@ -296,12 +285,11 @@ await compStatusChannel.Writer.WriteAsync(new CSVComputerStatus } } - if ((_methods & ResolvedCollectionMethod.UserRights) != 0) - { + if ((_methods & CollectionMethod.UserRights) != 0) { await _context.DoDelay(); var userRights = _userRightsAssignmentProcessor.GetUserRightsAssignments( - resolvedSearchResult.DisplayName, resolvedSearchResult.ObjectId, - resolvedSearchResult.Domain, resolvedSearchResult.IsDomainController); + resolvedSearchResult.DisplayName, resolvedSearchResult.ObjectId, + resolvedSearchResult.Domain, resolvedSearchResult.IsDomainController); ret.UserRights = await userRights.ToArrayAsync(); } @@ -317,123 +305,100 @@ await compStatusChannel.Writer.WriteAsync(new CSVComputerStatus return ret; } - private Group ProcessGroupObject(ISearchResultEntry entry, - ResolvedSearchResult resolvedSearchResult) - { - var ret = new Group - { + private async Task ProcessGroupObject(IDirectoryObject entry, + ResolvedSearchResult resolvedSearchResult) { + var ret = new Group { ObjectIdentifier = resolvedSearchResult.ObjectId }; - ret.Properties.Add("domain", resolvedSearchResult.Domain); - ret.Properties.Add("name", resolvedSearchResult.DisplayName); - ret.Properties.Add("distinguishedname", entry.DistinguishedName.ToUpper()); - ret.Properties.Add("domainsid", resolvedSearchResult.DomainSid); + ret.Properties = new Dictionary(GetCommonProperties(entry, resolvedSearchResult)); ret.Properties.Add("samaccountname", entry.GetProperty(LDAPProperties.SAMAccountName)); - if ((_methods & ResolvedCollectionMethod.ACL) != 0) - { - ret.Aces = _aclProcessor.ProcessACL(resolvedSearchResult, entry).ToArray(); + if ((_methods & CollectionMethod.ACL) != 0) { + ret.Aces = await _aclProcessor.ProcessACL(resolvedSearchResult, entry).ToArrayAsync(); ret.IsACLProtected = _aclProcessor.IsACLProtected(entry); ret.Properties.Add("isaclprotected", ret.IsACLProtected); } - if ((_methods & ResolvedCollectionMethod.Group) != 0) - ret.Members = _groupProcessor + if ((_methods & CollectionMethod.Group) != 0) + ret.Members = await _groupProcessor .ReadGroupMembers(resolvedSearchResult, entry) - .ToArray(); + .ToArrayAsync(); - if ((_methods & ResolvedCollectionMethod.ObjectProps) != 0) - { - var groupProps = LDAPPropertyProcessor.ReadGroupProperties(entry); + if ((_methods & CollectionMethod.ObjectProps) != 0) { + var groupProps = LdapPropertyProcessor.ReadGroupProperties(entry); ret.Properties = ContextUtils.Merge(ret.Properties, groupProps); - if (_context.Flags.CollectAllProperties) - { + if (_context.Flags.CollectAllProperties) { ret.Properties = ContextUtils.Merge(_ldapPropertyProcessor.ParseAllProperties(entry), ret.Properties); } } - if ((_methods & ResolvedCollectionMethod.Container) != 0) - { - ret.ContainedBy = _containerProcessor.GetContainingObject(entry.DistinguishedName); + if ((_methods & CollectionMethod.Container) != 0) { + if (entry.TryGetDistinguishedName(out var dn) && + await _containerProcessor.GetContainingObject(dn) is (true, var container)) { + ret.ContainedBy = container; + } } return ret; } - private async Task ProcessDomainObject(ISearchResultEntry entry, - ResolvedSearchResult resolvedSearchResult) - { - var ret = new Domain - { + private async Task ProcessDomainObject(IDirectoryObject entry, + ResolvedSearchResult resolvedSearchResult) { + var ret = new Domain { ObjectIdentifier = resolvedSearchResult.ObjectId }; - ret.Properties.Add("domain", resolvedSearchResult.Domain); - ret.Properties.Add("name", resolvedSearchResult.DisplayName); - ret.Properties.Add("distinguishedname", entry.DistinguishedName.ToUpper()); - ret.Properties.Add("domainsid", resolvedSearchResult.DomainSid); + ret.Properties = new Dictionary(GetCommonProperties(entry, resolvedSearchResult)); - if ((_methods & ResolvedCollectionMethod.ACL) != 0) - { - ret.Aces = _aclProcessor.ProcessACL(resolvedSearchResult, entry).ToArray(); + if ((_methods & CollectionMethod.ACL) != 0) { + ret.Aces = await _aclProcessor.ProcessACL(resolvedSearchResult, entry).ToArrayAsync(); ret.IsACLProtected = _aclProcessor.IsACLProtected(entry); ret.Properties.Add("isaclprotected", ret.IsACLProtected); + ret.InheritanceHashes = _aclProcessor.GetInheritedAceHashes(entry, resolvedSearchResult).ToArray(); } - if ((_methods & ResolvedCollectionMethod.Trusts) != 0) - ret.Trusts = _domainTrustProcessor.EnumerateDomainTrusts(resolvedSearchResult.Domain).ToArray(); + if ((_methods & CollectionMethod.Trusts) != 0) + ret.Trusts = await _domainTrustProcessor.EnumerateDomainTrusts(resolvedSearchResult.Domain) + .ToArrayAsync(); - if ((_methods & ResolvedCollectionMethod.ObjectProps) != 0) - { - ret.Properties = ContextUtils.Merge(ret.Properties, LDAPPropertyProcessor.ReadDomainProperties(entry)); - if (_context.Flags.CollectAllProperties) - { + if ((_methods & CollectionMethod.ObjectProps) != 0) { + ret.Properties = ContextUtils.Merge(ret.Properties, LdapPropertyProcessor.ReadDomainProperties(entry)); + if (_context.Flags.CollectAllProperties) { ret.Properties = ContextUtils.Merge(_ldapPropertyProcessor.ParseAllProperties(entry), ret.Properties); } } - if ((_methods & ResolvedCollectionMethod.Container) != 0) - { - ret.Links = _containerProcessor.ReadContainerGPLinks(resolvedSearchResult, entry).ToArray(); + if ((_methods & CollectionMethod.Container) != 0) { + ret.Links = await _containerProcessor.ReadContainerGPLinks(resolvedSearchResult, entry).ToArrayAsync(); } - if ((_methods & ResolvedCollectionMethod.GPOLocalGroup) != 0) - { - var gplink = entry.GetProperty(LDAPProperties.GPLink); - ret.GPOChanges = await _gpoLocalGroupProcessor.ReadGPOLocalGroups(gplink, entry.DistinguishedName); + if ((_methods & CollectionMethod.GPOLocalGroup) != 0) { + ret.GPOChanges = await _gpoLocalGroupProcessor.ReadGPOLocalGroups(entry); } return ret; } - private GPO ProcessGPOObject(ISearchResultEntry entry, - ResolvedSearchResult resolvedSearchResult) - { - var ret = new GPO - { + private async Task ProcessGPOObject(IDirectoryObject entry, + ResolvedSearchResult resolvedSearchResult) { + var ret = new GPO { ObjectIdentifier = resolvedSearchResult.ObjectId }; - ret.Properties.Add("domain", resolvedSearchResult.Domain); - ret.Properties.Add("name", resolvedSearchResult.DisplayName); - ret.Properties.Add("distinguishedname", entry.DistinguishedName.ToUpper()); - ret.Properties.Add("domainsid", resolvedSearchResult.DomainSid); + ret.Properties = new Dictionary(GetCommonProperties(entry, resolvedSearchResult)); - if ((_methods & ResolvedCollectionMethod.ACL) != 0) - { - ret.Aces = _aclProcessor.ProcessACL(resolvedSearchResult, entry).ToArray(); + if ((_methods & CollectionMethod.ACL) != 0) { + ret.Aces = await _aclProcessor.ProcessACL(resolvedSearchResult, entry).ToArrayAsync(); ret.IsACLProtected = _aclProcessor.IsACLProtected(entry); ret.Properties.Add("isaclprotected", ret.IsACLProtected); } - if ((_methods & ResolvedCollectionMethod.ObjectProps) != 0) - { - ret.Properties = ContextUtils.Merge(ret.Properties, LDAPPropertyProcessor.ReadGPOProperties(entry)); - if (_context.Flags.CollectAllProperties) - { + if ((_methods & CollectionMethod.ObjectProps) != 0) { + ret.Properties = ContextUtils.Merge(ret.Properties, LdapPropertyProcessor.ReadGPOProperties(entry)); + if (_context.Flags.CollectAllProperties) { ret.Properties = ContextUtils.Merge(_ldapPropertyProcessor.ParseAllProperties(entry), ret.Properties); } @@ -442,223 +407,200 @@ private GPO ProcessGPOObject(ISearchResultEntry entry, return ret; } - private async Task ProcessOUObject(ISearchResultEntry entry, - ResolvedSearchResult resolvedSearchResult) - { - var ret = new OU - { + private async Task ProcessOUObject(IDirectoryObject entry, + ResolvedSearchResult resolvedSearchResult) { + var ret = new OU { ObjectIdentifier = resolvedSearchResult.ObjectId }; - ret.Properties.Add("domain", resolvedSearchResult.Domain); - ret.Properties.Add("name", resolvedSearchResult.DisplayName); - ret.Properties.Add("distinguishedname", entry.DistinguishedName.ToUpper()); - ret.Properties.Add("domainsid", resolvedSearchResult.DomainSid); + ret.Properties = new Dictionary(GetCommonProperties(entry, resolvedSearchResult)); - if ((_methods & ResolvedCollectionMethod.ACL) != 0) - { - ret.Aces = _aclProcessor.ProcessACL(resolvedSearchResult, entry).ToArray(); + if ((_methods & CollectionMethod.ACL) != 0) { + ret.Aces = await _aclProcessor.ProcessACL(resolvedSearchResult, entry).ToArrayAsync(); ret.IsACLProtected = _aclProcessor.IsACLProtected(entry); ret.Properties.Add("isaclprotected", ret.IsACLProtected); + ret.InheritanceHashes = _aclProcessor.GetInheritedAceHashes(entry, resolvedSearchResult).ToArray(); } - if ((_methods & ResolvedCollectionMethod.ObjectProps) != 0) - { - ret.Properties = ContextUtils.Merge(ret.Properties, LDAPPropertyProcessor.ReadOUProperties(entry)); - if (_context.Flags.CollectAllProperties) - { + if ((_methods & CollectionMethod.ObjectProps) != 0) { + ret.Properties = ContextUtils.Merge(ret.Properties, LdapPropertyProcessor.ReadOUProperties(entry)); + if (_context.Flags.CollectAllProperties) { ret.Properties = ContextUtils.Merge(_ldapPropertyProcessor.ParseAllProperties(entry), ret.Properties); } } - if ((_methods & ResolvedCollectionMethod.Container) != 0) - { - ret.ContainedBy = _containerProcessor.GetContainingObject(entry.DistinguishedName); + if ((_methods & CollectionMethod.Container) != 0) { + if (await _containerProcessor.GetContainingObject(entry) is (true, var container)) { + ret.ContainedBy = container; + } + ret.Properties.Add("blocksinheritance", - ContainerProcessor.ReadBlocksInheritance(entry.GetProperty("gpoptions"))); - ret.Links = _containerProcessor.ReadContainerGPLinks(resolvedSearchResult, entry).ToArray(); + ContainerProcessor.ReadBlocksInheritance(entry.GetProperty(LDAPProperties.GroupPolicyOptions))); + ret.Links = await _containerProcessor.ReadContainerGPLinks(resolvedSearchResult, entry).ToArrayAsync(); } - if ((_methods & ResolvedCollectionMethod.GPOLocalGroup) != 0) - { - var gplink = entry.GetProperty(LDAPProperties.GPLink); - ret.GPOChanges = await _gpoLocalGroupProcessor.ReadGPOLocalGroups(gplink, entry.DistinguishedName); + if ((_methods & CollectionMethod.GPOLocalGroup) != 0) { + ret.GPOChanges = await _gpoLocalGroupProcessor.ReadGPOLocalGroups(entry); } + return ret; } - private Container ProcessContainerObject(ISearchResultEntry entry, - ResolvedSearchResult resolvedSearchResult) - { - var ret = new Container - { + private async Task ProcessContainerObject(IDirectoryObject entry, + ResolvedSearchResult resolvedSearchResult) { + var ret = new Container { ObjectIdentifier = resolvedSearchResult.ObjectId }; - ret.Properties.Add("domain", resolvedSearchResult.Domain); - ret.Properties.Add("name", resolvedSearchResult.DisplayName); - ret.Properties.Add("distinguishedname", entry.DistinguishedName.ToUpper()); - ret.Properties.Add("domainsid", resolvedSearchResult.DomainSid); + ret.Properties = new Dictionary(GetCommonProperties(entry, resolvedSearchResult)); - if ((_methods & ResolvedCollectionMethod.Container) != 0 || (_methods & ResolvedCollectionMethod.CertServices) != 0) - ret.ContainedBy = _containerProcessor.GetContainingObject(entry.DistinguishedName); + if ((_methods & CollectionMethod.Container) != 0 || (_methods & CollectionMethod.CertServices) != 0) + if (await _containerProcessor.GetContainingObject(entry) is (true, var container)) { + ret.ContainedBy = container; + } - if ((_methods & ResolvedCollectionMethod.ACL) != 0 || (_methods & ResolvedCollectionMethod.CertServices) != 0) - { - ret.Aces = _aclProcessor.ProcessACL(resolvedSearchResult, entry) - .ToArray(); + if ((_methods & CollectionMethod.ACL) != 0 || (_methods & CollectionMethod.CertServices) != 0) { + ret.Aces = await _aclProcessor.ProcessACL(resolvedSearchResult, entry) + .ToArrayAsync(); ret.IsACLProtected = _aclProcessor.IsACLProtected(entry); ret.Properties.Add("isaclprotected", ret.IsACLProtected); + ret.InheritanceHashes = _aclProcessor.GetInheritedAceHashes(entry, resolvedSearchResult).ToArray(); } - if ((_methods & ResolvedCollectionMethod.ObjectProps) != 0 || (_methods & ResolvedCollectionMethod.CertServices) != 0) - { - if (_context.Flags.CollectAllProperties) - { + if ((_methods & CollectionMethod.ObjectProps) != 0 || (_methods & CollectionMethod.CertServices) != 0) { + ret.Properties = + ContextUtils.Merge(LdapPropertyProcessor.ReadContainerProperties(entry), ret.Properties); + if (_context.Flags.CollectAllProperties) { ret.Properties = ContextUtils.Merge(_ldapPropertyProcessor.ParseAllProperties(entry), ret.Properties); } - //ret.Properties = ContextUtils.Merge(ret.Properties, LDAPPropertyProcessor.) } return ret; } - private RootCA ProcessRootCA(ISearchResultEntry entry, ResolvedSearchResult resolvedSearchResult) - { - var ret = new RootCA - { + private async Task ProcessRootCA(IDirectoryObject entry, ResolvedSearchResult resolvedSearchResult) { + var ret = new RootCA { ObjectIdentifier = resolvedSearchResult.ObjectId, DomainSID = resolvedSearchResult.DomainSid }; - ret.Properties.Add("domain", resolvedSearchResult.Domain); - ret.Properties.Add("name", resolvedSearchResult.DisplayName); - ret.Properties.Add("distinguishedname", entry.DistinguishedName.ToUpper()); - ret.Properties.Add("domainsid", resolvedSearchResult.DomainSid); - + ret.Properties = new Dictionary(GetCommonProperties(entry, resolvedSearchResult)); - if ((_methods & ResolvedCollectionMethod.ACL) != 0 || (_methods & ResolvedCollectionMethod.CertServices) != 0) - { - ret.Aces = _aclProcessor.ProcessACL(resolvedSearchResult, entry).ToArray(); + + if ((_methods & CollectionMethod.ACL) != 0 || (_methods & CollectionMethod.CertServices) != 0) { + ret.Aces = await _aclProcessor.ProcessACL(resolvedSearchResult, entry).ToArrayAsync(); ret.IsACLProtected = _aclProcessor.IsACLProtected(entry); ret.Properties.Add("isaclprotected", ret.IsACLProtected); } - if ((_methods & ResolvedCollectionMethod.ObjectProps) != 0 || (_methods & ResolvedCollectionMethod.CertServices) != 0) - { - var props = LDAPPropertyProcessor.ReadRootCAProperties(entry); + if ((_methods & CollectionMethod.ObjectProps) != 0 || (_methods & CollectionMethod.CertServices) != 0) { + var props = LdapPropertyProcessor.ReadRootCAProperties(entry); ret.Properties.Merge(props); } - if ((_methods & ResolvedCollectionMethod.Container) != 0 || (_methods & ResolvedCollectionMethod.CertServices) != 0) - { - ret.ContainedBy = _containerProcessor.GetContainingObject(entry.DistinguishedName); + if ((_methods & CollectionMethod.Container) != 0 || (_methods & CollectionMethod.CertServices) != 0) { + if (await _containerProcessor.GetContainingObject(entry) is (true, var container)) { + ret.ContainedBy = container; + } } return ret; } - private AIACA ProcessAIACA(ISearchResultEntry entry, ResolvedSearchResult resolvedSearchResult) - { - var ret = new AIACA - { + private async Task ProcessAIACA(IDirectoryObject entry, ResolvedSearchResult resolvedSearchResult) { + var ret = new AIACA { ObjectIdentifier = resolvedSearchResult.ObjectId }; - ret.Properties.Add("domain", resolvedSearchResult.Domain); - ret.Properties.Add("name", resolvedSearchResult.DisplayName); - ret.Properties.Add("distinguishedname", entry.DistinguishedName.ToUpper()); - ret.Properties.Add("domainsid", resolvedSearchResult.DomainSid); + ret.Properties = new Dictionary(GetCommonProperties(entry, resolvedSearchResult)); - if ((_methods & ResolvedCollectionMethod.ACL) != 0 || (_methods & ResolvedCollectionMethod.CertServices) != 0) - { - ret.Aces = _aclProcessor.ProcessACL(resolvedSearchResult, entry).ToArray(); + if ((_methods & CollectionMethod.ACL) != 0 || (_methods & CollectionMethod.CertServices) != 0) { + ret.Aces = await _aclProcessor.ProcessACL(resolvedSearchResult, entry).ToArrayAsync(); ret.IsACLProtected = _aclProcessor.IsACLProtected(entry); ret.Properties.Add("isaclprotected", ret.IsACLProtected); } - if ((_methods & ResolvedCollectionMethod.ObjectProps) != 0 || (_methods & ResolvedCollectionMethod.CertServices) != 0) - { - var props = LDAPPropertyProcessor.ReadAIACAProperties(entry); + if ((_methods & CollectionMethod.ObjectProps) != 0 || (_methods & CollectionMethod.CertServices) != 0) { + var props = LdapPropertyProcessor.ReadAIACAProperties(entry); ret.Properties.Merge(props); } - if ((_methods & ResolvedCollectionMethod.Container) != 0 || (_methods & ResolvedCollectionMethod.CertServices) != 0) - { - ret.ContainedBy = _containerProcessor.GetContainingObject(entry.DistinguishedName); + if ((_methods & CollectionMethod.Container) != 0 || (_methods & CollectionMethod.CertServices) != 0) { + if (await _containerProcessor.GetContainingObject(entry) is (true, var container)) { + ret.ContainedBy = container; + } } return ret; } - private async Task ProcessEnterpriseCA(ISearchResultEntry entry, ResolvedSearchResult resolvedSearchResult) - { - var ret = new EnterpriseCA - { + private async Task ProcessEnterpriseCA(IDirectoryObject entry, + ResolvedSearchResult resolvedSearchResult) { + var ret = new EnterpriseCA { ObjectIdentifier = resolvedSearchResult.ObjectId, }; - ret.Properties.Add("domain", resolvedSearchResult.Domain); - ret.Properties.Add("name", resolvedSearchResult.DisplayName); - ret.Properties.Add("distinguishedname", entry.DistinguishedName.ToUpper()); - ret.Properties.Add("domainsid", resolvedSearchResult.DomainSid); + ret.Properties = new Dictionary(GetCommonProperties(entry, resolvedSearchResult)); - if ((_methods & ResolvedCollectionMethod.ACL) != 0 || (_methods & ResolvedCollectionMethod.CertServices) != 0) - { - ret.Aces = _aclProcessor.ProcessACL(resolvedSearchResult, entry).ToArray(); + if ((_methods & CollectionMethod.ACL) != 0 || (_methods & CollectionMethod.CertServices) != 0) { + ret.Aces = await _aclProcessor.ProcessACL(resolvedSearchResult, entry).ToArrayAsync(); ret.IsACLProtected = _aclProcessor.IsACLProtected(entry); ret.Properties.Add("isaclprotected", ret.IsACLProtected); } - if ((_methods & ResolvedCollectionMethod.ObjectProps) != 0 || (_methods & ResolvedCollectionMethod.CertServices) != 0) - { - var props = LDAPPropertyProcessor.ReadEnterpriseCAProperties(entry); + if ((_methods & CollectionMethod.ObjectProps) != 0 || (_methods & CollectionMethod.CertServices) != 0) { + var props = LdapPropertyProcessor.ReadEnterpriseCAProperties(entry); ret.Properties.Merge(props); - // Enabled/published cert templates - (IEnumerable resolvedTemplates, IEnumerable unresolvedTemplates) = _certAbuseProcessor.ProcessCertTemplates(entry.GetArrayProperty(LDAPProperties.CertificateTemplates), resolvedSearchResult.Domain); - ret.EnabledCertTemplates = resolvedTemplates.ToArray(); - ret.Properties.Add("unresolvedpublishedtemplates", unresolvedTemplates.ToArray()); + // Enabled cert templates + if (entry.TryGetArrayProperty(LDAPProperties.CertificateTemplates, out var rawTemplates)) { + var (resolvedTemplates, unresolvedTemplates) = await _certAbuseProcessor.ProcessCertTemplates( + rawTemplates, resolvedSearchResult.Domain); + ret.EnabledCertTemplates = resolvedTemplates.ToArray(); + ret.Properties.Add("unresolvedpublishedtemplates", unresolvedTemplates.ToArray()); + } } - if ((_methods & ResolvedCollectionMethod.Container) != 0 || (_methods & ResolvedCollectionMethod.CertServices) != 0) - { - ret.ContainedBy = _containerProcessor.GetContainingObject(entry.DistinguishedName); + if ((_methods & CollectionMethod.Container) != 0 || (_methods & CollectionMethod.CertServices) != 0) { + if (await _containerProcessor.GetContainingObject(entry) is (true, var container)) { + ret.ContainedBy = container; + } } - if ((_methods & ResolvedCollectionMethod.CARegistry) != 0) - { - // Collect properties from CA server registry + if ((_methods & CollectionMethod.CARegistry) != 0) { + // Collect properties from CA server registry var cASecurityCollected = false; var enrollmentAgentRestrictionsCollected = false; var isUserSpecifiesSanEnabledCollected = false; var roleSeparationEnabledCollected = false; var caName = entry.GetProperty(LDAPProperties.Name); var dnsHostName = entry.GetProperty(LDAPProperties.DNSHostName); - if ((_methods & ResolvedCollectionMethod.CARegistry) != 0 && caName != null && dnsHostName != null) - { - ret.HostingComputer = await _context.LDAPUtils.ResolveHostToSid(dnsHostName, resolvedSearchResult.Domain); + if (caName != null && dnsHostName != null) { + _log.LogWarning("CA {Name} host ({Dns}) could not be resolved to a SID.", caName, dnsHostName); + if (await _context.LDAPUtils.ResolveHostToSid(dnsHostName, resolvedSearchResult.DomainSid) is + (true, var sid) && sid.StartsWith("S-1-")) { + ret.HostingComputer = sid; + } // If ResolveHostToSid does not return a valid SID, we don't want to record this host - if (ret.HostingComputer != null && !ret.HostingComputer.StartsWith("S-1-")) - { - _log.LogWarning("CA host could not be resolved to a SID.", dnsHostName, resolvedSearchResult.Domain); + if (ret.HostingComputer != null && !ret.HostingComputer.StartsWith("S-1-")) { ret.HostingComputer = null; } - CARegistryData cARegistryData = new() - { + CARegistryData cARegistryData = new() { IsUserSpecifiesSanEnabled = _certAbuseProcessor.IsUserSpecifiesSanEnabled(dnsHostName, caName), + EnrollmentAgentRestrictions = await _certAbuseProcessor.ProcessEAPermissions(caName, + resolvedSearchResult.Domain, dnsHostName, ret.HostingComputer), RoleSeparationEnabled = _certAbuseProcessor.RoleSeparationEnabled(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) + CASecurity = await _certAbuseProcessor.ProcessRegistryEnrollmentPermissions(caName, + resolvedSearchResult.Domain, dnsHostName, ret.HostingComputer) }; cASecurityCollected = cARegistryData.CASecurity.Collected; @@ -673,117 +615,101 @@ private async Task ProcessEnterpriseCA(ISearchResultEntry entry, R ret.Properties.Add("isuserspecifiessanenabledcollected", isUserSpecifiesSanEnabledCollected); ret.Properties.Add("roleseparationenabledcollected", roleSeparationEnabledCollected); } - + return ret; } - private NTAuthStore ProcessNTAuthStore(ISearchResultEntry entry, ResolvedSearchResult resolvedSearchResult) - { - var ret = new NTAuthStore - { + private async Task ProcessNTAuthStore(IDirectoryObject entry, + ResolvedSearchResult resolvedSearchResult) { + var ret = new NTAuthStore { ObjectIdentifier = resolvedSearchResult.ObjectId, DomainSID = resolvedSearchResult.DomainSid }; - ret.Properties.Add("domain", resolvedSearchResult.Domain); - ret.Properties.Add("name", resolvedSearchResult.DisplayName); - ret.Properties.Add("distinguishedname", entry.DistinguishedName.ToUpper()); - ret.Properties.Add("domainsid", resolvedSearchResult.DomainSid); + ret.Properties = new Dictionary(GetCommonProperties(entry, resolvedSearchResult)); - if ((_methods & ResolvedCollectionMethod.ACL) != 0 || (_methods & ResolvedCollectionMethod.CertServices) != 0) - { - ret.Aces = _aclProcessor.ProcessACL(resolvedSearchResult, entry).ToArray(); + if ((_methods & CollectionMethod.ACL) != 0 || (_methods & CollectionMethod.CertServices) != 0) { + ret.Aces = await _aclProcessor.ProcessACL(resolvedSearchResult, entry).ToArrayAsync(); ret.IsACLProtected = _aclProcessor.IsACLProtected(entry); ret.Properties.Add("isaclprotected", ret.IsACLProtected); } - if ((_methods & ResolvedCollectionMethod.ObjectProps) != 0 || (_methods & ResolvedCollectionMethod.CertServices) != 0) - { - var props = LDAPPropertyProcessor.ReadNTAuthStoreProperties(entry); + if ((_methods & CollectionMethod.ObjectProps) != 0 || (_methods & CollectionMethod.CertServices) != 0) { + var props = LdapPropertyProcessor.ReadNTAuthStoreProperties(entry); - // Cert thumbprints - var rawCertificates = entry.GetByteArrayProperty(LDAPProperties.CACertificate); - var certificates = from rawCertificate in rawCertificates - select new X509Certificate2(rawCertificate).Thumbprint; - ret.Properties.Add("certthumbprints", certificates.ToArray()); + if (entry.TryGetByteArrayProperty(LDAPProperties.CACertificate, out var rawCertificates)) { + var certificates = from rawCertificate in rawCertificates + select new X509Certificate2(rawCertificate).Thumbprint; + ret.Properties.Add("certthumbprints", certificates.ToArray()); + } ret.Properties.Merge(props); } - if ((_methods & ResolvedCollectionMethod.Container) != 0 || (_methods & ResolvedCollectionMethod.CertServices) != 0) - { - ret.ContainedBy = _containerProcessor.GetContainingObject(entry.DistinguishedName); + if ((_methods & CollectionMethod.Container) != 0 || (_methods & CollectionMethod.CertServices) != 0) { + if (await _containerProcessor.GetContainingObject(entry) is (true, var container)) { + ret.ContainedBy = container; + } } return ret; } - private CertTemplate ProcessCertTemplate(ISearchResultEntry entry, ResolvedSearchResult resolvedSearchResult) - { - var ret = new CertTemplate - { + private async Task ProcessCertTemplate(IDirectoryObject entry, + ResolvedSearchResult resolvedSearchResult) { + var ret = new CertTemplate { ObjectIdentifier = resolvedSearchResult.ObjectId }; - ret.Properties.Add("domain", resolvedSearchResult.Domain); - ret.Properties.Add("name", resolvedSearchResult.DisplayName); - ret.Properties.Add("distinguishedname", entry.DistinguishedName.ToUpper()); - ret.Properties.Add("domainsid", resolvedSearchResult.DomainSid); + ret.Properties = new Dictionary(GetCommonProperties(entry, resolvedSearchResult)); - if ((_methods & ResolvedCollectionMethod.ACL) != 0 || (_methods & ResolvedCollectionMethod.CertServices) != 0) - { - ret.Aces = _aclProcessor.ProcessACL(resolvedSearchResult, entry).ToArray(); + if ((_methods & CollectionMethod.ACL) != 0 || (_methods & CollectionMethod.CertServices) != 0) { + ret.Aces = await _aclProcessor.ProcessACL(resolvedSearchResult, entry).ToArrayAsync(); ret.IsACLProtected = _aclProcessor.IsACLProtected(entry); ret.Properties.Add("isaclprotected", ret.IsACLProtected); } - if ((_methods & ResolvedCollectionMethod.ObjectProps) != 0 || (_methods & ResolvedCollectionMethod.CertServices) != 0) - { - var certTemplatesProps = LDAPPropertyProcessor.ReadCertTemplateProperties(entry); + if ((_methods & CollectionMethod.ObjectProps) != 0 || (_methods & CollectionMethod.CertServices) != 0) { + var certTemplatesProps = LdapPropertyProcessor.ReadCertTemplateProperties(entry); ret.Properties.Merge(certTemplatesProps); } - if ((_methods & ResolvedCollectionMethod.Container) != 0 || (_methods & ResolvedCollectionMethod.CertServices) != 0) - { - ret.ContainedBy = _containerProcessor.GetContainingObject(entry.DistinguishedName); + if ((_methods & CollectionMethod.Container) != 0 || (_methods & CollectionMethod.CertServices) != 0) { + if (await _containerProcessor.GetContainingObject(entry) is (true, var container)) { + ret.ContainedBy = container; + } } return ret; } - private IssuancePolicy ProcessIssuancePolicy(ISearchResultEntry entry, - ResolvedSearchResult resolvedSearchResult) - { - var ret = new IssuancePolicy - { + private async Task ProcessIssuancePolicy(IDirectoryObject entry, + ResolvedSearchResult resolvedSearchResult) { + var ret = new IssuancePolicy { ObjectIdentifier = resolvedSearchResult.ObjectId }; - - ret.Properties.Add("domain", resolvedSearchResult.Domain); - ret.Properties.Add("name", resolvedSearchResult.DisplayName); - ret.Properties.Add("distinguishedname", entry.DistinguishedName.ToUpper()); - ret.Properties.Add("domainsid", resolvedSearchResult.DomainSid); - - if ((_methods & ResolvedCollectionMethod.ACL) != 0 || (_methods & ResolvedCollectionMethod.CertServices) != 0) - { - ret.Aces = _aclProcessor.ProcessACL(resolvedSearchResult, entry).ToArray(); + + ret.Properties = new Dictionary(GetCommonProperties(entry, resolvedSearchResult)); + + if ((_methods & CollectionMethod.ACL) != 0 || (_methods & CollectionMethod.CertServices) != 0) { + ret.Aces = await _aclProcessor.ProcessACL(resolvedSearchResult, entry).ToArrayAsync(); ret.IsACLProtected = _aclProcessor.IsACLProtected(entry); ret.Properties.Add("isaclprotected", ret.IsACLProtected); } - - if ((_methods & ResolvedCollectionMethod.ObjectProps) != 0 || (_methods & ResolvedCollectionMethod.CertServices) != 0) - { - var issuancePolicyProps = _ldapPropertyProcessor.ReadIssuancePolicyProperties(entry); + + if ((_methods & CollectionMethod.ObjectProps) != 0 || (_methods & CollectionMethod.CertServices) != 0) { + var issuancePolicyProps = await _ldapPropertyProcessor.ReadIssuancePolicyProperties(entry); ret.Properties.Merge(issuancePolicyProps.Props); ret.GroupLink = issuancePolicyProps.GroupLink; } - - if ((_methods & ResolvedCollectionMethod.Container) != 0 || (_methods & ResolvedCollectionMethod.CertServices) != 0) - { - ret.ContainedBy = _containerProcessor.GetContainingObject(entry.DistinguishedName); + + if ((_methods & CollectionMethod.Container) != 0 || (_methods & CollectionMethod.CertServices) != 0) { + if (await _containerProcessor.GetContainingObject(entry) is (true, var container)) { + ret.ContainedBy = container; + } } return ret; } } -} +} \ No newline at end of file diff --git a/src/Sharphound.cs b/src/Sharphound.cs index 920c73a..db87f3f 100644 --- a/src/Sharphound.cs +++ b/src/Sharphound.cs @@ -32,62 +32,51 @@ using SharpHoundCommonLib.Processors; using Timer = System.Timers.Timer; -namespace Sharphound -{ +namespace Sharphound { #region Reference Implementations - internal class BasicLogger : ILogger - { + internal class BasicLogger : ILogger { private readonly int _verbosity; - public BasicLogger(int verbosity) - { + public BasicLogger(int verbosity) { _verbosity = verbosity; } public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, - Func formatter) - { + Func formatter) { WriteLevel(logLevel, state.ToString(), exception); } - public bool IsEnabled(LogLevel logLevel) - { + public bool IsEnabled(LogLevel logLevel) { return (int)logLevel >= _verbosity; } - public IDisposable BeginScope(TState state) - { + public IDisposable BeginScope(TState state) { return null; } - private void WriteLevel(LogLevel level, string message, Exception e = null) - { + private void WriteLevel(LogLevel level, string message, Exception e = null) { if (IsEnabled(level)) Console.WriteLine(FormatLog(level, message, e)); } - private static string FormatLog(LogLevel level, string message, Exception e) - { + private static string FormatLog(LogLevel level, string message, Exception e) { var time = DateTime.Now; return $"{time:O}|{level.ToString().ToUpper()}|{message}{(e != null ? $"\n{e}" : "")}"; } } - internal class SharpLinks : Links - { + internal class SharpLinks : Links { /// /// Init and check defaults /// /// /// /// - public IContext Initialize(IContext context, LDAPConfig options) - { + public IContext Initialize(IContext context, LdapConfig options) { context.Logger.LogTrace("Entering initialize link"); - JsonConvert.DefaultSettings = () => new JsonSerializerSettings - { - Converters = new List {new KindConvertor()} + JsonConvert.DefaultSettings = () => new JsonSerializerSettings { + Converters = new List { new KindConvertor() } }; CommonLib.ReconfigureLogging(context.Logger); //We've successfully parsed arguments, lets do some options post-processing. @@ -98,18 +87,30 @@ public IContext Initialize(IContext context, LDAPConfig options) // Check to make sure both LDAP options are set if either is set if (options.Password != null && options.Username == null || - options.Username != null && options.Password == null) - { + options.Username != null && options.Password == null) { context.Logger.LogTrace("You must specify both LdapUsername and LdapPassword if using these options!"); context.Flags.IsFaulted = true; return context; } - + + if (string.IsNullOrWhiteSpace(context.DomainName)) { + if (!context.LDAPUtils.GetDomain(out var d)) { + context.Logger.LogCritical("unable to get current domain"); + context.Flags.IsFaulted = true; + } else { + context.DomainName = d.Name; + context.Logger.LogInformation("Resolved current domain to {Domain}", d.Name); + } + } + //Check some loop options - if (!context.Flags.Loop) return context; + if (!context.Flags.Loop) { + context.Logger.LogTrace("Exiting initialize link"); + return context; + } + //If loop is set, ensure we actually set options properly - if (context.LoopDuration == TimeSpan.Zero) - { + if (context.LoopDuration == TimeSpan.Zero) { context.Logger.LogTrace("Loop specified without a duration. Defaulting to 2 hours!"); context.LoopDuration = TimeSpan.FromHours(2); } @@ -117,39 +118,31 @@ public IContext Initialize(IContext context, LDAPConfig options) if (context.LoopInterval == TimeSpan.Zero) context.LoopInterval = TimeSpan.FromSeconds(30); - if (!context.Flags.NoOutput) - { + if (!context.Flags.NoOutput) { var filename = context.ResolveFileName(Path.GetRandomFileName(), "", false); - try - { - using (File.Create(filename)) - { + try { + using (File.Create(filename)) { } - + File.Delete(filename); - } - catch (Exception e) - { + } catch (Exception e) { context.Logger.LogCritical(e, "unable to write to target directory"); context.Flags.IsFaulted = true; } } - + + context.Logger.LogTrace("Exiting initialize link"); return context; } - public IContext TestConnection(IContext context) - { - context.Logger.LogTrace("Entering TestConnection link"); + public async Task TestConnection(IContext context) { + context.Logger.LogTrace("Entering TestConnection link, testing domain {Domain}", context.DomainName); //2. TestConnection() // Initial LDAP connection test. Search for the well known administrator SID to make sure we can connect successfully. - var result = context.LDAPUtils.TestLDAPConfig(context.DomainName); - - if (!result) - { - context.Logger.LogError("Unable to connect to LDAP, verify your credentials"); + if (await context.LDAPUtils.TestLdapConnection(context.DomainName) is (false, var message)) { + context.Logger.LogError("Unable to connect to LDAP: {Message}", message); context.Flags.IsFaulted = true; } @@ -157,14 +150,13 @@ public IContext TestConnection(IContext context) context.Flags.NeedsCancellation = false; context.Timer = null; context.LoopEnd = DateTime.Now; - + context.Logger.LogTrace("Exiting TestConnection link"); return context; } - public IContext SetSessionUserName(string overrideUserName, IContext context) - { + public IContext SetSessionUserName(string overrideUserName, IContext context) { context.Logger.LogTrace("Entering SetSessionUserName"); //3. SetSessionUserName() // Set the current user name for session collection. @@ -174,29 +166,23 @@ public IContext SetSessionUserName(string overrideUserName, IContext context) return context; } - public IContext InitCommonLib(IContext context) - { + public IContext InitCommonLib(IContext context) { context.Logger.LogTrace("Entering InitCommonLib"); //4. Create our Cache/Initialize Common Lib context.Logger.LogTrace("Getting cache path"); var path = context.GetCachePath(); context.Logger.LogTrace("Cache Path: {Path}", path); Cache cache; - if (!File.Exists(path)) - { + if (!File.Exists(path)) { context.Logger.LogTrace("Cache file does not exist"); cache = null; - } - else - try - { + } else + try { context.Logger.LogTrace("Loading cache from disk"); var json = File.ReadAllText(path); cache = JsonConvert.DeserializeObject(json, CacheContractResolver.Settings); context.Logger.LogInformation("Loaded cache with stats: {stats}", cache?.GetCacheStats()); - } - catch (Exception e) - { + } catch (Exception e) { context.Logger.LogError("Error loading cache: {exception}, creating new", e); cache = null; } @@ -206,80 +192,114 @@ public IContext InitCommonLib(IContext context) return context; } - public IContext GetDomainsForEnumeration(IContext context) - { + public async Task GetDomainsForEnumeration(IContext context) { context.Logger.LogTrace("Entering GetDomainsForEnumeration"); if (context.Flags.RecurseDomains) { - context.Logger.LogInformation("[RecurseDomains] Cross-domain enumeration may result in reduced data quality"); - context.Domains = BuildRecursiveDomainList(context).ToArray(); - } else if (context.Flags.SearchForest) { - context.Logger.LogInformation("[SearchForest] Cross-domain enumeration may result in reduced data quality"); - var forest = context.LDAPUtils.GetForest(context.DomainName); - if (forest == null) - { - context.Logger.LogError("Unable to contact forest to get domains for SearchForest"); + context.Logger.LogInformation( + "[RecurseDomains] Cross-domain enumeration may result in reduced data quality"); + context.Domains = await BuildRecursiveDomainList(context).ToArrayAsync(); + return context; + } + + if (context.Flags.SearchForest) { + context.Logger.LogInformation( + "[SearchForest] Cross-domain enumeration may result in reduced data quality"); + if (!context.LDAPUtils.GetDomain(context.DomainName, out var dObj)) { + context.Logger.LogError("Unable to get domain object for SearchForest"); + context.Flags.IsFaulted = true; + return context; + } + + Forest forest; + try { + forest = dObj.Forest; + } catch (Exception e) { + context.Logger.LogError("Unable to get forest object for SearchForest: {Message}", e.Message); context.Flags.IsFaulted = true; return context; } - context.Domains = (from Domain d in forest.Domains select new EnumerationDomain() - { - Name = d.Name, - DomainSid = d.GetDirectoryEntry().GetSid(), - }).ToArray(); - context.Logger.LogInformation("Domains for enumeration: {Domains}", JsonConvert.SerializeObject(context.Domains)); + var temp = new List(); + foreach (Domain d in forest.Domains) { + var entry = d.GetDirectoryEntry().ToDirectoryObject(); + if (!entry.TryGetSecurityIdentifier(out var domainSid)) { + continue; + } + + temp.Add(new EnumerationDomain() { + Name = d.Name, + DomainSid = domainSid + }); + } + + context.Domains = temp.ToArray(); + context.Logger.LogInformation("Domains for enumeration: {Domains}", + JsonConvert.SerializeObject(context.Domains)); + return context; + } + + if (!context.LDAPUtils.GetDomain(context.DomainName, out var domainObject)) { + context.Logger.LogError("Unable to resolve a domain to use, manually specify one or check spelling"); + context.Flags.IsFaulted = true; return context; } - var domainObject = context.LDAPUtils.GetDomain(context.DomainName); var domain = domainObject?.Name ?? context.DomainName; - if (domain == null) - { + if (domain == null) { context.Logger.LogError("Unable to resolve a domain to use, manually specify one or check spelling"); context.Flags.IsFaulted = true; + return context; } - - context.Domains = new[] { new EnumerationDomain - { - Name = domain, - DomainSid = domainObject?.GetDirectoryEntry().GetSid() ?? "Unknown" - } - }; + + if (domainObject != null && domainObject.GetDirectoryEntry().ToDirectoryObject() + .TryGetSecurityIdentifier(out var sid)) { + context.Domains = new[] { + new EnumerationDomain { + Name = domain, + DomainSid = sid + } + }; + } else { + context.Domains = new[] { + new EnumerationDomain { + Name = domain, + DomainSid = "Unknown" + } + }; + } + context.Logger.LogTrace("Exiting GetDomainsForEnumeration"); return context; } - - private IEnumerable BuildRecursiveDomainList(IContext context) - { + + private async IAsyncEnumerable BuildRecursiveDomainList(IContext context) { var domainResults = new List(); var enumeratedDomains = new HashSet(); var enumerationQueue = new Queue<(string domainSid, string domainName)>(); var utils = context.LDAPUtils; var log = context.Logger; - var domain = utils.GetDomain(); - if (domain == null) + if (!utils.GetDomain(out var domain)) { yield break; + } var trustHelper = new DomainTrustProcessor(utils); - var dSid = domain.GetDirectoryEntry().GetSid(); + var dSidSuccess = domain.GetDirectoryEntry().ToDirectoryObject().TryGetSecurityIdentifier(out var dSid); + var dName = domain.Name; enumerationQueue.Enqueue((dSid, dName)); - domainResults.Add(new EnumerationDomain - { + domainResults.Add(new EnumerationDomain { Name = dName.ToUpper(), DomainSid = dSid.ToUpper() }); - while (enumerationQueue.Count > 0) - { + while (enumerationQueue.Count > 0) { var (domainSid, domainName) = enumerationQueue.Dequeue(); enumeratedDomains.Add(domainSid.ToUpper()); - foreach (var trust in trustHelper.EnumerateDomainTrusts(domainName)) - { - log.LogDebug("Got trusted domain {Name} with sid {Sid} and {Type}", trust.TargetDomainName.ToUpper(), + await foreach (var trust in trustHelper.EnumerateDomainTrusts(domainName)) { + log.LogDebug("Got trusted domain {Name} with sid {Sid} and {Type}", + trust.TargetDomainName.ToUpper(), trust.TargetDomainSid.ToUpper(), trust.TrustType.ToString()); - domainResults.Add(new EnumerationDomain - { + domainResults.Add(new EnumerationDomain { Name = trust.TargetDomainName.ToUpper(), DomainSid = trust.TargetDomainSid.ToUpper() }); @@ -293,8 +313,7 @@ private IEnumerable BuildRecursiveDomainList(IContext context yield return domainResult; } - public IContext StartBaseCollectionTask(IContext context) - { + public IContext StartBaseCollectionTask(IContext context) { context.Logger.LogTrace("Entering StartBaseCollectionTask"); context.Logger.LogInformation("Flags: {flags}", context.ResolvedCollectionMethods.GetIndividualFlags()); //5. Start the collection @@ -304,28 +323,24 @@ public IContext StartBaseCollectionTask(IContext context) return context; } - public async Task AwaitBaseRunCompletion(IContext context) - { + public async Task AwaitBaseRunCompletion(IContext context) { // 6. Wait for the collection to complete await context.CollectionTask; return context; } - public async Task AwaitLoopCompletion(IContext context) - { + public async Task AwaitLoopCompletion(IContext context) { await context.CollectionTask; return context; } - public IContext DisposeTimer(IContext context) - { + public IContext DisposeTimer(IContext context) { //14. Dispose the context. context.Timer?.Dispose(); return context; } - public IContext Finish(IContext context) - { + public IContext Finish(IContext context) { ////16. And we're done! var currTime = DateTime.Now; context.Logger.LogInformation( @@ -334,8 +349,7 @@ public IContext Finish(IContext context) return context; } - public IContext SaveCacheFile(IContext context) - { + public IContext SaveCacheFile(IContext context) { if (context.Flags.MemCache) return context; // 15. Program exit started. Save the cache file @@ -347,13 +361,13 @@ public IContext SaveCacheFile(IContext context) stream.Write(serialized); return context; } - - public IContext StartLoop(IContext context) - { + + public IContext StartLoop(IContext context) { if (!context.Flags.Loop || context.CancellationTokenSource.IsCancellationRequested) return context; context.ResolvedCollectionMethods = context.ResolvedCollectionMethods.GetLoopCollectionMethods(); - context.Logger.LogInformation("Creating loop manager with methods {Methods}", context.ResolvedCollectionMethods); + context.Logger.LogInformation("Creating loop manager with methods {Methods}", + context.ResolvedCollectionMethods); var manager = new LoopManager(context); context.Logger.LogInformation("Starting looping"); context.CollectionTask = manager.StartLooping(); @@ -361,15 +375,13 @@ public IContext StartLoop(IContext context) return context; } - public IContext StartLoopTimer(IContext context) - { + public IContext StartLoopTimer(IContext context) { //If loop is set, set up our timer for the loop now if (!context.Flags.Loop || context.CancellationTokenSource.IsCancellationRequested) return context; context.LoopEnd = context.LoopEnd.AddMilliseconds(context.LoopDuration.TotalMilliseconds); context.Timer = new Timer(); - context.Timer.Elapsed += (_, _) => - { + context.Timer.Elapsed += (_, _) => { if (context.Flags.InitialCompleted) context.CancellationTokenSource.Cancel(); else @@ -387,30 +399,24 @@ public IContext StartLoopTimer(IContext context) #region Console Entrypoint - public class Program - { - public static async Task Main(string[] args) - { + public class Program { + public static async Task Main(string[] args) { var logger = new BasicLogger((int)LogLevel.Information); logger.LogInformation("This version of SharpHound is compatible with the 5.0.0 Release of BloodHound"); - try - { - var parser = new Parser(with => - { + try { + var parser = new Parser(with => { with.CaseInsensitiveEnumValues = true; with.CaseSensitive = false; with.HelpWriter = Console.Error; }); var options = parser.ParseArguments(args); - await options.WithParsedAsync(async options => - { + await options.WithParsedAsync(async options => { if (!options.ResolveCollectionMethods(logger, out var resolved, out var dconly)) return; logger = new BasicLogger(options.Verbosity); - var flags = new Flags - { + var flags = new Flags { Loop = options.Loop, DumpComputerStatus = options.TrackComputerCalls, NoRegistryLoggedOn = options.SkipRegistryLoggedOn, @@ -418,7 +424,7 @@ await options.WithParsedAsync(async options => SkipPortScan = options.SkipPortCheck, SkipPasswordAgeCheck = options.SkipPasswordCheck, DisableKerberosSigning = options.DisableSigning, - SecureLDAP = options.SecureLDAP, + SecureLDAP = options.ForceSecureLDAP, InvalidateCache = options.RebuildCache, NoZip = options.NoZip, NoOutput = false, @@ -430,24 +436,23 @@ await options.WithParsedAsync(async options => PrettyPrint = options.PrettyPrint, SearchForest = options.SearchForest, RecurseDomains = options.RecurseDomains, - DoLocalAdminSessionEnum = options.DoLocalAdminSessionEnum + DoLocalAdminSessionEnum = options.DoLocalAdminSessionEnum, + ParititonLdapQueries = options.PartitionLdapQueries }; - var ldapOptions = new LDAPConfig - { + var ldapOptions = new LdapConfig { Port = options.LDAPPort, + SSLPort = options.LDAPSSLPort, DisableSigning = options.DisableSigning, - SSL = options.SecureLDAP, + ForceSSL = options.ForceSecureLDAP, AuthType = AuthType.Negotiate, DisableCertVerification = options.DisableCertVerification }; if (options.DomainController != null) ldapOptions.Server = options.DomainController; - if (options.LDAPUsername != null) - { - if (options.LDAPPassword == null) - { + if (options.LDAPUsername != null) { + if (options.LDAPPassword == null) { logger.LogError("You must specify LDAPPassword if using the LDAPUsername options"); return; } @@ -455,40 +460,37 @@ await options.WithParsedAsync(async options => ldapOptions.Username = options.LDAPUsername; ldapOptions.Password = options.LDAPPassword; } - + // Check to make sure both Local Admin Session Enum options are set if either is set if (options.LocalAdminPassword != null && options.LocalAdminUsername == null || - options.LocalAdminUsername != null && options.LocalAdminPassword == null) - { - logger.LogError("You must specify both LocalAdminUsername and LocalAdminPassword if using these options!"); + options.LocalAdminUsername != null && options.LocalAdminPassword == null) { + logger.LogError( + "You must specify both LocalAdminUsername and LocalAdminPassword if using these options!"); return; } // Check to make sure doLocalAdminSessionEnum is set when specifying localadmin and password - if (options.LocalAdminPassword != null || options.LocalAdminUsername != null) - { - if (options.DoLocalAdminSessionEnum == false) - { - logger.LogError("You must use the --doLocalAdminSessionEnum switch in combination with --LocalAdminUsername and --LocalAdminPassword!"); + if (options.LocalAdminPassword != null || options.LocalAdminUsername != null) { + if (options.DoLocalAdminSessionEnum == false) { + logger.LogError( + "You must use the --doLocalAdminSessionEnum switch in combination with --LocalAdminUsername and --LocalAdminPassword!"); return; } } // Check to make sure LocalAdminUsername and LocalAdminPassword are set when using doLocalAdminSessionEnum - if (options.DoLocalAdminSessionEnum == true) - { - if (options.LocalAdminPassword == null || options.LocalAdminUsername == null) - { - logger.LogError("You must specify both LocalAdminUsername and LocalAdminPassword if using the --doLocalAdminSessionEnum option!"); + if (options.DoLocalAdminSessionEnum == true) { + if (options.LocalAdminPassword == null || options.LocalAdminUsername == null) { + logger.LogError( + "You must specify both LocalAdminUsername and LocalAdminPassword if using the --doLocalAdminSessionEnum option!"); return; } } - - IContext context = new BaseContext(logger, ldapOptions, flags) - { + + IContext context = new BaseContext(logger, ldapOptions, flags) { DomainName = options.Domain, CacheFileName = options.CacheName, ZipFilename = options.ZipFilename, @@ -510,7 +512,6 @@ await options.WithParsedAsync(async options => IsFaulted = false, LocalAdminUsername = options.LocalAdminUsername, LocalAdminPassword = options.LocalAdminPassword - }; var cancellationTokenSource = new CancellationTokenSource(); @@ -529,12 +530,12 @@ await options.WithParsedAsync(async options => context = links.Initialize(context, ldapOptions); if (context.Flags.IsFaulted) return; - context = links.TestConnection(context); + context = await links.TestConnection(context); if (context.Flags.IsFaulted) return; context = links.SetSessionUserName(options.OverrideUserName, context); context = links.InitCommonLib(context); - context = links.GetDomainsForEnumeration(context); + context = await links.GetDomainsForEnumeration(context); if (context.Flags.IsFaulted) return; context = links.StartBaseCollectionTask(context); @@ -545,19 +546,16 @@ await options.WithParsedAsync(async options => context = links.SaveCacheFile(context); links.Finish(context); }); - } - catch (Exception ex) - { + } catch (Exception ex) { logger.LogError($"Error running SharpHound: {ex.Message}\n{ex.StackTrace}"); } } // Accessor function for the PS1 to work, do not change or remove - public static void InvokeSharpHound(string[] args) - { + public static void InvokeSharpHound(string[] args) { Main(args).Wait(); } } #endregion -} +} \ No newline at end of file diff --git a/src/Writers/JsonDataWriter.cs b/src/Writers/JsonDataWriter.cs index 9bb456a..9d2226d 100644 --- a/src/Writers/JsonDataWriter.cs +++ b/src/Writers/JsonDataWriter.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection; using System.Text; using System.Threading.Tasks; using Newtonsoft.Json; @@ -95,7 +96,8 @@ internal override async Task FlushWriter() Count = Count, CollectionMethods = (long)_context.ResolvedCollectionMethods, DataType = DataType, - Version = DataVersion + Version = DataVersion, + CollectorVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString() }; await _jsonWriter.FlushAsync();