|
11 | 11 | using SharpHoundCommonLib.DirectoryObjects; |
12 | 12 | using SharpHoundCommonLib.Enums; |
13 | 13 | using SharpHoundCommonLib.OutputTypes; |
| 14 | +using System.Linq; |
14 | 15 |
|
15 | 16 | namespace SharpHoundCommonLib.Processors { |
16 | 17 | public class ACLProcessor { |
@@ -41,11 +42,32 @@ static ACLProcessor() { |
41 | 42 | }; |
42 | 43 | } |
43 | 44 |
|
44 | | - public ACLProcessor(ILdapUtils utils, ILogger log = null) { |
| 45 | + public ACLProcessor(ILdapUtils utils, ILogger log = null) |
| 46 | + { |
45 | 47 | _utils = utils; |
46 | 48 | _log = log ?? Logging.LogProvider.CreateLogger("ACLProc"); |
47 | 49 | } |
48 | 50 |
|
| 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 | + |
49 | 71 | /// <summary> |
50 | 72 | /// Builds a mapping of GUID -> Name for LDAP rights. Used for rights that are created using an extended schema such as |
51 | 73 | /// LAPS |
@@ -123,8 +145,58 @@ public bool IsACLProtected(byte[] ntSecurityDescriptor) { |
123 | 145 | return descriptor.AreAccessRulesProtected(); |
124 | 146 | } |
125 | 147 |
|
| 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 | + |
126 | 197 | internal static string CalculateInheritanceHash(string identityReference, ActiveDirectoryRights rights, |
127 | | - string aceType, string inheritedObjectType) { |
| 198 | + string aceType, string inheritedObjectType) |
| 199 | + { |
128 | 200 | var hash = identityReference + rights + aceType + inheritedObjectType; |
129 | 201 | /* |
130 | 202 | * We're using SHA1 because its fast and this data isn't cryptographically important. |
@@ -160,44 +232,165 @@ public IEnumerable<string> GetInheritedAceHashes(IDirectoryObject directoryObjec |
160 | 232 | return Array.Empty<string>(); |
161 | 233 | } |
162 | 234 |
|
| 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 | + |
163 | 347 | /// <summary> |
164 | 348 | /// Gets the hashes for all aces that are pushing inheritance down the tree for later comparison |
165 | 349 | /// </summary> |
166 | 350 | /// <param name="ntSecurityDescriptor"></param> |
167 | 351 | /// <param name="objectName"></param> |
168 | 352 | /// <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 | + { |
171 | 357 | yield break; |
172 | 358 | } |
173 | | - |
| 359 | + |
174 | 360 | _log.LogDebug("Processing Inherited ACE hashes for {Name}", objectName); |
175 | 361 | var descriptor = _utils.MakeSecurityDescriptor(); |
176 | | - try { |
| 362 | + try |
| 363 | + { |
177 | 364 | descriptor.SetSecurityDescriptorBinaryForm(ntSecurityDescriptor); |
178 | | - } catch (OverflowException) { |
| 365 | + } |
| 366 | + catch (OverflowException) |
| 367 | + { |
179 | 368 | _log.LogWarning( |
180 | 369 | "Security descriptor on object {Name} exceeds maximum allowable length. Unable to process", |
181 | 370 | objectName); |
182 | 371 | yield break; |
183 | 372 | } |
184 | 373 |
|
185 | | - foreach (var ace in descriptor.GetAccessRules(true, true, typeof(SecurityIdentifier))) { |
| 374 | + foreach (var ace in descriptor.GetAccessRules(true, true, typeof(SecurityIdentifier))) |
| 375 | + { |
186 | 376 | //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 | + { |
188 | 379 | continue; |
189 | 380 | } |
190 | 381 |
|
191 | 382 | var ir = ace.IdentityReference(); |
192 | 383 | var principalSid = Helpers.PreProcessSID(ir); |
193 | 384 |
|
194 | 385 | //Skip aces for filtered principals |
195 | | - if (principalSid == null) { |
| 386 | + if (principalSid == null) |
| 387 | + { |
196 | 388 | continue; |
197 | 389 | } |
198 | 390 |
|
199 | 391 | var iFlags = ace.InheritanceFlags; |
200 | | - if (iFlags == InheritanceFlags.None) { |
| 392 | + if (iFlags == InheritanceFlags.None) |
| 393 | + { |
201 | 394 | continue; |
202 | 395 | } |
203 | 396 |
|
@@ -652,7 +845,7 @@ or Label.NTAuthStore |
652 | 845 |
|
653 | 846 | var cARights = (CertificationAuthorityRights)aceRights; |
654 | 847 |
|
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. |
656 | 849 | if ((cARights & CertificationAuthorityRights.ManageCA) != 0) |
657 | 850 | yield return new ACE { |
658 | 851 | PrincipalType = resolvedPrincipal.ObjectType, |
@@ -740,7 +933,7 @@ public async IAsyncEnumerable<ACE> ProcessGMSAReaders(byte[] groupMSAMembership, |
740 | 933 | objectName); |
741 | 934 | yield break; |
742 | 935 | } |
743 | | - |
| 936 | + |
744 | 937 | _log.LogDebug("Processing GMSA Readers for {ObjectName}", objectName); |
745 | 938 | foreach (var ace in descriptor.GetAccessRules(true, true, typeof(SecurityIdentifier))) { |
746 | 939 | if (ace == null || ace.AccessControlType() == AccessControlType.Deny) { |
|
0 commit comments