< Summary

Class:SharpHoundCommonLib.Processors.EnrollmentAgentRestriction
Assembly:SharpHoundCommonLib
File(s):D:\a\SharpHoundCommon\SharpHoundCommon\src\CommonLib\Processors\CertAbuseProcessor.cs
Covered lines:0
Uncovered lines:5
Coverable lines:5
Total lines:478
Line coverage:0% (0 of 5)
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        private 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 isDomainController = await _utils.IsDomainController(computerObjectId, objectDomain);
 61            var machineSid = await GetMachineSid(computerName, computerObjectId);
 62
 63            var aces = new List<ACE>();
 64
 65            if (ownerSid != null) {
 66                var processed = new SecurityIdentifier(ownerSid);
 67                if (await GetRegistryPrincipal(processed, objectDomain, computerName,
 68                        isDomainController, computerObjectId, machineSid) is (true, var resolvedOwner)) {
 69                    aces.Add(new ACE
 70                    {
 71                        PrincipalType = resolvedOwner.ObjectType,
 72                        PrincipalSID = resolvedOwner.ObjectIdentifier,
 73                        RightName = EdgeNames.Owns,
 74                        IsInherited = false
 75                    });
 76                } else {
 77                    aces.Add(new ACE
 78                    {
 79                        PrincipalType = Label.Base,
 80                        PrincipalSID = processed.Value,
 81                        RightName = EdgeNames.Owns,
 82                        IsInherited = false
 83                    });
 84                }
 85            }
 86            else
 87            {
 88                _log.LogDebug("Owner on CA {Name} is null", computerName);
 89            }
 90
 91            foreach (var rule in descriptor.GetAccessRules(true, true, typeof(SecurityIdentifier)))
 92            {
 93                if (rule == null)
 94                    continue;
 95
 96                if (rule.AccessControlType() == AccessControlType.Deny)
 97                    continue;
 98
 99                var principalSid = Helpers.PreProcessSID(rule.IdentityReference());
 100                if (principalSid == null)
 101                    continue;
 102
 103                var (getDomainSuccess, principalDomain) = await _utils.GetDomainNameFromSid(principalSid);
 104                if (!getDomainSuccess) {
 105                    //Fallback to computer's domain in case we cant resolve the principal domain
 106                    principalDomain = objectDomain;
 107                }
 108                var (resSuccess, resolvedPrincipal) = await GetRegistryPrincipal(new SecurityIdentifier(principalSid), p
 109                if (!resSuccess) {
 110                    resolvedPrincipal = new TypedPrincipal {
 111                        ObjectType = Label.Base,
 112                        ObjectIdentifier = principalSid
 113                    };
 114                }
 115                var isInherited = rule.IsInherited();
 116
 117                var cARights = (CertificationAuthorityRights)rule.ActiveDirectoryRights();
 118
 119                // TODO: These if statements are also present in ProcessACL. Move to shared location.
 120                if ((cARights & CertificationAuthorityRights.ManageCA) != 0)
 121                    aces.Add(new ACE
 122                    {
 123                        PrincipalType = resolvedPrincipal.ObjectType,
 124                        PrincipalSID = resolvedPrincipal.ObjectIdentifier,
 125                        IsInherited = isInherited,
 126                        RightName = EdgeNames.ManageCA
 127                    });
 128                if ((cARights & CertificationAuthorityRights.ManageCertificates) != 0)
 129                    aces.Add(new ACE
 130                    {
 131                        PrincipalType = resolvedPrincipal.ObjectType,
 132                        PrincipalSID = resolvedPrincipal.ObjectIdentifier,
 133                        IsInherited = isInherited,
 134                        RightName = EdgeNames.ManageCertificates
 135                    });
 136
 137                if ((cARights & CertificationAuthorityRights.Enroll) != 0)
 138                    aces.Add(new ACE
 139                    {
 140                        PrincipalType = resolvedPrincipal.ObjectType,
 141                        PrincipalSID = resolvedPrincipal.ObjectIdentifier,
 142                        IsInherited = isInherited,
 143                        RightName = EdgeNames.Enroll
 144                    });
 145            }
 146
 147            data.Data = aces.ToArray();
 148            return data;
 149        }
 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
 158        {
 159            var ret = new EnrollmentAgentRegistryAPIResult();
 160            var regData = GetEnrollmentAgentRights(computerName, caName);
 161
 162            ret.Collected = regData.Collected;
 163            if (!ret.Collected)
 164            {
 165                ret.FailureReason = regData.FailureReason;
 166                return ret;
 167            }
 168
 169            if (regData.Value == null)
 170            {
 171                return ret;
 172            }
 173
 174            var isDomainController = await _utils.IsDomainController(computerObjectId, objectDomain);
 175            var machineSid = await GetMachineSid(computerName, computerObjectId);
 176            var descriptor = new RawSecurityDescriptor(regData.Value as byte[], 0);
 177            var enrollmentAgentRestrictions = new List<EnrollmentAgentRestriction>();
 178            foreach (var genericAce in descriptor.DiscretionaryAcl)
 179            {
 180                var ace = (QualifiedAce)genericAce;
 181                if (await CreateEnrollmentAgentRestriction(ace, objectDomain, computerName, isDomainController,
 182                        computerObjectId, machineSid) is (true, var restriction)) {
 183                    enrollmentAgentRestrictions.Add(restriction);
 184                }
 185            }
 186
 187            ret.Restrictions = enrollmentAgentRestrictions.ToArray();
 188
 189            return ret;
 190        }
 191
 192        public async Task<(IEnumerable<TypedPrincipal> resolvedTemplates, IEnumerable<string> unresolvedTemplates)> Proc
 193        {
 194            var resolvedTemplates = new List<TypedPrincipal>();
 195            var unresolvedTemplates = new List<string>();
 196
 197            foreach (var templateCN in templates)
 198            {
 199                var res = await _utils.ResolveCertTemplateByProperty(Encoder.LdapFilterEncode(templateCN), LDAPPropertie
 200                if (res.Success) {
 201                    resolvedTemplates.Add(res.Principal);
 202                } else {
 203                    unresolvedTemplates.Add(templateCN);
 204                }
 205            }
 206
 207            return (resolvedTemplates: resolvedTemplates, unresolvedTemplates: unresolvedTemplates);
 208        }
 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 
 311        {
 312            _log.LogTrace("Got principal with sid {SID} on computer {ComputerName}", sid.Value, computerName);
 313
 314            //Check if our sid is filtered
 315            if (Helpers.IsSidFiltered(sid.Value))
 316                return (false, default);
 317
 318            if (isDomainController &&
 319                await _utils.ResolveIDAndType(sid.Value, computerDomain) is (true, var resolvedPrincipal)) {
 320                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
 324            if (await _utils.ConvertLocalWellKnownPrincipal(sid, computerObjectId, computerDomain) is
 325                (true, var principal)) {
 326                return (true, principal);
 327            }
 328
 329            //If the security identifier starts with the machine sid, we need to resolve it as a local principal
 330            if (machineSid != null && sid.IsEqualDomainSid(machineSid))
 331            {
 332                _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.
 336                var groupRid = sid.Rid();
 337                var newSid = $"{computerObjectId}-{groupRid}";
 338                return (true, new TypedPrincipal(newSid, Label.LocalGroup));
 339            }
 340
 341            //If we get here, we most likely have a domain principal. Do a lookup
 342            return await _utils.ResolveIDAndType(sid.Value, computerDomain);
 343        }
 344
 345        private async Task<SecurityIdentifier> GetMachineSid(string computerName, string computerObjectId)
 346        {
 347            SecurityIdentifier machineSid = null;
 348
 349            //Try to get the machine sid for the computer if its not already cached
 350            if (!Cache.GetMachineSid(computerObjectId, out var tempMachineSid))
 351            {
 352                // Open a handle to the server
 353                var openServerResult = OpenSamServer(computerName);
 354                if (openServerResult.IsFailed)
 355                {
 356                    _log.LogTrace("OpenServer failed on {ComputerName}: {Error}", computerName, openServerResult.SError)
 357                    await SendComputerStatus(new CSVComputerStatus
 358                    {
 359                        Task = "SamConnect",
 360                        ComputerName = computerName,
 361                        Status = openServerResult.SError
 362                    });
 363                    return null;
 364                }
 365
 366                var server = openServerResult.Value;
 367                var getMachineSidResult = server.GetMachineSid();
 368                if (getMachineSidResult.IsFailed)
 369                {
 370                    _log.LogTrace("GetMachineSid failed on {ComputerName}: {Error}", computerName, getMachineSidResult.S
 371                    await SendComputerStatus(new CSVComputerStatus
 372                    {
 373                        Status = getMachineSidResult.SError,
 374                        ComputerName = computerName,
 375                        Task = "GetMachineSid"
 376                    });
 377                    //If we can't get a machine sid, we wont be able to make local principals with unique object ids, or
 378                    _log.LogWarning("Unable to get machineSid for {Computer}: {Status}", computerName, getMachineSidResu
 379                    return null;
 380                }
 381
 382                machineSid = getMachineSidResult.Value;
 383                Cache.AddMachineSid(computerObjectId, machineSid.Value);
 384            }
 385            else
 386            {
 387                machineSid = new SecurityIdentifier(tempMachineSid);
 388            }
 389
 390            return machineSid;
 391        }
 392
 393        private async Task<(bool success, EnrollmentAgentRestriction restriction)> CreateEnrollmentAgentRestriction(Qual
 394            var targets = new List<TypedPrincipal>();
 395            var index = 0;
 396
 397            var accessType = ace.AceType.ToString();
 398            var agent = await GetRegistryPrincipal(ace.SecurityIdentifier, computerDomain, computerName, isDomainControl
 399                computerObjectId, machineSid);
 400
 401            var opaque = ace.GetOpaque();
 402            var sidCount = BitConverter.ToUInt32(opaque, 0);
 403            index += 4;
 404
 405            for (var i = 0; i < sidCount; i++) {
 406                var sid = new SecurityIdentifier(opaque, index);
 407                if (await GetRegistryPrincipal(sid, computerDomain, computerName, isDomainController, computerObjectId,
 408                        machineSid) is (true, var regPrincipal)) {
 409                    targets.Add(regPrincipal);
 410                }
 411
 412                index += sid.BinaryLength;
 413            }
 414
 415            var finalTargets = targets.ToArray();
 416            var allTemplates = index >= opaque.Length;
 417            if (index < opaque.Length) {
 418                var template = Encoding.Unicode.GetString(opaque, index, opaque.Length - index - 2).Replace("\u0000", st
 419                if (await _utils.ResolveCertTemplateByProperty(Encoder.LdapFilterEncode(template), LDAPProperties.Canoni
 420                    return (true, new EnrollmentAgentRestriction {
 421                        Template = resolvedTemplate,
 422                        Agent = agent.Principal,
 423                        AllTemplates = allTemplates,
 424                        AccessType = accessType,
 425                        Targets = finalTargets
 426                    });
 427                }
 428
 429                if (await _utils.ResolveCertTemplateByProperty(
 430                        Encoder.LdapFilterEncode(template), LDAPProperties.CertTemplateOID, computerDomain) is
 431                            (true, var resolvedOidTemplate)) {
 432                    return (true, new EnrollmentAgentRestriction {
 433                        Template = resolvedOidTemplate,
 434                        Agent = agent.Principal,
 435                        AllTemplates = allTemplates,
 436                        AccessType = accessType,
 437                        Targets = finalTargets
 438                    });
 439                }
 440            }
 441
 442            return (false, default);
 443        }
 444
 445        public virtual SharpHoundRPC.Result<ISAMServer> OpenSamServer(string computerName)
 446        {
 447            var result = SAMServer.OpenServer(computerName);
 448            if (result.IsFailed)
 449            {
 450                return SharpHoundRPC.Result<ISAMServer>.Fail(result.SError);
 451            }
 452
 453            return SharpHoundRPC.Result<ISAMServer>.Ok(result.Value);
 454        }
 455
 456        private async Task SendComputerStatus(CSVComputerStatus status)
 457        {
 458            if (ComputerStatusEvent is not null) await ComputerStatusEvent(status);
 459        }
 460
 461    }
 462
 463    public class EnrollmentAgentRestriction
 464    {
 0465        public string AccessType { get; set; }
 0466        public TypedPrincipal Agent { get; set; }
 0467        public TypedPrincipal[] Targets { get; set; }
 0468        public TypedPrincipal Template { get; set; }
 0469        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}