From 2d54077efa5d849ba12fd16d60b44f866abbe188 Mon Sep 17 00:00:00 2001 From: quentinr Date: Wed, 5 Nov 2025 19:33:11 -0800 Subject: [PATCH 1/5] 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/5] 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")] From a5e29be69de2d4c8c87c7b47bce9aff0cb7b2010 Mon Sep 17 00:00:00 2001 From: JonasBK Date: Mon, 15 Jun 2026 12:22:44 +0200 Subject: [PATCH 3/5] update site collection --- README.md | 2 +- src/PowerShell/Template.ps1 | 5 ++-- src/Runtime/ObjectProcessors.cs | 45 +++++++++++++++++++++++---------- 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 8ece40e..e7ddf9f 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ The listing below details the CLI arguments SharpHound supports. Additional deta ``` -c, --collectionmethods (Default: Default) Collection Methods: Container, Group, LocalGroup, GPOLocalGroup, Session, LoggedOn, ObjectProps, ACL, ComputerOnly, Trusts, Default, RDP, DCOM, DCOnly, UserRights, - CARegistry, DCRegistry, CertServices, WebClientService, NTLMRegistry,SMBInfo,LdapServices + CARegistry, DCRegistry, CertServices, Site, WebClientService, NTLMRegistry, SMBInfo, LdapServices -d, --domain Specify domain to enumerate diff --git a/src/PowerShell/Template.ps1 b/src/PowerShell/Template.ps1 index f19d7bb..676e789 100644 --- a/src/PowerShell/Template.ps1 +++ b/src/PowerShell/Template.ps1 @@ -30,12 +30,13 @@ LoggedOn - Collect session information using privileged methods (needs admin!) ObjectProps - Collect node property information for users and computers SPNTargets - Collect SPN targets (currently only MSSQL) - Default - Collect Group Membership, Local Admin, Sessions, Containers, ACLs, Domain Trusts, and ADCS objects - DcOnly - Collect Group Membership, ACLs, ObjectProps, Trusts, Containers, GPO Admins, and ADCS objects + Default - Collect Group Membership, Local Admin, Sessions, Containers, ACLs, Domain Trusts, ADCS objects, and AD sites + DcOnly - Collect Group Membership, ACLs, ObjectProps, Trusts, Containers, GPO Admins, ADCS objects, and AD sites UserRights - Collect User Rights Assignment from domain computers (needs admin) CARegistry - Collect ADCS properties from registry of Certificate Authority servers DCRegistry - Collect properties from registry of Domain Controller servers CertServices - Collect ADCS objects from Certificate Services + Site - Collect AD site, site server, and site subnet data All - Collect all data This can be a list of comma separated valued as well to run multiple collection methods! diff --git a/src/Runtime/ObjectProcessors.cs b/src/Runtime/ObjectProcessors.cs index dea022a..c30ab9e 100644 --- a/src/Runtime/ObjectProcessors.cs +++ b/src/Runtime/ObjectProcessors.cs @@ -974,7 +974,7 @@ private async Task ProcessSiteObject(IDirectoryObject entry, ret.Properties = new Dictionary(GetCommonProperties(entry, resolvedSearchResult)); - if (_methods.HasFlag(CollectionMethod.ACL)) + if (_methods.HasFlag(CollectionMethod.ACL) || _methods.HasFlag(CollectionMethod.Site)) { var aces = await _aclProcessor.ProcessACL(resolvedSearchResult, entry, true) .ToArrayAsync(cancellationToken: _cancellationToken); @@ -983,9 +983,10 @@ private async Task ProcessSiteObject(IDirectoryObject entry, ret.Aces = aces; ret.IsACLProtected = _aclProcessor.IsACLProtected(entry); ret.Properties.Add("isaclprotected", ret.IsACLProtected); + ret.InheritanceHashes = _aclProcessor.GetInheritedAceHashes(entry, resolvedSearchResult).ToArray(); } - if (_methods.HasFlag(CollectionMethod.ObjectProps)) + if (_methods.HasFlag(CollectionMethod.ObjectProps) || _methods.HasFlag(CollectionMethod.Site)) { ret.Properties = ContextUtils.Merge(LdapPropertyProcessor.ReadSiteProperties(entry), ret.Properties); @@ -996,7 +997,16 @@ private async Task ProcessSiteObject(IDirectoryObject entry, } } - ret.Links = await _siteProcessor.ReadSiteGPLinks(resolvedSearchResult, entry).ToArrayAsync(); + if (_methods.HasFlag(CollectionMethod.Container) || _methods.HasFlag(CollectionMethod.Site)) { + if (await _containerProcessor.GetContainingObject(entry) is (true, var container)) { + ret.ContainedBy = container; + } + } + + if (_methods.HasFlag(CollectionMethod.Site)) + { + ret.Links = await _siteProcessor.ReadSiteGPLinks(resolvedSearchResult, entry).ToArrayAsync(); + } return ret; } @@ -1010,14 +1020,8 @@ private async Task ProcessSiteServerObject(IDirectoryObject entry, }; ret.Properties = new Dictionary(GetCommonProperties(entry, resolvedSearchResult)); - - - if (await _siteProcessor.GetContainingSiteForServer(entry) is (true, var container)) - { - ret.ContainedBy = container; - } - if (_methods.HasFlag(CollectionMethod.ACL)) + if (_methods.HasFlag(CollectionMethod.ACL) || _methods.HasFlag(CollectionMethod.Site)) { var aces = await _aclProcessor.ProcessACL(resolvedSearchResult, entry, true) .ToArrayAsync(cancellationToken: _cancellationToken); @@ -1028,7 +1032,7 @@ private async Task ProcessSiteServerObject(IDirectoryObject entry, ret.Properties.Add("isaclprotected", ret.IsACLProtected); } - if (_methods.HasFlag(CollectionMethod.ObjectProps)) + if (_methods.HasFlag(CollectionMethod.ObjectProps) || _methods.HasFlag(CollectionMethod.Site)) { ret.Properties = ContextUtils.Merge(LdapPropertyProcessor.ReadSiteServerProperties(entry), ret.Properties); @@ -1039,6 +1043,20 @@ private async Task ProcessSiteServerObject(IDirectoryObject entry, } } + if (_methods.HasFlag(CollectionMethod.Container) || _methods.HasFlag(CollectionMethod.Site)) { + if (await _siteProcessor.GetContainingSiteForServer(entry) is (true, var container)) + { + ret.ContainedBy = container; + } + } + + if (_methods.HasFlag(CollectionMethod.Site)) { + if (await _siteProcessor.GetReferencedComputerForServer(entry) is (true, var server)) + { + ret.ServerIs = server; + } + } + return ret; } @@ -1052,7 +1070,7 @@ private async Task ProcessSiteSubnetObject(IDirectoryObject entry, ret.Properties = new Dictionary(GetCommonProperties(entry, resolvedSearchResult)); - if (_methods.HasFlag(CollectionMethod.ACL)) + if (_methods.HasFlag(CollectionMethod.ACL) || _methods.HasFlag(CollectionMethod.Site)) { var aces = await _aclProcessor.ProcessACL(resolvedSearchResult, entry, true) .ToArrayAsync(cancellationToken: _cancellationToken); @@ -1063,7 +1081,7 @@ private async Task ProcessSiteSubnetObject(IDirectoryObject entry, ret.Properties.Add("isaclprotected", ret.IsACLProtected); } - if (_methods.HasFlag(CollectionMethod.ObjectProps)) + if (_methods.HasFlag(CollectionMethod.ObjectProps) || _methods.HasFlag(CollectionMethod.Container) || _methods.HasFlag(CollectionMethod.Site)) { ret.Properties = ContextUtils.Merge(LdapPropertyProcessor.ReadSiteSubnetProperties(entry), ret.Properties); @@ -1074,7 +1092,6 @@ private async Task ProcessSiteSubnetObject(IDirectoryObject entry, } // 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; From 96afd60c86a6013d31df01d7fa41b0ba95d865bb Mon Sep 17 00:00:00 2001 From: JonasBK Date: Mon, 15 Jun 2026 13:59:35 +0200 Subject: [PATCH 4/5] minor things in OutputWriter --- src/Runtime/OutputWriter.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Runtime/OutputWriter.cs b/src/Runtime/OutputWriter.cs index feaec9c..206c667 100644 --- a/src/Runtime/OutputWriter.cs +++ b/src/Runtime/OutputWriter.cs @@ -1,19 +1,18 @@ -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; 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 { @@ -166,6 +165,7 @@ internal async Task StartWriter() } } + Console.WriteLine("Closing writers"); return await FlushWriters(); } From d3e34daaa9bfe72d519cf086ed7c936a40601884 Mon Sep 17 00:00:00 2001 From: JonasBK Date: Mon, 22 Jun 2026 15:14:42 +0200 Subject: [PATCH 5/5] add missing space --- src/Runtime/OutputWriter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Runtime/OutputWriter.cs b/src/Runtime/OutputWriter.cs index 206c667..32c2aa1 100644 --- a/src/Runtime/OutputWriter.cs +++ b/src/Runtime/OutputWriter.cs @@ -216,7 +216,7 @@ 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(), });