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