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);
+ }
+ }
+}