From ccf8a60a95a70b7fa448c6e8f2c6a8fe8cd48387 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20B=C3=BClow=20Knudsen?= <12843299+JonasBK@users.noreply.github.com> Date: Thu, 15 Aug 2024 17:50:23 +0200 Subject: [PATCH] Additional AD properties (#150) * feat: additional ldap properties --- .../Enums/KerberosEncryptionTypes.cs | 11 ++ src/CommonLib/Enums/LDAPProperties.cs | 12 ++ src/CommonLib/Enums/TrustAttributes.cs | 8 +- src/CommonLib/ILdapUtils.cs | 3 +- src/CommonLib/LdapQueries/CommonPaths.cs | 3 + src/CommonLib/LdapQueries/CommonProperties.cs | 7 +- src/CommonLib/LdapUtils.cs | 20 +++ src/CommonLib/OutputTypes/DomainTrust.cs | 2 + .../Processors/DomainTrustProcessor.cs | 11 +- .../Processors/LdapPropertyProcessor.cs | 120 +++++++++++++++++- test/unit/DomainTrustProcessorTest.cs | 2 +- test/unit/Facades/MockLdapUtils.cs | 5 + test/unit/LdapPropertyTests.cs | 10 +- 13 files changed, 200 insertions(+), 14 deletions(-) create mode 100644 src/CommonLib/Enums/KerberosEncryptionTypes.cs diff --git a/src/CommonLib/Enums/KerberosEncryptionTypes.cs b/src/CommonLib/Enums/KerberosEncryptionTypes.cs new file mode 100644 index 00000000..25d29465 --- /dev/null +++ b/src/CommonLib/Enums/KerberosEncryptionTypes.cs @@ -0,0 +1,11 @@ +namespace SharpHoundCommonLib.Enums +{ + public class KerberosEncryptionTypes + { + public const int DES_CBC_CRC = 1; + public const int DES_CBC_MD5 = 2; + public const int RC4_HMAC_MD5 = 4; + public const int AES128_CTS_HMAC_SHA1_96 = 8; + public const int AES256_CTS_HMAC_SHA1_96 = 16; + } +} \ No newline at end of file diff --git a/src/CommonLib/Enums/LDAPProperties.cs b/src/CommonLib/Enums/LDAPProperties.cs index 5c241b9b..0b202e46 100644 --- a/src/CommonLib/Enums/LDAPProperties.cs +++ b/src/CommonLib/Enums/LDAPProperties.cs @@ -71,6 +71,10 @@ public static class LDAPProperties public const string CertificateTemplates = "certificatetemplates"; public const string CrossCertificatePair = "crosscertificatepair"; public const string Flags = "flags"; + public const string ExpirePasswordsOnSmartCardOnlyAccounts = "msds-expirepasswordsonsmartcardonlyaccounts"; + public const string MachineAccountQuota = "ms-ds-machineaccountquota"; + public const string SupportedEncryptionTypes = "msds-supportedencryptiontypes"; + public const string DSHeuristics = "dsheuristics"; public const string DefaultNamingContext = "defaultnamingcontext"; public const string RootDomainNamingContext = "rootdomainnamingcontext"; public const string ConfigurationNamingContext = "configurationnamingcontext"; @@ -81,5 +85,13 @@ public static class LDAPProperties public const string OU = "ou"; public const string ProfilePath = "profilepath"; public const string DSASignature = "dsasignature"; + public const string MinPwdLength = "minpwdlength"; + public const string PwdProperties = "pwdproperties"; + public const string MinPwdAge = "minpwdage"; + public const string MaxPwdAge = "maxpwdage"; + public const string PwdHistoryLength = "pwdhistorylength"; + public const string LockoutDuration = "lockoutduration"; + public const string LockoutThreshold = "lockoutthreshold"; + public const string LockOutObservationWindow = "lockoutobservationwindow"; } } diff --git a/src/CommonLib/Enums/TrustAttributes.cs b/src/CommonLib/Enums/TrustAttributes.cs index 584140b2..352e2c43 100644 --- a/src/CommonLib/Enums/TrustAttributes.cs +++ b/src/CommonLib/Enums/TrustAttributes.cs @@ -7,15 +7,17 @@ public enum TrustAttributes { NonTransitive = 0x1, UplevelOnly = 0x2, - FilterSids = 0x4, + QuarantinedDomain = 0x4, ForestTransitive = 0x8, CrossOrganization = 0x10, WithinForest = 0x20, TreatAsExternal = 0x40, - TrustUsesRc4 = 0x80, + UsesRc4Encryption = 0x80, TrustUsesAes = 0x100, CrossOrganizationNoTGTDelegation = 0x200, PIMTrust = 0x400, + CrossOrganizationEnableTGTDelegation = 0x800, + DisableAuthTargetValidation = 0x1000, Unknown = 0x400000, } -} \ No newline at end of file +} diff --git a/src/CommonLib/ILdapUtils.cs b/src/CommonLib/ILdapUtils.cs index 9f0bbc42..56ebd782 100644 --- a/src/CommonLib/ILdapUtils.cs +++ b/src/CommonLib/ILdapUtils.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Security.Principal; using System.Threading; @@ -150,6 +150,7 @@ IAsyncEnumerable> RangedRetrieval(string distinguishedName, Task<(bool Success, TypedPrincipal Principal)> ResolveDistinguishedName(string distinguishedName); void AddDomainController(string domainControllerSID); IAsyncEnumerable GetWellKnownPrincipalOutput(); + Task<(bool Success, string DSHeuristics)> GetDSHueristics(string domain, string dn); /// /// Sets the ldap config for this utils instance. Will dispose if any existing ldap connections when set /// diff --git a/src/CommonLib/LdapQueries/CommonPaths.cs b/src/CommonLib/LdapQueries/CommonPaths.cs index cfb36c4e..6e852510 100644 --- a/src/CommonLib/LdapQueries/CommonPaths.cs +++ b/src/CommonLib/LdapQueries/CommonPaths.cs @@ -5,6 +5,9 @@ public static class CommonPaths public const string QueryPolicyPath = "CN=Default Query Policy,CN=Query-Policies,CN=Directory Service,CN=Windows NT,CN=Services,CN=Configuration"; + public const string DirectoryServicePath = + "CN=Directory Service,CN=Windows NT,CN=Services,CN=Configuration"; + public const string ConfigurationPath = "CN=Configuration"; public static string CreateDNPath(string prePath, string baseDomainDN) diff --git a/src/CommonLib/LdapQueries/CommonProperties.cs b/src/CommonLib/LdapQueries/CommonProperties.cs index 7362f1e5..9aa84b3d 100644 --- a/src/CommonLib/LdapQueries/CommonProperties.cs +++ b/src/CommonLib/LdapQueries/CommonProperties.cs @@ -55,7 +55,12 @@ public static class CommonProperties LDAPProperties.GroupPolicyOptions, LDAPProperties.AllowedToDelegateTo, LDAPProperties.AllowedToActOnBehalfOfOtherIdentity, LDAPProperties.WhenCreated, LDAPProperties.HostServiceAccount, LDAPProperties.UnixUserPassword, LDAPProperties.MsSFU30Password, - LDAPProperties.UnicodePassword, LDAPProperties.ProfilePath, LDAPProperties.ScriptPath + LDAPProperties.UnicodePassword, LDAPProperties.ProfilePath, LDAPProperties.ScriptPath, + LDAPProperties.ExpirePasswordsOnSmartCardOnlyAccounts, LDAPProperties.MachineAccountQuota, + LDAPProperties.SupportedEncryptionTypes, LDAPProperties.DSHeuristics, + LDAPProperties.MinPwdLength, LDAPProperties.PwdProperties, LDAPProperties.MinPwdAge, + LDAPProperties.MaxPwdAge, LDAPProperties.PwdHistoryLength, LDAPProperties.LockoutDuration, + LDAPProperties.LockoutThreshold, LDAPProperties.LockOutObservationWindow }; public static readonly string[] ContainerProps = diff --git a/src/CommonLib/LdapUtils.cs b/src/CommonLib/LdapUtils.cs index 6f4025d1..2eb3f7da 100644 --- a/src/CommonLib/LdapUtils.cs +++ b/src/CommonLib/LdapUtils.cs @@ -895,6 +895,26 @@ public async Task IsDomainController(string computerObjectId, string domai } } + public async Task<(bool Success, string DSHeuristics)> GetDSHueristics(string domain, string dn) + { + var configPath = CommonPaths.CreateDNPath(CommonPaths.DirectoryServicePath, dn); + var queryParameters = new LdapQueryParameters { + Attributes = new[] { LDAPProperties.DSHeuristics }, + SearchScope = SearchScope.Base, + DomainName = domain, + LDAPFilter = new LdapFilter().AddAllObjects().GetFilter(), + NamingContext = NamingContext.Configuration, + SearchBase = configPath + }; + + var result = await Query(queryParameters).DefaultIfEmpty(LdapResult.Fail()).FirstOrDefaultAsync(); + if (result.IsSuccess && + result.Value.TryGetProperty(LDAPProperties.DSHeuristics, out var dsh)) { + return (true, dsh); + } + return (false, null); + } + public void AddDomainController(string domainControllerSID) { DomainControllers.TryAdd(domainControllerSID, new byte()); } diff --git a/src/CommonLib/OutputTypes/DomainTrust.cs b/src/CommonLib/OutputTypes/DomainTrust.cs index 09f37f77..9c62a82c 100644 --- a/src/CommonLib/OutputTypes/DomainTrust.cs +++ b/src/CommonLib/OutputTypes/DomainTrust.cs @@ -8,6 +8,8 @@ public class DomainTrust public string TargetDomainName { get; set; } public bool IsTransitive { get; set; } public bool SidFilteringEnabled { get; set; } + public bool TGTDelegationEnabled { get; set; } + public string TrustAttributes { get; set; } public TrustDirection TrustDirection { get; set; } public TrustType TrustType { get; set; } } diff --git a/src/CommonLib/Processors/DomainTrustProcessor.cs b/src/CommonLib/Processors/DomainTrustProcessor.cs index 2103c636..1df373ec 100644 --- a/src/CommonLib/Processors/DomainTrustProcessor.cs +++ b/src/CommonLib/Processors/DomainTrustProcessor.cs @@ -71,6 +71,7 @@ public async IAsyncEnumerable EnumerateDomainTrusts(string domain) continue; } + trust.TrustAttributes = ta.ToString(); attributes = (TrustAttributes) ta; trust.IsTransitive = !attributes.HasFlag(TrustAttributes.NonTransitive); @@ -78,7 +79,15 @@ public async IAsyncEnumerable EnumerateDomainTrusts(string domain) trust.TargetDomainName = cn.ToUpper(); } - trust.SidFilteringEnabled = attributes.HasFlag(TrustAttributes.FilterSids); + trust.SidFilteringEnabled = + attributes.HasFlag(TrustAttributes.QuarantinedDomain) || + (attributes.HasFlag(TrustAttributes.ForestTransitive) && + !attributes.HasFlag(TrustAttributes.TreatAsExternal)); + + trust.TGTDelegationEnabled = + !attributes.HasFlag(TrustAttributes.QuarantinedDomain) && + (attributes.HasFlag(TrustAttributes.CrossOrganizationEnableTGTDelegation) + || !attributes.HasFlag(TrustAttributes.CrossOrganizationNoTGTDelegation)); trust.TrustType = TrustAttributesToType(attributes); yield return trust; diff --git a/src/CommonLib/Processors/LdapPropertyProcessor.cs b/src/CommonLib/Processors/LdapPropertyProcessor.cs index 0075bf14..b2abc562 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; @@ -57,15 +57,39 @@ private static Dictionary GetCommonProps(IDirectoryObject entry) /// /// /// - public static Dictionary ReadDomainProperties(IDirectoryObject entry) { + public async Task> ReadDomainProperties(IDirectoryObject entry, string domain) + { var props = GetCommonProps(entry); + + props.Add("expirepasswordsonsmartcardonlyaccounts", entry.GetProperty(LDAPProperties.ExpirePasswordsOnSmartCardOnlyAccounts)); + props.Add("machineaccountquota", entry.GetProperty(LDAPProperties.MachineAccountQuota)); + props.Add("minpwdlength", entry.GetProperty(LDAPProperties.MinPwdLength)); + props.Add("pwdproperties", entry.GetProperty(LDAPProperties.PwdProperties)); + props.Add("pwdhistorylength", entry.GetProperty(LDAPProperties.PwdHistoryLength)); + props.Add("lockoutthreshold", entry.GetProperty(LDAPProperties.LockoutThreshold)); + + if (entry.TryGetLongProperty(LDAPProperties.MinPwdAge, out var minpwdage)) { + props.Add("minpwdage", ConvertNanoDuration(minpwdage)); + } + if (entry.TryGetLongProperty(LDAPProperties.MaxPwdAge, out var maxpwdage)) { + props.Add("maxpwdage", ConvertNanoDuration(maxpwdage)); + } + if (entry.TryGetLongProperty(LDAPProperties.LockoutDuration, out var lockoutduration)) { + props.Add("lockoutduration", ConvertNanoDuration(lockoutduration)); + } + if (entry.TryGetLongProperty(LDAPProperties.LockOutObservationWindow, out var lockoutobservationwindow)) { + props.Add("lockoutobservationwindow", ConvertNanoDuration(lockoutobservationwindow)); + } if (!entry.TryGetLongProperty(LDAPProperties.DomainFunctionalLevel, out var functionalLevel)) { functionalLevel = -1; } - props.Add("functionallevel", FunctionalLevelToString((int)functionalLevel)); + var dn = entry.GetProperty(LDAPProperties.DistinguishedName); + var dsh = await _utils.GetDSHueristics(domain, dn); + props.Add("dsheuristics", dsh.DSHeuristics); + return props; } @@ -84,6 +108,7 @@ public static string FunctionalLevelToString(int level) { 5 => "2012", 6 => "2012 R2", 7 => "2016", + 8 => "2025", _ => "Unknown" }; @@ -161,6 +186,13 @@ public async Task ReadUserProperties(IDirectoryObject entry, str props.Add("pwdneverexpires", uacFlags.HasFlag(UacFlags.DontExpirePassword)); props.Add("enabled", !uacFlags.HasFlag(UacFlags.AccountDisable)); props.Add("trustedtoauth", uacFlags.HasFlag(UacFlags.TrustedToAuthForDelegation)); + props.Add("smartcardrequired", uacFlags.HasFlag(UacFlags.SmartcardRequired)); + props.Add("encryptedtextpwdallowed", uacFlags.HasFlag(UacFlags.EncryptedTextPwdAllowed)); + props.Add("usedeskeyonly", uacFlags.HasFlag(UacFlags.UseDesKeyOnly)); + props.Add("logonscriptenabled", uacFlags.HasFlag(UacFlags.Script)); + props.Add("lockedout", uacFlags.HasFlag(UacFlags.Lockout)); + props.Add("passwordcantchange", uacFlags.HasFlag(UacFlags.PasswordCantChange)); + props.Add("passwordexpired", uacFlags.HasFlag(UacFlags.PasswordExpired)); var comps = new List(); if (uacFlags.HasFlag(UacFlags.TrustedToAuthForDelegation) && @@ -212,11 +244,15 @@ public async Task ReadUserProperties(IDirectoryObject entry, str props.Add("unicodepassword", entry.GetProperty(LDAPProperties.UnicodePassword)); props.Add("sfupassword", entry.GetProperty(LDAPProperties.MsSFU30Password)); props.Add("logonscript", entry.GetProperty(LDAPProperties.ScriptPath)); + props.Add("useraccountcontrol", uac); props.Add("profilepath", entry.GetProperty(LDAPProperties.ProfilePath)); entry.TryGetLongProperty(LDAPProperties.AdminCount, out var ac); props.Add("admincount", ac != 0); + var encryptionTypes = ConvertEncryptionTypes(entry.GetProperty(LDAPProperties.SupportedEncryptionTypes)); + props.Add("supportedencryptiontypes", encryptionTypes); + entry.TryGetByteArrayProperty(LDAPProperties.SIDHistory, out var sh); var sidHistoryList = new List(); var sidHistoryPrincipals = new List(); @@ -267,6 +303,14 @@ 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)); + props.Add("encryptedtextpwdallowed", flags.HasFlag(UacFlags.EncryptedTextPwdAllowed)); + props.Add("usedeskeyonly", flags.HasFlag(UacFlags.UseDesKeyOnly)); + props.Add("logonscriptenabled", flags.HasFlag(UacFlags.Script)); + props.Add("lockedout", flags.HasFlag(UacFlags.Lockout)); + props.Add("passwordexpired", flags.HasFlag(UacFlags.PasswordExpired)); + + var encryptionTypes = ConvertEncryptionTypes(entry.GetProperty(LDAPProperties.SupportedEncryptionTypes)); + props.Add("supportedencryptiontypes", encryptionTypes); var comps = new List(); if (flags.HasFlag(UacFlags.TrustedToAuthForDelegation) && @@ -308,6 +352,7 @@ public async Task ReadComputerProperties(IDirectoryObject en entry.TryGetArrayProperty(LDAPProperties.ServicePrincipalNames, out var spn); props.Add("serviceprincipalnames", spn); props.Add("email", entry.GetProperty(LDAPProperties.Email)); + props.Add("useraccountcontrol", uac); var os = entry.GetProperty(LDAPProperties.OperatingSystem); var sp = entry.GetProperty(LDAPProperties.ServicePack); @@ -641,6 +686,75 @@ private static object BestGuessConvert(string value) { return value; } + private static List ConvertEncryptionTypes(string encryptionTypes) + { + if (encryptionTypes == null) { + return null; + } + + int encryptionTypesInt = Int32.Parse(encryptionTypes); + List supportedEncryptionTypes = new List(); + if (encryptionTypesInt == 0) { + supportedEncryptionTypes.Add("Not defined"); + } + + if ((encryptionTypesInt & KerberosEncryptionTypes.DES_CBC_CRC) == KerberosEncryptionTypes.DES_CBC_CRC) + { + supportedEncryptionTypes.Add("DES-CBC-CRC"); + } + if ((encryptionTypesInt & KerberosEncryptionTypes.DES_CBC_MD5) == KerberosEncryptionTypes.DES_CBC_MD5) + { + supportedEncryptionTypes.Add("DES-CBC-MD5"); + } + if ((encryptionTypesInt & KerberosEncryptionTypes.RC4_HMAC_MD5) == KerberosEncryptionTypes.RC4_HMAC_MD5) + { + supportedEncryptionTypes.Add("RC4-HMAC-MD5"); + } + if ((encryptionTypesInt & KerberosEncryptionTypes.AES128_CTS_HMAC_SHA1_96) == KerberosEncryptionTypes.AES128_CTS_HMAC_SHA1_96) + { + supportedEncryptionTypes.Add("AES128-CTS-HMAC-SHA1-96"); + } + if ((encryptionTypesInt & KerberosEncryptionTypes.AES256_CTS_HMAC_SHA1_96) == KerberosEncryptionTypes.AES256_CTS_HMAC_SHA1_96) + { + supportedEncryptionTypes.Add("AES256-CTS-HMAC-SHA1-96"); + } + + return supportedEncryptionTypes; + } + + private static string ConvertNanoDuration(long duration) + { + // duration is in 100-nanosecond intervals + // Convert it to TimeSpan (which uses 1 tick = 100 nanoseconds) + TimeSpan durationSpan = TimeSpan.FromTicks(Math.Abs(duration)); + + // Create a list to hold non-zero time components + List timeComponents = new List(); + + // Add each time component if it's greater than zero + if (durationSpan.Days > 0) + { + timeComponents.Add($"{durationSpan.Days} {(durationSpan.Days == 1 ? "day" : "days")}"); + } + if (durationSpan.Hours > 0) + { + timeComponents.Add($"{durationSpan.Hours} {(durationSpan.Hours == 1 ? "hour" : "hours")}"); + } + if (durationSpan.Minutes > 0) + { + timeComponents.Add($"{durationSpan.Minutes} {(durationSpan.Minutes == 1 ? "minute" : "minutes")}"); + } + if (durationSpan.Seconds > 0) + { + timeComponents.Add($"{durationSpan.Seconds} {(durationSpan.Seconds == 1 ? "second" : "seconds")}"); + } + + // Join the non-zero components into a single readable string + string readableDuration = string.Join(", ", timeComponents); + + return readableDuration; + } + /// /// Converts PKIExpirationPeriod/PKIOverlappedPeriod attributes to time approximate times /// diff --git a/test/unit/DomainTrustProcessorTest.cs b/test/unit/DomainTrustProcessorTest.cs index c31326d0..c5fb3ef8 100644 --- a/test/unit/DomainTrustProcessorTest.cs +++ b/test/unit/DomainTrustProcessorTest.cs @@ -120,7 +120,7 @@ public void DomainTrustProcessor_TrustAttributesToType() test = DomainTrustProcessor.TrustAttributesToType(attrib); Assert.Equal(TrustType.External, test); - attrib = TrustAttributes.FilterSids; + attrib = TrustAttributes.QuarantinedDomain; test = DomainTrustProcessor.TrustAttributesToType(attrib); Assert.Equal(TrustType.External, test); } diff --git a/test/unit/Facades/MockLdapUtils.cs b/test/unit/Facades/MockLdapUtils.cs index 6b854a57..90e8cce3 100644 --- a/test/unit/Facades/MockLdapUtils.cs +++ b/test/unit/Facades/MockLdapUtils.cs @@ -1120,5 +1120,10 @@ public bool IsDomainController(string computerObjectId, string domainName) public void Dispose() { } + + public async Task<(bool Success, string DSHeuristics)> GetDSHueristics(string domain, string dn) + { + return (true, "0"); + } } } \ No newline at end of file diff --git a/test/unit/LdapPropertyTests.cs b/test/unit/LdapPropertyTests.cs index 8c40016c..485dd174 100644 --- a/test/unit/LdapPropertyTests.cs +++ b/test/unit/LdapPropertyTests.cs @@ -28,7 +28,7 @@ public LdapPropertyTests(ITestOutputHelper testOutputHelper) } [Fact] - public void LDAPPropertyProcessor_ReadDomainProperties_TestGoodData() + public async void LDAPPropertyProcessor_ReadDomainProperties_TestGoodData() { var mock = new MockDirectoryObject("DC\u003dtestlab,DC\u003dlocal", new Dictionary { @@ -36,7 +36,8 @@ public void LDAPPropertyProcessor_ReadDomainProperties_TestGoodData() {"msds-behavior-version", "6"} }, "S-1-5-21-3130019616-2776909439-2417379446",""); - var test = LdapPropertyProcessor.ReadDomainProperties(mock); + var processor = new LdapPropertyProcessor(new MockLdapUtils()); + var test = await processor.ReadDomainProperties(mock, "testlab.local"); Assert.Contains("functionallevel", test.Keys); Assert.Equal("2012 R2", test["functionallevel"] as string); Assert.Contains("description", test.Keys); @@ -44,14 +45,15 @@ public void LDAPPropertyProcessor_ReadDomainProperties_TestGoodData() } [Fact] - public void LDAPPropertyProcessor_ReadDomainProperties_TestBadFunctionalLevel() + public async void LDAPPropertyProcessor_ReadDomainProperties_TestBadFunctionalLevel() { var mock = new MockDirectoryObject("DC\u003dtestlab,DC\u003dlocal", new Dictionary { {"msds-behavior-version", "a"} }, "S-1-5-21-3130019616-2776909439-2417379446",""); - var test = LdapPropertyProcessor.ReadDomainProperties(mock); + var processor = new LdapPropertyProcessor(new MockLdapUtils()); + var test = await processor.ReadDomainProperties(mock,"testlab.local"); Assert.Contains("functionallevel", test.Keys); Assert.Equal("Unknown", test["functionallevel"] as string); }