From 4d27c02079ef17db2806b43eb0e8c7cd2133646e Mon Sep 17 00:00:00 2001 From: Rohan Vazarkar Date: Fri, 22 Mar 2024 10:20:06 -0400 Subject: [PATCH] BED-4255: IssuancePolicy Nodes (#111) * feat: add IssuancePolicy nodes * wip: testable searchresults * chore: revert framework * test: add some tests for GetLabel * feat: add issuancepolicy acl info * chore: update name set for issuancepolicy * feat: add issuancepolicy properties --- src/CommonLib/Enums/DirectoryPaths.cs | 1 + src/CommonLib/Enums/Labels.cs | 3 +- src/CommonLib/Extensions.cs | 17 +++++- src/CommonLib/LDAPProperties.cs | 1 + src/CommonLib/Processors/ACLProcessor.cs | 16 ++++-- .../Processors/LDAPPropertyProcessor.cs | 46 ++++++++++++---- src/CommonLib/SearchResultEntryWrapper.cs | 9 +++- test/unit/Facades/FacadeHelpers.cs | 8 ++- test/unit/Facades/MockableDomain.cs | 2 +- test/unit/Facades/MockableForest.cs | 2 +- .../unit/Facades/MockableSearchResultEntry.cs | 53 +++++++++++++++++++ test/unit/SearchResultEntryTests.cs | 32 +++++++++++ 12 files changed, 171 insertions(+), 19 deletions(-) create mode 100644 test/unit/Facades/MockableSearchResultEntry.cs create mode 100644 test/unit/SearchResultEntryTests.cs diff --git a/src/CommonLib/Enums/DirectoryPaths.cs b/src/CommonLib/Enums/DirectoryPaths.cs index 1b076c5a..744639d9 100644 --- a/src/CommonLib/Enums/DirectoryPaths.cs +++ b/src/CommonLib/Enums/DirectoryPaths.cs @@ -9,5 +9,6 @@ public class DirectoryPaths 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"; + public const string OIDContainerLocation = "CN=OID,CN=Public Key Services,CN=Services,CN=Configuration"; } } \ No newline at end of file diff --git a/src/CommonLib/Enums/Labels.cs b/src/CommonLib/Enums/Labels.cs index 898b316b..b0bacb68 100644 --- a/src/CommonLib/Enums/Labels.cs +++ b/src/CommonLib/Enums/Labels.cs @@ -17,6 +17,7 @@ public enum Label RootCA, AIACA, EnterpriseCA, - NTAuthStore + NTAuthStore, + IssuancePolicy } } diff --git a/src/CommonLib/Extensions.cs b/src/CommonLib/Extensions.cs index cfae61bd..abd80dc3 100644 --- a/src/CommonLib/Extensions.cs +++ b/src/CommonLib/Extensions.cs @@ -375,7 +375,7 @@ public static Label GetLabel(this SearchResultEntry entry) objectType = Label.CertTemplate; else if (objectClasses.Contains(PKIEnrollmentServiceClass, StringComparer.InvariantCultureIgnoreCase)) objectType = Label.EnterpriseCA; - else if (objectClasses.Contains(CertificationAutorityClass, StringComparer.InvariantCultureIgnoreCase)) + else if (objectClasses.Contains(CertificationAuthorityClass, StringComparer.InvariantCultureIgnoreCase)) { if (entry.DistinguishedName.Contains(DirectoryPaths.RootCALocation)) objectType = Label.RootCA; @@ -383,6 +383,18 @@ public static Label GetLabel(this SearchResultEntry entry) objectType = Label.AIACA; else if (entry.DistinguishedName.Contains(DirectoryPaths.NTAuthStoreLocation)) objectType = Label.NTAuthStore; + }else if (objectClasses.Contains(OIDContainerClass, StringComparer.InvariantCultureIgnoreCase)) + { + if (entry.DistinguishedName.StartsWith(DirectoryPaths.OIDContainerLocation, + StringComparison.InvariantCultureIgnoreCase)) + objectType = Label.Container; + else + { + if (entry.GetPropertyAsInt(LDAPProperties.Flags, out var flags) && flags == 2) + { + objectType = Label.IssuancePolicy; + } + } } } @@ -400,7 +412,8 @@ public static Label GetLabel(this SearchResultEntry entry) private const string ConfigurationClass = "configuration"; private const string PKICertificateTemplateClass = "pKICertificateTemplate"; private const string PKIEnrollmentServiceClass = "pKIEnrollmentService"; - private const string CertificationAutorityClass = "certificationAuthority"; + private const string CertificationAuthorityClass = "certificationAuthority"; + private const string OIDContainerClass = "msPKI-Enterprise-Oid"; #endregion } diff --git a/src/CommonLib/LDAPProperties.cs b/src/CommonLib/LDAPProperties.cs index f4dcffd8..5803c2a9 100644 --- a/src/CommonLib/LDAPProperties.cs +++ b/src/CommonLib/LDAPProperties.cs @@ -55,6 +55,7 @@ public static class LDAPProperties public const string PKIOverlappedPeriod = "pkioverlapperiod"; public const string TemplateSchemaVersion = "mspki-template-schema-version"; public const string CertTemplateOID = "mspki-cert-template-oid"; + public const string OIDGroupLink = "msds-oidtogrouplink"; public const string PKIEnrollmentFlag = "mspki-enrollment-flag"; public const string PKINameFlag = "mspki-certificate-name-flag"; public const string ExtendedKeyUsage = "pkiextendedkeyusage"; diff --git a/src/CommonLib/Processors/ACLProcessor.cs b/src/CommonLib/Processors/ACLProcessor.cs index 3220b4e3..1680848e 100644 --- a/src/CommonLib/Processors/ACLProcessor.cs +++ b/src/CommonLib/Processors/ACLProcessor.cs @@ -36,7 +36,8 @@ static ACLProcessor() {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"} + {Label.CertTemplate, "e5209ca2-3bba-11d2-90cc-00c04fd91ab1"}, + {Label.IssuancePolicy, "37cfd85c-6719-4ad8-8f9e-8678ba627563"} }; } @@ -374,7 +375,16 @@ public IEnumerable ProcessACL(byte[] ntSecurityDescriptor, string objectDom if (aceRights.HasFlag(ActiveDirectoryRights.GenericWrite) || aceRights.HasFlag(ActiveDirectoryRights.WriteProperty)) { - if (objectType is Label.User or Label.Group or Label.Computer or Label.GPO or Label.CertTemplate or Label.RootCA or Label.EnterpriseCA or Label.AIACA or Label.NTAuthStore) + if (objectType is Label.User + or Label.Group + or Label.Computer + or Label.GPO + or Label.CertTemplate + or Label.RootCA + or Label.EnterpriseCA + or Label.AIACA + or Label.NTAuthStore + or Label.IssuancePolicy) if (aceType is ACEGuids.AllGuid or "") yield return new ACE { @@ -582,4 +592,4 @@ public IEnumerable ProcessGMSAReaders(byte[] groupMSAMembership, string obj } } } -} +} \ No newline at end of file diff --git a/src/CommonLib/Processors/LDAPPropertyProcessor.cs b/src/CommonLib/Processors/LDAPPropertyProcessor.cs index d0fbe704..5c1a7819 100644 --- a/src/CommonLib/Processors/LDAPPropertyProcessor.cs +++ b/src/CommonLib/Processors/LDAPPropertyProcessor.cs @@ -385,7 +385,7 @@ public static Dictionary ReadRootCAProperties(ISearchResultEntry var rawCertificate = entry.GetByteProperty(LDAPProperties.CACertificate); if (rawCertificate != null) { - ParsedCertificate cert = new ParsedCertificate(rawCertificate); + var cert = new ParsedCertificate(rawCertificate); props.Add("certthumbprint", cert.Thumbprint); props.Add("certname", cert.Name); props.Add("certchain", cert.Chain); @@ -414,7 +414,7 @@ public static Dictionary ReadAIACAProperties(ISearchResultEntry var rawCertificate = entry.GetByteProperty(LDAPProperties.CACertificate); if (rawCertificate != null) { - ParsedCertificate cert = new ParsedCertificate(rawCertificate); + var cert = new ParsedCertificate(rawCertificate); props.Add("certthumbprint", cert.Thumbprint); props.Add("certname", cert.Name); props.Add("certchain", cert.Chain); @@ -436,7 +436,7 @@ public static Dictionary ReadEnterpriseCAProperties(ISearchResul var rawCertificate = entry.GetByteProperty(LDAPProperties.CACertificate); if (rawCertificate != null) { - ParsedCertificate cert = new ParsedCertificate(rawCertificate); + var cert = new ParsedCertificate(rawCertificate); props.Add("certthumbprint", cert.Thumbprint); props.Add("certname", cert.Name); props.Add("certchain", cert.Chain); @@ -506,15 +506,15 @@ public static Dictionary ReadCertTemplateProperties(ISearchResul nameFlags.HasFlag(PKICertificateNameFlag.SUBJECT_REQUIRE_EMAIL)); } - string[] ekus = entry.GetArrayProperty(LDAPProperties.ExtendedKeyUsage); + var ekus = entry.GetArrayProperty(LDAPProperties.ExtendedKeyUsage); props.Add("ekus", ekus); - string[] certificateapplicationpolicy = entry.GetArrayProperty(LDAPProperties.CertificateApplicationPolicy); + var certificateapplicationpolicy = entry.GetArrayProperty(LDAPProperties.CertificateApplicationPolicy); props.Add("certificateapplicationpolicy", certificateapplicationpolicy); if (entry.GetIntProperty(LDAPProperties.NumSignaturesRequired, out var authorizedSignatures)) props.Add("authorizedsignatures", authorizedSignatures); - bool hasUseLegacyProvider = false; + var hasUseLegacyProvider = false; if (entry.GetIntProperty(LDAPProperties.PKIPrivateKeyFlag, out var privateKeyFlagsRaw)) { var privateKeyFlags = (PKIPrivateKeyFlag)privateKeyFlagsRaw; @@ -525,16 +525,38 @@ public static Dictionary ReadCertTemplateProperties(ISearchResul props.Add("issuancepolicies", entry.GetArrayProperty(LDAPProperties.IssuancePolicies)); // Construct effectiveekus - string[] effectiveekus = schemaVersion == 1 & ekus.Length > 0 ? ekus : certificateapplicationpolicy; + var effectiveekus = schemaVersion == 1 & ekus.Length > 0 ? ekus : certificateapplicationpolicy; props.Add("effectiveekus", effectiveekus); // Construct authenticationenabled - bool authenticationenabled = effectiveekus.Intersect(Helpers.AuthenticationOIDs).Any() | effectiveekus.Length == 0; + var authenticationenabled = effectiveekus.Intersect(Helpers.AuthenticationOIDs).Any() | effectiveekus.Length == 0; props.Add("authenticationenabled", authenticationenabled); return props; } + public IssuancePolicyProperties ReadIssuancePolicyProperties(ISearchResultEntry entry) + { + var ret = new IssuancePolicyProperties(); + var props = GetCommonProps(entry); + props.Add("displayname", entry.GetProperty(LDAPProperties.DisplayName)); + props.Add("oid", entry.GetProperty(LDAPProperties.CertTemplateOID)); + + var link = entry.GetProperty(LDAPProperties.OIDGroupLink); + if (!string.IsNullOrEmpty(link)) + { + var linkedGroup = _utils.ResolveDistinguishedName(link); + if (linkedGroup != null) + { + props.Add("oidgrouplink", linkedGroup.ObjectIdentifier); + ret.GroupLink = linkedGroup; + } + } + + ret.Props = props; + return ret; + } + /// /// Attempts to parse all LDAP attributes outside of the ones already collected and converts them to a human readable /// format using a best guess @@ -602,7 +624,7 @@ private static string[] ParseCertTemplateApplicationPolicies(string[] applicatio // Format: "Name`Type`Value`Name`Type`Value`..." // (https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-crtd/c55ec697-be3f-4117-8316-8895e4399237) // Return the Value of Name = "msPKI-RA-Application-Policies" entries - string[] entries = applicationPolicies[0].Split('`'); + var entries = applicationPolicies[0].Split('`'); return Enumerable.Range(0, entries.Length / 3) .Select(i => entries.Skip(i * 3).Take(3).ToArray()) .Where(parts => parts.Length == 3 && parts[0].Equals(LDAPProperties.ApplicationPolicies, StringComparison.OrdinalIgnoreCase)) @@ -782,4 +804,10 @@ public class ComputerProperties public TypedPrincipal[] SidHistory { get; set; } = Array.Empty(); public TypedPrincipal[] DumpSMSAPassword { get; set; } = Array.Empty(); } + + public class IssuancePolicyProperties + { + public Dictionary Props { get; set; } = new(); + public TypedPrincipal GroupLink { get; set; } = new TypedPrincipal(); + } } diff --git a/src/CommonLib/SearchResultEntryWrapper.cs b/src/CommonLib/SearchResultEntryWrapper.cs index 2e191c02..ef681ba8 100644 --- a/src/CommonLib/SearchResultEntryWrapper.cs +++ b/src/CommonLib/SearchResultEntryWrapper.cs @@ -146,6 +146,7 @@ public ResolvedSearchResult ResolveBloodHoundInfo() res.DisplayName = $"{samAccountName}@{itemDomain}"; break; case Label.Computer: + { var shortName = samAccountName?.TrimEnd('$'); var dns = GetProperty(LDAPProperties.DNSHostName); var cn = GetProperty(LDAPProperties.CanonicalName); @@ -160,9 +161,15 @@ public ResolvedSearchResult ResolveBloodHoundInfo() res.DisplayName = $"{cn}.{itemDomain}"; break; + } case Label.GPO: - res.DisplayName = $"{GetProperty(LDAPProperties.DisplayName)}@{itemDomain}"; + case Label.IssuancePolicy: + { + var cn = GetProperty(LDAPProperties.CanonicalName); + var displayName = GetProperty(LDAPProperties.DisplayName); + res.DisplayName = string.IsNullOrEmpty(displayName) ? $"{cn}@{itemDomain}" : $"{GetProperty(LDAPProperties.DisplayName)}@{itemDomain}"; break; + } case Label.Domain: res.DisplayName = itemDomain; break; diff --git a/test/unit/Facades/FacadeHelpers.cs b/test/unit/Facades/FacadeHelpers.cs index a43f54d4..2b097e8b 100644 --- a/test/unit/Facades/FacadeHelpers.cs +++ b/test/unit/Facades/FacadeHelpers.cs @@ -13,10 +13,16 @@ internal static T GetUninitializedObject() return (T) FormatterServices.GetUninitializedObject(typeof(T)); } - internal static void SetProperty(T1 obj, string propertyName, T2 propertyValue) + internal static void SetField(T1 obj, string propertyName, T2 propertyValue) { var set = typeof(T1).GetField(propertyName, nonPublicInstance); if (set != null) set.SetValue(obj, propertyValue); } + + internal static void SetProperty(T1 obj, string propertyName, T2 propertyValue) + { + var set = typeof(T1).GetProperty(propertyName, nonPublicInstance); + if (set != null) set.SetValue(obj, propertyValue); + } } } \ No newline at end of file diff --git a/test/unit/Facades/MockableDomain.cs b/test/unit/Facades/MockableDomain.cs index 7c0a0480..059918d1 100644 --- a/test/unit/Facades/MockableDomain.cs +++ b/test/unit/Facades/MockableDomain.cs @@ -7,7 +7,7 @@ public class MockableDomain public static Domain Construct(string domainName) { var domain = FacadeHelpers.GetUninitializedObject(); - FacadeHelpers.SetProperty(domain, "partitionName", domainName); + FacadeHelpers.SetField(domain, "partitionName", domainName); return domain; } diff --git a/test/unit/Facades/MockableForest.cs b/test/unit/Facades/MockableForest.cs index 3bc1b5ec..9893e633 100644 --- a/test/unit/Facades/MockableForest.cs +++ b/test/unit/Facades/MockableForest.cs @@ -7,7 +7,7 @@ public class MockableForest public static Forest Construct(string forestDnsName) { var forest = FacadeHelpers.GetUninitializedObject(); - FacadeHelpers.SetProperty(forest, "_forestDnsName", forestDnsName); + FacadeHelpers.SetField(forest, "_forestDnsName", forestDnsName); return forest; } diff --git a/test/unit/Facades/MockableSearchResultEntry.cs b/test/unit/Facades/MockableSearchResultEntry.cs new file mode 100644 index 00000000..c45669c0 --- /dev/null +++ b/test/unit/Facades/MockableSearchResultEntry.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.DirectoryServices.Protocols; +using SharpHoundCommonLib; +using BindingFlags = System.Reflection.BindingFlags; + +namespace CommonLibTest.Facades +{ + public class MockableSearchResultEntry + { + public static SearchResultEntry Construct(Dictionary values, string distinguishedName) + { + var attributes = CreateAttributes(values); + + return CreateSearchResultEntry(attributes, distinguishedName); + } + + + private static SearchResultAttributeCollection CreateAttributes(Dictionary values) + { + var coll = + (SearchResultAttributeCollection)typeof(SearchResultAttributeCollection) + .GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, Type.EmptyTypes, null) + .Invoke(null); + + var dict = (IDictionary) typeof(SearchResultAttributeCollection).GetProperty("Dictionary", + BindingFlags.NonPublic | BindingFlags.Instance).GetValue(coll); + + foreach (var v in values) + { + dict.Add(v.Key, new DirectoryAttribute(v.Key, v.Value)); + } + return coll; + } + + private static SearchResultEntry CreateSearchResultEntry(SearchResultAttributeCollection attributes, + string distinguishedName) + { + var types = new[] + { + typeof(string), + typeof(SearchResultAttributeCollection), + }; + + var sre = (SearchResultEntry)typeof(SearchResultEntry) + .GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, types, null) + .Invoke(new object[]{ distinguishedName, attributes}); + + return sre; + } + } +} \ No newline at end of file diff --git a/test/unit/SearchResultEntryTests.cs b/test/unit/SearchResultEntryTests.cs new file mode 100644 index 00000000..8eb59319 --- /dev/null +++ b/test/unit/SearchResultEntryTests.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Security.Principal; +using CommonLibTest.Facades; +using SharpHoundCommonLib; +using SharpHoundCommonLib.Enums; +using Xunit; + +namespace CommonLibTest +{ + public class SearchResultEntryTests + { + [WindowsOnlyFact] + public void Test_GetLabelIssuanceOIDObjects() + { + var sid = new SecurityIdentifier("S-1-5-21-3130019616-2776909439-2417379446-500"); + var bsid = new byte[sid.BinaryLength]; + sid.GetBinaryForm(bsid, 0); + var attribs = new Dictionary + { + { "objectsid", bsid}, + { "objectclass", "msPKI-Enterprise-Oid" }, + { "flags", "2" } + }; + + var sre = MockableSearchResultEntry.Construct(attribs, "CN=Test,CN=OID,CN=Public Key Services,CN=Services,CN=Configuration"); + Assert.Equal(Label.IssuancePolicy, sre.GetLabel()); + + sre = MockableSearchResultEntry.Construct(attribs, "CN=OID,CN=Public Key Services,CN=Services,CN=Configuration"); + Assert.Equal(Label.Container, sre.GetLabel()); + } + } +} \ No newline at end of file