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