diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index b75f52bd..a3a10778 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -8,7 +8,7 @@
## Build
-``` powershell
+```powershell
dotnet build
```
@@ -17,31 +17,30 @@ dotnet build
This project is configured to generate test coverage every time tests are run and produces a HTML report at
[./docfx/coverage/report](./docfx/coverage/report).
-
-``` powershell
+```powershell
dotnet test
```
## Documentation
-Documentation is generated into Html from Markdown using [docfx](https://https://dotnet.github.io/docfx/).
+Documentation is generated into HTML from Markdown using [docfx](https://dotnet.github.io/docfx/).
To build the docs:
-``` powershell
+```powershell
dotnet build docfx
```
To preview the docs:
-``` powershell
+```powershell
dotnet build docfx
dotnet build docfx -t:Serve
```
To preview the docs with test coverage:
-``` powershell
+```powershell
dotnet test
dotnet build docfx
dotnet build docfx -t:Serve
diff --git a/src/CommonLib/EdgeNames.cs b/src/CommonLib/EdgeNames.cs
index e0688a4a..276d5b00 100644
--- a/src/CommonLib/EdgeNames.cs
+++ b/src/CommonLib/EdgeNames.cs
@@ -21,5 +21,12 @@ public static class EdgeNames
public const string AddKeyCredentialLink = "AddKeyCredentialLink";
public const string SQLAdmin = "SQLAdmin";
public const string WriteAccountRestrictions = "WriteAccountRestrictions";
+
+ //CertAbuse edges
+ public const string WritePKIEnrollmentFlag = "WritePKIEnrollmentFlag";
+ public const string WritePKINameFlag = "WritePKINameFlag";
+ public const string ManageCA = "ManageCA";
+ public const string ManageCertificates = "ManageCertificates";
+ public const string Enroll = "Enroll";
}
}
\ No newline at end of file
diff --git a/src/CommonLib/Enums/CAExtensionTypes.cs b/src/CommonLib/Enums/CAExtensionTypes.cs
new file mode 100644
index 00000000..582c85e7
--- /dev/null
+++ b/src/CommonLib/Enums/CAExtensionTypes.cs
@@ -0,0 +1,15 @@
+namespace SharpHoundCommonLib.Enums
+{
+ // From https://learn.microsoft.com/en-us/windows/win32/seccertenroll/supported-extensions
+ public static class CAExtensionTypes
+ {
+ public const string AuthorityInformationAccess = "1.3.6.1.5.5.7.1.1";
+ public const string AuthorityKeyIdentifier = "2.5.29.35";
+ public const string BasicConstraints = "2.5.29.19";
+ public const string NameConstraints = "2.5.29.30";
+ public const string EnhancedKeyUsage = "2.5.29.37";
+ public const string KeyUsage = "2.5.29.15";
+ public const string SubjectAlternativeNames = "2.5.29.17";
+ public const string SubjectKeyIdentifier = "2.5.29.14";
+ }
+}
\ No newline at end of file
diff --git a/src/CommonLib/Enums/CertificationAuthorityRights.cs b/src/CommonLib/Enums/CertificationAuthorityRights.cs
new file mode 100644
index 00000000..30fd801b
--- /dev/null
+++ b/src/CommonLib/Enums/CertificationAuthorityRights.cs
@@ -0,0 +1,15 @@
+using System;
+
+namespace SharpHoundCommonLib.Enums
+{
+ [Flags]
+ public enum CertificationAuthorityRights
+ {
+ ManageCA = 1, // Administrator
+ ManageCertificates = 2, // Officer
+ Auditor = 4,
+ Operator = 8,
+ Read = 256,
+ Enroll = 512
+ }
+}
\ No newline at end of file
diff --git a/src/CommonLib/Enums/CollectionMethods.cs b/src/CommonLib/Enums/CollectionMethods.cs
index 3c9bdb2c..d4ebf61e 100644
--- a/src/CommonLib/Enums/CollectionMethods.cs
+++ b/src/CommonLib/Enums/CollectionMethods.cs
@@ -22,10 +22,13 @@ public enum ResolvedCollectionMethod
SPNTargets = 1 << 13,
PSRemote = 1 << 14,
UserRights = 1 << 15,
+ CARegistry = 1 << 16,
+ DCRegistry = 1 << 17,
+ CertServices = 1 << 18,
LocalGroups = DCOM | RDP | LocalAdmin | PSRemote,
- ComputerOnly = LocalGroups | Session | UserRights,
- DCOnly = ACL | Container | Group | ObjectProps | Trusts | GPOLocalGroup,
- Default = Group | Session | Trusts | ACL | ObjectProps | LocalGroups | SPNTargets | Container,
- All = Default | LoggedOn | GPOLocalGroup | UserRights
+ ComputerOnly = LocalGroups | Session | UserRights | CARegistry | DCRegistry,
+ DCOnly = ACL | Container | Group | ObjectProps | Trusts | GPOLocalGroup | CertServices,
+ Default = Group | Session | Trusts | ACL | ObjectProps | LocalGroups | SPNTargets | Container | CertServices,
+ All = Default | LoggedOn | GPOLocalGroup | UserRights | CARegistry | DCRegistry
}
}
\ No newline at end of file
diff --git a/src/CommonLib/Enums/CommonOids.cs b/src/CommonLib/Enums/CommonOids.cs
new file mode 100644
index 00000000..ebb1d901
--- /dev/null
+++ b/src/CommonLib/Enums/CommonOids.cs
@@ -0,0 +1,13 @@
+namespace SharpHoundCommonLib.Enums
+{
+ // More can be found here: https://www.pkisolutions.com/object-identifiers-oid-in-pki/
+ public static class CommonOids
+ {
+ public static string AnyPurpose = "2.5.29.37.0";
+ public static string ClientAuthentication = "1.3.6.1.5.5.7.3.2";
+ public static string PKINITClientAuthentication = "1.3.6.1.5.2.3.4";
+ public static string SmartcardLogon = "1.3.6.1.4.1.311.20.2.2";
+ public static string CertificateRequestAgent = "1.3.6.1.4.1.311.20.2.1";
+ public static string CertificateRequestAgentPolicy = "1.3.6.1.4.1.311.20.2.1";
+ }
+}
\ No newline at end of file
diff --git a/src/CommonLib/Enums/DataType.cs b/src/CommonLib/Enums/DataType.cs
index 4ace7b4d..c2d9986c 100644
--- a/src/CommonLib/Enums/DataType.cs
+++ b/src/CommonLib/Enums/DataType.cs
@@ -9,5 +9,10 @@ public static class DataType
public const string GPOs = "gpos";
public const string OUs = "ous";
public const string Containers = "containers";
+ public const string RootCAs = "rootcas";
+ public const string AIACAs = "aiacas";
+ public const string NTAuthStores = "ntauthstores";
+ public const string EnterpriseCAs = "enterprisecas";
+ public const string CertTemplates = "certtemplates";
}
}
\ No newline at end of file
diff --git a/src/CommonLib/Enums/DirectoryPaths.cs b/src/CommonLib/Enums/DirectoryPaths.cs
new file mode 100644
index 00000000..1b076c5a
--- /dev/null
+++ b/src/CommonLib/Enums/DirectoryPaths.cs
@@ -0,0 +1,13 @@
+namespace SharpHoundCommonLib.Enums
+{
+ public class DirectoryPaths
+ {
+ public const string EnterpriseCALocation = "CN=Enrollment Services,CN=Public Key Services,CN=Services,CN=Configuration";
+ public const string RootCALocation = "CN=Certification Authorities,CN=Public Key Services,CN=Services,CN=Configuration";
+ public const string AIACALocation = "CN=AIA,CN=Public Key Services,CN=Services,CN=Configuration";
+ public const string CertTemplateLocation = "CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration";
+ public const string NTAuthStoreLocation = "CN=NTAuthCertificates,CN=Public Key Services,CN=Services,CN=Configuration";
+ public const string PKILocation = "CN=Public Key Services,CN=Services,CN=Configuration";
+ public const string ConfigLocation = "CN=Configuration";
+ }
+}
\ No newline at end of file
diff --git a/src/CommonLib/Enums/Labels.cs b/src/CommonLib/Enums/Labels.cs
index a7d1986f..7f07f5ce 100644
--- a/src/CommonLib/Enums/Labels.cs
+++ b/src/CommonLib/Enums/Labels.cs
@@ -11,6 +11,13 @@ public enum Label
GPO,
Domain,
OU,
- Container
+ Container,
+ Configuration,
+ CertTemplate,
+ CertAuthority,
+ RootCA,
+ AIACA,
+ EnterpriseCA,
+ NTAuthStore
}
}
\ No newline at end of file
diff --git a/src/CommonLib/Enums/PKICertificateAuthorityFlags.cs b/src/CommonLib/Enums/PKICertificateAuthorityFlags.cs
new file mode 100644
index 00000000..3fc663ea
--- /dev/null
+++ b/src/CommonLib/Enums/PKICertificateAuthorityFlags.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace SharpHoundCommonLib.Enums
+{
+ [Flags]
+ public enum PKICertificateAuthorityFlags
+ {
+ NO_TEMPLATE_SUPPORT = 0x00000001,
+ SUPPORTS_NT_AUTHENTICATION = 0x00000002,
+ CA_SUPPORTS_MANUAL_AUTHENTICATION = 0x00000004,
+ CA_SERVERTYPE_ADVANCED = 0x00000008
+ }
+}
\ No newline at end of file
diff --git a/src/CommonLib/Enums/PKICertificateNameFlag.cs b/src/CommonLib/Enums/PKICertificateNameFlag.cs
new file mode 100644
index 00000000..3c12de71
--- /dev/null
+++ b/src/CommonLib/Enums/PKICertificateNameFlag.cs
@@ -0,0 +1,25 @@
+using System;
+
+namespace SharpHoundCommonLib.Enums
+{
+ [Flags]
+ public enum PKICertificateNameFlag : uint
+ {
+ ENROLLEE_SUPPLIES_SUBJECT = 0x00000001,
+ ADD_EMAIL = 0x00000002,
+ ADD_OBJ_GUID = 0x00000004,
+ OLD_CERT_SUPPLIES_SUBJECT_AND_ALT_NAME = 0x00000008,
+ ADD_DIRECTORY_PATH = 0x00000100,
+ ENROLLEE_SUPPLIES_SUBJECT_ALT_NAME = 0x00010000,
+ SUBJECT_ALT_REQUIRE_DOMAIN_DNS = 0x00400000,
+ SUBJECT_ALT_REQUIRE_SPN = 0x00800000,
+ SUBJECT_ALT_REQUIRE_DIRECTORY_GUID = 0x01000000,
+ SUBJECT_ALT_REQUIRE_UPN = 0x02000000,
+ SUBJECT_ALT_REQUIRE_EMAIL = 0x04000000,
+ SUBJECT_ALT_REQUIRE_DNS = 0x08000000,
+ SUBJECT_REQUIRE_DNS_AS_CN = 0x10000000,
+ SUBJECT_REQUIRE_EMAIL = 0x20000000,
+ SUBJECT_REQUIRE_COMMON_NAME = 0x40000000,
+ SUBJECT_REQUIRE_DIRECTORY_PATH = 0x80000000
+ }
+}
\ No newline at end of file
diff --git a/src/CommonLib/Enums/PKIEnrollmentFlag.cs b/src/CommonLib/Enums/PKIEnrollmentFlag.cs
new file mode 100644
index 00000000..997a280f
--- /dev/null
+++ b/src/CommonLib/Enums/PKIEnrollmentFlag.cs
@@ -0,0 +1,32 @@
+using System;
+
+namespace SharpHoundCommonLib.Enums
+{
+ // from https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-crtd/ec71fd43-61c2-407b-83c9-b52272dec8a1
+ // and from certutil.exe -v -dstemplate
+ [Flags]
+ public enum PKIEnrollmentFlag : uint
+ {
+ NONE = 0x00000000,
+ INCLUDE_SYMMETRIC_ALGORITHMS = 0x00000001,
+ PEND_ALL_REQUESTS = 0x00000002,
+ PUBLISH_TO_KRA_CONTAINER = 0x00000004,
+ PUBLISH_TO_DS = 0x00000008,
+ AUTO_ENROLLMENT_CHECK_USER_DS_CERTIFICATE = 0x00000010,
+ AUTO_ENROLLMENT = 0x00000020,
+ CT_FLAG_DOMAIN_AUTHENTICATION_NOT_REQUIRED = 0x80,
+ PREVIOUS_APPROVAL_VALIDATE_REENROLLMENT = 0x00000040,
+ USER_INTERACTION_REQUIRED = 0x00000100,
+ ADD_TEMPLATE_NAME = 0x200,
+ REMOVE_INVALID_CERTIFICATE_FROM_PERSONAL_STORE = 0x00000400,
+ ALLOW_ENROLL_ON_BEHALF_OF = 0x00000800,
+ ADD_OCSP_NOCHECK = 0x00001000,
+ ENABLE_KEY_REUSE_ON_NT_TOKEN_KEYSET_STORAGE_FULL = 0x00002000,
+ NOREVOCATIONINFOINISSUEDCERTS = 0x00004000,
+ INCLUDE_BASIC_CONSTRAINTS_FOR_EE_CERTS = 0x00008000,
+ ALLOW_PREVIOUS_APPROVAL_KEYBASEDRENEWAL_VALIDATE_REENROLLMENT = 0x00010000,
+ ISSUANCE_POLICIES_FROM_REQUEST = 0x00020000,
+ SKIP_AUTO_RENEWAL = 0x00040000,
+ NO_SECURITY_EXTENSION = 0x00080000
+ }
+}
\ No newline at end of file
diff --git a/src/CommonLib/Extensions.cs b/src/CommonLib/Extensions.cs
index bf862f86..cfae61bd 100644
--- a/src/CommonLib/Extensions.cs
+++ b/src/CommonLib/Extensions.cs
@@ -3,6 +3,7 @@
using System.DirectoryServices;
using System.DirectoryServices.Protocols;
using System.Linq;
+using System.Security.Cryptography.X509Certificates;
using System.Security.Principal;
using System.Text;
using System.Threading.Tasks;
@@ -85,11 +86,7 @@ public static string GetSid(this DirectoryEntry result)
///
public static bool IsComputerCollectionSet(this ResolvedCollectionMethod methods)
{
- return (methods & ResolvedCollectionMethod.LocalAdmin) != 0 ||
- (methods & ResolvedCollectionMethod.DCOM) != 0 || (methods & ResolvedCollectionMethod.RDP) != 0 ||
- (methods & ResolvedCollectionMethod.PSRemote) != 0 ||
- (methods & ResolvedCollectionMethod.Session) != 0 ||
- (methods & ResolvedCollectionMethod.LoggedOn) != 0;
+ return (methods & ResolvedCollectionMethod.ComputerOnly) != 0;
}
///
@@ -99,9 +96,7 @@ public static bool IsComputerCollectionSet(this ResolvedCollectionMethod methods
///
public static bool IsLocalGroupCollectionSet(this ResolvedCollectionMethod methods)
{
- return (methods & ResolvedCollectionMethod.DCOM) != 0 ||
- (methods & ResolvedCollectionMethod.LocalAdmin) != 0 ||
- (methods & ResolvedCollectionMethod.PSRemote) != 0 || (methods & ResolvedCollectionMethod.RDP) != 0;
+ return (methods & ResolvedCollectionMethod.LocalGroups) != 0;
}
///
@@ -252,6 +247,37 @@ public static byte[] GetPropertyAsBytes(this SearchResultEntry searchResultEntry
return bytes;
}
+ ///
+ /// Gets the specified property as an int
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static bool GetPropertyAsInt(this SearchResultEntry entry, string property, out int value)
+ {
+ var prop = entry.GetProperty(property);
+ if (prop != null) return int.TryParse(prop, out value);
+ value = 0;
+ return false;
+ }
+
+ ///
+ /// Gets the specified property as an array of X509 certificates.
+ ///
+ ///
+ ///
+ ///
+ public static X509Certificate2[] GetPropertyAsArrayOfCertificates(this SearchResultEntry searchResultEntry,
+ string property)
+ {
+ if (!searchResultEntry.Attributes.Contains(property))
+ return null;
+
+ return searchResultEntry.GetPropertyAsArrayOfBytes(property).Select(x => new X509Certificate2(x)).ToArray();
+ }
+
+
///
/// Attempts to get the unique object identifier as used by BloodHound for the Search Result Entry. Tries to get
/// objectsid first, and then objectguid next.
@@ -343,6 +369,21 @@ public static Label GetLabel(this SearchResultEntry entry)
objectType = Label.Domain;
else if (objectClasses.Contains(ContainerClass, StringComparer.InvariantCultureIgnoreCase))
objectType = Label.Container;
+ else if (objectClasses.Contains(ConfigurationClass, StringComparer.InvariantCultureIgnoreCase))
+ objectType = Label.Configuration;
+ else if (objectClasses.Contains(PKICertificateTemplateClass, StringComparer.InvariantCultureIgnoreCase))
+ objectType = Label.CertTemplate;
+ else if (objectClasses.Contains(PKIEnrollmentServiceClass, StringComparer.InvariantCultureIgnoreCase))
+ objectType = Label.EnterpriseCA;
+ else if (objectClasses.Contains(CertificationAutorityClass, StringComparer.InvariantCultureIgnoreCase))
+ {
+ if (entry.DistinguishedName.Contains(DirectoryPaths.RootCALocation))
+ objectType = Label.RootCA;
+ else if (entry.DistinguishedName.Contains(DirectoryPaths.AIACALocation))
+ objectType = Label.AIACA;
+ else if (entry.DistinguishedName.Contains(DirectoryPaths.NTAuthStoreLocation))
+ objectType = Label.NTAuthStore;
+ }
}
Log.LogDebug("GetLabel - Final label for {ObjectID}: {Label}", objectId, objectType);
@@ -356,6 +397,10 @@ public static Label GetLabel(this SearchResultEntry entry)
private const string OrganizationalUnitClass = "organizationalUnit";
private const string DomainClass = "domain";
private const string ContainerClass = "container";
+ private const string ConfigurationClass = "configuration";
+ private const string PKICertificateTemplateClass = "pKICertificateTemplate";
+ private const string PKIEnrollmentServiceClass = "pKIEnrollmentService";
+ private const string CertificationAutorityClass = "certificationAuthority";
#endregion
}
diff --git a/src/CommonLib/Helpers.cs b/src/CommonLib/Helpers.cs
index f882567f..7ac7cf7d 100644
--- a/src/CommonLib/Helpers.cs
+++ b/src/CommonLib/Helpers.cs
@@ -6,6 +6,11 @@
using System.Text;
using System.Text.RegularExpressions;
using SharpHoundCommonLib.Enums;
+using Microsoft.Extensions.Logging;
+using System.IO;
+using System.Security;
+using SharpHoundCommonLib.Processors;
+using Microsoft.Win32;
namespace SharpHoundCommonLib
{
@@ -241,6 +246,21 @@ public static long ConvertLdapTimeToLong(string ldapTime)
return time;
}
+ ///
+ /// Removes some commonly seen SIDs that have no use in the schema
+ ///
+ ///
+ ///
+ internal static string PreProcessSID(string sid)
+ {
+ sid = sid?.ToUpper();
+ if (sid != null)
+ //Ignore Local System/Creator Owner/Principal Self
+ return sid is "S-1-5-18" or "S-1-3-0" or "S-1-5-10" ? null : sid;
+
+ return null;
+ }
+
public static bool IsSidFiltered(string sid)
{
//Uppercase just in case we get a lowercase s
@@ -254,6 +274,59 @@ public static bool IsSidFiltered(string sid)
return false;
}
+
+ public static RegistryResult GetRegistryKeyData(string target, string subkey, string subvalue, ILogger log)
+ {
+ var data = new RegistryResult();
+
+ try
+ {
+ var baseKey = OpenRemoteRegistry(target);
+ var value = baseKey.GetValue(subkey, subvalue);
+ data.Value = value;
+
+ data.Collected = true;
+ }
+ catch (IOException e)
+ {
+ log.LogError(e, "Error getting data from registry for {Target}: {RegSubKey}:{RegValue}",
+ target, subkey, subvalue);
+ data.FailureReason = "Target machine was not found or not connectable";
+ }
+ catch (SecurityException e)
+ {
+ log.LogError(e, "Error getting data from registry for {Target}: {RegSubKey}:{RegValue}",
+ target, subkey, subvalue);
+ data.FailureReason = "User does not have the proper permissions to perform this operation";
+ }
+ catch (UnauthorizedAccessException e)
+ {
+ log.LogError(e, "Error getting data from registry for {Target}: {RegSubKey}:{RegValue}",
+ target, subkey, subvalue);
+ data.FailureReason = "User does not have the necessary registry rights";
+ }
+ catch (Exception e)
+ {
+ log.LogError(e, "Error getting data from registry for {Target}: {RegSubKey}:{RegValue}",
+ target, subkey, subvalue);
+ data.FailureReason = e.Message;
+ }
+
+ return data;
+ }
+
+ public static IRegistryKey OpenRemoteRegistry(string target)
+ {
+ var key = new SHRegistryKey(RegistryHive.LocalMachine, target);
+ return key;
+ }
+
+ public static string[] AuthenticationOIDs = new string[] {
+ CommonOids.ClientAuthentication,
+ CommonOids.PKINITClientAuthentication,
+ CommonOids.SmartcardLogon,
+ CommonOids.AnyPurpose
+ };
}
public class ParsedGPLink
diff --git a/src/CommonLib/ILDAPUtils.cs b/src/CommonLib/ILDAPUtils.cs
index 2e1d039b..6423fb9b 100644
--- a/src/CommonLib/ILDAPUtils.cs
+++ b/src/CommonLib/ILDAPUtils.cs
@@ -34,6 +34,7 @@ public interface ILDAPUtils
bool TestLDAPConfig(string domain);
string[] GetUserGlobalCatalogMatches(string name);
TypedPrincipal ResolveIDAndType(string id, string fallbackDomain);
+ TypedPrincipal ResolveCertTemplateByProperty(string propValue, string propName, string containerDN, string domainName);
Label LookupSidType(string sid, string domain);
Label LookupGuidType(string guid, string domain);
string GetDomainNameFromSid(string sid);
@@ -129,7 +130,11 @@ IEnumerable QueryLDAP(string ldapFilter, SearchScope scope,
string adsPath = null, bool globalCatalog = false, bool skipCache = false, bool throwException = false);
Forest GetForest(string domainName = null);
+ string GetConfigurationPath(string domainName);
+ string GetSchemaPath(string domainName);
ActiveDirectorySecurityDescriptor MakeSecurityDescriptor();
+ string BuildLdapPath(string dnPath, string domain);
+ bool IsDomainController(string computerObjectId, string domainName);
}
}
\ No newline at end of file
diff --git a/src/CommonLib/IRegistryKey.cs b/src/CommonLib/IRegistryKey.cs
new file mode 100644
index 00000000..7490b8fb
--- /dev/null
+++ b/src/CommonLib/IRegistryKey.cs
@@ -0,0 +1,34 @@
+using Microsoft.Win32;
+
+namespace SharpHoundCommonLib
+{
+ public interface IRegistryKey
+ {
+ public object GetValue(string subkey, string name);
+ }
+
+ public class SHRegistryKey : IRegistryKey
+ {
+ private RegistryKey _currentKey;
+
+ public SHRegistryKey(RegistryHive hive, string machineName)
+ {
+ var remoteKey = RegistryKey.OpenRemoteBaseKey(hive, machineName);
+ _currentKey = remoteKey;
+ }
+
+ public object GetValue(string subkey, string name)
+ {
+ var key = _currentKey.OpenSubKey(subkey);
+ return key?.GetValue(name);
+ }
+ }
+
+ public class MockRegistryKey : IRegistryKey
+ {
+ public virtual object GetValue(string subkey, string name)
+ {
+ throw new System.NotImplementedException();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CommonLib/LDAPProperties.cs b/src/CommonLib/LDAPProperties.cs
index 76bacab6..29ab56b5 100644
--- a/src/CommonLib/LDAPProperties.cs
+++ b/src/CommonLib/LDAPProperties.cs
@@ -1,6 +1,6 @@
namespace SharpHoundCommonLib
{
- public class LDAPProperties
+ public static class LDAPProperties
{
public const string GroupMSAMembership = "msds-groupmsamembership";
public const string UserAccountControl = "useraccountcontrol";
@@ -51,5 +51,20 @@ public class LDAPProperties
public const string ScriptPath = "scriptpath";
public const string LdapAdminLimits = "ldapadminlimits";
public const string HostServiceAccount = "msds-hostserviceaccount";
+ public const string PKIExpirationPeriod = "pkiexpirationperiod";
+ public const string PKIOverlappedPeriod = "pkioverlapperiod";
+ public const string TemplateSchemaVersion = "mspki-template-schema-version";
+ public const string CertTemplateOID = "mspki-cert-template-oid";
+ public const string PKIEnrollmentFlag = "mspki-enrollment-flag";
+ public const string PKINameFlag = "mspki-certificate-name-flag";
+ public const string ExtendedKeyUsage = "pkiextendedkeyusage";
+ public const string NumSignaturesRequired = "mspki-ra-signature";
+ public const string ApplicationPolicies = "mspki-ra-application-policies";
+ public const string IssuancePolicies = "mspki-ra-policies";
+ public const string CertificateApplicationPolicy = "mspki-certificate-application-policy";
+ public const string CACertificate = "cacertificate";
+ public const string CertificateTemplates = "certificatetemplates";
+ public const string CrossCertificatePair = "crosscertificatepair";
+ public const string Flags = "flags";
}
}
diff --git a/src/CommonLib/LDAPQueries/CommonProperties.cs b/src/CommonLib/LDAPQueries/CommonProperties.cs
index f3367799..9e503b7e 100644
--- a/src/CommonLib/LDAPQueries/CommonProperties.cs
+++ b/src/CommonLib/LDAPQueries/CommonProperties.cs
@@ -77,5 +77,14 @@ public static class CommonProperties
{
LDAPProperties.GPLink, LDAPProperties.Name
};
+
+ public static readonly string[] CertAbuseProps =
+ {
+ LDAPProperties.CertificateTemplates, LDAPProperties.Flags, LDAPProperties.DNSHostName, LDAPProperties.CACertificate, LDAPProperties.PKINameFlag,
+ LDAPProperties.PKIEnrollmentFlag, LDAPProperties.DisplayName, LDAPProperties.Name, LDAPProperties.TemplateSchemaVersion, LDAPProperties.CertTemplateOID,
+ LDAPProperties.PKIOverlappedPeriod, LDAPProperties.PKIExpirationPeriod, LDAPProperties.ExtendedKeyUsage, LDAPProperties.NumSignaturesRequired,
+ LDAPProperties.CertificateApplicationPolicy, LDAPProperties.IssuancePolicies, LDAPProperties.CrossCertificatePair,
+ LDAPProperties.ApplicationPolicies
+ };
}
}
\ No newline at end of file
diff --git a/src/CommonLib/LDAPQueries/LDAPFilter.cs b/src/CommonLib/LDAPQueries/LDAPFilter.cs
index 36e68e1c..ab7296ad 100644
--- a/src/CommonLib/LDAPQueries/LDAPFilter.cs
+++ b/src/CommonLib/LDAPQueries/LDAPFilter.cs
@@ -142,6 +142,18 @@ public LDAPFilter AddContainers(params string[] conditions)
return this;
}
+ ///
+ /// Add a filter that will include Configuration objects
+ ///
+ ///
+ ///
+ public LDAPFilter AddConfiguration(params string[] conditions)
+ {
+ _filterParts.Add(BuildString("(objectClass=configuration)", conditions));
+
+ return this;
+ }
+
///
/// Add a filter that will include Computer objects
///
@@ -155,6 +167,40 @@ public LDAPFilter AddComputers(params string[] conditions)
return this;
}
+ ///
+ /// Add a filter that will include PKI Certificate templates
+ ///
+ ///
+ ///
+ public LDAPFilter AddCertificateTemplates(params string[] conditions)
+ {
+ _filterParts.Add(BuildString("(objectclass=pKICertificateTemplate)", conditions));
+ return this;
+ }
+
+ ///
+ /// Add a filter that will include Certificate Authorities
+ ///
+ ///
+ ///
+ public LDAPFilter AddCertificateAuthorities(params string[] conditions)
+ {
+ _filterParts.Add(BuildString("(|(objectClass=certificationAuthority)(objectClass=pkiEnrollmentService))",
+ conditions));
+ return this;
+ }
+
+ ///
+ /// Add a filter that will include Enterprise Certificate Authorities
+ ///
+ ///
+ ///
+ public LDAPFilter AddEnterpriseCertificationAuthorities(params string[] conditions)
+ {
+ _filterParts.Add(BuildString("(objectCategory=pKIEnrollmentService)", conditions));
+ return this;
+ }
+
///
/// Add a filter that will include schema items
///
@@ -199,13 +245,23 @@ public LDAPFilter AddFilter(string filter, bool enforce)
///
public string GetFilter()
{
- var temp = string.Join("", _filterParts.ToArray());
- temp = _filterParts.Count == 1 ? _filterParts[0] : $"(|{temp})";
- var mandatory = string.Join("", _mandatory.ToArray());
- temp = _mandatory.Count > 0 ? $"(&{temp}{mandatory})" : temp;
+ var filterPartList = _filterParts.ToArray().Distinct();
+ var mandatoryList = _mandatory.ToArray().Distinct();
+
+ var filterPartsExceptMandatory = filterPartList.Except(mandatoryList).ToList();
+
+ var filterPartsDistinct = string.Join("", filterPartsExceptMandatory);
+ var mandatoryDistinct = string.Join("", mandatoryList);
+
+ if (filterPartsExceptMandatory.Count == 1)
+ filterPartsDistinct = filterPartsExceptMandatory[0];
+ else if (filterPartsExceptMandatory.Count > 1)
+ filterPartsDistinct = $"(|{filterPartsDistinct})";
+
+ filterPartsDistinct = _mandatory.Count > 0 ? $"(&{filterPartsDistinct}{mandatoryDistinct})" : filterPartsDistinct;
- return temp;
+ return filterPartsDistinct;
}
public IEnumerable GetFilterList()
diff --git a/src/CommonLib/LDAPUtils.cs b/src/CommonLib/LDAPUtils.cs
index 8945f989..bb757da8 100644
--- a/src/CommonLib/LDAPUtils.cs
+++ b/src/CommonLib/LDAPUtils.cs
@@ -43,7 +43,7 @@ public class LDAPUtils : ILDAPUtils
0x41, 0x41, 0x41, 0x41, 0x41, 0x00, 0x00, 0x21,
0x00, 0x01
};
-
+
private static readonly ConcurrentDictionary
SeenWellKnownPrincipals = new();
@@ -159,6 +159,7 @@ public IEnumerable GetWellKnownPrincipalOutput(string domain)
Label.Domain => new OutputTypes.Domain(),
Label.OU => new OU(),
Label.Container => new Container(),
+ Label.Configuration => new Container(),
_ => throw new ArgumentOutOfRangeException()
};
@@ -204,7 +205,7 @@ public string[] GetUserGlobalCatalogMatches(string name)
return sids;
var query = new LDAPFilter().AddUsers($"samaccountname={tempName}").GetFilter();
- var results = QueryLDAP(query, SearchScope.Subtree, new[] {"objectsid"}, globalCatalog: true)
+ var results = QueryLDAP(query, SearchScope.Subtree, new[] { "objectsid" }, globalCatalog: true)
.Select(x => x.GetSid()).Where(x => x != null).ToArray();
Cache.AddGCCache(tempName, results);
return results;
@@ -230,6 +231,35 @@ public TypedPrincipal ResolveIDAndType(string id, string fallbackDomain)
return new TypedPrincipal(id, type);
}
+ public TypedPrincipal ResolveCertTemplateByProperty(string propValue, string propertyName, string containerDN, string domainName)
+ {
+ var filter = new LDAPFilter().AddCertificateTemplates().AddFilter(propertyName + "=" + propValue, true);
+ var res = QueryLDAP(filter.GetFilter(), SearchScope.OneLevel,
+ CommonProperties.TypeResolutionProps, adsPath: containerDN, domainName: domainName);
+
+ if (res == null)
+ {
+ _log.LogWarning("Could not find certificate template with '{propertyName}:{propValue}' under {containerDN}; null result", propertyName, propValue, containerDN);
+ return null;
+ }
+
+ List resList = new List(res);
+ if (resList.Count == 0)
+ {
+ _log.LogWarning("Could not find certificate template with '{propertyName}:{propValue}' under {containerDN}; empty list", propertyName, propValue, containerDN);
+ return null;
+ }
+
+ if (resList.Count > 1)
+ {
+ _log.LogWarning("Found more than one certificate template with '{propertyName}:{propValue}' under {containerDN}", propertyName, propValue, containerDN);
+ return null;
+ }
+
+ ISearchResultEntry searchResultEntry = resList.FirstOrDefault();
+ return new TypedPrincipal(searchResultEntry.GetGuid(), Label.CertTemplate);
+ }
+
///
/// Attempts to lookup the Label for a sid
///
@@ -466,7 +496,7 @@ public IEnumerable DoRangedRetrieval(string distinguishedName, string at
var currentRange = $"{baseString};range={index}-*";
var complete = false;
- var searchRequest = CreateSearchRequest($"{attributeName}=*", SearchScope.Base, new[] {currentRange},
+ var searchRequest = CreateSearchRequest($"{attributeName}=*", SearchScope.Base, new[] { currentRange },
domainName, distinguishedName);
if (searchRequest == null)
@@ -474,13 +504,13 @@ public IEnumerable DoRangedRetrieval(string distinguishedName, string at
var backoffDelay = MinBackoffDelay;
var retryCount = 0;
-
+
while (true)
{
SearchResponse response;
try
{
- response = (SearchResponse) conn.SendRequest(searchRequest);
+ response = (SearchResponse)conn.SendRequest(searchRequest);
}
catch (LdapException le) when (le.ErrorCode == (int)LdapErrorCodes.Busy && retryCount < MaxRetries)
{
@@ -688,7 +718,7 @@ public TypedPrincipal ResolveAccountName(string name, string domain)
_log.LogDebug("ResolveAccountName - unable to get result for {Name}", name);
return null;
}
-
+
type = result.GetLabel();
id = result.GetObjectIdentifier();
@@ -809,14 +839,14 @@ public IEnumerable QueryLDAP(string ldapFilter, SearchScope
{
var queryParams = SetupLDAPQueryFilter(
ldapFilter, scope, props, includeAcl, domainName, includeAcl, adsPath, globalCatalog, skipCache);
-
+
if (queryParams.Exception != null)
{
_log.LogWarning("Failed to setup LDAP Query Filter: {Message}", queryParams.Exception.Message);
if (throwException) throw new LDAPQueryException("Failed to setup LDAP Query Filter", queryParams.Exception);
yield break;
}
-
+
var conn = queryParams.Connection;
var request = queryParams.SearchRequest;
var pageControl = queryParams.PageControl;
@@ -833,9 +863,9 @@ public IEnumerable QueryLDAP(string ldapFilter, SearchScope
try
{
_log.LogTrace("Sending LDAP request for {Filter}", ldapFilter);
- response = (SearchResponse) conn.SendRequest(request);
+ response = (SearchResponse)conn.SendRequest(request);
if (response != null)
- pageResponse = (PageResultResponseControl) response.Controls
+ pageResponse = (PageResultResponseControl)response.Controls
.Where(x => x is PageResultResponseControl).DefaultIfEmpty(null).FirstOrDefault();
}catch (LdapException le) when (le.ErrorCode == (int)LdapErrorCodes.ServerDown &&
retryCount < MaxRetries)
@@ -859,7 +889,9 @@ public IEnumerable QueryLDAP(string ldapFilter, SearchScope
backoffDelay = TimeSpan.FromSeconds(Math.Min(
backoffDelay.TotalSeconds * BackoffDelayMultiplier.TotalSeconds, MaxBackoffDelay.TotalSeconds));
continue;
- } catch (LdapException le) {
+ }
+ catch (LdapException le)
+ {
if (le.ErrorCode != 82)
if (throwException)
throw new LDAPQueryException(
@@ -958,10 +990,10 @@ public virtual IEnumerable QueryLDAP(string ldapFilter, Sear
var pageControl = queryParams.PageControl;
PageResultResponseControl pageResponse = null;
-
+
var backoffDelay = MinBackoffDelay;
var retryCount = 0;
-
+
while (true)
{
SearchResponse response;
@@ -1070,6 +1102,16 @@ public ActiveDirectorySecurityDescriptor MakeSecurityDescriptor()
return new ActiveDirectorySecurityDescriptor(new ActiveDirectorySecurity());
}
+ public string BuildLdapPath(string dnPath, string domainName)
+ {
+ var domain = GetDomain(domainName)?.Name;
+ if (domain == null)
+ return null;
+
+ var adPath = $"{dnPath},DC={domain.Replace(".", ",DC=")}";
+ return adPath;
+ }
+
///
/// Tests the current LDAP config to ensure its valid by pulling a domain object
///
@@ -1078,7 +1120,7 @@ public bool TestLDAPConfig(string domain)
{
var filter = new LDAPFilter();
filter.AddDomains();
-
+
var resDomain = GetDomain(domain)?.Name ?? domain;
_log.LogTrace("Testing LDAP connection for domain {Domain}", resDomain);
@@ -1118,7 +1160,7 @@ public virtual Domain GetDomain(string domainName = null)
}
catch (Exception e)
{
- _log.LogDebug(e,"GetDomain call failed at {StackTrace}", new StackFrame());
+ _log.LogDebug(e, "GetDomain call failed at {StackTrace}", new StackFrame());
domain = null;
}
@@ -1231,7 +1273,7 @@ private Group GetBaseEnterpriseDC(string domain)
{
var forest = GetForest(domain)?.Name;
if (forest == null) _log.LogWarning("Error getting forest, ENTDC sid is likely incorrect");
- var g = new Group {ObjectIdentifier = $"{forest}-S-1-5-9".ToUpper()};
+ var g = new Group { ObjectIdentifier = $"{forest}-S-1-5-9".ToUpper() };
g.Properties.Add("name", $"ENTERPRISE DOMAIN CONTROLLERS@{forest ?? "UNKNOWN"}".ToUpper());
g.Properties.Add("domainsid", GetSidFromDomainName(forest));
g.Properties.Add("domain", forest);
@@ -1257,7 +1299,7 @@ private string GetDomainNameFromSidLdap(string sid)
//Search using objectsid first
var result =
QueryLDAP($"(&(objectclass=domain)(objectsid={hexSid}))", SearchScope.Subtree,
- new[] {"distinguishedname"}, globalCatalog: true).DefaultIfEmpty(null).FirstOrDefault();
+ new[] { "distinguishedname" }, globalCatalog: true).DefaultIfEmpty(null).FirstOrDefault();
if (result != null)
{
@@ -1268,7 +1310,7 @@ private string GetDomainNameFromSidLdap(string sid)
//Try trusteddomain objects with the securityidentifier attribute
result =
QueryLDAP($"(&(objectclass=trusteddomain)(securityidentifier={sid}))", SearchScope.Subtree,
- new[] {"cn"}, globalCatalog: true).DefaultIfEmpty(null).FirstOrDefault();
+ new[] { "cn" }, globalCatalog: true).DefaultIfEmpty(null).FirstOrDefault();
if (result != null)
{
@@ -1466,7 +1508,7 @@ private async Task CreateLDAPConnection(string domainName = null
{
string targetServer;
if (_ldapConfig.Server != null)
- targetServer = _ldapConfig.Server;
+ targetServer = _ldapConfig.Server;
else
{
var domain = GetDomain(domainName);
@@ -1482,14 +1524,14 @@ private async Task CreateLDAPConnection(string domainName = null
if (targetServer == null)
throw new LDAPQueryException($"No usable domain controller found for {domainName}");
-
+
if (!skipCache)
if (_ldapConnections.TryGetValue(targetServer, out var conn))
return conn;
var port = _ldapConfig.GetPort();
var ident = new LdapDirectoryIdentifier(targetServer, port, false, false);
- var connection = new LdapConnection(ident) {Timeout = new TimeSpan(0, 0, 5, 0)};
+ var connection = new LdapConnection(ident) { Timeout = new TimeSpan(0, 0, 5, 0) };
if (_ldapConfig.Username != null)
{
var cred = new NetworkCredential(_ldapConfig.Username, _ldapConfig.Password);
@@ -1524,7 +1566,7 @@ private async Task GetUsableDomainController(Domain domain, bool gc = fa
{
if (!gc && _domainControllerCache.TryGetValue(domain.Name.ToUpper(), out var dc))
return dc;
-
+
var port = gc ? 3268 : _ldapConfig.GetPort();
var pdc = domain.PdcRoleOwner.Name;
if (await _portScanner.CheckPort(pdc, port))
@@ -1624,7 +1666,7 @@ public int GetDomainRangeSize(string domainName = null, int defaultRangeSize = 7
var configPath = CommonPaths.CreateDNPath(CommonPaths.QueryPolicyPath, domainPath);
var enumerable = QueryLDAP("(objectclass=*)", SearchScope.Base, null, adsPath: configPath);
- var config = enumerable.DefaultIfEmpty(null).FirstOrDefault();
+ var config = enumerable.DefaultIfEmpty(null).FirstOrDefault();
var pageSize = config?.GetArrayProperty(LDAPProperties.LdapAdminLimits).FirstOrDefault(x => x.StartsWith("MaxPageSize", StringComparison.OrdinalIgnoreCase));
if (pageSize == null)
{
@@ -1639,13 +1681,13 @@ public int GetDomainRangeSize(string domainName = null, int defaultRangeSize = 7
_log.LogInformation("Found page size {PageSize} for {Domain}", parsedPageSize, domainName ?? "current domain");
return parsedPageSize;
}
-
+
_log.LogDebug("Failed to parse pagesize for {Domain}, returning default", domainName ?? "current domain");
_ldapRangeSizeCache.TryAdd(domainPath.ToUpper(), defaultRangeSize);
return defaultRangeSize;
}
-
+
private string DomainNameToDistinguishedName(string domain)
{
var resolvedDomain = GetDomain(domain)?.Name ?? domain;
@@ -1657,5 +1699,33 @@ private class ResolvedWellKnownPrincipal
public string DomainName { get; set; }
public string WkpId { get; set; }
}
+
+ public string GetConfigurationPath(string domainName = null)
+ {
+ var rootDse = domainName == null
+ ? new DirectoryEntry("LDAP://RootDSE")
+ : new DirectoryEntry($"LDAP://{NormalizeDomainName(domainName)}/RootDSE");
+
+ return $"{rootDse.Properties["configurationNamingContext"]?[0]}";
+ }
+
+ public string GetSchemaPath(string domainName)
+ {
+ var rootDse = domainName == null
+ ? new DirectoryEntry("LDAP://RootDSE")
+ : new DirectoryEntry($"LDAP://{NormalizeDomainName(domainName)}/RootDSE");
+
+ return $"{rootDse.Properties["schemaNamingContext"]?[0]}";
+ }
+
+ public bool IsDomainController(string computerObjectId, string domainName)
+ {
+ var filter = new LDAPFilter().AddFilter(LDAPProperties.ObjectSID + "=" + computerObjectId, true).AddFilter(CommonFilters.DomainControllers, true);
+ var res = QueryLDAP(filter.GetFilter(), SearchScope.Subtree,
+ CommonProperties.ObjectID, domainName: domainName);
+ if (res.Count() > 0)
+ return true;
+ return false;
+ }
}
}
diff --git a/src/CommonLib/OutputTypes/AIACA.cs b/src/CommonLib/OutputTypes/AIACA.cs
new file mode 100644
index 00000000..ea5307cd
--- /dev/null
+++ b/src/CommonLib/OutputTypes/AIACA.cs
@@ -0,0 +1,6 @@
+namespace SharpHoundCommonLib.OutputTypes
+{
+ public class AIACA : OutputBase
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/CommonLib/OutputTypes/AceRegistryAPIResult.cs b/src/CommonLib/OutputTypes/AceRegistryAPIResult.cs
new file mode 100644
index 00000000..f98bf432
--- /dev/null
+++ b/src/CommonLib/OutputTypes/AceRegistryAPIResult.cs
@@ -0,0 +1,9 @@
+using System;
+
+namespace SharpHoundCommonLib.OutputTypes
+{
+ public class AceRegistryAPIResult : APIResult
+ {
+ public ACE[] Data { get; set; } = Array.Empty();
+ }
+}
\ No newline at end of file
diff --git a/src/CommonLib/OutputTypes/BoolRegistryAPIResult.cs b/src/CommonLib/OutputTypes/BoolRegistryAPIResult.cs
new file mode 100644
index 00000000..46d1a4d8
--- /dev/null
+++ b/src/CommonLib/OutputTypes/BoolRegistryAPIResult.cs
@@ -0,0 +1,7 @@
+namespace SharpHoundCommonLib.OutputTypes
+{
+ public class BoolRegistryAPIResult : APIResult
+ {
+ public bool Value { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/CommonLib/OutputTypes/CARegistryData.cs b/src/CommonLib/OutputTypes/CARegistryData.cs
new file mode 100644
index 00000000..7c0b0c41
--- /dev/null
+++ b/src/CommonLib/OutputTypes/CARegistryData.cs
@@ -0,0 +1,9 @@
+namespace SharpHoundCommonLib.OutputTypes
+{
+ public class CARegistryData
+ {
+ public AceRegistryAPIResult CASecurity { get; set; }
+ public EnrollmentAgentRegistryAPIResult EnrollmentAgentRestrictions { get; set; }
+ public BoolRegistryAPIResult IsUserSpecifiesSanEnabled { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/CommonLib/OutputTypes/CertTemplate.cs b/src/CommonLib/OutputTypes/CertTemplate.cs
new file mode 100644
index 00000000..68e07fb0
--- /dev/null
+++ b/src/CommonLib/OutputTypes/CertTemplate.cs
@@ -0,0 +1,8 @@
+using SharpHoundCommonLib.Processors;
+
+namespace SharpHoundCommonLib.OutputTypes
+{
+ public class CertTemplate : OutputBase
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/CommonLib/OutputTypes/CertificateExtension.cs b/src/CommonLib/OutputTypes/CertificateExtension.cs
new file mode 100644
index 00000000..9aa743ef
--- /dev/null
+++ b/src/CommonLib/OutputTypes/CertificateExtension.cs
@@ -0,0 +1,17 @@
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+
+namespace SharpHoundCommonLib.OutputTypes
+{
+ public class CertificateExtension
+ {
+ public Oid Oid { get; set; }
+ public bool Critical { get; set; }
+
+ public CertificateExtension(X509Extension extension)
+ {
+ Oid = new Oid(extension.Oid);
+ Critical = extension.Critical;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CommonLib/OutputTypes/Computer.cs b/src/CommonLib/OutputTypes/Computer.cs
index 3eeb2876..351b72d3 100644
--- a/src/CommonLib/OutputTypes/Computer.cs
+++ b/src/CommonLib/OutputTypes/Computer.cs
@@ -17,9 +17,16 @@ public class Computer : OutputBase
public SessionAPIResult RegistrySessions { get; set; } = new();
public LocalGroupAPIResult[] LocalGroups { get; set; } = Array.Empty();
public UserRightsAssignmentAPIResult[] UserRights { get; set; } = Array.Empty();
+ public DCRegistryData DCRegistryData { get; set; } = new();
public ComputerStatus Status { get; set; }
}
+ public class DCRegistryData
+ {
+ public IntRegistryAPIResult CertificateMappingMethods { get; set; }
+ public IntRegistryAPIResult StrongCertificateBindingEnforcement { get; set; }
+ }
+
public class ComputerStatus
{
public bool Connectable { get; set; }
diff --git a/src/CommonLib/OutputTypes/DomainController.cs b/src/CommonLib/OutputTypes/DomainController.cs
new file mode 100644
index 00000000..e69de29b
diff --git a/src/CommonLib/OutputTypes/EnrollmentAgentRegistryAPIResult.cs b/src/CommonLib/OutputTypes/EnrollmentAgentRegistryAPIResult.cs
new file mode 100644
index 00000000..462c862c
--- /dev/null
+++ b/src/CommonLib/OutputTypes/EnrollmentAgentRegistryAPIResult.cs
@@ -0,0 +1,10 @@
+using System;
+using SharpHoundCommonLib.Processors;
+
+namespace SharpHoundCommonLib.OutputTypes
+{
+ public class EnrollmentAgentRegistryAPIResult : APIResult
+ {
+ public EnrollmentAgentRestriction[] Restrictions { get; set; } = Array.Empty();
+ }
+}
\ No newline at end of file
diff --git a/src/CommonLib/OutputTypes/EnterpriseCA.cs b/src/CommonLib/OutputTypes/EnterpriseCA.cs
new file mode 100644
index 00000000..658f38a6
--- /dev/null
+++ b/src/CommonLib/OutputTypes/EnterpriseCA.cs
@@ -0,0 +1,9 @@
+namespace SharpHoundCommonLib.OutputTypes
+{
+ public class EnterpriseCA : OutputBase
+ {
+ public string HostingComputer { get; set; }
+ public CARegistryData CARegistryData { get; set; }
+ public TypedPrincipal[] EnabledCertTemplates { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/CommonLib/OutputTypes/IntRegistryAPIResult.cs b/src/CommonLib/OutputTypes/IntRegistryAPIResult.cs
new file mode 100644
index 00000000..1dca2b3d
--- /dev/null
+++ b/src/CommonLib/OutputTypes/IntRegistryAPIResult.cs
@@ -0,0 +1,7 @@
+namespace SharpHoundCommonLib.OutputTypes
+{
+ public class IntRegistryAPIResult : APIResult
+ {
+ public int Value { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/CommonLib/OutputTypes/NTAuthStore.cs b/src/CommonLib/OutputTypes/NTAuthStore.cs
new file mode 100644
index 00000000..0720df66
--- /dev/null
+++ b/src/CommonLib/OutputTypes/NTAuthStore.cs
@@ -0,0 +1,7 @@
+namespace SharpHoundCommonLib.OutputTypes
+{
+ public class NTAuthStore : OutputBase
+ {
+ public string DomainSID { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/CommonLib/OutputTypes/RootCA.cs b/src/CommonLib/OutputTypes/RootCA.cs
new file mode 100644
index 00000000..7bf1d2e0
--- /dev/null
+++ b/src/CommonLib/OutputTypes/RootCA.cs
@@ -0,0 +1,7 @@
+namespace SharpHoundCommonLib.OutputTypes
+{
+ public class RootCA : OutputBase
+ {
+ public string DomainSID { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/CommonLib/Processors/ACEGuids.cs b/src/CommonLib/Processors/ACEGuids.cs
index 8420498a..b4224f85 100644
--- a/src/CommonLib/Processors/ACEGuids.cs
+++ b/src/CommonLib/Processors/ACEGuids.cs
@@ -12,5 +12,11 @@ public class ACEGuids
public const string WriteSPN = "f3a64788-5306-11d1-a9c5-0000f80367c1";
public const string AddKeyPrincipal = "5b47d60f-6090-40b2-9f37-2a4de88f3063";
public const string UserAccountRestrictions = "4c164200-20c0-11d0-a768-00aa006e0529";
+
+ //Cert abuse ACEs
+ public const string PKINameFlag = "ea1dddc4-60ff-416e-8cc0-17cee534bce7";
+ public const string PKIEnrollmentFlag = "d15ef7d8-f226-46db-ae79-b34e560bd12c";
+ public const string Enroll = "0e10c968-78fb-11d2-90d4-00c04f79dc55";
+ public const string AutoEnroll = "a05b8cc2-17bc-4802-a710-e7c15ab866a2"; //TODO: Add this if it becomes abusable
}
}
\ No newline at end of file
diff --git a/src/CommonLib/Processors/ACLProcessor.cs b/src/CommonLib/Processors/ACLProcessor.cs
index 811418d3..18e91138 100644
--- a/src/CommonLib/Processors/ACLProcessor.cs
+++ b/src/CommonLib/Processors/ACLProcessor.cs
@@ -30,7 +30,13 @@ static ACLProcessor()
{Label.Domain, "19195a5a-6da0-11d0-afd3-00c04fd930c9"},
{Label.GPO, "f30e3bc2-9ff0-11d1-b603-0000f80367c1"},
{Label.OU, "bf967aa5-0de6-11d0-a285-00aa003049e2"},
- {Label.Container, "bf967a8b-0de6-11d0-a285-00aa003049e2"}
+ {Label.Container, "bf967a8b-0de6-11d0-a285-00aa003049e2"},
+ {Label.Configuration, "bf967a87-0de6-11d0-a285-00aa003049e2"},
+ {Label.RootCA, "3fdfee50-47f4-11d1-a9c3-0000f80367c1"},
+ {Label.AIACA, "3fdfee50-47f4-11d1-a9c3-0000f80367c1"},
+ {Label.EnterpriseCA, "ee4aa692-3bba-11d2-90cc-00c04fd91ab1"},
+ {Label.NTAuthStore, "3fdfee50-47f4-11d1-a9c3-0000f80367c1"},
+ {Label.CertTemplate, "e5209ca2-3bba-11d2-90cc-00c04fd91ab1"}
};
}
@@ -156,7 +162,7 @@ public IEnumerable ProcessACL(byte[] ntSecurityDescriptor, string objectDom
yield break;
}
- var ownerSid = PreProcessSID(descriptor.GetOwner(typeof(SecurityIdentifier)));
+ var ownerSid = Helpers.PreProcessSID(descriptor.GetOwner(typeof(SecurityIdentifier)));
if (ownerSid != null)
{
@@ -196,7 +202,7 @@ public IEnumerable ProcessACL(byte[] ntSecurityDescriptor, string objectDom
}
var ir = ace.IdentityReference();
- var principalSid = PreProcessSID(ir);
+ var principalSid = Helpers.PreProcessSID(ir);
if (principalSid == null)
{
@@ -263,7 +269,7 @@ public IEnumerable ProcessACL(byte[] ntSecurityDescriptor, string objectDom
RightName = EdgeNames.AddSelf
};
- //Process object type specific ACEs. Extended rights apply to users, domains, and computers
+ //Process object type specific ACEs. Extended rights apply to users, domains, computers, and cert templates
if (aceRights.HasFlag(ActiveDirectoryRights.ExtendedRight))
{
if (objectType == Label.Domain)
@@ -343,6 +349,25 @@ public IEnumerable ProcessACL(byte[] ntSecurityDescriptor, string objectDom
};
}
}
+ else if (objectType == Label.CertTemplate)
+ {
+ if (aceType is ACEGuids.AllGuid or "")
+ yield return new ACE
+ {
+ PrincipalType = resolvedPrincipal.ObjectType,
+ PrincipalSID = resolvedPrincipal.ObjectIdentifier,
+ IsInherited = inherited,
+ RightName = EdgeNames.AllExtendedRights
+ };
+ else if (aceType is ACEGuids.Enroll)
+ yield return new ACE
+ {
+ PrincipalType = resolvedPrincipal.ObjectType,
+ PrincipalSID = resolvedPrincipal.ObjectIdentifier,
+ IsInherited = inherited,
+ RightName = EdgeNames.Enroll
+ };
+ }
}
//GenericWrite encapsulates WriteProperty, so process them in tandem to avoid duplicate edges
@@ -399,6 +424,67 @@ public IEnumerable ProcessACL(byte[] ntSecurityDescriptor, string objectDom
IsInherited = inherited,
RightName = EdgeNames.AddKeyCredentialLink
};
+ else if (objectType is Label.CertTemplate)
+ {
+ if (aceType == ACEGuids.PKIEnrollmentFlag)
+ yield return new ACE
+ {
+ PrincipalType = resolvedPrincipal.ObjectType,
+ PrincipalSID = resolvedPrincipal.ObjectIdentifier,
+ IsInherited = inherited,
+ RightName = EdgeNames.WritePKIEnrollmentFlag
+ };
+ else if (aceType == ACEGuids.PKINameFlag)
+ yield return new ACE
+ {
+ PrincipalType = resolvedPrincipal.ObjectType,
+ PrincipalSID = resolvedPrincipal.ObjectIdentifier,
+ IsInherited = inherited,
+ RightName = EdgeNames.WritePKINameFlag
+ };
+ }
+ }
+
+ // EnterpriseCA rights
+ if (objectType == Label.EnterpriseCA)
+ {
+ if (aceType is ACEGuids.Enroll)
+ yield return new ACE
+ {
+ PrincipalType = resolvedPrincipal.ObjectType,
+ PrincipalSID = resolvedPrincipal.ObjectIdentifier,
+ IsInherited = inherited,
+ RightName = EdgeNames.Enroll
+ };
+
+ var cARights = (CertificationAuthorityRights)aceRights;
+
+ // TODO: These if statements are also present in ProcessRegistryEnrollmentPermissions. Move to shared location.
+ if ((cARights & CertificationAuthorityRights.ManageCA) != 0)
+ yield return new ACE
+ {
+ PrincipalType = resolvedPrincipal.ObjectType,
+ PrincipalSID = resolvedPrincipal.ObjectIdentifier,
+ IsInherited = inherited,
+ RightName = EdgeNames.ManageCA
+ };
+ if ((cARights & CertificationAuthorityRights.ManageCertificates) != 0)
+ yield return new ACE
+ {
+ PrincipalType = resolvedPrincipal.ObjectType,
+ PrincipalSID = resolvedPrincipal.ObjectIdentifier,
+ IsInherited = inherited,
+ RightName = EdgeNames.ManageCertificates
+ };
+
+ if ((cARights & CertificationAuthorityRights.Enroll) != 0)
+ yield return new ACE
+ {
+ PrincipalType = resolvedPrincipal.ObjectType,
+ PrincipalSID = resolvedPrincipal.ObjectIdentifier,
+ IsInherited = inherited,
+ RightName = EdgeNames.Enroll
+ };
}
}
}
@@ -473,7 +559,7 @@ public IEnumerable ProcessGMSAReaders(byte[] groupMSAMembership, string obj
}
var ir = ace.IdentityReference();
- var principalSid = PreProcessSID(ir);
+ var principalSid = Helpers.PreProcessSID(ir);
if (principalSid == null)
{
@@ -495,20 +581,5 @@ public IEnumerable ProcessGMSAReaders(byte[] groupMSAMembership, string obj
};
}
}
-
- ///
- /// Removes some commonly seen SIDs that have no use in the schema
- ///
- ///
- ///
- private static string PreProcessSID(string sid)
- {
- sid = sid?.ToUpper();
- if (sid != null)
- //Ignore Local System/Creator Owner/Principal Self
- return sid is "S-1-5-18" or "S-1-3-0" or "S-1-5-10" ? null : sid;
-
- return null;
- }
}
}
\ No newline at end of file
diff --git a/src/CommonLib/Processors/CertAbuseProcessor.cs b/src/CommonLib/Processors/CertAbuseProcessor.cs
new file mode 100644
index 00000000..e9593bea
--- /dev/null
+++ b/src/CommonLib/Processors/CertAbuseProcessor.cs
@@ -0,0 +1,459 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Security.AccessControl;
+using System.Security.Principal;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using SharpHoundCommonLib.Enums;
+using SharpHoundCommonLib.OutputTypes;
+using SharpHoundRPC;
+using SharpHoundRPC.Wrappers;
+
+namespace SharpHoundCommonLib.Processors
+{
+ public class CertAbuseProcessor
+ {
+ private readonly ILogger _log;
+ public readonly ILDAPUtils _utils;
+ public delegate Task ComputerStatusDelegate(CSVComputerStatus status);
+ public event ComputerStatusDelegate ComputerStatusEvent;
+
+
+ public CertAbuseProcessor(ILDAPUtils utils, ILogger log = null)
+ {
+ _utils = utils;
+ _log = log ?? Logging.LogProvider.CreateLogger("CAProc");
+ }
+
+ ///
+ /// This function should be called with the security data fetched from .
+ /// The resulting ACEs will contain the owner of the CA as well as Management rights.
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task ProcessRegistryEnrollmentPermissions(string caName, string objectDomain, string computerName, string computerObjectId)
+ {
+ var data = new AceRegistryAPIResult();
+
+ var aceData = GetCASecurity(computerName, caName);
+ data.Collected = aceData.Collected;
+ if (!aceData.Collected)
+ {
+ data.FailureReason = aceData.FailureReason;
+ return data;
+ }
+
+ if (aceData.Value == null)
+ {
+ return data;
+ }
+
+ var descriptor = _utils.MakeSecurityDescriptor();
+ descriptor.SetSecurityDescriptorBinaryForm(aceData.Value as byte[], AccessControlSections.All);
+
+ var ownerSid = Helpers.PreProcessSID(descriptor.GetOwner(typeof(SecurityIdentifier)));
+ var computerDomain = _utils.GetDomainNameFromSid(computerObjectId);
+ var isDomainController = _utils.IsDomainController(computerObjectId, computerDomain);
+ var machineSid = await GetMachineSid(computerName, computerObjectId, computerDomain, isDomainController);
+
+ var aces = new List();
+
+ if (ownerSid != null)
+ {
+ var resolvedOwner = GetRegistryPrincipal(new SecurityIdentifier(ownerSid), computerDomain, computerName, isDomainController, computerObjectId, machineSid);
+ if (resolvedOwner != null)
+ aces.Add(new ACE
+ {
+ PrincipalType = resolvedOwner.ObjectType,
+ PrincipalSID = resolvedOwner.ObjectIdentifier,
+ RightName = EdgeNames.Owns,
+ IsInherited = false
+ });
+ }
+ else
+ {
+ _log.LogDebug("Owner on CA {Name} is null", computerName);
+ }
+
+ foreach (var rule in descriptor.GetAccessRules(true, true, typeof(SecurityIdentifier)))
+ {
+ if (rule == null)
+ continue;
+
+ if (rule.AccessControlType() == AccessControlType.Deny)
+ continue;
+
+ var principalSid = Helpers.PreProcessSID(rule.IdentityReference());
+ if (principalSid == null)
+ continue;
+
+ var principalDomain = _utils.GetDomainNameFromSid(principalSid) ?? objectDomain;
+ var resolvedPrincipal = GetRegistryPrincipal(new SecurityIdentifier(principalSid), principalDomain, computerName, isDomainController, computerObjectId, machineSid);
+ var isInherited = rule.IsInherited();
+
+ var cARights = (CertificationAuthorityRights)rule.ActiveDirectoryRights();
+
+ // TODO: These if statements are also present in ProcessACL. Move to shared location.
+ if ((cARights & CertificationAuthorityRights.ManageCA) != 0)
+ aces.Add(new ACE
+ {
+ PrincipalType = resolvedPrincipal.ObjectType,
+ PrincipalSID = resolvedPrincipal.ObjectIdentifier,
+ IsInherited = isInherited,
+ RightName = EdgeNames.ManageCA
+ });
+ if ((cARights & CertificationAuthorityRights.ManageCertificates) != 0)
+ aces.Add(new ACE
+ {
+ PrincipalType = resolvedPrincipal.ObjectType,
+ PrincipalSID = resolvedPrincipal.ObjectIdentifier,
+ IsInherited = isInherited,
+ RightName = EdgeNames.ManageCertificates
+ });
+
+ if ((cARights & CertificationAuthorityRights.Enroll) != 0)
+ aces.Add(new ACE
+ {
+ PrincipalType = resolvedPrincipal.ObjectType,
+ PrincipalSID = resolvedPrincipal.ObjectIdentifier,
+ IsInherited = isInherited,
+ RightName = EdgeNames.Enroll
+ });
+ }
+
+ data.Data = aces.ToArray();
+ return data;
+ }
+
+ ///
+ /// This function should be called with the enrollment data fetched from .
+ /// The resulting items will contain enrollment agent restrictions
+ ///
+ ///
+ ///
+ public async Task ProcessEAPermissions(string caName, string objectDomain, string computerName, string computerObjectId)
+ {
+ var ret = new EnrollmentAgentRegistryAPIResult();
+ var regData = GetEnrollmentAgentRights(computerName, caName);
+
+ ret.Collected = regData.Collected;
+ if (!ret.Collected)
+ {
+ ret.FailureReason = regData.FailureReason;
+ return ret;
+ }
+
+ if (regData.Value == null)
+ {
+ return ret;
+ }
+
+ var computerDomain = _utils.GetDomainNameFromSid(computerObjectId);
+ var isDomainController = _utils.IsDomainController(computerObjectId, computerDomain);
+ var machineSid = await GetMachineSid(computerName, computerObjectId, computerDomain, isDomainController);
+ var certTemplatesLocation = _utils.BuildLdapPath(DirectoryPaths.CertTemplateLocation, computerDomain);
+ var descriptor = new RawSecurityDescriptor(regData.Value as byte[], 0);
+ var enrollmentAgentRestrictions = new List();
+ foreach (var genericAce in descriptor.DiscretionaryAcl)
+ {
+ var ace = (QualifiedAce)genericAce;
+ enrollmentAgentRestrictions.Add(new EnrollmentAgentRestriction(ace, computerDomain, certTemplatesLocation, this, computerName, isDomainController, computerObjectId, machineSid));
+ }
+
+ ret.Restrictions = enrollmentAgentRestrictions.ToArray();
+
+ return ret;
+ }
+
+ public IEnumerable ProcessCertTemplates(string[] templates, string domainName)
+ {
+ var certTemplatesLocation = _utils.BuildLdapPath(DirectoryPaths.CertTemplateLocation, domainName);
+ foreach (var templateCN in templates)
+ {
+ var res = _utils.ResolveCertTemplateByProperty(templateCN, LDAPProperties.CanonicalName, certTemplatesLocation, domainName);
+ yield return res;
+ }
+ }
+
+ ///
+ /// Get CA security registry value from the remote machine for processing security/enrollmentagentrights
+ ///
+ ///
+ ///
+ ///
+ [ExcludeFromCodeCoverage]
+ private RegistryResult GetCASecurity(string target, string caName)
+ {
+ var regSubKey = $"SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\{caName}";
+ const string regValue = "Security";
+
+ return Helpers.GetRegistryKeyData(target, regSubKey, regValue, _log);
+ }
+
+ ///
+ /// Get EnrollmentAgentRights registry value from the remote machine for processing security/enrollmentagentrights
+ ///
+ ///
+ ///
+ ///
+ [ExcludeFromCodeCoverage]
+ private RegistryResult GetEnrollmentAgentRights(string target, string caName)
+ {
+ var regSubKey = $"SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\{caName}";
+ var regValue = "EnrollmentAgentRights";
+
+ return Helpers.GetRegistryKeyData(target, regSubKey, regValue, _log);
+ }
+
+ ///
+ /// This function checks a registry setting on the target host for the specified CA to see if a requesting user can specify any SAN they want, which overrides template settings.
+ /// The ManageCA permission allows you to flip this bit as well. This appears to usually work, even if admin rights aren't available on the remote CA server
+ ///
+ /// https://blog.keyfactor.com/hidden-dangers-certificate-subject-alternative-names-sans
+ ///
+ ///
+ ///
+ ///
+ [ExcludeFromCodeCoverage]
+ public BoolRegistryAPIResult IsUserSpecifiesSanEnabled(string target, string caName)
+ {
+ var ret = new BoolRegistryAPIResult();
+ var subKey =
+ $"SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\{caName}\\PolicyModules\\CertificateAuthority_MicrosoftDefault.Policy";
+ const string subValue = "EditFlags";
+ var data = Helpers.GetRegistryKeyData(target, subKey, subValue, _log);
+
+ ret.Collected = data.Collected;
+ if (!data.Collected)
+ {
+ ret.FailureReason = data.FailureReason;
+ return ret;
+ }
+
+ if (data.Value == null)
+ {
+ return ret;
+ }
+
+ var editFlags = (int)data.Value;
+ ret.Value = (editFlags & 0x00040000) == 0x00040000;
+
+ return ret;
+ }
+
+ public TypedPrincipal GetRegistryPrincipal(SecurityIdentifier sid, string computerDomain, string computerName, bool isDomainController, string computerObjectId, SecurityIdentifier machineSid)
+ {
+ _log.LogTrace("Got principal with sid {SID} on computer {ComputerName}", sid.Value, computerName);
+
+ //Check if our sid is filtered
+ if (Helpers.IsSidFiltered(sid.Value))
+ return null;
+
+ if (isDomainController)
+ {
+ var result = ResolveDomainControllerPrincipal(sid.Value, computerDomain);
+ if (result != null)
+ return result;
+ }
+
+ //If we get a local well known principal, we need to convert it using the computer's domain sid
+ if (ConvertLocalWellKnownPrincipal(sid, computerObjectId, computerDomain, out var principal))
+ {
+ _log.LogTrace("Got Well Known Principal {SID} on computer {Computer} with type {Type}", principal.ObjectIdentifier, computerName, principal.ObjectType);
+ return principal;
+ }
+
+ //If the security identifier starts with the machine sid, we need to resolve it as a local principal
+ if (machineSid != null && sid.IsEqualDomainSid(machineSid))
+ {
+ _log.LogTrace("Got local principal {sid} on computer {Computer}", sid.Value, computerName);
+
+ // Set label to be local group. It could be a local user or alias but I'm not sure how we can confirm. Besides, it will not have any effect on the end result
+ var objectType = Label.LocalGroup;
+
+ // The local group sid is computer machine sid - group rid.
+ var groupRid = sid.Rid();
+ var newSid = $"{computerObjectId}-{groupRid}";
+ return (new TypedPrincipal
+ {
+ ObjectIdentifier = newSid,
+ ObjectType = objectType
+ });
+ }
+
+ //If we get here, we most likely have a domain principal. Do a lookup
+ return _utils.ResolveIDAndType(sid.Value, computerDomain);
+ }
+
+ private async Task GetMachineSid(string computerName, string computerObjectId, string computerDomain, bool isDomainController)
+ {
+ SecurityIdentifier machineSid = null;
+
+ //Try to get the machine sid for the computer if its not already cached
+ if (!Cache.GetMachineSid(computerObjectId, out var tempMachineSid))
+ {
+ // Open a handle to the server
+ var openServerResult = OpenSamServer(computerName);
+ if (openServerResult.IsFailed)
+ {
+ _log.LogTrace("OpenServer failed on {ComputerName}: {Error}", computerName, openServerResult.SError);
+ await SendComputerStatus(new CSVComputerStatus
+ {
+ Task = "SamConnect",
+ ComputerName = computerName,
+ Status = openServerResult.SError
+ });
+ return null;
+ }
+
+ var server = openServerResult.Value;
+ var getMachineSidResult = server.GetMachineSid();
+ if (getMachineSidResult.IsFailed)
+ {
+ _log.LogTrace("GetMachineSid failed on {ComputerName}: {Error}", computerName, getMachineSidResult.SError);
+ await SendComputerStatus(new CSVComputerStatus
+ {
+ Status = getMachineSidResult.SError,
+ ComputerName = computerName,
+ Task = "GetMachineSid"
+ });
+ //If we can't get a machine sid, we wont be able to make local principals with unique object ids, or differentiate local/domain objects
+ _log.LogWarning("Unable to get machineSid for {Computer}: {Status}", computerName, getMachineSidResult.SError);
+ return null;
+ }
+
+ machineSid = getMachineSidResult.Value;
+ Cache.AddMachineSid(computerObjectId, machineSid.Value);
+ }
+ else
+ {
+ machineSid = new SecurityIdentifier(tempMachineSid);
+ }
+
+ return machineSid;
+ }
+
+ // TODO: Copied from URA processor. Find a way to have this function in a shared spot
+ private TypedPrincipal ResolveDomainControllerPrincipal(string sid, string computerDomain)
+ {
+ //If the server is a domain controller and we have a well known group, use the domain value
+ if (_utils.GetWellKnownPrincipal(sid, computerDomain, out var wellKnown))
+ return wellKnown;
+ //Otherwise, do a domain lookup
+ return _utils.ResolveIDAndType(sid, computerDomain);
+ }
+
+ // TODO: Copied from URA processor. Find a way to have this function in a shared spot
+ private bool ConvertLocalWellKnownPrincipal(SecurityIdentifier sid, string computerDomainSid,
+ string computerDomain, out TypedPrincipal principal)
+ {
+ if (WellKnownPrincipal.GetWellKnownPrincipal(sid.Value, out var common))
+ {
+ //The everyone and auth users principals are special and will be converted to the domain equivalent
+ if (sid.Value is "S-1-1-0" or "S-1-5-11")
+ {
+ _utils.GetWellKnownPrincipal(sid.Value, computerDomain, out principal);
+ return true;
+ }
+
+ //Use the computer object id + the RID of the sid we looked up to create our new principal
+ principal = new TypedPrincipal
+ {
+ ObjectIdentifier = $"{computerDomainSid}-{sid.Rid()}",
+ ObjectType = common.ObjectType switch
+ {
+ Label.User => Label.LocalUser,
+ Label.Group => Label.LocalGroup,
+ _ => common.ObjectType
+ }
+ };
+
+ return true;
+ }
+
+ principal = null;
+ return false;
+ }
+
+ public virtual Result OpenSamServer(string computerName)
+ {
+ var result = SAMServer.OpenServer(computerName);
+ if (result.IsFailed)
+ {
+ return Result.Fail(result.SError);
+ }
+
+ return Result.Ok(result.Value);
+ }
+
+ private async Task SendComputerStatus(CSVComputerStatus status)
+ {
+ if (ComputerStatusEvent is not null) await ComputerStatusEvent(status);
+ }
+
+ }
+
+ public class EnrollmentAgentRestriction
+ {
+ public EnrollmentAgentRestriction(QualifiedAce ace, string computerDomain, string certTemplatesLocation, CertAbuseProcessor certAbuseProcessor, string computerName, bool isDomainController, string computerObjectId, SecurityIdentifier machineSid)
+ {
+ var targets = new List();
+ var index = 0;
+
+ // Access type (Allow/Deny)
+ AccessType = ace.AceType.ToString();
+
+ // Agent
+ Agent = certAbuseProcessor.GetRegistryPrincipal(ace.SecurityIdentifier, computerDomain, computerName, isDomainController, computerObjectId, machineSid);
+
+ // Targets
+ var opaque = ace.GetOpaque();
+ var sidCount = BitConverter.ToUInt32(opaque, 0);
+ index += 4;
+ for (var i = 0; i < sidCount; i++)
+ {
+ var sid = new SecurityIdentifier(opaque, index);
+ targets.Add(certAbuseProcessor.GetRegistryPrincipal(ace.SecurityIdentifier, computerDomain, computerName, isDomainController, computerObjectId, machineSid));
+ index += sid.BinaryLength;
+ }
+ Targets = targets.ToArray();
+
+ // Template
+ if (index < opaque.Length)
+ {
+ AllTemplates = false;
+ var template = Encoding.Unicode.GetString(opaque, index, opaque.Length - index - 2).Replace("\u0000", string.Empty);
+
+ // Attempt to resolve the cert template by CN
+ Template = certAbuseProcessor._utils.ResolveCertTemplateByProperty(template, LDAPProperties.CanonicalName, certTemplatesLocation, computerDomain);
+
+ // Attempt to resolve the cert template by OID
+ if (Template == null)
+ {
+ Template = certAbuseProcessor._utils.ResolveCertTemplateByProperty(template, LDAPProperties.CertTemplateOID, certTemplatesLocation, computerDomain);
+ }
+ }
+ else
+ {
+ AllTemplates = true;
+ }
+ }
+
+ public string AccessType { get; set; }
+ public TypedPrincipal Agent { get; set; }
+ public TypedPrincipal[] Targets { get; set; }
+ public TypedPrincipal Template { get; set; }
+ public bool AllTemplates { get; set; } = false;
+ }
+
+ public class CertRegistryResult
+ {
+ public bool Collected { get; set; } = false;
+ public byte[] Value { get; set; }
+ public string FailureReason { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/CommonLib/Processors/ContainerProcessor.cs b/src/CommonLib/Processors/ContainerProcessor.cs
index e5b6150a..19ce60ce 100644
--- a/src/CommonLib/Processors/ContainerProcessor.cs
+++ b/src/CommonLib/Processors/ContainerProcessor.cs
@@ -78,6 +78,7 @@ public IEnumerable GetContainerChildObjects(ResolvedSearchResult
public IEnumerable GetContainerChildObjects(string distinguishedName, string containerName = "")
{
var filter = new LDAPFilter().AddComputers().AddUsers().AddGroups().AddOUs().AddContainers();
+ filter.AddCertificateAuthorities().AddCertificateTemplates().AddEnterpriseCertificationAuthorities();
foreach (var childEntry in _utils.QueryLDAP(filter.GetFilter(), SearchScope.OneLevel,
CommonProperties.ObjectID, Helpers.DistinguishedNameToDomain(distinguishedName),
adsPath: distinguishedName))
diff --git a/src/CommonLib/Processors/DCRegistryProcessor.cs b/src/CommonLib/Processors/DCRegistryProcessor.cs
new file mode 100644
index 00000000..e3fb4b14
--- /dev/null
+++ b/src/CommonLib/Processors/DCRegistryProcessor.cs
@@ -0,0 +1,87 @@
+using SharpHoundCommonLib.OutputTypes;
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace SharpHoundCommonLib.Processors
+{
+ public class DCRegistryProcessor
+ {
+ private readonly ILogger _log;
+ public readonly ILDAPUtils _utils;
+ public delegate Task ComputerStatusDelegate(CSVComputerStatus status);
+
+ public DCRegistryProcessor(ILDAPUtils utils, ILogger log = null)
+ {
+ _utils = utils;
+ _log = log ?? Logging.LogProvider.CreateLogger("DCRegProc");
+ }
+
+ ///
+ /// This function gets the CertificateMappingMethods registry value stored on DCs.
+ ///
+ /// https://support.microsoft.com/en-us/topic/kb5014754-certificate-based-authentication-changes-on-windows-domain-controllers-ad2c23b0-15d8-4340-a468-4d4f3b188f16
+ ///
+ /// IntRegistryAPIResult
+ ///
+ [ExcludeFromCodeCoverage]
+ public IntRegistryAPIResult GetCertificateMappingMethods(string target)
+ {
+ var ret = new IntRegistryAPIResult();
+ var subKey = $"SYSTEM\\CurrentControlSet\\Control\\SecurityProviders\\Schannel";
+ const string subValue = "CertificateMappingMethods";
+ var data = Helpers.GetRegistryKeyData(target, subKey, subValue, _log);
+
+ ret.Collected = data.Collected;
+ if (!data.Collected)
+ {
+ ret.FailureReason = data.FailureReason;
+ return ret;
+ }
+
+ if (data.Value == null)
+ {
+ ret.Value = -1;
+ return ret;
+ }
+
+ ret.Value = (int)data.Value;
+
+ return ret;
+ }
+
+ ///
+ /// This function gets the StrongCertificateBindingEnforcement registry value stored on DCs.
+ ///
+ /// https://support.microsoft.com/en-us/topic/kb5014754-certificate-based-authentication-changes-on-windows-domain-controllers-ad2c23b0-15d8-4340-a468-4d4f3b188f16
+ ///
+ /// IntRegistryAPIResult
+ ///
+ [ExcludeFromCodeCoverage]
+ public IntRegistryAPIResult GetStrongCertificateBindingEnforcement(string target)
+ {
+ var ret = new IntRegistryAPIResult();
+ var subKey = $"SYSTEM\\CurrentControlSet\\Services\\Kdc";
+ const string subValue = "StrongCertificateBindingEnforcement";
+ var data = Helpers.GetRegistryKeyData(target, subKey, subValue, _log);
+
+ ret.Collected = data.Collected;
+ if (!data.Collected)
+ {
+ ret.FailureReason = data.FailureReason;
+ return ret;
+ }
+
+ if (data.Value == null)
+ {
+ ret.Value = -1;
+ return ret;
+ }
+
+ ret.Value = (int)data.Value;
+
+ return ret;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CommonLib/Processors/LDAPPropertyProcessor.cs b/src/CommonLib/Processors/LDAPPropertyProcessor.cs
index 6b4ebd80..f27a0176 100644
--- a/src/CommonLib/Processors/LDAPPropertyProcessor.cs
+++ b/src/CommonLib/Processors/LDAPPropertyProcessor.cs
@@ -2,8 +2,10 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
+using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
+using System.Security.Cryptography.X509Certificates;
using System.Security.Principal;
using System.Threading.Tasks;
using SharpHoundCommonLib.Enums;
@@ -151,7 +153,7 @@ public async Task ReadUserProperties(ISearchResultEntry entry)
bool enabled, trustedToAuth, sensitive, dontReqPreAuth, passwdNotReq, unconstrained, pwdNeverExpires;
if (int.TryParse(uac, out var flag))
{
- var flags = (UacFlags) flag;
+ var flags = (UacFlags)flag;
enabled = (flags & UacFlags.AccountDisable) == 0;
trustedToAuth = (flags & UacFlags.TrustedToAuthForDelegation) != 0;
sensitive = (flags & UacFlags.NotDelegated) != 0;
@@ -279,7 +281,7 @@ public async Task ReadComputerProperties(ISearchResultEntry
bool enabled, unconstrained, trustedToAuth;
if (int.TryParse(uac, out var flag))
{
- var flags = (UacFlags) flag;
+ var flags = (UacFlags)flag;
enabled = (flags & UacFlags.AccountDisable) == 0;
unconstrained = (flags & UacFlags.TrustedForDelegation) == UacFlags.TrustedForDelegation;
trustedToAuth = (flags & UacFlags.TrustedToAuthForDelegation) != 0;
@@ -374,7 +376,8 @@ public async Task ReadComputerProperties(ISearchResultEntry
var hsa = entry.GetArrayProperty(LDAPProperties.HostServiceAccount);
var smsaPrincipals = new List();
- if (hsa != null) {
+ if (hsa != null)
+ {
foreach (var dn in hsa)
{
var resolvedPrincipal = _utils.ResolveDistinguishedName(dn);
@@ -391,6 +394,153 @@ public async Task ReadComputerProperties(ISearchResultEntry
return compProps;
}
+ ///
+ /// Returns the properties associated with the RootCA
+ ///
+ ///
+ /// Returns a dictionary with the common properties of the RootCA
+ public static Dictionary ReadRootCAProperties(ISearchResultEntry entry)
+ {
+ var props = GetCommonProps(entry);
+
+ // Certificate
+ var rawCertificate = entry.GetByteProperty(LDAPProperties.CACertificate);
+ if (rawCertificate != null)
+ {
+ ParsedCertificate cert = new ParsedCertificate(rawCertificate);
+ props.Add("certthumbprint", cert.Thumbprint);
+ props.Add("certname", cert.Name);
+ props.Add("certchain", cert.Chain);
+ props.Add("hasbasicconstraints", cert.HasBasicConstraints);
+ props.Add("basicconstraintpathlength", cert.BasicConstraintPathLength);
+ }
+
+ return props;
+ }
+
+ ///
+ /// Returns the properties associated with the AIACA
+ ///
+ ///
+ /// Returns a dictionary with the common properties and the crosscertificatepair property of the AICA
+ public static Dictionary ReadAIACAProperties(ISearchResultEntry entry)
+ {
+ var props = GetCommonProps(entry);
+ var crossCertificatePair = entry.GetByteArrayProperty((LDAPProperties.CrossCertificatePair));
+ var hasCrossCertificatePair = crossCertificatePair.Length > 0;
+
+ props.Add("crosscertificatepair", crossCertificatePair);
+ props.Add("hascrosscertificatepair", hasCrossCertificatePair);
+
+ // Certificate
+ var rawCertificate = entry.GetByteProperty(LDAPProperties.CACertificate);
+ if (rawCertificate != null)
+ {
+ ParsedCertificate cert = new ParsedCertificate(rawCertificate);
+ props.Add("certthumbprint", cert.Thumbprint);
+ props.Add("certname", cert.Name);
+ props.Add("certchain", cert.Chain);
+ props.Add("hasbasicconstraints", cert.HasBasicConstraints);
+ props.Add("basicconstraintpathlength", cert.BasicConstraintPathLength);
+ }
+
+ return props;
+ }
+
+ public static Dictionary ReadEnterpriseCAProperties(ISearchResultEntry entry)
+ {
+ var props = GetCommonProps(entry);
+ if (entry.GetIntProperty("flags", out var flags)) props.Add("flags", (PKIEnrollmentFlag)flags);
+ props.Add("caname", entry.GetProperty(LDAPProperties.Name));
+ props.Add("dnshostname", entry.GetProperty(LDAPProperties.DNSHostName));
+
+ // Certificate
+ var rawCertificate = entry.GetByteProperty(LDAPProperties.CACertificate);
+ if (rawCertificate != null)
+ {
+ ParsedCertificate cert = new ParsedCertificate(rawCertificate);
+ props.Add("certthumbprint", cert.Thumbprint);
+ props.Add("certname", cert.Name);
+ props.Add("certchain", cert.Chain);
+ props.Add("hasbasicconstraints", cert.HasBasicConstraints);
+ props.Add("basicconstraintpathlength", cert.BasicConstraintPathLength);
+ }
+
+ return props;
+ }
+
+ ///
+ /// Returns the properties associated with the NTAuthStore. These properties will only contain common properties
+ ///
+ ///
+ /// Returns a dictionary with the common properties of the NTAuthStore
+ public static Dictionary ReadNTAuthStoreProperties(ISearchResultEntry entry)
+ {
+ var props = GetCommonProps(entry);
+ return props;
+ }
+
+ ///
+ /// Reads specific LDAP properties related to CertTemplates
+ ///
+ ///
+ /// Returns a dictionary associated with the CertTemplate properties that were read
+ public static Dictionary ReadCertTemplateProperties(ISearchResultEntry entry)
+ {
+ var props = GetCommonProps(entry);
+
+ props.Add("validityperiod", ConvertPKIPeriod(entry.GetByteProperty(LDAPProperties.PKIExpirationPeriod)));
+ props.Add("renewalperiod", ConvertPKIPeriod(entry.GetByteProperty(LDAPProperties.PKIOverlappedPeriod)));
+
+ if (entry.GetIntProperty(LDAPProperties.TemplateSchemaVersion, out var schemaVersion))
+ props.Add("schemaversion", schemaVersion);
+
+ props.Add("displayname", entry.GetProperty(LDAPProperties.DisplayName));
+ props.Add("oid", entry.GetProperty(LDAPProperties.CertTemplateOID));
+
+ if (entry.GetIntProperty(LDAPProperties.PKIEnrollmentFlag, out var enrollmentFlagsRaw))
+ {
+ var enrollmentFlags = (PKIEnrollmentFlag)enrollmentFlagsRaw;
+
+ props.Add("enrollmentflag", enrollmentFlags);
+ props.Add("requiresmanagerapproval", enrollmentFlags.HasFlag(PKIEnrollmentFlag.PEND_ALL_REQUESTS));
+ props.Add("nosecurityextension", enrollmentFlags.HasFlag(PKIEnrollmentFlag.NO_SECURITY_EXTENSION));
+ }
+
+ if (entry.GetIntProperty(LDAPProperties.PKINameFlag, out var nameFlagsRaw))
+ {
+ var nameFlags = (PKICertificateNameFlag)nameFlagsRaw;
+
+ props.Add("certificatenameflag", nameFlags);
+ props.Add("enrolleesuppliessubject",
+ nameFlags.HasFlag(PKICertificateNameFlag.ENROLLEE_SUPPLIES_SUBJECT));
+ props.Add("subjectaltrequireupn",
+ nameFlags.HasFlag(PKICertificateNameFlag.SUBJECT_ALT_REQUIRE_UPN));
+ }
+
+ string[] ekus = entry.GetArrayProperty(LDAPProperties.ExtendedKeyUsage);
+ props.Add("ekus", ekus);
+ string[] certificateapplicationpolicy = entry.GetArrayProperty(LDAPProperties.CertificateApplicationPolicy);
+ props.Add("certificateapplicationpolicy", certificateapplicationpolicy);
+
+ if (entry.GetIntProperty(LDAPProperties.NumSignaturesRequired, out var authorizedSignatures))
+ props.Add("authorizedsignatures", authorizedSignatures);
+
+ props.Add("applicationpolicies", entry.GetArrayProperty(LDAPProperties.ApplicationPolicies));
+ props.Add("issuancepolicies", entry.GetArrayProperty(LDAPProperties.IssuancePolicies));
+
+
+ // Construct effectiveekus
+ string[] effectiveekus = schemaVersion == 1 & ekus.Length > 0 ? ekus : certificateapplicationpolicy;
+ props.Add("effectiveekus", effectiveekus);
+
+ // Construct authenticationenabled
+ bool authenticationenabled = effectiveekus.Intersect(Helpers.AuthenticationOIDs).Any() | effectiveekus.Length == 0;
+ props.Add("authenticationenabled", authenticationenabled);
+
+ 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
@@ -400,6 +550,9 @@ public Dictionary ParseAllProperties(ISearchResultEntry entry)
{
var props = new Dictionary();
+ var type = typeof(LDAPProperties);
+ var reserved = type.GetFields(BindingFlags.Static | BindingFlags.Public).Select(x => x.GetValue(null).ToString()).ToArray();
+
foreach (var property in entry.PropertyNames())
{
if (ReservedAttributes.Contains(property, StringComparer.OrdinalIgnoreCase))
@@ -460,6 +613,66 @@ private static object BestGuessConvert(string property)
return property;
}
+ ///
+ /// Converts PKIExpirationPeriod/PKIOverlappedPeriod attributes to time approximate times
+ ///
+ /// https://www.sysadmins.lv/blog-en/how-to-convert-pkiexirationperiod-and-pkioverlapperiod-active-directory-attributes.aspx
+ ///
+ /// Returns a string representing the time period associated with the input byte array in a human readable form
+ private static string ConvertPKIPeriod(byte[] bytes)
+ {
+ if (bytes == null || bytes.Length == 0)
+ return "Unknown";
+
+ try
+ {
+ Array.Reverse(bytes);
+ var temp = BitConverter.ToString(bytes).Replace("-", "");
+ var value = Convert.ToInt64(temp, 16) * -.0000001;
+
+ if (value % 31536000 == 0 && value / 31536000 >= 1)
+ {
+ if (value / 31536000 == 1) return "1 year";
+
+ return $"{value / 31536000} years";
+ }
+
+ if (value % 2592000 == 0 && value / 2592000 >= 1)
+ {
+ if (value / 2592000 == 1) return "1 month";
+
+ return $"{value / 2592000} months";
+ }
+
+ if (value % 604800 == 0 && value / 604800 >= 1)
+ {
+ if (value / 604800 == 1) return "1 week";
+
+ return $"{value / 604800} weeks";
+ }
+
+ if (value % 86400 == 0 && value / 86400 >= 1)
+ {
+ if (value / 86400 == 1) return "1 day";
+
+ return $"{value / 86400} days";
+ }
+
+ if (value % 3600 == 0 && value / 3600 >= 1)
+ {
+ if (value / 3600 == 1) return "1 hour";
+
+ return $"{value / 3600} hours";
+ }
+
+ return "";
+ }
+ catch (Exception)
+ {
+ return "Unknown";
+ }
+ }
+
[DllImport("Advapi32", SetLastError = false)]
private static extern bool IsTextUnicode(byte[] buf, int len, ref IsTextUnicodeFlags opt);
@@ -492,6 +705,46 @@ private enum IsTextUnicodeFlags
}
}
+ public class ParsedCertificate
+ {
+ public string Thumbprint { get; set; }
+ public string Name { get; set; }
+ public string[] Chain { get; set; } = Array.Empty();
+ public bool HasBasicConstraints { get; set; } = false;
+ public int BasicConstraintPathLength { get; set; }
+
+ public ParsedCertificate(byte[] rawCertificate)
+ {
+ var parsedCertificate = new X509Certificate2(rawCertificate);
+ Thumbprint = parsedCertificate.Thumbprint;
+ var name = parsedCertificate.FriendlyName;
+ Name = string.IsNullOrEmpty(name) ? Thumbprint : name;
+
+ // Chain
+ X509Chain chain = new X509Chain();
+ chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
+ chain.Build(parsedCertificate);
+ var temp = new List();
+ foreach (X509ChainElement cert in chain.ChainElements) temp.Add(cert.Certificate.Thumbprint);
+ Chain = temp.ToArray();
+
+ // Extensions
+ X509ExtensionCollection extensions = parsedCertificate.Extensions;
+ List certificateExtensions = new List();
+ foreach (X509Extension extension in extensions)
+ {
+ CertificateExtension certificateExtension = new CertificateExtension(extension);
+ switch (certificateExtension.Oid.Value)
+ {
+ case CAExtensionTypes.BasicConstraints:
+ X509BasicConstraintsExtension ext = (X509BasicConstraintsExtension)extension;
+ HasBasicConstraints = ext.HasPathLengthConstraint;
+ BasicConstraintPathLength = ext.PathLengthConstraint;
+ break;
+ }
+ }
+ }
+ }
public class UserProperties
{
diff --git a/src/CommonLib/Processors/RegistryResult.cs b/src/CommonLib/Processors/RegistryResult.cs
new file mode 100644
index 00000000..205e2346
--- /dev/null
+++ b/src/CommonLib/Processors/RegistryResult.cs
@@ -0,0 +1,9 @@
+using SharpHoundCommonLib.OutputTypes;
+
+namespace SharpHoundCommonLib.Processors
+{
+ public class RegistryResult : APIResult
+ {
+ public object Value { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/CommonLib/SearchResultEntryWrapper.cs b/src/CommonLib/SearchResultEntryWrapper.cs
index 81e4113f..0f0856e3 100644
--- a/src/CommonLib/SearchResultEntryWrapper.cs
+++ b/src/CommonLib/SearchResultEntryWrapper.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.DirectoryServices.Protocols;
using System.Linq;
+using System.Security.Cryptography.X509Certificates;
using System.Security.Principal;
using Microsoft.Extensions.Logging;
using SharpHoundCommonLib.Enums;
@@ -16,6 +17,8 @@ public interface ISearchResultEntry
byte[] GetByteProperty(string propertyName);
string[] GetArrayProperty(string propertyName);
byte[][] GetByteArrayProperty(string propertyName);
+ bool GetIntProperty(string propertyName, out int value);
+ X509Certificate2[] GetCertificateArrayProperty(string propertyName);
string GetObjectIdentifier();
bool IsDeleted();
Label GetLabel();
@@ -139,6 +142,7 @@ public ResolvedSearchResult ResolveBloodHoundInfo()
{
case Label.User:
case Label.Group:
+ case Label.Base:
res.DisplayName = $"{samAccountName}@{itemDomain}";
break;
case Label.Computer:
@@ -164,11 +168,14 @@ public ResolvedSearchResult ResolveBloodHoundInfo()
break;
case Label.OU:
case Label.Container:
+ case Label.Configuration:
+ case Label.RootCA:
+ case Label.AIACA:
+ case Label.NTAuthStore:
+ case Label.EnterpriseCA:
+ case Label.CertTemplate:
res.DisplayName = $"{GetProperty(LDAPProperties.Name)}@{itemDomain}";
break;
- case Label.Base:
- res.DisplayName = $"{samAccountName}@{itemDomain}";
- break;
default:
throw new ArgumentOutOfRangeException();
}
@@ -196,6 +203,16 @@ public byte[][] GetByteArrayProperty(string propertyName)
return _entry.GetPropertyAsArrayOfBytes(propertyName);
}
+ public bool GetIntProperty(string propertyName, out int value)
+ {
+ return _entry.GetPropertyAsInt(propertyName, out value);
+ }
+
+ public X509Certificate2[] GetCertificateArrayProperty(string propertyName)
+ {
+ return _entry.GetPropertyAsArrayOfCertificates(propertyName);
+ }
+
public string GetObjectIdentifier()
{
return _entry.GetObjectIdentifier();
diff --git a/test/unit/ACLProcessorTest.cs b/test/unit/ACLProcessorTest.cs
index f5654234..0aa6c1df 100644
--- a/test/unit/ACLProcessorTest.cs
+++ b/test/unit/ACLProcessorTest.cs
@@ -55,7 +55,7 @@ public void SanityCheck()
public void ACLProcessor_IsACLProtected_NullNTSD_ReturnsFalse()
{
var processor = new ACLProcessor(new MockLDAPUtils(), true);
- var result = processor.IsACLProtected((byte[]) null);
+ var result = processor.IsACLProtected((byte[])null);
Assert.False(result);
}
@@ -206,7 +206,7 @@ public void ACLProcessor_ProcessGMSAReaders_Null_PrincipalID()
var collection = new List();
mockRule.Setup(x => x.AccessControlType()).Returns(AccessControlType.Allow);
- mockRule.Setup(x => x.IdentityReference()).Returns((string) null);
+ mockRule.Setup(x => x.IdentityReference()).Returns((string)null);
collection.Add(mockRule.Object);
mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny()))
@@ -267,7 +267,7 @@ public void ACLProcessor_ProcessACL_Null_SID()
mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(collection);
- mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null);
+ mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null);
mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object);
var processor = new ACLProcessor(mockLDAPUtils.Object, true);
@@ -287,7 +287,7 @@ public void ACLProcessor_ProcessACL_Null_ACE()
mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(collection);
- mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null);
+ mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null);
mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object);
var processor = new ACLProcessor(mockLDAPUtils.Object, true);
@@ -309,7 +309,7 @@ public void ACLProcessor_ProcessACL_Deny_ACE()
mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(collection);
- mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null);
+ mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null);
mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object);
var processor = new ACLProcessor(mockLDAPUtils.Object, true);
@@ -332,7 +332,7 @@ public void ACLProcessor_ProcessACL_Unmatched_Inheritance_ACE()
mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(collection);
- mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null);
+ mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null);
mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object);
var processor = new ACLProcessor(mockLDAPUtils.Object, true);
@@ -351,12 +351,12 @@ public void ACLProcessor_ProcessACL_Null_SID_ACE()
var collection = new List();
mockRule.Setup(x => x.AccessControlType()).Returns(AccessControlType.Allow);
mockRule.Setup(x => x.IsAceInheritedFrom(It.IsAny())).Returns(true);
- mockRule.Setup(x => x.IdentityReference()).Returns((string) null);
+ mockRule.Setup(x => x.IdentityReference()).Returns((string)null);
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);
+ mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string)null);
mockLDAPUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(mockSecurityDescriptor.Object);
var processor = new ACLProcessor(mockLDAPUtils.Object, true);
@@ -386,7 +386,7 @@ public void ACLProcessor_ProcessACL_GenericAll_Unmatched_Guid()
mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(collection);
- mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null);
+ 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()))
.Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType));
@@ -417,7 +417,7 @@ public void ACLProcessor_ProcessACL_GenericAll()
mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(collection);
- mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null);
+ 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()))
.Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType));
@@ -454,7 +454,7 @@ public void ACLProcessor_ProcessACL_WriteDacl()
mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(collection);
- mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null);
+ 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()))
.Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType));
@@ -491,7 +491,7 @@ public void ACLProcessor_ProcessACL_WriteOwner()
mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(collection);
- mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null);
+ 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()))
.Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType));
@@ -528,7 +528,7 @@ public void ACLProcessor_ProcessACL_Self()
mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(collection);
- mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null);
+ 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()))
.Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType));
@@ -550,7 +550,6 @@ public void ACLProcessor_ProcessACL_ExtendedRight_Domain_Unmatched()
{
var expectedPrincipalType = Label.Group;
var expectedPrincipalSID = "S-1-5-21-3130019616-2776909439-2417379446-512";
- var expectedRightName = EdgeNames.AddSelf;
var mockLDAPUtils = new Mock();
var mockSecurityDescriptor = new Mock(MockBehavior.Loose, null);
@@ -565,7 +564,7 @@ public void ACLProcessor_ProcessACL_ExtendedRight_Domain_Unmatched()
mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(collection);
- mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null);
+ 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()))
.Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType));
@@ -597,7 +596,7 @@ public void ACLProcessor_ProcessACL_ExtendedRight_Domain_DSReplicationGetChanges
mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(collection);
- mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null);
+ 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()))
.Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType));
@@ -634,7 +633,7 @@ public void ACLProcessor_ProcessACL_ExtendedRight_Domain_All()
mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(collection);
- mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null);
+ 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()))
.Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType));
@@ -671,7 +670,7 @@ public void ACLProcessor_ProcessACL_ExtendedRight_Domain_DSReplicationGetChanges
mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(collection);
- mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null);
+ 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()))
.Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType));
@@ -709,7 +708,7 @@ public void ACLProcessor_ProcessACL_ExtendedRight_User_Unmatched()
mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(collection);
- mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null);
+ 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()))
.Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType));
@@ -741,7 +740,7 @@ public void ACLProcessor_ProcessACL_ExtendedRight_User_UserForceChangePassword()
mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(collection);
- mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null);
+ 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()))
.Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType));
@@ -778,7 +777,7 @@ public void ACLProcessor_ProcessACL_ExtendedRight_User_All()
mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(collection);
- mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null);
+ 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()))
.Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType));
@@ -815,7 +814,7 @@ public void ACLProcessor_ProcessACL_ExtendedRight_Computer_NoLAPS()
mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(collection);
- mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null);
+ 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()))
.Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType));
@@ -847,7 +846,7 @@ public void ACLProcessor_ProcessACL_ExtendedRight_Computer_All()
mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(collection);
- mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null);
+ 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()))
.Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType));
@@ -889,7 +888,7 @@ public void ACLProcessor_ProcessACL_GenericWrite_Unmatched()
mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(collection);
- mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null);
+ 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()))
.Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType));
@@ -921,7 +920,7 @@ public void ACLProcessor_ProcessACL_GenericWrite_User_All()
mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(collection);
- mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null);
+ 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()))
.Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType));
@@ -958,7 +957,7 @@ public void ACLProcessor_ProcessACL_GenericWrite_User_WriteMember()
mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(collection);
- mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null);
+ 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()))
.Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType));
@@ -997,7 +996,7 @@ public void ACLProcessor_ProcessACL_GenericWrite_Computer_WriteAllowedToAct()
mockSecurityDescriptor.Setup(m => m.GetAccessRules(It.IsAny(), It.IsAny(), It.IsAny()))
.Returns(collection);
- mockSecurityDescriptor.Setup(m => m.GetOwner(It.IsAny())).Returns((string) null);
+ 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()))
.Returns(new TypedPrincipal(expectedPrincipalSID, expectedPrincipalType));
diff --git a/test/unit/CertAbuseProcessorTest.cs b/test/unit/CertAbuseProcessorTest.cs
new file mode 100644
index 00000000..6ecf8a0c
--- /dev/null
+++ b/test/unit/CertAbuseProcessorTest.cs
@@ -0,0 +1,143 @@
+using System;
+using System.DirectoryServices;
+using CommonLibTest.Facades;
+using Moq;
+using Newtonsoft.Json;
+using SharpHoundCommonLib;
+using SharpHoundCommonLib.Enums;
+using SharpHoundCommonLib.Processors;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace CommonLibTest
+{
+ public class CertAbuseProcessorTest : IDisposable
+ {
+ private const string CASecurityFixture =
+ "AQAUhCABAAAwAQAAFAAAAEQAAAACADAAAgAAAALAFAD//wAAAQEAAAAAAAEAAAAAAsAUAP//AAABAQAAAAAABQcAAAACANwABwAAAAADGAABAAAAAQIAAAAAAAUgAAAAIAIAAAADGAACAAAAAQIAAAAAAAUgAAAAIAIAAAADJAABAAAAAQUAAAAAAAUVAAAAIE+Qun9GhKV2SBaQAAIAAAADJAACAAAAAQUAAAAAAAUVAAAAIE+Qun9GhKV2SBaQAAIAAAADJAABAAAAAQUAAAAAAAUVAAAAIE+Qun9GhKV2SBaQBwIAAAADJAACAAAAAQUAAAAAAAUVAAAAIE+Qun9GhKV2SBaQBwIAAAADFAAAAgAAAQEAAAAAAAULAAAAAQIAAAAAAAUgAAAAIAIAAAECAAAAAAAFIAAAACACAAA=";
+
+ private readonly ITestOutputHelper _testOutputHelper;
+
+ public CertAbuseProcessorTest(ITestOutputHelper testOutputHelper)
+ {
+ _testOutputHelper = testOutputHelper;
+ }
+
+ public void Dispose()
+ {
+ }
+
+ // [Fact]
+ // public void CertAbuseProcessor_GetCASecurity_HappyPath()
+ // {
+ // var mockProcessor = new Mock(new MockLDAPUtils(), null);
+ //
+ // var mockRegistryKey = new Mock();
+ // mockRegistryKey.Setup(x => x.GetValue(It.IsAny(), It.IsAny()))
+ // .Returns(new byte[] { 0x20, 0x20 });
+ // mockProcessor.Setup(x => x.OpenRemoteRegistry(It.IsAny())).Returns(mockRegistryKey.Object);
+ //
+ // var processor = mockProcessor.Object;
+ // var results = processor.GetCASecurity("testlab.local", "blah");
+ // Assert.True(results.Collected);
+ // }
+
+ // [Fact]
+ // public void CertAbuseProcessor_GetTrustedCerts_EmptyForNonRoot()
+ // {
+ // var mockUtils = new Mock();
+ // mockUtils.Setup(x => x.IsForestRoot(It.IsAny())).Returns(false);
+ // var processor = new CertAbuseProcessor(mockUtils.Object);
+ //
+ // var results = processor.GetTrustedCerts("testlab.local");
+ // Assert.Empty(results);
+ // }
+ //
+ // [Fact]
+ // public void CertAbuseProcessor_GetTrustedCerts_NullConfigPath_ReturnsEmpty()
+ // {
+ // var mockUtils = new Mock();
+ // mockUtils.Setup(x => x.IsForestRoot(It.IsAny())).Returns(true);
+ // mockUtils.Setup(x => x.GetConfigurationPath(It.IsAny())).Returns((string)null);
+ // var processor = new CertAbuseProcessor(mockUtils.Object);
+ //
+ // var results = processor.GetTrustedCerts("testlab.local");
+ // Assert.Empty(results);
+ // }
+ //
+ // [Fact]
+ // public void CertAbuseProcessor_GetRootCAs_EmptyForNonRoot()
+ // {
+ // var mockUtils = new Mock();
+ // mockUtils.Setup(x => x.IsForestRoot(It.IsAny())).Returns(false);
+ // var processor = new CertAbuseProcessor(mockUtils.Object);
+ //
+ // var results = processor.GetRootCAs("testlab.local");
+ // Assert.Empty(results);
+ // }
+ //
+ // [Fact]
+ // public void CertAbuseProcessor_GetRootCAs_NullConfigPath_ReturnsEmpty()
+ // {
+ // var mockUtils = new Mock();
+ // mockUtils.Setup(x => x.IsForestRoot(It.IsAny())).Returns(true);
+ // mockUtils.Setup(x => x.GetConfigurationPath(It.IsAny())).Returns((string)null);
+ // var processor = new CertAbuseProcessor(mockUtils.Object);
+ //
+ // var results = processor.GetRootCAs("testlab.local");
+ // Assert.Empty(results);
+ // }
+
+ // [Fact]
+ // public void CertAbuseProcessor_ProcessCAPermissions_NullSecurity_ReturnsNull()
+ // {
+ // var mockUtils = new Mock();
+ // var processor = new CertAbuseProcessor(mockUtils.Object);
+
+ // var results = processor.ProcessRegistryEnrollmentPermissions(null, null, "test");
+
+ // Assert.Empty(results);
+ // }
+
+ // [WindowsOnlyFact]
+ // public void CertAbuseProcessor_ProcessCAPermissions_ReturnsCorrectValues()
+ // {
+ // var mockUtils = new Mock();
+ // var sd = new ActiveDirectorySecurityDescriptor(new ActiveDirectorySecurity());
+ // mockUtils.Setup(x => x.MakeSecurityDescriptor()).Returns(sd);
+ // var processor = new CertAbuseProcessor(mockUtils.Object);
+ // var bytes = Helpers.B64ToBytes(CASecurityFixture);
+
+ // var results = processor.ProcessCAPermissions(bytes, "TESTLAB.LOCAL", "test", false);
+ // _testOutputHelper.WriteLine(JsonConvert.SerializeObject(results, Formatting.Indented));
+ // Assert.Contains(results,
+ // x => x.RightName == EdgeNames.Owns && x.PrincipalSID == "TESTLAB.LOCAL-S-1-5-32-544" &&
+ // x.PrincipalType == Label.Group && !x.IsInherited);
+ // Assert.Contains(results,
+ // x => x.RightName == EdgeNames.Enroll && x.PrincipalSID == "TESTLAB.LOCAL-S-1-5-11" &&
+ // !x.IsInherited);
+ // Assert.Contains(results,
+ // x => x.RightName == EdgeNames.ManageCA && x.PrincipalSID == "TESTLAB.LOCAL-S-1-5-32-544" &&
+ // !x.IsInherited);
+ // Assert.Contains(results,
+ // x => x.RightName == EdgeNames.ManageCertificates && x.PrincipalSID == "TESTLAB.LOCAL-S-1-5-32-544" &&
+ // !x.IsInherited);
+ // Assert.Contains(results,
+ // x => x.RightName == EdgeNames.ManageCA &&
+ // x.PrincipalSID == "S-1-5-21-3130019616-2776909439-2417379446-512" &&
+ // !x.IsInherited);
+ // Assert.Contains(results,
+ // x => x.RightName == EdgeNames.ManageCertificates &&
+ // x.PrincipalSID == "S-1-5-21-3130019616-2776909439-2417379446-512" &&
+ // !x.IsInherited);
+ // Assert.Contains(results,
+ // x => x.RightName == EdgeNames.ManageCA &&
+ // x.PrincipalSID == "S-1-5-21-3130019616-2776909439-2417379446-519" &&
+ // !x.IsInherited);
+ // Assert.Contains(results,
+ // x => x.RightName == EdgeNames.ManageCertificates &&
+ // x.PrincipalSID == "S-1-5-21-3130019616-2776909439-2417379446-519" &&
+ // !x.IsInherited);
+ // }
+ }
+}
\ No newline at end of file
diff --git a/test/unit/Facades/MockLDAPUtils.cs b/test/unit/Facades/MockLDAPUtils.cs
index 85552c01..af088a5b 100644
--- a/test/unit/Facades/MockLDAPUtils.cs
+++ b/test/unit/Facades/MockLDAPUtils.cs
@@ -1052,11 +1052,41 @@ public ActiveDirectorySecurityDescriptor MakeSecurityDescriptor()
return mockSecurityDescriptor.Object;
}
+ public string BuildLdapPath(string dnPath, string domain)
+ {
+ throw new NotImplementedException();
+ }
+
private Group GetBaseEnterpriseDC()
{
var g = new Group {ObjectIdentifier = "TESTLAB.LOCAL-S-1-5-9".ToUpper()};
g.Properties.Add("name", "ENTERPRISE DOMAIN CONTROLLERS@TESTLAB.LOCAL".ToUpper());
return g;
}
+
+ public TypedPrincipal ResolveCertTemplateByCN(string cn, string containerDN, string domainName)
+ {
+ throw new NotImplementedException();
+ }
+
+ public string GetConfigurationPath(string domainName)
+ {
+ throw new NotImplementedException();
+ }
+
+ public string GetSchemaPath(string domainName)
+ {
+ throw new NotImplementedException();
+ }
+
+ TypedPrincipal ILDAPUtils.ResolveCertTemplateByProperty(string propValue, string propName, string containerDN, string domainName)
+ {
+ throw new NotImplementedException();
+ }
+
+ public bool IsDomainController(string computerObjectId, string domainName)
+ {
+ throw new NotImplementedException();
+ }
}
}
\ No newline at end of file
diff --git a/test/unit/Facades/MockSearchResultEntry.cs b/test/unit/Facades/MockSearchResultEntry.cs
index 1ab1140a..63f7cc97 100644
--- a/test/unit/Facades/MockSearchResultEntry.cs
+++ b/test/unit/Facades/MockSearchResultEntry.cs
@@ -1,6 +1,9 @@
using System;
using System.Collections;
using System.Collections.Generic;
+using System.Linq;
+using System.Security.Cryptography.X509Certificates;
+using System.Text;
using SharpHoundCommonLib;
using SharpHoundCommonLib.Enums;
@@ -35,17 +38,47 @@ public string GetProperty(string propertyName)
public byte[] GetByteProperty(string propertyName)
{
+ if (!_properties.Contains(propertyName))
+ return null;
+
+ if (_properties[propertyName] is string prop)
+ {
+ return Encoding.ASCII.GetBytes(prop);
+ }
+
return _properties[propertyName] as byte[];
}
public string[] GetArrayProperty(string propertyName)
{
- return _properties[propertyName] as string[];
+ if (!_properties.Contains(propertyName))
+ return Array.Empty();
+
+ var value = _properties[propertyName];
+ if (value.IsArray())
+ return value as string[];
+
+ return new [] { (value ?? "").ToString() };
}
public byte[][] GetByteArrayProperty(string propertyName)
{
- return _properties[propertyName] as byte[][];
+ if (!_properties.Contains(propertyName))
+ return Array.Empty();
+
+ var property = _properties[propertyName] as byte[][];
+ return property;
+ }
+
+ public bool GetIntProperty(string propertyName, out int value)
+ {
+ value = _properties[propertyName] is int ? (int)_properties[propertyName] : 0;
+ return true;
+ }
+
+ public X509Certificate2[] GetCertificateArrayProperty(string propertyName)
+ {
+ return GetByteArrayProperty(propertyName).Select(x => new X509Certificate2(x)).ToArray();
}
public string GetObjectIdentifier()
@@ -75,12 +108,19 @@ public string GetGuid()
public int PropCount(string prop)
{
- throw new NotImplementedException();
+ var property = _properties[prop];
+ if (property.IsArray())
+ {
+ var cast = property as string[];
+ return cast?.Length ?? 0;
+ }
+
+ return 1;
}
public IEnumerable PropertyNames()
{
- throw new NotImplementedException();
+ foreach (var property in _properties.Keys) yield return property.ToString().ToLower();
}
public bool IsMSA()
diff --git a/test/unit/Helpers.cs b/test/unit/Helpers.cs
index 44877516..fa3f1482 100644
--- a/test/unit/Helpers.cs
+++ b/test/unit/Helpers.cs
@@ -33,6 +33,14 @@ internal static async Task ToArrayAsync(this IAsyncEnumerable items,
results.Add(item);
return results.ToArray();
}
+
+ internal static bool IsArray(this object obj)
+ {
+ var valueType = obj?.GetType();
+ if (valueType == null)
+ return false;
+ return valueType.IsArray;
+ }
}
public sealed class WindowsOnlyFact : FactAttribute
diff --git a/test/unit/LDAPPropertyTests.cs b/test/unit/LDAPPropertyTests.cs
index 7099d196..ab71cf83 100644
--- a/test/unit/LDAPPropertyTests.cs
+++ b/test/unit/LDAPPropertyTests.cs
@@ -120,7 +120,7 @@ public void LDAPPropertyProcessor_ReadGroupProperties_TestGoodData()
Assert.Contains("description", test.Keys);
Assert.Equal("Test", test["description"] as string);
Assert.Contains("admincount", test.Keys);
- Assert.True((bool) test["admincount"]);
+ Assert.True((bool)test["admincount"]);
}
[Fact]
@@ -137,7 +137,7 @@ public void LDAPPropertyProcessor_ReadGroupProperties_TestGoodData_FalseAdminCou
Assert.Contains("description", test.Keys);
Assert.Equal("Test", test["description"] as string);
Assert.Contains("admincount", test.Keys);
- Assert.False((bool) test["admincount"]);
+ Assert.False((bool)test["admincount"]);
}
[Fact]
@@ -153,7 +153,7 @@ public void LDAPPropertyProcessor_ReadGroupProperties_NullAdminCount()
Assert.Contains("description", test.Keys);
Assert.Equal("Test", test["description"] as string);
Assert.Contains("admincount", test.Keys);
- Assert.False((bool) test["admincount"]);
+ Assert.False((bool)test["admincount"]);
}
[Fact]
@@ -250,7 +250,7 @@ public async Task LDAPPropertyProcessor_ReadUserProperties_NullAdminCount()
var props = test.Props;
var keys = props.Keys;
Assert.Contains("admincount", keys);
- Assert.False((bool) props["admincount"]);
+ Assert.False((bool)props["admincount"]);
}
[WindowsOnlyFact]
@@ -289,33 +289,33 @@ public async Task LDAPPropertyProcessor_ReadUserProperties_HappyPath()
Assert.Contains("description", keys);
Assert.Equal("Test", props["description"] as string);
Assert.Contains("admincount", keys);
- Assert.True((bool) props["admincount"]);
+ Assert.True((bool)props["admincount"]);
Assert.Contains("lastlogon", keys);
- Assert.Equal(1622827514, (long) props["lastlogon"]);
+ Assert.Equal(1622827514, (long)props["lastlogon"]);
Assert.Contains("lastlogontimestamp", keys);
- Assert.Equal(1622558209, (long) props["lastlogontimestamp"]);
+ Assert.Equal(1622558209, (long)props["lastlogontimestamp"]);
Assert.Contains("pwdlastset", keys);
- Assert.Equal(1568693134, (long) props["pwdlastset"]);
+ Assert.Equal(1568693134, (long)props["pwdlastset"]);
Assert.Contains("homedirectory", keys);
Assert.Equal(@"\\win10\testdir", props["homedirectory"] as string);
//UAC stuff
Assert.Contains("sensitive", keys);
- Assert.False((bool) props["sensitive"]);
+ Assert.False((bool)props["sensitive"]);
Assert.Contains("dontreqpreauth", keys);
- Assert.False((bool) props["dontreqpreauth"]);
+ Assert.False((bool)props["dontreqpreauth"]);
Assert.Contains("passwordnotreqd", keys);
- Assert.False((bool) props["passwordnotreqd"]);
+ Assert.False((bool)props["passwordnotreqd"]);
Assert.Contains("unconstraineddelegation", keys);
- Assert.False((bool) props["unconstraineddelegation"]);
+ Assert.False((bool)props["unconstraineddelegation"]);
Assert.Contains("enabled", keys);
- Assert.True((bool) props["enabled"]);
+ Assert.True((bool)props["enabled"]);
Assert.Contains("trustedtoauth", keys);
- Assert.False((bool) props["trustedtoauth"]);
+ Assert.False((bool)props["trustedtoauth"]);
//SPN
Assert.Contains("hasspn", keys);
- Assert.True((bool) props["hasspn"]);
+ Assert.True((bool)props["hasspn"]);
Assert.Contains("serviceprincipalnames", keys);
Assert.Contains("MSSQLSVC/win10", props["serviceprincipalnames"] as string[]);
@@ -367,7 +367,7 @@ public async Task LDAPPropertyProcessor_ReadUserProperties_TestBadPaths()
Assert.Contains("sidhistory", keys);
Assert.Empty(props["sidhistory"] as string[]);
Assert.Contains("admincount", keys);
- Assert.False((bool) props["admincount"]);
+ Assert.False((bool)props["admincount"]);
Assert.Contains("sensitive", keys);
Assert.Contains("dontreqpreauth", keys);
Assert.Contains("passwordnotreqd", keys);
@@ -375,13 +375,13 @@ public async Task LDAPPropertyProcessor_ReadUserProperties_TestBadPaths()
Assert.Contains("pwdneverexpires", keys);
Assert.Contains("enabled", keys);
Assert.Contains("trustedtoauth", keys);
- Assert.False((bool) props["trustedtoauth"]);
- Assert.False((bool) props["sensitive"]);
- Assert.False((bool) props["dontreqpreauth"]);
- Assert.False((bool) props["passwordnotreqd"]);
- Assert.False((bool) props["unconstraineddelegation"]);
- Assert.False((bool) props["pwdneverexpires"]);
- Assert.True((bool) props["enabled"]);
+ Assert.False((bool)props["trustedtoauth"]);
+ Assert.False((bool)props["sensitive"]);
+ Assert.False((bool)props["dontreqpreauth"]);
+ Assert.False((bool)props["passwordnotreqd"]);
+ Assert.False((bool)props["unconstraineddelegation"]);
+ Assert.False((bool)props["pwdneverexpires"]);
+ Assert.True((bool)props["enabled"]);
}
[WindowsOnlyFact]
@@ -437,15 +437,15 @@ public async Task LDAPPropertyProcessor_ReadComputerProperties_HappyPath()
Assert.Contains("lastlogon", keys);
Assert.Contains("lastlogontimestamp", keys);
Assert.Contains("pwdlastset", keys);
- Assert.True((bool) props["enabled"]);
- Assert.False((bool) props["unconstraineddelegation"]);
+ Assert.True((bool)props["enabled"]);
+ Assert.False((bool)props["unconstraineddelegation"]);
Assert.Contains("lastlogon", keys);
- Assert.Equal(1622827514, (long) props["lastlogon"]);
+ Assert.Equal(1622827514, (long)props["lastlogon"]);
Assert.Contains("lastlogontimestamp", keys);
- Assert.Equal(1622558209, (long) props["lastlogontimestamp"]);
+ Assert.Equal(1622558209, (long)props["lastlogontimestamp"]);
Assert.Contains("pwdlastset", keys);
- Assert.Equal(1568693134, (long) props["pwdlastset"]);
+ Assert.Equal(1568693134, (long)props["pwdlastset"]);
//AllowedToDelegate
Assert.Single(test.AllowedToDelegate);
@@ -524,13 +524,14 @@ public async Task LDAPPropertyProcessor_ReadComputerProperties_TestBadPaths()
Assert.Contains("unconstraineddelegation", keys);
Assert.Contains("enabled", keys);
Assert.Contains("trustedtoauth", keys);
- Assert.False((bool) props["unconstraineddelegation"]);
- Assert.True((bool) props["enabled"]);
- Assert.False((bool) props["trustedtoauth"]);
+ Assert.False((bool)props["unconstraineddelegation"]);
+ Assert.True((bool)props["enabled"]);
+ Assert.False((bool)props["trustedtoauth"]);
Assert.Contains("sidhistory", keys);
Assert.Empty(props["sidhistory"] as string[]);
}
+
[Fact]
public async Task LDAPPropertyProcessor_ReadComputerProperties_TestDumpSMSAPassword()
{
@@ -602,6 +603,234 @@ public async Task LDAPPropertyProcessor_ReadComputerProperties_TestDumpSMSAPassw
}
- // //TODO: Add coverage for ParseAllProperties
+ [Fact]
+ public void LDAPPropertyProcessor_ReadRootCAProperties()
+ {
+ var mock = new MockSearchResultEntry(
+ "CN\u003dDUMPSTER-DC01-CA,CN\u003dCERTIFICATION AUTHORITIES,CN\u003dPUBLIC KEY SERVICES,CN\u003dSERVICES,CN\u003dCONFIGURATION,DC\u003dDUMPSTER,DC\u003dFIRE",
+ new Dictionary
+ {
+ {"description", null},
+ {"domain", "DUMPSTER.FIRE"},
+ {"name", "DUMPSTER-DC01-CA@DUMPSTER.FIRE"},
+ {"domainsid", "S-1-5-21-2697957641-2271029196-387917394"},
+ {"whencreated", 1683986131},
+ }, "2F9F3630-F46A-49BF-B186-6629994EBCF9", Label.RootCA);
+
+ var test = LDAPPropertyProcessor.ReadRootCAProperties(mock);
+ var keys = test.Keys;
+
+ //These are not common properties
+ Assert.DoesNotContain("domain", keys);
+ Assert.DoesNotContain("name", keys);
+ Assert.DoesNotContain("domainsid", keys);
+
+ Assert.Contains("description", keys);
+ Assert.Contains("whencreated", keys);
+ }
+
+ [Fact]
+ public void LDAPPropertyProcessor_ReadAIACAProperties()
+ {
+ var mock = new MockSearchResultEntry(
+ "CN\u003dDUMPSTER-DC01-CA,CN\u003dAIA,CN\u003dPUBLIC KEY SERVICES,CN\u003dSERVICES,CN\u003dCONFIGURATION,DC\u003dDUMPSTER,DC\u003dFIRE",
+ new Dictionary
+ {
+ {"description", null},
+ {"domain", "DUMPSTER.FIRE"},
+ {"name", "DUMPSTER-DC01-CA@DUMPSTER.FIRE"},
+ {"domainsid", "S-1-5-21-2697957641-2271029196-387917394"},
+ {"whencreated", 1683986131},
+ {"hascrosscertificatepair", true},
+ }, "2F9F3630-F46A-49BF-B186-6629994EBCF9", Label.AIACA);
+
+ var test = LDAPPropertyProcessor.ReadAIACAProperties(mock);
+ var keys = test.Keys;
+
+ //These are not common properties
+ Assert.DoesNotContain("domain", keys);
+ Assert.DoesNotContain("name", keys);
+ Assert.DoesNotContain("domainsid", keys);
+
+ Assert.Contains("description", keys);
+ Assert.Contains("whencreated", keys);
+ Assert.Contains("crosscertificatepair", keys);
+ }
+
+ [Fact]
+ public void LDAPPropertyProcessor_ReadNTAuthStoreProperties()
+ {
+ var mock = new MockSearchResultEntry("CN\u003dNTAUTHCERTIFICATES,CN\u003dPUBLIC KEY SERVICES,CN\u003dSERVICES,CN\u003dCONFIGURATION,DC\u003dDUMPSTER,DC\u003dFIRE",
+ new Dictionary
+ {
+ {"description", null},
+ {"domain", "DUMPSTER.FIRE"},
+ {"name", "NTAUTHCERTIFICATES@DUMPSTER.FIRE"},
+ {"domainsid", "S-1-5-21-2697957641-2271029196-387917394"},
+ {"whencreated", 1683986131},
+ }, "2F9F3630-F46A-49BF-B186-6629994EBCF9", Label.NTAuthStore);
+
+ var test = LDAPPropertyProcessor.ReadNTAuthStoreProperties(mock);
+ var keys = test.Keys;
+
+ //These are not common properties
+ Assert.DoesNotContain("domain", keys);
+ Assert.DoesNotContain("name", keys);
+ Assert.DoesNotContain("domainsid", keys);
+
+ Assert.Contains("description", keys);
+ Assert.Contains("whencreated", keys);
+ }
+
+ [Fact]
+ public void LDAPPropertyProcessor_ReadCertTemplateProperties()
+ {
+ var mock = new MockSearchResultEntry("CN\u003dWORKSTATION,CN\u003dCERTIFICATE TEMPLATES,CN\u003dPUBLIC KEY SERVICES,CN\u003dSERVICES,CN\u003dCONFIGURATION,DC\u003dEXTERNAL,DC\u003dLOCAL",
+ new Dictionary
+ {
+ {"domain", "EXTERNAL.LOCAL"},
+ {"name", "WORKSTATION@EXTERNAL.LOCAL"},
+ {"domainsid", "S-1-5-21-3702535222-3822678775-2090119576"},
+ {"description", null},
+ {"whencreated", 1683986183},
+ {"validityperiod", 31536000},
+ {"renewalperiod", 3628800},
+ {"schemaversion", 2},
+ {"displayname", "Workstation Authentication"},
+ {"oid", "1.3.6.1.4.1.311.21.8.4571196.1884641.3293620.10686285.12068043.134.1.30"},
+ {"enrollmentflag", 32},
+ {"requiresmanagerapproval", false},
+ {"certificatenameflag", 134217728},
+ {"enrolleesuppliessubject", false},
+ {"subjectaltrequireupn", false},
+ {"ekus", new[]
+ {"1.3.6.1.5.5.7.3.2"}
+ },
+ {"certificateapplicationpolicy", new[]
+ {"1.3.6.1.5.5.7.3.2"}
+ },
+ {"authorizedsignatures", 1},
+ {"applicationpolicies", new[]
+ { "1.3.6.1.4.1.311.20.2.1"}
+ },
+ {"issuancepolicies", new[]
+ {"1.3.6.1.4.1.311.21.8.4571196.1884641.3293620.10686285.12068043.134.1.400",
+ "1.3.6.1.4.1.311.21.8.4571196.1884641.3293620.10686285.12068043.134.1.402"}
+ },
+ }, "2F9F3630-F46A-49BF-B186-6629994EBCF9", Label.CertTemplate);
+
+ var test = LDAPPropertyProcessor.ReadCertTemplateProperties(mock);
+ var keys = test.Keys;
+
+ //These are not common properties
+ Assert.DoesNotContain("domain", keys);
+ Assert.DoesNotContain("name", keys);
+ Assert.DoesNotContain("domainsid", keys);
+
+ Assert.Contains("description", keys);
+ Assert.Contains("whencreated", keys);
+ Assert.Contains("validityperiod", keys);
+ Assert.Contains("renewalperiod", keys);
+ Assert.Contains("schemaversion", keys);
+ Assert.Contains("displayname", keys);
+ Assert.Contains("oid", keys);
+ Assert.Contains("enrollmentflag", keys);
+ Assert.Contains("requiresmanagerapproval", keys);
+ Assert.Contains("certificatenameflag", keys);
+ Assert.Contains("enrolleesuppliessubject", keys);
+ Assert.Contains("subjectaltrequireupn", keys);
+ Assert.Contains("ekus", keys);
+ Assert.Contains("certificateapplicationpolicy", keys);
+ Assert.Contains("authorizedsignatures", keys);
+ Assert.Contains("applicationpolicies", keys);
+ Assert.Contains("issuancepolicies", keys);
+
+ }
+
+ [Fact]
+ public void LDAPPropertyProcessor_ParseAllProperties()
+ {
+ var mock = new MockSearchResultEntry("CN\u003dNTAUTHCERTIFICATES,CN\u003dPUBLIC KEY SERVICES,CN\u003dSERVICES,CN\u003dCONFIGURATION,DC\u003dDUMPSTER,DC\u003dFIRE",
+ new Dictionary
+ {
+ {"description", null},
+ {"domain", "DUMPSTER.FIRE"},
+ {"name", "NTAUTHCERTIFICATES@DUMPSTER.FIRE"},
+ {"domainsid", "S-1-5-21-2697957641-2271029196-387917394"},
+ {"whencreated", 1683986131},
+ }, "2F9F3630-F46A-49BF-B186-6629994EBCF9", Label.NTAuthStore);
+
+ var processor = new LDAPPropertyProcessor(new MockLDAPUtils());
+ var props = processor.ParseAllProperties(mock);
+ var keys = props.Keys;
+
+ //These are reserved properties and so they should be filtered out
+ Assert.DoesNotContain("description", keys);
+ Assert.DoesNotContain("whencreated", keys);
+ Assert.DoesNotContain("name", keys);
+
+ Assert.Contains("domainsid", keys);
+ Assert.Contains("domain", keys);
+ }
+
+ [Fact]
+ public void LDAPPropertyProcessor_ParseAllProperties_NoProperties()
+ {
+ var mock = new MockSearchResultEntry("CN\u003dNTAUTHCERTIFICATES,CN\u003dPUBLIC KEY SERVICES,CN\u003dSERVICES,CN\u003dCONFIGURATION,DC\u003dDUMPSTER,DC\u003dFIRE",
+ new Dictionary
+ { }, "2F9F3630-F46A-49BF-B186-6629994EBCF9", Label.NTAuthStore);
+
+ var processor = new LDAPPropertyProcessor(new MockLDAPUtils());
+ var props = processor.ParseAllProperties(mock);
+ var keys = props.Keys;
+
+ Assert.Empty(keys);
+
+ }
+
+ [Fact]
+ public void LDAPPropertyProcessor_ParseAllProperties_CollectionCountOne_NullString()
+ {
+ var mock = new MockSearchResultEntry("CN\u003dNTAUTHCERTIFICATES,CN\u003dPUBLIC KEY SERVICES,CN\u003dSERVICES,CN\u003dCONFIGURATION,DC\u003dDUMPSTER,DC\u003dFIRE",
+ new Dictionary
+ {{"domainsid", null} }, "2F9F3630-F46A-49BF-B186-6629994EBCF9", Label.NTAuthStore);
+
+ var processor = new LDAPPropertyProcessor(new MockLDAPUtils());
+ var props = processor.ParseAllProperties(mock);
+ var keys = props.Keys;
+
+ Assert.Empty(keys);
+ }
+
+ [Fact]
+ public void LDAPPropertyProcessor_ParseAllProperties_CollectionCountOne_BadPasswordTime()
+ {
+ var mock = new MockSearchResultEntry("CN\u003dNTAUTHCERTIFICATES,CN\u003dPUBLIC KEY SERVICES,CN\u003dSERVICES,CN\u003dCONFIGURATION,DC\u003dDUMPSTER,DC\u003dFIRE",
+ new Dictionary
+ {{"badpasswordtime", "130435290000000000"} }, "2F9F3630-F46A-49BF-B186-6629994EBCF9", Label.NTAuthStore);
+
+ var processor = new LDAPPropertyProcessor(new MockLDAPUtils());
+ var props = processor.ParseAllProperties(mock);
+ var keys = props.Keys;
+
+ Assert.Contains("badpasswordtime", keys);
+ Assert.Single(keys);
+ }
+
+ [Fact]
+ public void LDAPPropertyProcessor_ParseAllProperties_CollectionCountOne_NotBadPasswordTime()
+ {
+ var mock = new MockSearchResultEntry("CN\u003dNTAUTHCERTIFICATES,CN\u003dPUBLIC KEY SERVICES,CN\u003dSERVICES,CN\u003dCONFIGURATION,DC\u003dDUMPSTER,DC\u003dFIRE",
+ new Dictionary
+ {{"domainsid", "S-1-5-21-2697957641-2271029196-387917394"}}, "2F9F3630-F46A-49BF-B186-6629994EBCF9", Label.NTAuthStore);
+
+ var processor = new LDAPPropertyProcessor(new MockLDAPUtils());
+ var props = processor.ParseAllProperties(mock);
+ var keys = props.Keys;
+
+ Assert.Contains("domainsid", keys);
+ Assert.Single(keys);
+ }
+
}
}
\ No newline at end of file
diff --git a/test/unit/LDAPUtilsTest.cs b/test/unit/LDAPUtilsTest.cs
index 7aebdf34..aecedd97 100644
--- a/test/unit/LDAPUtilsTest.cs
+++ b/test/unit/LDAPUtilsTest.cs
@@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.DirectoryServices.ActiveDirectory;
using System.DirectoryServices.Protocols;
-using System.Linq;
using System.Threading;
using CommonLibTest.Facades;
using Moq;
@@ -94,6 +93,29 @@ public void GetWellKnownPrincipal_EnterpriseDomainControllers_ReturnsCorrectedSI
Assert.Equal(Label.Group, typedPrincipal.ObjectType);
}
+ [Fact]
+ public void BuildLdapPath_BadDomain_ReturnsNull()
+ {
+ var mock = new Mock();
+ //var mockDomain = MockableDomain.Construct("TESTLAB.LOCAL");
+ mock.Setup(x => x.GetDomain(It.IsAny()))
+ .Returns((Domain)null);
+ var result = mock.Object.BuildLdapPath("TEST", "ABC");
+ Assert.Null(result);
+ }
+
+ [WindowsOnlyFact]
+ public void BuildLdapPath_HappyPath()
+ {
+ var mock = new Mock();
+ var mockDomain = MockableDomain.Construct("TESTLAB.LOCAL");
+ mock.Setup(x => x.GetDomain(It.IsAny()))
+ .Returns(mockDomain);
+ var result = mock.Object.BuildLdapPath(DirectoryPaths.PKILocation, "ABC");
+ Assert.NotNull(result);
+ Assert.Equal("CN=Public Key Services,CN=Services,CN=Configuration,DC=TESTLAB,DC=LOCAL", result);
+ }
+
[Fact]
public void GetWellKnownPrincipal_NonWellKnown_ReturnsNull()
{
@@ -111,7 +133,7 @@ public void GetWellKnownPrincipal_WithDomain_ConvertsSID()
Assert.Equal(Label.Group, typedPrincipal.ObjectType);
Assert.Equal($"{_testDomainName}-S-1-5-32-544", typedPrincipal.ObjectIdentifier);
}
-
+
[Fact]
public void DistinguishedNameToDomain_RegularObject_CorrectDomain()
{
@@ -127,7 +149,7 @@ public void DistinguishedNameToDomain_RegularObject_CorrectDomain()
public void GetDomainRangeSize_BadDomain_ReturnsDefault()
{
var mock = new Mock