diff --git a/src/CommonLib/Enums/CollectionMethod.cs b/src/CommonLib/Enums/CollectionMethod.cs index 19191d126..458a3a05b 100644 --- a/src/CommonLib/Enums/CollectionMethod.cs +++ b/src/CommonLib/Enums/CollectionMethod.cs @@ -27,6 +27,7 @@ public enum CollectionMethod { WebClientService = 1 << 21, SmbInfo = 1 << 22, NTLMRegistry = 1 << 23, + Site = 1 << 24, //TODO: Re-introduce this when we're ready for Event Log collection //EventLogs = 1 << 23, LocalGroups = DCOM | RDP | LocalAdmin | PSRemote, @@ -34,7 +35,7 @@ public enum CollectionMethod { DCOnly = ACL | Container | Group | ObjectProps | Trusts | GPOLocalGroup | CertServices, Default = Group | Session | Trusts | ACL | ObjectProps | LocalGroups | SPNTargets | Container | CertServices | - LdapServices | SmbInfo | WebClientService, + LdapServices | SmbInfo | WebClientService | Site, All = Default | LoggedOn | GPOLocalGroup | UserRights | CARegistry | DCRegistry | WebClientService | LdapServices | NTLMRegistry diff --git a/src/CommonLib/Enums/DataType.cs b/src/CommonLib/Enums/DataType.cs index c96be5be9..2553fd529 100644 --- a/src/CommonLib/Enums/DataType.cs +++ b/src/CommonLib/Enums/DataType.cs @@ -15,5 +15,8 @@ public static class DataType public const string EnterpriseCAs = "enterprisecas"; public const string CertTemplates = "certtemplates"; public const string IssuancePolicies = "issuancepolicies"; + public const string Sites = "sites"; + public const string SiteServers = "siteservers"; + public const string SiteSubnets = "sitesubnets"; } } diff --git a/src/CommonLib/Enums/LDAPProperties.cs b/src/CommonLib/Enums/LDAPProperties.cs index 0bf6b726e..da1298d05 100644 --- a/src/CommonLib/Enums/LDAPProperties.cs +++ b/src/CommonLib/Enums/LDAPProperties.cs @@ -96,5 +96,7 @@ public static class LDAPProperties public const string LockOutObservationWindow = "lockoutobservationwindow"; public const string PrincipalName = "msds-principalname"; public const string GroupType = "grouptype"; + public const string ServerReference = "serverreference"; + public const string SiteObject = "siteobject"; } } diff --git a/src/CommonLib/Enums/Labels.cs b/src/CommonLib/Enums/Labels.cs index b0bacb68e..76e05812e 100644 --- a/src/CommonLib/Enums/Labels.cs +++ b/src/CommonLib/Enums/Labels.cs @@ -18,6 +18,9 @@ public enum Label AIACA, EnterpriseCA, NTAuthStore, - IssuancePolicy + IssuancePolicy, + Site, + SiteServer, + SiteSubnet } } diff --git a/src/CommonLib/Enums/ObjectClass.cs b/src/CommonLib/Enums/ObjectClass.cs index f8bab0fc9..db98dd5e4 100644 --- a/src/CommonLib/Enums/ObjectClass.cs +++ b/src/CommonLib/Enums/ObjectClass.cs @@ -12,4 +12,7 @@ public static class ObjectClass { public const string OIDContainerClass = "msPKI-Enterprise-Oid"; public const string GMSAClass = "msds-groupmanagedserviceaccount"; public const string MSAClass = "msds-managedserviceaccount"; + public const string SiteClass = "site"; + public const string SiteServerClass = "server"; + public const string SiteSubnetClass = "subnet"; } \ No newline at end of file diff --git a/src/CommonLib/LdapProducerQueryGenerator.cs b/src/CommonLib/LdapProducerQueryGenerator.cs index 25c6cb9bf..c3ce2e25f 100644 --- a/src/CommonLib/LdapProducerQueryGenerator.cs +++ b/src/CommonLib/LdapProducerQueryGenerator.cs @@ -104,9 +104,10 @@ public static GeneratedLdapParameters GenerateConfigurationPartitionParameters(C properties.AddRange(CommonProperties.TypeResolutionProps); if (methods.HasFlag(CollectionMethod.ACL) || methods.HasFlag(CollectionMethod.ObjectProps) || - methods.HasFlag(CollectionMethod.Container) || methods.HasFlag(CollectionMethod.CertServices)) { + methods.HasFlag(CollectionMethod.Container) || methods.HasFlag(CollectionMethod.CertServices) || + methods.HasFlag(CollectionMethod.Site)) { filter = filter.AddContainers().AddConfiguration().AddCertificateTemplates().AddCertificateAuthorities() - .AddEnterpriseCertificationAuthorities().AddIssuancePolicies(); + .AddEnterpriseCertificationAuthorities().AddIssuancePolicies().AddSites().AddSiteServers().AddSiteSubnets(); if (methods.HasFlag(CollectionMethod.ObjectProps)) { properties.AddRange(CommonProperties.ObjectPropsProps); @@ -131,6 +132,13 @@ public static GeneratedLdapParameters GenerateConfigurationPartitionParameters(C properties.AddRange(CommonProperties.CertAbuseProps); } + if (methods.HasFlag(CollectionMethod.Site)) + { + properties.AddRange(CommonProperties.SiteProps); + properties.AddRange(CommonProperties.SiteServerProps); + properties.AddRange(CommonProperties.SiteSubnetProps); + } + return new GeneratedLdapParameters { Filter = filter, Attributes = properties.Distinct().ToArray() diff --git a/src/CommonLib/LdapQueries/CommonProperties.cs b/src/CommonLib/LdapQueries/CommonProperties.cs index 508b5490c..88cef7462 100644 --- a/src/CommonLib/LdapQueries/CommonProperties.cs +++ b/src/CommonLib/LdapQueries/CommonProperties.cs @@ -98,5 +98,23 @@ public static class CommonProperties public static readonly string[] StealthProperties = { LDAPProperties.HomeDirectory, LDAPProperties.ScriptPath, LDAPProperties.ProfilePath }; + + public static readonly string[] SiteProps = + { + LDAPProperties.DisplayName, LDAPProperties.Name, LDAPProperties.ObjectGUID, LDAPProperties.GPLink, + LDAPProperties.GroupPolicyOptions, LDAPProperties.ObjectClass + }; + + public static readonly string[] SiteServerProps = + { + LDAPProperties.DisplayName, LDAPProperties.Name, LDAPProperties.ObjectGUID, LDAPProperties.ObjectClass, LDAPProperties.DNSHostName, + LDAPProperties.ServerReference + }; + + public static readonly string[] SiteSubnetProps = + { + LDAPProperties.DisplayName, LDAPProperties.Name, LDAPProperties.CanonicalName, LDAPProperties.ObjectGUID, LDAPProperties.ObjectClass, + LDAPProperties.SiteObject + }; } } \ No newline at end of file diff --git a/src/CommonLib/LdapQueries/LdapFilter.cs b/src/CommonLib/LdapQueries/LdapFilter.cs index e98660ce1..a16584dce 100644 --- a/src/CommonLib/LdapQueries/LdapFilter.cs +++ b/src/CommonLib/LdapQueries/LdapFilter.cs @@ -215,6 +215,45 @@ public LdapFilter AddComputersNoMSAs(params string[] conditions) { return this; } + /// + /// Add a filter that will match Active Directory sites + /// + /// + /// + public LdapFilter AddSites(params string[] conditions) + { + _filterParts.Add(BuildString( + "(objectClass=site)", + conditions)); + return this; + } + + /// + /// Add a filter that will match Active Directory site servers + /// + /// + /// + public LdapFilter AddSiteServers(params string[] conditions) + { + _filterParts.Add(BuildString( + "(objectClass=server)", + conditions)); + return this; + } + + /// + /// Add a filter that will match Active Directory site subnets + /// + /// + /// + public LdapFilter AddSiteSubnets(params string[] conditions) + { + _filterParts.Add(BuildString( + "(objectClass=subnet)", + conditions)); + return this; + } + /// /// Adds a generic user specified filter /// diff --git a/src/CommonLib/LdapUtils.cs b/src/CommonLib/LdapUtils.cs index 14612da12..8c33a614c 100644 --- a/src/CommonLib/LdapUtils.cs +++ b/src/CommonLib/LdapUtils.cs @@ -1205,6 +1205,18 @@ internal static bool ResolveLabel(string objectIdentifier, string distinguishedN type = Label.IssuancePolicy; } } + else if (objectClasses.Contains(ObjectClass.SiteClass, StringComparer.OrdinalIgnoreCase)) + { + type = Label.Site; + } + else if (objectClasses.Contains(ObjectClass.SiteServerClass, StringComparer.OrdinalIgnoreCase)) + { + type = Label.SiteServer; + } + else if (objectClasses.Contains(ObjectClass.SiteSubnetClass, StringComparer.OrdinalIgnoreCase)) + { + type = Label.SiteSubnet; + } return type != Label.Base; } @@ -1214,7 +1226,7 @@ internal static bool ResolveLabel(string objectIdentifier, string distinguishedN if (!directoryObject.GetObjectIdentifier(out var objectIdentifier)) { return (false, default); } - + var res = new ResolvedSearchResult { ObjectId = objectIdentifier }; @@ -1270,11 +1282,10 @@ await utils.GetDomainNameFromSid(objectIdentifier) is (true, var domainName)) { if (await utils.GetWellKnownPrincipal(objectIdentifier, domain) is (true, var convertedPrincipal)) { res.ObjectId = convertedPrincipal.ObjectIdentifier; } - return (true, res); } - res.ObjectType = await ComputeLabel(directoryObject, objectIdentifier, domain, utils); + res.ObjectType = await ComputeLabel(directoryObject, objectIdentifier, domain, utils); directoryObject.TryGetProperty(LDAPProperties.SAMAccountName, out var samAccountName); res.DisplayName = ComputeDisplayName(directoryObject, domain, res.ObjectType, samAccountName); @@ -1395,6 +1406,43 @@ private static string ComputeDisplayName(IDirectoryObject directoryObject, strin displayName = $"UNKNOWN@{domain}"; } + break; + } + case Label.Site: { + if (directoryObject.TryGetProperty(LDAPProperties.Name, out var name)) + { + displayName = $"{name}@{domain}"; + } + else + { + displayName = $"UNKNOWN@{domain}"; + } + break; + } + case Label.SiteServer: + { + // Not specifying @{domain} here since Site servers may belong to other domains, so this might confuse the user + if (directoryObject.TryGetProperty(LDAPProperties.Name, out var name)) + { + displayName = $"{name}"; + } + else + { + displayName = $"UNKNOWN"; + } + break; + } + case Label.SiteSubnet: + { + // Not specifying @{domain} here since subnets are not domain-specific + if (directoryObject.TryGetProperty(LDAPProperties.Name, out var name)) + { + displayName = $"{name}"; + } + else + { + displayName = $"UNKNOWN"; + } break; } default: diff --git a/src/CommonLib/OutputTypes/Site.cs b/src/CommonLib/OutputTypes/Site.cs new file mode 100644 index 000000000..b2ce5c241 --- /dev/null +++ b/src/CommonLib/OutputTypes/Site.cs @@ -0,0 +1,12 @@ +using System; + +namespace SharpHoundCommonLib.OutputTypes +{ + public class Site : OutputBase + { + // Subnets and Servers are common site children; keep them optional and empty by default. + //public string[] Subnets { get; set; } = Array.Empty(); + //public TypedPrincipal[] Servers { get; set; } = Array.Empty(); + public GPLink[] Links { get; set; } = Array.Empty(); + } +} \ No newline at end of file diff --git a/src/CommonLib/OutputTypes/SiteServer.cs b/src/CommonLib/OutputTypes/SiteServer.cs new file mode 100644 index 000000000..07c1542fc --- /dev/null +++ b/src/CommonLib/OutputTypes/SiteServer.cs @@ -0,0 +1,7 @@ +namespace SharpHoundCommonLib.OutputTypes +{ + public class SiteServer : OutputBase + { + + } +} \ No newline at end of file diff --git a/src/CommonLib/OutputTypes/SiteSubnet.cs b/src/CommonLib/OutputTypes/SiteSubnet.cs new file mode 100644 index 000000000..568277f0b --- /dev/null +++ b/src/CommonLib/OutputTypes/SiteSubnet.cs @@ -0,0 +1,7 @@ +namespace SharpHoundCommonLib.OutputTypes +{ + public class SiteSubnet : OutputBase + { + + } +} \ No newline at end of file diff --git a/src/CommonLib/Processors/ACLProcessor.cs b/src/CommonLib/Processors/ACLProcessor.cs index 383b69aff..a79c0a16b 100644 --- a/src/CommonLib/Processors/ACLProcessor.cs +++ b/src/CommonLib/Processors/ACLProcessor.cs @@ -38,7 +38,10 @@ static ACLProcessor() { { Label.EnterpriseCA, "ee4aa692-3bba-11d2-90cc-00c04fd91ab1" }, { Label.NTAuthStore, "3fdfee50-47f4-11d1-a9c3-0000f80367c1" }, { Label.CertTemplate, "e5209ca2-3bba-11d2-90cc-00c04fd91ab1" }, - { Label.IssuancePolicy, "37cfd85c-6719-4ad8-8f9e-8678ba627563" } + { Label.IssuancePolicy, "37cfd85c-6719-4ad8-8f9e-8678ba627563" }, + { Label.Site, "bf967ab3-0de6-11d0-a285-00aa003049e2" }, + { Label.SiteServer, "bf967a92-0de6-11d0-a285-00aa003049e2" }, + { Label.SiteSubnet, "b7b13124-b82e-11d0-afee-0000f80367c1" } }; } @@ -734,7 +737,10 @@ or Label.RootCA or Label.EnterpriseCA or Label.AIACA or Label.NTAuthStore - or Label.IssuancePolicy) + or Label.IssuancePolicy + or Label.Site + or Label.SiteServer + or Label.SiteSubnet) if (aceType is ACEGuids.AllGuid or "") yield return new ACE { PrincipalType = resolvedPrincipal.ObjectType, @@ -776,7 +782,7 @@ or Label.NTAuthStore IsPermissionForOwnerRightsSid = isPermissionForOwnerRightsSid, IsInheritedPermissionForOwnerRightsSid = isInheritedPermissionForOwnerRightsSid, }; - else if (objectType is Label.OU or Label.Domain && aceType == ACEGuids.WriteGPLink) + else if (objectType is Label.OU or Label.Domain or Label.Site && aceType == ACEGuids.WriteGPLink) yield return new ACE { PrincipalType = resolvedPrincipal.ObjectType, PrincipalSID = resolvedPrincipal.ObjectIdentifier, diff --git a/src/CommonLib/Processors/LdapPropertyProcessor.cs b/src/CommonLib/Processors/LdapPropertyProcessor.cs index 4899b390f..3d38fb461 100644 --- a/src/CommonLib/Processors/LdapPropertyProcessor.cs +++ b/src/CommonLib/Processors/LdapPropertyProcessor.cs @@ -643,6 +643,29 @@ public async Task ReadIssuancePolicyProperties(IDirect return ret; } + public static Dictionary ReadSiteProperties(IDirectoryObject entry) + { + var props = GetCommonProps(entry); + return props; + } + + + public static Dictionary ReadSiteServerProperties(IDirectoryObject entry) + { + var props = GetCommonProps(entry); + props.Add("dnshostname", entry.GetProperty(LDAPProperties.DNSHostName)); + props.Add("serverreference", entry.GetProperty(LDAPProperties.ServerReference)); + return props; + } + + public static Dictionary ReadSiteSubnetProperties(IDirectoryObject entry) + { + var props = GetCommonProps(entry); + props.Add("cn", entry.GetProperty(LDAPProperties.CanonicalName)); + props.Add("siteObject", entry.GetProperty(LDAPProperties.SiteObject)); + return props; + } + /// /// Attempts to parse all LDAP attributes outside of the ones already collected and converts them to a human readable /// format using a best guess diff --git a/src/CommonLib/Processors/SiteProcessor.cs b/src/CommonLib/Processors/SiteProcessor.cs new file mode 100644 index 000000000..a38bcdbbb --- /dev/null +++ b/src/CommonLib/Processors/SiteProcessor.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using SharpHoundCommonLib.OutputTypes; + +namespace SharpHoundCommonLib.Processors +{ + public class SiteProcessor + { + private readonly ILogger _log; + private readonly ILdapUtils _utils; + + public SiteProcessor(ILdapUtils utils, ILogger log = null) + { + _utils = utils; + _log = log ?? Logging.LogProvider.CreateLogger("SiteProc"); + } + + + /// + /// Helper function to pass commonlib types to GetContainingSiteForServer + /// + /// + /// + public async Task<(bool Success, TypedPrincipal principal)> GetContainingSiteForServer(IDirectoryObject entry) + { + if (entry.TryGetDistinguishedName(out var dn)) + { + _log.LogTrace("Reading containing site for server {DN}", dn); + return await GetContainingSiteForServer(dn); + } + + return (false, default); + } + + /// + /// Helper function to pass commonlib types to GetContainingSiteForSubnet + /// + /// + /// + public async Task<(bool Success, TypedPrincipal principal)> GetContainingSiteForSubnet(Dictionary subnetProperties) + { + if (subnetProperties.TryGetValue("siteObject", out var siteObject)) + { + return await GetContainingSiteForSubnet(siteObject.ToString()); + } + return (false, default); + } + + /// + /// Uses the distinguishedname of a site server object to get its containing site by stripping the two first parts and using the remainder to find the container object + /// Saves lots of LDAP calls compared to enumerating container info directly + /// + /// + /// + public async Task<(bool Success, TypedPrincipal Principal)> GetContainingSiteForServer(string distinguishedName) + { + var servercontainerdn = Helpers.RemoveDistinguishedNamePrefix(distinguishedName); + var sitedn = Helpers.RemoveDistinguishedNamePrefix(servercontainerdn); + return await _utils.ResolveDistinguishedName(sitedn); + } + + /// + /// Uses the siteObject of a subnet to get its containing site + /// + /// + /// + public async Task<(bool Success, TypedPrincipal Principal)> GetContainingSiteForSubnet(string siteObject) + { + return await _utils.ResolveDistinguishedName(siteObject); + } + + public IAsyncEnumerable ReadSiteGPLinks(ResolvedSearchResult result, IDirectoryObject entry) + { + if (entry.TryGetProperty(LDAPProperties.GPLink, out var links)) + { + return ReadSiteGPLinks(links); + } + + return AsyncEnumerable.Empty(); + } + + /// + /// Reads the "gplink" property from a SearchResult and converts the links into the acceptable SharpHound format + /// + /// + /// + public async IAsyncEnumerable ReadSiteGPLinks(string gpLink) + { + if (gpLink == null) + yield break; + + foreach (var link in Helpers.SplitGPLinkProperty(gpLink)) + { + var enforced = link.Status.Equals("2"); + + var res = await _utils.ResolveDistinguishedName(link.DistinguishedName); + + if (res.Success) + { + yield return new GPLink + { + GUID = res.Principal.ObjectIdentifier, + IsEnforced = enforced + }; + } + } + } + } +} \ No newline at end of file