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
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 count collection is disabled for this utils instance
/// </summary>
bool SkipDenyAcesCount { 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 SkipDenyAcesCount { 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($"SkipDenyAcesCount: {SkipDenyAcesCount}");
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 SkipDenyAcesCount => _ldapConfig.SkipDenyAcesCount;

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();
}
}
}
}
177 changes: 177 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,17 @@ 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();
private const string CustomExplicitDenyAcesCountProperty = "customexplicitdenyacescount";
private const string CustomInheritedDenyAcesCountProperty = "custominheriteddenyacescount";
// 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 +59,17 @@ 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;
}

/// Represents a lightweight Access Control Entry (ACE) used to compute hash values
/// for AdminSDHolder purposes
internal class ACEForHashing {
Expand Down Expand Up @@ -881,6 +903,161 @@ or Label.NTAuthStore
}
}

public Task<CustomDenyAceCounts> GetCustomDenyAceCounts(ResolvedSearchResult result,
IDirectoryObject searchResult) {
if (!searchResult.TryGetByteProperty(LDAPProperties.SecurityDescriptor, out var descriptor)) {
return Task.FromResult(new CustomDenyAceCounts());
}

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

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

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 new CustomDenyAceCounts();
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if (descriptor.DiscretionaryAcl == null || descriptor.DiscretionaryAcl.Count == 0) {
return new CustomDenyAceCounts();
}

var explicitCount = 0;
var inheritedCount = 0;

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

if ((ace.AceFlags & AceFlags.Inherited) == AceFlags.Inherited) {
inheritedCount++;
} else {
explicitCount++;
}
}

return new CustomDenyAceCounts(explicitCount, inheritedCount);
}

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

if (counts.Total > 0) {
props[CustomExplicitDenyAcesCountProperty] = counts.ExplicitCount;
props[CustomInheritedDenyAcesCountProperty] = counts.InheritedCount;
}
}

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

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