| | 1 | | using System; |
| | 2 | | using System.Collections.Generic; |
| | 3 | | using System.Linq; |
| | 4 | | using System.Security.Principal; |
| | 5 | | using System.Text.RegularExpressions; |
| | 6 | | using System.Threading.Tasks; |
| | 7 | | using Impersonate; |
| | 8 | | using Microsoft.Extensions.Logging; |
| | 9 | | using Microsoft.Win32; |
| | 10 | | using SharpHoundCommonLib.OutputTypes; |
| | 11 | |
|
| | 12 | | namespace SharpHoundCommonLib.Processors |
| | 13 | | { |
| | 14 | | public class ComputerSessionProcessor |
| | 15 | | { |
| | 16 | | public delegate Task ComputerStatusDelegate(CSVComputerStatus status); |
| | 17 | |
|
| 0 | 18 | | private static readonly Regex SidRegex = new(@"S-1-5-21-[0-9]+-[0-9]+-[0-9]+-[0-9]+$", RegexOptions.Compiled); |
| | 19 | | private readonly string _currentUserName; |
| | 20 | | private readonly ILogger _log; |
| | 21 | | private readonly NativeMethods _nativeMethods; |
| | 22 | | private readonly ILDAPUtils _utils; |
| | 23 | | private readonly bool _doLocalAdminSessionEnum; |
| | 24 | | private readonly string _localAdminUsername; |
| | 25 | | private readonly string _localAdminPassword; |
| | 26 | |
|
| 8 | 27 | | public ComputerSessionProcessor(ILDAPUtils utils, string currentUserName = null, NativeMethods nativeMethods = n |
| 8 | 28 | | { |
| 8 | 29 | | _utils = utils; |
| 8 | 30 | | _nativeMethods = nativeMethods ?? new NativeMethods(); |
| 8 | 31 | | _currentUserName = currentUserName ?? WindowsIdentity.GetCurrent().Name.Split('\\')[1]; |
| 8 | 32 | | _log = log ?? Logging.LogProvider.CreateLogger("CompSessions"); |
| 8 | 33 | | _doLocalAdminSessionEnum = doLocalAdminSessionEnum; |
| 8 | 34 | | _localAdminUsername = localAdminUsername; |
| 8 | 35 | | _localAdminPassword = localAdminPassword; |
| 8 | 36 | | } |
| | 37 | |
|
| | 38 | | public event ComputerStatusDelegate ComputerStatusEvent; |
| | 39 | |
|
| | 40 | | /// <summary> |
| | 41 | | /// Uses the NetSessionEnum Win32 API call to get network sessions from a remote computer. |
| | 42 | | /// These are usually from SMB share accesses or other network sessions of the sort |
| | 43 | | /// </summary> |
| | 44 | | /// <param name="computerName"></param> |
| | 45 | | /// <param name="computerSid"></param> |
| | 46 | | /// <param name="computerDomain"></param> |
| | 47 | | /// <returns></returns> |
| | 48 | | public async Task<SessionAPIResult> ReadUserSessions(string computerName, string computerSid, |
| | 49 | | string computerDomain) |
| 6 | 50 | | { |
| 6 | 51 | | var ret = new SessionAPIResult(); |
| | 52 | | SharpHoundRPC.NetAPINative.NetAPIResult<IEnumerable<SharpHoundRPC.NetAPINative.NetSessionEnumResults>> resul |
| | 53 | |
|
| 6 | 54 | | if (_doLocalAdminSessionEnum) |
| 0 | 55 | | { |
| | 56 | | // If we are authenticating using a local admin, we need to impersonate for this |
| | 57 | | Impersonator Impersonate; |
| 0 | 58 | | using (Impersonate = new Impersonator(_localAdminUsername, ".", _localAdminPassword, LogonType.LOGON32_L |
| 0 | 59 | | { |
| 0 | 60 | | result = _nativeMethods.NetSessionEnum(computerName); |
| 0 | 61 | | } |
| | 62 | |
|
| 0 | 63 | | if (result.IsFailed) |
| 0 | 64 | | { |
| | 65 | | // Fall back to default User |
| 0 | 66 | | _log.LogDebug("NetSessionEnum failed on {ComputerName} with local admin credentials: {Status}. Fallb |
| 0 | 67 | | result = _nativeMethods.NetSessionEnum(computerName); |
| 0 | 68 | | } |
| 0 | 69 | | } |
| | 70 | | else |
| 6 | 71 | | { |
| 6 | 72 | | result = _nativeMethods.NetSessionEnum(computerName); |
| 6 | 73 | | } |
| | 74 | |
|
| 6 | 75 | | if (result.IsFailed) |
| 1 | 76 | | { |
| 1 | 77 | | await SendComputerStatus(new CSVComputerStatus |
| 1 | 78 | | { |
| 1 | 79 | | Status = result.Status.ToString(), |
| 1 | 80 | | Task = "NetSessionEnum", |
| 1 | 81 | | ComputerName = computerName |
| 1 | 82 | | }); |
| 1 | 83 | | _log.LogDebug("NetSessionEnum failed on {ComputerName}: {Status}", computerName, result.Status); |
| 1 | 84 | | ret.Collected = false; |
| 1 | 85 | | ret.FailureReason = result.Status.ToString(); |
| 1 | 86 | | return ret; |
| | 87 | | } |
| | 88 | |
|
| 5 | 89 | | _log.LogDebug("NetSessionEnum succeeded on {ComputerName}", computerName); |
| 5 | 90 | | await SendComputerStatus(new CSVComputerStatus |
| 5 | 91 | | { |
| 5 | 92 | | Status = CSVComputerStatus.StatusSuccess, |
| 5 | 93 | | Task = "NetSessionEnum", |
| 5 | 94 | | ComputerName = computerName |
| 5 | 95 | | }); |
| | 96 | |
|
| 5 | 97 | | ret.Collected = true; |
| 5 | 98 | | var results = new List<Session>(); |
| | 99 | |
|
| 29 | 100 | | foreach (var sesInfo in result.Value) |
| 7 | 101 | | { |
| 7 | 102 | | var username = sesInfo.Username; |
| 7 | 103 | | var computerSessionName = sesInfo.ComputerName; |
| | 104 | |
|
| 7 | 105 | | _log.LogTrace("NetSessionEnum Entry: {Username}@{ComputerSessionName} from {ComputerName}", username, |
| 7 | 106 | | computerSessionName, computerName); |
| | 107 | |
|
| | 108 | | //Filter out blank/null cnames/usernames |
| 7 | 109 | | if (string.IsNullOrWhiteSpace(computerSessionName) || string.IsNullOrWhiteSpace(username)) |
| 1 | 110 | | { |
| 1 | 111 | | _log.LogTrace("Skipping NetSessionEnum entry with null session/user"); |
| 1 | 112 | | continue; |
| | 113 | | } |
| | 114 | |
|
| | 115 | | //Filter out blank usernames, computer accounts, the user we're doing enumeration with, and anonymous lo |
| 6 | 116 | | if (username.EndsWith("$") || |
| 6 | 117 | | username.Equals(_currentUserName, StringComparison.CurrentCultureIgnoreCase) || |
| 6 | 118 | | username.Equals("anonymous logon", StringComparison.CurrentCultureIgnoreCase)) |
| 1 | 119 | | { |
| 1 | 120 | | _log.LogTrace("Skipping NetSessionEnum entry for {Username}", username); |
| 1 | 121 | | continue; |
| | 122 | | } |
| | 123 | |
|
| | 124 | | // Remove leading slashes for unc paths |
| 5 | 125 | | computerSessionName = computerSessionName.TrimStart('\\'); |
| | 126 | |
|
| 5 | 127 | | string resolvedComputerSID = null; |
| | 128 | |
|
| | 129 | | //Resolve "localhost" equivalents to the computer sid |
| 5 | 130 | | if (computerSessionName is "[::1]" or "127.0.0.1") |
| 3 | 131 | | resolvedComputerSID = computerSid; |
| | 132 | | else |
| | 133 | | //Attempt to resolve the host name to a SID |
| 2 | 134 | | resolvedComputerSID = await _utils.ResolveHostToSid(computerSessionName, computerDomain); |
| | 135 | |
|
| | 136 | | //Throw out this data if we couldn't resolve it successfully. |
| 5 | 137 | | if (resolvedComputerSID == null || !resolvedComputerSID.StartsWith("S-1")) |
| 1 | 138 | | { |
| 1 | 139 | | _log.LogTrace("Unable to resolve {ComputerSessionName} to real SID", computerSessionName); |
| 1 | 140 | | continue; |
| | 141 | | } |
| | 142 | |
|
| 4 | 143 | | var matches = _utils.GetUserGlobalCatalogMatches(username); |
| 4 | 144 | | if (matches.Length > 0) |
| 3 | 145 | | { |
| 3 | 146 | | results.AddRange( |
| 7 | 147 | | matches.Select(s => new Session {ComputerSID = resolvedComputerSID, UserSID = s})); |
| 3 | 148 | | } |
| | 149 | | else |
| 1 | 150 | | { |
| 1 | 151 | | var res = _utils.ResolveAccountName(username, computerDomain); |
| 1 | 152 | | if (res != null) |
| 1 | 153 | | results.Add(new Session |
| 1 | 154 | | { |
| 1 | 155 | | ComputerSID = resolvedComputerSID, |
| 1 | 156 | | UserSID = res.ObjectIdentifier |
| 1 | 157 | | }); |
| 1 | 158 | | } |
| 4 | 159 | | } |
| | 160 | |
|
| 5 | 161 | | ret.Results = results.ToArray(); |
| | 162 | |
|
| 5 | 163 | | return ret; |
| 6 | 164 | | } |
| | 165 | |
|
| | 166 | | /// <summary> |
| | 167 | | /// Uses the privileged win32 API, NetWkstaUserEnum, to return the logged on users on a remote computer. |
| | 168 | | /// Requires administrator rights on the target system |
| | 169 | | /// </summary> |
| | 170 | | /// <param name="computerName"></param> |
| | 171 | | /// <param name="computerSamAccountName"></param> |
| | 172 | | /// <param name="computerSid"></param> |
| | 173 | | /// <returns></returns> |
| | 174 | | public async Task<SessionAPIResult> ReadUserSessionsPrivileged(string computerName, |
| | 175 | | string computerSamAccountName, string computerSid) |
| 2 | 176 | | { |
| 2 | 177 | | var ret = new SessionAPIResult(); |
| | 178 | | SharpHoundRPC.NetAPINative.NetAPIResult<IEnumerable<SharpHoundRPC.NetAPINative.NetWkstaUserEnumResults>> res |
| | 179 | |
|
| 2 | 180 | | if (_doLocalAdminSessionEnum) |
| 0 | 181 | | { |
| | 182 | | // If we are authenticating using a local admin, we need to impersonate for this |
| | 183 | | Impersonator Impersonate; |
| 0 | 184 | | using (Impersonate = new Impersonator(_localAdminUsername, ".", _localAdminPassword, LogonType.LOGON32_L |
| 0 | 185 | | { |
| 0 | 186 | | result = _nativeMethods.NetWkstaUserEnum(computerName); |
| 0 | 187 | | } |
| | 188 | |
|
| 0 | 189 | | if (result.IsFailed) |
| 0 | 190 | | { |
| | 191 | | // Fall back to default User |
| 0 | 192 | | _log.LogDebug("NetWkstaUserEnum failed on {ComputerName} with local admin credentials: {Status}. Fal |
| 0 | 193 | | result = _nativeMethods.NetWkstaUserEnum(computerName); |
| 0 | 194 | | } |
| 0 | 195 | | } |
| | 196 | | else |
| 2 | 197 | | { |
| 2 | 198 | | result = _nativeMethods.NetWkstaUserEnum(computerName); |
| 2 | 199 | | } |
| | 200 | |
|
| 2 | 201 | | if (result.IsFailed) |
| 1 | 202 | | { |
| 1 | 203 | | await SendComputerStatus(new CSVComputerStatus |
| 1 | 204 | | { |
| 1 | 205 | | Status = result.Status.ToString(), |
| 1 | 206 | | Task = "NetWkstaUserEnum", |
| 1 | 207 | | ComputerName = computerName |
| 1 | 208 | | }); |
| 1 | 209 | | _log.LogDebug("NetWkstaUserEnum failed on {ComputerName}: {Status}", computerName, result.Status); |
| 1 | 210 | | ret.Collected = false; |
| 1 | 211 | | ret.FailureReason = result.Status.ToString(); |
| 1 | 212 | | return ret; |
| | 213 | | } |
| | 214 | |
|
| 1 | 215 | | _log.LogDebug("NetWkstaUserEnum succeeded on {ComputerName}", computerName); |
| 1 | 216 | | await SendComputerStatus(new CSVComputerStatus |
| 1 | 217 | | { |
| 1 | 218 | | Status = result.Status.ToString(), |
| 1 | 219 | | Task = "NetWkstaUserEnum", |
| 1 | 220 | | ComputerName = computerName |
| 1 | 221 | | }); |
| | 222 | |
|
| 1 | 223 | | ret.Collected = true; |
| | 224 | |
|
| 1 | 225 | | var results = new List<TypedPrincipal>(); |
| 23 | 226 | | foreach (var wkstaUserInfo in result.Value) |
| 10 | 227 | | { |
| 10 | 228 | | var domain = wkstaUserInfo.LogonDomain; |
| 10 | 229 | | var username = wkstaUserInfo.Username; |
| | 230 | |
|
| 10 | 231 | | _log.LogTrace("NetWkstaUserEnum entry: {Username}@{Domain} from {ComputerName}", username, domain, |
| 10 | 232 | | computerName); |
| | 233 | |
|
| | 234 | | //These are local computer accounts. |
| 10 | 235 | | if (domain.Equals(computerSamAccountName, StringComparison.CurrentCultureIgnoreCase)) |
| 1 | 236 | | { |
| 1 | 237 | | _log.LogTrace("Skipping local entry {Username}@{Domain}", username, domain); |
| 1 | 238 | | continue; |
| | 239 | | } |
| | 240 | |
|
| | 241 | | //Filter out empty usernames and computer sessions |
| 9 | 242 | | if (string.IsNullOrWhiteSpace(username) || username.EndsWith("$", StringComparison.Ordinal)) |
| 4 | 243 | | { |
| 4 | 244 | | _log.LogTrace("Skipping null or computer session"); |
| 4 | 245 | | continue; |
| | 246 | | } |
| | 247 | |
|
| | 248 | | //If we dont have a domain, ignore this object |
| 5 | 249 | | if (string.IsNullOrWhiteSpace(domain)) |
| 1 | 250 | | { |
| 1 | 251 | | _log.LogTrace("Skipping null/empty domain"); |
| 1 | 252 | | continue; |
| | 253 | | } |
| | 254 | |
|
| | 255 | | //Any domain with a space is unusable. It'll be things like NT Authority or Font Driver |
| 4 | 256 | | if (domain.Contains(" ")) |
| 1 | 257 | | { |
| 1 | 258 | | _log.LogTrace("Skipping domain with space: {Domain}", domain); |
| 1 | 259 | | continue; |
| | 260 | | } |
| | 261 | |
|
| 3 | 262 | | var res = _utils.ResolveAccountName(username, domain); |
| 3 | 263 | | if (res == null) |
| 1 | 264 | | continue; |
| | 265 | |
|
| 2 | 266 | | _log.LogTrace("Resolved NetWkstaUserEnum entry: {SID}", res.ObjectIdentifier); |
| 2 | 267 | | results.Add(res); |
| 2 | 268 | | } |
| | 269 | |
|
| 3 | 270 | | ret.Results = results.Select(x => new Session |
| 3 | 271 | | { |
| 3 | 272 | | ComputerSID = computerSid, |
| 3 | 273 | | UserSID = x.ObjectIdentifier |
| 3 | 274 | | }).ToArray(); |
| | 275 | |
|
| 1 | 276 | | return ret; |
| 2 | 277 | | } |
| | 278 | |
|
| | 279 | | public async Task<SessionAPIResult> ReadUserSessionsRegistry(string computerName, string computerDomain, |
| | 280 | | string computerSid) |
| 0 | 281 | | { |
| 0 | 282 | | var ret = new SessionAPIResult(); |
| | 283 | |
|
| 0 | 284 | | RegistryKey key = null; |
| | 285 | |
|
| | 286 | | try |
| 0 | 287 | | { |
| 0 | 288 | | var task = OpenRegistryKey(computerName, RegistryHive.Users); |
| | 289 | |
|
| 0 | 290 | | if (await Task.WhenAny(task, Task.Delay(10000)) != task) |
| 0 | 291 | | { |
| 0 | 292 | | _log.LogDebug("Hit timeout on registry enum on {Server}. Abandoning registry enum", computerName); |
| 0 | 293 | | ret.Collected = false; |
| 0 | 294 | | ret.FailureReason = "Timeout"; |
| 0 | 295 | | await SendComputerStatus(new CSVComputerStatus |
| 0 | 296 | | { |
| 0 | 297 | | Status = "Timeout", |
| 0 | 298 | | Task = "RegistrySessionEnum", |
| 0 | 299 | | ComputerName = computerName |
| 0 | 300 | | }); |
| 0 | 301 | | return ret; |
| | 302 | | } |
| | 303 | |
|
| 0 | 304 | | key = task.Result; |
| | 305 | |
|
| 0 | 306 | | ret.Collected = true; |
| 0 | 307 | | await SendComputerStatus(new CSVComputerStatus |
| 0 | 308 | | { |
| 0 | 309 | | Status = CSVComputerStatus.StatusSuccess, |
| 0 | 310 | | Task = "RegistrySessionEnum", |
| 0 | 311 | | ComputerName = computerName |
| 0 | 312 | | }); |
| 0 | 313 | | _log.LogDebug("Registry session enum succeeded on {ComputerName}", computerName); |
| 0 | 314 | | ret.Results = key.GetSubKeyNames() |
| 0 | 315 | | .Where(subkey => SidRegex.IsMatch(subkey)) |
| 0 | 316 | | .Select(x => _utils.ResolveIDAndType(x, computerDomain)) |
| 0 | 317 | | .Where(x => x != null) |
| 0 | 318 | | .Select(x => |
| 0 | 319 | | new Session |
| 0 | 320 | | { |
| 0 | 321 | | ComputerSID = computerSid, |
| 0 | 322 | | UserSID = x.ObjectIdentifier |
| 0 | 323 | | }) |
| 0 | 324 | | .ToArray(); |
| | 325 | |
|
| 0 | 326 | | return ret; |
| | 327 | | } |
| 0 | 328 | | catch (Exception e) |
| 0 | 329 | | { |
| 0 | 330 | | _log.LogDebug("Registry session enum failed on {ComputerName}: {Status}", computerName, e.Message); |
| 0 | 331 | | await SendComputerStatus(new CSVComputerStatus |
| 0 | 332 | | { |
| 0 | 333 | | Status = e.Message, |
| 0 | 334 | | Task = "RegistrySessionEnum", |
| 0 | 335 | | ComputerName = computerName |
| 0 | 336 | | }); |
| 0 | 337 | | ret.Collected = false; |
| 0 | 338 | | ret.FailureReason = e.Message; |
| 0 | 339 | | return ret; |
| | 340 | | } |
| | 341 | | finally |
| 0 | 342 | | { |
| 0 | 343 | | key?.Dispose(); |
| 0 | 344 | | } |
| 0 | 345 | | } |
| | 346 | |
|
| | 347 | | private Task<RegistryKey> OpenRegistryKey(string computerName, RegistryHive hive) |
| 0 | 348 | | { |
| 0 | 349 | | return Task.Run(() => RegistryKey.OpenRemoteBaseKey(hive, computerName)); |
| 0 | 350 | | } |
| | 351 | |
|
| | 352 | | private async Task SendComputerStatus(CSVComputerStatus status) |
| 8 | 353 | | { |
| 8 | 354 | | if (ComputerStatusEvent is not null) await ComputerStatusEvent.Invoke(status); |
| 8 | 355 | | } |
| | 356 | | } |
| | 357 | | } |