/// <summary> /// Wraps the GetNetLocalGroupMembers call with a timeout, and then processes the results into objects /// </summary> /// <param name="computer"></param> /// <param name="rid">The relative ID of the group we want to query</param> /// <returns></returns> private static async Task <List <GenericMember> > GetNetLocalGroupMembers(Computer computer, LocalGroupRids rid) { var sids = new IntPtr[0]; var groupMemberList = new List <GenericMember>(); var task = Task.Run(() => CallLocalGroupApi(computer, rid, out sids)); //Run the API call along with a 10 second timeout if (await Task.WhenAny(task, Task.Delay(10000)) != task) { OutputTasks.AddComputerStatus(new ComputerStatus { ComputerName = computer.DisplayName, Status = "Timeout", Task = $"GetNetLocalGroup-{rid}" }); return(groupMemberList); } //Check the result of the task var taskResult = task.Result; if (!taskResult) { return(groupMemberList); } if (Options.Instance.DumpComputerStatus) { OutputTasks.AddComputerStatus(new ComputerStatus { ComputerName = computer.DisplayName, Status = "Success", Task = $"GetNetLocalGroup-{rid}" }); } //Take our pointers to sids and convert them into string sids for matching var convertedSids = new List <string>(); for (var i = 0; i < sids.Length; i++) { try { var sid = new SecurityIdentifier(sids[i]).Value; convertedSids.Add(sid); } catch { //SID Resolution failed for some reason, so ignore it } finally { //Set the IntPtr to zero, so we can GC those sids[i] = IntPtr.Zero; } } //Null out sids, so garbage collection takes care of it sids = null; //Extract the domain SID from the computer's sid, to avoid creating more SecurityIdentifier objects var domainSid = computer.ObjectIdentifier.Substring(0, computer.ObjectIdentifier.LastIndexOf('-')); // The first account in our list should always be the default RID 500 for the machine, but we'll take some extra precautions var machineSid = convertedSids.DefaultIfEmpty("DUMMYSTRING").FirstOrDefault(x => x.EndsWith("-500") && !x.StartsWith(domainSid)) ?? "DUMMYSTRING"; //If we found a machine sid, strip the ending bit off if (machineSid.StartsWith("S-1-5-21")) { machineSid = machineSid.Substring(0, machineSid.LastIndexOf('-')); } foreach (var sid in convertedSids) { //Filter out local accounts if (sid.StartsWith(machineSid)) { continue; } var(finalSid, type) = await ResolutionHelpers.ResolveSidAndGetType(sid, computer.Domain); //Filter out null sids, usually from deconflictions if (finalSid == null) { continue; } groupMemberList.Add(new GenericMember { MemberType = type, MemberId = finalSid }); } return(groupMemberList); }
/// <summary> /// Modified version of GetNetLocalGroupMembers which eliminates several unnecessary LSA/SAMRPC calls /// </summary> /// <param name="computer"></param> /// <param name="rid"></param> /// <param name="sids"></param> /// <returns></returns> private static bool CallLocalGroupApi(Computer computer, LocalGroupRids rid, out IntPtr[] sids) { //Initialize pointers for later var serverHandle = IntPtr.Zero; var domainHandle = IntPtr.Zero; var aliasHandle = IntPtr.Zero; var members = IntPtr.Zero; sids = new IntPtr[0]; //Create some objects required for SAMRPC calls var server = new UNICODE_STRING(computer.APIName); var objectAttributes = new OBJECT_ATTRIBUTES(); try { //Step 1: Call SamConnect to open a handle to the computer's SAM //0x1 = SamServerLookupDomain, 0x20 = SamServerConnect var status = SamConnect(ref server, out serverHandle, 0x1 | 0x20, ref objectAttributes); switch (status) { case NtStatus.StatusRpcServerUnavailable: if (Options.Instance.DumpComputerStatus) { OutputTasks.AddComputerStatus(new ComputerStatus { ComputerName = computer.DisplayName, Status = status.ToString(), Task = $"GetNetLocalGroup-{rid}" }); } return(false); case NtStatus.StatusSuccess: break; default: if (Options.Instance.DumpComputerStatus) { OutputTasks.AddComputerStatus(new ComputerStatus { ComputerName = computer.DisplayName, Status = status.ToString(), Task = $"GetNetLocalGroup-{rid}" }); } return(false); } //Step 2 - Open the built in domain, which is identified by the SID S-1-5-32 //0x200 = Lookup status = SamOpenDomain(serverHandle, 0x200, LocalSidBytes.Value, out domainHandle); if (status != NtStatus.StatusSuccess) { if (Options.Instance.DumpComputerStatus) { OutputTasks.AddComputerStatus(new ComputerStatus { ComputerName = computer.DisplayName, Status = status.ToString(), Task = $"GetNetLocalGroup-{rid}" }); } return(false); } //Step 3 - Open the alias that corresponds to the group we want to enumerate. //0x4 = ListMembers status = SamOpenAlias(domainHandle, 0x4, (int)rid, out aliasHandle); if (status != NtStatus.StatusSuccess) { if (Options.Instance.DumpComputerStatus) { OutputTasks.AddComputerStatus(new ComputerStatus { ComputerName = computer.DisplayName, Status = status.ToString(), Task = $"GetNetLocalGroup-{rid}" }); } } //Step 4 - Get the members of the alias we opened in step 3. status = SamGetMembersInAlias(aliasHandle, out members, out var count); if (status != NtStatus.StatusSuccess) { if (Options.Instance.DumpComputerStatus) { OutputTasks.AddComputerStatus(new ComputerStatus { ComputerName = computer.DisplayName, Status = status.ToString(), Task = $"GetNetLocalGroup-{rid}" }); } return(false); } //If we didn't get any objects, just return false if (count == 0) { return(false); } //Copy the IntPtr to an array so we can loop over it sids = new IntPtr[count]; Marshal.Copy(members, sids, 0, count); return(true); } finally { //Free memory from handles acquired during the process if (serverHandle != IntPtr.Zero) { SamCloseHandle(serverHandle); } if (domainHandle != IntPtr.Zero) { SamCloseHandle(domainHandle); } if (aliasHandle != IntPtr.Zero) { SamCloseHandle(aliasHandle); } if (members != IntPtr.Zero) { SamFreeMemory(members); } } }
public static IEnumerable <LocalMember> GetGroupMembers(ResolvedEntry entry, LocalGroupRids rid) { if (rid.Equals(LocalGroupRids.Administrators) && !Utils.IsMethodSet(ResolvedCollectionMethod.LocalAdmin)) { yield break; } if (rid.Equals(LocalGroupRids.RemoteDesktopUsers) && !Utils.IsMethodSet(ResolvedCollectionMethod.RDP)) { yield break; } if (rid.Equals(LocalGroupRids.DcomUsers) && !Utils.IsMethodSet(ResolvedCollectionMethod.DCOM)) { yield break; } Utils.Debug("Starting GetSamAdmins"); string machineSid = null; Utils.Debug("Starting Task"); var t = Task <SamEnumerationObject[]> .Factory.StartNew(() => { try { return(NetLocalGroupGetMembers(entry, (int)rid, out machineSid)); } catch (ApiFailedException) { return(new SamEnumerationObject[0]); } catch (SystemDownException) { return(new SamEnumerationObject[0]); } }); var success = t.Wait(Timeout); Utils.Debug("Task Finished"); if (!success) { Utils.Debug("SamAdmin Timeout"); throw new TimeoutException(); } Utils.Debug("SamAdmin success"); var resolvedObjects = t.Result; if (resolvedObjects.Length == 0) { Utils.Debug("SamAdmins returned 0 objects"); yield break; } Utils.Debug("Processing data"); //Process our list of stuff now foreach (var data in resolvedObjects) { var sid = data?.AccountSid; Utils.Debug($"Processing sid: {sid}"); if (sid == null) { Utils.Debug("Null sid"); continue; } if (data.AccountName.Equals(string.Empty)) { Utils.Debug("Empty AccountName"); continue; } if (sid.StartsWith(machineSid)) { Utils.Debug("Local Account"); continue; } string type; switch (data.SidUsage) { case SidNameUse.SidTypeUser: type = "user"; break; case SidNameUse.SidTypeGroup: type = "group"; break; case SidNameUse.SidTypeComputer: type = "computer"; break; case SidNameUse.SidTypeWellKnownGroup: type = "wellknown"; break; case SidNameUse.SidTypeAlias: type = "group"; break; default: type = null; break; } if (type == null) { continue; } if (data.AccountName.EndsWith("$")) { type = "unknown"; } string resolvedName; Utils.Debug($"Object Type: {type}"); if (type.Equals("unknown")) { Utils.Debug("Resolving Sid to object UnknownType"); var mp = _utils.UnknownSidTypeToDisplay(sid, _utils.SidToDomainName(sid), AdminProps); if (mp == null) { continue; } Utils.Debug($"Got Object: {mp.PrincipalName}"); resolvedName = mp.PrincipalName; type = mp.ObjectType; } else if (type == "wellknown") { if (MappedPrincipal.GetCommon(sid, out var result)) { if (result.PrincipalName.Equals("Local System")) { continue; } string domain; try { var split = string.Join(".", entry.BloodHoundDisplay.Split('.').Skip(1).ToArray()); domain = split; } catch { domain = _utils.GetDomain(_options.Domain).Name; } type = result.ObjectType; resolvedName = $"{result.PrincipalName}@{domain}".ToUpper(); } else { continue; } } else { Utils.Debug("Resolving Sid to Object"); resolvedName = _utils.SidToDisplay(sid, _utils.SidToDomainName(sid), AdminProps, type); if (resolvedName == null) { continue; } Utils.Debug($"Got Object: {resolvedName}"); } yield return(new LocalMember { Type = type, Name = resolvedName }); } Utils.DoJitter(); }
internal LocalGroupResult GetGroupMembers(LocalGroupRids rid) { var result = new LocalGroupResult(); var status = SamOpenAlias(_domainHandle, AliasOpenFlags.ListMembers, (int)rid, out var aliasHandle); if (status != NativeMethods.NtStatus.StatusSuccess) { SamCloseHandle(aliasHandle); result.FailureReason = $"SamOpenAlias returned {status.ToString()}"; return(result); } status = SamGetMembersInAlias(aliasHandle, out var members, out var count); SamCloseHandle(aliasHandle); if (status != NativeMethods.NtStatus.StatusSuccess) { SamFreeMemory(members); result.FailureReason = $"SamGetMembersInAlias returned {status.ToString()}"; return(result); } Logging.Debug($"API call returned count of {count} "); if (count == 0) { SamFreeMemory(members); result.Collected = true; return(result); } var sids = new List <string>(); for (var i = 0; i < count; i++) { try { var intptr = Marshal.ReadIntPtr(members, Marshal.SizeOf(typeof(IntPtr)) * i); var sid = new SecurityIdentifier(intptr).Value; sids.Add(sid); } catch (Exception e) { Logging.Debug($"Exception converting sid: {e}"); } } SamFreeMemory(members); var machineSid = GetMachineSid(); Logging.Debug($"Resolved machine sid to {machineSid}"); var converted = sids.Select(x => { Logging.Debug(x); //Filter out machine accounts, service accounts, iis app pool accounts, window manager, font driver if (x.StartsWith(machineSid) || x.StartsWith("S-1-5-80") || x.StartsWith("S-1-5-82") || x.StartsWith("S-1-5-90") || x.StartsWith("S-1-5-96")) { return(null); } if (_filteredSids.Contains(x)) { return(null); } x = WellKnownPrincipal.TryConvert(x, _computerDomain); return(x.StartsWith("S-1-5-21") ? x : null); }).Where(x => x != null); result.Collected = true; result.Members = converted.ToArray(); return(result); }
/// <summary> /// Wraps the GetNetLocalGroupMembers call with a timeout, and then processes the results into objects /// </summary> /// <param name="computer"></param> /// <param name="rid">The relative ID of the group we want to query</param> /// <returns></returns> private static async Task <List <GenericMember> > GetNetLocalGroupMembers(Computer computer, LocalGroupRids rid) { var sids = new IntPtr[0]; var groupMemberList = new List <GenericMember>(); var task = Task.Run(() => CallLocalGroupApi(computer, rid, out sids)); if (await Task.WhenAny(task, Task.Delay(10000)) != task) { OutputTasks.AddComputerStatus(new ComputerStatus { ComputerName = computer.DisplayName, Status = "Timeout", Task = $"GetNetLocalGroup-{rid}" }); return(groupMemberList); } var taskResult = task.Result; if (!taskResult) { return(groupMemberList); } if (Options.Instance.DumpComputerStatus) { OutputTasks.AddComputerStatus(new ComputerStatus { ComputerName = computer.DisplayName, Status = "Success", Task = $"GetNetLocaGroup-{rid}" }); } //Take our pointers to sids and convert them into string sids for matching var convertedSids = new List <string>(); for (var i = 0; i < sids.Length; i++) { try { var sid = new SecurityIdentifier(sids[i]).Value; convertedSids.Add(sid); } catch { // ignored } finally { //Set the IntPtr to zero, so we can GC those sids[i] = IntPtr.Zero; } } //Null out sids, so garbage collection takes care of it sids = null; //Extract the domain SID from the computer's sid, to avoid creating more SecurityIdentifier objects var domainSid = computer.ObjectIdentifier.Substring(0, computer.ObjectIdentifier.LastIndexOf('-')); string machineSid; // The first account in our list should always be the default RID 500 for the machine, but we'll take some extra precautions try { machineSid = convertedSids.First(x => x.EndsWith("-500") && !x.StartsWith(domainSid)); } catch { machineSid = "DUMMYSTRING"; } foreach (var sid in convertedSids) { if (sid.StartsWith(machineSid)) { continue; } LdapTypeEnum type; var finalSid = sid; if (CommonPrincipal.GetCommonSid(finalSid, out var common)) { finalSid = Helpers.ConvertCommonSid(sid, null); type = common.Type; } else { type = await Helpers.LookupSidType(sid); } groupMemberList.Add(new GenericMember { MemberType = type, MemberId = finalSid }); } return(groupMemberList); }