From ee58db9f19ecfb081766e3ea9fc5e63a638b8c6a Mon Sep 17 00:00:00 2001 From: JonasBK Date: Wed, 10 Jul 2024 20:58:24 -0700 Subject: [PATCH 1/3] feat: additional ldap properties --- src/CommonLib/Enums/TrustAttributes.cs | 8 ++- src/CommonLib/ILDAPUtils.cs | 1 + src/CommonLib/LDAPProperties.cs | 5 ++ src/CommonLib/LDAPQueries/CommonPaths.cs | 3 + src/CommonLib/LDAPQueries/CommonProperties.cs | 4 +- src/CommonLib/LDAPUtils.cs | 10 ++++ src/CommonLib/OutputTypes/DomainTrust.cs | 2 + .../Processors/DomainTrustProcessor.cs | 16 ++++-- .../Processors/LDAPPropertyProcessor.cs | 24 +++++++- test/unit/DomainTrustProcessorTest.cs | 2 +- test/unit/Facades/MockLDAPUtils.cs | 5 ++ test/unit/LDAPPropertyTests.cs | 56 +++++++++---------- 12 files changed, 98 insertions(+), 38 deletions(-) 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 6423fb9b..a0a6f6de 100644 --- a/src/CommonLib/ILDAPUtils.cs +++ b/src/CommonLib/ILDAPUtils.cs @@ -78,6 +78,7 @@ public interface ILDAPUtils /// A TypedPrincipal object with the SID and Label TypedPrincipal ResolveDistinguishedName(string dn); + string GetDSHueristics(string dn); /// /// Performs an LDAP query using the parameters specified by the user. /// diff --git a/src/CommonLib/LDAPProperties.cs b/src/CommonLib/LDAPProperties.cs index e2ea3ce1..9c65ed1f 100644 --- a/src/CommonLib/LDAPProperties.cs +++ b/src/CommonLib/LDAPProperties.cs @@ -69,5 +69,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 WhenChanged = "whenchanged"; + public const string DSHeuristics = "dsheuristics"; } } 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 c1644aac..0107b709 100644 --- a/src/CommonLib/LDAPQueries/CommonProperties.cs +++ b/src/CommonLib/LDAPQueries/CommonProperties.cs @@ -53,7 +53,9 @@ public static class CommonProperties LDAPProperties.GroupPolicyOptions, LDAPProperties.AllowedToDelegateTo, LDAPProperties.AllowedToActOnBehalfOfOtherIdentity, LDAPProperties.WhenCreated, LDAPProperties.HostServiceAccount, LDAPProperties.UnixUserPassword, LDAPProperties.MsSFU30Password, - LDAPProperties.UnicodePassword + LDAPProperties.UnicodePassword, LDAPProperties.ExpirePasswordsOnSmartCardOnlyAccounts, + LDAPProperties.MachineAccountQuota, LDAPProperties.WhenChanged, LDAPProperties.SupportedEncryptionTypes, + LDAPProperties.DSHeuristics }; public static readonly string[] ContainerProps = diff --git a/src/CommonLib/LDAPUtils.cs b/src/CommonLib/LDAPUtils.cs index 9847ae17..89f1be72 100644 --- a/src/CommonLib/LDAPUtils.cs +++ b/src/CommonLib/LDAPUtils.cs @@ -803,6 +803,16 @@ public TypedPrincipal ResolveDistinguishedName(string dn) }; } + public string GetDSHueristics(string dn) + { + var configPath = CommonPaths.CreateDNPath(CommonPaths.DirectoryServicePath, dn); + var enumerable = QueryLDAP("(objectclass=*)", SearchScope.Base, null, adsPath: configPath); + if (enumerable == null) + return null; + var obj = enumerable.DefaultIfEmpty(null).FirstOrDefault(); + return obj.GetProperty(LDAPProperties.DSHeuristics); + } + /// /// Queries LDAP using LDAPQueryOptions /// 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 bb157e8e..fecd205c 100644 --- a/src/CommonLib/Processors/DomainTrustProcessor.cs +++ b/src/CommonLib/Processors/DomainTrustProcessor.cs @@ -61,10 +61,10 @@ public IEnumerable EnumerateDomainTrusts(string domain) continue; } - + var trustAttributes = result.GetProperty(LDAPProperties.TrustAttributes); + trust.TrustAttributes = trustAttributes; TrustAttributes attributes; - - if (int.TryParse(result.GetProperty(LDAPProperties.TrustAttributes), out var ta)) + if (int.TryParse(trustAttributes, out var ta)) { attributes = (TrustAttributes) ta; } @@ -79,7 +79,15 @@ public IEnumerable EnumerateDomainTrusts(string domain) if (name != null) trust.TargetDomainName = name; - 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 33cd6d4d..b10c1c8e 100644 --- a/src/CommonLib/Processors/LDAPPropertyProcessor.cs +++ b/src/CommonLib/Processors/LDAPPropertyProcessor.cs @@ -48,7 +48,7 @@ private static Dictionary GetCommonProps(ISearchResultEntry entr /// /// /// - public static Dictionary ReadDomainProperties(ISearchResultEntry entry) + public Dictionary ReadDomainProperties(ISearchResultEntry entry) { var props = GetCommonProps(entry); @@ -56,6 +56,12 @@ public static Dictionary ReadDomainProperties(ISearchResultEntry props.Add("functionallevel", FunctionalLevelToString(level)); + props.Add("expirepasswordsonsmartcardonlyaccounts", entry.GetProperty(LDAPProperties.ExpirePasswordsOnSmartCardOnlyAccounts)); + props.Add("machineaccountquota", entry.GetProperty(LDAPProperties.MachineAccountQuota)); + + var dsh = _utils.GetDSHueristics(entry.DistinguishedName); + props.Add("dsheuristics", dsh); + return props; } @@ -163,6 +169,13 @@ public async Task ReadUserProperties(ISearchResultEntry entry) 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 domain = Helpers.DistinguishedNameToDomain(entry.DistinguishedName); @@ -206,6 +219,8 @@ public async Task ReadUserProperties(ISearchResultEntry entry) props.Add("unicodepassword", entry.GetProperty(LDAPProperties.UnicodePassword)); props.Add("sfupassword", entry.GetProperty(LDAPProperties.MsSFU30Password)); props.Add("logonscript", entry.GetProperty(LDAPProperties.ScriptPath)); + props.Add("supportedencryptiontypes", entry.GetProperty(LDAPProperties.SupportedEncryptionTypes)); + props.Add("useraccountcontrol", uac); var ac = entry.GetProperty(LDAPProperties.AdminCount); if (ac != null) @@ -272,6 +287,11 @@ public async Task ReadComputerProperties(ISearchResultEntry 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 domain = Helpers.DistinguishedNameToDomain(entry.DistinguishedName); @@ -319,6 +339,8 @@ public async Task ReadComputerProperties(ISearchResultEntry Helpers.ConvertFileTimeToUnixEpoch(entry.GetProperty(LDAPProperties.PasswordLastSet))); props.Add("serviceprincipalnames", entry.GetArrayProperty(LDAPProperties.ServicePrincipalNames)); props.Add("email", entry.GetProperty(LDAPProperties.Email)); + props.Add("supportedencryptiontypes", entry.GetProperty(LDAPProperties.SupportedEncryptionTypes)); + props.Add("useraccountcontrol", uac); var os = entry.GetProperty(LDAPProperties.OperatingSystem); var sp = entry.GetProperty(LDAPProperties.ServicePack); diff --git a/test/unit/DomainTrustProcessorTest.cs b/test/unit/DomainTrustProcessorTest.cs index ad0dc5b2..a12bc81f 100644 --- a/test/unit/DomainTrustProcessorTest.cs +++ b/test/unit/DomainTrustProcessorTest.cs @@ -121,7 +121,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 5c99bbdc..6be26228 100644 --- a/test/unit/Facades/MockLDAPUtils.cs +++ b/test/unit/Facades/MockLDAPUtils.cs @@ -1094,5 +1094,10 @@ public bool IsDomainController(string computerObjectId, string domainName) { throw new NotImplementedException(); } + + public string GetDSHueristics(string dn) + { + throw new NotImplementedException(); + } } } \ No newline at end of file diff --git a/test/unit/LDAPPropertyTests.cs b/test/unit/LDAPPropertyTests.cs index e405a18a..f078a1bc 100644 --- a/test/unit/LDAPPropertyTests.cs +++ b/test/unit/LDAPPropertyTests.cs @@ -20,34 +20,34 @@ public LDAPPropertyTests(ITestOutputHelper testOutputHelper) _testOutputHelper = testOutputHelper; } - [Fact] - public void LDAPPropertyProcessor_ReadDomainProperties_TestGoodData() - { - var mock = new MockSearchResultEntry("DC\u003dtestlab,DC\u003dlocal", new Dictionary - { - {"description", "TESTLAB Domain"}, - {"msds-behavior-version", "6"} - }, "S-1-5-21-3130019616-2776909439-2417379446", Label.Domain); - - var test = LDAPPropertyProcessor.ReadDomainProperties(mock); - Assert.Contains("functionallevel", test.Keys); - Assert.Equal("2012 R2", test["functionallevel"] as string); - Assert.Contains("description", test.Keys); - Assert.Equal("TESTLAB Domain", test["description"] as string); - } - - [Fact] - public void LDAPPropertyProcessor_ReadDomainProperties_TestBadFunctionalLevel() - { - var mock = new MockSearchResultEntry("DC\u003dtestlab,DC\u003dlocal", new Dictionary - { - {"msds-behavior-version", "a"} - }, "S-1-5-21-3130019616-2776909439-2417379446", Label.Domain); - - var test = LDAPPropertyProcessor.ReadDomainProperties(mock); - Assert.Contains("functionallevel", test.Keys); - Assert.Equal("Unknown", test["functionallevel"] as string); - } + // [Fact] + // public void LDAPPropertyProcessor_ReadDomainProperties_TestGoodData() + // { + // var mock = new MockSearchResultEntry("DC\u003dtestlab,DC\u003dlocal", new Dictionary + // { + // {"description", "TESTLAB Domain"}, + // {"msds-behavior-version", "6"} + // }, "S-1-5-21-3130019616-2776909439-2417379446", Label.Domain); + + // var test = LDAPPropertyProcessor.ReadDomainProperties(mock); + // Assert.Contains("functionallevel", test.Keys); + // Assert.Equal("2012 R2", test["functionallevel"] as string); + // Assert.Contains("description", test.Keys); + // Assert.Equal("TESTLAB Domain", test["description"] as string); + // } + + // [Fact] + // public void LDAPPropertyProcessor_ReadDomainProperties_TestBadFunctionalLevel() + // { + // var mock = new MockSearchResultEntry("DC\u003dtestlab,DC\u003dlocal", new Dictionary + // { + // {"msds-behavior-version", "a"} + // }, "S-1-5-21-3130019616-2776909439-2417379446", Label.Domain); + + // var test = LDAPPropertyProcessor.ReadDomainProperties(mock); + // Assert.Contains("functionallevel", test.Keys); + // Assert.Equal("Unknown", test["functionallevel"] as string); + // } [Fact] public void LDAPPropertyProcessor_FunctionalLevelToString_TestFunctionalLevels() From 556948e81d767b281caf1a3764639b8974551e93 Mon Sep 17 00:00:00 2001 From: jknudsen Date: Mon, 5 Aug 2024 15:10:33 -0700 Subject: [PATCH 2/3] fix stuff and add more properties --- src/CommonLib/Enums/LDAPProperties.cs | 8 ++++++++ src/CommonLib/ILdapUtils.cs | 2 +- src/CommonLib/LdapQueries/CommonProperties.cs | 5 ++++- src/CommonLib/LdapUtils.cs | 20 +++++++++++++------ .../Processors/DomainTrustProcessor.cs | 2 +- .../Processors/LdapPropertyProcessor.cs | 17 ++++++++++++---- test/unit/Facades/MockLdapUtils.cs | 2 +- test/unit/LdapPropertyTests.cs | 6 ++++-- 8 files changed, 46 insertions(+), 16 deletions(-) diff --git a/src/CommonLib/Enums/LDAPProperties.cs b/src/CommonLib/Enums/LDAPProperties.cs index 29e0be23..d6f52554 100644 --- a/src/CommonLib/Enums/LDAPProperties.cs +++ b/src/CommonLib/Enums/LDAPProperties.cs @@ -86,5 +86,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/ILdapUtils.cs b/src/CommonLib/ILdapUtils.cs index 7d718b23..8347cb24 100644 --- a/src/CommonLib/ILdapUtils.cs +++ b/src/CommonLib/ILdapUtils.cs @@ -150,7 +150,7 @@ IAsyncEnumerable> RangedRetrieval(string distinguishedName, Task<(bool Success, TypedPrincipal Principal)> ResolveDistinguishedName(string distinguishedName); void AddDomainController(string domainControllerSID); IAsyncEnumerable GetWellKnownPrincipalOutput(); - string GetDSHueristics(string dn); + Task<(bool Success, string DSHeuristics)> GetDSHueristics(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/CommonProperties.cs b/src/CommonLib/LdapQueries/CommonProperties.cs index 7fabb0f9..1e79c976 100644 --- a/src/CommonLib/LdapQueries/CommonProperties.cs +++ b/src/CommonLib/LdapQueries/CommonProperties.cs @@ -57,7 +57,10 @@ public static class CommonProperties LDAPProperties.HostServiceAccount, LDAPProperties.UnixUserPassword, LDAPProperties.MsSFU30Password, LDAPProperties.UnicodePassword, LDAPProperties.ProfilePath, LDAPProperties.ScriptPath, LDAPProperties.ExpirePasswordsOnSmartCardOnlyAccounts, LDAPProperties.MachineAccountQuota, - LDAPProperties.WhenChanged, LDAPProperties.SupportedEncryptionTypes, LDAPProperties.DSHeuristics + LDAPProperties.WhenChanged, 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 2dc4470e..c4436eb8 100644 --- a/src/CommonLib/LdapUtils.cs +++ b/src/CommonLib/LdapUtils.cs @@ -1367,14 +1367,22 @@ public async Task IsDomainController(string computerObjectId, string domai } } - public string GetDSHueristics(string dn) + public async Task<(bool Success, string DSHeuristics)> GetDSHueristics(string dn) { var configPath = CommonPaths.CreateDNPath(CommonPaths.DirectoryServicePath, dn); - var enumerable = QueryLDAP("(objectclass=*)", SearchScope.Base, null, adsPath: configPath); - if (enumerable == null) - return null; - var obj = enumerable.DefaultIfEmpty(null).FirstOrDefault(); - return obj.GetProperty(LDAPProperties.DSHeuristics); + var queryParameters = new LdapQueryParameters { + Attributes = new[] { LDAPProperties.DSHeuristics }, + SearchScope = SearchScope.Base, + SearchBase = configPath + }; + var result = await Query(queryParameters).DefaultIfEmpty(null).FirstOrDefaultAsync(); + + if (result.IsSuccess && result.Value.GetObjectIdentifier(out var id)) { + var entry = result.Value; + return (true, entry.GetProperty(LDAPProperties.DSHeuristics)); + } + + return (false, null); } public void AddDomainController(string domainControllerSID) { diff --git a/src/CommonLib/Processors/DomainTrustProcessor.cs b/src/CommonLib/Processors/DomainTrustProcessor.cs index d2b83277..1df373ec 100644 --- a/src/CommonLib/Processors/DomainTrustProcessor.cs +++ b/src/CommonLib/Processors/DomainTrustProcessor.cs @@ -71,7 +71,7 @@ public async IAsyncEnumerable EnumerateDomainTrusts(string domain) continue; } - trust.TrustAttributes = trustAttributes; + trust.TrustAttributes = ta.ToString(); attributes = (TrustAttributes) ta; trust.IsTransitive = !attributes.HasFlag(TrustAttributes.NonTransitive); diff --git a/src/CommonLib/Processors/LdapPropertyProcessor.cs b/src/CommonLib/Processors/LdapPropertyProcessor.cs index 9ea81341..8d95c1d3 100644 --- a/src/CommonLib/Processors/LdapPropertyProcessor.cs +++ b/src/CommonLib/Processors/LdapPropertyProcessor.cs @@ -57,7 +57,8 @@ private static Dictionary GetCommonProps(IDirectoryObject entry) /// /// /// - public static Dictionary ReadDomainProperties(IDirectoryObject entry) { + public Dictionary ReadDomainProperties(IDirectoryObject entry) + { var props = GetCommonProps(entry); if (!entry.TryGetLongProperty(LDAPProperties.DomainFunctionalLevel, out var functionalLevel)) { @@ -68,9 +69,17 @@ public static Dictionary ReadDomainProperties(IDirectoryObject e props.Add("expirepasswordsonsmartcardonlyaccounts", entry.GetProperty(LDAPProperties.ExpirePasswordsOnSmartCardOnlyAccounts)); props.Add("machineaccountquota", entry.GetProperty(LDAPProperties.MachineAccountQuota)); - - var dsh = _utils.GetDSHueristics(entry.DistinguishedName); - props.Add("dsheuristics", dsh); + props.Add("minpwdlength", entry.GetProperty(LDAPProperties.MinPwdLength)); + props.Add("pwdproperties", entry.GetProperty(LDAPProperties.PwdProperties)); + props.Add("minpwdage", entry.GetProperty(LDAPProperties.MinPwdAge)); + props.Add("maxpwdage", entry.GetProperty(LDAPProperties.MaxPwdAge)); + props.Add("pwdhistorylength", entry.GetProperty(LDAPProperties.PwdHistoryLength)); + props.Add("lockoutduration", entry.GetProperty(LDAPProperties.LockoutDuration)); + props.Add("lockoutthreshold", entry.GetProperty(LDAPProperties.LockoutThreshold)); + props.Add("lockoutobservationwindow", entry.GetProperty(LDAPProperties.LockOutObservationWindow)); + + var dn = entry.GetProperty(LDAPProperties.DistinguishedName); + props.Add("dsheuristics", _utils.GetDSHueristics(dn)); return props; } diff --git a/test/unit/Facades/MockLdapUtils.cs b/test/unit/Facades/MockLdapUtils.cs index e33389e8..8c7cfcbd 100644 --- a/test/unit/Facades/MockLdapUtils.cs +++ b/test/unit/Facades/MockLdapUtils.cs @@ -1121,7 +1121,7 @@ public void Dispose() { } - public string GetDSHueristics(string dn) + public Task<(bool Success, string DSHeuristics)> GetDSHueristics(string dn) { throw new NotImplementedException(); } diff --git a/test/unit/LdapPropertyTests.cs b/test/unit/LdapPropertyTests.cs index 8c40016c..fb322844 100644 --- a/test/unit/LdapPropertyTests.cs +++ b/test/unit/LdapPropertyTests.cs @@ -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 = processor.ReadDomainProperties(mock); Assert.Contains("functionallevel", test.Keys); Assert.Equal("2012 R2", test["functionallevel"] as string); Assert.Contains("description", test.Keys); @@ -51,7 +52,8 @@ public void LDAPPropertyProcessor_ReadDomainProperties_TestBadFunctionalLevel() {"msds-behavior-version", "a"} }, "S-1-5-21-3130019616-2776909439-2417379446",""); - var test = LdapPropertyProcessor.ReadDomainProperties(mock); + var processor = new LdapPropertyProcessor(new MockLdapUtils()); + var test = processor.ReadDomainProperties(mock); Assert.Contains("functionallevel", test.Keys); Assert.Equal("Unknown", test["functionallevel"] as string); } From c9245f02a064963de40a9337109ca6e8b100dcd3 Mon Sep 17 00:00:00 2001 From: JonasBK Date: Mon, 5 Aug 2024 15:51:40 -0700 Subject: [PATCH 3/3] fix things --- .../Enums/KerberosEncryptionTypes.cs | 11 ++ src/CommonLib/Enums/LDAPProperties.cs | 1 - src/CommonLib/ILdapUtils.cs | 4 +- src/CommonLib/LdapQueries/CommonProperties.cs | 2 +- src/CommonLib/LdapUtils.cs | 14 ++- .../Processors/LdapPropertyProcessor.cs | 111 +++++++++++++++--- test/unit/Facades/MockLdapUtils.cs | 4 +- test/unit/LdapPropertyTests.cs | 8 +- 8 files changed, 125 insertions(+), 30 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 d6f52554..0b202e46 100644 --- a/src/CommonLib/Enums/LDAPProperties.cs +++ b/src/CommonLib/Enums/LDAPProperties.cs @@ -74,7 +74,6 @@ public static class LDAPProperties public const string ExpirePasswordsOnSmartCardOnlyAccounts = "msds-expirepasswordsonsmartcardonlyaccounts"; public const string MachineAccountQuota = "ms-ds-machineaccountquota"; public const string SupportedEncryptionTypes = "msds-supportedencryptiontypes"; - public const string WhenChanged = "whenchanged"; public const string DSHeuristics = "dsheuristics"; public const string DefaultNamingContext = "defaultnamingcontext"; public const string RootDomainNamingContext = "rootdomainnamingcontext"; diff --git a/src/CommonLib/ILdapUtils.cs b/src/CommonLib/ILdapUtils.cs index 8347cb24..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,7 +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 dn); + 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/CommonProperties.cs b/src/CommonLib/LdapQueries/CommonProperties.cs index 1e79c976..9aa84b3d 100644 --- a/src/CommonLib/LdapQueries/CommonProperties.cs +++ b/src/CommonLib/LdapQueries/CommonProperties.cs @@ -57,7 +57,7 @@ public static class CommonProperties LDAPProperties.HostServiceAccount, LDAPProperties.UnixUserPassword, LDAPProperties.MsSFU30Password, LDAPProperties.UnicodePassword, LDAPProperties.ProfilePath, LDAPProperties.ScriptPath, LDAPProperties.ExpirePasswordsOnSmartCardOnlyAccounts, LDAPProperties.MachineAccountQuota, - LDAPProperties.WhenChanged, LDAPProperties.SupportedEncryptionTypes, LDAPProperties.DSHeuristics, + LDAPProperties.SupportedEncryptionTypes, LDAPProperties.DSHeuristics, LDAPProperties.MinPwdLength, LDAPProperties.PwdProperties, LDAPProperties.MinPwdAge, LDAPProperties.MaxPwdAge, LDAPProperties.PwdHistoryLength, LDAPProperties.LockoutDuration, LDAPProperties.LockoutThreshold, LDAPProperties.LockOutObservationWindow diff --git a/src/CommonLib/LdapUtils.cs b/src/CommonLib/LdapUtils.cs index c4436eb8..e546d10f 100644 --- a/src/CommonLib/LdapUtils.cs +++ b/src/CommonLib/LdapUtils.cs @@ -1367,21 +1367,23 @@ public async Task IsDomainController(string computerObjectId, string domai } } - public async Task<(bool Success, string DSHeuristics)> GetDSHueristics(string dn) + 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(null).FirstOrDefaultAsync(); - if (result.IsSuccess && result.Value.GetObjectIdentifier(out var id)) { - var entry = result.Value; - return (true, entry.GetProperty(LDAPProperties.DSHeuristics)); + 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); } diff --git a/src/CommonLib/Processors/LdapPropertyProcessor.cs b/src/CommonLib/Processors/LdapPropertyProcessor.cs index 8d95c1d3..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,29 +57,38 @@ private static Dictionary GetCommonProps(IDirectoryObject entry) /// /// /// - public Dictionary ReadDomainProperties(IDirectoryObject entry) + public async Task> ReadDomainProperties(IDirectoryObject entry, string domain) { var props = GetCommonProps(entry); - if (!entry.TryGetLongProperty(LDAPProperties.DomainFunctionalLevel, out var functionalLevel)) { - functionalLevel = -1; - } - - props.Add("functionallevel", FunctionalLevelToString((int)functionalLevel)); 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("minpwdage", entry.GetProperty(LDAPProperties.MinPwdAge)); - props.Add("maxpwdage", entry.GetProperty(LDAPProperties.MaxPwdAge)); props.Add("pwdhistorylength", entry.GetProperty(LDAPProperties.PwdHistoryLength)); - props.Add("lockoutduration", entry.GetProperty(LDAPProperties.LockoutDuration)); props.Add("lockoutthreshold", entry.GetProperty(LDAPProperties.LockoutThreshold)); - props.Add("lockoutobservationwindow", entry.GetProperty(LDAPProperties.LockOutObservationWindow)); + + 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); - props.Add("dsheuristics", _utils.GetDSHueristics(dn)); + var dsh = await _utils.GetDSHueristics(domain, dn); + props.Add("dsheuristics", dsh.DSHeuristics); return props; } @@ -99,6 +108,7 @@ public static string FunctionalLevelToString(int level) { 5 => "2012", 6 => "2012 R2", 7 => "2016", + 8 => "2025", _ => "Unknown" }; @@ -234,13 +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("supportedencryptiontypes", entry.GetProperty(LDAPProperties.SupportedEncryptionTypes)); 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(); @@ -297,6 +309,9 @@ public async Task ReadComputerProperties(IDirectoryObject en 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) && entry.TryGetArrayProperty(LDAPProperties.AllowedToDelegateTo, out var delegates)) { @@ -337,7 +352,6 @@ 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("supportedencryptiontypes", entry.GetProperty(LDAPProperties.SupportedEncryptionTypes)); props.Add("useraccountcontrol", uac); var os = entry.GetProperty(LDAPProperties.OperatingSystem); var sp = entry.GetProperty(LDAPProperties.ServicePack); @@ -672,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/Facades/MockLdapUtils.cs b/test/unit/Facades/MockLdapUtils.cs index 8c7cfcbd..90e8cce3 100644 --- a/test/unit/Facades/MockLdapUtils.cs +++ b/test/unit/Facades/MockLdapUtils.cs @@ -1121,9 +1121,9 @@ public void Dispose() { } - public Task<(bool Success, string DSHeuristics)> GetDSHueristics(string dn) + public async Task<(bool Success, string DSHeuristics)> GetDSHueristics(string domain, string dn) { - throw new NotImplementedException(); + return (true, "0"); } } } \ No newline at end of file diff --git a/test/unit/LdapPropertyTests.cs b/test/unit/LdapPropertyTests.cs index fb322844..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 { @@ -37,7 +37,7 @@ public void LDAPPropertyProcessor_ReadDomainProperties_TestGoodData() }, "S-1-5-21-3130019616-2776909439-2417379446",""); var processor = new LdapPropertyProcessor(new MockLdapUtils()); - var test = processor.ReadDomainProperties(mock); + 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); @@ -45,7 +45,7 @@ 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 { @@ -53,7 +53,7 @@ public void LDAPPropertyProcessor_ReadDomainProperties_TestBadFunctionalLevel() }, "S-1-5-21-3130019616-2776909439-2417379446",""); var processor = new LdapPropertyProcessor(new MockLdapUtils()); - var test = processor.ReadDomainProperties(mock); + var test = await processor.ReadDomainProperties(mock,"testlab.local"); Assert.Contains("functionallevel", test.Keys); Assert.Equal("Unknown", test["functionallevel"] as string); }