diff --git a/src/CommonLib/Enums/LDAPProperties.cs b/src/CommonLib/Enums/LDAPProperties.cs index 8b13359b..5c241b9b 100644 --- a/src/CommonLib/Enums/LDAPProperties.cs +++ b/src/CommonLib/Enums/LDAPProperties.cs @@ -80,5 +80,6 @@ public static class LDAPProperties public const string ServerName = "servername"; public const string OU = "ou"; public const string ProfilePath = "profilepath"; + public const string DSASignature = "dsasignature"; } } diff --git a/src/CommonLib/Processors/LdapPropertyProcessor.cs b/src/CommonLib/Processors/LdapPropertyProcessor.cs index 784b091b..f21f3eea 100644 --- a/src/CommonLib/Processors/LdapPropertyProcessor.cs +++ b/src/CommonLib/Processors/LdapPropertyProcessor.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -11,16 +11,27 @@ using SharpHoundCommonLib.Enums; using SharpHoundCommonLib.LDAPQueries; using SharpHoundCommonLib.OutputTypes; + // ReSharper disable StringLiteralTypo namespace SharpHoundCommonLib.Processors { public class LdapPropertyProcessor { - private static readonly string[] ReservedAttributes = CommonProperties.TypeResolutionProps - .Concat(CommonProperties.BaseQueryProps).Concat(CommonProperties.GroupResolutionProps) - .Concat(CommonProperties.ComputerMethodProps).Concat(CommonProperties.ACLProps) - .Concat(CommonProperties.ObjectPropsProps).Concat(CommonProperties.ContainerProps) - .Concat(CommonProperties.SPNTargetProps).Concat(CommonProperties.DomainTrustProps) - .Concat(CommonProperties.GPOLocalGroupProps).Concat(CommonProperties.CertAbuseProps).ToArray(); + private static readonly HashSet ReservedAttributes = new(); + + static LdapPropertyProcessor() { + ReservedAttributes.UnionWith(CommonProperties.TypeResolutionProps); + ReservedAttributes.UnionWith(CommonProperties.BaseQueryProps); + ReservedAttributes.UnionWith(CommonProperties.GroupResolutionProps); + ReservedAttributes.UnionWith(CommonProperties.ComputerMethodProps); + ReservedAttributes.UnionWith(CommonProperties.ACLProps); + ReservedAttributes.UnionWith(CommonProperties.ObjectPropsProps); + ReservedAttributes.UnionWith(CommonProperties.ContainerProps); + ReservedAttributes.UnionWith(CommonProperties.SPNTargetProps); + ReservedAttributes.UnionWith(CommonProperties.DomainTrustProps); + ReservedAttributes.UnionWith(CommonProperties.GPOLocalGroupProps); + ReservedAttributes.UnionWith(CommonProperties.CertAbuseProps); + ReservedAttributes.Add(LDAPProperties.DSASignature); + } private readonly ILdapUtils _utils; @@ -176,7 +187,7 @@ public async Task ReadUserProperties(IDirectoryObject entry, str } props.Add("lastlogon", Helpers.ConvertFileTimeToUnixEpoch(lastLogon)); - + if (!entry.TryGetProperty(LDAPProperties.LastLogonTimestamp, out var lastLogonTimeStamp)) { lastLogonTimeStamp = null; } @@ -186,6 +197,7 @@ public async Task ReadUserProperties(IDirectoryObject entry, str if (!entry.TryGetProperty(LDAPProperties.PasswordLastSet, out var passwordLastSet)) { passwordLastSet = null; } + props.Add("pwdlastset", Helpers.ConvertFileTimeToUnixEpoch(passwordLastSet)); entry.TryGetArrayProperty(LDAPProperties.ServicePrincipalNames, out var spn); @@ -254,7 +266,7 @@ public async Task ReadComputerProperties(IDirectoryObject en props.Add("unconstraineddelegation", flags.HasFlag(UacFlags.TrustedForDelegation)); props.Add("trustedtoauth", flags.HasFlag(UacFlags.TrustedToAuthForDelegation)); props.Add("isdc", flags.HasFlag(UacFlags.ServerTrustAccount)); - + var comps = new List(); if (flags.HasFlag(UacFlags.TrustedToAuthForDelegation) && entry.TryGetArrayProperty(LDAPProperties.AllowedToDelegateTo, out var delegates)) { @@ -355,7 +367,7 @@ public static Dictionary ReadRootCAProperties(IDirectoryObject e props.Add("hasbasicconstraints", cert.HasBasicConstraints); props.Add("basicconstraintpathlength", cert.BasicConstraintPathLength); } - + return props; } @@ -366,7 +378,7 @@ public static Dictionary ReadRootCAProperties(IDirectoryObject e /// Returns a dictionary with the common properties and the crosscertificatepair property of the AICA public static Dictionary ReadAIACAProperties(IDirectoryObject entry) { var props = GetCommonProps(entry); - entry.TryGetByteArrayProperty(LDAPProperties.CrossCertificatePair, out var crossCertificatePair); + entry.TryGetByteArrayProperty(LDAPProperties.CrossCertificatePair, out var crossCertificatePair); var hasCrossCertificatePair = crossCertificatePair.Length > 0; props.Add("crosscertificatepair", crossCertificatePair); @@ -387,7 +399,8 @@ public static Dictionary ReadAIACAProperties(IDirectoryObject en public static Dictionary ReadEnterpriseCAProperties(IDirectoryObject entry) { var props = GetCommonProps(entry); - if (entry.TryGetIntProperty("flags", out var flags)) props.Add("flags", (PKICertificateAuthorityFlags)flags); + if (entry.TryGetIntProperty("flags", out var flags)) + props.Add("flags", (PKICertificateAuthorityFlags)flags); props.Add("caname", entry.GetProperty(LDAPProperties.Name)); props.Add("dnshostname", entry.GetProperty(LDAPProperties.DNSHostName)); @@ -461,7 +474,8 @@ public static Dictionary ReadCertTemplateProperties(IDirectoryOb entry.TryGetArrayProperty(LDAPProperties.ExtendedKeyUsage, out var ekus); props.Add("ekus", ekus); - entry.TryGetArrayProperty(LDAPProperties.CertificateApplicationPolicy, out var certificateApplicationPolicy); + entry.TryGetArrayProperty(LDAPProperties.CertificateApplicationPolicy, + out var certificateApplicationPolicy); props.Add("certificateapplicationpolicy", certificateApplicationPolicy); entry.TryGetArrayProperty(LDAPProperties.CertificatePolicy, out var certificatePolicy); @@ -477,7 +491,7 @@ public static Dictionary ReadCertTemplateProperties(IDirectoryOb } entry.TryGetArrayProperty(LDAPProperties.ApplicationPolicies, out var appPolicies); - + props.Add("applicationpolicies", ParseCertTemplateApplicationPolicies(appPolicies, schemaVersion, hasUseLegacyProvider)); @@ -520,6 +534,7 @@ public async Task ReadIssuancePolicyProperties(IDirect /// public Dictionary ParseAllProperties(IDirectoryObject entry) { var props = new Dictionary(); + foreach (var property in entry.PropertyNames()) { if (ReservedAttributes.Contains(property, StringComparer.OrdinalIgnoreCase)) continue; @@ -530,13 +545,37 @@ public Dictionary ParseAllProperties(IDirectoryObject entry) { if (collCount == 1) { var testString = entry.GetProperty(property); - - if (!string.IsNullOrEmpty(testString)) + if (!string.IsNullOrEmpty(testString)) { if (property.Equals("badpasswordtime", StringComparison.OrdinalIgnoreCase)) props.Add(property, Helpers.ConvertFileTimeToUnixEpoch(testString)); else props.Add(property, BestGuessConvert(testString)); + } } else { + if (entry.TryGetByteProperty(property, out var testBytes)) { + if (testBytes == null || testBytes.Length == 0) { + continue; + } + + // SIDs + try { + var sid = new SecurityIdentifier(testBytes, 0); + props.Add(property, sid.Value); + continue; + } catch { + /* Ignore */ + } + + // GUIDs + try { + var guid = new Guid(testBytes); + props.Add(property, guid.ToString()); + continue; + } catch { + /* Ignore */ + } + } + if (entry.TryGetArrayProperty(property, out var arr) && arr.Length > 0) { props.Add(property, arr.Select(BestGuessConvert).ToArray()); } @@ -577,23 +616,28 @@ private static string[] ParseCertTemplateApplicationPolicies(string[] applicatio /// /// Does a best guess conversion of the property to a type useable by the UI /// - /// + /// /// - private static object BestGuessConvert(string property) { + private static object BestGuessConvert(string value) { //Parse boolean values - if (bool.TryParse(property, out var boolResult)) return boolResult; + if (bool.TryParse(value, out var boolResult)) return boolResult; //A string ending with 0Z is likely a timestamp - if (property.EndsWith("0Z")) return Helpers.ConvertTimestampToUnixEpoch(property); + if (value.EndsWith("0Z")) return Helpers.ConvertTimestampToUnixEpoch(value); //This string corresponds to the max int, and is usually set in accountexpires - if (property == "9223372036854775807") return -1; + if (value == "9223372036854775807") return -1; //Try parsing as an int - if (int.TryParse(property, out var num)) return num; + if (int.TryParse(value, out var num)) return num; + + // If we have binary unicode, encode it + foreach (char c in value) { + if (char.IsControl(c)) return System.Text.Encoding.UTF8.GetBytes(value); + } //Just return the property as a string - return property; + return value; } /// diff --git a/test/unit/Facades/MockDirectoryObject.cs b/test/unit/Facades/MockDirectoryObject.cs index db8d69fc..dcf3afe8 100644 --- a/test/unit/Facades/MockDirectoryObject.cs +++ b/test/unit/Facades/MockDirectoryObject.cs @@ -75,7 +75,11 @@ public bool TryGetArrayProperty(string propertyName, out string[] value) { var temp = Properties[propertyName]; if (temp.IsArray()) { - value = temp as string[]; + if (temp is string[] s) { + value = s; + return true; + } + value = Array.Empty(); return true; } @@ -153,8 +157,15 @@ public int PropertyCount(string propertyName) { var property = Properties[propertyName]; if (property.IsArray()) { - var cast = property as string[]; - return cast?.Length ?? 0; + if (property is string[] s) { + return s.Length; + } + + if (property is byte[] b) { + return b.Length; + } + + return 0; } return 1; diff --git a/test/unit/LdapPropertyTests.cs b/test/unit/LdapPropertyTests.cs index 30c35cd4..8c40016c 100644 --- a/test/unit/LdapPropertyTests.cs +++ b/test/unit/LdapPropertyTests.cs @@ -1,15 +1,19 @@ -using System; +using System; using System.Collections.Generic; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; +using System.Security.Principal; using System.Threading.Tasks; using CommonLibTest.Facades; using SharpHoundCommonLib; using SharpHoundCommonLib.Enums; using SharpHoundCommonLib.OutputTypes; using SharpHoundCommonLib.Processors; +using SharpHoundRPC; using Xunit; using Xunit.Abstractions; +using static System.Text.Encoding; + // ReSharper disable StringLiteralTypo namespace CommonLibTest @@ -855,7 +859,8 @@ public void LDAPPropertyProcessor_ParseAllProperties() {"name", "NTAUTHCERTIFICATES@DUMPSTER.FIRE"}, {"domainsid", "S-1-5-21-2697957641-2271029196-387917394"}, {"whencreated", 1683986131}, - }, "","2F9F3630-F46A-49BF-B186-6629994EBCF9"); + {LDAPProperties.DSASignature, "jkr"} + }, "", "2F9F3630-F46A-49BF-B186-6629994EBCF9"); var processor = new LdapPropertyProcessor(new MockLdapUtils()); var props = processor.ParseAllProperties(mock); @@ -865,6 +870,7 @@ public void LDAPPropertyProcessor_ParseAllProperties() Assert.DoesNotContain("description", keys); Assert.DoesNotContain("whencreated", keys); Assert.DoesNotContain("name", keys); + Assert.DoesNotContain(LDAPProperties.DSASignature, keys); Assert.Contains("domainsid", keys); Assert.Contains("domain", keys); @@ -929,5 +935,57 @@ public void LDAPPropertyProcessor_ParseAllProperties_CollectionCountOne_NotBadPa Assert.Single(keys); } + [Fact] + public void LDAPPropertyProcessor_ParseAllProperties_CollectionCountOne_ControlCharactersAreEncoded() { + var mock = new MockDirectoryObject("CN\u003dNTAUTHCERTIFICATES,CN\u003dPUBLIC KEY SERVICES,CN\u003dSERVICES,CN\u003dCONFIGURATION,DC\u003dDUMPSTER,DC\u003dFIRE", + new Dictionary + {{"usercertificate", "\u0000"}}, "", "2F9F3630-F46A-49BF-B186-6629994EBCF9"); + + var processor = new LdapPropertyProcessor(new MockLdapUtils()); + var props = processor.ParseAllProperties(mock); + var keys = props.Keys; + + Assert.Contains("usercertificate", keys); + Assert.Single(keys); + var hasCert = props.TryGetValue("usercertificate", out var usercert); + Assert.True(hasCert); + Assert.Equal("\u0000", UTF8.GetString(usercert as byte[])); + } + + [WindowsOnlyFact] + public void LDAPPropertyProcessor_ParseAllProperties_CollectionCountOne_SID() { + var creatorSIDExpected = "S-1-5-21-2697957641-2271029196-387917394"; + var sidBytes = new SecurityIdentifier(creatorSIDExpected).GetBytes(); + var mock = new MockDirectoryObject("CN\u003dNTAUTHCERTIFICATES,CN\u003dPUBLIC KEY SERVICES,CN\u003dSERVICES,CN\u003dCONFIGURATION,DC\u003dDUMPSTER,DC\u003dFIRE", + new Dictionary + {{"ms-ds-creatorsid", sidBytes}}, "", "2F9F3630-F46A-49BF-B186-6629994EBCF9"); + + var processor = new LdapPropertyProcessor(new MockLdapUtils()); + var props = processor.ParseAllProperties(mock); + var keys = props.Keys; + + Assert.Contains("ms-ds-creatorsid", keys); + Assert.Single(keys); + var hasSID = props.TryGetValue("ms-ds-creatorsid", out var creatorSIDActual); + Assert.True(hasSID); + Assert.Equal(creatorSIDExpected, creatorSIDActual.ToString()); + } + + [Fact] + public void LDAPPropertyProcessor_ParseAllProperties_GUID() { + var guidExpected = Guid.NewGuid(); + var mock = new MockDirectoryObject("CN\u003dNTAUTHCERTIFICATES,CN\u003dPUBLIC KEY SERVICES,CN\u003dSERVICES,CN\u003dCONFIGURATION,DC\u003dDUMPSTER,DC\u003dFIRE", + new Dictionary + {{"guid", guidExpected.ToByteArray()}}, "", "2F9F3630-F46A-49BF-B186-6629994EBCF9"); + + var processor = new LdapPropertyProcessor(new MockLdapUtils()); + var props = processor.ParseAllProperties(mock); + var keys = props.Keys; + + Assert.Single(keys); + var hasGuid = props.TryGetValue("guid", out var guidActual); + Assert.True(hasGuid); + Assert.Equal(guidExpected.ToString(), guidActual); + } } }