| | 1 | | using System; |
| | 2 | | using System.Collections.Generic; |
| | 3 | | using System.Security.Principal; |
| | 4 | | using System.Threading.Tasks; |
| | 5 | | using Microsoft.Extensions.Logging; |
| | 6 | | using SharpHoundCommonLib.Enums; |
| | 7 | | using SharpHoundCommonLib.OutputTypes; |
| | 8 | | using SharpHoundRPC; |
| | 9 | | using SharpHoundRPC.Shared; |
| | 10 | | using SharpHoundRPC.Wrappers; |
| | 11 | |
|
| | 12 | | namespace SharpHoundCommonLib.Processors |
| | 13 | | { |
| | 14 | | public class UserRightsAssignmentProcessor |
| | 15 | | { |
| | 16 | | public delegate Task ComputerStatusDelegate(CSVComputerStatus status); |
| | 17 | |
|
| | 18 | | private readonly ILogger _log; |
| | 19 | | private readonly ILdapUtils _utils; |
| | 20 | |
|
| 3 | 21 | | public UserRightsAssignmentProcessor(ILdapUtils utils, ILogger log = null) |
| 3 | 22 | | { |
| 3 | 23 | | _utils = utils; |
| 3 | 24 | | _log = log ?? Logging.LogProvider.CreateLogger("UserRightsAssignmentProcessor"); |
| 3 | 25 | | } |
| | 26 | |
|
| | 27 | | public event ComputerStatusDelegate ComputerStatusEvent; |
| | 28 | |
|
| | 29 | | public virtual SharpHoundRPC.Result<ILSAPolicy> OpenLSAPolicy(string computerName) |
| 0 | 30 | | { |
| 0 | 31 | | var result = LSAPolicy.OpenPolicy(computerName); |
| 0 | 32 | | if (result.IsFailed) return SharpHoundRPC.Result<ILSAPolicy>.Fail(result.SError); |
| | 33 | |
|
| 0 | 34 | | return SharpHoundRPC.Result<ILSAPolicy>.Ok(result.Value); |
| 0 | 35 | | } |
| | 36 | |
|
| | 37 | | public IAsyncEnumerable<UserRightsAssignmentAPIResult> GetUserRightsAssignments(ResolvedSearchResult result, |
| | 38 | | string[] desiredPrivileges = null) |
| 0 | 39 | | { |
| 0 | 40 | | return GetUserRightsAssignments(result.DisplayName, result.ObjectId, result.Domain, |
| 0 | 41 | | result.IsDomainController, desiredPrivileges); |
| 0 | 42 | | } |
| | 43 | |
|
| | 44 | | /// <summary> |
| | 45 | | /// Gets principals with the requested privileges on the target computer |
| | 46 | | /// </summary> |
| | 47 | | /// <param name="computerName"></param> |
| | 48 | | /// <param name="computerObjectId">The objectid of the computer in the domain</param> |
| | 49 | | /// <param name="computerDomain"></param> |
| | 50 | | /// <param name="isDomainController">Is the computer a domain controller</param> |
| | 51 | | /// <param name="desiredPrivileges"></param> |
| | 52 | | /// <param name="timeout"></param> |
| | 53 | | /// <returns></returns> |
| | 54 | | public async IAsyncEnumerable<UserRightsAssignmentAPIResult> GetUserRightsAssignments(string computerName, |
| | 55 | | string computerObjectId, string computerDomain, bool isDomainController, string[] desiredPrivileges = null, |
| 3 | 56 | | { |
| 5 | 57 | | if (timeout == default) { |
| 2 | 58 | | timeout = TimeSpan.FromMinutes(2); |
| 2 | 59 | | } |
| 6 | 60 | | var policyOpenResult = await Task.Run(() => OpenLSAPolicy(computerName)).TimeoutAfter(timeout); |
| 3 | 61 | | if (!policyOpenResult.IsSuccess) |
| 1 | 62 | | { |
| 1 | 63 | | _log.LogDebug("LSAOpenPolicy failed on {ComputerName} with status {Status}", computerName, |
| 1 | 64 | | policyOpenResult.Error); |
| 1 | 65 | | await SendComputerStatus(new CSVComputerStatus |
| 1 | 66 | | { |
| 1 | 67 | | Task = "LSAOpenPolicy", |
| 1 | 68 | | ComputerName = computerName, |
| 1 | 69 | | Status = policyOpenResult.Error |
| 1 | 70 | | }); |
| 1 | 71 | | yield break; |
| | 72 | | } |
| | 73 | |
|
| 2 | 74 | | var server = policyOpenResult.Value; |
| 2 | 75 | | desiredPrivileges ??= LSAPrivileges.DesiredPrivileges; |
| | 76 | |
|
| | 77 | | SecurityIdentifier machineSid; |
| 2 | 78 | | if (!Cache.GetMachineSid(computerObjectId, out var temp)) |
| 2 | 79 | | { |
| 4 | 80 | | var getMachineSidResult = await Task.Run(() => server.GetLocalDomainInformation()).TimeoutAfter(timeout) |
| 2 | 81 | | if (getMachineSidResult.IsFailed) |
| 0 | 82 | | { |
| 0 | 83 | | _log.LogWarning("Failed to get machine sid for {Server}: {Status}. Abandoning URA collection", |
| 0 | 84 | | computerName, getMachineSidResult.SError); |
| 0 | 85 | | await SendComputerStatus(new CSVComputerStatus |
| 0 | 86 | | { |
| 0 | 87 | | ComputerName = computerName, |
| 0 | 88 | | Status = getMachineSidResult.SError, |
| 0 | 89 | | Task = "LSAGetMachineSID" |
| 0 | 90 | | }); |
| 0 | 91 | | yield break; |
| | 92 | | } |
| | 93 | |
|
| 2 | 94 | | machineSid = new SecurityIdentifier(getMachineSidResult.Value.Sid); |
| 2 | 95 | | Cache.AddMachineSid(computerObjectId, getMachineSidResult.Value.Sid); |
| 2 | 96 | | } |
| | 97 | | else |
| 0 | 98 | | { |
| 0 | 99 | | machineSid = new SecurityIdentifier(temp); |
| 0 | 100 | | } |
| | 101 | |
|
| 10 | 102 | | foreach (var privilege in desiredPrivileges) |
| 2 | 103 | | { |
| 2 | 104 | | _log.LogTrace("Getting principals for privilege {Priv} on computer {ComputerName}", privilege, computerN |
| 2 | 105 | | var ret = new UserRightsAssignmentAPIResult |
| 2 | 106 | | { |
| 2 | 107 | | Collected = false, |
| 2 | 108 | | Privilege = privilege |
| 2 | 109 | | }; |
| | 110 | |
|
| | 111 | | //Ask for all principals with the specified privilege. |
| 4 | 112 | | var enumerateAccountsResult = await Task.Run(() => server.GetResolvedPrincipalsWithPrivilege(privilege)) |
| 2 | 113 | | if (enumerateAccountsResult.IsFailed) |
| 0 | 114 | | { |
| 0 | 115 | | _log.LogDebug( |
| 0 | 116 | | "LSAEnumerateAccountsWithUserRight failed on {ComputerName} with status {Status} for privilege { |
| 0 | 117 | | computerName, policyOpenResult.Error, privilege); |
| 0 | 118 | | await SendComputerStatus(new CSVComputerStatus |
| 0 | 119 | | { |
| 0 | 120 | | ComputerName = computerName, |
| 0 | 121 | | Status = enumerateAccountsResult.SError, |
| 0 | 122 | | Task = "LSAEnumerateAccountsWithUserRight" |
| 0 | 123 | | }); |
| 0 | 124 | | ret.FailureReason = |
| 0 | 125 | | $"LSAEnumerateAccountsWithUserRights returned {enumerateAccountsResult.SError}"; |
| 0 | 126 | | yield return ret; |
| 0 | 127 | | if (enumerateAccountsResult.IsTimeout) { |
| 0 | 128 | | yield break; |
| | 129 | | } |
| 0 | 130 | | continue; |
| | 131 | | } |
| | 132 | |
|
| 2 | 133 | | await SendComputerStatus(new CSVComputerStatus |
| 2 | 134 | | { |
| 2 | 135 | | ComputerName = computerName, |
| 2 | 136 | | Status = CSVComputerStatus.StatusSuccess, |
| 2 | 137 | | Task = "LSAEnumerateAccountsWithUserRight" |
| 2 | 138 | | }); |
| | 139 | |
|
| 2 | 140 | | var resolved = new List<TypedPrincipal>(); |
| 2 | 141 | | var names = new List<NamedPrincipal>(); |
| | 142 | |
|
| 16 | 143 | | foreach (var value in enumerateAccountsResult.Value) |
| 5 | 144 | | { |
| 5 | 145 | | var (sid, name, use, _) = value; |
| 5 | 146 | | _log.LogTrace("Got principal {Name} with sid {SID} and use {Use} for privilege {Priv} on computer {C |
| | 147 | | //Check if our sid is filtered |
| 5 | 148 | | if (Helpers.IsSidFiltered(sid.Value)) |
| 0 | 149 | | continue; |
| | 150 | |
|
| 5 | 151 | | if (isDomainController) |
| 1 | 152 | | { |
| 1 | 153 | | var result = await ResolveDomainControllerPrincipal(sid.Value, computerDomain); |
| 1 | 154 | | if (result != null) |
| 1 | 155 | | resolved.Add(result); |
| 1 | 156 | | continue; |
| | 157 | | } |
| | 158 | |
|
| | 159 | | //If we get a local well known principal, we need to convert it using the computer's domain sid |
| 4 | 160 | | if (await _utils.ConvertLocalWellKnownPrincipal(sid, computerObjectId, computerDomain) is (true, var |
| 2 | 161 | | { |
| 2 | 162 | | _log.LogTrace("Got Well Known Principal {SID} on computer {Computer} for privilege {Privilege} a |
| 2 | 163 | | resolved.Add(principal); |
| 2 | 164 | | continue; |
| | 165 | | } |
| | 166 | |
|
| | 167 | | //If the security identifier starts with the machine sid, we need to resolve it as a local account |
| 2 | 168 | | if (sid.IsEqualDomainSid(machineSid)) |
| 2 | 169 | | { |
| 2 | 170 | | _log.LogTrace("Got local account {sid} on computer {Computer} for privilege {Privilege}", sid.Va |
| 2 | 171 | | var objectType = use switch |
| 2 | 172 | | { |
| 1 | 173 | | SharedEnums.SidNameUse.User => Label.LocalUser, |
| 0 | 174 | | SharedEnums.SidNameUse.Group => Label.LocalGroup, |
| 1 | 175 | | SharedEnums.SidNameUse.Alias => Label.LocalGroup, |
| 0 | 176 | | _ => Label.Base |
| 2 | 177 | | }; |
| | 178 | |
|
| | 179 | | //Throw out local user accounts |
| 2 | 180 | | if (objectType == Label.LocalUser) |
| 1 | 181 | | continue; |
| | 182 | |
|
| | 183 | | //The local group sid is computer machine sid - group rid. |
| 1 | 184 | | var groupRid = sid.Rid(); |
| 1 | 185 | | var newSid = $"{computerObjectId}-{groupRid}"; |
| 1 | 186 | | if (name != null) |
| 1 | 187 | | names.Add(new NamedPrincipal |
| 1 | 188 | | { |
| 1 | 189 | | ObjectId = newSid, |
| 1 | 190 | | PrincipalName = name |
| 1 | 191 | | }); |
| | 192 | |
|
| 1 | 193 | | resolved.Add(new TypedPrincipal |
| 1 | 194 | | { |
| 1 | 195 | | ObjectIdentifier = newSid, |
| 1 | 196 | | ObjectType = objectType |
| 1 | 197 | | }); |
| 1 | 198 | | continue; |
| | 199 | | } |
| | 200 | |
|
| | 201 | | //If we get here, we most likely have a domain principal in a local group. Do a lookup |
| 0 | 202 | | var resolvedPrincipal = await _utils.ResolveIDAndType(sid.Value, computerDomain); |
| 0 | 203 | | if (resolvedPrincipal.Success) resolved.Add(resolvedPrincipal.Principal); |
| 0 | 204 | | } |
| | 205 | |
|
| 2 | 206 | | ret.Collected = true; |
| 2 | 207 | | ret.LocalNames = names.ToArray(); |
| 2 | 208 | | ret.Results = resolved.ToArray(); |
| 2 | 209 | | yield return ret; |
| 2 | 210 | | } |
| 3 | 211 | | } |
| | 212 | |
|
| | 213 | | private async Task<TypedPrincipal> ResolveDomainControllerPrincipal(string sid, string computerDomain) |
| 1 | 214 | | { |
| | 215 | | //If the server is a domain controller and we have a well known group, use the domain value |
| 1 | 216 | | if (await _utils.GetWellKnownPrincipal(sid, computerDomain) is (true, var wellKnown)) |
| 1 | 217 | | return wellKnown; |
| | 218 | | //Otherwise, do a domain lookup |
| 0 | 219 | | var domainPrinciple = await _utils.ResolveIDAndType(sid, computerDomain); |
| 0 | 220 | | return domainPrinciple.Principal; |
| 1 | 221 | | } |
| | 222 | |
|
| | 223 | |
|
| | 224 | | private async Task SendComputerStatus(CSVComputerStatus status) |
| 3 | 225 | | { |
| 4 | 226 | | if (ComputerStatusEvent is not null) await ComputerStatusEvent.Invoke(status); |
| 3 | 227 | | } |
| | 228 | | } |
| | 229 | | } |