< Summary

Class:SharpHoundCommonLib.Processors.LocalGroupProcessor
Assembly:SharpHoundCommonLib
File(s):D:\a\SharpHoundCommon\SharpHoundCommon\src\CommonLib\Processors\LocalGroupProcessor.cs
Covered lines:145
Uncovered lines:114
Coverable lines:259
Total lines:387
Line coverage:55.9% (145 of 259)
Covered branches:95
Total branches:149
Branch coverage:63.7% (95 of 149)

Metrics

MethodBranch coverage Cyclomatic complexity NPath complexity Sequence coverage
.ctor(...)100%20100%
OpenSamServer(...)0%200%
GetLocalGroups(...)100%100%
GetLocalGroups()66.38%119051.85%
ResolveDomainControllerPrincipal(...)100%20100%
ConvertLocalWellKnownPrincipal(...)58.33%12076.19%
ResolveGroupName(...)50%8069.23%
SendComputerStatus()25%40100%

File(s)

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

#LineLine coverage
 1using System;
 2using System.Collections.Concurrent;
 3using System.Collections.Generic;
 4using System.Security.Principal;
 5using System.Threading.Tasks;
 6using Microsoft.Extensions.Logging;
 7using SharpHoundCommonLib.Enums;
 8using SharpHoundCommonLib.OutputTypes;
 9using SharpHoundRPC;
 10using SharpHoundRPC.Shared;
 11using SharpHoundRPC.Wrappers;
 12
 13namespace SharpHoundCommonLib.Processors
 14{
 15    public class LocalGroupProcessor
 16    {
 17        public delegate Task ComputerStatusDelegate(CSVComputerStatus status);
 18        private readonly ILogger _log;
 19        private readonly ILDAPUtils _utils;
 20
 421        public LocalGroupProcessor(ILDAPUtils utils, ILogger log = null)
 422        {
 423            _utils = utils;
 424            _log = log ?? Logging.LogProvider.CreateLogger("LocalGroupProcessor");
 425        }
 26
 27        public event ComputerStatusDelegate ComputerStatusEvent;
 28
 29        public virtual Result<ISAMServer> OpenSamServer(string computerName)
 030        {
 031            var result = SAMServer.OpenServer(computerName);
 032            if (result.IsFailed)
 033            {
 034                return Result<ISAMServer>.Fail(result.SError);
 35            }
 36
 037            return Result<ISAMServer>.Ok(result.Value);
 038        }
 39
 40        public IAsyncEnumerable<LocalGroupAPIResult> GetLocalGroups(ResolvedSearchResult result)
 041        {
 042            return GetLocalGroups(result.DisplayName, result.ObjectId, result.Domain, result.IsDomainController);
 043        }
 44
 45        /// <summary>
 46        ///     Gets local groups from a computer
 47        /// </summary>
 48        /// <param name="computerName"></param>
 49        /// <param name="computerObjectId">The objectsid of the computer in the domain</param>
 50        /// <param name="computerDomain">The domain the computer belongs too</param>
 51        /// <param name="isDomainController">Is the computer a domain controller</param>
 52        /// <returns></returns>
 53        public async IAsyncEnumerable<LocalGroupAPIResult> GetLocalGroups(string computerName, string computerObjectId,
 54            string computerDomain, bool isDomainController)
 255        {
 56            //Open a handle to the server
 257            var openServerResult = OpenSamServer(computerName);
 258            if (openServerResult.IsFailed)
 059            {
 060                _log.LogTrace("OpenServer failed on {ComputerName}: {Error}", computerName, openServerResult.SError);
 061                await SendComputerStatus(new CSVComputerStatus
 062                {
 063                    Task = "SamConnect",
 064                    ComputerName = computerName,
 065                    Status = openServerResult.SError
 066                });
 067                yield break;
 68            }
 69
 270            var server = openServerResult.Value;
 271            var typeCache = new ConcurrentDictionary<string, CachedLocalItem>();
 72
 73            //Try to get the machine sid for the computer if its not already cached
 74            SecurityIdentifier machineSid;
 275            if (!Cache.GetMachineSid(computerObjectId, out var tempMachineSid))
 276            {
 277                var getMachineSidResult = server.GetMachineSid();
 278                if (getMachineSidResult.IsFailed)
 079                {
 080                    _log.LogTrace("GetMachineSid failed on {ComputerName}: {Error}", computerName, getMachineSidResult.S
 081                    await SendComputerStatus(new CSVComputerStatus
 082                    {
 083                        Status = getMachineSidResult.SError,
 084                        ComputerName = computerName,
 085                        Task = "GetMachineSid"
 086                    });
 87                    //If we can't get a machine sid, we wont be able to make local principals with unique object ids, or
 088                    _log.LogWarning("Unable to get machineSid for {Computer}: {Status}. Abandoning local group processin
 089                    yield break;
 90                }
 91
 292                machineSid = getMachineSidResult.Value;
 293                Cache.AddMachineSid(computerObjectId, machineSid.Value);
 294            }
 95            else
 096            {
 097                machineSid = new SecurityIdentifier(tempMachineSid);
 098            }
 99
 100            //Get all available domains in the server
 2101            var getDomainsResult = server.GetDomains();
 2102            if (getDomainsResult.IsFailed)
 0103            {
 0104                _log.LogTrace("GetDomains failed on {ComputerName}: {Error}", computerName, getDomainsResult.SError);
 0105                await SendComputerStatus(new CSVComputerStatus
 0106                {
 0107                    Task = "GetDomains",
 0108                    ComputerName = computerName,
 0109                    Status = getDomainsResult.SError
 0110                });
 0111                yield break;
 112            }
 113
 114            //Loop over each domain result and process its member groups
 12115            foreach (var domainResult in getDomainsResult.Value)
 3116            {
 117                //Skip non-builtin domains on domain controllers
 3118                if (isDomainController && !domainResult.Name.Equals("builtin", StringComparison.OrdinalIgnoreCase))
 0119                    continue;
 120
 121                //Open a handle to the domain
 3122                var openDomainResult = server.OpenDomain(domainResult.Name);
 3123                if (openDomainResult.IsFailed)
 0124                {
 0125                    _log.LogTrace("Failed to open domain {Domain} on {ComputerName}: {Error}", domainResult.Name, comput
 0126                    await SendComputerStatus(new CSVComputerStatus
 0127                    {
 0128                        Task = $"OpenDomain - {domainResult.Name}",
 0129                        ComputerName = computerName,
 0130                        Status = openDomainResult.SError
 0131                    });
 0132                    continue;
 133                }
 134
 3135                var domain = openDomainResult.Value;
 136
 137                //Open a handle to the available aliases
 3138                var getAliasesResult = domain.GetAliases();
 139
 3140                if (getAliasesResult.IsFailed)
 0141                {
 0142                    _log.LogTrace("Failed to open Aliases on Domain {Domain} on on {ComputerName}: {Error}", domainResul
 0143                    await SendComputerStatus(new CSVComputerStatus
 0144                    {
 0145                        Task = $"GetAliases - {domainResult.Name}",
 0146                        ComputerName = computerName,
 0147                        Status = getAliasesResult.SError
 0148                    });
 0149                    continue;
 150                }
 151
 19152                foreach (var alias in getAliasesResult.Value)
 5153                {
 5154                    _log.LogTrace("Opening alias {Alias} with RID {Rid} in domain {Domain} on computer {ComputerName}", 
 155                    //Try and resolve the group name using several different criteria
 5156                    var resolvedName = ResolveGroupName(alias.Name, computerName, computerObjectId, computerDomain, alia
 5157                        isDomainController,
 5158                        domainResult.Name.Equals("builtin", StringComparison.OrdinalIgnoreCase));
 159
 5160                    var ret = new LocalGroupAPIResult
 5161                    {
 5162                        Name = resolvedName.PrincipalName,
 5163                        ObjectIdentifier = resolvedName.ObjectId
 5164                    };
 165
 166                    //Open a handle to the alias
 5167                    var openAliasResult = domain.OpenAlias(alias.Rid);
 5168                    if (openAliasResult.IsFailed)
 0169                    {
 0170                        _log.LogTrace("Failed to open alias {Alias} with RID {Rid} in domain {Domain} on computer {Compu
 0171                        await SendComputerStatus(new CSVComputerStatus
 0172                        {
 0173                            Task = $"OpenAlias - {alias.Name}",
 0174                            ComputerName = computerName,
 0175                            Status = openAliasResult.SError
 0176                        });
 0177                        ret.Collected = false;
 0178                        ret.FailureReason = $"SamOpenAliasInDomain failed with status {openAliasResult.SError}";
 0179                        yield return ret;
 0180                        continue;
 181                    }
 182
 5183                    var localGroup = openAliasResult.Value;
 184                    //Call GetMembersInAlias to get raw group members
 5185                    var getMembersResult = localGroup.GetMembers();
 5186                    if (getMembersResult.IsFailed)
 0187                    {
 0188                        _log.LogTrace("Failed to get members in alias {Alias} with RID {Rid} in domain {Domain} on compu
 0189                        await SendComputerStatus(new CSVComputerStatus
 0190                        {
 0191                            Task = $"GetMembersInAlias - {alias.Name}",
 0192                            ComputerName = computerName,
 0193                            Status = getMembersResult.SError
 0194                        });
 0195                        ret.Collected = false;
 0196                        ret.FailureReason = $"SamGetMembersInAlias failed with status {getMembersResult.SError}";
 0197                        yield return ret;
 0198                        continue;
 199                    }
 200
 5201                    await SendComputerStatus(new CSVComputerStatus
 5202                    {
 5203                        Task = $"GetMembersInAlias - {alias.Name}",
 5204                        ComputerName = computerName,
 5205                        Status = CSVComputerStatus.StatusSuccess
 5206                    });
 207
 5208                    var results = new List<TypedPrincipal>();
 5209                    var names = new List<NamedPrincipal>();
 210
 37211                    foreach (var securityIdentifier in getMembersResult.Value)
 11212                    {
 11213                        _log.LogTrace("Got member sid {Sid} in alias {Alias} with RID {Rid} in domain {Domain} on comput
 214                        //Check if the sid is one of our filtered ones. Throw it out if it is
 11215                        if (Helpers.IsSidFiltered(securityIdentifier.Value))
 1216                            continue;
 217
 10218                        var sidValue = securityIdentifier.Value;
 219
 10220                        if (isDomainController)
 5221                        {
 5222                            var result = ResolveDomainControllerPrincipal(sidValue, computerDomain);
 7223                            if (result != null) results.Add(result);
 5224                            continue;
 225                        }
 226
 227                        //If we get a local well known principal, we need to convert it using the computer's objectid
 5228                        if (ConvertLocalWellKnownPrincipal(securityIdentifier, computerObjectId, computerDomain, out var
 1229                        {
 230                            //If the principal is null, it means we hit a weird edge case, but this is a local well know
 1231                            if (principal != null)
 1232                                results.Add(principal);
 1233                            continue;
 234                        }
 235
 236                        //If the security identifier starts with the machine sid, we need to resolve it as a local objec
 4237                        if (securityIdentifier.IsEqualDomainSid(machineSid))
 3238                        {
 239                            //Check if we've already previously resolved and cached this sid
 3240                            if (typeCache.TryGetValue(sidValue, out var cachedLocalItem))
 0241                            {
 0242                                results.Add(new TypedPrincipal
 0243                                {
 0244                                    ObjectIdentifier = sidValue,
 0245                                    ObjectType = cachedLocalItem.Type
 0246                                });
 247
 0248                                names.Add(new NamedPrincipal
 0249                                {
 0250                                    ObjectId = sidValue,
 0251                                    PrincipalName = cachedLocalItem.Name
 0252                                });
 253                                //Move on
 0254                                continue;
 255                            }
 256
 257                            //Attempt to lookup the principal in the server directly
 3258                            var lookupUserResult = server.LookupPrincipalBySid(securityIdentifier);
 3259                            if (lookupUserResult.IsFailed)
 0260                            {
 0261                                _log.LogTrace("Unable to resolve local sid {SID}: {Error}", sidValue, lookupUserResult.S
 0262                                continue;
 263                            }
 264
 3265                            var (name, use) = lookupUserResult.Value;
 3266                            var objectType = use switch
 3267                            {
 2268                                SharedEnums.SidNameUse.User => Label.LocalUser,
 0269                                SharedEnums.SidNameUse.Group => Label.LocalGroup,
 1270                                SharedEnums.SidNameUse.Alias => Label.LocalGroup,
 0271                                _ => Label.Base
 3272                            };
 273
 274                            // Cache whatever we looked up for future lookups
 3275                            typeCache.TryAdd(sidValue, new CachedLocalItem(name, objectType));
 276
 277                            // Throw out local users
 3278                            if (objectType == Label.LocalUser)
 2279                                continue;
 280
 1281                            var newSid = $"{computerObjectId}-{securityIdentifier.Rid()}";
 282
 1283                            results.Add(new TypedPrincipal
 1284                            {
 1285                                ObjectIdentifier = newSid,
 1286                                ObjectType = objectType
 1287                            });
 288
 1289                            names.Add(new NamedPrincipal
 1290                            {
 1291                                PrincipalName = name,
 1292                                ObjectId = newSid
 1293                            });
 1294                            continue;
 295                        }
 296
 297                        //If we get here, we most likely have a domain principal in a local group
 1298                        var resolvedPrincipal = _utils.ResolveIDAndType(sidValue, computerDomain);
 2299                        if (resolvedPrincipal != null) results.Add(resolvedPrincipal);
 1300                    }
 301
 5302                    ret.Collected = true;
 5303                    ret.LocalNames = names.ToArray();
 5304                    ret.Results = results.ToArray();
 5305                    yield return ret;
 5306                }
 3307            }
 2308        }
 309
 310        private TypedPrincipal ResolveDomainControllerPrincipal(string sid, string computerDomain)
 5311        {
 312            //If the server is a domain controller and we have a well known group, use the domain value
 5313            if (_utils.GetWellKnownPrincipal(sid, computerDomain, out var wellKnown))
 1314                return wellKnown;
 4315            return _utils.ResolveIDAndType(sid, computerDomain);
 5316        }
 317
 318        private bool ConvertLocalWellKnownPrincipal(SecurityIdentifier sid, string computerObjectId, string computerDoma
 5319        {
 5320            if (WellKnownPrincipal.GetWellKnownPrincipal(sid.Value, out var common))
 1321            {
 1322                if (sid.Value is "S-1-1-0" or "S-1-5-11")
 0323                {
 0324                    _utils.GetWellKnownPrincipal(sid.Value, computerDomain, out principal);
 0325                    return true;
 326                }
 327
 1328                principal = new TypedPrincipal
 1329                {
 1330                    ObjectIdentifier = $"{computerObjectId}-{sid.Rid()}",
 1331                    ObjectType = common.ObjectType switch
 1332                    {
 0333                        Label.User => Label.LocalUser,
 1334                        Label.Group => Label.LocalGroup,
 0335                        _ => common.ObjectType
 1336                    }
 1337                };
 338
 1339                return true;
 340            }
 341
 4342            principal = null;
 4343            return false;
 5344        }
 345
 346        private NamedPrincipal ResolveGroupName(string baseName, string computerName, string computerDomainSid,
 347            string domainName, int groupRid, bool isDc, bool isBuiltIn)
 7348        {
 7349            if (isDc)
 3350            {
 3351                if (isBuiltIn)
 3352                {
 353                    //If this is the builtin group on the DC, the groups correspond to the domain well known groups
 3354                    _utils.GetWellKnownPrincipal($"S-1-5-32-{groupRid}".ToUpper(), domainName, out var principal);
 3355                    return new NamedPrincipal
 3356                    {
 3357                        ObjectId = principal.ObjectIdentifier,
 3358                        PrincipalName = "IGNOREME"
 3359                    };
 360                }
 361
 0362                if (computerDomainSid == null)
 0363                    return null;
 364                //We shouldn't hit this provided our isDC logic is correct since we're skipping non-builtin groups
 0365                return new NamedPrincipal
 0366                {
 0367                    ObjectId = $"{computerDomainSid}-{groupRid}".ToUpper(),
 0368                    PrincipalName = "IGNOREME"
 0369                };
 370            }
 371
 4372            if (computerDomainSid == null)
 0373                return null;
 374            //Take the local machineSid, append the groupRid, and make a name from the group name + computername
 4375            return new NamedPrincipal
 4376            {
 4377                ObjectId = $"{computerDomainSid}-{groupRid}",
 4378                PrincipalName = $"{baseName}@{computerName}".ToUpper()
 4379            };
 7380        }
 381
 382        private async Task SendComputerStatus(CSVComputerStatus status)
 5383        {
 5384            if (ComputerStatusEvent is not null) await ComputerStatusEvent(status);
 5385        }
 386    }
 387}