diff --git a/src/CommonLib/Extensions.cs b/src/CommonLib/Extensions.cs index cfae61bd..295a501c 100644 --- a/src/CommonLib/Extensions.cs +++ b/src/CommonLib/Extensions.cs @@ -393,6 +393,21 @@ public static Label GetLabel(this SearchResultEntry entry) return objectType; } + /// + /// Add multiple entries to a dictionary. + /// + /// Dictionary (this) + /// KeyValuePairs to add + /// Key + /// Value + public static void AddMany(this Dictionary me, IEnumerable> keyValuePairs) + { + foreach (var kvp in keyValuePairs) + { + me.Add(kvp.Key, kvp.Value); + } + } + private const string GroupPolicyContainerClass = "groupPolicyContainer"; private const string OrganizationalUnitClass = "organizationalUnit"; private const string DomainClass = "domain"; diff --git a/src/CommonLib/Processors/LDAPPropertyProcessor.cs b/src/CommonLib/Processors/LDAPPropertyProcessor.cs index 101752a2..4227eb10 100644 --- a/src/CommonLib/Processors/LDAPPropertyProcessor.cs +++ b/src/CommonLib/Processors/LDAPPropertyProcessor.cs @@ -32,15 +32,9 @@ public LDAPPropertyProcessor(ILDAPUtils utils) private static Dictionary GetCommonProps(ISearchResultEntry entry) { - return new Dictionary - { - { - "description", entry.GetProperty(LDAPProperties.Description) - }, - { - "whencreated", Helpers.ConvertTimestampToUnixEpoch(entry.GetProperty(LDAPProperties.WhenCreated)) - } - }; + var props = GetProperties(LDAPProperties.Description, entry); + props.AddMany(GetProperties(LDAPProperties.WhenCreated, entry)); + return props; } /// @@ -51,37 +45,11 @@ private static Dictionary GetCommonProps(ISearchResultEntry entr public static Dictionary ReadDomainProperties(ISearchResultEntry entry) { var props = GetCommonProps(entry); - - if (!int.TryParse(entry.GetProperty(LDAPProperties.DomainFunctionalLevel), out var level)) level = -1; - - props.Add("functionallevel", FunctionalLevelToString(level)); + props.AddMany(GetProperties(LDAPProperties.DomainFunctionalLevel, entry)); return props; } - /// - /// Converts a numeric representation of a functional level to its appropriate functional level string - /// - /// - /// - public static string FunctionalLevelToString(int level) - { - var functionalLevel = level switch - { - 0 => "2000 Mixed/Native", - 1 => "2003 Interim", - 2 => "2003", - 3 => "2008", - 4 => "2008 R2", - 5 => "2012", - 6 => "2012 R2", - 7 => "2016", - _ => "Unknown" - }; - - return functionalLevel; - } - /// /// Reads specific LDAP properties related to GPOs /// @@ -90,7 +58,7 @@ public static string FunctionalLevelToString(int level) public static Dictionary ReadGPOProperties(ISearchResultEntry entry) { var props = GetCommonProps(entry); - props.Add("gpcpath", entry.GetProperty(LDAPProperties.GPCFileSYSPath)?.ToUpper()); + props.AddMany(GetProperties(LDAPProperties.GPCFileSYSPath, entry)); return props; } @@ -113,17 +81,7 @@ public static Dictionary ReadOUProperties(ISearchResultEntry ent public static Dictionary ReadGroupProperties(ISearchResultEntry entry) { var props = GetCommonProps(entry); - - var ac = entry.GetProperty(LDAPProperties.AdminCount); - if (ac != null) - { - var a = int.Parse(ac); - props.Add("admincount", a != 0); - } - else - { - props.Add("admincount", false); - } + props.AddMany(GetProperties(LDAPProperties.AdminCount, entry)); return props; } @@ -149,28 +107,83 @@ public async Task ReadUserProperties(ISearchResultEntry entry) var userProps = new UserProperties(); var props = GetCommonProps(entry); - var uacFlags = (UacFlags)0; - var uac = entry.GetProperty(LDAPProperties.UserAccountControl); - if (int.TryParse(uac, out var flag)) - { - uacFlags = (UacFlags)flag; - } + props.AddMany((GetProperties(LDAPProperties.AllowedToDelegateTo, entry))); + userProps.AllowedToDelegate = (await ReadPropertyDelegates(entry)).ToArray(); + + props.AddMany(GetProperties(LDAPProperties.UserAccountControl, entry)); + props.AddMany(GetProperties(LDAPProperties.LastLogon, entry)); + props.AddMany(GetProperties(LDAPProperties.LastLogonTimestamp, entry)); + props.AddMany(GetProperties(LDAPProperties.PasswordLastSet, entry)); + props.AddMany(GetProperties(LDAPProperties.ServicePrincipalNames, entry)); + props.AddMany(GetProperties(LDAPProperties.DisplayName, entry)); + props.AddMany(GetProperties(LDAPProperties.Email, entry)); + props.AddMany(GetProperties(LDAPProperties.Title, entry)); + props.AddMany(GetProperties(LDAPProperties.HomeDirectory, entry)); + props.AddMany(GetProperties(LDAPProperties.UserPassword, entry)); + props.AddMany(GetProperties(LDAPProperties.UnixUserPassword, entry)); + props.AddMany(GetProperties(LDAPProperties.UnicodePassword, entry)); + props.AddMany(GetProperties(LDAPProperties.MsSFU30Password, entry)); + props.AddMany(GetProperties(LDAPProperties.ScriptPath, entry)); + props.AddMany(GetProperties(LDAPProperties.AdminCount, entry)); + + var sidHistory = ReadSidHistory(entry); + props.Add(PropertyMap.GetPropertyName(LDAPProperties.SIDHistory), sidHistory.ToArray()); + userProps.SidHistory = sidHistory.Select(ssid => ReadSidPrincipal(entry, ssid)).ToArray(); + + userProps.Props = props; + + return userProps; + } + + /// + /// Reads specific LDAP properties related to Computers + /// + /// + /// + public async Task ReadComputerProperties(ISearchResultEntry entry) + { + var compProps = new ComputerProperties(); + var props = GetCommonProps(entry); - props.Add("sensitive", uacFlags.HasFlag(UacFlags.NotDelegated)); - props.Add("dontreqpreauth", uacFlags.HasFlag(UacFlags.DontReqPreauth)); - props.Add("passwordnotreqd", uacFlags.HasFlag(UacFlags.PasswordNotRequired)); - props.Add("unconstraineddelegation", uacFlags.HasFlag(UacFlags.TrustedForDelegation)); - props.Add("pwdneverexpires", uacFlags.HasFlag(UacFlags.DontExpirePassword)); - props.Add("enabled", !uacFlags.HasFlag(UacFlags.AccountDisable)); - props.Add("trustedtoauth", uacFlags.HasFlag(UacFlags.TrustedToAuthForDelegation)); + props.AddMany(GetProperties(LDAPProperties.UserAccountControl, entry)); - var domain = Helpers.DistinguishedNameToDomain(entry.DistinguishedName); + props.AddMany((GetProperties(LDAPProperties.AllowedToDelegateTo, entry))); + compProps.AllowedToDelegate = (await ReadPropertyDelegates(entry)).ToArray(); + + compProps.AllowedToAct = ReadAllowedToActPrincipals(entry).ToArray(); + + props.AddMany(GetProperties(LDAPProperties.LastLogon, entry)); + props.AddMany(GetProperties(LDAPProperties.LastLogonTimestamp, entry)); + props.AddMany(GetProperties(LDAPProperties.PasswordLastSet, entry)); + props.AddMany(GetProperties(LDAPProperties.ServicePrincipalNames, entry)); + props.AddMany(GetProperties(LDAPProperties.Email, entry)); + props.AddMany(GetProperties(LDAPProperties.OperatingSystem, entry)); + + var sidHistory = ReadSidHistory(entry); + props.Add(PropertyMap.GetPropertyName(LDAPProperties.SIDHistory), sidHistory.ToArray()); + compProps.SidHistory = sidHistory.Select(ssid => ReadSidPrincipal(entry, ssid)).ToArray(); + + compProps.DumpSMSAPassword = ReadSmsaPrincipals(entry).ToArray(); + + compProps.Props = props; + return compProps; + } + + /// + /// Reads principals user or computer may delegate. + /// + /// + /// + public async Task> ReadPropertyDelegates(ISearchResultEntry entry) + { var comps = new List(); - if (uacFlags.HasFlag(UacFlags.TrustedToAuthForDelegation)) + + var domain = Helpers.DistinguishedNameToDomain(entry.DistinguishedName); + var uacFlags = GetUacFlags(entry); + if (uacFlags[UacFlags.TrustedToAuthForDelegation]) { var delegates = entry.GetArrayProperty(LDAPProperties.AllowedToDelegateTo); - props.Add("allowedtodelegate", delegates); foreach (var d in delegates) { @@ -187,42 +200,19 @@ public async Task ReadUserProperties(ISearchResultEntry entry) } } - userProps.AllowedToDelegate = comps.Distinct().ToArray(); - - props.Add("lastlogon", Helpers.ConvertFileTimeToUnixEpoch(entry.GetProperty(LDAPProperties.LastLogon))); - props.Add("lastlogontimestamp", - Helpers.ConvertFileTimeToUnixEpoch(entry.GetProperty(LDAPProperties.LastLogonTimestamp))); - props.Add("pwdlastset", - Helpers.ConvertFileTimeToUnixEpoch(entry.GetProperty(LDAPProperties.PasswordLastSet))); - var spn = entry.GetArrayProperty(LDAPProperties.ServicePrincipalNames); - props.Add("serviceprincipalnames", spn); - props.Add("hasspn", spn.Length > 0); - props.Add("displayname", entry.GetProperty(LDAPProperties.DisplayName)); - props.Add("email", entry.GetProperty(LDAPProperties.Email)); - props.Add("title", entry.GetProperty(LDAPProperties.Title)); - props.Add("homedirectory", entry.GetProperty(LDAPProperties.HomeDirectory)); - props.Add("userpassword", entry.GetProperty(LDAPProperties.UserPassword)); - props.Add("unixpassword", entry.GetProperty(LDAPProperties.UnixUserPassword)); - props.Add("unicodepassword", entry.GetProperty(LDAPProperties.UnicodePassword)); - props.Add("sfupassword", entry.GetProperty(LDAPProperties.MsSFU30Password)); - props.Add("logonscript", entry.GetProperty(LDAPProperties.ScriptPath)); - - var ac = entry.GetProperty(LDAPProperties.AdminCount); - if (ac != null) - { - if (int.TryParse(ac, out var parsed)) - props.Add("admincount", parsed != 0); - else - props.Add("admincount", false); - } - else - { - props.Add("admincount", false); - } + return comps.Distinct().ToList(); + } + /// + /// Reads history of SIDs for domain object. + /// + /// + /// + public List ReadSidHistory(ISearchResultEntry entry) + { + var domain = Helpers.DistinguishedNameToDomain(entry.DistinguishedName); var sh = entry.GetByteArrayProperty(LDAPProperties.SIDHistory); var sidHistoryList = new List(); - var sidHistoryPrincipals = new List(); foreach (var sid in sh) { string sSid; @@ -236,68 +226,33 @@ public async Task ReadUserProperties(ISearchResultEntry entry) } sidHistoryList.Add(sSid); - - var res = _utils.ResolveIDAndType(sSid, domain); - - sidHistoryPrincipals.Add(res); } - userProps.SidHistory = sidHistoryPrincipals.Distinct().ToArray(); - - props.Add("sidhistory", sidHistoryList.ToArray()); - - userProps.Props = props; - - return userProps; + return sidHistoryList; } /// - /// Reads specific LDAP properties related to Computers + /// Get SID principal. /// /// + /// /// - public async Task ReadComputerProperties(ISearchResultEntry entry) + public TypedPrincipal ReadSidPrincipal(ISearchResultEntry entry, string sidHistoryItem) { - var compProps = new ComputerProperties(); - var props = GetCommonProps(entry); - - var flags = (UacFlags)0; - var uac = entry.GetProperty(LDAPProperties.UserAccountControl); - if (int.TryParse(uac, out var flag)) - { - flags = (UacFlags)flag; - } - - props.Add("enabled", !flags.HasFlag(UacFlags.AccountDisable)); - props.Add("unconstraineddelegation", flags.HasFlag(UacFlags.TrustedForDelegation)); - props.Add("trustedtoauth", flags.HasFlag(UacFlags.TrustedToAuthForDelegation)); - props.Add("isdc", flags.HasFlag(UacFlags.ServerTrustAccount)); - var domain = Helpers.DistinguishedNameToDomain(entry.DistinguishedName); + return _utils.ResolveIDAndType(sidHistoryItem, domain); + } - var comps = new List(); - if (flags.HasFlag(UacFlags.TrustedToAuthForDelegation)) - { - var delegates = entry.GetArrayProperty(LDAPProperties.AllowedToDelegateTo); - props.Add("allowedtodelegate", delegates); - - foreach (var d in delegates) - { - var hname = d.Contains("/") ? d.Split('/')[1] : d; - hname = hname.Split(':')[0]; - var resolvedHost = await _utils.ResolveHostToSid(hname, domain); - if (resolvedHost != null && (resolvedHost.Contains(".") || resolvedHost.Contains("S-1"))) - comps.Add(new TypedPrincipal - { - ObjectIdentifier = resolvedHost, - ObjectType = Label.Computer - }); - } - } - - compProps.AllowedToDelegate = comps.Distinct().ToArray(); - + /// + /// Read principals for identities domain object may act on behalf of. + /// + /// + /// + public List ReadAllowedToActPrincipals(ISearchResultEntry entry) + { var allowedToActPrincipals = new List(); + + var domain = Helpers.DistinguishedNameToDomain(entry.DistinguishedName); var rawAllowedToAct = entry.GetByteProperty(LDAPProperties.AllowedToActOnBehalfOfOtherIdentity); if (rawAllowedToAct != null) { @@ -310,50 +265,18 @@ public async Task ReadComputerProperties(ISearchResultEntry } } - compProps.AllowedToAct = allowedToActPrincipals.ToArray(); - - props.Add("lastlogon", Helpers.ConvertFileTimeToUnixEpoch(entry.GetProperty(LDAPProperties.LastLogon))); - props.Add("lastlogontimestamp", - Helpers.ConvertFileTimeToUnixEpoch(entry.GetProperty(LDAPProperties.LastLogonTimestamp))); - props.Add("pwdlastset", - Helpers.ConvertFileTimeToUnixEpoch(entry.GetProperty(LDAPProperties.PasswordLastSet))); - props.Add("serviceprincipalnames", entry.GetArrayProperty(LDAPProperties.ServicePrincipalNames)); - props.Add("email", entry.GetProperty(LDAPProperties.Email)); - var os = entry.GetProperty(LDAPProperties.OperatingSystem); - var sp = entry.GetProperty(LDAPProperties.ServicePack); - - if (sp != null) os = $"{os} {sp}"; - - props.Add("operatingsystem", os); - - var sh = entry.GetByteArrayProperty(LDAPProperties.SIDHistory); - var sidHistoryList = new List(); - var sidHistoryPrincipals = new List(); - foreach (var sid in sh) - { - string sSid; - try - { - sSid = new SecurityIdentifier(sid, 0).Value; - } - catch - { - continue; - } - - sidHistoryList.Add(sSid); - - var res = _utils.ResolveIDAndType(sSid, domain); - - sidHistoryPrincipals.Add(res); - } - - compProps.SidHistory = sidHistoryPrincipals.ToArray(); - - props.Add("sidhistory", sidHistoryList.ToArray()); + return allowedToActPrincipals; + } - var hsa = entry.GetArrayProperty(LDAPProperties.HostServiceAccount); + /// + /// Read Standalone Managed Service Accounts of domain object. + /// + /// + /// + public List ReadSmsaPrincipals(ISearchResultEntry entry) + { var smsaPrincipals = new List(); + var hsa = entry.GetArrayProperty(LDAPProperties.HostServiceAccount); if (hsa != null) { foreach (var dn in hsa) @@ -365,11 +288,7 @@ public async Task ReadComputerProperties(ISearchResultEntry } } - compProps.DumpSMSAPassword = smsaPrincipals.ToArray(); - - compProps.Props = props; - - return compProps; + return smsaPrincipals; } /// @@ -583,6 +502,167 @@ public Dictionary ParseAllProperties(ISearchResultEntry entry) return props; } + + public static Dictionary GetProperties(string ldapProperty, ISearchResultEntry entry) + { + var props = new Dictionary(); + switch (ldapProperty) + { + case LDAPProperties.Description: + props.Add(PropertyMap.GetPropertyName(ldapProperty), entry.GetProperty(LDAPProperties.Description)); + break; + case LDAPProperties.WhenCreated: + props.Add(PropertyMap.GetPropertyName(ldapProperty), Helpers.ConvertTimestampToUnixEpoch(entry.GetProperty(LDAPProperties.WhenCreated))); + break; + case LDAPProperties.DomainFunctionalLevel: + if (!int.TryParse(entry.GetProperty(LDAPProperties.DomainFunctionalLevel), out var level)) + level = -1; + props.Add(PropertyMap.GetPropertyName(ldapProperty), FunctionalLevelToString(level)); + break; + case LDAPProperties.GPCFileSYSPath: + props.Add(PropertyMap.GetPropertyName(ldapProperty), entry.GetProperty(LDAPProperties.GPCFileSYSPath)?.ToUpper()); + break; + case LDAPProperties.LastLogon: + props.Add(PropertyMap.GetPropertyName(ldapProperty), Helpers.ConvertFileTimeToUnixEpoch(entry.GetProperty(LDAPProperties.LastLogon))); + break; + case LDAPProperties.LastLogonTimestamp: + props.Add(PropertyMap.GetPropertyName(ldapProperty), Helpers.ConvertFileTimeToUnixEpoch(entry.GetProperty(LDAPProperties.LastLogonTimestamp))); + break; + case LDAPProperties.PasswordLastSet: + props.Add(PropertyMap.GetPropertyName(ldapProperty), Helpers.ConvertFileTimeToUnixEpoch(entry.GetProperty(LDAPProperties.PasswordLastSet))); + break; + case LDAPProperties.ServicePrincipalNames: + var spn = entry.GetArrayProperty(LDAPProperties.ServicePrincipalNames); + props.Add(PropertyMap.GetPropertyName(ldapProperty), spn); + props.Add("hasspn", spn.Length > 0); + break; + case LDAPProperties.DisplayName: + props.Add(PropertyMap.GetPropertyName(ldapProperty), entry.GetProperty(LDAPProperties.DisplayName)); + break; + case LDAPProperties.Email: + props.Add(PropertyMap.GetPropertyName(ldapProperty), entry.GetProperty(LDAPProperties.Email)); + break; + case LDAPProperties.Title: + props.Add(PropertyMap.GetPropertyName(ldapProperty), entry.GetProperty(LDAPProperties.Title)); + break; + case LDAPProperties.HomeDirectory: + props.Add(PropertyMap.GetPropertyName(ldapProperty), entry.GetProperty(LDAPProperties.HomeDirectory)); + break; + case LDAPProperties.UserPassword: + props.Add(PropertyMap.GetPropertyName(ldapProperty), entry.GetProperty(LDAPProperties.UserPassword)); + break; + case LDAPProperties.UnixUserPassword: + props.Add(PropertyMap.GetPropertyName(ldapProperty), entry.GetProperty(LDAPProperties.UnixUserPassword)); + break; + case LDAPProperties.UnicodePassword: + props.Add(PropertyMap.GetPropertyName(ldapProperty), entry.GetProperty(LDAPProperties.UnicodePassword)); + break; + case LDAPProperties.MsSFU30Password: + props.Add(PropertyMap.GetPropertyName(ldapProperty), entry.GetProperty(LDAPProperties.MsSFU30Password)); + break; + case LDAPProperties.ScriptPath: + props.Add(PropertyMap.GetPropertyName(ldapProperty), entry.GetProperty(LDAPProperties.ScriptPath)); + break; + case LDAPProperties.AdminCount: + var ac = entry.GetProperty(LDAPProperties.AdminCount); + if (ac != null) + { + int.TryParse(ac, out var a); + props.Add(PropertyMap.GetPropertyName(ldapProperty), a != 0); + } + else + { + props.Add(PropertyMap.GetPropertyName(ldapProperty), false); + } + break; + case LDAPProperties.UserAccountControl: + var allFlags = GetUacFlags(entry); + props.Add(PropertyMap.GetUacPropertyName(UacFlags.NotDelegated), allFlags[UacFlags.NotDelegated]); + props.Add(PropertyMap.GetUacPropertyName(UacFlags.DontReqPreauth), allFlags[UacFlags.DontReqPreauth]); + props.Add(PropertyMap.GetUacPropertyName(UacFlags.PasswordNotRequired), allFlags[UacFlags.PasswordNotRequired]); + props.Add(PropertyMap.GetUacPropertyName(UacFlags.TrustedForDelegation), allFlags[UacFlags.TrustedForDelegation]); + props.Add(PropertyMap.GetUacPropertyName(UacFlags.DontExpirePassword), allFlags[UacFlags.DontExpirePassword]); + // Note that we flip the flag for Account Disable ("enabled" by resolved name) + props.Add(PropertyMap.GetUacPropertyName(UacFlags.AccountDisable), !allFlags[UacFlags.AccountDisable]); + props.Add(PropertyMap.GetUacPropertyName(UacFlags.TrustedToAuthForDelegation), allFlags[UacFlags.TrustedToAuthForDelegation]); + props.Add(PropertyMap.GetUacPropertyName(UacFlags.ServerTrustAccount), allFlags[UacFlags.ServerTrustAccount]); + break; + case LDAPProperties.OperatingSystem: + var os = entry.GetProperty(LDAPProperties.OperatingSystem); + var sp = entry.GetProperty(LDAPProperties.ServicePack); + if (sp != null) os = $"{os} {sp}"; + props.Add(PropertyMap.GetPropertyName(ldapProperty), os); + break; + case LDAPProperties.AllowedToDelegateTo: + var delegates = entry.GetArrayProperty(LDAPProperties.AllowedToDelegateTo); + props.Add(PropertyMap.GetPropertyName(ldapProperty), delegates); + break; + + default: + throw new ArgumentException("Cannot resolve to output property name.", ldapProperty); + } + + return props; + } + + /// + /// Converts a numeric representation of a functional level to its appropriate functional level string + /// + /// + /// + public static string FunctionalLevelToString(int level) + { + var functionalLevel = level switch + { + 0 => "2000 Mixed/Native", + 1 => "2003 Interim", + 2 => "2003", + 3 => "2008", + 4 => "2008 R2", + 5 => "2012", + 6 => "2012 R2", + 7 => "2016", + _ => "Unknown" + }; + + return functionalLevel; + } + + /// + /// Returns all flags of User Account Control and whether or not they're active. + /// + /// + /// + public static Dictionary GetUacFlags(ISearchResultEntry entry) + { + var props = new Dictionary(); + + var uacFlags = (UacFlags)0; + var uac = entry.GetProperty(LDAPProperties.UserAccountControl); + if (int.TryParse(uac, out var flags)) + { + uacFlags = (UacFlags)flags; + } + + return ReadFlags(uacFlags); + } + + /// + /// Get all flags of a domain object by enumeration. + /// + /// + /// + /// + private static Dictionary ReadFlags(T flags) + where T : Enum + { + return Enum.GetValues(typeof(T)) + .Cast() + .ToDictionary( + val => val, + val => flags.HasFlag(val) + ); + } /// /// Parse CertTemplate attribute msPKI-RA-Application-Policies @@ -726,6 +806,97 @@ private enum IsTextUnicodeFlags } } + /// + /// Provides single-truth mapping of domain object properties. + /// + public static class PropertyMap + { + /// + /// Get output name of a domain object property. + /// + /// + /// + /// + public static string GetPropertyName(string ldapProperty) + { + switch (ldapProperty) + { + case LDAPProperties.Description: + return "description"; + case LDAPProperties.WhenCreated: + return "whencreated"; + case LDAPProperties.DomainFunctionalLevel: + return "functionallevel"; + case LDAPProperties.GPCFileSYSPath: + return "gpcpath"; + case LDAPProperties.LastLogon: + return "lastlogon"; + case LDAPProperties.LastLogonTimestamp: + return "lastlogontimestamp"; + case LDAPProperties.PasswordLastSet: + return "pwdlastset"; + case LDAPProperties.ServicePrincipalNames: + return "serviceprincipalnames"; + case LDAPProperties.DisplayName: + return "displayname"; + case LDAPProperties.Email: + return "email"; + case LDAPProperties.Title: + return "title"; + case LDAPProperties.HomeDirectory: + return "homedirectory"; + case LDAPProperties.UserPassword: + return "userpassword"; + case LDAPProperties.UnixUserPassword: + return "unixpassword"; + case LDAPProperties.UnicodePassword: + return "unicodepassword"; + case LDAPProperties.MsSFU30Password: + return "sfupassword"; + case LDAPProperties.ScriptPath: + return "logonscript"; + case LDAPProperties.AdminCount: + return "admincount"; + case LDAPProperties.OperatingSystem: + return "operatingsystem"; + case LDAPProperties.AllowedToDelegateTo: + return "allowedtodelegate"; + case LDAPProperties.SIDHistory: + return "sidhistory"; + + default: + throw new ArgumentException("Cannot resolve to output property name.", ldapProperty); + } + } + + public static string GetUacPropertyName(UacFlags flag) + { + switch (flag) + { + case UacFlags.NotDelegated: + return "sensitive"; + case UacFlags.DontReqPreauth: + return "dontreqpreauth"; + case UacFlags.PasswordNotRequired: + return "passwordnotreqd"; + case UacFlags.TrustedForDelegation: + return "unconstraineddelegation"; + case UacFlags.DontExpirePassword: + return "pwdneverexpires"; + // Note that we flip the flag for output + case UacFlags.AccountDisable: + return "enabled"; + case UacFlags.TrustedToAuthForDelegation: + return "trustedtoauth"; + case UacFlags.ServerTrustAccount: + return "isdc"; + + default: + throw new ArgumentException("Cannot resolve to output property name.", Enum.GetName(typeof(UacFlags), flag)); + } + } + } + public class ParsedCertificate { public string Thumbprint { get; set; } diff --git a/test/unit/LDAPPropertyTests.cs b/test/unit/LDAPPropertyTests.cs index 5da83d68..2f6fbb4a 100644 --- a/test/unit/LDAPPropertyTests.cs +++ b/test/unit/LDAPPropertyTests.cs @@ -1,7 +1,15 @@ using System; using System.Collections.Generic; +using System.DirectoryServices; +using System.Linq; +using System.Runtime.Serialization; +using System.Security.AccessControl; +using System.Security.Principal; using System.Threading.Tasks; using CommonLibTest.Facades; +using FluentAssertions.Specialized; +using Moq; +using SharpHoundCommonLib; using SharpHoundCommonLib.Enums; using SharpHoundCommonLib.OutputTypes; using SharpHoundCommonLib.Processors; @@ -845,5 +853,305 @@ public void LDAPPropertyProcessor_ParseAllProperties_CollectionCountOne_NotBadPa Assert.Single(keys); } + [Fact] + public async Task LDAPPropertyProcessor_ReadPropertyDelegates_ReturnsPoplatedList() + { + var mock = new MockSearchResultEntry("CN\u003dWIN10,OU\u003dTestOU,DC\u003dtestlab,DC\u003dlocal", + new Dictionary + { + {"useraccountcontrol", 0x1000000.ToString()}, + { + "msds-allowedtodelegateto", new[] + { + "ldap/PRIMARY.testlab.local/testlab.local", + "ldap/PRIMARY.testlab.local", + "ldap/PRIMARY", + "ldap/WIN10" + } + } + }, "S-1-5-21-3130019616-2776909439-2417379446-1101", Label.Computer); + + var utils = new MockLDAPUtils(); + var processor = new LDAPPropertyProcessor(utils); + var props = await processor.ReadPropertyDelegates(mock); + var delegates = props.Select(d => d.ObjectIdentifier); + + foreach (var principal in mock.GetArrayProperty("msds-allowedtodelegateto")) + { + var host = await utils.ResolveHostToSid(principal, mock.GetProperty(LDAPProperties.DistinguishedName)); + Assert.Single(delegates, host); + } + } + + [WindowsOnlyFact] + public void LDAPPropertyProcessor_ReadSidHistory_ReturnsPopulatedList() + { + var mock = new MockSearchResultEntry("CN\u003dWIN10,OU\u003dTestOU,DC\u003dtestlab,DC\u003dlocal", + new Dictionary + { + { + "sidhistory", new[] + { + Helpers.B64ToBytes("AQUAAAAAAAUVAAAAIE+Qun9GhKV2SBaQUQQAAA==") + } + }, + }, "S-1-5-21-3130019616-2776909439-2417379446-1101", Label.Computer); + + var processor = new LDAPPropertyProcessor(new MockLDAPUtils()); + var sids = processor.ReadSidHistory(mock); + + Assert.Contains("S-1-5-21-3130019616-2776909439-2417379446-1105", sids); + Assert.Single(sids); + } + + [Fact] + public void LDAPPropertyProcessor_ReadSidPrincipal_GetPrincipal() + { + var mock = new MockSearchResultEntry("CN\u003dWIN10,OU\u003dTestOU,DC\u003dtestlab,DC\u003dlocal", + new Dictionary(), "S-1-5-21-3130019616-2776909439-2417379446-1101", Label.Computer); + + var sid = "S-1-5-21-3130019616-2776909439-2417379446-1105"; + var processor = new LDAPPropertyProcessor(new MockLDAPUtils()); + var principal = processor.ReadSidPrincipal(mock, sid); + + Assert.Equal(new TypedPrincipal + { + ObjectIdentifier = "S-1-5-21-3130019616-2776909439-2417379446-1105", + ObjectType = Label.User + }, principal); + } + + [Fact] + public void LDAPPropertyProcessor_ReadSmsaPrincipals_ReturnsPopulatedList() + { + var mock = new MockSearchResultEntry("CN\u003dWIN10,OU\u003dTestOU,DC\u003dtestlab,DC\u003dlocal", + new Dictionary + { + { + "msds-hostserviceaccount", new[] + { + "CN=dfm,CN=Users,DC=testlab,DC=local", + "CN=krbtgt,CN=Users,DC=testlab,DC=local" + } + } + }, "S-1-5-21-3130019616-2776909439-2417379446-1101", Label.Computer); + + var processor = new LDAPPropertyProcessor(new MockLDAPUtils()); + var smsaPrincipals = processor.ReadSmsaPrincipals(mock); + var sids = smsaPrincipals.Select(p => p.ObjectIdentifier); + + Assert.Single(sids, "S-1-5-21-3130019616-2776909439-2417379446-1105"); + Assert.Single(sids, "S-1-5-21-3130019616-2776909439-2417379446-502"); + } + + public static IEnumerable UserAccessControlData => + new List + { + new object[] + { + ((int)(UacFlags.NotDelegated | UacFlags.AccountDisable)).ToString(), + new Dictionary {{ "sensitive", true }, { "enabled", false }} + }, + new object[] + { + ((int)(UacFlags.ServerTrustAccount | UacFlags.PasswordNotRequired | UacFlags.TrustedForDelegation)).ToString(), + new Dictionary {{ "isdc", true }, { "passwordnotreqd", true }, { "unconstraineddelegation", true }, { "enabled", true }} + }, + }; + + [Theory] + [MemberData(nameof(UserAccessControlData))] + public void LDAPPropertyProcessor_GetProperties_UserAccountControl(string property, Dictionary expectedFlags) + { + var mock = new MockSearchResultEntry("CN\u003dWIN10,OU\u003dTestOU,DC\u003dtestlab,DC\u003dlocal", + new Dictionary + { + { + "useraccountcontrol", property + } + }, "S-1-5-21-3130019616-2776909439-2417379446-1101", Label.User); + + var props = LDAPPropertyProcessor.GetProperties(LDAPProperties.UserAccountControl, mock); + + foreach (var flag in props) + { + var expectedFlag = expectedFlags.ContainsKey(flag.Key) && expectedFlags[flag.Key]; + Assert.Equal(expectedFlag, (bool)flag.Value); + } + } + + public static IEnumerable SimplePropertyTestData => + new List + { + new object[] + { + LDAPProperties.Description, + new Dictionary {{ LDAPProperties.Description, "test desc" }}, + new Dictionary {{ "description", "test desc" }}, + }, + new object[] + { + LDAPProperties.DomainFunctionalLevel, + new Dictionary {{ LDAPProperties.DomainFunctionalLevel, "1" }}, + new Dictionary {{ "functionallevel", "2003 Interim" }}, + }, + new object[] + { + LDAPProperties.DomainFunctionalLevel, + new Dictionary {{ LDAPProperties.DomainFunctionalLevel, "nope" }}, + new Dictionary {{ "functionallevel", "Unknown" }}, + }, + new object[] + { + LDAPProperties.GPCFileSYSPath, + new Dictionary {{ LDAPProperties.GPCFileSYSPath, "/test/testy/test" }}, + new Dictionary {{ "gpcpath", "/TEST/TESTY/TEST" }}, + }, + new object[] + { + LDAPProperties.DisplayName, + new Dictionary {{ LDAPProperties.DisplayName, "one test of a display name" }}, + new Dictionary {{ "displayname", "one test of a display name" }}, + }, + new object[] + { + LDAPProperties.Email, + new Dictionary {{ LDAPProperties.Email, "test@testdomain.com" }}, + new Dictionary {{ "email", "test@testdomain.com" }}, + }, + new object[] + { + LDAPProperties.Title, + new Dictionary {{ LDAPProperties.Title, "Test Title" }}, + new Dictionary {{ "title", "Test Title" }}, + }, + new object[] + { + LDAPProperties.HomeDirectory, + new Dictionary {{ LDAPProperties.HomeDirectory, "/users/test" }}, + new Dictionary {{ "homedirectory", "/users/test" }}, + }, + new object[] + { + LDAPProperties.UserPassword, + new Dictionary {{ LDAPProperties.UserPassword, "1234" }}, + new Dictionary {{ "userpassword", "1234" }}, + }, + new object[] + { + LDAPProperties.UnixUserPassword, + new Dictionary {{ LDAPProperties.UnixUserPassword, "1234" }}, + new Dictionary {{ "unixpassword", "1234" }}, + }, + new object[] + { + LDAPProperties.UnicodePassword, + new Dictionary {{ LDAPProperties.UnicodePassword, "1234" }}, + new Dictionary {{ "unicodepassword", "1234" }}, + }, + new object[] + { + LDAPProperties.MsSFU30Password, + new Dictionary {{ LDAPProperties.MsSFU30Password, "1234" }}, + new Dictionary {{ "sfupassword", "1234" }}, + }, + new object[] + { + LDAPProperties.ScriptPath, + new Dictionary {{ LDAPProperties.ScriptPath, "/scripts" }}, + new Dictionary {{ "logonscript", "/scripts" }}, + }, + new object[] + { + LDAPProperties.AdminCount, + new Dictionary {{ LDAPProperties.AdminCount, "1" }}, + new Dictionary {{ "admincount", true }}, + }, + new object[] + { + LDAPProperties.AdminCount, + new Dictionary {{ LDAPProperties.AdminCount, "0" }}, + new Dictionary {{ "admincount", false }}, + }, + new object[] + { + LDAPProperties.AdminCount, + new Dictionary {{ LDAPProperties.AdminCount, "nope" }}, + new Dictionary {{ "admincount", false }}, + }, + new object[] + { + LDAPProperties.OperatingSystem, + new Dictionary {{ LDAPProperties.OperatingSystem, "TestOS" }, { LDAPProperties.ServicePack, "SP1" }}, + new Dictionary {{ "operatingsystem", "TestOS SP1" }}, + }, + new object[] + { + LDAPProperties.AllowedToDelegateTo, + new Dictionary {{ LDAPProperties.AllowedToDelegateTo, new[] { "test1", "test2", "test3" } }}, + new Dictionary {{ "allowedtodelegate", new[] { "test1", "test2", "test3" } }}, + }, + new object[] + { + LDAPProperties.ServicePrincipalNames, + new Dictionary {{ + LDAPProperties.ServicePrincipalNames, + new[] + { + "WSMAN/WIN10", + "WSMAN/WIN10.testlab.local", + "RestrictedKrbHost/WIN10", + "HOST/WIN10", + "RestrictedKrbHost/WIN10.testlab.local", + "HOST/WIN10.testlab.local", + } + }}, + new Dictionary {{ + "serviceprincipalnames", + new[] + { + "WSMAN/WIN10", + "WSMAN/WIN10.testlab.local", + "RestrictedKrbHost/WIN10", + "HOST/WIN10", + "RestrictedKrbHost/WIN10.testlab.local", + "HOST/WIN10.testlab.local", + }}, + { "hasspn", true } + } + }, + new object[] + { + LDAPProperties.ServicePrincipalNames, + new Dictionary {{ + LDAPProperties.ServicePrincipalNames, + new string[] {} + }}, + new Dictionary {{ + "serviceprincipalnames", + new string[] {} + }, + { "hasspn", false } + } + }, + }; + + [Theory] + [MemberData(nameof(SimplePropertyTestData))] + public void LDAPPropertyProcessor_GetProperties_SimplePropertyTest(string ldapPropertyName, Dictionary testInput, Dictionary expectedOutput) + { + var mock = new MockSearchResultEntry("CN\u003dWIN10,OU\u003dTestOU,DC\u003dtestlab,DC\u003dlocal", + testInput, + "S-1-5-21-3130019616-2776909439-2417379446-1101", Label.User); + + var resolvedProps = LDAPPropertyProcessor.GetProperties(ldapPropertyName, mock); + + Assert.Equal(resolvedProps.Count, expectedOutput.Count); + foreach (var expected in expectedOutput) + { + Assert.Single(resolvedProps.Keys, expected.Key); + Assert.Equal(expected.Value, resolvedProps[expected.Key]); + } + } } }