< Summary

Class:SharpHoundCommonLib.Processors.CertRegistryResult
Assembly:SharpHoundCommonLib
File(s):D:\a\SharpHoundCommon\SharpHoundCommon\src\CommonLib\Processors\CertAbuseProcessor.cs
Covered lines:0
Uncovered lines:3
Coverable lines:3
Total lines:503
Line coverage:0% (0 of 3)
Covered branches:0
Total branches:0

File(s)

D:\a\SharpHoundCommon\SharpHoundCommon\src\CommonLib\Processors\CertAbuseProcessor.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Diagnostics.CodeAnalysis;
 4using System.Security.AccessControl;
 5using System.Security.Principal;
 6using System.Text;
 7using System.Threading.Tasks;
 8using Microsoft.Extensions.Logging;
 9using SharpHoundCommonLib.Enums;
 10using SharpHoundCommonLib.OutputTypes;
 11using SharpHoundRPC;
 12using SharpHoundRPC.Wrappers;
 13using Encoder = Microsoft.Security.Application.Encoder;
 14
 15namespace SharpHoundCommonLib.Processors
 16{
 17    public class CertAbuseProcessor
 18    {
 19        private readonly ILogger _log;
 20        public readonly ILDAPUtils _utils;
 21        public delegate Task ComputerStatusDelegate(CSVComputerStatus status);
 22        public event ComputerStatusDelegate ComputerStatusEvent;
 23
 24
 25        public CertAbuseProcessor(ILDAPUtils utils, ILogger log = null)
 26        {
 27            _utils = utils;
 28            _log = log ?? Logging.LogProvider.CreateLogger("CAProc");
 29        }
 30
 31        /// <summary>
 32        /// This function should be called with the security data fetched from <see cref="GetCARegistryValues"/>.
 33        /// The resulting ACEs will contain the owner of the CA as well as Management rights.
 34        /// </summary>
 35        /// <param name="security"></param>
 36        /// <param name="objectDomain"></param>
 37        /// <param name="computerName"></param>
 38        /// <returns></returns>
 39        public async Task<AceRegistryAPIResult> ProcessRegistryEnrollmentPermissions(string caName, string objectDomain,
 40        {
 41            var data = new AceRegistryAPIResult();
 42
 43            var aceData = GetCASecurity(computerName, caName);
 44            data.Collected = aceData.Collected;
 45            if (!aceData.Collected)
 46            {
 47                data.FailureReason = aceData.FailureReason;
 48                return data;
 49            }
 50
 51            if (aceData.Value == null)
 52            {
 53                return data;
 54            }
 55
 56            var descriptor = _utils.MakeSecurityDescriptor();
 57            descriptor.SetSecurityDescriptorBinaryForm(aceData.Value as byte[], AccessControlSections.All);
 58
 59            var ownerSid = Helpers.PreProcessSID(descriptor.GetOwner(typeof(SecurityIdentifier)));
 60            var computerDomain = _utils.GetDomainNameFromSid(computerObjectId);
 61            var isDomainController = _utils.IsDomainController(computerObjectId, computerDomain);
 62            var machineSid = await GetMachineSid(computerName, computerObjectId, computerDomain, isDomainController);
 63
 64            var aces = new List<ACE>();
 65
 66            if (ownerSid != null)
 67            {
 68                var resolvedOwner = GetRegistryPrincipal(new SecurityIdentifier(ownerSid), computerDomain, computerName,
 69                if (resolvedOwner != null)
 70                    aces.Add(new ACE
 71                    {
 72                        PrincipalType = resolvedOwner.ObjectType,
 73                        PrincipalSID = resolvedOwner.ObjectIdentifier,
 74                        RightName = EdgeNames.Owns,
 75                        IsInherited = false
 76                    });
 77            }
 78            else
 79            {
 80                _log.LogDebug("Owner on CA {Name} is null", computerName);
 81            }
 82
 83            foreach (var rule in descriptor.GetAccessRules(true, true, typeof(SecurityIdentifier)))
 84            {
 85                if (rule == null)
 86                    continue;
 87
 88                if (rule.AccessControlType() == AccessControlType.Deny)
 89                    continue;
 90
 91                var principalSid = Helpers.PreProcessSID(rule.IdentityReference());
 92                if (principalSid == null)
 93                    continue;
 94
 95                var principalDomain = _utils.GetDomainNameFromSid(principalSid) ?? objectDomain;
 96                var resolvedPrincipal = GetRegistryPrincipal(new SecurityIdentifier(principalSid), principalDomain, comp
 97                var isInherited = rule.IsInherited();
 98
 99                var cARights = (CertificationAuthorityRights)rule.ActiveDirectoryRights();
 100
 101                // TODO: These if statements are also present in ProcessACL. Move to shared location.
 102                if ((cARights & CertificationAuthorityRights.ManageCA) != 0)
 103                    aces.Add(new ACE
 104                    {
 105                        PrincipalType = resolvedPrincipal.ObjectType,
 106                        PrincipalSID = resolvedPrincipal.ObjectIdentifier,
 107                        IsInherited = isInherited,
 108                        RightName = EdgeNames.ManageCA
 109                    });
 110                if ((cARights & CertificationAuthorityRights.ManageCertificates) != 0)
 111                    aces.Add(new ACE
 112                    {
 113                        PrincipalType = resolvedPrincipal.ObjectType,
 114                        PrincipalSID = resolvedPrincipal.ObjectIdentifier,
 115                        IsInherited = isInherited,
 116                        RightName = EdgeNames.ManageCertificates
 117                    });
 118
 119                if ((cARights & CertificationAuthorityRights.Enroll) != 0)
 120                    aces.Add(new ACE
 121                    {
 122                        PrincipalType = resolvedPrincipal.ObjectType,
 123                        PrincipalSID = resolvedPrincipal.ObjectIdentifier,
 124                        IsInherited = isInherited,
 125                        RightName = EdgeNames.Enroll
 126                    });
 127            }
 128
 129            data.Data = aces.ToArray();
 130            return data;
 131        }
 132
 133        /// <summary>
 134        /// This function should be called with the enrollment data fetched from <see cref="GetCARegistryValues"/>.
 135        /// The resulting items will contain enrollment agent restrictions
 136        /// </summary>
 137        /// <param name="enrollmentAgentRestrictions"></param>
 138        /// <returns></returns>
 139        public async Task<EnrollmentAgentRegistryAPIResult> ProcessEAPermissions(string caName, string objectDomain, str
 140        {
 141            var ret = new EnrollmentAgentRegistryAPIResult();
 142            var regData = GetEnrollmentAgentRights(computerName, caName);
 143
 144            ret.Collected = regData.Collected;
 145            if (!ret.Collected)
 146            {
 147                ret.FailureReason = regData.FailureReason;
 148                return ret;
 149            }
 150
 151            if (regData.Value == null)
 152            {
 153                return ret;
 154            }
 155
 156            var computerDomain = _utils.GetDomainNameFromSid(computerObjectId);
 157            var isDomainController = _utils.IsDomainController(computerObjectId, computerDomain);
 158            var machineSid = await GetMachineSid(computerName, computerObjectId, computerDomain, isDomainController);
 159            var certTemplatesLocation = _utils.BuildLdapPath(DirectoryPaths.CertTemplateLocation, computerDomain);
 160            var descriptor = new RawSecurityDescriptor(regData.Value as byte[], 0);
 161            var enrollmentAgentRestrictions = new List<EnrollmentAgentRestriction>();
 162            foreach (var genericAce in descriptor.DiscretionaryAcl)
 163            {
 164                var ace = (QualifiedAce)genericAce;
 165                enrollmentAgentRestrictions.Add(new EnrollmentAgentRestriction(ace, computerDomain, certTemplatesLocatio
 166            }
 167
 168            ret.Restrictions = enrollmentAgentRestrictions.ToArray();
 169
 170            return ret;
 171        }
 172
 173        public (IEnumerable<TypedPrincipal> resolvedTemplates, IEnumerable<String> unresolvedTemplates) ProcessCertTempl
 174        {
 175            var resolvedTemplates = new List<TypedPrincipal>();
 176            var unresolvedTemplates = new List<String>();
 177
 178            var certTemplatesLocation = _utils.BuildLdapPath(DirectoryPaths.CertTemplateLocation, domainName);
 179            foreach (var templateCN in templates)
 180            {
 181                var res = _utils.ResolveCertTemplateByProperty(Encoder.LdapFilterEncode(templateCN), LDAPProperties.Cano
 182                if (res != null) {
 183                    resolvedTemplates.Add(res);
 184                } else {
 185                    unresolvedTemplates.Add(templateCN);
 186                }
 187            }
 188
 189            return (resolvedTemplates: resolvedTemplates, unresolvedTemplates: unresolvedTemplates);
 190        }
 191
 192        /// <summary>
 193        /// Get CA security registry value from the remote machine for processing security/enrollmentagentrights
 194        /// </summary>
 195        /// <param name="target"></param>
 196        /// <param name="caName"></param>
 197        /// <returns></returns>
 198        [ExcludeFromCodeCoverage]
 199        private RegistryResult GetCASecurity(string target, string caName)
 200        {
 201            var regSubKey = $"SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\{caName}";
 202            const string regValue = "Security";
 203
 204            return Helpers.GetRegistryKeyData(target, regSubKey, regValue, _log);
 205        }
 206
 207        /// <summary>
 208        /// Get EnrollmentAgentRights registry value from the remote machine for processing security/enrollmentagentrigh
 209        /// </summary>
 210        /// <param name="target"></param>
 211        /// <param name="caName"></param>
 212        /// <returns></returns>
 213        [ExcludeFromCodeCoverage]
 214        private RegistryResult GetEnrollmentAgentRights(string target, string caName)
 215        {
 216            var regSubKey = $"SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\{caName}";
 217            var regValue = "EnrollmentAgentRights";
 218
 219            return Helpers.GetRegistryKeyData(target, regSubKey, regValue, _log);
 220        }
 221
 222        /// <summary>
 223        /// This function checks a registry setting on the target host for the specified CA to see if a requesting user 
 224        /// The ManageCA permission allows you to flip this bit as well. This appears to usually work, even if admin rig
 225        /// </summary>
 226        /// <remarks>https://blog.keyfactor.com/hidden-dangers-certificate-subject-alternative-names-sans</remarks>
 227        /// <param name="target"></param>
 228        /// <param name="caName"></param>
 229        /// <returns></returns>
 230        /// <exception cref="Exception"></exception>
 231        [ExcludeFromCodeCoverage]
 232        public BoolRegistryAPIResult IsUserSpecifiesSanEnabled(string target, string caName)
 233        {
 234            var ret = new BoolRegistryAPIResult();
 235            var subKey =
 236                $"SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\{caName}\\PolicyModules\\CertificateAutho
 237            const string subValue = "EditFlags";
 238            var data = Helpers.GetRegistryKeyData(target, subKey, subValue, _log);
 239
 240            ret.Collected = data.Collected;
 241            if (!data.Collected)
 242            {
 243                ret.FailureReason = data.FailureReason;
 244                return ret;
 245            }
 246
 247            if (data.Value == null)
 248            {
 249                return ret;
 250            }
 251
 252            var editFlags = (int)data.Value;
 253            ret.Value = (editFlags & 0x00040000) == 0x00040000;
 254
 255            return ret;
 256        }
 257
 258        /// <summary>
 259        /// This function checks a registry setting on the target host for the specified CA to see if role seperation is
 260        /// If enabled, you cannot perform any CA actions if you have both ManageCA and ManageCertificates permissions. 
 261        /// </summary>
 262        /// <remarks>https://www.itprotoday.com/security/q-how-can-i-make-sure-given-windows-account-assigned-only-singl
 263        /// <param name="target"></param>
 264        /// <param name="caName"></param>
 265        /// <returns></returns>
 266        /// <exception cref="Exception"></exception>
 267        [ExcludeFromCodeCoverage]
 268        public BoolRegistryAPIResult RoleSeparationEnabled(string target, string caName)
 269        {
 270            var ret = new BoolRegistryAPIResult();
 271            var regSubKey = $"SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\{caName}";
 272            const string regValue = "RoleSeparationEnabled";
 273            var data = Helpers.GetRegistryKeyData(target, regSubKey, regValue, _log);
 274
 275            ret.Collected = data.Collected;
 276            if (!data.Collected)
 277            {
 278                ret.FailureReason = data.FailureReason;
 279                return ret;
 280            }
 281
 282            if (data.Value == null)
 283            {
 284                return ret;
 285            }
 286
 287            ret.Value = (int)data.Value == 1;
 288
 289            return ret;
 290        }
 291
 292        public TypedPrincipal GetRegistryPrincipal(SecurityIdentifier sid, string computerDomain, string computerName, b
 293        {
 294            _log.LogTrace("Got principal with sid {SID} on computer {ComputerName}", sid.Value, computerName);
 295
 296            //Check if our sid is filtered
 297            if (Helpers.IsSidFiltered(sid.Value))
 298                return null;
 299
 300            if (isDomainController)
 301            {
 302                var result = ResolveDomainControllerPrincipal(sid.Value, computerDomain);
 303                if (result != null)
 304                    return result;
 305            }
 306
 307            //If we get a local well known principal, we need to convert it using the computer's domain sid
 308            if (ConvertLocalWellKnownPrincipal(sid, computerObjectId, computerDomain, out var principal))
 309            {
 310                _log.LogTrace("Got Well Known Principal {SID} on computer {Computer} with type {Type}", principal.Object
 311                return principal;
 312            }
 313
 314            //If the security identifier starts with the machine sid, we need to resolve it as a local principal
 315            if (machineSid != null && sid.IsEqualDomainSid(machineSid))
 316            {
 317                _log.LogTrace("Got local principal {sid} on computer {Computer}", sid.Value, computerName);
 318
 319                // Set label to be local group. It could be a local user or alias but I'm not sure how we can confirm. B
 320                var objectType = Label.LocalGroup;
 321
 322                // The local group sid is computer machine sid - group rid.
 323                var groupRid = sid.Rid();
 324                var newSid = $"{computerObjectId}-{groupRid}";
 325                return (new TypedPrincipal
 326                {
 327                    ObjectIdentifier = newSid,
 328                    ObjectType = objectType
 329                });
 330            }
 331
 332            //If we get here, we most likely have a domain principal. Do a lookup
 333            return _utils.ResolveIDAndType(sid.Value, computerDomain);
 334        }
 335
 336        private async Task<SecurityIdentifier> GetMachineSid(string computerName, string computerObjectId, string comput
 337        {
 338            SecurityIdentifier machineSid = null;
 339
 340            //Try to get the machine sid for the computer if its not already cached
 341            if (!Cache.GetMachineSid(computerObjectId, out var tempMachineSid))
 342            {
 343                // Open a handle to the server
 344                var openServerResult = OpenSamServer(computerName);
 345                if (openServerResult.IsFailed)
 346                {
 347                    _log.LogTrace("OpenServer failed on {ComputerName}: {Error}", computerName, openServerResult.SError)
 348                    await SendComputerStatus(new CSVComputerStatus
 349                    {
 350                        Task = "SamConnect",
 351                        ComputerName = computerName,
 352                        Status = openServerResult.SError
 353                    });
 354                    return null;
 355                }
 356
 357                var server = openServerResult.Value;
 358                var getMachineSidResult = server.GetMachineSid();
 359                if (getMachineSidResult.IsFailed)
 360                {
 361                    _log.LogTrace("GetMachineSid failed on {ComputerName}: {Error}", computerName, getMachineSidResult.S
 362                    await SendComputerStatus(new CSVComputerStatus
 363                    {
 364                        Status = getMachineSidResult.SError,
 365                        ComputerName = computerName,
 366                        Task = "GetMachineSid"
 367                    });
 368                    //If we can't get a machine sid, we wont be able to make local principals with unique object ids, or
 369                    _log.LogWarning("Unable to get machineSid for {Computer}: {Status}", computerName, getMachineSidResu
 370                    return null;
 371                }
 372
 373                machineSid = getMachineSidResult.Value;
 374                Cache.AddMachineSid(computerObjectId, machineSid.Value);
 375            }
 376            else
 377            {
 378                machineSid = new SecurityIdentifier(tempMachineSid);
 379            }
 380
 381            return machineSid;
 382        }
 383
 384        // TODO: Copied from URA processor. Find a way to have this function in a shared spot
 385        private TypedPrincipal ResolveDomainControllerPrincipal(string sid, string computerDomain)
 386        {
 387            //If the server is a domain controller and we have a well known group, use the domain value
 388            if (_utils.GetWellKnownPrincipal(sid, computerDomain, out var wellKnown))
 389                return wellKnown;
 390            //Otherwise, do a domain lookup
 391            return _utils.ResolveIDAndType(sid, computerDomain);
 392        }
 393
 394        // TODO: Copied from URA processor. Find a way to have this function in a shared spot
 395        private bool ConvertLocalWellKnownPrincipal(SecurityIdentifier sid, string computerDomainSid,
 396            string computerDomain, out TypedPrincipal principal)
 397        {
 398            if (WellKnownPrincipal.GetWellKnownPrincipal(sid.Value, out var common))
 399            {
 400                //The everyone and auth users principals are special and will be converted to the domain equivalent
 401                if (sid.Value is "S-1-1-0" or "S-1-5-11")
 402                {
 403                    _utils.GetWellKnownPrincipal(sid.Value, computerDomain, out principal);
 404                    return true;
 405                }
 406
 407                //Use the computer object id + the RID of the sid we looked up to create our new principal
 408                principal = new TypedPrincipal
 409                {
 410                    ObjectIdentifier = $"{computerDomainSid}-{sid.Rid()}",
 411                    ObjectType = common.ObjectType switch
 412                    {
 413                        Label.User => Label.LocalUser,
 414                        Label.Group => Label.LocalGroup,
 415                        _ => common.ObjectType
 416                    }
 417                };
 418
 419                return true;
 420            }
 421
 422            principal = null;
 423            return false;
 424        }
 425
 426        public virtual Result<ISAMServer> OpenSamServer(string computerName)
 427        {
 428            var result = SAMServer.OpenServer(computerName);
 429            if (result.IsFailed)
 430            {
 431                return Result<ISAMServer>.Fail(result.SError);
 432            }
 433
 434            return Result<ISAMServer>.Ok(result.Value);
 435        }
 436
 437        private async Task SendComputerStatus(CSVComputerStatus status)
 438        {
 439            if (ComputerStatusEvent is not null) await ComputerStatusEvent(status);
 440        }
 441
 442    }
 443
 444    public class EnrollmentAgentRestriction
 445    {
 446        public EnrollmentAgentRestriction(QualifiedAce ace, string computerDomain, string certTemplatesLocation, CertAbu
 447        {
 448            var targets = new List<TypedPrincipal>();
 449            var index = 0;
 450
 451            // Access type (Allow/Deny)
 452            AccessType = ace.AceType.ToString();
 453
 454            // Agent
 455            Agent = certAbuseProcessor.GetRegistryPrincipal(ace.SecurityIdentifier, computerDomain, computerName, isDoma
 456
 457            // Targets
 458            var opaque = ace.GetOpaque();
 459            var sidCount = BitConverter.ToUInt32(opaque, 0);
 460            index += 4;
 461            for (var i = 0; i < sidCount; i++)
 462            {
 463                var sid = new SecurityIdentifier(opaque, index);
 464                targets.Add(certAbuseProcessor.GetRegistryPrincipal(ace.SecurityIdentifier, computerDomain, computerName
 465                index += sid.BinaryLength;
 466            }
 467            Targets = targets.ToArray();
 468
 469            // Template
 470            if (index < opaque.Length)
 471            {
 472                AllTemplates = false;
 473                var template = Encoding.Unicode.GetString(opaque, index, opaque.Length - index - 2).Replace("\u0000", st
 474
 475                // Attempt to resolve the cert template by CN
 476                Template = certAbuseProcessor._utils.ResolveCertTemplateByProperty(Encoder.LdapFilterEncode(template), L
 477
 478                // Attempt to resolve the cert template by OID
 479                if (Template == null)
 480                {
 481                    Template = certAbuseProcessor._utils.ResolveCertTemplateByProperty(template, LDAPProperties.CertTemp
 482                }
 483            }
 484            else
 485            {
 486                AllTemplates = true;
 487            }
 488        }
 489
 490        public string AccessType { get; set; }
 491        public TypedPrincipal Agent { get; set; }
 492        public TypedPrincipal[] Targets { get; set; }
 493        public TypedPrincipal Template { get; set; }
 494        public bool AllTemplates { get; set; } = false;
 495    }
 496
 497    public class CertRegistryResult
 498    {
 0499        public bool Collected { get; set; } = false;
 0500        public byte[] Value { get; set; }
 0501        public string FailureReason { get; set; }
 502    }
 503}

Methods/Properties

Collected()
Value()
FailureReason()