Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docfx/Docfx.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<IsPackable>false</IsPackable>
<BuildDocFx Condition="'$(BuildDocFx)' == '' and '$(OS)' != 'Windows_NT'">false</BuildDocFx>
<PreviewOutputFolder>$([MSBuild]::NormalizeDirectory($(MSBuildThisFileDirectory), `..`, `docs`))</PreviewOutputFolder>
<PreviewPort Condition=" '$(PreviewPort)' == '' ">8002</PreviewPort>
<LogFile>$(MSBuildThisFileDirectory)docfx.log</LogFile>
Expand Down
3 changes: 2 additions & 1 deletion src/CommonLib/Enums/DirectoryPaths.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ public static class DirectoryPaths
public const string CertTemplateLocation = "CN=Certificate Templates,CN=Public Key Services,CN=Services";
public const string NTAuthStoreLocation = "CN=NTAuthCertificates,CN=Public Key Services,CN=Services";
public const string PKILocation = "CN=Public Key Services,CN=Services";
public const string ExchangeLocation = "CN=Microsoft Exchange,CN=Services,CN=Configuration";
public const string ConfigLocation = "CN=Configuration";
public const string OIDContainerLocation = "CN=OID,CN=Public Key Services,CN=Services";
}
}
}
6 changes: 5 additions & 1 deletion src/CommonLib/ILdapUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,10 @@ IAsyncEnumerable<Result<string>> RangedRetrieval(string distinguishedName,
/// <param name="config">The new ldap config</param>
void SetLdapConfig(LdapConfig config);
/// <summary>
/// Gets whether custom deny ACE collection is disabled for this utils instance
/// </summary>
bool SkipDenyAces { get; }
/// <summary>
/// Tests if a LDAP connection can be made successfully to a domain
/// </summary>
/// <param name="domain">The domain to test</param>
Expand All @@ -175,4 +179,4 @@ IAsyncEnumerable<Result<string>> RangedRetrieval(string distinguishedName,
/// </summary>
void ResetUtils();
}
}
}
4 changes: 3 additions & 1 deletion src/CommonLib/LdapConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public class LdapConfig
public bool ForceSSL { get; set; } = false;
public bool DisableSigning { get; set; } = false;
public bool DisableCertVerification { get; set; } = false;
public bool SkipDenyAces { get; set; } = false;
public AuthType AuthType { get; set; } = AuthType.Kerberos;
public int MaxConcurrentQueries { get; set; } = 15;

Expand Down Expand Up @@ -41,6 +42,7 @@ public override string ToString() {
sb.AppendLine($"LdapPort: {GetPort(false)}");
sb.AppendLine($"LdapSSLPort: {GetPort(true)}");
sb.AppendLine($"ForceSSL: {ForceSSL}");
sb.AppendLine($"SkipDenyAces: {SkipDenyAces}");
sb.AppendLine($"AuthType: {AuthType.ToString()}");
sb.AppendLine($"MaxConcurrentQueries: {MaxConcurrentQueries}");
if (!string.IsNullOrWhiteSpace(Username)) {
Expand All @@ -53,4 +55,4 @@ public override string ToString() {
return sb.ToString();
}
}
}
}
7 changes: 5 additions & 2 deletions src/CommonLib/LdapUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -881,7 +881,8 @@ public ActiveDirectorySecurityDescriptor MakeSecurityDescriptor() {
string computerDomainSid, string computerDomain) {
if (!WellKnownPrincipal.GetWellKnownPrincipal(sid.Value, out var common)) return (false, null);
//The "Everyone" and "Authenticated Users" principals are special and will be converted to the domain equivalent
if (sid.Value is "S-1-1-0" or "S-1-5-11") {
if (sid.Value is var sidValue &&
(sidValue == WellKnownPrincipal.EveryoneSid || sidValue == "S-1-5-11")) {
return await GetWellKnownPrincipal(sid.Value, computerDomain);
}

Expand Down Expand Up @@ -1076,6 +1077,8 @@ public void SetLdapConfig(LdapConfig config) {
_connectionPool = new ConnectionPoolManager(_ldapConfig, scanner: _portScanner);
}

public bool SkipDenyAces => _ldapConfig.SkipDenyAces;

public Task<(bool Success, string Message)> TestLdapConnection(string domain) {
return _connectionPool.TestDomainConnection(domain, false);
}
Expand Down Expand Up @@ -1418,4 +1421,4 @@ private static string ComputeDisplayName(IDirectoryObject directoryObject, strin
return displayName.ToUpper();
}
}
}
}
178 changes: 178 additions & 0 deletions src/CommonLib/Processors/ACLProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Microsoft.Extensions.Logging;
using SharpHoundCommonLib.DirectoryObjects;
using SharpHoundCommonLib.Enums;
using SharpHoundCommonLib.LDAPQueries;
using SharpHoundCommonLib.OutputTypes;
using System.Linq;

