< Summary

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

Metrics

MethodBranch coverage Cyclomatic complexity NPath complexity Sequence coverage
.ctor(...)0%200%
ProcessRegistryEnrollmentPermissions()0%2600%
ProcessEAPermissions()0%800%
ProcessCertTemplates(...)0%400%
GetRegistryPrincipal(...)0%1200%
GetMachineSid()0%1000%
ResolveDomainControllerPrincipal(...)0%200%
ConvertLocalWellKnownPrincipal(...)0%1200%
OpenSamServer(...)0%200%
SendComputerStatus()0%400%

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
 025        public CertAbuseProcessor(ILDAPUtils utils, ILogger log = null)
 026        {
 027            _utils = utils;
 028            _log = log ?? Logging.LogProvider.CreateLogger("CAProc");
 029        }
 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,
 040        {
 041            var data = new AceRegistryAPIResult();
 42
 043            var aceData = GetCASecurity(computerName, caName);
 044            data.Collected = aceData.Collected;
 045            if (!aceData.Collected)
 046            {
 047                data.FailureReason = aceData.FailureReason;
 048                return data;
 49            }
 50
 051            if (aceData.Value == null)
 052            {
 053                return data;
 54            }
 55
 056            var descriptor = _utils.MakeSecurityDescriptor();
 057            descriptor.SetSecurityDescriptorBinaryForm(aceData.Value as byte[], AccessControlSections.All);
 58
 059            var ownerSid = Helpers.PreProcessSID(descriptor.GetOwner(typeof(SecurityIdentifier)));
 060            var computerDomain = _utils.GetDomainNameFromSid(computerObjectId);
 061            var isDomainController = _utils.IsDomainController(computerObjectId, computerDomain);
 062            var machineSid = await GetMachineSid(computerName, computerObjectId, computerDomain, isDomainController);
 63
 064            var aces = new List<ACE>();
 65
 066            if (ownerSid != null)
 067            {
 068                var resolvedOwner = GetRegistryPrincipal(new SecurityIdentifier(ownerSid), computerDomain, computerName,
 069                if (resolvedOwner != null)
 070                    aces.Add(new ACE
 071                    {
 072                        PrincipalType = resolvedOwner.ObjectType,
 073                        PrincipalSID = resolvedOwner.ObjectIdentifier,
 074                        RightName = EdgeNames.Owns,
 075                        IsInherited = false
 076                    });
 077            }
 78            else
 079            {
 080                _log.LogDebug("Owner on CA {Name} is null", computerName);
 081            }
 82
 083            foreach (var rule in descriptor.GetAccessRules(true, true, typeof(SecurityIdentifier)))
 084            {
 085                if (rule == null)
 086                    continue;
 87
 088                if (rule.AccessControlType() == AccessControlType.Deny)
 089                    continue;
 90
 091                var principalSid = Helpers.PreProcessSID(rule.IdentityReference());
 092                if (principalSid == null)
 093                    continue;
 94
 095                var principalDomain = _utils.GetDomainNameFromSid(principalSid) ?? objectDomain;
 096                var resolvedPrincipal = GetRegistryPrincipal(new SecurityIdentifier(principalSid), principalDomain, comp
 097                var isInherited = rule.IsInherited();
 98
 099                var cARights = (CertificationAuthorityRights)rule.ActiveDirectoryRights();
 100
 101                // TODO: These if statements are also present in ProcessACL. Move to shared location.
 0102                if ((cARights & CertificationAuthorityRights.ManageCA) != 0)
 0103                    aces.Add(new ACE
 0104                    {
 0105                        PrincipalType = resolvedPrincipal.ObjectType,
 0106                        PrincipalSID = resolvedPrincipal.ObjectIdentifier,
 0107                        IsInherited = isInherited,
 0108                        RightName = EdgeNames.ManageCA
 0109                    });
 0110                if ((cARights & CertificationAuthorityRights.ManageCertificates) != 0)
 0111                    aces.Add(new ACE
 0112                    {
 0113                        PrincipalType = resolvedPrincipal.ObjectType,
 0114                        PrincipalSID = resolvedPrincipal.ObjectIdentifier,
 0115                        IsInherited = isInherited,
 0116                        RightName = EdgeNames.ManageCertificates
 0117                    });
 118
 0119                if ((cARights & CertificationAuthorityRights.Enroll) != 0)
 0120                    aces.Add(new ACE
 0121                    {
 0122                        PrincipalType = resolvedPrincipal.ObjectType,
 0123                        PrincipalSID = resolvedPrincipal.ObjectIdentifier,
 0124                        IsInherited = isInherited,
 0125                        RightName = EdgeNames.Enroll
 0126                    });
 0127            }
 128
 0129            data.Data = aces.ToArray();
 0130            return data;
 0131        }
 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
 0140        {
 0141            var ret = new EnrollmentAgentRegistryAPIResult();
 0142            var regData = GetEnrollmentAgentRights(computerName, caName);
 143
 0144            ret.Collected = regData.Collected;
 0145            if (!ret.Collected)
 0146            {
 0147                ret.FailureReason = regData.FailureReason;
 0148                return ret;
 149            }
 150
 0151            if (regData.Value == null)
 0152            {
 0153                return ret;
 154            }
 155
 0156            var computerDomain = _utils.GetDomainNameFromSid(computerObjectId);
 0157            var isDomainController = _utils.IsDomainController(computerObjectId, computerDomain);
 0158            var machineSid = await GetMachineSid(computerName, computerObjectId, computerDomain, isDomainController);
 0159            var certTemplatesLocation = _utils.BuildLdapPath(DirectoryPaths.CertTemplateLocation, computerDomain);
 0160            var descriptor = new RawSecurityDescriptor(regData.Value as byte[], 0);
 0161            var enrollmentAgentRestrictions = new List<EnrollmentAgentRestriction>();
 0162            foreach (var genericAce in descriptor.DiscretionaryAcl)
 0163            {
 0164                var ace = (QualifiedAce)genericAce;
 0165                enrollmentAgentRestrictions.Add(new EnrollmentAgentRestriction(ace, computerDomain, certTemplatesLocatio
 0166            }
 167
 0168            ret.Restrictions = enrollmentAgentRestrictions.ToArray();
 169
 0170            return ret;
 0171        }
 172
 173        public (IEnumerable<TypedPrincipal> resolvedTemplates, IEnumerable<String> unresolvedTemplates) ProcessCertTempl
 0174        {
 0175            var resolvedTemplates = new List<TypedPrincipal>();
 0176            var unresolvedTemplates = new List<String>();
 177
 0178            var certTemplatesLocation = _utils.BuildLdapPath(DirectoryPaths.CertTemplateLocation, domainName);
 0179            foreach (var templateCN in templates)
 0180            {
 0181                var res = _utils.ResolveCertTemplateByProperty(Encoder.LdapFilterEncode(templateCN), LDAPProperties.Cano
 0182                if (res != null) {
 0183                    resolvedTemplates.Add(res);
 0184                } else {
 0185                    unresolvedTemplates.Add(templateCN);
 0186                }
 0187            }
 188
 0189            return (resolvedTemplates: resolvedTemplates, unresolvedTemplates: unresolvedTemplates);
 0190        }
 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
 0293        {
 0294            _log.LogTrace("Got principal with sid {SID} on computer {ComputerName}", sid.Value, computerName);
 295
 296            //Check if our sid is filtered
 0297            if (Helpers.IsSidFiltered(sid.Value))
 0298                return null;
 299
 0300            if (isDomainController)
 0301            {
 0302                var result = ResolveDomainControllerPrincipal(sid.Value, computerDomain);
 0303                if (result != null)
 0304                    return result;
 0305            }
 306
 307            //If we get a local well known principal, we need to convert it using the computer's domain sid
 0308            if (ConvertLocalWellKnownPrincipal(sid, computerObjectId, computerDomain, out var principal))
 0309            {
 0310                _log.LogTrace("Got Well Known Principal {SID} on computer {Computer} with type {Type}", principal.Object
 0311                return principal;
 312            }
 313
 314            //If the security identifier starts with the machine sid, we need to resolve it as a local principal
 0315            if (machineSid != null && sid.IsEqualDomainSid(machineSid))
 0316            {
 0317                _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
 0320                var objectType = Label.LocalGroup;
 321
 322                // The local group sid is computer machine sid - group rid.
 0323                var groupRid = sid.Rid();
 0324                var newSid = $"{computerObjectId}-{groupRid}";
 0325                return (new TypedPrincipal
 0326                {
 0327                    ObjectIdentifier = newSid,
 0328                    ObjectType = objectType
 0329                });
 330            }
 331
 332            //If we get here, we most likely have a domain principal. Do a lookup
 0333            return _utils.ResolveIDAndType(sid.Value, computerDomain);
 0334        }
 335
 336        private async Task<SecurityIdentifier> GetMachineSid(string computerName, string computerObjectId, string comput
 0337        {
 0338            SecurityIdentifier machineSid = null;
 339
 340            //Try to get the machine sid for the computer if its not already cached
 0341            if (!Cache.GetMachineSid(computerObjectId, out var tempMachineSid))
 0342            {
 343                // Open a handle to the server
 0344                var openServerResult = OpenSamServer(computerName);
 0345                if (openServerResult.IsFailed)
 0346                {
 0347                    _log.LogTrace("OpenServer failed on {ComputerName}: {Error}", computerName, openServerResult.SError)
 0348                    await SendComputerStatus(new CSVComputerStatus
 0349                    {
 0350                        Task = "SamConnect",
 0351                        ComputerName = computerName,
 0352                        Status = openServerResult.SError
 0353                    });
 0354                    return null;
 355                }
 356
 0357                var server = openServerResult.Value;
 0358                var getMachineSidResult = server.GetMachineSid();
 0359                if (getMachineSidResult.IsFailed)
 0360                {
 0361                    _log.LogTrace("GetMachineSid failed on {ComputerName}: {Error}", computerName, getMachineSidResult.S
 0362                    await SendComputerStatus(new CSVComputerStatus
 0363                    {
 0364                        Status = getMachineSidResult.SError,
 0365                        ComputerName = computerName,
 0366                        Task = "GetMachineSid"
 0367                    });
 368                    //If we can't get a machine sid, we wont be able to make local principals with unique object ids, or
 0369                    _log.LogWarning("Unable to get machineSid for {Computer}: {Status}", computerName, getMachineSidResu
 0370                    return null;
 371                }
 372
 0373                machineSid = getMachineSidResult.Value;
 0374                Cache.AddMachineSid(computerObjectId, machineSid.Value);
 0375            }
 376            else
 0377            {
 0378                machineSid = new SecurityIdentifier(tempMachineSid);
 0379            }
 380
 0381            return machineSid;
 0382        }
 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)
 0386        {
 387            //If the server is a domain controller and we have a well known group, use the domain value
 0388            if (_utils.GetWellKnownPrincipal(sid, computerDomain, out var wellKnown))
 0389                return wellKnown;
 390            //Otherwise, do a domain lookup
 0391            return _utils.ResolveIDAndType(sid, computerDomain);
 0392        }
 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)
 0397        {
 0398            if (WellKnownPrincipal.GetWellKnownPrincipal(sid.Value, out var common))
 0399            {
 400                //The everyone and auth users principals are special and will be converted to the domain equivalent
 0401                if (sid.Value is "S-1-1-0" or "S-1-5-11")
 0402                {
 0403                    _utils.GetWellKnownPrincipal(sid.Value, computerDomain, out principal);
 0404                    return true;
 405                }
 406
 407                //Use the computer object id + the RID of the sid we looked up to create our new principal
 0408                principal = new TypedPrincipal
 0409                {
 0410                    ObjectIdentifier = $"{computerDomainSid}-{sid.Rid()}",
 0411                    ObjectType = common.ObjectType switch
 0412                    {
 0413                        Label.User => Label.LocalUser,
 0414                        Label.Group => Label.LocalGroup,
 0415                        _ => common.ObjectType
 0416                    }
 0417                };
 418
 0419                return true;
 420            }
 421
 0422            principal = null;
 0423            return false;
 0424        }
 425
 426        public virtual Result<ISAMServer> OpenSamServer(string computerName)
 0427        {
 0428            var result = SAMServer.OpenServer(computerName);
 0429            if (result.IsFailed)
 0430            {
 0431                return Result<ISAMServer>.Fail(result.SError);
 432            }
 433
 0434            return Result<ISAMServer>.Ok(result.Value);
 0435        }
 436
 437        private async Task SendComputerStatus(CSVComputerStatus status)
 0438        {
 0439            if (ComputerStatusEvent is not null) await ComputerStatusEvent(status);
 0440        }
 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    {
 499        public bool Collected { get; set; } = false;
 500        public byte[] Value { get; set; }
 501        public string FailureReason { get; set; }
 502    }
 503}