Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ coverage.*
logs/
*.log

# Codex
.codex

################
# Boilerplate Settings (modify as needed)
# (https://github.com/github/gitignore)
Expand Down
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";
}
}
}
2 changes: 1 addition & 1 deletion src/CommonLib/ILdapUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -175,4 +175,4 @@ IAsyncEnumerable<Result<string>> RangedRetrieval(string distinguishedName,
/// </summary>
void ResetUtils();
}
}
}
2 changes: 1 addition & 1 deletion src/CommonLib/LdapConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,4 @@ public override string ToString() {
return sb.ToString();
}
}
}
}
4 changes: 2 additions & 2 deletions src/CommonLib/LdapUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -881,7 +881,7 @@ 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 WellKnownPrincipal.EveryoneSid or "S-1-5-11") {
return await GetWellKnownPrincipal(sid.Value, computerDomain);
}

Expand Down Expand Up @@ -1418,4 +1418,4 @@ private static string ComputeDisplayName(IDirectoryObject directoryObject, strin
return displayName.ToUpper();
}
}
}
}
175 changes: 173 additions & 2 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 @@ -48,6 +57,44 @@ public ACLProcessor(ILdapUtils utils, ILogger log = null)
_log = log ?? Logging.LogProvider.CreateLogger("ACLProc");
}

public readonly struct CustomDenyAceCounts {
public CustomDenyAceCounts(int explicitCount, int inheritedCount) {
ExplicitCount = explicitCount;
InheritedCount = inheritedCount;
}

public int ExplicitCount { get; }
public int InheritedCount { get; }
public int Total => ExplicitCount + InheritedCount;
}

public sealed class ACLProcessingResult {
public ACLProcessingResult(ACE[] aces, CustomDenyAceCounts customDenyAceCounts) {
Aces = aces;
CustomDenyAceCounts = customDenyAceCounts;
}

public ACE[] Aces { get; }
public CustomDenyAceCounts CustomDenyAceCounts { get; }
}

private sealed class CustomDenyAceAccumulator {
private int _explicitCount;
private int _inheritedCount;

public void Add(bool inherited) {
if (inherited) {
_inheritedCount++;
} else {
_explicitCount++;
}
}

public CustomDenyAceCounts ToCounts() {
return new CustomDenyAceCounts(_explicitCount, _inheritedCount);
}
}