Expand All @@ -20,7 +21,15 @@ public class ACLProcessor {
private readonly ILogger _log;
private readonly ILdapUtils _utils;
private readonly ConcurrentHashSet _builtDomainCaches = new(StringComparer.OrdinalIgnoreCase);
private readonly ConcurrentDictionary<string, string[]> _exchangeTrusteeSidCache = new(StringComparer.OrdinalIgnoreCase);
private readonly object _lock = new();
// These Exchange principals commonly carry product-added deny ACEs that we intentionally suppress.
private static readonly HashSet<string> ExchangeTrusteeNames = new(StringComparer.OrdinalIgnoreCase) {
"Exchange Windows Permissions",
"Exchange Trusted Subsystem",
"Exchange Servers",
"Organization Management"
};

static ACLProcessor() {
//Create a dictionary with the base GUIDs of each object type
Expand Down Expand Up @@ -881,6 +890,175 @@ or Label.NTAuthStore
}
}

public Task<string[]> GetCustomDenyAces(ResolvedSearchResult result, IDirectoryObject searchResult) {
if (!searchResult.TryGetByteProperty(LDAPProperties.SecurityDescriptor, out var descriptor)) {
return Task.FromResult(Array.Empty<string>());
}

searchResult.TryGetDistinguishedName(out var distinguishedName);
return GetCustomDenyAces(
descriptor,
result.Domain,
result.ObjectType,
distinguishedName,
searchResult.IsMSA() || searchResult.IsGMSA(),
result.DisplayName);
}

