Skip to content

Commit c25048c

Browse files
feat: AdminSDHolder hash functions (#230)
* feat: AdminSDHolder hash functions - Add class to represent ACE for hashing - Add method to determine if the SD hash of the provided object matches the AdminSDHolder SD hash and set 'adminsdholderprotected' if true - Add method to calculate SHA1 hash of authoratative SD - Add adminCount property to Comptuer nodes - Add mock tests Resolves BED-6221 * fix: PR comment fixes * fix: CodeRabbit comments --------- Co-authored-by: Alex Nemeth <80649445+definitelynotagoblin@users.noreply.github.com>
1 parent 1f5e0ea commit c25048c

4 files changed

Lines changed: 337 additions & 51 deletions

File tree

src/CommonLib/AdaptiveTimeout.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,7 @@ private bool EnoughSuccessesSince(DateTime startTime) {
320320
// target == 0
321321
// 1: this thread
322322
// 2: interceding thread
323-
323+
324324
1: do {
325325
1: var initialVal = target;
326326
2: target = 2;

src/CommonLib/Processors/ACLProcessor.cs

Lines changed: 206 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using SharpHoundCommonLib.DirectoryObjects;
1212
using SharpHoundCommonLib.Enums;
1313
using SharpHoundCommonLib.OutputTypes;
14+
using System.Linq;
1415

1516
namespace SharpHoundCommonLib.Processors {
1617
public class ACLProcessor {
@@ -41,11 +42,32 @@ static ACLProcessor() {
4142
};
4243
}
4344

44-
public ACLProcessor(ILdapUtils utils, ILogger log = null) {
45+
public ACLProcessor(ILdapUtils utils, ILogger log = null)
46+
{
4547
_utils = utils;
4648
_log = log ?? Logging.LogProvider.CreateLogger("ACLProc");
4749
}
4850

51+
/// Represents a lightweight Access Control Entry (ACE) used to compute hash values
52+
/// for AdminSDHolder purposes
53+
internal class ACEForHashing {
54+
public string IdentityReference { get; set; }
55+
public ActiveDirectoryRights Rights { get; set; }
56+
public AccessControlType AccessControlType { get; set; }
57+
public string ObjectType { get; set; }
58+
public string InheritedObjectType { get; set; }
59+
public InheritanceFlags InheritanceFlags { get; set; }
60+
/// <summary>
61+
/// Converts the object to its string representation, providing a meaningful representation for debugging or display purposes.
62+
/// </summary>
63+
/// <returns>
64+
/// A string that represents the current object.
65+
/// </returns>
66+
public override string ToString() {
67+
return $"{IdentityReference}|{Rights}|{AccessControlType}|{ObjectType}|{InheritedObjectType}|{InheritanceFlags}";
68+
}
69+
}
70+
4971
/// <summary>
5072
/// Builds a mapping of GUID -> Name for LDAP rights. Used for rights that are created using an extended schema such as
5173
/// LAPS
@@ -123,8 +145,58 @@ public bool IsACLProtected(byte[] ntSecurityDescriptor) {
123145
return descriptor.AreAccessRulesProtected();
124146
}
125147

148+
/// <summary>
149+
/// Helper function to use commonlib types in IsAdminSDHolderProtected
150+
/// </summary>
151+
/// <param name="entry"></param>
152+
/// <returns></returns>
153+
public bool? IsAdminSDHolderProtected(IDirectoryObject entry, string adminSdHolderHash = null) {
154+
if (entry.TryGetByteProperty(LDAPProperties.SecurityDescriptor, out var ntSecurityDescriptor)) {
155+
entry.TryGetDistinguishedName(out var objectName);
156+
return IsAdminSDHolderProtected(ntSecurityDescriptor, adminSdHolderHash, objectName);
157+
}
158+
159+
return null;
160+
}
161+
162+
/// <summary>
163+
/// Determines if the security descriptor is protected by AdminSDHolder by comparing its hash
164+
/// with the AdminSDHolder hash.
165+
/// </summary>
166+
/// <param name="ntSecurityDescriptor">The security descriptor to check</param>
167+
/// <param name="adminSdHolderHash">The AdminSDHolder hash to compare against</param>
168+
/// <param name="objectName">The name of the object being checked (for logging)</param>
169+
/// <returns>
170+
/// True if protected by AdminSDHolder, False if not protected, or null if the check couldn't be performed
171+
/// </returns>
172+
public bool? IsAdminSDHolderProtected(byte[] ntSecurityDescriptor, string adminSdHolderHash = null, string objectName = "") {
173+
bool? isAdminSdHolderProtected = null;
174+
175+
if (ntSecurityDescriptor == null || ntSecurityDescriptor.Length == 0 || string.IsNullOrEmpty(adminSdHolderHash)) {
176+
_log.LogDebug("Required input(s) missing for AdminSDHolder hash comparison for object: {Name}", objectName);
177+
return isAdminSdHolderProtected;
178+
}
179+
180+
// Calculate the implicit ACL hash for the current object
181+
string currentObjectHash = CalculateImplicitACLHash(ntSecurityDescriptor, objectName);
182+
183+
// If we got a valid hash, check if it matches this domain's AdminSDHolder hash
184+
if (!string.IsNullOrEmpty(currentObjectHash)) {
185+
_log.LogDebug("Comparing ACL hash {Hash} with AdminSDHolder hashes for {Name}",
186+
currentObjectHash, objectName);
187+
isAdminSdHolderProtected = adminSdHolderHash.Equals(currentObjectHash, StringComparison.OrdinalIgnoreCase);
188+
189+
if (isAdminSdHolderProtected == true) {
190+
_log.LogDebug("Object {Name} is protected by AdminSDHolder", objectName);
191+
}
192+
}
193+
194+
return isAdminSdHolderProtected;
195+
}
196+
126197
internal static string CalculateInheritanceHash(string identityReference, ActiveDirectoryRights rights,
127-
string aceType, string inheritedObjectType) {
198+
string aceType, string inheritedObjectType)
199+
{
128200
var hash = identityReference + rights + aceType + inheritedObjectType;
129201
/*
130202
* We're using SHA1 because its fast and this data isn't cryptographically important.
@@ -160,44 +232,165 @@ public IEnumerable<string> GetInheritedAceHashes(IDirectoryObject directoryObjec
160232
return Array.Empty<string>();
161233
}
162234

235+
/// <summary>
236+
/// Calculates a hash of all implicit (non-inherited) ACEs in the security descriptor and the ACL protection status
237+
/// </summary>
238+
/// <param name="ntSecurityDescriptor">The raw security descriptor bytes</param>
239+
/// <param name="objectName">Optional name for logging purposes</param>
240+
/// <returns>A SHA1 hash of the concatenated implicit ACEs + IsACLProtected, or empty string if error</returns>
241+
public string CalculateImplicitACLHash(byte[] ntSecurityDescriptor, string objectName = "")
242+
{
243+
if (ntSecurityDescriptor == null) {
244+
_log.LogDebug("Security Descriptor is null for {Name}", objectName);
245+
return string.Empty;
246+
}
247+
248+
_log.LogInformation("Calculating hash of implicit ACEs for {Name}", objectName);
249+
var descriptor = _utils.MakeSecurityDescriptor();
250+
251+
try
252+
{
253+
descriptor.SetSecurityDescriptorBinaryForm(ntSecurityDescriptor);
254+
}
255+
catch (OverflowException)
256+
{
257+
_log.LogWarning(
258+
"Security descriptor on object {Name} exceeds maximum allowable length. Unable to process",
259+
objectName);
260+
return string.Empty;
261+
}
262+
263+
// Check if DACL is protected
264+
bool isDaclProtected = descriptor.AreAccessRulesProtected();
265+
_log.LogInformation("DACL Protection status for {Name}: {IsProtected}", objectName, isDaclProtected);
266+
267+
// Get all ACEs, including Deny ACEs, but skip inherited ones
268+
var aceList = new List<ACEForHashing>();
269+
270+
foreach (var ace in descriptor.GetAccessRules(true, false, typeof(SecurityIdentifier))) {
271+
if (ace == null) {
272+
continue; // Skip null ACEs
273+
}
274+
275+
var ir = ace.IdentityReference();
276+
if (ir == null) {
277+
_log.LogDebug("Skipping ACE with null identity reference for {Name}", objectName);
278+
continue;
279+
}
280+
281+
// Create a simplified representation of the ACE for consistent ordering and hashing
282+
// No filtering of principals - include all principals in the hash calculation
283+
aceList.Add(new ACEForHashing
284+
{
285+
IdentityReference = ir,
286+
Rights = ace.ActiveDirectoryRights(),
287+
AccessControlType = ace.AccessControlType(),
288+
ObjectType = ace.ObjectType().ToString().ToLower(),
289+
InheritedObjectType = ace.InheritedObjectType().ToString().ToLower(),
290+
InheritanceFlags = ace.InheritanceFlags,
291+
});
292+
}
293+
// TODO: From here through the end of the method I'm not sure this is the most efficient path forward.
294+
// Using an IComparer to sort and then instead of string comparison consider serializing data to a byte array
295+
296+
// Sort the ACEs to ensure consistent ordering
297+
var sortedAces = aceList.OrderBy(a => a.AccessControlType)
298+
.ThenBy(a => a.IdentityReference)
299+
.ThenBy(a => a.Rights)
300+
.ThenBy(a => a.ObjectType)
301+
.ThenBy(a => a.InheritedObjectType)
302+
.ThenBy(a => a.InheritanceFlags)
303+
.ToList();
304+
305+
if (sortedAces.Count == 0) {
306+
_log.LogDebug("No implicit ACEs found for {Name}", objectName);
307+
return string.Empty;
308+
}
309+
310+
// Concatenate all ACE strings & DaclProtected status using pure StringBuilder for performance on large DACLs
311+
// Calculate more accurate capacity based on first ACE or use a conservative estimate
312+
var estimatedCapacity = sortedAces.Count > 0 ? sortedAces[0].ToString().Length * sortedAces.Count * 1.2 : 1024;
313+
var stringBuilder = new StringBuilder((int)estimatedCapacity);
314+
bool first = true;
315+
foreach (var ace in sortedAces)
316+
{
317+
if (!first)
318+
stringBuilder.Append(';');
319+
else
320+
first = false;
321+
stringBuilder.Append(ace.ToString());
322+
}
323+
stringBuilder.Append("|DaclProtected:");
324+
stringBuilder.Append(isDaclProtected);
325+
var concatenatedAces = stringBuilder.ToString();
326+
327+
328+
// Calculate SHA1 hash of the concatenated string
329+
try
330+
{
331+
/*
332+
* We're using SHA1 because its fast and this data isn't cryptographically important.
333+
* Additionally, the chances of a collision in our data size is miniscule and irrelevant.
334+
* We cannot use MD5 as it is not FIPS compliant and environments can enforce this setting
335+
*/
336+
using var sha1 = SHA1.Create();
337+
var bytes = sha1.ComputeHash(Encoding.UTF8.GetBytes(concatenatedAces));
338+
return BitConverter.ToString(bytes).Replace("-", string.Empty).ToUpper();
339+
}
340+
catch (Exception ex)
341+
{
342+
_log.LogWarning("Error calculating SHA1 hash for {Name}: {Error}", objectName, ex.Message);
343+
return string.Empty;
344+
}
345+
}
346+
163347
/// <summary>
164348
/// Gets the hashes for all aces that are pushing inheritance down the tree for later comparison
165349
/// </summary>
166350
/// <param name="ntSecurityDescriptor"></param>
167351
/// <param name="objectName"></param>
168352
/// <returns></returns>
169-
public IEnumerable<string> GetInheritedAceHashes(byte[] ntSecurityDescriptor, string objectName = "") {
170-
if (ntSecurityDescriptor == null) {
353+
public IEnumerable<string> GetInheritedAceHashes(byte[] ntSecurityDescriptor, string objectName = "")
354+
{
355+
if (ntSecurityDescriptor == null)
356+
{
171357
yield break;
172358
}
173-
359+
174360
_log.LogDebug("Processing Inherited ACE hashes for {Name}", objectName);
175361
var descriptor = _utils.MakeSecurityDescriptor();
176-
try {
362+
try
363+
{
177364
descriptor.SetSecurityDescriptorBinaryForm(ntSecurityDescriptor);
178-
} catch (OverflowException) {
365+
}
366+
catch (OverflowException)
367+
{
179368
_log.LogWarning(
180369
"Security descriptor on object {Name} exceeds maximum allowable length. Unable to process",
181370
objectName);
182371
yield break;
183372
}
184373

185-
foreach (var ace in descriptor.GetAccessRules(true, true, typeof(SecurityIdentifier))) {
374+
foreach (var ace in descriptor.GetAccessRules(true, true, typeof(SecurityIdentifier)))
375+
{
186376
//Skip all null/deny/inherited aces
187-
if (ace == null || ace.AccessControlType() == AccessControlType.Deny || ace.IsInherited()) {
377+
if (ace == null || ace.AccessControlType() == AccessControlType.Deny || ace.IsInherited())
378+
{
188379
continue;
189380
}
190381

191382
var ir = ace.IdentityReference();
192383
var principalSid = Helpers.PreProcessSID(ir);
193384

194385
//Skip aces for filtered principals
195-
if (principalSid == null) {
386+
if (principalSid == null)
387+
{
196388
continue;
197389
}
198390

199391
var iFlags = ace.InheritanceFlags;
200-
if (iFlags == InheritanceFlags.None) {
392+
if (iFlags == InheritanceFlags.None)
393+
{
201394
continue;
202395
}
203396

@@ -652,7 +845,7 @@ or Label.NTAuthStore
652845

653846
var cARights = (CertificationAuthorityRights)aceRights;
654847

655-
// TODO: These if statements are also present in ProcessRegistryEnrollmentPermissions. Move to shared location.
848+
// TODO: These if statements are also present in ProcessRegistryEnrollmentPermissions. Move to shared location.
656849
if ((cARights & CertificationAuthorityRights.ManageCA) != 0)
657850
yield return new ACE {
658851
PrincipalType = resolvedPrincipal.ObjectType,
@@ -740,7 +933,7 @@ public async IAsyncEnumerable<ACE> ProcessGMSAReaders(byte[] groupMSAMembership,
740933
objectName);
741934
yield break;
742935
}
743-
936+
744937
_log.LogDebug("Processing GMSA Readers for {ObjectName}", objectName);
745938
foreach (var ace in descriptor.GetAccessRules(true, true, typeof(SecurityIdentifier))) {
746939
if (ace == null || ace.AccessControlType() == AccessControlType.Deny) {

src/CommonLib/Processors/LdapPropertyProcessor.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ public async Task<Dictionary<string, object>> ReadDomainProperties(IDirectoryObj
120120
}
121121
props.Add("functionallevel", FunctionalLevelToString((int)functionalLevel));
122122

123-
if (entry.TryGetProperty(LDAPProperties.PrincipalName, out var principalname)) {
123+
if (entry.TryGetProperty(LDAPProperties.PrincipalName, out var principalname)) {
124124
if (!string.IsNullOrEmpty(principalname) && principalname.IndexOf('\\') > 0) {
125125
var netBios = principalname.Split('\\')[0];
126126
props.Add("netbios", netBios);
@@ -365,6 +365,9 @@ public async Task<ComputerProperties> ReadComputerProperties(IDirectoryObject en
365365
var encryptionTypes = ConvertEncryptionTypes(entry.GetProperty(LDAPProperties.SupportedEncryptionTypes));
366366
props.Add("supportedencryptiontypes", encryptionTypes);
367367

368+
entry.TryGetLongProperty(LDAPProperties.AdminCount, out var ac);
369+
props.Add("admincount", ac != 0);
370+
368371
var comps = new List<TypedPrincipal>();
369372
if (flags.HasFlag(UacFlags.TrustedToAuthForDelegation) &&
370373
entry.TryGetArrayProperty(LDAPProperties.AllowedToDelegateTo, out var delegates)) {
@@ -939,7 +942,7 @@ private enum IsTextUnicodeFlags {
939942
IS_TEXT_UNICODE_NOT_UNICODE_MASK = 0x0F00,
940943
IS_TEXT_UNICODE_NOT_ASCII_MASK = 0xF000
941944
}
942-
945+
943946
private async Task SendComputerStatus(CSVComputerStatus status) {
944947
if (ComputerStatusEvent is not null) await ComputerStatusEvent.Invoke(status);
945948
}

0 commit comments

Comments
 (0)