Host
What's running on this box right now?
Get-CimInstance Win32_Process | Select-Object Name, ProcessId, ParentProcessId, CommandLineThe money command. Parent PID plus full command line is how you catch winword.exe spawning powershell.exe -enc.Get-Process | Select-Object Name, Id, Path, Company, StartTime | Sort-Object StartTimeNewest processes at the bottom. Blank Company or a Path under Temp/AppData deserves a look.Get-Process | Sort-Object CPU -Descending | Select-Object -First 20Quick "what's burning cycles" pass. Miners and scanners show up here.Get-Service | Where-Object Status -eq 'Running'Compare against a known-good host. Odd display names and blank descriptions stand out.Get-ScheduledTask | Where-Object State -ne 'Disabled' | Get-ScheduledTaskInfoLastRunTime and NextRunTime. Tasks created in the last day or two are the interesting ones.Get-CimInstance Win32_LoggedOnUserEvery session on the host, including service and network logons.Host
Who's talking to what?
Get-NetTCPConnection -State Established | Select-Object LocalPort, RemoteAddress, RemotePort, OwningProcessPair OwningProcess with Get-Process -Id to name the thing behind the socket.Get-NetTCPConnection -State ListenNew listeners on a workstation are rarely legit. Check the owning process.Get-NetUDPEndpointDon't forget UDP. DNS tunnelling and some C2 lives here.Get-DnsClientCacheWhat the host resolved recently. Fast way to find a C2 domain without packet capture.Resolve-DnsName suspicious-domain.comConfirm what an IOC resolves to right now. Add -Type TXT or -Server 8.8.8.8 as needed.Test-NetConnection 10.0.0.5 -Port 445Can this host reach that share/port? Useful when tracing lateral movement.Get-NetFirewallRule | Where-Object Enabled -eq 'True' | Select-Object DisplayName, Direction, ActionAttackers add allow rules. Sort by DisplayName and look for the ones that don't belong.Host
What's in the event logs?
Always use -FilterHashtable. Piping Get-WinEvent into Where-Object on a busy DC is minutes versus seconds.
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4624; StartTime=(Get-Date).AddHours(-24)}Successful logons, last 24h. LogonType 3 = network, 10 = RDP, 2 = interactive.Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4625} | Select-Object -First 50Failed logons. A burst against many accounts is a spray; many against one is brute force.Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4688}Process creation. Only useful if command-line auditing is on (and it should be).Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4720,4728,4732,4756}Account created, added to global / local / universal group. Privilege escalation trail.Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-PowerShell/Operational'; Id=4104}Script block logging. Obfuscated PowerShell gets decoded for you here.Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-Sysmon/Operational'} -MaxEvents 100If Sysmon is deployed, this beats Security for process, network and file detail.Get-WinEvent -ListLog * | Where-Object RecordCount -gt 0Which logs even have data. Handy on an unfamiliar box.(Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4624})[0].PropertiesPull raw fields when Message parsing gets annoying. Index 5 is usually the account name.Reference
Event IDs worth memorizing
| ID | Log | Meaning |
|---|---|---|
| 4624 | Security | Successful logon |
| 4625 | Security | Failed logon |
| 4648 | Security | Logon with explicit credentials (runas) |
| 4672 | Security | Special privileges assigned (admin logon) |
| 4688 | Security | Process created |
| 4698 | Security | Scheduled task created |
| 4720 | Security | User account created |
| 4724 | Security | Password reset attempt |
| ID | Log | Meaning |
|---|---|---|
| 4728 / 4732 / 4756 | Security | Member added to global / local / universal group |
| 4768 / 4769 | Security | Kerberos TGT / service ticket requested |
| 4776 | Security | NTLM credential validation |
| 1102 | Security | Audit log cleared |
| 7045 | System | New service installed |
| 4104 | PowerShell/Operational | Script block logged (decoded) |
| 1 / 3 / 11 | Sysmon | Process create / network connect / file create |
| 1149 | TerminalServices-RCM | RDP connection accepted |
Host
How did it persist?
Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run'Machine-wide autostarts.Get-ItemProperty 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run'Per-user autostarts. Run as the affected user, or load their hive.Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce'One-shot entries that clean up after themselves.Get-ChildItem "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup"Old trick, still works. Check ProgramData\…\StartUp too.Get-CimInstance Win32_StartupCommandRegistry and folder autostarts in one list.Get-CimInstance -Namespace root\subscription -Class __EventFilterWMI event subscriptions. The one people forget. Look here if the box keeps getting reinfected.Get-CimInstance -Namespace root\subscription -Class CommandLineEventConsumerThe payload half of a WMI subscription. Pair with __FilterToConsumerBinding.Get-CimInstance Win32_Service | Select-Object Name, PathName, StartMode, StateUnquoted paths, binaries in user-writable folders, services with no description.Get-ScheduledTask | Where-Object { $_.Actions.Execute -match 'powershell|cmd|wscript|mshta|rundll32' }Tasks that launch an interpreter instead of a real program.Host
What is this file?
Get-FileHash C:\path\to\file.exe -Algorithm SHA256Hash first, then search VT / your EDR / threat intel. Never upload a possibly sensitive file blind.Get-AuthenticodeSignature C:\path\to\file.exeValid, NotSigned, or HashMismatch. Unsigned in System32 is a red flag.Get-Item C:\path\to\file.exe -Stream *Alternate data streams. Payloads hide behind innocent-looking files.Get-Content C:\path\to\file.exe:Zone.IdentifierMark of the Web. Tells you it was downloaded, and often the source URL.Get-ChildItem C:\Users -Recurse -Include *.exe,*.dll,*.ps1,*.vbs,*.js,*.lnk -ErrorAction SilentlyContinue | Where-Object LastWriteTime -gt (Get-Date).AddDays(-1)Anything executable dropped in user land in the last day.Select-String -Path C:\logs\*.log -Pattern 'mimikatz|sekurlsa|Invoke-Mimikatz|-enc 'Grep for PowerShell. Add your own IOC strings.Get-ChildItem $env:TEMP, C:\Windows\Temp, "$env:LOCALAPPDATA\Temp" | Sort-Object LastWriteTime -Descending | Select-Object -First 30The three temp folders where droppers land.Identity
Who's on this machine and what can they do?
whoami /allCurrent user, groups, privileges. First thing on any shell.query userInteractive and RDP sessions with idle time.Get-LocalUser | Select-Object Name, Enabled, LastLogon, PasswordLastSetLocal accounts. A recently created or re-enabled one is worth a look.Get-LocalGroupMember AdministratorsWho's local admin. Should be short.Get-ADUser -Identity jsmith -Properties LastLogonDate, PasswordLastSet, MemberOf, EnabledThe account in question. MemberOf tells you the blast radius.Get-ADGroupMember 'Domain Admins' -RecursiveRecursive catches nested groups. Compare against the last known-good list.Get-ADUser -Filter * -Properties whenCreated | Where-Object whenCreated -gt (Get-Date).AddDays(-7)Accounts created this week.Get-ADComputer -Filter * -Properties LastLogonDate | Where-Object LastLogonDate -lt (Get-Date).AddDays(-90)Stale machine accounts. Attackers love those.Fleet
Remote triage without RDP
Enter-PSSession -ComputerName WS-042Interactive shell on the box. Exit-PSSession to leave.Invoke-Command -ComputerName WS-042 -ScriptBlock { Get-Process | Where-Object Name -like '*powershell*' }Run one thing remotely and get objects back.Invoke-Command -ComputerName (Get-Content hosts.txt) -ScriptBlock { Get-FileHash C:\Windows\Temp\*.exe }Same command across a list of hosts. Add -ThrottleLimit 50 for big fleets.Invoke-Command -ComputerName WS-042 -FilePath .\triage.ps1Ship a whole script instead of a one-liner.Copy-Item -Path C:\evidence\* -Destination \\forensics\share\WS-042\ -RecurseGet evidence off the host before you touch anything else.Test-WSMan WS-042Is WinRM even listening? Check this before blaming the network.Response
Contain it
Stop-Process -Id 4812 -ForceKill by PID. Grab a memory dump first if you'll want it later.Stop-Service -Name BadService; Set-Service -Name BadService -StartupType DisabledStop it and keep it stopped.Disable-LocalUser -Name backdoorLocal account off.Disable-ADAccount -Identity compromised.userDomain account off. Reset the password too, then kill Kerberos tickets.New-NetFirewallRule -DisplayName 'Block C2' -Direction Outbound -RemoteAddress 203.0.113.10 -Action BlockHost-level block while you wait on the perimeter change.Get-MpThreatDetection; Get-MpThreatWhat Defender already saw and did.Start-MpScan -ScanType FullScanFull scan. Use -ScanType QuickScan when you need an answer now.Unregister-ScheduledTask -TaskName 'Updater' -Confirm:$falseRemove a malicious task after you've exported it with Export-ScheduledTask.Before you contain: hash it, copy it, screenshot it. Containment destroys evidence. Order matters.
Tenant
Microsoft 365 and Entra ID
Inbox rules, forwarding, new MFA methods, and consented OAuth apps are the four places BEC attackers leave fingerprints. Check all four every time.
Connect-ExchangeOnlineExchange Online module. Needs ExchangeOnlineManagement installed.Get-InboxRule -Mailbox [email protected] | Select-Object Name, Enabled, ForwardTo, RedirectTo, DeleteMessage, MoveToFolderRules that forward, delete, or bury mail in RSS Feeds. The classic BEC tell.Get-Mailbox -ResultSize Unlimited | Where-Object { $_.ForwardingSmtpAddress -or $_.ForwardingAddress }Mailbox-level forwarding across the whole tenant.Search-UnifiedAuditLog -StartDate (Get-Date).AddDays(-7) -EndDate (Get-Date) -UserIds [email protected] -ResultSize 5000Everything one user did this week. AuditData is JSON; pipe through ConvertFrom-Json.Search-UnifiedAuditLog -StartDate (Get-Date).AddDays(-7) -EndDate (Get-Date) -Operations New-InboxRule,Set-InboxRule,Add-MailboxPermission,UpdateInboxRulesRule and permission changes tenant-wide.Get-MessageTrace -SenderAddress [email protected] -StartDate (Get-Date).AddDays(-2) -EndDate (Get-Date)Who else got the phish. 10-day window; older needs Start-HistoricalSearch.Get-MailboxPermission -Identity [email protected] | Where-Object User -notlike 'NT AUTHORITY\SELF'Delegates that shouldn't be there.Connect-MgGraph -Scopes AuditLog.Read.All,Directory.Read.All,User.ReadWrite.AllMicrosoft Graph module. Scope list is the minimum for the commands below.Get-MgAuditLogSignIn -Filter "userPrincipalName eq '[email protected]'" -Top 50 | Select-Object CreatedDateTime, IpAddress, AppDisplayName, Status, LocationSign-ins with IP and location. Impossible travel shows up fast.Get-MgAuditLogDirectoryAudit -Filter "activityDisplayName eq 'Add member to role'"Role assignments. Also try 'Consent to application' and 'Update user'.Get-MgUserAuthenticationMethod -UserId [email protected]MFA methods. A new phone number or authenticator you don't recognise means the attacker registered it.Get-MgUserRegisteredDevice -UserId [email protected]Devices tied to the account.Get-MgUserOauth2PermissionGrant -UserId [email protected]OAuth apps the user consented to. Illicit consent grants survive a password reset.Get-MgServicePrincipal -Filter "servicePrincipalType eq 'Application'" | Select-Object DisplayName, AppId, AccountEnabledEnterprise apps in the tenant. Look for the ones added recently with generic names.Revoke-MgUserSignInSession -UserId [email protected]Kill every refresh token. Do this after the password reset, not before.Update-MgUser -UserId [email protected] -AccountEnabled:$falseBlock sign-in at the tenant level.Practice
Habits that matter more than any single command
Run Start-Transcript at the beginning of any hands-on-keyboard triage. Your own actions become part of the timeline, and you'll want them documented when someone asks what changed.
Learn Select-Object, Where-Object, Sort-Object, Group-Object, and Format-Table -AutoSize cold. They're the glue for everything else on this page.
Pipe anything you're about to hand off through Export-Csv -NoTypeInformation so it lands in a ticket cleanly. Out-GridView is fine for you; nobody else wants a screenshot of it.
Use Get-Help <cmd> -Examples when you forget syntax rather than guessing. Get-Member tells you what properties an object actually has, which beats reading docs.
Filter left, format right. Put -Filter and -FilterHashtable on the cmdlet itself instead of piping everything through Where-Object. Save Format-Table for the very end.
Collect before you contain. Hashes, copies, memory, screenshots, then kill. The order is the difference between a clean root cause and a shrug.