public async Task<string[]> GetCustomDenyAces(byte[] ntSecurityDescriptor, string objectDomain,
Label objectType, string distinguishedName = null, bool isMSA = false, string objectName = "") {
if (ntSecurityDescriptor == null) {
return Array.Empty<string>();
}

RawSecurityDescriptor descriptor;
try {
descriptor = new RawSecurityDescriptor(ntSecurityDescriptor, 0);
}
catch (Exception e) when (e is OverflowException or ArgumentException) {
_log.LogWarning(
"Security descriptor on object {Name} exceeds maximum allowable length. Unable to process custom deny ACEs",
objectName);
return Array.Empty<string>();
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if (descriptor.DiscretionaryAcl == null || descriptor.DiscretionaryAcl.Count == 0) {
return Array.Empty<string>();
}

var results = new List<string>();

// Walk the raw DACL so we can preserve deny ACE ordering and serialize each ACE back to SDDL verbatim.
foreach (GenericAce ace in descriptor.DiscretionaryAcl) {
if (!TryGetDenyAceData(ace, out var principalSid, out var rights, out var objectAceType)) {
continue;
}

if (await ShouldExcludeCustomDenyAce(principalSid, rights, objectAceType, objectDomain, objectType,
distinguishedName, isMSA)) {
continue;
}

var sddl = SerializeAceToSddl(ace);
if (!string.IsNullOrWhiteSpace(sddl)) {
results.Add(sddl);
}
}

return results.Count == 0 ? Array.Empty<string>() : results.ToArray();
}

public async Task AddCustomDenyAcesProperty(Dictionary<string, object> props, byte[] ntSecurityDescriptor,
string objectDomain, Label objectType, string distinguishedName = null, bool isMSA = false,
string objectName = "") {
var customDenyAces = await GetCustomDenyAces(ntSecurityDescriptor, objectDomain, objectType,
distinguishedName, isMSA, objectName);

if (customDenyAces.Length > 0) {
props["customdenyaces"] = customDenyAces;
}
}

private static bool TryGetDenyAceData(GenericAce ace, out string principalSid, out ActiveDirectoryRights rights,
out Guid objectAceType) {
principalSid = null;
rights = 0;
objectAceType = Guid.Empty;

switch (ace) {
case CommonAce commonAce when commonAce.AceQualifier == AceQualifier.AccessDenied:
principalSid = commonAce.SecurityIdentifier?.Value;
rights = (ActiveDirectoryRights)commonAce.AccessMask;
return !string.IsNullOrWhiteSpace(principalSid);
case ObjectAce objectAce when objectAce.AceQualifier == AceQualifier.AccessDenied:
principalSid = objectAce.SecurityIdentifier?.Value;
rights = (ActiveDirectoryRights)objectAce.AccessMask;
objectAceType = objectAce.ObjectAceType;
return !string.IsNullOrWhiteSpace(principalSid);
default:
return false;
}
}

private async Task<bool> ShouldExcludeCustomDenyAce(string principalSid, ActiveDirectoryRights rights,
Guid objectAceType, string objectDomain, Label objectType, string distinguishedName, bool isMSA) {
// Filter Exchange Deny ACEs
if (!string.IsNullOrWhiteSpace(distinguishedName) &&
distinguishedName.IndexOf(DirectoryPaths.ExchangeLocation, StringComparison.OrdinalIgnoreCase) >= 0) {
return true;
}

if (await IsExchangeTrustee(principalSid, objectDomain)) {
return true;
}

// Filter default Everyone Deny ACEs
if (principalSid.Equals(WellKnownPrincipal.EveryoneSid, StringComparison.OrdinalIgnoreCase)) {
if ((objectType is Label.OU or Label.Container) &&
rights.HasFlag(ActiveDirectoryRights.Delete) &&
rights.HasFlag(ActiveDirectoryRights.DeleteTree)) {
return true;
}

if (isMSA &&
rights.HasFlag(ActiveDirectoryRights.ExtendedRight) &&
objectAceType.Equals(new Guid(ACEGuids.UserForceChangePassword))) {
return true;
}

if (objectType == Label.Domain && rights.HasFlag(ActiveDirectoryRights.DeleteChild)) {
return true;
}
Comment thread
JonasBK marked this conversation as resolved.
}

return false;
}

private async Task<bool> IsExchangeTrustee(string principalSid, string objectDomain) {
if (string.IsNullOrWhiteSpace(principalSid) || string.IsNullOrWhiteSpace(objectDomain)) {
return false;
}

if (_exchangeTrusteeSidCache.TryGetValue(objectDomain, out var cachedSids)) {
return cachedSids.Contains(principalSid, StringComparer.OrdinalIgnoreCase);
}

// Well-known principals never match the Exchange groups we are suppressing.
if (WellKnownPrincipal.GetWellKnownPrincipal(principalSid, out _)) {
return false;
}

// Resolve the small fixed set of Exchange trustee names once per domain using the shared name -> ID cache path.
var resolvedSids = new List<string>();
foreach (var trusteeName in ExchangeTrusteeNames) {
if (await _utils.ResolveAccountName(trusteeName, objectDomain) is (true, var principal) &&
!string.IsNullOrWhiteSpace(principal.ObjectIdentifier)) {
resolvedSids.Add(principal.ObjectIdentifier);
}
}

var exchangeTrusteeSids = resolvedSids.Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
_exchangeTrusteeSidCache.TryAdd(objectDomain, exchangeTrusteeSids);
return exchangeTrusteeSids.Contains(principalSid, StringComparer.OrdinalIgnoreCase);
}

private static string SerializeAceToSddl(GenericAce ace) {
// Rehydrate the ACE inside a one-entry DACL and let the framework emit the canonical ACE SDDL for us.
var acl = new RawAcl(ace is ObjectAce ? GenericAcl.AclRevisionDS : GenericAcl.AclRevision, 1);
acl.InsertAce(0, CloneAce(ace));

var descriptor = new RawSecurityDescriptor(ControlFlags.DiscretionaryAclPresent, null, null, null, acl);
var sddl = descriptor.GetSddlForm(AccessControlSections.Access);

return sddl.StartsWith("D:", StringComparison.OrdinalIgnoreCase) ? sddl.Substring(2) : sddl;
}

private static GenericAce CloneAce(GenericAce ace) {
var buffer = new byte[ace.BinaryLength];
ace.GetBinaryForm(buffer, 0);
return GenericAce.CreateFromBinaryForm(buffer, 0);
}


/// <summary>
/// Helper function to use commonlib types and pass to ProcessGMSAReaders
Expand Down
Loading
Loading