From 2d54077efa5d849ba12fd16d60b44f866abbe188 Mon Sep 17 00:00:00 2001 From: quentinr Date: Wed, 5 Nov 2025 19:33:11 -0800 Subject: [PATCH 1/2] feat: Process data for Active Directory sites * Process data associated with sites, sites servers, sites subnets * Add collection method "site", included in default collection methods --- src/Client/Enums.cs | 1 + src/Options.cs | 1 + src/Runtime/ObjectProcessors.cs | 128 ++++++++++++++++++++++++++++++++ src/Runtime/OutputWriter.cs | 39 +++++++--- 4 files changed, 159 insertions(+), 10 deletions(-) diff --git a/src/Client/Enums.cs b/src/Client/Enums.cs index 665ce5d..cec91c5 100644 --- a/src/Client/Enums.cs +++ b/src/Client/Enums.cs @@ -31,6 +31,7 @@ public enum CollectionMethodOptions LdapServices, SmbInfo, NTLMRegistry, + Site, // Re-introduce this when we're ready for Event Log collection // EventLogs, All diff --git a/src/Options.cs b/src/Options.cs index e560b02..080ca9d 100644 --- a/src/Options.cs +++ b/src/Options.cs @@ -208,6 +208,7 @@ internal bool ResolveCollectionMethods(ILogger logger, out CollectionMethod reso CollectionMethodOptions.LdapServices => CollectionMethod.LdapServices, CollectionMethodOptions.SmbInfo => CollectionMethod.SmbInfo, CollectionMethodOptions.NTLMRegistry => CollectionMethod.NTLMRegistry, + CollectionMethodOptions.Site => CollectionMethod.Site, // Re-introduce this when we're ready for Event Log collection // CollectionMethodOptions.EventLogs => CollectionMethod.EventLogs, CollectionMethodOptions.All => CollectionMethod.All, diff --git a/src/Runtime/ObjectProcessors.cs b/src/Runtime/ObjectProcessors.cs index f5571af..855b2c1 100644 --- a/src/Runtime/ObjectProcessors.cs +++ b/src/Runtime/ObjectProcessors.cs @@ -39,6 +39,7 @@ public class ObjectProcessors { private readonly SPNProcessors _spnProcessor; private readonly WebClientServiceProcessor _webClientProcessor; private readonly SmbProcessor _smbProcessor; + private readonly SiteProcessor _siteProcessor; private readonly ConcurrentDictionary _registryProcessorMap = new(); public ObjectProcessors(IContext context, ILogger log) { _context = context; @@ -60,6 +61,7 @@ public ObjectProcessors(IContext context, ILogger log) { _localGroupProcessor = new LocalGroupProcessor(context.LDAPUtils); _webClientProcessor = new WebClientServiceProcessor(log); _smbProcessor = new SmbProcessor(context.PortScanTimeout); + _siteProcessor = new SiteProcessor(context.LDAPUtils); _methods = context.ResolvedCollectionMethods; _cancellationToken = context.CancellationTokenSource.Token; _log = log; @@ -95,6 +97,12 @@ internal async Task ProcessObject(IDirectoryObject entry, return await ProcessCertTemplate(entry, resolvedSearchResult); case Label.IssuancePolicy: return await ProcessIssuancePolicy(entry, resolvedSearchResult); + case Label.Site: + return await ProcessSiteObject(entry, resolvedSearchResult); + case Label.SiteServer: + return await ProcessSiteServerObject(entry, resolvedSearchResult); + case Label.SiteSubnet: + return await ProcessSiteSubnetObject(entry, resolvedSearchResult); case Label.Base: return null; default: @@ -926,5 +934,125 @@ private async Task ProcessIssuancePolicy(IDirectoryObject entry, return ret; } + + private async Task ProcessSiteObject(IDirectoryObject entry, + ResolvedSearchResult resolvedSearchResult) + { + var ret = new Site + { + ObjectIdentifier = resolvedSearchResult.ObjectId + }; + + ret.Properties = new Dictionary(GetCommonProperties(entry, resolvedSearchResult)); + + if (_methods.HasFlag(CollectionMethod.ACL)) + { + var aces = await _aclProcessor.ProcessACL(resolvedSearchResult, entry, true) + .ToArrayAsync(cancellationToken: _cancellationToken); + ret.Properties.Add("doesanyacegrantownerrights", aces.Any(ace => ace.IsPermissionForOwnerRightsSid)); + ret.Properties.Add("doesanyinheritedacegrantownerrights", aces.Any(ace => ace.IsInheritedPermissionForOwnerRightsSid)); + ret.Aces = aces; + ret.IsACLProtected = _aclProcessor.IsACLProtected(entry); + ret.Properties.Add("isaclprotected", ret.IsACLProtected); + } + + if (_methods.HasFlag(CollectionMethod.ObjectProps)) + { + ret.Properties = + ContextUtils.Merge(LdapPropertyProcessor.ReadSiteProperties(entry), ret.Properties); + if (_context.Flags.CollectAllProperties) + { + ret.Properties = ContextUtils.Merge(_ldapPropertyProcessor.ParseAllProperties(entry), + ret.Properties); + } + } + + ret.Links = await _siteProcessor.ReadSiteGPLinks(resolvedSearchResult, entry).ToArrayAsync(); + + return ret; + } + + private async Task ProcessSiteServerObject(IDirectoryObject entry, + ResolvedSearchResult resolvedSearchResult) + { + var ret = new SiteServer + { + ObjectIdentifier = resolvedSearchResult.ObjectId + }; + + ret.Properties = new Dictionary(GetCommonProperties(entry, resolvedSearchResult)); + + + if (await _siteProcessor.GetContainingSiteForServer(entry) is (true, var container)) + { + ret.ContainedBy = container; + } + + if (_methods.HasFlag(CollectionMethod.ACL)) + { + var aces = await _aclProcessor.ProcessACL(resolvedSearchResult, entry, true) + .ToArrayAsync(cancellationToken: _cancellationToken); + ret.Properties.Add("doesanyacegrantownerrights", aces.Any(ace => ace.IsPermissionForOwnerRightsSid)); + ret.Properties.Add("doesanyinheritedacegrantownerrights", aces.Any(ace => ace.IsInheritedPermissionForOwnerRightsSid)); + ret.Aces = aces; + ret.IsACLProtected = _aclProcessor.IsACLProtected(entry); + ret.Properties.Add("isaclprotected", ret.IsACLProtected); + } + + if (_methods.HasFlag(CollectionMethod.ObjectProps)) + { + ret.Properties = + ContextUtils.Merge(LdapPropertyProcessor.ReadSiteServerProperties(entry), ret.Properties); + if (_context.Flags.CollectAllProperties) + { + ret.Properties = ContextUtils.Merge(_ldapPropertyProcessor.ParseAllProperties(entry), + ret.Properties); + } + } + + return ret; + } + + private async Task ProcessSiteSubnetObject(IDirectoryObject entry, + ResolvedSearchResult resolvedSearchResult) + { + var ret = new SiteSubnet + { + ObjectIdentifier = resolvedSearchResult.ObjectId + }; + + ret.Properties = new Dictionary(GetCommonProperties(entry, resolvedSearchResult)); + + if (_methods.HasFlag(CollectionMethod.ACL)) + { + var aces = await _aclProcessor.ProcessACL(resolvedSearchResult, entry, true) + .ToArrayAsync(cancellationToken: _cancellationToken); + ret.Properties.Add("doesanyacegrantownerrights", aces.Any(ace => ace.IsPermissionForOwnerRightsSid)); + ret.Properties.Add("doesanyinheritedacegrantownerrights", aces.Any(ace => ace.IsInheritedPermissionForOwnerRightsSid)); + ret.Aces = aces; + ret.IsACLProtected = _aclProcessor.IsACLProtected(entry); + ret.Properties.Add("isaclprotected", ret.IsACLProtected); + } + + if (_methods.HasFlag(CollectionMethod.ObjectProps)) + { + ret.Properties = + ContextUtils.Merge(LdapPropertyProcessor.ReadSiteSubnetProperties(entry), ret.Properties); + if (_context.Flags.CollectAllProperties) + { + ret.Properties = ContextUtils.Merge(_ldapPropertyProcessor.ParseAllProperties(entry), + ret.Properties); + } + + // Can only deduce containing site for a subnet if we read the object properties, including siteObject + + if (await _siteProcessor.GetContainingSiteForSubnet(ret.Properties) is (true, var container)) + { + ret.ContainedBy = container; + } + } + + return ret; + } } } \ No newline at end of file diff --git a/src/Runtime/OutputWriter.cs b/src/Runtime/OutputWriter.cs index 70820af..feaec9c 100644 --- a/src/Runtime/OutputWriter.cs +++ b/src/Runtime/OutputWriter.cs @@ -1,18 +1,19 @@ -using System; +using ICSharpCode.SharpZipLib.Core; +using ICSharpCode.SharpZipLib.Zip; +using Microsoft.Extensions.Logging; +using Sharphound.Client; +using Sharphound.Writers; +using SharpHoundCommonLib.Enums; +using SharpHoundCommonLib.OutputTypes; +using System; using System.Collections.Generic; using System.Diagnostics; +using System.DirectoryServices; using System.IO; using System.Linq; using System.Threading.Channels; using System.Threading.Tasks; using System.Timers; -using ICSharpCode.SharpZipLib.Core; -using ICSharpCode.SharpZipLib.Zip; -using Microsoft.Extensions.Logging; -using Sharphound.Client; -using Sharphound.Writers; -using SharpHoundCommonLib.Enums; -using SharpHoundCommonLib.OutputTypes; namespace Sharphound.Runtime { @@ -34,6 +35,9 @@ public class OutputWriter private readonly JsonDataWriter _nTAuthStoreOutput; private readonly JsonDataWriter _certTemplateOutput; private readonly JsonDataWriter _issuancePolicyOutput; + private readonly JsonDataWriter _siteOutput; + private readonly JsonDataWriter _siteServerOutput; + private readonly JsonDataWriter _siteSubnetOutput; private int _completedCount; @@ -57,6 +61,9 @@ public OutputWriter(IContext context, Channel outputChannel) _nTAuthStoreOutput = new JsonDataWriter(_context, DataType.NTAuthStores); _certTemplateOutput = new JsonDataWriter(_context, DataType.CertTemplates); _issuancePolicyOutput = new JsonDataWriter(_context, DataType.IssuancePolicies); + _siteOutput = new JsonDataWriter(_context, DataType.Sites); + _siteServerOutput = new JsonDataWriter(_context, DataType.SiteServers); + _siteSubnetOutput = new JsonDataWriter(_context, DataType.SiteSubnets); _runTimer = new Stopwatch(); _statusTimer = new Timer(_context.StatusInterval); @@ -145,12 +152,20 @@ internal async Task StartWriter() case IssuancePolicy issuancePolicy: await _issuancePolicyOutput.AcceptObject(issuancePolicy); break; + case Site site: + await _siteOutput.AcceptObject(site); + break; + case SiteServer siteServer: + await _siteServerOutput.AcceptObject(siteServer); + break; + case SiteSubnet siteSubnet: + await _siteSubnetOutput.AcceptObject(siteSubnet); + break; default: throw new ArgumentOutOfRangeException(nameof(item)); } } - Console.WriteLine("Closing writers"); return await FlushWriters(); } @@ -169,6 +184,9 @@ private async Task FlushWriters() await _nTAuthStoreOutput.FlushWriter(); await _certTemplateOutput.FlushWriter(); await _issuancePolicyOutput.FlushWriter(); + await _siteOutput.FlushWriter(); + await _siteServerOutput.FlushWriter(); + await _siteSubnetOutput.FlushWriter(); CloseOutput(); var fileName = ZipFiles(); return fileName; @@ -198,7 +216,8 @@ private string ZipFiles() _containerOutput.GetFilename(), _domainOutput.GetFilename(), _gpoOutput.GetFilename(), _ouOutput.GetFilename(), _rootCAOutput.GetFilename(), _aIACAOutput.GetFilename(), _enterpriseCAOutput.GetFilename(), _nTAuthStoreOutput.GetFilename(), - _certTemplateOutput.GetFilename(),_issuancePolicyOutput.GetFilename() + _certTemplateOutput.GetFilename(),_issuancePolicyOutput.GetFilename(), + _siteOutput.GetFilename(), _siteServerOutput.GetFilename(), _siteSubnetOutput.GetFilename(), }); foreach (var entry in fileList.Where(x => !string.IsNullOrEmpty(x))) From ebc37438ded502ffea36a9337d01c2a6f6e5a561 Mon Sep 17 00:00:00 2001 From: quentinr Date: Wed, 5 Nov 2025 23:30:04 -0800 Subject: [PATCH 2/2] chore: Add the "Site" collection method to help message --- src/Options.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Options.cs b/src/Options.cs index 080ca9d..d95f285 100644 --- a/src/Options.cs +++ b/src/Options.cs @@ -14,7 +14,7 @@ public class Options // Options that affect what is collected [Option('c', "collectionmethods", Default = new[] { "Default" }, HelpText = - "Collection Methods: Group, LocalGroup, LocalAdmin, RDP, DCOM, PSRemote, Session, Trusts, ACL, Container, ComputerOnly, GPOLocalGroup, LoggedOn, ObjectProps, SPNTargets, UserRights, Default, DCOnly, CARegistry, DCRegistry, CertServices, WebClientService, LdapServices, SmbInfo, NTLMRegistry, All")] + "Collection Methods: Group, LocalGroup, LocalAdmin, RDP, DCOM, PSRemote, Session, Trusts, ACL, Container, ComputerOnly, GPOLocalGroup, LoggedOn, ObjectProps, SPNTargets, UserRights, Default, DCOnly, CARegistry, DCRegistry, CertServices, Site, WebClientService, LdapServices, SmbInfo, NTLMRegistry, All")] public IEnumerable CollectionMethods { get; set; } [Option('d', "domain", Default = null, HelpText = "Specify domain to enumerate")]