Skip to content

Commit

Permalink
Additional AD properties (#150)
Browse files Browse the repository at this point in the history
* feat: additional ldap properties
  • Loading branch information
JonasBK authored Aug 15, 2024
1 parent d8e6757 commit ccf8a60
Show file tree
Hide file tree
Showing 13 changed files with 200 additions and 14 deletions.
11 changes: 11 additions & 0 deletions src/CommonLib/Enums/KerberosEncryptionTypes.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
12 changes: 12 additions & 0 deletions src/CommonLib/Enums/LDAPProperties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";
}
}
8 changes: 5 additions & 3 deletions src/CommonLib/Enums/TrustAttributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
}
3 changes: 2 additions & 1 deletion src/CommonLib/ILdapUtils.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Security.Principal;
using System.Threading;
Expand Down Expand Up @@ -150,6 +150,7 @@ IAsyncEnumerable<Result<string>> RangedRetrieval(string distinguishedName,
Task<(bool Success, TypedPrincipal Principal)> ResolveDistinguishedName(string distinguishedName);
void AddDomainController(string domainControllerSID);
IAsyncEnumerable<OutputBase> GetWellKnownPrincipalOutput();
Task<(bool Success, string DSHeuristics)> GetDSHueristics(string domain, string dn);
/// <summary>
/// Sets the ldap config for this utils instance. Will dispose if any existing ldap connections when set
/// </summary>
Expand Down
3 changes: 3 additions & 0 deletions src/CommonLib/LdapQueries/CommonPaths.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 6 additions & 1 deletion src/CommonLib/LdapQueries/CommonProperties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
20 changes: 20 additions & 0 deletions src/CommonLib/LdapUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -895,6 +895,26 @@ public async Task<bool> 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<IDirectoryObject>.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());
}
Expand Down
2 changes: 2 additions & 0 deletions src/CommonLib/OutputTypes/DomainTrust.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
Expand Down
11 changes: 10 additions & 1 deletion src/CommonLib/Processors/DomainTrustProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,23 @@ public async IAsyncEnumerable<DomainTrust> EnumerateDomainTrusts(string domain)
continue;
}

trust.TrustAttributes = ta.ToString();
attributes = (TrustAttributes) ta;

trust.IsTransitive = !attributes.HasFlag(TrustAttributes.NonTransitive);
if (entry.TryGetProperty(LDAPProperties.CanonicalName, out var cn)) {
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;
Expand Down
120 changes: 117 additions & 3 deletions src/CommonLib/Processors/LdapPropertyProcessor.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
Expand Down Expand Up @@ -57,15 +57,39 @@ private static Dictionary<string, object> GetCommonProps(IDirectoryObject entry)
/// </summary>
/// <param name="entry"></param>
/// <returns></returns>
public static Dictionary<string, object> ReadDomainProperties(IDirectoryObject entry) {
public async Task<Dictionary<string, object>> 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;
}

Expand All @@ -84,6 +108,7 @@ public static string FunctionalLevelToString(int level) {
5 => "2012",
6 => "2012 R2",
7 => "2016",
8 => "2025",
_ => "Unknown"
};

Expand Down Expand Up @@ -161,6 +186,13 @@ public async Task<UserProperties> 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<TypedPrincipal>();
if (uacFlags.HasFlag(UacFlags.TrustedToAuthForDelegation) &&
Expand Down Expand Up @@ -212,11 +244,15 @@ public async Task<UserProperties> 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<string>();
var sidHistoryPrincipals = new List<TypedPrincipal>();
Expand Down Expand Up @@ -267,6 +303,14 @@ public async Task<ComputerProperties> 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<TypedPrincipal>();
if (flags.HasFlag(UacFlags.TrustedToAuthForDelegation) &&
Expand Down Expand Up @@ -308,6 +352,7 @@ public async Task<ComputerProperties> 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);

Expand Down Expand Up @@ -641,6 +686,75 @@ private static object BestGuessConvert(string value) {
return value;
}

private static List<string> ConvertEncryptionTypes(string encryptionTypes)
{
if (encryptionTypes == null) {
return null;
}

int encryptionTypesInt = Int32.Parse(encryptionTypes);
List<string> supportedEncryptionTypes = new List<string>();
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<string> timeComponents = new List<string>();

// 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;
}

/// <summary>
/// Converts PKIExpirationPeriod/PKIOverlappedPeriod attributes to time approximate times
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion test/unit/DomainTrustProcessorTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
5 changes: 5 additions & 0 deletions test/unit/Facades/MockLdapUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}
}
Loading

0 comments on commit ccf8a60

Please sign in to comment.