< Summary

Class:SharpHoundCommonLib.Processors.LocalGroupProcessor
Assembly:SharpHoundCommonLib
File(s):D:\a\SharpHoundCommon\SharpHoundCommon\src\CommonLib\Processors\LocalGroupProcessor.cs
Covered lines:139
Uncovered lines:109
Coverable lines:248
Total lines:375
Line coverage:56% (139 of 248)
Covered branches:149
Total branches:213
Branch coverage:69.9% (149 of 213)

Metrics

MethodBranch coverage Cyclomatic complexity NPath complexity Sequence coverage
.ctor(...)100%20100%
OpenSamServer(...)0%200%
GetLocalGroups(...)100%100%
GetLocalGroups()71.58%183052.87%
ResolveDomainControllerPrincipal()75%80100%
ResolveGroupName()50%14066.66%
SendComputerStatus()75%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
 521        public LocalGroupProcessor(ILdapUtils utils, ILogger log = null)
 522        {
 523            _utils = utils;
 524            _log = log ?? Logging.LogProvider.CreateLogger("LocalGroupProcessor");
 525        }
 26
 27        public event ComputerStatusDelegate ComputerStatusEvent;
 28
 29        public virtual SharpHoundRPC.Result<ISAMServer> OpenSamServer(string computerName)
 030        {
 031            var result = SAMServer.OpenServer(computerName);
 032            if (result.IsFailed)
 033            {
 034                return SharpHoundRPC.Result<ISAMServer>.Fail(result.SError);
 35            }
 36
 037            return SharpHoundRPC.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        /// <param name="timeout"></param>
 53        /// <returns></returns>
 54        public async IAsyncEnumerable<LocalGroupAPIResult> GetLocalGroups(string computerName, string computerObjectId,
 55            string computerDomain, bool isDomainController, TimeSpan timeout = default)
 356        {
 557            if (timeout == default) {
 258                timeout = TimeSpan.FromMinutes(2);
 259            }
 60
 61            //Open a handle to the server
 662            var openServerResult = await Task.Run(() => OpenSamServer(computerName)).TimeoutAfter(timeout);
 363            if (openServerResult.IsFailed)
 164            {
 165                _log.LogTrace("OpenServer failed on {ComputerName}: {Error}", computerName, openServerResult.SError);
 166                await SendComputerStatus(new CSVComputerStatus
 167                {
 168                    Task = "SamConnect",
 169                    ComputerName = computerName,
 170                    Status = openServerResult.SError
 171                });
 172                yield break;
 73            }
 74
 275            var server = openServerResult.Value;
 276            var typeCache = new ConcurrentDictionary<string, CachedLocalItem>();
 77
 78            //Try to get the machine sid for the computer if its not already cached
 79            SecurityIdentifier machineSid;
 480            if (!Cache.GetMachineSid(computerObjectId, out var tempMachineSid)) {
 481                var getMachineSidResult = await Task.Run(() => server.GetMachineSid()).TimeoutAfter(timeout);
 282                if (getMachineSidResult.IsFailed)
 083                {
 084                    _log.LogTrace("GetMachineSid failed on {ComputerName}: {Error}", computerName, getMachineSidResult.S
 085                    await SendComputerStatus(new CSVComputerStatus
 086                    {
 087                        Status = getMachineSidResult.SError,
 088                        ComputerName = computerName,
 089                        Task = "GetMachineSid"
 090                    });
 91                    //If we can't get a machine sid, we wont be able to make local principals with unique object ids, or
 092                    _log.LogWarning("Unable to get machineSid for {Computer}: {Status}. Abandoning local group processin
 093                    yield break;
 94                }
 95
 296                machineSid = getMachineSidResult.Value;
 297                Cache.AddMachineSid(computerObjectId, machineSid.Value);
 298            }
 99            else
 0100            {
 0101                machineSid = new SecurityIdentifier(tempMachineSid);
 0102            }
 103
 104            //Get all available domains in the server
 4105            var getDomainsResult = await Task.Run(() => server.GetDomains()).TimeoutAfter(timeout);
 2106            if (getDomainsResult.IsFailed)
 0107            {
 0108                _log.LogTrace("GetDomains failed on {ComputerName}: {Error}", computerName, getDomainsResult.SError);
 0109                await SendComputerStatus(new CSVComputerStatus
 0110                {
 0111                    Task = "GetDomains",
 0112                    ComputerName = computerName,
 0113                    Status = getDomainsResult.SError
 0114                });
 0115                yield break;
 116            }
 117
 118            //Loop over each domain result and process its member groups
 12119            foreach (var domainResult in getDomainsResult.Value)
 3120            {
 121                //Skip non-builtin domains on domain controllers
 3122                if (isDomainController && !domainResult.Name.Equals("builtin", StringComparison.OrdinalIgnoreCase))
 0123                    continue;
 124
 125                //Open a handle to the domain
 6126                var openDomainResult = await Task.Run(() => server.OpenDomain(domainResult.Name)).TimeoutAfter(timeout);
 3127                if (openDomainResult.IsFailed)
 0128                {
 0129                    _log.LogTrace("Failed to open domain {Domain} on {ComputerName}: {Error}", domainResult.Name, comput
 0130                    await SendComputerStatus(new CSVComputerStatus
 0131                    {
 0132                        Task = $"OpenDomain - {domainResult.Name}",
 0133                        ComputerName = computerName,
 0134                        Status = openDomainResult.SError
 0135                    });
 0136                    if (openDomainResult.IsTimeout) {
 0137                        yield break;
 138                    }
 0139                    continue;
 140                }
 141
 3142                var domain = openDomainResult.Value;
 143
 144                //Open a handle to the available aliases
 6145                var getAliasesResult = await Task.Run(() => domain.GetAliases()).TimeoutAfter(timeout);
 146
 3147                if (getAliasesResult.IsFailed)
 0148                {
 0149                    _log.LogTrace("Failed to open Aliases on Domain {Domain} on on {ComputerName}: {Error}", domainResul
 0150                    await SendComputerStatus(new CSVComputerStatus
 0151                    {
 0152                        Task = $"GetAliases - {domainResult.Name}",
 0153                        ComputerName = computerName,
 0154                        Status = getAliasesResult.SError
 0155                    });
 156
 0157                    if (getAliasesResult.IsTimeout) {
 0158                        yield break;
 159                    }
 0160                    continue;
 161                }
 162
 19163                foreach (var alias in getAliasesResult.Value)
 5164                {
 5165                    _log.LogTrace("Opening alias {Alias} with RID {Rid} in domain {Domain} on computer {ComputerName}", 
 166                    //Try and resolve the group name using several different criteria
 5167                    var resolvedName = await ResolveGroupName(alias.Name, computerName, computerObjectId, computerDomain
 5168                        isDomainController,
 5169                        domainResult.Name.Equals("builtin", StringComparison.OrdinalIgnoreCase));
 170
 5171                    var ret = new LocalGroupAPIResult
 5172                    {
 5173                        Name = resolvedName.PrincipalName,
 5174                        ObjectIdentifier = resolvedName.ObjectId
 5175                    };
 176
 177                    //Open a handle to the alias
 10178                    var openAliasResult = await Task.Run(() => domain.OpenAlias(alias.Rid)).TimeoutAfter(timeout);
 5179                    if (openAliasResult.IsFailed)
 0180                    {
 0181                        _log.LogTrace("Failed to open alias {Alias} with RID {Rid} in domain {Domain} on computer {Compu
 0182                        await SendComputerStatus(new CSVComputerStatus
 0183                        {
 0184                            Task = $"OpenAlias - {alias.Name}",
 0185                            ComputerName = computerName,
 0186                            Status = openAliasResult.SError
 0187                        });
 0188                        ret.Collected = false;
 0189                        ret.FailureReason = $"SamOpenAliasInDomain failed with status {openAliasResult.SError}";
 0190                        yield return ret;
 0191                        if (openAliasResult.IsTimeout) {
 0192                            yield break;
 193                        }
 0194                        continue;
 195                    }
 196
 5197                    var localGroup = openAliasResult.Value;
 198                    //Call GetMembersInAlias to get raw group members
 10199                    var getMembersResult = await Task.Run(() => localGroup.GetMembers()).TimeoutAfter(timeout);
 5200                    if (getMembersResult.IsFailed)
 0201                    {
 0202                        _log.LogTrace("Failed to get members in alias {Alias} with RID {Rid} in domain {Domain} on compu
 0203                        await SendComputerStatus(new CSVComputerStatus
 0204                        {
 0205                            Task = $"GetMembersInAlias - {alias.Name}",
 0206                            ComputerName = computerName,
 0207                            Status = getMembersResult.SError
 0208                        });
 0209                        ret.Collected = false;
 0210                        ret.FailureReason = $"SamGetMembersInAlias failed with status {getMembersResult.SError}";
 0211                        yield return ret;
 0212                        if (getMembersResult.IsTimeout) {
 0213                            yield break;
 214                        }
 0215                        continue;
 216                    }
 217
 5218                    await SendComputerStatus(new CSVComputerStatus
 5219                    {
 5220                        Task = $"GetMembersInAlias - {alias.Name}",
 5221                        ComputerName = computerName,
 5222                        Status = CSVComputerStatus.StatusSuccess
 5223                    });
 224
 5225                    var results = new List<TypedPrincipal>();
 5226                    var names = new List<NamedPrincipal>();
 227
 37228                    foreach (var securityIdentifier in getMembersResult.Value)
 11229                    {
 11230                        _log.LogTrace("Got member sid {Sid} in alias {Alias} with RID {Rid} in domain {Domain} on comput
 231                        //Check if the sid is one of our filtered ones. Throw it out if it is
 11232                        if (Helpers.IsSidFiltered(securityIdentifier.Value))
 1233                            continue;
 234
 10235                        var sidValue = securityIdentifier.Value;
 236
 10237                        if (isDomainController)
 5238                        {
 5239                            var result = await ResolveDomainControllerPrincipal(sidValue, computerDomain);
 7240                            if (result != null) results.Add(result);
 5241                            continue;
 242                        }
 243
 244                        //If we get a local well known principal, we need to convert it using the computer's objectid
 5245                        if (await _utils.ConvertLocalWellKnownPrincipal(securityIdentifier, computerObjectId, computerDo
 1246                        {
 247                            //If the principal is null, it means we hit a weird edge case, but this is a local well know
 1248                            results.Add(principal);
 1249                            continue;
 250                        }
 251
 252                        //If the security identifier starts with the machine sid, we need to resolve it as a local objec
 4253                        if (securityIdentifier.IsEqualDomainSid(machineSid))
 3254                        {
 255                            //Check if we've already previously resolved and cached this sid
 3256                            if (typeCache.TryGetValue(sidValue, out var cachedLocalItem))
 0257                            {
 0258                                results.Add(new TypedPrincipal
 0259                                {
 0260                                    ObjectIdentifier = sidValue,
 0261                                    ObjectType = cachedLocalItem.Type
 0262                                });
 263
 0264                                names.Add(new NamedPrincipal
 0265                                {
 0266                                    ObjectId = sidValue,
 0267                                    PrincipalName = cachedLocalItem.Name
 0268                                });
 269                                //Move on
 0270                                continue;
 271                            }
 272
 273                            //Attempt to lookup the principal in the server directly
 3274                            var lookupUserResult = server.LookupPrincipalBySid(securityIdentifier);
 3275                            if (lookupUserResult.IsFailed)
 0276                            {
 0277                                _log.LogTrace("Unable to resolve local sid {SID}: {Error}", sidValue, lookupUserResult.S
 0278                                continue;
 279                            }
 280
 3281                            var (name, use) = lookupUserResult.Value;
 3282                            var objectType = use switch
 3283                            {
 2284                                SharedEnums.SidNameUse.User => Label.LocalUser,
 0285                                SharedEnums.SidNameUse.Group => Label.LocalGroup,
 1286                                SharedEnums.SidNameUse.Alias => Label.LocalGroup,
 0287                                _ => Label.Base
 3288                            };
 289
 290                            // Cache whatever we looked up for future lookups
 3291                            typeCache.TryAdd(sidValue, new CachedLocalItem(name, objectType));
 292
 293                            // Throw out local users
 3294                            if (objectType == Label.LocalUser)
 2295                                continue;
 296
 1297                            var newSid = $"{computerObjectId}-{securityIdentifier.Rid()}";
 298
 1299                            results.Add(new TypedPrincipal
 1300                            {
 1301                                ObjectIdentifier = newSid,
 1302                                ObjectType = objectType
 1303                            });
 304
 1305                            names.Add(new NamedPrincipal
 1306                            {
 1307                                PrincipalName = name,
 1308                                ObjectId = newSid
 1309                            });
 1310                            continue;
 311                        }
 312
 313                        //If we get here, we most likely have a domain principal in a local group
 1314                        var resolvedPrincipal = await _utils.ResolveIDAndType(sidValue, computerDomain);
 2315                        if (resolvedPrincipal.Success) results.Add(resolvedPrincipal.Principal);
 1316                    }
 317
 5318                    ret.Collected = true;
 5319                    ret.LocalNames = names.ToArray();
 5320                    ret.Results = results.ToArray();
 5321                    yield return ret;
 5322                }
 3323            }
 3324        }
 325
 326        private async Task<TypedPrincipal> ResolveDomainControllerPrincipal(string sid, string computerDomain)
 5327        {
 328            //If the server is a domain controller and we have a well known group, use the domain value
 5329            if (await _utils.GetWellKnownPrincipal(sid, computerDomain) is (true, var wellKnown))
 1330                return wellKnown;
 4331            return (await _utils.ResolveIDAndType(sid, computerDomain)).Principal;
 5332        }
 333
 334        private async Task<NamedPrincipal> ResolveGroupName(string baseName, string computerName, string computerDomainS
 335            string domainName, int groupRid, bool isDc, bool isBuiltIn)
 7336        {
 7337            if (isDc)
 3338            {
 3339                if (isBuiltIn)
 3340                {
 341                    //If this is the builtin group on the DC, the groups correspond to the domain well known groups
 3342                    if (await _utils.GetWellKnownPrincipal($"S-1-5-32-{groupRid}".ToUpper(), domainName) is (true, var p
 3343                        return new NamedPrincipal
 3344                        {
 3345                            ObjectId = principal.ObjectIdentifier,
 3346                            PrincipalName = "IGNOREME"
 3347                        };
 0348                }
 349
 0350                if (computerDomainSid == null)
 0351                    return null;
 352                //We shouldn't hit this provided our isDC logic is correct since we're skipping non-builtin groups
 0353                return new NamedPrincipal
 0354                {
 0355                    ObjectId = $"{computerDomainSid}-{groupRid}".ToUpper(),
 0356                    PrincipalName = "IGNOREME"
 0357                };
 358            }
 359
 4360            if (computerDomainSid == null)
 0361                return null;
 362            //Take the local machineSid, append the groupRid, and make a name from the group name + computername
 4363            return new NamedPrincipal
 4364            {
 4365                ObjectId = $"{computerDomainSid}-{groupRid}",
 4366                PrincipalName = $"{baseName}@{computerName}".ToUpper()
 4367            };
 7368        }
 369
 370        private async Task SendComputerStatus(CSVComputerStatus status)
 6371        {
 7372            if (ComputerStatusEvent is not null) await ComputerStatusEvent(status);
 6373        }
 374    }
 375}