| | 1 | | using System; |
| | 2 | | using System.Collections.Concurrent; |
| | 3 | | using System.Collections.Generic; |
| | 4 | | using System.DirectoryServices; |
| | 5 | | using System.Security.AccessControl; |
| | 6 | | using System.Security.Cryptography; |
| | 7 | | using System.Security.Principal; |
| | 8 | | using System.Text; |
| | 9 | | using System.Threading.Tasks; |
| | 10 | | using Microsoft.Extensions.Logging; |
| | 11 | | using SharpHoundCommonLib.DirectoryObjects; |
| | 12 | | using SharpHoundCommonLib.Enums; |
| | 13 | | using SharpHoundCommonLib.OutputTypes; |
| | 14 | |
|
| | 15 | | namespace SharpHoundCommonLib.Processors { |
| | 16 | | public class ACLProcessor { |
| | 17 | | private static readonly Dictionary<Label, string> BaseGuids; |
| 79 | 18 | | private readonly ConcurrentDictionary<string, string> _guidMap = new(); |
| | 19 | | private readonly ILogger _log; |
| | 20 | | private readonly ILdapUtils _utils; |
| 79 | 21 | | private readonly ConcurrentHashSet _builtDomainCaches = new(StringComparer.OrdinalIgnoreCase); |
| | 22 | |
|
| 1 | 23 | | static ACLProcessor() { |
| | 24 | | //Create a dictionary with the base GUIDs of each object type |
| 1 | 25 | | BaseGuids = new Dictionary<Label, string> { |
| 1 | 26 | | { Label.User, "bf967aba-0de6-11d0-a285-00aa003049e2" }, |
| 1 | 27 | | { Label.Computer, "bf967a86-0de6-11d0-a285-00aa003049e2" }, |
| 1 | 28 | | { Label.Group, "bf967a9c-0de6-11d0-a285-00aa003049e2" }, |
| 1 | 29 | | { Label.Domain, "19195a5a-6da0-11d0-afd3-00c04fd930c9" }, |
| 1 | 30 | | { Label.GPO, "f30e3bc2-9ff0-11d1-b603-0000f80367c1" }, |
| 1 | 31 | | { Label.OU, "bf967aa5-0de6-11d0-a285-00aa003049e2" }, |
| 1 | 32 | | { Label.Container, "bf967a8b-0de6-11d0-a285-00aa003049e2" }, |
| 1 | 33 | | { Label.Configuration, "bf967a87-0de6-11d0-a285-00aa003049e2" }, |
| 1 | 34 | | { Label.RootCA, "3fdfee50-47f4-11d1-a9c3-0000f80367c1" }, |
| 1 | 35 | | { Label.AIACA, "3fdfee50-47f4-11d1-a9c3-0000f80367c1" }, |
| 1 | 36 | | { Label.EnterpriseCA, "ee4aa692-3bba-11d2-90cc-00c04fd91ab1" }, |
| 1 | 37 | | { Label.NTAuthStore, "3fdfee50-47f4-11d1-a9c3-0000f80367c1" }, |
| 1 | 38 | | { Label.CertTemplate, "e5209ca2-3bba-11d2-90cc-00c04fd91ab1" }, |
| 1 | 39 | | { Label.IssuancePolicy, "37cfd85c-6719-4ad8-8f9e-8678ba627563" } |
| 1 | 40 | | }; |
| 1 | 41 | | } |
| | 42 | |
|
| 158 | 43 | | public ACLProcessor(ILdapUtils utils, ILogger log = null) { |
| 79 | 44 | | _utils = utils; |
| 79 | 45 | | _log = log ?? Logging.LogProvider.CreateLogger("ACLProc"); |
| 79 | 46 | | } |
| | 47 | |
|
| | 48 | | /// <summary> |
| | 49 | | /// Builds a mapping of GUID -> Name for LDAP rights. Used for rights that are created using an extended sch |
| | 50 | | /// LAPS |
| | 51 | | /// </summary> |
| 27 | 52 | | private async Task BuildGuidCache(string domain) { |
| 27 | 53 | | _log.LogInformation("Building GUID Cache for {Domain}", domain); |
| 99 | 54 | | await foreach (var result in _utils.PagedQuery(new LdapQueryParameters { |
| 27 | 55 | | DomainName = domain, |
| 27 | 56 | | LDAPFilter = "(schemaIDGUID=*)", |
| 27 | 57 | | NamingContext = NamingContext.Schema, |
| 27 | 58 | | Attributes = new[] { LDAPProperties.SchemaIDGUID, LDAPProperties.Name }, |
| 36 | 59 | | })) { |
| 10 | 60 | | if (result.IsSuccess) { |
| 1 | 61 | | if (!result.Value.TryGetProperty(LDAPProperties.Name, out var name) || |
| 1 | 62 | | !result.Value.TryGetByteProperty(LDAPProperties.SchemaIDGUID, out var schemaGuid)) { |
| 0 | 63 | | continue; |
| | 64 | | } |
| | 65 | |
|
| 1 | 66 | | name = name.ToLower(); |
| | 67 | |
|
| | 68 | | string guid; |
| | 69 | | try |
| 1 | 70 | | { |
| 1 | 71 | | guid = new Guid(schemaGuid).ToString(); |
| 1 | 72 | | } |
| 0 | 73 | | catch |
| 0 | 74 | | { |
| 0 | 75 | | continue; |
| | 76 | | } |
| | 77 | |
|
| 2 | 78 | | if (name is LDAPProperties.LAPSPlaintextPassword or LDAPProperties.LAPSEncryptedPassword or LDAPProp |
| 1 | 79 | | _log.LogInformation("Found GUID for ACL Right {Name}: {Guid} in domain {Domain}", name, guid, do |
| 1 | 80 | | _guidMap.TryAdd(guid, name); |
| 1 | 81 | | } |
| 9 | 82 | | } else { |
| 8 | 83 | | _log.LogDebug("Error while building GUID cache for {Domain}: {Message}", domain, result.Error); |
| 8 | 84 | | } |
| 9 | 85 | | } |
| 27 | 86 | | } |
| | 87 | |
|
| | 88 | | /// <summary> |
| | 89 | | /// Helper function to use commonlib types in IsACLProtected |
| | 90 | | /// </summary> |
| | 91 | | /// <param name="entry"></param> |
| | 92 | | /// <returns></returns> |
| 0 | 93 | | public bool IsACLProtected(IDirectoryObject entry) { |
| 0 | 94 | | if (entry.TryGetByteProperty(LDAPProperties.SecurityDescriptor, out var ntSecurityDescriptor)) { |
| 0 | 95 | | return IsACLProtected(ntSecurityDescriptor); |
| | 96 | | } |
| | 97 | |
|
| 0 | 98 | | return false; |
| 0 | 99 | | } |
| | 100 | |
|
| | 101 | | /// <summary> |
| | 102 | | /// Gets the protection state of the access control list |
| | 103 | | /// </summary> |
| | 104 | | /// <param name="ntSecurityDescriptor"></param> |
| | 105 | | /// <returns></returns> |
| 5 | 106 | | public bool IsACLProtected(byte[] ntSecurityDescriptor) { |
| 5 | 107 | | if (ntSecurityDescriptor == null) |
| 1 | 108 | | return false; |
| | 109 | |
|
| 4 | 110 | | var descriptor = _utils.MakeSecurityDescriptor(); |
| 4 | 111 | | descriptor.SetSecurityDescriptorBinaryForm(ntSecurityDescriptor); |
| | 112 | |
|
| 4 | 113 | | return descriptor.AreAccessRulesProtected(); |
| 5 | 114 | | } |
| | 115 | |
|
| | 116 | | /// <summary> |
| | 117 | | /// Helper function to use common lib types and pass appropriate vars to ProcessACL |
| | 118 | | /// </summary> |
| | 119 | | /// <param name="result"></param> |
| | 120 | | /// <param name="searchResult"></param> |
| | 121 | | /// <returns></returns> |
| 0 | 122 | | public IAsyncEnumerable<ACE> ProcessACL(ResolvedSearchResult result, IDirectoryObject searchResult) { |
| 0 | 123 | | if (!searchResult.TryGetByteProperty(LDAPProperties.SecurityDescriptor, out var descriptor)) { |
| 0 | 124 | | return AsyncEnumerable.Empty<ACE>(); |
| | 125 | | } |
| | 126 | |
|
| 0 | 127 | | var domain = result.Domain; |
| 0 | 128 | | var type = result.ObjectType; |
| 0 | 129 | | var hasLaps = searchResult.HasLAPS(); |
| 0 | 130 | | var name = result.DisplayName; |
| | 131 | |
|
| 0 | 132 | | return ProcessACL(descriptor, domain, type, hasLaps, name); |
| 0 | 133 | | } |
| | 134 | |
|
| | 135 | | internal static string CalculateInheritanceHash(string identityReference, ActiveDirectoryRights rights, |
| 8 | 136 | | string aceType, string inheritedObjectType) { |
| 8 | 137 | | var hash = identityReference + rights + aceType + inheritedObjectType; |
| | 138 | | /* |
| | 139 | | * We're using SHA1 because its fast and this data isn't cryptographically important. |
| | 140 | | * Additionally, the chances of a collision in our data size is miniscule and irrelevant. |
| | 141 | | * We cannot use MD5 as it is not FIPS compliant and environments can enforce this setting |
| | 142 | | */ |
| | 143 | | try |
| 8 | 144 | | { |
| 8 | 145 | | using (var sha1 = SHA1.Create()) |
| 8 | 146 | | { |
| 8 | 147 | | var bytes = sha1.ComputeHash(Encoding.UTF8.GetBytes(hash)); |
| 8 | 148 | | return BitConverter.ToString(bytes).Replace("-", string.Empty).ToUpper(); |
| | 149 | | } |
| | 150 | | } |
| 0 | 151 | | catch |
| 0 | 152 | | { |
| 0 | 153 | | return ""; |
| | 154 | | } |
| 8 | 155 | | } |
| | 156 | |
|
| | 157 | | /// <summary> |
| | 158 | | /// Helper function to get inherited ACE hashes using CommonLib types |
| | 159 | | /// </summary> |
| | 160 | | /// <param name="directoryObject"></param> |
| | 161 | | /// <param name="resolvedSearchResult"></param> |
| | 162 | | /// <returns></returns> |
| | 163 | | public IEnumerable<string> GetInheritedAceHashes(IDirectoryObject directoryObject, |
| 0 | 164 | | ResolvedSearchResult resolvedSearchResult) { |
| 0 | 165 | | if (directoryObject.TryGetByteProperty(LDAPProperties.SecurityDescriptor, out var value)) { |
| 0 | 166 | | return GetInheritedAceHashes(value, resolvedSearchResult.DisplayName); |
| | 167 | | } |
| | 168 | |
|
| 0 | 169 | | return Array.Empty<string>(); |
| 0 | 170 | | } |
| | 171 | |
|
| | 172 | | /// <summary> |
| | 173 | | /// Gets the hashes for all aces that are pushing inheritance down the tree for later comparison |
| | 174 | | /// </summary> |
| | 175 | | /// <param name="ntSecurityDescriptor"></param> |
| | 176 | | /// <param name="objectName"></param> |
| | 177 | | /// <returns></returns> |
| 2 | 178 | | public IEnumerable<string> GetInheritedAceHashes(byte[] ntSecurityDescriptor, string objectName = "") { |
| 3 | 179 | | if (ntSecurityDescriptor == null) { |
| 1 | 180 | | yield break; |
| | 181 | | } |
| | 182 | |
|
| 1 | 183 | | _log.LogDebug("Processing Inherited ACE hashes for {Name}", objectName); |
| 1 | 184 | | var descriptor = _utils.MakeSecurityDescriptor(); |
| 1 | 185 | | try { |
| 1 | 186 | | descriptor.SetSecurityDescriptorBinaryForm(ntSecurityDescriptor); |
| 1 | 187 | | } catch (OverflowException) { |
| 0 | 188 | | _log.LogWarning( |
| 0 | 189 | | "Security descriptor on object {Name} exceeds maximum allowable length. Unable to process", |
| 0 | 190 | | objectName); |
| 0 | 191 | | yield break; |
| | 192 | | } |
| | 193 | |
|
| 9 | 194 | | foreach (var ace in descriptor.GetAccessRules(true, true, typeof(SecurityIdentifier))) { |
| | 195 | | //Skip all null/deny/inherited aces |
| 3 | 196 | | if (ace == null || ace.AccessControlType() == AccessControlType.Deny || ace.IsInherited()) { |
| 1 | 197 | | continue; |
| | 198 | | } |
| | 199 | |
|
| 1 | 200 | | var ir = ace.IdentityReference(); |
| 1 | 201 | | var principalSid = Helpers.PreProcessSID(ir); |
| | 202 | |
|
| | 203 | | //Skip aces for filtered principals |
| 1 | 204 | | if (principalSid == null) { |
| 0 | 205 | | continue; |
| | 206 | | } |
| | 207 | |
|
| 1 | 208 | | var iFlags = ace.InheritanceFlags; |
| 1 | 209 | | if (iFlags == InheritanceFlags.None) { |
| 0 | 210 | | continue; |
| | 211 | | } |
| | 212 | |
|
| 1 | 213 | | var aceRights = ace.ActiveDirectoryRights(); |
| | 214 | | //Lowercase this just in case. As far as I know it should always come back that way anyways, but better |
| 1 | 215 | | var aceType = ace.ObjectType().ToString().ToLower(); |
| 1 | 216 | | var inheritanceType = ace.InheritedObjectType(); |
| | 217 | |
|
| 1 | 218 | | var hash = CalculateInheritanceHash(ir, aceRights, aceType, inheritanceType); |
| 1 | 219 | | if (!string.IsNullOrEmpty(hash)) |
| 1 | 220 | | { |
| 1 | 221 | | yield return hash; |
| 1 | 222 | | } |
| 1 | 223 | | } |
| 1 | 224 | | } |
| | 225 | |
|
| | 226 | | /// <summary> |
| | 227 | | /// Read's a raw ntSecurityDescriptor and processes the ACEs in the ACL, filtering out ACEs that |
| | 228 | | /// BloodHound is not interested in as well as principals we don't care about |
| | 229 | | /// </summary> |
| | 230 | | /// <param name="ntSecurityDescriptor"></param> |
| | 231 | | /// <param name="objectDomain"></param> |
| | 232 | | /// <param name="objectName"></param> |
| | 233 | | /// <param name="objectType"></param> |
| | 234 | | /// <param name="hasLaps"></param> |
| | 235 | | /// <returns></returns> |
| | 236 | | public async IAsyncEnumerable<ACE> ProcessACL(byte[] ntSecurityDescriptor, string objectDomain, |
| | 237 | | Label objectType, |
| 27 | 238 | | bool hasLaps, string objectName = "") { |
| 54 | 239 | | if (!_builtDomainCaches.Contains(objectDomain)) { |
| 27 | 240 | | _builtDomainCaches.Add(objectDomain); |
| 27 | 241 | | await BuildGuidCache(objectDomain); |
| 27 | 242 | | } |
| | 243 | |
|
| 28 | 244 | | if (ntSecurityDescriptor == null) { |
| 1 | 245 | | _log.LogDebug("Security Descriptor is null for {Name}", objectName); |
| 1 | 246 | | yield break; |
| | 247 | | } |
| | 248 | |
|
| 26 | 249 | | var descriptor = _utils.MakeSecurityDescriptor(); |
| 26 | 250 | | try { |
| 26 | 251 | | descriptor.SetSecurityDescriptorBinaryForm(ntSecurityDescriptor); |
| 26 | 252 | | } catch (OverflowException) { |
| 0 | 253 | | _log.LogWarning( |
| 0 | 254 | | "Security descriptor on object {Name} exceeds maximum allowable length. Unable to process", |
| 0 | 255 | | objectName); |
| 0 | 256 | | yield break; |
| | 257 | | } |
| | 258 | |
|
| 26 | 259 | | _log.LogDebug("Processing ACL for {ObjectName}", objectName); |
| 26 | 260 | | var ownerSid = Helpers.PreProcessSID(descriptor.GetOwner(typeof(SecurityIdentifier))); |
| | 261 | |
|
| 28 | 262 | | if (ownerSid != null) { |
| 4 | 263 | | if (await _utils.ResolveIDAndType(ownerSid, objectDomain) is (true, var resolvedOwner)) { |
| 2 | 264 | | yield return new ACE { |
| 2 | 265 | | PrincipalType = resolvedOwner.ObjectType, |
| 2 | 266 | | PrincipalSID = resolvedOwner.ObjectIdentifier, |
| 2 | 267 | | RightName = EdgeNames.Owns, |
| 2 | 268 | | IsInherited = false, |
| 2 | 269 | | InheritanceHash = "" |
| 2 | 270 | | }; |
| 2 | 271 | | } else { |
| 0 | 272 | | _log.LogTrace("Failed to resolve owner for {Name}", objectName); |
| 0 | 273 | | yield return new ACE { |
| 0 | 274 | | PrincipalType = Label.Base, |
| 0 | 275 | | PrincipalSID = ownerSid, |
| 0 | 276 | | RightName = EdgeNames.Owns, |
| 0 | 277 | | IsInherited = false, |
| 0 | 278 | | InheritanceHash = "" |
| 0 | 279 | | }; |
| 0 | 280 | | } |
| 2 | 281 | | } |
| | 282 | |
|
| 210 | 283 | | foreach (var ace in descriptor.GetAccessRules(true, true, typeof(SecurityIdentifier))) { |
| 52 | 284 | | if (ace == null || ace.AccessControlType() == AccessControlType.Deny || !ace.IsAceInheritedFrom(BaseGuid |
| 8 | 285 | | continue; |
| | 286 | | } |
| | 287 | |
|
| 36 | 288 | | var ir = ace.IdentityReference(); |
| 36 | 289 | | var principalSid = Helpers.PreProcessSID(ir); |
| | 290 | |
|
| | 291 | | //Preprocess returns null if this is an ignored sid |
| 41 | 292 | | if (principalSid == null) { |
| 5 | 293 | | continue; |
| | 294 | | } |
| | 295 | |
|
| 31 | 296 | | var (success, resolvedPrincipal) = await _utils.ResolveIDAndType(principalSid, objectDomain); |
| 31 | 297 | | if (!success) { |
| 0 | 298 | | _log.LogTrace("Failed to resolve type for principal {Sid} on ACE for {Object}", principalSid, object |
| 0 | 299 | | resolvedPrincipal.ObjectIdentifier = principalSid; |
| 0 | 300 | | resolvedPrincipal.ObjectType = Label.Base; |
| 0 | 301 | | } |
| | 302 | |
|
| 31 | 303 | | var aceRights = ace.ActiveDirectoryRights(); |
| | 304 | | //Lowercase this just in case. As far as I know it should always come back that way anyways, but better |
| 31 | 305 | | var aceType = ace.ObjectType().ToString().ToLower(); |
| 31 | 306 | | var inherited = ace.IsInherited(); |
| | 307 | |
|
| 31 | 308 | | var aceInheritanceHash = ""; |
| 36 | 309 | | if (inherited) { |
| 5 | 310 | | aceInheritanceHash = CalculateInheritanceHash(ir, aceRights, aceType, ace.InheritedObjectType()); |
| 5 | 311 | | } |
| | 312 | |
|
| 31 | 313 | | _log.LogTrace("Processing ACE with rights {Rights} and guid {GUID} on object {Name}", aceRights, |
| 31 | 314 | | aceType, objectName); |
| | 315 | |
|
| | 316 | | //GenericAll applies to every object |
| 36 | 317 | | if (aceRights.HasFlag(ActiveDirectoryRights.GenericAll)) { |
| 5 | 318 | | if (aceType is ACEGuids.AllGuid or "") |
| 4 | 319 | | yield return new ACE { |
| 4 | 320 | | PrincipalType = resolvedPrincipal.ObjectType, |
| 4 | 321 | | PrincipalSID = resolvedPrincipal.ObjectIdentifier, |
| 4 | 322 | | IsInherited = inherited, |
| 4 | 323 | | RightName = EdgeNames.GenericAll, |
| 4 | 324 | | InheritanceHash = aceInheritanceHash |
| 4 | 325 | | }; |
| | 326 | | //This is a special case. If we don't continue here, every other ACE will match because GenericAll i |
| 5 | 327 | | continue; |
| | 328 | | } |
| | 329 | |
|
| | 330 | | //WriteDACL and WriteOwner are always useful no matter what the object type is as well because they enab |
| 26 | 331 | | if (aceRights.HasFlag(ActiveDirectoryRights.WriteDacl)) |
| 2 | 332 | | yield return new ACE { |
| 2 | 333 | | PrincipalType = resolvedPrincipal.ObjectType, |
| 2 | 334 | | PrincipalSID = resolvedPrincipal.ObjectIdentifier, |
| 2 | 335 | | IsInherited = inherited, |
| 2 | 336 | | RightName = EdgeNames.WriteDacl, |
| 2 | 337 | | InheritanceHash = aceInheritanceHash |
| 2 | 338 | | }; |
| | 339 | |
|
| 26 | 340 | | if (aceRights.HasFlag(ActiveDirectoryRights.WriteOwner)) |
| 2 | 341 | | yield return new ACE { |
| 2 | 342 | | PrincipalType = resolvedPrincipal.ObjectType, |
| 2 | 343 | | PrincipalSID = resolvedPrincipal.ObjectIdentifier, |
| 2 | 344 | | IsInherited = inherited, |
| 2 | 345 | | RightName = EdgeNames.WriteOwner, |
| 2 | 346 | | InheritanceHash = aceInheritanceHash |
| 2 | 347 | | }; |
| | 348 | |
|
| | 349 | | //Cool ACE courtesy of @rookuu. Allows a principal to add itself to a group and no one else |
| 26 | 350 | | if (aceRights.HasFlag(ActiveDirectoryRights.Self) && |
| 26 | 351 | | !aceRights.HasFlag(ActiveDirectoryRights.WriteProperty) && |
| 26 | 352 | | !aceRights.HasFlag(ActiveDirectoryRights.GenericWrite) && objectType == Label.Group && |
| 26 | 353 | | aceType == ACEGuids.WriteMember) |
| 2 | 354 | | yield return new ACE { |
| 2 | 355 | | PrincipalType = resolvedPrincipal.ObjectType, |
| 2 | 356 | | PrincipalSID = resolvedPrincipal.ObjectIdentifier, |
| 2 | 357 | | IsInherited = inherited, |
| 2 | 358 | | RightName = EdgeNames.AddSelf, |
| 2 | 359 | | InheritanceHash = aceInheritanceHash |
| 2 | 360 | | }; |
| | 361 | |
|
| | 362 | | //Process object type specific ACEs. Extended rights apply to users, domains, computers, and cert templa |
| 38 | 363 | | if (aceRights.HasFlag(ActiveDirectoryRights.ExtendedRight)) { |
| 16 | 364 | | if (objectType == Label.Domain) { |
| 4 | 365 | | if (aceType == ACEGuids.DSReplicationGetChanges) |
| 1 | 366 | | yield return new ACE { |
| 1 | 367 | | PrincipalType = resolvedPrincipal.ObjectType, |
| 1 | 368 | | PrincipalSID = resolvedPrincipal.ObjectIdentifier, |
| 1 | 369 | | IsInherited = inherited, |
| 1 | 370 | | RightName = EdgeNames.GetChanges, |
| 1 | 371 | | InheritanceHash = aceInheritanceHash |
| 1 | 372 | | }; |
| 3 | 373 | | else if (aceType == ACEGuids.DSReplicationGetChangesAll) |
| 1 | 374 | | yield return new ACE { |
| 1 | 375 | | PrincipalType = resolvedPrincipal.ObjectType, |
| 1 | 376 | | PrincipalSID = resolvedPrincipal.ObjectIdentifier, |
| 1 | 377 | | IsInherited = inherited, |
| 1 | 378 | | RightName = EdgeNames.GetChangesAll, |
| 1 | 379 | | InheritanceHash = aceInheritanceHash |
| 1 | 380 | | }; |
| 2 | 381 | | else if (aceType == ACEGuids.DSReplicationGetChangesInFilteredSet) |
| 0 | 382 | | yield return new ACE { |
| 0 | 383 | | PrincipalType = resolvedPrincipal.ObjectType, |
| 0 | 384 | | PrincipalSID = resolvedPrincipal.ObjectIdentifier, |
| 0 | 385 | | IsInherited = inherited, |
| 0 | 386 | | RightName = EdgeNames.GetChangesInFilteredSet, |
| 0 | 387 | | InheritanceHash = aceInheritanceHash |
| 0 | 388 | | }; |
| 2 | 389 | | else if (aceType is ACEGuids.AllGuid or "") |
| 1 | 390 | | yield return new ACE { |
| 1 | 391 | | PrincipalType = resolvedPrincipal.ObjectType, |
| 1 | 392 | | PrincipalSID = resolvedPrincipal.ObjectIdentifier, |
| 1 | 393 | | IsInherited = inherited, |
| 1 | 394 | | RightName = EdgeNames.AllExtendedRights, |
| 1 | 395 | | InheritanceHash = aceInheritanceHash |
| 1 | 396 | | }; |
| 15 | 397 | | } else if (objectType == Label.User) { |
| 3 | 398 | | if (aceType == ACEGuids.UserForceChangePassword) |
| 1 | 399 | | yield return new ACE { |
| 1 | 400 | | PrincipalType = resolvedPrincipal.ObjectType, |
| 1 | 401 | | PrincipalSID = resolvedPrincipal.ObjectIdentifier, |
| 1 | 402 | | IsInherited = inherited, |
| 1 | 403 | | RightName = EdgeNames.ForceChangePassword, |
| 1 | 404 | | InheritanceHash = aceInheritanceHash |
| 1 | 405 | | }; |
| 2 | 406 | | else if (aceType is ACEGuids.AllGuid or "") |
| 1 | 407 | | yield return new ACE { |
| 1 | 408 | | PrincipalType = resolvedPrincipal.ObjectType, |
| 1 | 409 | | PrincipalSID = resolvedPrincipal.ObjectIdentifier, |
| 1 | 410 | | IsInherited = inherited, |
| 1 | 411 | | RightName = EdgeNames.AllExtendedRights, |
| 1 | 412 | | InheritanceHash = aceInheritanceHash |
| 1 | 413 | | }; |
| 11 | 414 | | } else if (objectType == Label.Computer) { |
| | 415 | | //ReadLAPSPassword is only applicable if the computer actually has LAPS. Check the world readabl |
| 5 | 416 | | if (hasLaps) { |
| 2 | 417 | | if (aceType is ACEGuids.AllGuid or "") |
| 1 | 418 | | yield return new ACE { |
| 1 | 419 | | PrincipalType = resolvedPrincipal.ObjectType, |
| 1 | 420 | | PrincipalSID = resolvedPrincipal.ObjectIdentifier, |
| 1 | 421 | | IsInherited = inherited, |
| 1 | 422 | | RightName = EdgeNames.AllExtendedRights, |
| 1 | 423 | | InheritanceHash = aceInheritanceHash |
| 1 | 424 | | }; |
| 1 | 425 | | else if (_guidMap.TryGetValue(aceType, out var lapsAttribute)) |
| 1 | 426 | | { |
| | 427 | | // Compare the retrieved attribute name against LDAPProperties values |
| 1 | 428 | | if (lapsAttribute == LDAPProperties.LegacyLAPSPassword || |
| 1 | 429 | | lapsAttribute == LDAPProperties.LAPSPlaintextPassword || |
| 1 | 430 | | lapsAttribute == LDAPProperties.LAPSEncryptedPassword) |
| 1 | 431 | | { |
| 1 | 432 | | yield return new ACE |
| 1 | 433 | | { |
| 1 | 434 | | PrincipalType = resolvedPrincipal.ObjectType, |
| 1 | 435 | | PrincipalSID = resolvedPrincipal.ObjectIdentifier, |
| 1 | 436 | | IsInherited = inherited, |
| 1 | 437 | | RightName = EdgeNames.ReadLAPSPassword, |
| 1 | 438 | | InheritanceHash = aceInheritanceHash |
| 1 | 439 | | }; |
| 1 | 440 | | } |
| 1 | 441 | | } |
| 2 | 442 | | } |
| 5 | 443 | | } else if (objectType == Label.CertTemplate) { |
| 0 | 444 | | if (aceType is ACEGuids.AllGuid or "") |
| 0 | 445 | | yield return new ACE { |
| 0 | 446 | | PrincipalType = resolvedPrincipal.ObjectType, |
| 0 | 447 | | PrincipalSID = resolvedPrincipal.ObjectIdentifier, |
| 0 | 448 | | IsInherited = inherited, |
| 0 | 449 | | RightName = EdgeNames.AllExtendedRights, |
| 0 | 450 | | InheritanceHash = aceInheritanceHash |
| 0 | 451 | | }; |
| 0 | 452 | | else if (aceType is ACEGuids.Enroll) |
| 0 | 453 | | yield return new ACE { |
| 0 | 454 | | PrincipalType = resolvedPrincipal.ObjectType, |
| 0 | 455 | | PrincipalSID = resolvedPrincipal.ObjectIdentifier, |
| 0 | 456 | | IsInherited = inherited, |
| 0 | 457 | | RightName = EdgeNames.Enroll, |
| 0 | 458 | | InheritanceHash = aceInheritanceHash |
| 0 | 459 | | }; |
| 0 | 460 | | } |
| 12 | 461 | | } |
| | 462 | |
|
| | 463 | | //GenericWrite encapsulates WriteProperty, so process them in tandem to avoid duplicate edges |
| 26 | 464 | | if (aceRights.HasFlag(ActiveDirectoryRights.GenericWrite) || |
| 32 | 465 | | aceRights.HasFlag(ActiveDirectoryRights.WriteProperty)) { |
| 6 | 466 | | if (objectType is Label.User |
| 6 | 467 | | or Label.Group |
| 6 | 468 | | or Label.Computer |
| 6 | 469 | | or Label.GPO |
| 6 | 470 | | or Label.OU |
| 6 | 471 | | or Label.Domain |
| 6 | 472 | | or Label.CertTemplate |
| 6 | 473 | | or Label.RootCA |
| 6 | 474 | | or Label.EnterpriseCA |
| 6 | 475 | | or Label.AIACA |
| 6 | 476 | | or Label.NTAuthStore |
| 6 | 477 | | or Label.IssuancePolicy) |
| 5 | 478 | | if (aceType is ACEGuids.AllGuid or "") |
| 2 | 479 | | yield return new ACE { |
| 2 | 480 | | PrincipalType = resolvedPrincipal.ObjectType, |
| 2 | 481 | | PrincipalSID = resolvedPrincipal.ObjectIdentifier, |
| 2 | 482 | | IsInherited = inherited, |
| 2 | 483 | | RightName = EdgeNames.GenericWrite, |
| 2 | 484 | | InheritanceHash = aceInheritanceHash |
| 2 | 485 | | }; |
| | 486 | |
|
| 6 | 487 | | if (objectType == Label.User && aceType == ACEGuids.WriteSPN) |
| 0 | 488 | | yield return new ACE { |
| 0 | 489 | | PrincipalType = resolvedPrincipal.ObjectType, |
| 0 | 490 | | PrincipalSID = resolvedPrincipal.ObjectIdentifier, |
| 0 | 491 | | IsInherited = inherited, |
| 0 | 492 | | RightName = EdgeNames.WriteSPN, |
| 0 | 493 | | InheritanceHash = aceInheritanceHash |
| 0 | 494 | | }; |
| 6 | 495 | | else if (objectType == Label.Computer && aceType == ACEGuids.WriteAllowedToAct) |
| 1 | 496 | | yield return new ACE { |
| 1 | 497 | | PrincipalType = resolvedPrincipal.ObjectType, |
| 1 | 498 | | PrincipalSID = resolvedPrincipal.ObjectIdentifier, |
| 1 | 499 | | IsInherited = inherited, |
| 1 | 500 | | RightName = EdgeNames.AddAllowedToAct, |
| 1 | 501 | | InheritanceHash = aceInheritanceHash |
| 1 | 502 | | }; |
| 5 | 503 | | else if (objectType == Label.Computer && aceType == ACEGuids.UserAccountRestrictions) |
| 0 | 504 | | yield return new ACE { |
| 0 | 505 | | PrincipalType = resolvedPrincipal.ObjectType, |
| 0 | 506 | | PrincipalSID = resolvedPrincipal.ObjectIdentifier, |
| 0 | 507 | | IsInherited = inherited, |
| 0 | 508 | | RightName = EdgeNames.WriteAccountRestrictions, |
| 0 | 509 | | InheritanceHash = aceInheritanceHash |
| 0 | 510 | | }; |
| 5 | 511 | | else if (objectType is Label.OU or Label.Domain && aceType == ACEGuids.WriteGPLink) |
| 0 | 512 | | yield return new ACE |
| 0 | 513 | | { |
| 0 | 514 | | PrincipalType = resolvedPrincipal.ObjectType, |
| 0 | 515 | | PrincipalSID = resolvedPrincipal.ObjectIdentifier, |
| 0 | 516 | | IsInherited = inherited, |
| 0 | 517 | | RightName = EdgeNames.WriteGPLink, |
| 0 | 518 | | InheritanceHash = aceInheritanceHash |
| 0 | 519 | | }; |
| 5 | 520 | | else if (objectType == Label.Group && aceType == ACEGuids.WriteMember) |
| 2 | 521 | | yield return new ACE { |
| 2 | 522 | | PrincipalType = resolvedPrincipal.ObjectType, |
| 2 | 523 | | PrincipalSID = resolvedPrincipal.ObjectIdentifier, |
| 2 | 524 | | IsInherited = inherited, |
| 2 | 525 | | RightName = EdgeNames.AddMember, |
| 2 | 526 | | InheritanceHash = aceInheritanceHash |
| 2 | 527 | | }; |
| 3 | 528 | | else if (objectType is Label.User or Label.Computer && aceType == ACEGuids.AddKeyPrincipal) |
| 0 | 529 | | yield return new ACE { |
| 0 | 530 | | PrincipalType = resolvedPrincipal.ObjectType, |
| 0 | 531 | | PrincipalSID = resolvedPrincipal.ObjectIdentifier, |
| 0 | 532 | | IsInherited = inherited, |
| 0 | 533 | | RightName = EdgeNames.AddKeyCredentialLink, |
| 0 | 534 | | InheritanceHash = aceInheritanceHash |
| 0 | 535 | | }; |
| 3 | 536 | | else if (objectType is Label.CertTemplate) { |
| 0 | 537 | | if (aceType == ACEGuids.PKIEnrollmentFlag) |
| 0 | 538 | | yield return new ACE { |
| 0 | 539 | | PrincipalType = resolvedPrincipal.ObjectType, |
| 0 | 540 | | PrincipalSID = resolvedPrincipal.ObjectIdentifier, |
| 0 | 541 | | IsInherited = inherited, |
| 0 | 542 | | RightName = EdgeNames.WritePKIEnrollmentFlag, |
| 0 | 543 | | InheritanceHash = aceInheritanceHash |
| 0 | 544 | | }; |
| 0 | 545 | | else if (aceType == ACEGuids.PKINameFlag) |
| 0 | 546 | | yield return new ACE { |
| 0 | 547 | | PrincipalType = resolvedPrincipal.ObjectType, |
| 0 | 548 | | PrincipalSID = resolvedPrincipal.ObjectIdentifier, |
| 0 | 549 | | IsInherited = inherited, |
| 0 | 550 | | RightName = EdgeNames.WritePKINameFlag, |
| 0 | 551 | | InheritanceHash = aceInheritanceHash |
| 0 | 552 | | }; |
| 0 | 553 | | } |
| 6 | 554 | | } |
| | 555 | |
|
| | 556 | | // EnterpriseCA rights |
| 26 | 557 | | if (objectType == Label.EnterpriseCA) { |
| 0 | 558 | | if (aceType is ACEGuids.Enroll) |
| 0 | 559 | | yield return new ACE { |
| 0 | 560 | | PrincipalType = resolvedPrincipal.ObjectType, |
| 0 | 561 | | PrincipalSID = resolvedPrincipal.ObjectIdentifier, |
| 0 | 562 | | IsInherited = inherited, |
| 0 | 563 | | RightName = EdgeNames.Enroll, |
| 0 | 564 | | InheritanceHash = aceInheritanceHash |
| 0 | 565 | | }; |
| | 566 | |
|
| 0 | 567 | | var cARights = (CertificationAuthorityRights)aceRights; |
| | 568 | |
|
| | 569 | | // TODO: These if statements are also present in ProcessRegistryEnrollmentPermissions. Move to share |
| 0 | 570 | | if ((cARights & CertificationAuthorityRights.ManageCA) != 0) |
| 0 | 571 | | yield return new ACE { |
| 0 | 572 | | PrincipalType = resolvedPrincipal.ObjectType, |
| 0 | 573 | | PrincipalSID = resolvedPrincipal.ObjectIdentifier, |
| 0 | 574 | | IsInherited = inherited, |
| 0 | 575 | | RightName = EdgeNames.ManageCA, |
| 0 | 576 | | InheritanceHash = aceInheritanceHash |
| 0 | 577 | | }; |
| 0 | 578 | | if ((cARights & CertificationAuthorityRights.ManageCertificates) != 0) |
| 0 | 579 | | yield return new ACE { |
| 0 | 580 | | PrincipalType = resolvedPrincipal.ObjectType, |
| 0 | 581 | | PrincipalSID = resolvedPrincipal.ObjectIdentifier, |
| 0 | 582 | | IsInherited = inherited, |
| 0 | 583 | | RightName = EdgeNames.ManageCertificates, |
| 0 | 584 | | InheritanceHash = aceInheritanceHash |
| 0 | 585 | | }; |
| | 586 | |
|
| 0 | 587 | | if ((cARights & CertificationAuthorityRights.Enroll) != 0) |
| 0 | 588 | | yield return new ACE { |
| 0 | 589 | | PrincipalType = resolvedPrincipal.ObjectType, |
| 0 | 590 | | PrincipalSID = resolvedPrincipal.ObjectIdentifier, |
| 0 | 591 | | IsInherited = inherited, |
| 0 | 592 | | RightName = EdgeNames.Enroll, |
| 0 | 593 | | InheritanceHash = aceInheritanceHash |
| 0 | 594 | | }; |
| 0 | 595 | | } |
| 26 | 596 | | } |
| 27 | 597 | | } |
| | 598 | |
|
| | 599 | | /// <summary> |
| | 600 | | /// Helper function to use commonlib types and pass to ProcessGMSAReaders |
| | 601 | | /// </summary> |
| | 602 | | /// <param name="resolvedSearchResult"></param> |
| | 603 | | /// <param name="searchResultEntry"></param> |
| | 604 | | /// <returns></returns> |
| | 605 | | public IAsyncEnumerable<ACE> ProcessGMSAReaders(ResolvedSearchResult resolvedSearchResult, |
| 0 | 606 | | IDirectoryObject searchResultEntry) { |
| 0 | 607 | | if (!searchResultEntry.TryGetByteProperty(LDAPProperties.GroupMSAMembership, out var descriptor)) { |
| 0 | 608 | | return AsyncEnumerable.Empty<ACE>(); |
| | 609 | | } |
| | 610 | |
|
| 0 | 611 | | var domain = resolvedSearchResult.Domain; |
| 0 | 612 | | var name = resolvedSearchResult.DisplayName; |
| | 613 | |
|
| 0 | 614 | | return ProcessGMSAReaders(descriptor, name, domain); |
| 0 | 615 | | } |
| | 616 | |
|
| | 617 | | /// <summary> |
| | 618 | | /// ProcessGMSAMembership with no account name |
| | 619 | | /// </summary> |
| | 620 | | /// <param name="groupMSAMembership"></param> |
| | 621 | | /// <param name="objectDomain"></param> |
| | 622 | | /// <returns></returns> |
| 5 | 623 | | public IAsyncEnumerable<ACE> ProcessGMSAReaders(byte[] groupMSAMembership, string objectDomain) { |
| 5 | 624 | | return ProcessGMSAReaders(groupMSAMembership, "", objectDomain); |
| 5 | 625 | | } |
| | 626 | |
|
| | 627 | | /// <summary> |
| | 628 | | /// Processes the msds-groupmsamembership property and returns ACEs representing principals that can read th |
| | 629 | | /// password from an object |
| | 630 | | /// </summary> |
| | 631 | | /// <param name="groupMSAMembership"></param> |
| | 632 | | /// <param name="objectName"></param> |
| | 633 | | /// <param name="objectDomain"></param> |
| | 634 | | /// <returns></returns> |
| | 635 | | public async IAsyncEnumerable<ACE> ProcessGMSAReaders(byte[] groupMSAMembership, string objectName, |
| 5 | 636 | | string objectDomain) { |
| 6 | 637 | | if (groupMSAMembership == null) { |
| 1 | 638 | | _log.LogDebug("GMSA bytes are null for {Name}", objectName); |
| 1 | 639 | | yield break; |
| | 640 | | } |
| | 641 | |
|
| 4 | 642 | | var descriptor = _utils.MakeSecurityDescriptor(); |
| 4 | 643 | | try { |
| 4 | 644 | | descriptor.SetSecurityDescriptorBinaryForm(groupMSAMembership); |
| 4 | 645 | | } catch (OverflowException) { |
| 0 | 646 | | _log.LogWarning("GMSA ACL length on object {Name} exceeds allowable length. Unable to process", |
| 0 | 647 | | objectName); |
| 0 | 648 | | yield break; |
| | 649 | | } |
| | 650 | |
|
| 4 | 651 | | _log.LogDebug("Processing GMSA Readers for {ObjectName}", objectName); |
| 24 | 652 | | foreach (var ace in descriptor.GetAccessRules(true, true, typeof(SecurityIdentifier))) { |
| 6 | 653 | | if (ace == null || ace.AccessControlType() == AccessControlType.Deny) { |
| 2 | 654 | | continue; |
| | 655 | | } |
| | 656 | |
|
| 2 | 657 | | var ir = ace.IdentityReference(); |
| 2 | 658 | | var principalSid = Helpers.PreProcessSID(ir); |
| | 659 | |
|
| 3 | 660 | | if (principalSid == null) { |
| 1 | 661 | | continue; |
| | 662 | | } |
| | 663 | |
|
| 1 | 664 | | _log.LogTrace("Processing GMSA ACE with principal {Principal}", principalSid); |
| | 665 | |
|
| 2 | 666 | | if (await _utils.ResolveIDAndType(principalSid, objectDomain) is (true, var resolvedPrincipal)) { |
| 1 | 667 | | yield return new ACE { |
| 1 | 668 | | RightName = EdgeNames.ReadGMSAPassword, |
| 1 | 669 | | PrincipalType = resolvedPrincipal.ObjectType, |
| 1 | 670 | | PrincipalSID = resolvedPrincipal.ObjectIdentifier, |
| 1 | 671 | | IsInherited = ace.IsInherited() |
| 1 | 672 | | }; |
| 1 | 673 | | } |
| 1 | 674 | | } |
| 5 | 675 | | } |
| | 676 | | } |
| | 677 | | } |