This is the triage checklist I actually use at the start of a Windows incident or serious CTF challenge. It is not exhaustive — deep-dive analysis follows once scope is understood. The goal of this phase is to answer three questions: what
happened, roughly when, and how widely has it spread?
Phase 0 — Before touching anything
If the system is live and you have time:
# Note the current system time and verify it against a trusted source w32tm /query /status # Check "Stratum" and "Last Successful Sync Time" - large skew = potential tampering # Confirm your investigation user has the right privileges whoami /priv | findstr /i "SeDebugPrivilege\|SeBackupPrivilege" # SeDebugPrivilege is needed for memory access and some NTFS operations
Document the system state before any commands run. Take a screenshot of Task Manager. Note the system time. These baselines matter if the case goes to legal proceedings.
Phase 1 — Volatile data collection (live system only)
# Running processes with full paths and command lines
Get-WmiObject Win32_Process | Select-Object ProcessId, ParentProcessId, Name, ExecutablePath, CommandLine | Export-Csv C:\ir\processes.csv
# Network connections with owning process
Get-NetTCPConnection | Select-Object LocalAddress, LocalPort, RemoteAddress, RemotePort, State, OwningProcess | Export-Csv C:\ir\connections.csv
# Cross-reference connection PIDs with process list
$procs = Get-Process -IncludeUserName
Get-NetTCPConnection -State Established | ForEach-Object {
$proc = $procs | Where-Object {$_.Id -eq $_.OwningProcess}
[PSCustomObject]@{
Remote = "$($_.RemoteAddress):$($_.RemotePort)"
PID = $_.OwningProcess
Process = $proc.Name
Path = $proc.Path
User = $proc.UserName
}
} | Export-Csv C:\ir\active_connections.csv
# Scheduled tasks - full details including actions and triggers
schtasks /query /fo CSV /v > C:\ir\schtasks.csv
# Services - include binary path and start type
Get-WmiObject Win32_Service | Select-Object Name, State, StartMode, PathName, StartName | Export-Csv C:\ir\services.csv
# Loaded drivers
Get-WmiObject Win32_SystemDriver | Select-Object Name, State, PathName | Export-Csv C:\ir\drivers.csv
# DNS cache (can reveal C2 domains contacted)
Get-DnsClientCache | Export-Csv C:\ir\dns_cache.csv
Phase 2 — Event log triage
Extract targeted events from the last 48 hours rather than pulling entire log files:
# Account logon events - identify unusual accounts or times
Get-WinEvent -FilterHashtable @{
LogName = "Security"
Id = 4624
StartTime = (Get-Date).AddHours(-48)
} | Where-Object {
$_.Properties[8].Value -ne 5 # Exclude service logons
} | Select-Object TimeCreated,
@{N="Account";E={$_.Properties[5].Value}},
@{N="LogonType";E={$_.Properties[8].Value}},
@{N="SourceIP";E={$_.Properties[18].Value}}
# New local accounts created
Get-WinEvent -FilterHashtable @{LogName="Security"; Id=4720; StartTime=(Get-Date).AddHours(-48)}
# Services installed (common persistence)
Get-WinEvent -FilterHashtable @{LogName="System"; Id=7045; StartTime=(Get-Date).AddHours(-48)} |
ForEach-Object {
[xml]$xml = $_.ToXml()
[PSCustomObject]@{
Time = $_.TimeCreated
ServiceName = $xml.Event.EventData.Data[0]."#text"
ImagePath = $xml.Event.EventData.Data[2]."#text"
}
}
# PowerShell script block logging - look for suspicious content
Get-WinEvent -FilterHashtable @{
LogName = "Microsoft-Windows-PowerShell/Operational"
Id = 4104
Level = 3 # Warning level = flagged by PowerShell as suspicious
StartTime = (Get-Date).AddHours(-48)
} | Select-Object TimeCreated, Message | Format-List
Phase 3 — Persistence locations
# Registry Run keys - all users
$runkeys = @(
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run",
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce",
"HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run",
"HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce",
"HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Run"
)
foreach ($key in $runkeys) {
if (Test-Path $key) {
Get-ItemProperty $key | Select-Object * -ExcludeProperty PS*
}
}
# WMI event subscriptions - commonly missed in IR checklists
Get-WMIObject -Namespace root\subscription -Class __EventFilter
Get-WMIObject -Namespace root\subscription -Class __EventConsumer
Get-WMIObject -Namespace root\subscription -Class __FilterToConsumerBinding
# AppInit_DLLs - DLL injection via registry
Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows" -Name AppInit_DLLs
Get-ItemProperty "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows NT\CurrentVersion\Windows" -Name AppInit_DLLs
# COM object hijacking candidates
$userCOM = "HKCU:\SOFTWARE\Classes\CLSID"
if (Test-Path $userCOM) {
Get-ChildItem $userCOM | Select-Object Name, LastWriteTime
}
# Startup folders
Get-ChildItem "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup" -ErrorAction SilentlyContinue
Get-ChildItem "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup" -ErrorAction SilentlyContinue
Phase 4 — Execution artefacts
# Prefetch - execution history
if (Test-Path C:\Windows\Prefetch) {
Get-ChildItem C:\Windows\Prefetch\*.pf |
Select-Object Name, LastWriteTime, CreationTime |
Sort-Object LastWriteTime -Descending |
Where-Object { $_.LastWriteTime -gt (Get-Date).AddHours(-48) }
}
# Amcache - file execution with hashes (run PECmd/AmcacheParser offline)
if (Test-Path C:\Windows\AppCompat\Programs\Amcache.hve) {
Write-Output "Amcache.hve present - copy for offline analysis"
Copy-Item C:\Windows\AppCompat\Programs\Amcache.hve C:\ir\
}
# Recently accessed files (Shellbags, LNK files)
Get-ChildItem "$env:APPDATA\Microsoft\Windows\Recent" -ErrorAction SilentlyContinue |
Sort-Object LastWriteTime -Descending | Select-Object Name, LastWriteTime | head -20
The most common mistake
Spending the first 90 minutes going deep on a single suspicious process before completing broad triage. The suspicious process is often a distraction — a secondary tool run after the real persistence mechanism was already established. Complete
the full checklist first. The most important finding is usually not the most obvious one.