{"id":201,"date":"2025-09-23T09:00:00","date_gmt":"2025-09-23T09:00:00","guid":{"rendered":"http:\/\/justruss.tech\/index.php\/2022\/12\/04\/my-windows-dfir-checklist-for-initial-triage\/"},"modified":"2026-05-15T10:34:55","modified_gmt":"2026-05-15T10:34:55","slug":"my-windows-dfir-checklist-for-initial-triage","status":"publish","type":"post","link":"https:\/\/justruss.tech\/index.php\/2025\/09\/23\/my-windows-dfir-checklist-for-initial-triage\/","title":{"rendered":"My Windows DFIR Checklist for Initial Triage"},"content":{"rendered":"<p>The first 30 minutes of a Windows incident investigation set the tone for everything that follows. The goal in this phase is not deep analysis. It is answering three questions as quickly and completely as possible: what happened, roughly when, and how widely has it spread. This checklist is built around those questions in order of priority.<\/p>\n<p>This is a working document. It will not cover every scenario but it covers the things that matter most consistently across real incidents and CTF forensics challenges.<\/p>\n<h3>Before touching anything: baseline the system state<\/h3>\n<p>Note the current system time and compare it against a trusted time source. Large clock skew can indicate deliberate tampering to confuse timeline analysis, or simply that the system clock drifted. Either way it affects every timestamp you are about to collect and needs to be documented.<\/p>\n<pre>w32tm \/query \/status\n# Key fields:\n# Stratum: 2 or 3 is normal. 16 means no sync.\n# Last Successful Sync Time: should be recent\n# Source: your domain controller or pool.ntp.org\n\n# Get current time and UTC offset\nGet-Date\n[System.TimeZoneInfo]::Local<\/pre>\n<p>On systems where the case may have legal consequences, photograph the screen showing the current time and system state before any commands are run. Once you start collecting volatile data the state starts changing.<\/p>\n<h3>Phase 1: Volatile data collection (live system only)<\/h3>\n<p>These artefacts exist only in RAM and are lost when the system powers off. Collect them first, before anything else, before you even check the event logs.<\/p>\n<pre>\/\/ Create the output directory\nNew-Item -ItemType Directory -Path C:\\ir -Force\n\n\/\/ Running processes with full detail\nGet-WmiObject Win32_Process | Select-Object ProcessId, ParentProcessId,\n    Name, ExecutablePath, CommandLine,\n    @{N=\"CreationDate\";E={$_.ConvertToDateTime($_.CreationDate)}} |\n    Export-Csv C:\\ir\\processes.csv -NoTypeInformation\n\n\/\/ Map processes to their parent names (makes the process tree readable)\n$procs = Get-WmiObject Win32_Process\n$procs | ForEach-Object {\n    $parent = $procs | Where-Object {$_.ProcessId -eq $_.ParentProcessId}\n    [PSCustomObject]@{\n        PID        = $_.ProcessId\n        PPID       = $_.ParentProcessId\n        Name       = $_.Name\n        ParentName = $parent.Name\n        Path       = $_.ExecutablePath\n        CmdLine    = $_.CommandLine\n        Created    = $_.ConvertToDateTime($_.CreationDate)\n    }\n} | Export-Csv C:\\ir\\process_tree.csv -NoTypeInformation<\/pre>\n<pre>\/\/ Active network connections with process correlation\nGet-NetTCPConnection -State Established | ForEach-Object {\n    $proc = Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue\n    [PSCustomObject]@{\n        LocalAddress  = $_.LocalAddress\n        LocalPort     = $_.LocalPort\n        RemoteAddress = $_.RemoteAddress\n        RemotePort    = $_.RemotePort\n        State         = $_.State\n        PID           = $_.OwningProcess\n        ProcessName   = $proc.Name\n        ProcessPath   = $proc.Path\n    }\n} | Export-Csv C:\\ir\\connections.csv -NoTypeInformation<\/pre>\n<pre>\/\/ DNS cache (reveals domains contacted recently, even if connection is closed)\nGet-DnsClientCache | Select-Object Entry, RecordName, RecordType, TimeToLive,\n    DataLength, Section, Data |\n    Export-Csv C:\\ir\\dns_cache.csv -NoTypeInformation\n\n\/\/ Tip: sort by TTL to find recently resolved entries\nGet-DnsClientCache | Sort-Object TimeToLive | Select-Object -First 30<\/pre>\n<pre>\/\/ Logged in users (both local and remote sessions)\nquery user\nGet-WmiObject Win32_LoggedOnUser | ForEach-Object {\n    $account = $_.Antecedent -match 'Name=\"(.+?)\"' | Out-Null\n    $session = $_.Dependent -match 'LogonId=\"(.+?)\"' | Out-Null\n    Write-Output \"$($Matches[1])\"\n}\n\n\/\/ Open file handles and shared resources\nnet use\nnet share\nGet-WmiObject Win32_NetworkConnection |\n    Select-Object LocalName, RemoteName, Status, ConnectionState<\/pre>\n<pre>\/\/ Services in a non-standard state\nGet-WmiObject Win32_Service |\n    Where-Object { $_.State -ne \"Stopped\" -and $_.StartMode -ne \"Disabled\" } |\n    Select-Object Name, State, StartMode, PathName, StartName |\n    Export-Csv C:\\ir\\services.csv -NoTypeInformation\n\n\/\/ New or recently modified services\nGet-WinEvent -FilterHashtable @{LogName=\"System\"; Id=7045; StartTime=(Get-Date).AddHours(-48)} |\n    ForEach-Object {\n        [xml]$xml = $_.ToXml()\n        [PSCustomObject]@{\n            Time        = $_.TimeCreated\n            ServiceName = $xml.Event.EventData.Data[0].\"#text\"\n            ImagePath   = $xml.Event.EventData.Data[2].\"#text\"\n            StartType   = $xml.Event.EventData.Data[3].\"#text\"\n            Account     = $xml.Event.EventData.Data[4].\"#text\"\n        }\n    }<\/pre>\n<pre>\/\/ Scheduled tasks with full command detail\nschtasks \/query \/fo CSV \/v &gt; C:\\ir\\schtasks.csv\n\n\/\/ PowerShell version with anomaly highlighting\nGet-ScheduledTask | Where-Object {$_.State -ne \"Disabled\"} | ForEach-Object {\n    $action = $_.Actions | Select-Object -First 1\n    [PSCustomObject]@{\n        TaskName    = $_.TaskName\n        TaskPath    = $_.TaskPath\n        State       = $_.State\n        Execute     = $action.Execute\n        Arguments   = $action.Arguments\n        WorkingDir  = $action.WorkingDirectory\n        Author      = $_.Author\n        Description = $_.Description\n    }\n} | Where-Object {\n    # Flag tasks pointing outside standard locations\n    $_.Execute -notmatch \"^C:\\\\Windows\\\\\" -and\n    $_.Execute -notmatch \"^C:\\\\Program Files\"\n} | Export-Csv C:\\ir\\suspicious_tasks.csv -NoTypeInformation<\/pre>\n<h3>Phase 2: Event log triage<\/h3>\n<p>Extract targeted events from the last 48 hours rather than pulling entire log files. Query for the events most likely to show attacker activity first.<\/p>\n<pre>\/\/ Authentication events: find unusual accounts, times, and source IPs\nGet-WinEvent -FilterHashtable @{\n    LogName   = \"Security\"\n    Id        = 4624\n    StartTime = (Get-Date).AddHours(-48)\n} | ForEach-Object {\n    $props = $_.Properties\n    [PSCustomObject]@{\n        Time       = $_.TimeCreated\n        Account    = $props[5].Value\n        Domain     = $props[6].Value\n        LogonType  = $props[8].Value\n        SourceIP   = $props[18].Value\n        WorkStation = $props[13].Value\n        LogonProc  = $props[9].Value\n    }\n} | Where-Object {\n    # Flag non-interactive logons from external IPs or unusual accounts\n    $_.LogonType -in @(3, 10) -and\n    $_.SourceIP -ne \"127.0.0.1\" -and\n    $_.SourceIP -ne \"-\"\n} | Export-Csv C:\\ir\\logons.csv -NoTypeInformation<\/pre>\n<pre>\/\/ Failed logon attempts (possible brute force or credential stuffing)\nGet-WinEvent -FilterHashtable @{LogName=\"Security\"; Id=4625; StartTime=(Get-Date).AddHours(-48)} |\n    ForEach-Object {\n        $props = $_.Properties\n        [PSCustomObject]@{\n            Time         = $_.TimeCreated\n            Account      = $props[5].Value\n            SourceIP     = $props[19].Value\n            FailureReason = $props[9].Value\n        }\n    } | Group-Object Account | Sort-Object Count -Descending | Select-Object -First 20<\/pre>\n<pre>\/\/ New local accounts created\nGet-WinEvent -FilterHashtable @{LogName=\"Security\"; Id=4720; StartTime=(Get-Date).AddHours(-72)} |\n    Select-Object TimeCreated, Message\n\n\/\/ Account added to privileged groups\nGet-WinEvent -FilterHashtable @{\n    LogName   = \"Security\"\n    Id        = @(4728, 4732, 4756)  \/\/ Domain Admins, Local Admins, Universal\n    StartTime = (Get-Date).AddHours(-72)\n} | Select-Object TimeCreated, Id, Message<\/pre>\n<pre>\/\/ Process creation with command lines (requires 4688 + cmdline logging enabled)\nGet-WinEvent -FilterHashtable @{LogName=\"Security\"; Id=4688; StartTime=(Get-Date).AddHours(-24)} |\n    Where-Object {\n        $_.Message -match \"-EncodedCommand|-enc |-nop|-windowstyle hidden|iex |invoke-expression|downloadstring|downloadfile|bypass\"\n    } |\n    Select-Object TimeCreated, Message | Format-List\n\n\/\/ Suspicious parent-child relationships\nGet-WinEvent -FilterHashtable @{LogName=\"Security\"; Id=4688; StartTime=(Get-Date).AddHours(-24)} |\n    Where-Object {\n        $_.Message -match \"Creator Process Name.*\\\\(winword|excel|powerpnt|outlook|mspub)\\.exe\" -and\n        $_.Message -match \"New Process Name.*\\\\(cmd|powershell|wscript|cscript|mshta)\\.exe\"\n    } | Select-Object TimeCreated, Message | Format-List<\/pre>\n<pre>\/\/ PowerShell script block logging (Event ID 4104)\n\/\/ This shows decoded PowerShell content even if it was obfuscated\nGet-WinEvent -FilterHashtable @{\n    LogName   = \"Microsoft-Windows-PowerShell\/Operational\"\n    Id        = 4104\n    Level     = 3  \/\/ Warning level = PowerShell flagged this as suspicious\n    StartTime = (Get-Date).AddHours(-48)\n} | ForEach-Object {\n    [PSCustomObject]@{\n        Time         = $_.TimeCreated\n        ScriptBlock  = $_.Properties[2].Value | Select-Object -First 500\n        ScriptPath   = $_.Properties[4].Value\n    }\n} | Format-List\n\n\/\/ All 4104 events (not just warnings) for full coverage\nGet-WinEvent -FilterHashtable @{\n    LogName   = \"Microsoft-Windows-PowerShell\/Operational\"\n    Id        = 4104\n    StartTime = (Get-Date).AddHours(-24)\n} | Where-Object {\n    $_.Message -match \"DownloadString|DownloadFile|WebClient|IEX|Invoke-Expression|FromBase64|EncodedCommand|Reflection\\.Assembly\"\n} | Select-Object TimeCreated, Message | Format-List<\/pre>\n<h3>Phase 3: Persistence locations<\/h3>\n<p>Check every standard persistence location. Attackers who know their tooling will clean up temporary files but frequently forget persistence mechanisms installed before they established a stable foothold.<\/p>\n<pre>\/\/ Registry run keys across all hives\n$runkeys = @(\n    \"HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run\",\n    \"HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\RunOnce\",\n    \"HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\RunServices\",\n    \"HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Run\",\n    \"HKCU:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run\",\n    \"HKCU:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\RunOnce\"\n)\n\nforeach ($key in $runkeys) {\n    if (Test-Path $key) {\n        $values = Get-ItemProperty $key | Select-Object * -ExcludeProperty PS*\n        $values.PSObject.Properties | Where-Object {$_.Name -ne \"\"} |\n            ForEach-Object {\n                [PSCustomObject]@{\n                    Key   = $key\n                    Name  = $_.Name\n                    Value = $_.Value\n                    # Flag entries pointing outside standard directories\n                    Suspicious = ($_.Value -notmatch \"^C:\\\\Windows\\\\\" -and\n                                  $_.Value -notmatch \"^C:\\\\Program Files\")\n                }\n            } | Format-Table -AutoSize\n    }\n}<\/pre>\n<pre>\/\/ Winlogon hijacking (a common persistence location)\nGet-ItemProperty \"HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\" |\n    Select-Object Shell, Userinit, Taskman\n\n\/\/ Expected values:\n\/\/ Shell: explorer.exe\n\/\/ Userinit: C:\\Windows\\system32\\userinit.exe,\n\/\/ Taskman: (empty or legitimate path)\n\n\/\/ AppInit_DLLs (DLL loaded into every process that loads User32.dll)\nGet-ItemProperty \"HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Windows\" |\n    Select-Object AppInit_DLLs, LoadAppInit_DLLs\nGet-ItemProperty \"HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows NT\\CurrentVersion\\Windows\" |\n    Select-Object AppInit_DLLs, LoadAppInit_DLLs\n\n\/\/ Image File Execution Options (hijacks debugger for specific executables)\nGet-ChildItem \"HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Image File Execution Options\" |\n    ForEach-Object {\n        $debugger = (Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue).Debugger\n        if ($debugger) {\n            Write-Output \"IFEO: $($_.PSChildName) -&gt; Debugger: $debugger\"\n        }\n    }<\/pre>\n<pre>\/\/ WMI persistence (commonly missed in manual IR checklists)\nGet-WMIObject -Namespace root\\subscription -Class __EventFilter |\n    Select-Object Name, Query, QueryLanguage | Format-List\n\nGet-WMIObject -Namespace root\\subscription -Class __EventConsumer |\n    Select-Object Name, CommandLineTemplate, ScriptText | Format-List\n\nGet-WMIObject -Namespace root\\subscription -Class __FilterToConsumerBinding |\n    Select-Object Filter, Consumer | Format-List\n\n\/\/ If any of these have entries you did not create, you have WMI persistence\n\/\/ Common attacker pattern: EventFilter watches for system events (timer, login)\n\/\/ EventConsumer runs a command or script when the filter fires<\/pre>\n<pre>\/\/ Startup folders for all users\nGet-ChildItem \"C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\" -ErrorAction SilentlyContinue\nGet-ChildItem \"$env:APPDATA\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\" -ErrorAction SilentlyContinue\nGet-ChildItem \"C:\\Users\\*\\AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\" -ErrorAction SilentlyContinue<\/pre>\n<h3>Phase 4: Execution artefacts<\/h3>\n<p>These artefacts tell you what ran, when, and often where from, even after the attacker has deleted the tools they used.<\/p>\n<pre>\/\/ Prefetch: execution history with timestamps\n\/\/ Each file records the last 8 run times and referenced file paths\nif (Test-Path C:\\Windows\\Prefetch) {\n    Get-ChildItem C:\\Windows\\Prefetch\\*.pf |\n        Select-Object Name, LastWriteTime, CreationTime, Length |\n        Sort-Object LastWriteTime -Descending |\n        Where-Object { $_.LastWriteTime -gt (Get-Date).AddHours(-48) } |\n        Format-Table -AutoSize\n\n    \/\/ Flag executables with suspicious names that have run recently\n    Get-ChildItem C:\\Windows\\Prefetch\\*.pf |\n        Where-Object {\n            $_.Name -match \"MIMIKATZ|PROCDUMP|POWERSPLOIT|METERPRETER|COBALTSTRIKE|RUBEUS|BLOODHOUND|SHARPHOUND\"\n        } | Format-Table Name, LastWriteTime -AutoSize\n}\n\n\/\/ Parse with PECmd for full detail including referenced files\n\/\/ PECmd.exe -d C:\\Windows\\Prefetch --csv C:\\ir\\ --csvf prefetch.csv<\/pre>\n<pre>\/\/ Amcache: file execution records with SHA1 hashes\n\/\/ Copy for offline analysis (cannot be read while in use by the system)\nif (Test-Path C:\\Windows\\AppCompat\\Programs\\Amcache.hve) {\n    try {\n        Copy-Item C:\\Windows\\AppCompat\\Programs\\Amcache.hve C:\\ir\\Amcache.hve\n        Write-Output \"Amcache copied successfully\"\n    } catch {\n        Write-Output \"Could not copy Amcache (system file in use)\"\n        Write-Output \"Boot from WinPE or use a forensic acquisition tool\"\n    }\n}\n\n\/\/ Parse offline with AmcacheParser.exe:\n\/\/ AmcacheParser.exe -f C:\\ir\\Amcache.hve --csv C:\\ir\\ --csvf amcache.csv<\/pre>\n<pre>\/\/ Shimcache: file presence records\n\/\/ Copy the SYSTEM hive for offline parsing\nreg save HKLM\\SYSTEM C:\\ir\\SYSTEM.hive \/y\n\n\/\/ Parse offline with AppCompatCacheParser.exe:\n\/\/ AppCompatCacheParser.exe -f C:\\ir\\SYSTEM.hive --csv C:\\ir\\ --csvf shimcache.csv\n\n\/\/ Look for executables in unusual locations within the incident time window<\/pre>\n<pre>\/\/ Recently accessed files via LNK files (shows what the user interacted with)\nGet-ChildItem \"$env:APPDATA\\Microsoft\\Windows\\Recent\" -ErrorAction SilentlyContinue |\n    Sort-Object LastWriteTime -Descending | Select-Object Name, LastWriteTime | Select-Object -First 30\n\n\/\/ Shell bags (folder access history, useful for showing attacker navigation)\n\/\/ Requires offline parsing with ShellBagsExplorer against the ntuser.dat hive<\/pre>\n<h3>Phase 5: Quick scope assessment<\/h3>\n<p>Before going deep on any single finding, assess scope. How many systems are affected? What time range? This determines whether you are dealing with a contained incident or an active widespread compromise that needs immediate containment.<\/p>\n<pre>\/\/ If you have Velociraptor deployed, run this hunt across all endpoints:\n\/\/ SELECT Hostname, OS, ProcessId, Name, Exe, CommandLine\n\/\/ FROM pslist()\n\/\/ WHERE Name =~ \"mimikatz|meterpreter|cobaltstrike|empire|metasploit\"\n\n\/\/ If you have Splunk, correlate the source IP from the initial logon\n\/\/ across all other systems:\n\/\/ index=wineventlog EventCode=4624\n\/\/ | where src_ip=\"[suspicious_IP]\"\n\/\/ | stats count by ComputerName, Account_Name\n\/\/ | sort -count\n\n\/\/ If you have Elastic, use EQL correlation:\n\/\/ sequence by host.name with maxspan=10m\n\/\/   [process where process.name == \"cmd.exe\" and process.parent.name == \"winword.exe\"]\n\/\/   [network where destination.port == 443]<\/pre>\n<h3>The most important principle: breadth before depth<\/h3>\n<p>The single most common mistake in initial triage is spending 90 minutes going deep on one suspicious process before completing a broad sweep. In real incidents and in CTF challenges alike, the most obvious suspicious finding is often a decoy or a secondary tool planted after the real persistence mechanism was established.<\/p>\n<p>Complete the full checklist across all five phases first. Document everything you find. Then prioritise which finding to investigate in depth based on the complete picture. The most important finding is usually not the first one you notice.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>After working through enough incidents and CTF challenges, a checklist that<br \/>\ncovers the things that matter most in the first 30 minutes.<\/p>\n","protected":false},"author":0,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[4],"tags":[],"class_list":["post-201","post","type-post","status-publish","format-standard","hentry","category-dfir"],"_links":{"self":[{"href":"https:\/\/justruss.tech\/index.php\/wp-json\/wp\/v2\/posts\/201","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/justruss.tech\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/justruss.tech\/index.php\/wp-json\/wp\/v2\/types\/post"}],"replies":[{"embeddable":true,"href":"https:\/\/justruss.tech\/index.php\/wp-json\/wp\/v2\/comments?post=201"}],"version-history":[{"count":4,"href":"https:\/\/justruss.tech\/index.php\/wp-json\/wp\/v2\/posts\/201\/revisions"}],"predecessor-version":[{"id":275,"href":"https:\/\/justruss.tech\/index.php\/wp-json\/wp\/v2\/posts\/201\/revisions\/275"}],"wp:attachment":[{"href":"https:\/\/justruss.tech\/index.php\/wp-json\/wp\/v2\/media?parent=201"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/justruss.tech\/index.php\/wp-json\/wp\/v2\/categories?post=201"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/justruss.tech\/index.php\/wp-json\/wp\/v2\/tags?post=201"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}