diff --git a/.gitignore b/.gitignore index 6e28db82c..45103240f 100644 --- a/.gitignore +++ b/.gitignore @@ -603,3 +603,6 @@ FodyWeavers.xsd # JetBrains Rider .idea/ *.sln.iml + +# Codex local workspace state +.codex diff --git a/src/CommonLib/DirectoryObjects/DirectoryObjectExtensions.cs b/src/CommonLib/DirectoryObjects/DirectoryObjectExtensions.cs index 57399d66a..a5af2c693 100644 --- a/src/CommonLib/DirectoryObjects/DirectoryObjectExtensions.cs +++ b/src/CommonLib/DirectoryObjects/DirectoryObjectExtensions.cs @@ -22,6 +22,13 @@ public static bool IsGMSA(this IDirectoryObject directoryObject) { } public static bool GetObjectIdentifier(this IDirectoryObject directoryObject, out string objectIdentifier) { + // Builtin container has a SID (always "S-1-5-32"). We use the ObjectGUID as ID like for other containers + if (directoryObject.TryGetArrayProperty(LDAPProperties.ObjectClass, out var objectClasses) && + objectClasses.Contains(ObjectClass.BuiltinDomainClass, StringComparer.OrdinalIgnoreCase) && + directoryObject.TryGetGuid(out objectIdentifier) && !string.IsNullOrWhiteSpace(objectIdentifier)) { + return true; + } + if (directoryObject.TryGetSecurityIdentifier(out objectIdentifier) && !string.IsNullOrWhiteSpace(objectIdentifier)) { return true; } @@ -68,4 +75,4 @@ public static bool HasLAPS(this IDirectoryObject directoryObject) { return false; } -} \ No newline at end of file +} diff --git a/src/CommonLib/Enums/CollectionMethod.cs b/src/CommonLib/Enums/CollectionMethod.cs index 19191d126..68ebcdf89 100644 --- a/src/CommonLib/Enums/CollectionMethod.cs +++ b/src/CommonLib/Enums/CollectionMethod.cs @@ -27,14 +27,15 @@ 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, ComputerOnly = LocalGroups | Session | UserRights | CARegistry | DCRegistry | WebClientService | SmbInfo | NTLMRegistry, - DCOnly = ACL | Container | Group | ObjectProps | Trusts | GPOLocalGroup | CertServices, + DCOnly = ACL | Container | Group | ObjectProps | Trusts | GPOLocalGroup | CertServices | Site, 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/EdgeNames.cs b/src/CommonLib/Enums/EdgeNames.cs index ed0bd4af9..833621bc1 100644 --- a/src/CommonLib/Enums/EdgeNames.cs +++ b/src/CommonLib/Enums/EdgeNames.cs @@ -24,6 +24,7 @@ public static class EdgeNames public const string WriteGPLink = "WriteGPLink"; public const string WriteAltSecurityIdentities = "WriteAltSecurityIdentities"; public const string WritePublicInformation = "WritePublicInformation"; + public const string ServerIs = "ServerIs"; //CertAbuse edges public const string WritePKIEnrollmentFlag = "WritePKIEnrollmentFlag"; @@ -32,4 +33,4 @@ public static class EdgeNames public const string ManageCertificates = "ManageCertificates"; public const string Enroll = "Enroll"; } -} \ No newline at end of file +} 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 8d7f8c6d7..f961430a4 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..1113999df 100644 --- a/src/CommonLib/Enums/ObjectClass.cs +++ b/src/CommonLib/Enums/ObjectClass.cs @@ -6,10 +6,15 @@ public static class ObjectClass { public const string DomainClass = "domain"; public const string ContainerClass = "container"; public const string ConfigurationClass = "configuration"; + public const string BuiltinDomainClass = "builtinDomain"; + public const string SitesContainerClass = "sitesContainer"; public const string PKICertificateTemplateClass = "pKICertificateTemplate"; public const string PKIEnrollmentServiceClass = "pKIEnrollmentService"; public const string CertificationAuthorityClass = "certificationAuthority"; public const string OIDContainerClass = "msPKI-Enterprise-Oid"; public const string GMSAClass = "msds-groupmanagedserviceaccount"; public const string MSAClass = "msds-managedserviceaccount"; -} \ No newline at end of file + public const string SiteClass = "site"; + public const string SiteServerClass = "server"; + public const string SiteSubnetClass = "subnet"; +} diff --git a/src/CommonLib/LdapProducerQueryGenerator.cs b/src/CommonLib/LdapProducerQueryGenerator.cs index 25c6cb9bf..a328ea0ea 100644 --- a/src/CommonLib/LdapProducerQueryGenerator.cs +++ b/src/CommonLib/LdapProducerQueryGenerator.cs @@ -15,7 +15,8 @@ public static GeneratedLdapParameters GenerateDefaultPartitionParameters(Collect if (methods.HasFlag(CollectionMethod.ObjectProps) || methods.HasFlag(CollectionMethod.ACL) || methods.HasFlag(CollectionMethod.Container)) { - filter = filter.AddComputers().AddDomains().AddUsers().AddContainers().AddGPOs().AddOUs().AddGroups(); + filter = filter.AddComputers().AddDomains().AddUsers().AddContainers().AddBuiltinDomains().AddGPOs() + .AddOUs().AddGroups(); if (methods.HasFlag(CollectionMethod.Container)) { properties.AddRange(CommonProperties.ContainerProps); @@ -103,10 +104,31 @@ public static GeneratedLdapParameters GenerateConfigurationPartitionParameters(C properties.AddRange(CommonProperties.BaseQueryProps); properties.AddRange(CommonProperties.TypeResolutionProps); - if (methods.HasFlag(CollectionMethod.ACL) || methods.HasFlag(CollectionMethod.ObjectProps) || - methods.HasFlag(CollectionMethod.Container) || methods.HasFlag(CollectionMethod.CertServices)) { - filter = filter.AddContainers().AddConfiguration().AddCertificateTemplates().AddCertificateAuthorities() - .AddEnterpriseCertificationAuthorities().AddIssuancePolicies(); + var collectBroadConfigObjects = methods.HasFlag(CollectionMethod.ACL) || + methods.HasFlag(CollectionMethod.ObjectProps) || + methods.HasFlag(CollectionMethod.Container); + + if (collectBroadConfigObjects || methods.HasFlag(CollectionMethod.CertServices) || + methods.HasFlag(CollectionMethod.Site)) { + filter = filter.AddContainers() + .AddConfiguration() + .AddSitesContainer(); + + if (collectBroadConfigObjects || methods.HasFlag(CollectionMethod.CertServices)) { + filter.AddCertificateTemplates() + .AddCertificateAuthorities() + .AddEnterpriseCertificationAuthorities() + .AddIssuancePolicies(); + } + else if (methods.HasFlag(CollectionMethod.CARegistry)) { + filter.AddEnterpriseCertificationAuthorities(); + } + + if (collectBroadConfigObjects || methods.HasFlag(CollectionMethod.Site)) { + filter.AddSites() + .AddSiteServers() + .AddSiteSubnets(); + } if (methods.HasFlag(CollectionMethod.ObjectProps)) { properties.AddRange(CommonProperties.ObjectPropsProps); @@ -131,6 +153,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() @@ -152,4 +181,4 @@ public static GeneratedLdapParameters GenerateConfigurationPartitionParameters(C public class GeneratedLdapParameters { public string[] Attributes { get; set; } public LdapFilter Filter { get; set; } -} \ No newline at end of file +} diff --git a/src/CommonLib/LdapQueries/CommonProperties.cs b/src/CommonLib/LdapQueries/CommonProperties.cs index 508b5490c..f9a8e05c7 100644 --- a/src/CommonLib/LdapQueries/CommonProperties.cs +++ b/src/CommonLib/LdapQueries/CommonProperties.cs @@ -9,7 +9,8 @@ public static class CommonProperties LDAPProperties.Flags }; - public static readonly string[] ObjectID = { LDAPProperties.ObjectSID, LDAPProperties.ObjectGUID }; + public static readonly string[] ObjectID = + { LDAPProperties.ObjectSID, LDAPProperties.ObjectGUID, LDAPProperties.ObjectClass }; public static readonly string[] ObjectSID = { LDAPProperties.ObjectSID }; public static readonly string[] GPCFileSysPath = { LDAPProperties.GPCFileSYSPath }; @@ -98,5 +99,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..b700b2c0f 100644 --- a/src/CommonLib/LdapQueries/LdapFilter.cs +++ b/src/CommonLib/LdapQueries/LdapFilter.cs @@ -129,6 +129,17 @@ public LdapFilter AddContainers(params string[] conditions) { return this; } + /// + /// Add a filter that will include Builtin domain container objects. + /// + /// + /// + public LdapFilter AddBuiltinDomains(params string[] conditions) { + _filterParts.Add(BuildString("(objectClass=builtinDomain)", conditions)); + + return this; + } + /// /// Add a filter that will include Configuration objects /// @@ -140,6 +151,17 @@ public LdapFilter AddConfiguration(params string[] conditions) { return this; } + /// + /// Add a filter that will include Sites container objects. + /// + /// + /// + public LdapFilter AddSitesContainer(params string[] conditions) { + _filterParts.Add(BuildString("(objectClass=sitesContainer)", conditions)); + + return this; + } + /// /// Add a filter that will include Computer objects /// @@ -215,6 +237,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 /// @@ -274,4 +335,4 @@ public IEnumerable GetFilterList() { } } } -} \ No newline at end of file +} diff --git a/src/CommonLib/LdapUtils.cs b/src/CommonLib/LdapUtils.cs index 0745af9e8..037e1ac5c 100644 --- a/src/CommonLib/LdapUtils.cs +++ b/src/CommonLib/LdapUtils.cs @@ -1197,6 +1197,10 @@ internal static bool ResolveLabel(string objectIdentifier, string distinguishedN type = Label.Container; else if (objectClasses.Contains(ObjectClass.ConfigurationClass, StringComparer.OrdinalIgnoreCase)) type = Label.Configuration; + else if (objectClasses.Contains(ObjectClass.BuiltinDomainClass, StringComparer.OrdinalIgnoreCase)) + type = Label.Container; + else if (objectClasses.Contains(ObjectClass.SitesContainerClass, StringComparer.OrdinalIgnoreCase)) + type = Label.Container; else if (objectClasses.Contains(ObjectClass.PKICertificateTemplateClass, StringComparer.OrdinalIgnoreCase)) type = Label.CertTemplate; else if (objectClasses.Contains(ObjectClass.PKIEnrollmentServiceClass, StringComparer.OrdinalIgnoreCase)) @@ -1219,6 +1223,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; } @@ -1228,7 +1244,7 @@ internal static bool ResolveLabel(string objectIdentifier, string distinguishedN if (!directoryObject.GetObjectIdentifier(out var objectIdentifier)) { return (false, default); } - + var res = new ResolvedSearchResult { ObjectId = objectIdentifier }; @@ -1284,11 +1300,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); @@ -1409,6 +1424,46 @@ 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: + { + if (directoryObject.TryGetProperty(LDAPProperties.DNSHostName, out var dnsHostName) && + !string.IsNullOrWhiteSpace(dnsHostName)) + { + displayName = dnsHostName; + } + else if (directoryObject.TryGetProperty(LDAPProperties.Name, out var name)) + { + displayName = $"{name}@{domain}"; + } + else + { + displayName = $"UNKNOWN@{domain}"; + } + break; + } + case Label.SiteSubnet: + { + if (directoryObject.TryGetProperty(LDAPProperties.Name, out var name)) + { + displayName = $"{name}@{domain}"; + } + else + { + displayName = $"UNKNOWN@{domain}"; + } break; } default: @@ -1418,4 +1473,4 @@ private static string ComputeDisplayName(IDirectoryObject directoryObject, strin return displayName.ToUpper(); } } -} \ No newline at end of file +} diff --git a/src/CommonLib/OutputTypes/Site.cs b/src/CommonLib/OutputTypes/Site.cs new file mode 100644 index 000000000..b18976063 --- /dev/null +++ b/src/CommonLib/OutputTypes/Site.cs @@ -0,0 +1,11 @@ +using System; + +namespace SharpHoundCommonLib.OutputTypes +{ + public class Site : OutputBase + { + public TypedPrincipal[] ChildObjects { get; set; } = Array.Empty(); + public GPLink[] Links { get; set; } = Array.Empty(); + public string[] InheritanceHashes { get; set; } = Array.Empty(); + } +} diff --git a/src/CommonLib/OutputTypes/SiteServer.cs b/src/CommonLib/OutputTypes/SiteServer.cs new file mode 100644 index 000000000..5a82cfc50 --- /dev/null +++ b/src/CommonLib/OutputTypes/SiteServer.cs @@ -0,0 +1,7 @@ +namespace SharpHoundCommonLib.OutputTypes +{ + public class SiteServer : OutputBase + { + public TypedPrincipal ServerIs { get; set; } + } +} 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 119a66791..1488a4938 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" } }; } @@ -449,6 +452,11 @@ public IAsyncEnumerable ProcessACL(byte[] ntSecurityDescriptor, string obje public async IAsyncEnumerable ProcessACL(byte[] ntSecurityDescriptor, string objectDomain, Label objectType, bool hasLaps, bool checkForOwnerRights, string objectName) { + if (objectType is Label.SiteServer or Label.SiteSubnet) { + _log.LogDebug("Skipping ACL processing for {ObjectType} object {ObjectName}", objectType, objectName); + yield break; + } + await BuildGuidCache(objectDomain); if (ntSecurityDescriptor == null) { @@ -734,7 +742,8 @@ or Label.RootCA or Label.EnterpriseCA or Label.AIACA or Label.NTAuthStore - or Label.IssuancePolicy) + or Label.IssuancePolicy + or Label.Site) if (aceType is ACEGuids.AllGuid or "") yield return new ACE { PrincipalType = resolvedPrincipal.ObjectType, @@ -776,7 +785,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/ContainerProcessor.cs b/src/CommonLib/Processors/ContainerProcessor.cs index 4539d7217..298e0fd29 100644 --- a/src/CommonLib/Processors/ContainerProcessor.cs +++ b/src/CommonLib/Processors/ContainerProcessor.cs @@ -57,18 +57,6 @@ private static bool IsDistinguishedNameFiltered(string distinguishedName) { var containerDn = Helpers.RemoveDistinguishedNamePrefix(distinguishedName); - //If the container is the builtin container, we want to redirect the containing object to the domain of the object - if (containerDn.StartsWith("CN=BUILTIN", StringComparison.OrdinalIgnoreCase)) - { - //This is always safe - var domain = Helpers.DistinguishedNameToDomain(distinguishedName); - if (await _utils.GetDomainSidFromDomainName(domain) is (true, var domainSid)) { - return (true, new TypedPrincipal(domainSid, Label.Domain)); - } - - return (false, default); - } - return await _utils.ResolveDistinguishedName(containerDn); } @@ -97,7 +85,8 @@ public IAsyncEnumerable GetContainerChildObjects(ResolvedSearchR /// public async IAsyncEnumerable GetContainerChildObjects(string distinguishedName, string containerName = "") { - var filter = new LdapFilter().AddComputers().AddUsers().AddGroups().AddOUs().AddContainers(); + var filter = new LdapFilter().AddComputers().AddUsers().AddGroups().AddOUs().AddContainers() + .AddBuiltinDomains().AddSitesContainer(); filter.AddCertificateAuthorities().AddCertificateTemplates().AddEnterpriseCertificationAuthorities(); await foreach (var childEntryResult in _utils.Query(new LdapQueryParameters { DomainName = Helpers.DistinguishedNameToDomain(distinguishedName), @@ -175,4 +164,4 @@ public static bool ReadBlocksInheritance(string gpOptions) return gpOptions is "1"; } } -} \ No newline at end of file +} diff --git a/src/CommonLib/Processors/LdapPropertyProcessor.cs b/src/CommonLib/Processors/LdapPropertyProcessor.cs index 2625b62f0..380bb0935 100644 --- a/src/CommonLib/Processors/LdapPropertyProcessor.cs +++ b/src/CommonLib/Processors/LdapPropertyProcessor.cs @@ -45,6 +45,9 @@ public LdapPropertyProcessor(ILdapUtils utils, ILogger log = null) { private static Dictionary GetCommonProps(IDirectoryObject entry) { var ret = new Dictionary(); + entry.TryGetArrayProperty(LDAPProperties.ObjectClass, out var objectClasses); + ret["objectClass"] = objectClasses; + if (entry.TryGetProperty(LDAPProperties.Description, out var description)) { ret["description"] = description; } @@ -648,6 +651,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..f485764d0 --- /dev/null +++ b/src/CommonLib/Processors/SiteProcessor.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using SharpHoundCommonLib.Enums; +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)) + { + if (siteObject == null) + return (false, default); + + var siteObjectDn = siteObject.ToString(); + if (string.IsNullOrWhiteSpace(siteObjectDn)) + return (false, default); + + return await GetContainingSiteForSubnet(siteObjectDn); + } + return (false, default); + } + + public async Task<(bool Success, TypedPrincipal principal)> GetReferencedComputerForServer(IDirectoryObject entry) + { + if (entry.TryGetProperty(LDAPProperties.ServerReference, out var serverReference)) + { + return await GetReferencedComputerForServer(serverReference); + } + + return (false, default); + } + + public async Task<(bool Success, TypedPrincipal principal)> GetReferencedComputerForServer(Dictionary serverProperties) + { + if (!serverProperties.TryGetValue(LDAPProperties.ServerReference, out var serverReference) || + serverReference == null) + { + return (false, default); + } + + return await GetReferencedComputerForServer(serverReference.ToString()); + } + + /// + /// 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 async Task<(bool Success, TypedPrincipal Principal)> GetReferencedComputerForServer(string serverReference) + { + if (string.IsNullOrWhiteSpace(serverReference)) + { + return (false, default); + } + + var resolved = await _utils.ResolveDistinguishedName(serverReference); + if (!resolved.Success || resolved.Principal == null || resolved.Principal.ObjectType != Label.Computer) + { + return (false, default); + } + + return resolved; + } + + 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 + }; + } + } + } + } +} diff --git a/test/unit/ACLProcessorTest.cs b/test/unit/ACLProcessorTest.cs index a8e4d3b33..bb5a3a371 100644 --- a/test/unit/ACLProcessorTest.cs +++ b/test/unit/ACLProcessorTest.cs @@ -230,6 +230,24 @@ public async Task ACLProcessor_ProcessACL_Null_NTSecurityDescriptor() Assert.Empty(result); } + [Theory] + [InlineData(Label.SiteServer)] + [InlineData(Label.SiteSubnet)] + public async Task ACLProcessor_ProcessACL_SiteServerAndSiteSubnet_ReturnsNothing(Label objectType) + { + var mockLDAPUtils = new Mock(); + var processor = new ACLProcessor(mockLDAPUtils.Object); + var bytes = Utils.B64ToBytes(UnProtectedUserNtSecurityDescriptor); + + var result = await processor.ProcessACL(bytes, _testDomainName, objectType, false).ToArrayAsync(); + + Assert.Empty(result); + mockLDAPUtils.Verify(x => x.PagedQuery(It.IsAny(), It.IsAny()), + Times.Never); + mockLDAPUtils.Verify(x => x.MakeSecurityDescriptor(), Times.Never); + mockLDAPUtils.Verify(x => x.ResolveIDAndType(It.IsAny(), It.IsAny()), Times.Never); + } + [Fact] public async Task ACLProcessor_ProcessACL_Yields_Owns_ACE() { var expectedSID = "S-1-5-21-3130019616-2776909439-2417379446-512"; @@ -1764,6 +1782,45 @@ public async Task ACLProcessor_ProcessACL_GenericWrite_OU_WriteGPLink() Assert.Equal(expectedRightName, actual.RightName); } + [Fact] + public async Task ACLProcessor_ProcessACL_GenericWrite_Site_WriteGPLink() + { + var expectedPrincipalType = Label.Group; + var expectedPrincipalSID = "S-1-5-21-3130019616-2776909439-2417379446-512"; + var expectedRightName = EdgeNames.WriteGPLink; + + var mockLDAPUtils = new Mock(); + var mockSecurityDescriptor = new Mock(MockBehavior.Loose, null); + var mockRule = new Mock(MockBehavior.Loose, null); + var collection = new List(); + mockRule.Setup(x => x.AccessControlType()).Returns(AccessControlType.Allow); + mockRule.Setup(x => x.IsAceInheritedFrom("bf967ab3-0de6-11d0-a285-00aa003049e2")).Returns(true); + mockRule.Setup(x => x.IdentityReference()).Returns(expectedPrincipalSID); + mockRule.Setup(x => x.ActiveDirectoryRights()).Returns(ActiveDirectoryRights.GenericWrite); + mockRule.Setup(x => x.ObjectType()).Returns(new Guid(ACEGuids.WriteGPLink)); + collection.Add(mockRule.Object); + + mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(collection); + mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null); + mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object); + mockLDAPUtils.Setup(x => x.ResolveIDAndType(It.IsAny(), It.IsAny())) + .ReturnsAsync((true, new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType))); + mockLDAPUtils.Setup(x => x.PagedQuery(It.IsAny(), It.IsAny())) + .Returns(Array.Empty>().ToAsyncEnumerable); + + var processor = new ACLProcessor(mockLDAPUtils.Object); + var bytes = Utils.B64ToBytes(UnProtectedUserNtSecurityDescriptor); + var result = await processor.ProcessACL(bytes, _testDomainName, Label.Site, true).ToArrayAsync(); + + Assert.Single(result); + var actual = result.First(); + Assert.Equal(expectedPrincipalType, actual.PrincipalType); + Assert.Equal(expectedPrincipalSID, actual.PrincipalSID); + Assert.False(actual.IsInherited); + Assert.Equal(expectedRightName, actual.RightName); + } + [Fact] public async Task ACLProcessor_ProcessACL_GenericWrite_User_AddKeyPrincipal() { @@ -2289,4 +2346,4 @@ public async Task ACLProcessor_ProcessACL_GenericWrite_Computer_WritePublicInfor Assert.Equal(actual.RightName, expectedRightName); } } -} \ No newline at end of file +} diff --git a/test/unit/ContainerProcessorTest.cs b/test/unit/ContainerProcessorTest.cs index a4111bca1..f500b4acf 100644 --- a/test/unit/ContainerProcessorTest.cs +++ b/test/unit/ContainerProcessorTest.cs @@ -120,6 +120,24 @@ public async Task ContainerProcessor_GetContainerChildObjects_ReturnsCorrectData } + [Fact] + public async Task ContainerProcessor_GetContainerChildObjects_QueryIncludesAdditionalContainerClasses() + { + var mock = new Mock(); + LdapQueryParameters queryParameters = null; + mock.Setup(x => x.Query(It.IsAny(), It.IsAny())) + .Callback((parameters, _) => queryParameters = parameters) + .Returns(Array.Empty>().ToAsyncEnumerable); + + var processor = new ContainerProcessor(mock.Object); + + await processor.GetContainerChildObjects("DC=testlab,DC=local").ToArrayAsync(); + + Assert.NotNull(queryParameters); + Assert.Contains("(objectClass=builtinDomain)", queryParameters.LDAPFilter); + Assert.Contains("(objectClass=sitesContainer)", queryParameters.LDAPFilter); + } + [Fact] public void ContainerProcessor_ReadBlocksInheritance_ReturnsCorrectValues() { @@ -149,8 +167,8 @@ public async Task ContainerProcessor_GetContainingObject_ExpectedResult() Assert.True(success); (success, result) = await proc.GetContainingObject("CN=ADMINISTRATORS,CN=BUILTIN,DC=TESTLAB,DC=LOCAL"); - Assert.Equal(Label.Domain, result.ObjectType); - Assert.Equal("S-1-5-21-3130019616-2776909439-2417379446", result.ObjectIdentifier); + Assert.Equal(Label.Container, result.ObjectType); + Assert.Equal(MockLdapUtils.BuiltinContainerGuid, result.ObjectIdentifier); Assert.True(success); } @@ -164,4 +182,4 @@ public async Task ContainerProcessor_GetContainingObject_BadDN_ReturnsNull() Assert.False(success); } } -} \ No newline at end of file +} diff --git a/test/unit/DirectoryObjectTests.cs b/test/unit/DirectoryObjectTests.cs index 44966a57d..0940361a3 100644 --- a/test/unit/DirectoryObjectTests.cs +++ b/test/unit/DirectoryObjectTests.cs @@ -140,6 +140,20 @@ public void Test_GetLabel_WellKnownAdministratorsObject() { Assert.Equal(Label.Group, label); } + [Fact] + public void Test_GetObjectIdentifier_BuiltinContainer_UsesGuid() { + var expectedGuid = Guid.NewGuid().ToString().ToUpper(); + var attribs = new Dictionary { + { LDAPProperties.ObjectClass, new[] { "top", ObjectClass.BuiltinDomainClass } }, + }; + + var mock = new MockDirectoryObject("CN=BuiltIn,DC=Testlab,DC=Local", attribs, + "S-1-5-32", expectedGuid); + + Assert.True(mock.GetObjectIdentifier(out var objectIdentifier)); + Assert.Equal(expectedGuid, objectIdentifier); + } + [Fact] public void Test_GetLabel_Computer_Objects() { var attribs = new Dictionary { @@ -255,6 +269,21 @@ public void Test_GetLabel_ConfigurationObjects() { Assert.Equal(Label.Configuration, label); } + [Theory] + [InlineData(ObjectClass.BuiltinDomainClass)] + [InlineData(ObjectClass.SitesContainerClass)] + public void Test_GetLabel_AdditionalContainerClasses(string objectClass) { + var attribs = new Dictionary { + { LDAPProperties.ObjectClass, new[] { "top", objectClass } }, + }; + + var mock = new MockDirectoryObject("abc", attribs, + "123456", new Guid().ToString()); + + Assert.True(mock.GetLabel(out var label)); + Assert.Equal(Label.Container, label); + } + [Fact] public void Test_GetLabel_CertTemplateObjects() { var attribs = new Dictionary { @@ -299,6 +328,24 @@ public void Test_GetLabel_CertificationAuthorityObjects() { Assert.True(mock.GetLabel(out label)); Assert.Equal(Label.NTAuthStore, label); } + + [Theory] + [InlineData(ObjectClass.SiteClass, Label.Site)] + [InlineData(ObjectClass.SiteServerClass, Label.SiteServer)] + [InlineData(ObjectClass.SiteSubnetClass, Label.SiteSubnet)] + public void Test_GetLabel_SiteObjects(string objectClass, Label expectedLabel) { + var attribs = new Dictionary { + { LDAPProperties.ObjectClass, new[] { "top", objectClass } }, + }; + + var mock = new MockDirectoryObject("CN=Test,CN=Sites,CN=Configuration,DC=Testlab,DC=local", + attribs, + "", + new Guid().ToString()); + + Assert.True(mock.GetLabel(out var label)); + Assert.Equal(expectedLabel, label); + } [Fact] public void Test_GetLabel_NTAuthCertificateObject() { @@ -330,4 +377,4 @@ public void Test_GetLabel_NoLabel() { Assert.Equal(Label.Base, label); } } -} \ No newline at end of file +} diff --git a/test/unit/Facades/MockDirectoryObject.cs b/test/unit/Facades/MockDirectoryObject.cs index 131b890af..8e78b6e0b 100644 --- a/test/unit/Facades/MockDirectoryObject.cs +++ b/test/unit/Facades/MockDirectoryObject.cs @@ -16,7 +16,7 @@ public class MockDirectoryObject : IDirectoryObject { public MockDirectoryObject(string distinguishedName, IDictionary properties, string sid = "", string guid = "") { DistinguishedName = distinguishedName; - Properties = properties; + Properties = properties ?? new Dictionary(); _objectSID = sid; _objectGuid = guid; } @@ -176,4 +176,4 @@ public int PropertyCount(string propertyName) { public IEnumerable PropertyNames() { foreach (var property in Properties.Keys) yield return property.ToString().ToLower(); } -} \ No newline at end of file +} diff --git a/test/unit/Facades/MockLdapUtils.cs b/test/unit/Facades/MockLdapUtils.cs index f60608d16..ca74bb598 100644 --- a/test/unit/Facades/MockLdapUtils.cs +++ b/test/unit/Facades/MockLdapUtils.cs @@ -20,6 +20,7 @@ namespace CommonLibTest.Facades [SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")] public class MockLdapUtils : ILdapUtils { + public const string BuiltinContainerGuid = "C8D73525-F9B2-4F68-B1D3-9457896B3C80"; private readonly ConcurrentDictionary _domainControllers = new(); private readonly Forest _forest; private readonly ConcurrentDictionary _seenWellKnownPrincipals = new(); @@ -58,6 +59,7 @@ public virtual IAsyncEnumerable> RangedRetrieval(string distingui "S-1-5-21-3130019616-2776909439-2417379446-512", Label.Group), "S-1-5-21-3130019616-2776909439-2417379446-2606" => new TypedPrincipal( "S-1-5-21-3130019616-2776909439-2417379446-2606", Label.User), + BuiltinContainerGuid => new TypedPrincipal(BuiltinContainerGuid, Label.Container), "E32A6AC7-083B-4DD7-ACFF-6D9C2B1AFAF5" => new TypedPrincipal("E32A6AC7-083B-4DD7-ACFF-6D9C2B1AFAF5", Label.Container), "S-1-5-21-3130019616-2776909439-2417379446-519" => new TypedPrincipal( @@ -431,6 +433,7 @@ public virtual IAsyncEnumerable> RangedRetrieval(string distingui "S-1-5-21-3130019616-2776909439-2417379446-2105", Label.Computer), "S-1-5-21-3130019616-2776909439-2417379446-2120" => new TypedPrincipal( "S-1-5-21-3130019616-2776909439-2417379446-2120", Label.Computer), + "CN=BUILTIN,DC=TESTLAB,DC=LOCAL" => new TypedPrincipal(BuiltinContainerGuid, Label.Container), "CN=REPLICATOR,CN=BUILTIN,DC=TESTLAB,DC=LOCAL" => new TypedPrincipal("TESTLAB.LOCAL-S-1-5-32-552", Label.Group), "CN=PRINT OPERATORS,CN=BUILTIN,DC=TESTLAB,DC=LOCAL" => new TypedPrincipal("TESTLAB.LOCAL-S-1-5-32-550", @@ -762,6 +765,7 @@ public async Task ConvertWellKnownPrincipal(string sid, string domain) public async Task<(bool Success, TypedPrincipal Principal)> ResolveDistinguishedName(string distinguishedName) { var result = distinguishedName.ToUpper() switch { + "CN=BUILTIN,DC=TESTLAB,DC=LOCAL" => new TypedPrincipal(BuiltinContainerGuid, Label.Container), "CN=REPLICATOR,CN=BUILTIN,DC=TESTLAB,DC=LOCAL" => new TypedPrincipal("TESTLAB.LOCAL-S-1-5-32-552", Label.Group), "CN=PRINT OPERATORS,CN=BUILTIN,DC=TESTLAB,DC=LOCAL" => new TypedPrincipal("TESTLAB.LOCAL-S-1-5-32-550", @@ -1127,4 +1131,4 @@ public void Dispose() { return (true, "0"); } } -} \ No newline at end of file +} diff --git a/test/unit/LDAPFilterTest.cs b/test/unit/LDAPFilterTest.cs index 3c5f86d07..e02b5f829 100644 --- a/test/unit/LDAPFilterTest.cs +++ b/test/unit/LDAPFilterTest.cs @@ -106,6 +106,46 @@ public void LDAPFilter_GetFilterList_MergeFilter() Assert.Equal(2, filters.Count); } + [Theory] + [InlineData("site", "(objectClass=site)", "(&(objectClass=site)(name=Test))")] + [InlineData("server", "(objectClass=server)", "(&(objectClass=server)(name=Test))")] + [InlineData("subnet", "(objectClass=subnet)", "(&(objectClass=subnet)(name=Test))")] + [InlineData("sitesContainer", "(objectClass=sitesContainer)", "(&(objectClass=sitesContainer)(name=Test))")] + public void LDAPFilter_SiteFilters_FilterCorrect(string objectClass, string expectedFilter, + string expectedFilterWithCondition) + { + var test = objectClass switch + { + "site" => new LdapFilter().AddSites(), + "server" => new LdapFilter().AddSiteServers(), + "subnet" => new LdapFilter().AddSiteSubnets(), + "sitesContainer" => new LdapFilter().AddSitesContainer(), + _ => throw new ArgumentOutOfRangeException(nameof(objectClass)) + }; + + var testWithCondition = objectClass switch + { + "site" => new LdapFilter().AddSites("name=Test"), + "server" => new LdapFilter().AddSiteServers("name=Test"), + "subnet" => new LdapFilter().AddSiteSubnets("name=Test"), + "sitesContainer" => new LdapFilter().AddSitesContainer("name=Test"), + _ => throw new ArgumentOutOfRangeException(nameof(objectClass)) + }; + + Assert.Equal(expectedFilter, test.GetFilter()); + Assert.Equal(expectedFilterWithCondition, testWithCondition.GetFilter()); + } + + [Fact] + public void LDAPFilter_BuiltinDomain_FilterCorrect() + { + var test = new LdapFilter().AddBuiltinDomains(); + var testWithCondition = new LdapFilter().AddBuiltinDomains("name=Builtin"); + + Assert.Equal("(objectClass=builtinDomain)", test.GetFilter()); + Assert.Equal("(&(objectClass=builtinDomain)(name=Builtin))", testWithCondition.GetFilter()); + } + #endregion } -} \ No newline at end of file +} diff --git a/test/unit/LDAPUtilsTest.cs b/test/unit/LDAPUtilsTest.cs index 33ea5d067..906318a1a 100644 --- a/test/unit/LDAPUtilsTest.cs +++ b/test/unit/LDAPUtilsTest.cs @@ -249,6 +249,38 @@ public async Task Test_ResolveSearchResult_TrustAccount() { Assert.False(result.Deleted); } + [Theory] + [InlineData(ObjectClass.SiteClass, Label.Site, + "CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=TESTLAB,DC=LOCAL", + "Default-First-Site-Name", "DEFAULT-FIRST-SITE-NAME@TESTLAB.LOCAL")] + [InlineData(ObjectClass.SiteServerClass, Label.SiteServer, + "CN=PRIMARY,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=TESTLAB,DC=LOCAL", + "primary.testlab.local", "PRIMARY.TESTLAB.LOCAL@TESTLAB.LOCAL")] + [InlineData(ObjectClass.SiteSubnetClass, Label.SiteSubnet, + "CN=10.0.0.0/24,CN=Subnets,CN=Sites,CN=Configuration,DC=TESTLAB,DC=LOCAL", + "10.0.0.0/24", "10.0.0.0/24@TESTLAB.LOCAL")] + public async Task Test_ResolveSearchResult_SiteObjects(string objectClass, Label expectedLabel, + string distinguishedName, string name, string expectedDisplayName) { + var utils = new MockLdapUtils(); + var guid = new Guid().ToString(); + var attribs = new Dictionary { + { LDAPProperties.ObjectClass, new[] { "top", objectClass } }, + { LDAPProperties.Name, name } + }; + + var mock = new MockDirectoryObject(distinguishedName, attribs, "", guid); + + var (success, result) = await LdapUtils.ResolveSearchResult(mock, utils); + + Assert.True(success); + Assert.Equal(guid, result.ObjectId); + Assert.Equal(expectedLabel, result.ObjectType); + Assert.Equal(expectedDisplayName, result.DisplayName); + Assert.Equal("S-1-5-21-3130019616-2776909439-2417379446", result.DomainSid); + Assert.Equal("TESTLAB.LOCAL", result.Domain); + Assert.False(result.Deleted); + } + [Fact] public async Task Test_ResolveHostToSid_BlankHost() { var spn = "MSSQLSvc/:1433"; @@ -287,4 +319,4 @@ public async Task EnterpriseDomainControllersGroup_CorrectValues() { Assert.Equal(3, entDCGroup.Members.Length); } } -} \ No newline at end of file +} diff --git a/test/unit/LdapProducerQueryGeneratorTest.cs b/test/unit/LdapProducerQueryGeneratorTest.cs new file mode 100644 index 000000000..455f13385 --- /dev/null +++ b/test/unit/LdapProducerQueryGeneratorTest.cs @@ -0,0 +1,98 @@ +using System.Linq; +using SharpHoundCommonLib; +using SharpHoundCommonLib.Enums; +using SharpHoundCommonLib.LDAPQueries; +using Xunit; + +namespace CommonLibTest; + +public class LdapProducerQueryGeneratorTest +{ + [Fact] + public void GenerateDefaultPartitionParameters_Container_IncludesBuiltinDomainFilter() + { + var expectedFilter = new LdapFilter() + .AddComputers() + .AddDomains() + .AddUsers() + .AddContainers() + .AddBuiltinDomains() + .AddGPOs() + .AddOUs() + .AddGroups() + .GetFilter(); + + var result = LdapProducerQueryGenerator.GenerateDefaultPartitionParameters(CollectionMethod.Container); + + Assert.Equal(expectedFilter, result.Filter.GetFilter()); + Assert.Contains("(objectClass=builtinDomain)", result.Filter.GetFilter()); + } + + [Fact] + public void GenerateConfigurationPartitionParameters_Site_IncludesSiteFiltersAndProperties() + { + var expectedFilter = new LdapFilter() + .AddContainers() + .AddConfiguration() + .AddSitesContainer() + .AddSites() + .AddSiteServers() + .AddSiteSubnets() + .GetFilter(); + + var result = LdapProducerQueryGenerator.GenerateConfigurationPartitionParameters(CollectionMethod.Site); + + Assert.Equal(expectedFilter, result.Filter.GetFilter()); + Assert.All(CommonProperties.SiteProps.Concat(CommonProperties.SiteServerProps).Concat(CommonProperties.SiteSubnetProps), + attribute => Assert.Contains(attribute, result.Attributes)); + Assert.DoesNotContain("(objectclass=pKICertificateTemplate)", result.Filter.GetFilter()); + Assert.DoesNotContain("(objectClass=certificationAuthority)", result.Filter.GetFilter()); + Assert.DoesNotContain("(objectCategory=pKIEnrollmentService)", result.Filter.GetFilter()); + Assert.DoesNotContain("(objectClass=msPKI-Enterprise-Oid)", result.Filter.GetFilter()); + } + + [Fact] + public void GenerateConfigurationPartitionParameters_CertServices_IncludesCertFiltersAndProperties() + { + var expectedFilter = new LdapFilter() + .AddContainers() + .AddConfiguration() + .AddSitesContainer() + .AddCertificateTemplates() + .AddCertificateAuthorities() + .AddEnterpriseCertificationAuthorities() + .AddIssuancePolicies() + .GetFilter(); + + var result = LdapProducerQueryGenerator.GenerateConfigurationPartitionParameters(CollectionMethod.CertServices); + + Assert.Equal(expectedFilter, result.Filter.GetFilter()); + Assert.All(CommonProperties.CertAbuseProps, attribute => Assert.Contains(attribute, result.Attributes)); + Assert.DoesNotContain(LDAPProperties.ServerReference, result.Attributes); + Assert.DoesNotContain("(objectClass=site)", result.Filter.GetFilter()); + Assert.DoesNotContain("(objectClass=server)", result.Filter.GetFilter()); + Assert.DoesNotContain("(objectClass=subnet)", result.Filter.GetFilter()); + } + + [Fact] + public void GenerateConfigurationPartitionParameters_SiteAndCertServices_IncludesBothFilterSets() + { + var expectedFilter = new LdapFilter() + .AddContainers() + .AddConfiguration() + .AddSitesContainer() + .AddCertificateTemplates() + .AddCertificateAuthorities() + .AddEnterpriseCertificationAuthorities() + .AddIssuancePolicies() + .AddSites() + .AddSiteServers() + .AddSiteSubnets() + .GetFilter(); + + var result = LdapProducerQueryGenerator.GenerateConfigurationPartitionParameters( + CollectionMethod.Site | CollectionMethod.CertServices); + + Assert.Equal(expectedFilter, result.Filter.GetFilter()); + } +} diff --git a/test/unit/LdapPropertyTests.cs b/test/unit/LdapPropertyTests.cs index 9d3053d13..274794f72 100644 --- a/test/unit/LdapPropertyTests.cs +++ b/test/unit/LdapPropertyTests.cs @@ -119,6 +119,34 @@ public void LDAPPropertyProcessor_ReadOUProperties_TestGoodData() Assert.Equal("Test", test["description"] as string); } + [Fact] + public void LDAPPropertyProcessor_ReadContainerProperties_IncludesObjectClass() + { + var objectClasses = new[] { "top", ObjectClass.ContainerClass }; + var mock = new MockDirectoryObject("CN=Users,DC=testlab,DC=local", + new Dictionary + { + {LDAPProperties.ObjectClass, objectClasses} + }, "", "ECAD920E-8EB1-4E31-A80E-DD36367F81F4"); + + var test = LdapPropertyProcessor.ReadContainerProperties(mock); + + Assert.True(test.TryGetValue("objectClass", out var actual)); + Assert.Equal(objectClasses, Assert.IsType(actual)); + } + + [Fact] + public void LDAPPropertyProcessor_ReadOUProperties_ObjectClassDefaultsToEmptyArray() + { + var mock = new MockDirectoryObject("OU=TestOU,DC=testlab,DC=local", + new Dictionary(), "", "2A374493-816A-4193-BEFD-D2F4132C6DCA"); + + var test = LdapPropertyProcessor.ReadOUProperties(mock); + + Assert.True(test.TryGetValue("objectClass", out var actual)); + Assert.Empty(Assert.IsType(actual)); + } + [Fact] public async Task LDAPPropertyProcessor_ReadGroupProperties_TestGoodData() { @@ -1063,6 +1091,82 @@ public async Task LDAPPropertyProcessor_ReadIssuancePolicyProperties_NoOIDGroupL Assert.Contains("certtemplateoid", keys); } + [Fact] + public void LDAPPropertyProcessor_ReadSiteProperties() + { + var mock = new MockDirectoryObject("CN=DEFAULT-FIRST-SITE-NAME,CN=SITES,CN=CONFIGURATION,DC=TESTLAB,DC=LOCAL", + new Dictionary + { + {LDAPProperties.Description, "Default site"}, + {LDAPProperties.WhenCreated, 1712567279}, + {"domain", "TESTLAB.LOCAL"}, + {"name", "DEFAULT-FIRST-SITE-NAME@TESTLAB.LOCAL"}, + {"domainsid", "S-1-5-21-3130019616-2776909439-2417379446"} + }, "", "2F9F3630-F46A-49BF-B186-6629994EBCF9"); + + var test = LdapPropertyProcessor.ReadSiteProperties(mock); + var keys = test.Keys; + + Assert.DoesNotContain("domain", keys); + Assert.DoesNotContain("name", keys); + Assert.DoesNotContain("domainsid", keys); + Assert.Contains("description", keys); + Assert.Contains("whencreated", keys); + } + + [Fact] + public void LDAPPropertyProcessor_ReadSiteServerProperties() + { + var serverReference = "CN=PRIMARY,OU=DOMAIN CONTROLLERS,DC=TESTLAB,DC=LOCAL"; + var mock = new MockDirectoryObject("CN=PRIMARY,CN=SERVERS,CN=DEFAULT-FIRST-SITE-NAME,CN=SITES,CN=CONFIGURATION,DC=TESTLAB,DC=LOCAL", + new Dictionary + { + {LDAPProperties.Description, "Site server"}, + {LDAPProperties.WhenCreated, 1712567279}, + {LDAPProperties.DNSHostName, "primary.testlab.local"}, + {LDAPProperties.ServerReference, serverReference}, + {"domain", "TESTLAB.LOCAL"}, + {"name", "PRIMARY"} + }, "", "2F9F3630-F46A-49BF-B186-6629994EBCF9"); + + var test = LdapPropertyProcessor.ReadSiteServerProperties(mock); + var keys = test.Keys; + + Assert.DoesNotContain("domain", keys); + Assert.DoesNotContain("name", keys); + Assert.Contains("description", keys); + Assert.Contains("whencreated", keys); + Assert.Equal("primary.testlab.local", test["dnshostname"]); + Assert.Equal(serverReference, test["serverreference"]); + } + + [Fact] + public void LDAPPropertyProcessor_ReadSiteSubnetProperties() + { + var siteObject = "CN=DEFAULT-FIRST-SITE-NAME,CN=SITES,CN=CONFIGURATION,DC=TESTLAB,DC=LOCAL"; + var canonicalName = "TESTLAB.LOCAL/Configuration/Sites/Subnets/10.0.0.0/24"; + var mock = new MockDirectoryObject("CN=10.0.0.0/24,CN=SUBNETS,CN=SITES,CN=CONFIGURATION,DC=TESTLAB,DC=LOCAL", + new Dictionary + { + {LDAPProperties.Description, "Site subnet"}, + {LDAPProperties.WhenCreated, 1712567279}, + {LDAPProperties.CanonicalName, canonicalName}, + {LDAPProperties.SiteObject, siteObject}, + {"domain", "TESTLAB.LOCAL"}, + {"name", "10.0.0.0/24"} + }, "", "2F9F3630-F46A-49BF-B186-6629994EBCF9"); + + var test = LdapPropertyProcessor.ReadSiteSubnetProperties(mock); + var keys = test.Keys; + + Assert.DoesNotContain("domain", keys); + Assert.DoesNotContain("name", keys); + Assert.Contains("description", keys); + Assert.Contains("whencreated", keys); + Assert.Equal(canonicalName, test["cn"]); + Assert.Equal(siteObject, test["siteObject"]); + } + [Fact] public void LDAPPropertyProcessor_ParseAllProperties() { diff --git a/test/unit/SiteProcessorTest.cs b/test/unit/SiteProcessorTest.cs new file mode 100644 index 000000000..6b45195e3 --- /dev/null +++ b/test/unit/SiteProcessorTest.cs @@ -0,0 +1,220 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using CommonLibTest.Facades; +using Moq; +using SharpHoundCommonLib; +using SharpHoundCommonLib.Enums; +using SharpHoundCommonLib.OutputTypes; +using SharpHoundCommonLib.Processors; +using Xunit; + +namespace CommonLibTest +{ + public class SiteProcessorTest + { + private const string TestGPLinkString = + "[LDAP://cn={94DD0260-38B5-497E-8876-10E7A96E80D0},cn=policies,cn=system,DC=testlab,DC=local;0][LDAP://cn={C52F168C-CD05-4487-B405-564934DA8EFF},cn=policies,cn=system,DC=testlab,DC=local;2]"; + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task SiteProcessor_GetContainingSiteForSubnet_InvalidSiteObject_ReturnsFalse(string siteObject) + { + var utils = new Mock(MockBehavior.Strict); + var processor = new SiteProcessor(utils.Object); + + var (success, principal) = await processor.GetContainingSiteForSubnet(new Dictionary + { + ["siteObject"] = siteObject + }); + + Assert.False(success); + Assert.Null(principal); + utils.Verify(x => x.ResolveDistinguishedName(It.IsAny()), Times.Never); + } + + [Fact] + public async Task SiteProcessor_GetContainingSiteForSubnet_ValidSiteObject_ResolvesDistinguishedName() + { + const string siteObject = "CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=testlab,DC=local"; + var expected = new TypedPrincipal("TESTLAB.LOCAL-SITE", Label.Site); + var utils = new Mock(); + utils.Setup(x => x.ResolveDistinguishedName(siteObject)).ReturnsAsync((true, expected)); + var processor = new SiteProcessor(utils.Object); + + var (success, principal) = await processor.GetContainingSiteForSubnet(new Dictionary + { + ["siteObject"] = siteObject + }); + + Assert.True(success); + Assert.Equal(expected, principal); + utils.Verify(x => x.ResolveDistinguishedName(siteObject), Times.Once); + } + + [Fact] + public async Task SiteProcessor_GetContainingSiteForServer_NoDistinguishedName_ReturnsFalse() + { + var utils = new Mock(MockBehavior.Strict); + var processor = new SiteProcessor(utils.Object); + var entry = new MockDirectoryObject("", new Dictionary(), "", ""); + + var (success, principal) = await processor.GetContainingSiteForServer(entry); + + Assert.False(success); + Assert.Null(principal); + utils.Verify(x => x.ResolveDistinguishedName(It.IsAny()), Times.Never); + } + + [Fact] + public async Task SiteProcessor_GetContainingSiteForServer_ValidDistinguishedName_ResolvesParentSite() + { + const string serverDn = "CN=PRIMARY,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=testlab,DC=local"; + const string siteDn = "CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=testlab,DC=local"; + var expected = new TypedPrincipal("TESTLAB.LOCAL-SITE", Label.Site); + var utils = new Mock(); + utils.Setup(x => x.ResolveDistinguishedName(siteDn)).ReturnsAsync((true, expected)); + var processor = new SiteProcessor(utils.Object); + + var (success, principal) = await processor.GetContainingSiteForServer(serverDn); + + Assert.True(success); + Assert.Equal(expected, principal); + utils.Verify(x => x.ResolveDistinguishedName(siteDn), Times.Once); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task SiteProcessor_GetReferencedComputerForServer_InvalidServerReference_ReturnsFalse(string serverReference) + { + var utils = new Mock(MockBehavior.Strict); + var processor = new SiteProcessor(utils.Object); + + var (success, principal) = await processor.GetReferencedComputerForServer(new Dictionary + { + [LDAPProperties.ServerReference] = serverReference + }); + + Assert.False(success); + Assert.Null(principal); + utils.Verify(x => x.ResolveDistinguishedName(It.IsAny()), Times.Never); + } + + [Fact] + public async Task SiteProcessor_GetReferencedComputerForServer_ValidServerReference_ResolvesComputer() + { + const string serverReference = "CN=PRIMARY,OU=DOMAIN CONTROLLERS,DC=TESTLAB,DC=LOCAL"; + var expected = new TypedPrincipal("S-1-5-21-3130019616-2776909439-2417379446-1001", Label.Computer); + var utils = new Mock(); + utils.Setup(x => x.ResolveDistinguishedName(serverReference)).ReturnsAsync((true, expected)); + var processor = new SiteProcessor(utils.Object); + + var (success, principal) = await processor.GetReferencedComputerForServer(new Dictionary + { + [LDAPProperties.ServerReference] = serverReference + }); + + Assert.True(success); + Assert.Equal(expected, principal); + utils.Verify(x => x.ResolveDistinguishedName(serverReference), Times.Once); + } + + [Fact] + public async Task SiteProcessor_GetReferencedComputerForServer_NonComputerReference_ReturnsFalse() + { + const string serverReference = "CN=ADMINISTRATORS,CN=BUILTIN,DC=TESTLAB,DC=LOCAL"; + var utils = new Mock(); + utils.Setup(x => x.ResolveDistinguishedName(serverReference)) + .ReturnsAsync((true, new TypedPrincipal("TESTLAB.LOCAL-S-1-5-32-544", Label.Group))); + var processor = new SiteProcessor(utils.Object); + + var (success, principal) = await processor.GetReferencedComputerForServer(serverReference); + + Assert.False(success); + Assert.Null(principal); + utils.Verify(x => x.ResolveDistinguishedName(serverReference), Times.Once); + } + + [Fact] + public async Task SiteProcessor_GetReferencedComputerForServer_DirectoryObject_UsesServerReference() + { + const string serverReference = "CN=PRIMARY,OU=DOMAIN CONTROLLERS,DC=TESTLAB,DC=LOCAL"; + var processor = new SiteProcessor(new MockLdapUtils()); + var entry = new MockDirectoryObject("", new Dictionary + { + [LDAPProperties.ServerReference] = serverReference + }, "", ""); + + var (success, principal) = await processor.GetReferencedComputerForServer(entry); + + Assert.True(success); + Assert.Equal(new TypedPrincipal("S-1-5-21-3130019616-2776909439-2417379446-1001", Label.Computer), principal); + } + + [Fact] + public void Site_DefaultRelationships_AreEmpty() + { + var site = new Site(); + + Assert.Empty(site.ChildObjects); + Assert.Empty(site.Links); + Assert.Empty(site.InheritanceHashes); + } + + [Fact] + public void SiteServer_ServerIs_EdgeNameMatchesOutputProperty() + { + Assert.Equal(nameof(SiteServer.ServerIs), EdgeNames.ServerIs); + } + + [Fact] + public async Task SiteProcessor_ReadSiteGPLinks_IgnoresNull() + { + var processor = new SiteProcessor(new MockLdapUtils()); + + var test = await processor.ReadSiteGPLinks(null).ToArrayAsync(); + + Assert.Empty(test); + } + + [Fact] + public async Task SiteProcessor_ReadSiteGPLinks_UnresolvedGPLink_IsIgnored() + { + var processor = new SiteProcessor(new MockLdapUtils()); + const string gpLink = + "[LDAP://cn={94DD0260-38B5-497E-8876-ABCDEFG},cn=policies,cn=system,DC=testlab,DC=local;0]"; + + var test = await processor.ReadSiteGPLinks(gpLink).ToArrayAsync(); + + Assert.Empty(test); + } + + [Fact] + public async Task SiteProcessor_ReadSiteGPLinks_ReturnsCorrectValues() + { + var processor = new SiteProcessor(new MockLdapUtils()); + + var test = await processor.ReadSiteGPLinks(TestGPLinkString).ToArrayAsync(); + + var expected = new GPLink[] + { + new() + { + GUID = "B39818AF-6349-401A-AE0A-E4972F5BF6D9", + IsEnforced = false + }, + new() + { + GUID = "ACDD64D3-67B3-401F-A6CC-804B3F7B1533", + IsEnforced = true + } + }; + + Assert.Equal(2, test.Length); + Assert.Equal(expected, test); + } + } +}