Skip to content

Commit

Permalink
BED-4255: IssuancePolicy Nodes (#111)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
rvazarkar authored Mar 22, 2024
1 parent 733372c commit 4d27c02
Show file tree
Hide file tree
Showing 12 changed files with 171 additions and 19 deletions.
1 change: 1 addition & 0 deletions src/CommonLib/Enums/DirectoryPaths.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
}
3 changes: 2 additions & 1 deletion src/CommonLib/Enums/Labels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public enum Label
RootCA,
AIACA,
EnterpriseCA,
NTAuthStore
NTAuthStore,
IssuancePolicy
}
}
17 changes: 15 additions & 2 deletions src/CommonLib/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -375,14 +375,26 @@ 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;
else if (entry.DistinguishedName.Contains(DirectoryPaths.AIACALocation))
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;
}
}
}
}

Expand All @@ -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
}
Expand Down
1 change: 1 addition & 0 deletions src/CommonLib/LDAPProperties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
16 changes: 13 additions & 3 deletions src/CommonLib/Processors/ACLProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
};
}

Expand Down Expand Up @@ -374,7 +375,16 @@ public IEnumerable<ACE> 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
{
Expand Down Expand Up @@ -582,4 +592,4 @@ public IEnumerable<ACE> ProcessGMSAReaders(byte[] groupMSAMembership, string obj
}
}
}
}
}
46 changes: 37 additions & 9 deletions src/CommonLib/Processors/LDAPPropertyProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ public static Dictionary<string, object> 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);
Expand Down Expand Up @@ -414,7 +414,7 @@ public static Dictionary<string, object> 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);
Expand All @@ -436,7 +436,7 @@ public static Dictionary<string, object> 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);
Expand Down Expand Up @@ -506,15 +506,15 @@ public static Dictionary<string, object> 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;
Expand All @@ -525,16 +525,38 @@ public static Dictionary<string, object> 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;
}

/// <summary>
/// Attempts to parse all LDAP attributes outside of the ones already collected and converts them to a human readable
/// format using a best guess
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -782,4 +804,10 @@ public class ComputerProperties
public TypedPrincipal[] SidHistory { get; set; } = Array.Empty<TypedPrincipal>();
public TypedPrincipal[] DumpSMSAPassword { get; set; } = Array.Empty<TypedPrincipal>();
}

public class IssuancePolicyProperties
{
public Dictionary<string, object> Props { get; set; } = new();
public TypedPrincipal GroupLink { get; set; } = new TypedPrincipal();
}
}
9 changes: 8 additions & 1 deletion src/CommonLib/SearchResultEntryWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
Expand Down
8 changes: 7 additions & 1 deletion test/unit/Facades/FacadeHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,16 @@ internal static T GetUninitializedObject<T>()
return (T) FormatterServices.GetUninitializedObject(typeof(T));
}

internal static void SetProperty<T1, T2>(T1 obj, string propertyName, T2 propertyValue)
internal static void SetField<T1, T2>(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, T2>(T1 obj, string propertyName, T2 propertyValue)
{
var set = typeof(T1).GetProperty(propertyName, nonPublicInstance);
if (set != null) set.SetValue(obj, propertyValue);
}
}
}
2 changes: 1 addition & 1 deletion test/unit/Facades/MockableDomain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public class MockableDomain
public static Domain Construct(string domainName)
{
var domain = FacadeHelpers.GetUninitializedObject<Domain>();
FacadeHelpers.SetProperty(domain, "partitionName", domainName);
FacadeHelpers.SetField(domain, "partitionName", domainName);

return domain;
}
Expand Down
2 changes: 1 addition & 1 deletion test/unit/Facades/MockableForest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public class MockableForest
public static Forest Construct(string forestDnsName)
{
var forest = FacadeHelpers.GetUninitializedObject<Forest>();
FacadeHelpers.SetProperty(forest, "_forestDnsName", forestDnsName);
FacadeHelpers.SetField(forest, "_forestDnsName", forestDnsName);

return forest;
}
Expand Down
53 changes: 53 additions & 0 deletions test/unit/Facades/MockableSearchResultEntry.cs
Original file line number Diff line number Diff line change
@@ -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<string, object> values, string distinguishedName)
{
var attributes = CreateAttributes(values);

return CreateSearchResultEntry(attributes, distinguishedName);
}


private static SearchResultAttributeCollection CreateAttributes(Dictionary<string, object> 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;
}
}
}
32 changes: 32 additions & 0 deletions test/unit/SearchResultEntryTests.cs
Original file line number Diff line number Diff line change
@@ -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<string, object>
{
{ "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());
}
}
}

0 comments on commit 4d27c02

Please sign in to comment.