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(); - mock.Setup(x => x.GetDomain(It.IsAny())).Returns((Domain) null); + mock.Setup(x => x.GetDomain(It.IsAny())).Returns((Domain)null); var result = mock.Object.GetDomainRangeSize(); Assert.Equal(750, result); } @@ -136,13 +158,13 @@ public void GetDomainRangeSize_BadDomain_ReturnsDefault() public void GetDomainRangeSize_RespectsDefaultParam() { var mock = new Mock(); - mock.Setup(x => x.GetDomain(It.IsAny())).Returns((Domain) null); + mock.Setup(x => x.GetDomain(It.IsAny())).Returns((Domain)null); var result = mock.Object.GetDomainRangeSize(null, 1000); Assert.Equal(1000, result); } - [Fact] + [WindowsOnlyFact] public void GetDomainRangeSize_NoLdapEntry_ReturnsDefault() { var mock = new Mock(); @@ -156,7 +178,7 @@ public void GetDomainRangeSize_NoLdapEntry_ReturnsDefault() Assert.Equal(750, result); } - [Fact] + [WindowsOnlyFact] public void GetDomainRangeSize_ExpectedResults() { var mock = new Mock(); @@ -169,10 +191,10 @@ public void GetDomainRangeSize_ExpectedResults() "MaxPageSize=1250" }}, }, "abc123", Label.Base); - + mock.Setup(x => x.QueryLDAP(It.IsAny(), It.IsAny(), null, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny())).Returns(new List {searchResult}); + It.IsAny(), It.IsAny())).Returns(new List { searchResult }); var result = mock.Object.GetDomainRangeSize(); Assert.Equal(1250, result); }