< Summary

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

Metrics

MethodBranch coverage Cyclomatic complexity NPath complexity Sequence coverage
.ctor(...)0%200%
ProcessRegistryEnrollmentPermissions()0%3800%
ProcessEAPermissions()0%1600%
ProcessCertTemplates()0%600%
GetRegistryPrincipal()0%2200%
GetMachineSid()0%1000%
CreateEnrollmentAgentRestriction()0%2400%
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        private 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 isDomainController = await _utils.IsDomainController(computerObjectId, objectDomain);
 061            var machineSid = await GetMachineSid(computerName, computerObjectId);
 62
 063            var aces = new List<ACE>();
 64
 065            if (ownerSid != null) {
 066                var processed = new SecurityIdentifier(ownerSid);
 067                if (await GetRegistryPrincipal(processed, objectDomain, computerName,
 068                        isDomainController, computerObjectId, machineSid) is (true, var resolvedOwner)) {
 069                    aces.Add(new ACE
 070                    {
 071                        PrincipalType = resolvedOwner.ObjectType,
 072                        PrincipalSID = resolvedOwner.ObjectIdentifier,
 073                        RightName = EdgeNames.Owns,
 074                        IsInherited = false
 075                    });
 076                } else {
 077                    aces.Add(new ACE
 078                    {
 079                        PrincipalType = Label.Base,
 080                        PrincipalSID = processed.Value,
 081                        RightName = EdgeNames.Owns,
 082                        IsInherited = false
 083                    });
 084                }
 085            }
 86            else
 087            {
 088                _log.LogDebug("Owner on CA {Name} is null", computerName);
 089            }
 90
 091            foreach (var rule in descriptor.GetAccessRules(true, true, typeof(SecurityIdentifier)))
 092            {
 093                if (rule == null)
 094                    continue;
 95
 096                if (rule.AccessControlType() == AccessControlType.Deny)
 097                    continue;
 98
 099                var principalSid = Helpers.PreProcessSID(rule.IdentityReference());
 0100                if (principalSid == null)
 0101                    continue;
 102
 0103                var (getDomainSuccess, principalDomain) = await _utils.GetDomainNameFromSid(principalSid);
 0104                if (!getDomainSuccess) {
 105                    //Fallback to computer's domain in case we cant resolve the principal domain
 0106                    principalDomain = objectDomain;
 0107                }
 0108                var (resSuccess, resolvedPrincipal) = await GetRegistryPrincipal(new SecurityIdentifier(principalSid), p
 0109                if (!resSuccess) {
 0110                    resolvedPrincipal = new TypedPrincipal {
 0111                        ObjectType = Label.Base,
 0112                        ObjectIdentifier = principalSid
 0113                    };
 0114                }
 0115                var isInherited = rule.IsInherited();
 116
 0117                var cARights = (CertificationAuthorityRights)rule.ActiveDirectoryRights();
 118
 119                // TODO: These if statements are also present in ProcessACL. Move to shared location.
 0120                if ((cARights & CertificationAuthorityRights.ManageCA) != 0)
 0121                    aces.Add(new ACE
 0122                    {
 0123                        PrincipalType = resolvedPrincipal.ObjectType,
 0124                        PrincipalSID = resolvedPrincipal.ObjectIdentifier,
 0125                        IsInherited = isInherited,
 0126                        RightName = EdgeNames.ManageCA
 0127                    });
 0128                if ((cARights & CertificationAuthorityRights.ManageCertificates) != 0)
 0129                    aces.Add(new ACE
 0130                    {
 0131                        PrincipalType = resolvedPrincipal.ObjectType,
 0132                        PrincipalSID = resolvedPrincipal.ObjectIdentifier,
 0133                        IsInherited = isInherited,
 0134                        RightName = EdgeNames.ManageCertificates
 0135                    });
 136
 0137                if ((cARights & CertificationAuthorityRights.Enroll) != 0)
 0138                    aces.Add(new ACE
 0139                    {
 0140                        PrincipalType = resolvedPrincipal.ObjectType,
 0141                        PrincipalSID = resolvedPrincipal.ObjectIdentifier,
 0142                        IsInherited = isInherited,
 0143                        RightName = EdgeNames.Enroll
 0144                    });
 0145            }
 146
 0147            data.Data = aces.ToArray();
 0148            return data;
 0149        }
 150
 151        /// <summary>
 152        /// This function should be called with the enrollment data fetched from <see cref="GetCARegistryValues"/>.
 153        /// The resulting items will contain enrollment agent restrictions
 154        /// </summary>
 155        /// <param name="enrollmentAgentRestrictions"></param>
 156        /// <returns></returns>
 157        public async Task<EnrollmentAgentRegistryAPIResult> ProcessEAPermissions(string caName, string objectDomain, str
 0158        {
 0159            var ret = new EnrollmentAgentRegistryAPIResult();
 0160            var regData = GetEnrollmentAgentRights(computerName, caName);
 161
 0162            ret.Collected = regData.Collected;
 0163            if (!ret.Collected)
 0164            {
 0165                ret.FailureReason = regData.FailureReason;
 0166                return ret;
 167            }
 168
 0169            if (regData.Value == null)
 0170            {
 0171                return ret;
 172            }
 173
 0174            var isDomainController = await _utils.IsDomainController(computerObjectId, objectDomain);
 0175            var machineSid = await GetMachineSid(computerName, computerObjectId);
 0176            var descriptor = new RawSecurityDescriptor(regData.Value as byte[], 0);
 0177            var enrollmentAgentRestrictions = new List<EnrollmentAgentRestriction>();
 0178            foreach (var genericAce in descriptor.DiscretionaryAcl)
 0179            {
 0180                var ace = (QualifiedAce)genericAce;
 0181                if (await CreateEnrollmentAgentRestriction(ace, objectDomain, computerName, isDomainController,
 0182                        computerObjectId, machineSid) is (true, var restriction)) {
 0183                    enrollmentAgentRestrictions.Add(restriction);
 0184                }
 0185            }
 186
 0187            ret.Restrictions = enrollmentAgentRestrictions.ToArray();
 188
 0189            return ret;
 0190        }
 191
 192        public async Task<(IEnumerable<TypedPrincipal> resolvedTemplates, IEnumerable<string> unresolvedTemplates)> Proc
 0193        {
 0194            var resolvedTemplates = new List<TypedPrincipal>();
 0195            var unresolvedTemplates = new List<string>();
 196
 0197            foreach (var templateCN in templates)
 0198            {
 0199                var res = await _utils.ResolveCertTemplateByProperty(Encoder.LdapFilterEncode(templateCN), LDAPPropertie
 0200                if (res.Success) {
 0201                    resolvedTemplates.Add(res.Principal);
 0202                } else {
 0203                    unresolvedTemplates.Add(templateCN);
 0204                }
 0205            }
 206
 0207            return (resolvedTemplates: resolvedTemplates, unresolvedTemplates: unresolvedTemplates);
 0208        }
 209
 210        /// <summary>
 211        /// Get CA security registry value from the remote machine for processing security/enrollmentagentrights
 212        /// </summary>
 213        /// <param name="target"></param>
 214        /// <param name="caName"></param>
 215        /// <returns></returns>
 216        [ExcludeFromCodeCoverage]
 217        private RegistryResult GetCASecurity(string target, string caName)
 218        {
 219            var regSubKey = $"SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\{caName}";
 220            const string regValue = "Security";
 221
 222            return Helpers.GetRegistryKeyData(target, regSubKey, regValue, _log);
 223        }
 224
 225        /// <summary>
 226        /// Get EnrollmentAgentRights registry value from the remote machine for processing security/enrollmentagentrigh
 227        /// </summary>
 228        /// <param name="target"></param>
 229        /// <param name="caName"></param>
 230        /// <returns></returns>
 231        [ExcludeFromCodeCoverage]
 232        private RegistryResult GetEnrollmentAgentRights(string target, string caName)
 233        {
 234            var regSubKey = $"SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\{caName}";
 235            var regValue = "EnrollmentAgentRights";
 236
 237            return Helpers.GetRegistryKeyData(target, regSubKey, regValue, _log);
 238        }
 239
 240        /// <summary>
 241        /// This function checks a registry setting on the target host for the specified CA to see if a requesting user 
 242        /// The ManageCA permission allows you to flip this bit as well. This appears to usually work, even if admin rig
 243        /// </summary>
 244        /// <remarks>https://blog.keyfactor.com/hidden-dangers-certificate-subject-alternative-names-sans</remarks>
 245        /// <param name="target"></param>
 246        /// <param name="caName"></param>
 247        /// <returns></returns>
 248        /// <exception cref="Exception"></exception>
 249        [ExcludeFromCodeCoverage]
 250        public BoolRegistryAPIResult IsUserSpecifiesSanEnabled(string target, string caName)
 251        {
 252            var ret = new BoolRegistryAPIResult();
 253            var subKey =
 254                $"SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\{caName}\\PolicyModules\\CertificateAutho
 255            const string subValue = "EditFlags";
 256            var data = Helpers.GetRegistryKeyData(target, subKey, subValue, _log);
 257
 258            ret.Collected = data.Collected;
 259            if (!data.Collected)
 260            {
 261                ret.FailureReason = data.FailureReason;
 262                return ret;
 263            }
 264
 265            if (data.Value == null)
 266            {
 267                return ret;
 268            }
 269
 270            var editFlags = (int)data.Value;
 271            ret.Value = (editFlags & 0x00040000) == 0x00040000;
 272
 273            return ret;
 274        }
 275
 276        /// <summary>
 277        /// This function checks a registry setting on the target host for the specified CA to see if role seperation is
 278        /// If enabled, you cannot perform any CA actions if you have both ManageCA and ManageCertificates permissions. 
 279        /// </summary>
 280        /// <remarks>https://www.itprotoday.com/security/q-how-can-i-make-sure-given-windows-account-assigned-only-singl
 281        /// <param name="target"></param>
 282        /// <param name="caName"></param>
 283        /// <returns></returns>
 284        /// <exception cref="Exception"></exception>
 285        [ExcludeFromCodeCoverage]
 286        public BoolRegistryAPIResult RoleSeparationEnabled(string target, string caName)
 287        {
 288            var ret = new BoolRegistryAPIResult();
 289            var regSubKey = $"SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\{caName}";
 290            const string regValue = "RoleSeparationEnabled";
 291            var data = Helpers.GetRegistryKeyData(target, regSubKey, regValue, _log);
 292
 293            ret.Collected = data.Collected;
 294            if (!data.Collected)
 295            {
 296                ret.FailureReason = data.FailureReason;
 297                return ret;
 298            }
 299
 300            if (data.Value == null)
 301            {
 302                return ret;
 303            }
 304
 305            ret.Value = (int)data.Value == 1;
 306
 307            return ret;
 308        }
 309
 310        public async Task<(bool Success, TypedPrincipal Principal)> GetRegistryPrincipal(SecurityIdentifier sid, string 
 0311        {
 0312            _log.LogTrace("Got principal with sid {SID} on computer {ComputerName}", sid.Value, computerName);
 313
 314            //Check if our sid is filtered
 0315            if (Helpers.IsSidFiltered(sid.Value))
 0316                return (false, default);
 317
 0318            if (isDomainController &&
 0319                await _utils.ResolveIDAndType(sid.Value, computerDomain) is (true, var resolvedPrincipal)) {
 0320                return (true, resolvedPrincipal);
 321            }
 322
 323            //If we get a local well known principal, we need to convert it using the computer's domain sid
 0324            if (await _utils.ConvertLocalWellKnownPrincipal(sid, computerObjectId, computerDomain) is
 0325                (true, var principal)) {
 0326                return (true, principal);
 327            }
 328
 329            //If the security identifier starts with the machine sid, we need to resolve it as a local principal
 0330            if (machineSid != null && sid.IsEqualDomainSid(machineSid))
 0331            {
 0332                _log.LogTrace("Got local principal {sid} on computer {Computer}", sid.Value, computerName);
 333
 334                // Set label to be local group. It could be a local user or alias but I'm not sure how we can confirm. B
 335                // The local group sid is computer machine sid - group rid.
 0336                var groupRid = sid.Rid();
 0337                var newSid = $"{computerObjectId}-{groupRid}";
 0338                return (true, new TypedPrincipal(newSid, Label.LocalGroup));
 339            }
 340
 341            //If we get here, we most likely have a domain principal. Do a lookup
 0342            return await _utils.ResolveIDAndType(sid.Value, computerDomain);
 0343        }
 344
 345        private async Task<SecurityIdentifier> GetMachineSid(string computerName, string computerObjectId)
 0346        {
 0347            SecurityIdentifier machineSid = null;
 348
 349            //Try to get the machine sid for the computer if its not already cached
 0350            if (!Cache.GetMachineSid(computerObjectId, out var tempMachineSid))
 0351            {
 352                // Open a handle to the server
 0353                var openServerResult = OpenSamServer(computerName);
 0354                if (openServerResult.IsFailed)
 0355                {
 0356                    _log.LogTrace("OpenServer failed on {ComputerName}: {Error}", computerName, openServerResult.SError)
 0357                    await SendComputerStatus(new CSVComputerStatus
 0358                    {
 0359                        Task = "SamConnect",
 0360                        ComputerName = computerName,
 0361                        Status = openServerResult.SError
 0362                    });
 0363                    return null;
 364                }
 365
 0366                var server = openServerResult.Value;
 0367                var getMachineSidResult = server.GetMachineSid();
 0368                if (getMachineSidResult.IsFailed)
 0369                {
 0370                    _log.LogTrace("GetMachineSid failed on {ComputerName}: {Error}", computerName, getMachineSidResult.S
 0371                    await SendComputerStatus(new CSVComputerStatus
 0372                    {
 0373                        Status = getMachineSidResult.SError,
 0374                        ComputerName = computerName,
 0375                        Task = "GetMachineSid"
 0376                    });
 377                    //If we can't get a machine sid, we wont be able to make local principals with unique object ids, or
 0378                    _log.LogWarning("Unable to get machineSid for {Computer}: {Status}", computerName, getMachineSidResu
 0379                    return null;
 380                }
 381
 0382                machineSid = getMachineSidResult.Value;
 0383                Cache.AddMachineSid(computerObjectId, machineSid.Value);
 0384            }
 385            else
 0386            {
 0387                machineSid = new SecurityIdentifier(tempMachineSid);
 0388            }
 389
 0390            return machineSid;
 0391        }
 392
 0393        private async Task<(bool success, EnrollmentAgentRestriction restriction)> CreateEnrollmentAgentRestriction(Qual
 0394            var targets = new List<TypedPrincipal>();
 0395            var index = 0;
 396
 0397            var accessType = ace.AceType.ToString();
 0398            var agent = await GetRegistryPrincipal(ace.SecurityIdentifier, computerDomain, computerName, isDomainControl
 0399                computerObjectId, machineSid);
 400
 0401            var opaque = ace.GetOpaque();
 0402            var sidCount = BitConverter.ToUInt32(opaque, 0);
 0403            index += 4;
 404
 0405            for (var i = 0; i < sidCount; i++) {
 0406                var sid = new SecurityIdentifier(opaque, index);
 0407                if (await GetRegistryPrincipal(sid, computerDomain, computerName, isDomainController, computerObjectId,
 0408                        machineSid) is (true, var regPrincipal)) {
 0409                    targets.Add(regPrincipal);
 0410                }
 411
 0412                index += sid.BinaryLength;
 0413            }
 414
 0415            var finalTargets = targets.ToArray();
 0416            var allTemplates = index >= opaque.Length;
 0417            if (index < opaque.Length) {
 0418                var template = Encoding.Unicode.GetString(opaque, index, opaque.Length - index - 2).Replace("\u0000", st
 0419                if (await _utils.ResolveCertTemplateByProperty(Encoder.LdapFilterEncode(template), LDAPProperties.Canoni
 0420                    return (true, new EnrollmentAgentRestriction {
 0421                        Template = resolvedTemplate,
 0422                        Agent = agent.Principal,
 0423                        AllTemplates = allTemplates,
 0424                        AccessType = accessType,
 0425                        Targets = finalTargets
 0426                    });
 427                }
 428
 0429                if (await _utils.ResolveCertTemplateByProperty(
 0430                        Encoder.LdapFilterEncode(template), LDAPProperties.CertTemplateOID, computerDomain) is
 0431                            (true, var resolvedOidTemplate)) {
 0432                    return (true, new EnrollmentAgentRestriction {
 0433                        Template = resolvedOidTemplate,
 0434                        Agent = agent.Principal,
 0435                        AllTemplates = allTemplates,
 0436                        AccessType = accessType,
 0437                        Targets = finalTargets
 0438                    });
 439                }
 0440            }
 441
 0442            return (false, default);
 0443        }
 444
 445        public virtual SharpHoundRPC.Result<ISAMServer> OpenSamServer(string computerName)
 0446        {
 0447            var result = SAMServer.OpenServer(computerName);
 0448            if (result.IsFailed)
 0449            {
 0450                return SharpHoundRPC.Result<ISAMServer>.Fail(result.SError);
 451            }
 452
 0453            return SharpHoundRPC.Result<ISAMServer>.Ok(result.Value);
 0454        }
 455
 456        private async Task SendComputerStatus(CSVComputerStatus status)
 0457        {
 0458            if (ComputerStatusEvent is not null) await ComputerStatusEvent(status);
 0459        }
 460
 461    }
 462
 463    public class EnrollmentAgentRestriction
 464    {
 465        public string AccessType { get; set; }
 466        public TypedPrincipal Agent { get; set; }
 467        public TypedPrincipal[] Targets { get; set; }
 468        public TypedPrincipal Template { get; set; }
 469        public bool AllTemplates { get; set; } = false;
 470    }
 471
 472    public class CertRegistryResult
 473    {
 474        public bool Collected { get; set; } = false;
 475        public byte[] Value { get; set; }
 476        public string FailureReason { get; set; }
 477    }
 478}