/// Represents a lightweight Access Control Entry (ACE) used to compute hash values
/// for AdminSDHolder purposes
internal class ACEForHashing {
Expand Down Expand Up @@ -431,6 +478,24 @@ public IAsyncEnumerable<ACE> ProcessACL(ResolvedSearchResult result, IDirectoryO
return ProcessACL(descriptor, result.Domain, result.ObjectType, searchResult.HasLAPS(), checkForOwnerRights, result.DisplayName);
}

/// <summary>
/// Processes the regular ACL edges and custom deny ACE counts in one ACL traversal.
/// </summary>
/// <remarks>
/// Callers that do not want custom deny ACE counts should continue to call <see cref="ProcessACL(ResolvedSearchResult, IDirectoryObject, bool)"/>.
/// </remarks>
public Task<ACLProcessingResult> ProcessACLWithCustomDenyAces(ResolvedSearchResult result,
IDirectoryObject searchResult, bool checkForOwnerRights = true) {
if (!searchResult.TryGetByteProperty(LDAPProperties.SecurityDescriptor, out var descriptor)) {
return Task.FromResult(new ACLProcessingResult(Array.Empty<ACE>(), new CustomDenyAceCounts()));
}

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

/// <summary>
/// Read's a raw ntSecurityDescriptor and processes the ACEs in the ACL, filtering out ACEs that
/// BloodHound is not interested in as well as principals we don't care about
Expand All @@ -447,8 +512,25 @@ public IAsyncEnumerable<ACE> ProcessACL(byte[] ntSecurityDescriptor, string obje
return ProcessACL(ntSecurityDescriptor, objectDomain, objectType, hasLaps, true, objectName);
}

public async IAsyncEnumerable<ACE> ProcessACL(byte[] ntSecurityDescriptor, string objectDomain,
public IAsyncEnumerable<ACE> ProcessACL(byte[] ntSecurityDescriptor, string objectDomain,
Label objectType, bool hasLaps, bool checkForOwnerRights, string objectName) {
return ProcessACLInternal(ntSecurityDescriptor, objectDomain, objectType, hasLaps, checkForOwnerRights,
objectName);
}

public async Task<ACLProcessingResult> ProcessACLWithCustomDenyAces(byte[] ntSecurityDescriptor,
string objectDomain, Label objectType, bool hasLaps, bool checkForOwnerRights = true,
string distinguishedName = null, bool isMSA = false, string objectName = "") {
var accumulator = new CustomDenyAceAccumulator();
var aces = await ProcessACLInternal(ntSecurityDescriptor, objectDomain, objectType, hasLaps,
checkForOwnerRights, objectName, accumulator, distinguishedName, isMSA).ToArrayAsync();
return new ACLProcessingResult(aces, accumulator.ToCounts());
}

private async IAsyncEnumerable<ACE> ProcessACLInternal(byte[] ntSecurityDescriptor, string objectDomain,
Label objectType, bool hasLaps, bool checkForOwnerRights, string objectName,
CustomDenyAceAccumulator customDenyAceAccumulator = null, string distinguishedName = null,
bool isMSA = false) {
await BuildGuidCache(objectDomain);

if (ntSecurityDescriptor == null) {
Expand Down Expand Up @@ -496,7 +578,19 @@ public async IAsyncEnumerable<ACE> ProcessACL(byte[] ntSecurityDescriptor, strin
bool isPermissionForOwnerRightsSid = false;
bool isInheritedPermissionForOwnerRightsSid = false;

if (ace == null || ace.AccessControlType() == AccessControlType.Deny || !ace.IsAceInheritedFrom(BaseGuids[objectType])) {
if (ace == null) {
continue;
}

if (ace.AccessControlType() == AccessControlType.Deny) {
if (customDenyAceAccumulator != null) {
await CountCustomDenyAce(ace, customDenyAceAccumulator, objectDomain, objectType,
distinguishedName, isMSA);
}
continue;
}

if (!ace.IsAceInheritedFrom(BaseGuids[objectType])) {
continue;
}

Expand Down Expand Up @@ -902,6 +996,83 @@ or Label.NTAuthStore
}
}

Comment thread
JonasBK marked this conversation as resolved.
private async Task CountCustomDenyAce(ActiveDirectoryRuleDescriptor ace,
CustomDenyAceAccumulator accumulator, string objectDomain, Label objectType, string distinguishedName,
bool isMSA) {
var principalSid = ace.IdentityReference();
if (string.IsNullOrWhiteSpace(principalSid)) {
return;
}

if (await ShouldExcludeCustomDenyAce(principalSid, ace.ActiveDirectoryRights(), ace.ObjectType(),
objectDomain, objectType, distinguishedName, isMSA)) {
return;
}

accumulator.Add(ace.IsInherited());
}

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.Domain && rights.Equals(ActiveDirectoryRights.DeleteChild)) {
return true;
}

if ((objectType is Label.OU or Label.Container) &&
rights.Equals(ActiveDirectoryRights.Delete | ActiveDirectoryRights.DeleteTree)) {
return true;
}

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

}

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

/// <summary>
/// Helper function to use commonlib types and pass to ProcessGMSAReaders
Expand Down
4 changes: 3 additions & 1 deletion src/CommonLib/WellKnownPrincipal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ namespace SharpHoundCommonLib
{
public static class WellKnownPrincipal
{
public const string EveryoneSid = "S-1-1-0";

/// <summary>
/// Gets the principal associated with a well known SID
/// </summary>
Expand All @@ -18,7 +20,7 @@ public static bool GetWellKnownPrincipal(string sid, out TypedPrincipal commonPr
"S-1-0" => new TypedPrincipal("Null Authority", Label.User),
"S-1-0-0" => new TypedPrincipal("Nobody", Label.User),
"S-1-1" => new TypedPrincipal("World Authority", Label.User),
"S-1-1-0" => new TypedPrincipal("Everyone", Label.Group),
EveryoneSid => new TypedPrincipal("Everyone", Label.Group),
"S-1-2" => new TypedPrincipal("Local Authority", Label.User),
"S-1-2-0" => new TypedPrincipal("Local", Label.Group),
"S-1-2-1" => new TypedPrincipal("Console Logon", Label.Group),
Expand Down
Loading
Loading