PowerShell Mastery for Senior SOC Analysts
Advanced Threat Hunting, Detection Engineering & Incident Response
🎯 Why This Matters for Senior SOC Analysts
As a Senior SOC Analyst, PowerShell is your force multiplier. While junior analysts click through GUIs, you're automating threat hunts across 10,000 endpoints. While others wait for SIEM alerts, you're proactively hunting APT persistence. This guide assumes you know security fundamentals and focuses on making you a PowerShell-wielding threat hunter.
Key Focus Areas:
Advanced threat hunting at scale
Custom detection engineering
Automated incident response playbooks
SIEM integration and log enrichment
Cross-endpoint behavioural analysis
Malware analysis and deobfuscation
🛡️ Senior SOC Analyst's Advanced PowerShell Cheatsheet
Advanced Discovery & Intelligence
# Discover ALL cmdlets related to security
Get-Command -Module Microsoft.PowerShell.Security, Defender, NetSecurity
# Find commands by capability
Get-Command -ParameterName ComputerName # All cmdlets supporting remote execution
Get-Command -Verb Invoke # Execution-related cmdlets
# Deep object inspection
Get-Process | Get-Member -MemberType Properties
$event | Format-List -Property * -Force # Show ALL properties including hidden
# Discover Sysmon capabilities
Get-WinEvent -ListLog * | Where-Object {$_.LogName -like "*Sysmon*"}
Get-WinEvent -ListProvider *Sysmon* | Select-Object -ExpandProperty EventsAPT & Advanced Threat Hunting
# === MITRE ATT&CK T1218: LOLBin Abuse Detection ===
$LOLBins = @(
"certutil.exe", "bitsadmin.exe", "mshta.exe", "rundll32.exe",
"regsvr32.exe", "installutil.exe", "msbuild.exe", "cscript.exe",
"wscript.exe", "regasm.exe", "regsvcs.exe", "msiexec.exe"
)
Get-WinEvent -FilterHashtable @{LogName='Security'; ID=4688} -MaxEvents 5000 |
Where-Object {
$cmdline = $_.Properties[8].Value
$procName = $_.Properties[5].Value
$LOLBins | Where-Object {
$cmdline -like "*$_*" -and (
$cmdline -match "http" -or
$cmdline -match "script:" -or
$cmdline -match "javascript:" -or
$cmdline -match "-enc" -or
$cmdline -match "\.tmp"
)
}
} | Select-Object TimeCreated,
@{N='Process';E={$_.Properties[5].Value}},
@{N='CommandLine';E={$_.Properties[8].Value}},
@{N='User';E={$_.Properties[1].Value}},
@{N='ParentProcess';E={$_.Properties[13].Value}}
# === MITRE ATT&CK T1021.006: Lateral Movement via PSRemoting ===
Get-WinEvent -FilterHashtable @{
LogName='Microsoft-Windows-PowerShell/Operational'
ID=4103,4104
} -MaxEvents 10000 | Where-Object {
$_.Message -match "Enter-PSSession|Invoke-Command|New-PSSession"
} | Select-Object TimeCreated,
@{N='User';E={$_.UserId}},
@{N='ScriptBlock';E={$_.Properties[2].Value}},
@{N='Path';E={$_.Properties[4].Value}}
# === MITRE ATT&CK T1003: Credential Dumping Detection ===
# Hunt for LSASS access attempts
Get-WinEvent -FilterHashtable @{LogName='Security'; ID=4656} -MaxEvents 5000 |
Where-Object {
$_.Properties[6].Value -like "*lsass.exe*"
} | Select-Object TimeCreated,
@{N='Process';E={$_.Properties[11].Value}},
@{N='User';E={$_.Properties[1].Value}},
@{N='AccessMask';E={$_.Properties[9].Value}}
# Sysmon Event 10: LSASS access (Mimikatz signature)
Get-WinEvent -FilterHashtable @{
LogName='Microsoft-Windows-Sysmon/Operational'
ID=10
} | Where-Object {
$target = $_.Properties[4].Value # TargetImage
$granted = $_.Properties[6].Value # GrantedAccess
$target -like "*lsass.exe" -and $granted -in @("0x1010", "0x1410", "0x1438")
} | Select-Object TimeCreated,
@{N='SourceProcess';E={$_.Properties[2].Value}},
@{N='SourceUser';E={$_.Properties[3].Value}},
@{N='GrantedAccess';E={$_.Properties[6].Value}}
# === MITRE ATT&CK T1558.003: Kerberoasting Detection ===
Get-WinEvent -FilterHashtable @{LogName='Security'; ID=4769} -MaxEvents 10000 |
Where-Object {
$ticketEncryption = $_.Properties[7].Value
$serviceName = $_.Properties[0].Value
$ticketEncryption -eq "0x17" -and # RC4 encryption (weak)
$serviceName -notlike "*$*" # Not a machine account
} | Select-Object TimeCreated,
@{N='Account';E={$_.Properties[1].Value}},
@{N='ServiceName';E={$_.Properties[0].Value}},
@{N='SourceIP';E={$_.Properties[6].Value}},
@{N='TicketOptions';E={$_.Properties[5].Value}} |
Group-Object Account | Where-Object {$_.Count -gt 5} # Multiple SPN requests
# === MITRE ATT&CK T1003.006: DCSync Attack Detection ===
Get-WinEvent -FilterHashtable @{LogName='Security'; ID=4662} -MaxEvents 5000 |
Where-Object {
$props = $_.Properties[8].Value
$props -match "1131f6aa-9c07-11d1-f79f-00c04fc2dcd2" -or # DS-Replication-Get-Changes
$props -match "1131f6ad-9c07-11d1-f79f-00c04fc2dcd2" -or # DS-Replication-Get-Changes-All
$props -match "89e95b76-444d-4c62-991a-0facbeda640c" # DS-Replication-Get-Changes-In-Filtered-Set
} | Select-Object TimeCreated,
@{N='User';E={$_.Properties[1].Value}},
@{N='Object';E={$_.Properties[6].Value}},
@{N='SourceIP';E={$_.Properties[18].Value}}
# === MITRE ATT&CK T1053.005: Scheduled Task Persistence ===
Get-WinEvent -FilterHashtable @{LogName='Security'; ID=4698} -MaxEvents 1000 |
Where-Object {
$taskContent = $_.Properties[0].Value
$taskContent -match "powershell|cmd|wscript|cscript|mshta|regsvr32|rundll32"
} | Select-Object TimeCreated,
@{N='TaskName';E={$_.Properties[1].Value}},
@{N='User';E={$_.Properties[3].Value}},
@{N='TaskContent';E={$_.Properties[0].Value}}
# === MITRE ATT&CK T1543.003: Windows Service Persistence ===
Get-WinEvent -FilterHashtable @{LogName='System'; ID=7045} -MaxEvents 1000 |
Where-Object {
$servicePath = $_.Properties[1].Value
$servicePath -notmatch "C:\\Windows\\" -and
$servicePath -notmatch "C:\\Program Files" -or
$servicePath -match "powershell|cmd|wscript|%COMSPEC%"
} | Select-Object TimeCreated,
@{N='ServiceName';E={$_.Properties[0].Value}},
@{N='ImagePath';E={$_.Properties[1].Value}},
@{N='ServiceType';E={$_.Properties[2].Value}},
@{N='StartType';E={$_.Properties[4].Value}}
# === MITRE ATT&CK T1546: Event Triggered Execution ===
# WMI Event Subscriptions
Get-CimInstance -Namespace root/subscription -Class __EventFilter
Get-CimInstance -Namespace root/subscription -Class __EventConsumer
Get-CimInstance -Namespace root/subscription -Class __FilterToConsumerBinding
# Hunt for malicious WMI persistence
Get-CimInstance -Namespace root/subscription -Class __EventFilter |
Where-Object {
$_.Query -match "powershell|cmd|wscript" -or
$_.Query -match "http" -or
$_.Name -notlike "SCM*"
}Process Analysis & Memory Hunting
# Deep process inspection with parent-child relationships
Get-CimInstance Win32_Process | Select-Object ProcessId, Name,
ParentProcessId, CommandLine, ExecutablePath, CreationDate |
ForEach-Object {
$parent = Get-Process -Id $_.ParentProcessId -ErrorAction SilentlyContinue
$_ | Add-Member -NotePropertyName ParentName -NotePropertyValue $parent.Name -PassThru
} | Where-Object {
# Suspicious parent-child relationships
($_.Name -eq "cmd.exe" -and $_.ParentName -eq "wmiprvse.exe") -or
($_.Name -eq "powershell.exe" -and $_.ParentName -eq "winword.exe") -or
($_.Name -eq "powershell.exe" -and $_.ParentName -eq "excel.exe") -or
($_.Name -like "*.exe" -and $_.ParentName -eq "wscript.exe")
}
# Hunt for process hollowing (mismatched disk path vs memory)
Get-Process | Where-Object {$_.Path} | ForEach-Object {
$proc = $_
$expectedName = [System.IO.Path]::GetFileNameWithoutExtension($proc.Path)
if ($proc.ProcessName -ne $expectedName) {
[PSCustomObject]@{
PID = $proc.Id
ProcessName = $proc.ProcessName
ExpectedName = $expectedName
ActualPath = $proc.Path
CommandLine = (Get-CimInstance Win32_Process -Filter "ProcessId = $($proc.Id)").CommandLine
Suspicious = "Process Hollowing Indicator"
}
}
}
# Detect process injection via loaded DLLs
Get-Process | Where-Object {$_.Path} | ForEach-Object {
$proc = $_
$unexpectedDLLs = $proc.Modules | Where-Object {
$_.ModuleName -notmatch "^(kernel32|ntdll|user32|advapi32|msvcrt|ws2_32|ole32)" -and
$_.FileName -notmatch "C:\\Windows\\" -and
$_.FileName -notmatch "C:\\Program Files"
}
if ($unexpectedDLLs) {
[PSCustomObject]@{
ProcessName = $proc.Name
PID = $proc.Id
Path = $proc.Path
UnexpectedDLLs = ($unexpectedDLLs.ModuleName -join ", ")
DLLPaths = ($unexpectedDLLs.FileName -join "; ")
}
}
}
# Hunt for unsigned or suspicious processes
Get-Process | Where-Object {$_.Path} | ForEach-Object {
$sig = Get-AuthenticodeSignature -FilePath $_.Path -ErrorAction SilentlyContinue
[PSCustomObject]@{
Name = $_.Name
PID = $_.Id
Path = $_.Path
Signed = ($sig.Status -eq 'Valid')
Signer = $sig.SignerCertificate.Subject
Company = $_.Company
CommandLine = (Get-CimInstance Win32_Process -Filter "ProcessId = $($_.Id)").CommandLine
}
} | Where-Object {
-not $_.Signed -or
$_.Company -eq $null -or
($_.Path -like "*\Temp\*") -or
($_.Path -like "*\AppData\Local\*" -and $_.Path -notlike "*\Microsoft\*")
}
# Reconnaissance command detection in process command lines
$ReconPatterns = @(
"whoami /all", "net user", "net group", "net localgroup administrators",
"ipconfig /all", "systeminfo", "tasklist", "query user", "quser",
"nltest", "dsquery", "ldapsearch", "bloodhound", "sharphound",
"klist", "cmdkey", "netstat -ano", "route print", "arp -a"
)
Get-CimInstance Win32_Process | Where-Object {
$cmdline = $_.CommandLine
$ReconPatterns | Where-Object {$cmdline -match [regex]::Escape($_)}
} | Select-Object ProcessId, Name,
@{N='CommandLine';E={$_.CommandLine}},
@{N='User';E={$_.GetOwner().User}},
CreationDate | Sort-Object CreationDate -DescendingNetwork-Based Threat Hunting
# === C2 Beaconing Detection via Connection Timing Analysis ===
function Find-BeaconingBehavior {
param(
[int]$MinConnections = 5,
[int]$MaxStdDev = 10,
[int]$MinInterval = 5,
[int]$MaxInterval = 3600
)
$connections = Get-NetTCPConnection -State Established
$grouped = $connections | Group-Object -Property RemoteAddress |
Where-Object {$_.Count -ge $MinConnections}
foreach ($group in $grouped) {
$remoteIP = $group.Name
$conns = $group.Group | Sort-Object CreationTime
if ($conns.Count -lt 2) { continue }
# Calculate inter-connection intervals
$intervals = for ($i = 1; $i -lt $conns.Count; $i++) {
($conns[$i].CreationTime - $conns[$i-1].CreationTime).TotalSeconds
}
if ($intervals.Count -eq 0) { continue }
$avgInterval = ($intervals | Measure-Object -Average).Average
$variance = ($intervals | ForEach-Object {
[Math]::Pow($_ - $avgInterval, 2)
} | Measure-Object -Average).Average
$stdDev = [Math]::Sqrt($variance)
# Regular, consistent timing suggests automated beaconing
if ($stdDev -lt $MaxStdDev -and
$avgInterval -gt $MinInterval -and
$avgInterval -lt $MaxInterval) {
$process = Get-Process -Id $conns[0].OwningProcess -ErrorAction SilentlyContinue
[PSCustomObject]@{
RemoteIP = $remoteIP
ConnectionCount = $conns.Count
AvgIntervalSec = [Math]::Round($avgInterval, 2)
StdDeviation = [Math]::Round($stdDev, 2)
Jitter = [Math]::Round(($stdDev / $avgInterval) * 100, 2)
ProcessName = $process.Name
ProcessPath = $process.Path
PID = $conns[0].OwningProcess
LocalPort = $conns[0].LocalPort
RemotePort = $conns[0].RemotePort
Confidence = if ($stdDev -lt 5) {"High"} elseif ($stdDev -lt 10) {"Medium"} else {"Low"}
}
}
}
}
Find-BeaconingBehavior | Format-Table -AutoSize
# === DNS Tunneling & DGA Domain Detection ===
function Find-SuspiciousDNS {
Get-DnsClientCache | Where-Object {
$name = $_.Name
$subdomain = ($name -split '\.')[0]
# Suspicious indicators
$subdomain.Length -gt 30 -or # Unusually long subdomain (data exfil)
($name -match '^[a-z0-9]{15,}\.') -or # Random-looking (DGA)
($name -match '\.(tk|ml|ga|cf|gq|xyz|top|club|work|click)$') -or # Suspicious TLDs
($subdomain -match '^[A-Za-z0-9+/=]{20,}$') -or # Base64-like encoding
(($name -split '\.').Count -gt 5) # Too many subdomains
} | Select-Object Name, Type, TimeToLive, Data, Status,
@{N='SubdomainLength';E={($_.Name -split '\.')[0].Length}},
@{N='Entropy';E={
$chars = ($_.Name -split '\.')[0].ToCharArray()
$freq = $chars | Group-Object | ForEach-Object {$_.Count / $chars.Count}
-($freq | ForEach-Object {$_ * [Math]::Log($_,2)} | Measure-Object -Sum).Sum
}}
}
Find-SuspiciousDNS | Sort-Object Entropy -Descending | Format-Table -AutoSize
# === Comprehensive Network Connection Analysis ===
Get-NetTCPConnection -State Established | ForEach-Object {
$process = Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue
$sig = if ($process.Path) {
Get-AuthenticodeSignature -FilePath $process.Path -ErrorAction SilentlyContinue
}
[PSCustomObject]@{
LocalAddress = $_.LocalAddress
LocalPort = $_.LocalPort
RemoteAddress = $_.RemoteAddress
RemotePort = $_.RemotePort
State = $_.State
ProcessName = $process.Name
ProcessPath = $process.Path
PID = $_.OwningProcess
Company = $process.Company
Signed = ($sig.Status -eq 'Valid')
CreationTime = $_.CreationTime
}
} | Where-Object {
# Filter for suspicious characteristics
(
$_.RemotePort -notin @(80, 443, 53, 22, 445, 3389) -and
$_.RemoteAddress -notmatch '^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.)' -and
$_.RemoteAddress -ne '127.0.0.1'
) -or
(
-not $_.Signed -and $_.ProcessPath -notmatch 'C:\\Windows\\'
) -or
(
$_.Company -eq $null -and $_.ProcessPath
)
} | Sort-Object CreationTime -Descending
# === Port Scanning Detection ===
function Find-PortScanning {
param([int]$TimeWindowMinutes = 15, [int]$ConnectionThreshold = 50)
$StartTime = (Get-Date).AddMinutes(-$TimeWindowMinutes)
$RecentConnections = Get-NetTCPConnection | Where-Object {
$_.CreationTime -gt $StartTime
}
$RecentConnections | Group-Object -Property OwningProcess | Where-Object {
$_.Count -gt $ConnectionThreshold
} | ForEach-Object {
$process = Get-Process -Id $_.Name -ErrorAction SilentlyContinue
$uniqueIPs = ($_.Group | Select-Object -ExpandProperty RemoteAddress -Unique).Count
$uniquePorts = ($_.Group | Select-Object -ExpandProperty RemotePort -Unique).Count
[PSCustomObject]@{
ProcessName = $process.Name
ProcessPath = $process.Path
Company = $process.Company
PID = $_.Name
TotalConnections = $_.Count
UniqueDestIPs = $uniqueIPs
UniqueDestPorts = $uniquePorts
ScanIndicator = ($uniquePorts -gt 20 -or $uniqueIPs -gt 20)
PortDiversity = [Math]::Round($uniquePorts / $_.Count, 2)
}
} | Where-Object {$_.ScanIndicator -eq $true}
}
Find-PortScanning | Format-Table -AutoSize
# === Suspicious Listening Services ===
Get-NetTCPConnection -State Listen | ForEach-Object {
$process = Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue
$sig = if ($process.Path) {
Get-AuthenticodeSignature -FilePath $process.Path -ErrorAction SilentlyContinue
}
[PSCustomObject]@{
LocalAddress = $_.LocalAddress
LocalPort = $_.LocalPort
ProcessName = $process.Name
ProcessPath = $process.Path
Company = $process.Company
Signed = ($sig.Status -eq 'Valid')
PID = $_.OwningProcess
}
} | Where-Object {
# Known good ports (exclude)
$standardPorts = @(135, 445, 139, 3389, 5985, 5986, 49152..65535)
($_.LocalPort -notin $standardPorts) -and
(
$_.Company -notlike "Microsoft*" -or
-not $_.Signed -or
$_.ProcessPath -like "*\Temp\*" -or
$_.ProcessPath -like "*\AppData\*"
)
} | Sort-Object LocalPortAdvanced Event Log Forensics
# === PowerShell Script Block Logging Analysis ===
# Hunt for encoded/obfuscated PowerShell
Get-WinEvent -FilterHashtable @{
LogName='Microsoft-Windows-PowerShell/Operational'
ID=4104
} -MaxEvents 5000 | Where-Object {
$scriptBlock = $_.Properties[2].Value
# Obfuscation indicators
$scriptBlock -match "frombase64string" -or
$scriptBlock -match "\-enc.*[A-Za-z0-9+/=]{50,}" -or
$scriptBlock -match "invoke-expression.*\(" -or
$scriptBlock -match "iex\s+\(" -or
$scriptBlock -match "invoke-command.*scriptblock" -or
$scriptBlock -match "\.downloadstring" -or
$scriptBlock -match "\.downloadfile" -or
$scriptBlock -match "bitstransfer" -or
$scriptBlock -match "start-bitstransfer" -or
$scriptBlock -match "reflection\.assembly" -or
$scriptBlock -match "bypass.*executionpolicy" -or
$scriptBlock -match "noprofile.*noninteractive"
} | Select-Object TimeCreated,
@{N='User';E={$_.UserId}},
@{N='ScriptBlock';E={$_.Properties[2].Value}},
@{N='Path';E={$_.Properties[4].Value}}
# === Decode Base64 PowerShell Commands ===
function Decode-Base64Command {
param([string]$EncodedCommand)
try {
$bytes = [System.Convert]::FromBase64String($EncodedCommand)
[System.Text.Encoding]::Unicode.GetString($bytes)
} catch {
"Failed to decode: $_"
}
}
# Extract and decode from event logs
Get-WinEvent -FilterHashtable @{LogName='Security'; ID=4688} -MaxEvents 1000 |
Where-Object {$_.Properties[8].Value -match "-enc\s+([A-Za-z0-9+/=]+)"} |
ForEach-Object {
$encoded = $matches[1]
[PSCustomObject]@{
Time = $_.TimeCreated
User = $_.Properties[1].Value
Process = $_.Properties[5].Value
EncodedCommand = $encoded.Substring(0, [Math]::Min(50, $encoded.Length)) + "..."
DecodedCommand = Decode-Base64Command -EncodedCommand $encoded
}
}
# === AMSI Bypass Detection ===
Get-WinEvent -FilterHashtable @{
LogName='Microsoft-Windows-PowerShell/Operational'
ID=4104
} -MaxEvents 5000 | Where-Object {
$_.Properties[2].Value -match "amsi" -and (
$_.Properties[2].Value -match "AmsiScanBuffer" -or
$_.Properties[2].Value -match "AmsiInitFailed" -or
$_.Properties[2].Value -match "Reflection.Assembly.*Load" -or
$_.Properties[2].Value -match "\[Ref\]\.Assembly" -or
$_.Properties[2].Value -match "PatchAmsi" -or
$_.Properties[2].Value -match "0x5fc" -or # AMSI buffer offset
$_.Properties[2].Value -match "0x8007" # AMSI error code
)
}
# === Advanced Log Correlation ===
# Correlate Security 4688 (process creation) with Sysmon 1 (process create)
function Get-CorrelatedProcessCreation {
param(
[datetime]$StartTime = (Get-Date).AddHours(-1),
[datetime]$EndTime = (Get-Date)
)
$secEvents = Get-WinEvent -FilterHashtable @{
LogName='Security'
ID=4688
StartTime=$StartTime
EndTime=$EndTime
}
$sysmonEvents = Get-WinEvent -FilterHashtable @{
LogName='Microsoft-Windows-Sysmon/Operational'
ID=1
StartTime=$StartTime
EndTime=$EndTime
} -ErrorAction SilentlyContinue
foreach ($secEvent in $secEvents) {
$processName = $secEvent.Properties[5].Value
$commandLine = $secEvent.Properties[8].Value
$user = $secEvent.Properties[1].Value
$eventTime = $secEvent.TimeCreated
# Find matching Sysmon event (within 2 seconds)
$matchingSysmon = $sysmonEvents | Where-Object {
$timeDiff = [Math]::Abs(($_.TimeCreated - $eventTime).TotalSeconds)
$timeDiff -lt 2 -and $_.Properties[4].Value -eq $processName
} | Select-Object -First 1
[PSCustomObject]@{
Time = $eventTime
ProcessName = $processName
CommandLine = $commandLine
User = $user
ParentProcess = if ($matchingSysmon) {$matchingSysmon.Properties[20].Value}
Hashes = if ($matchingSysmon) {$matchingSysmon.Properties[12].Value}
SysmonCorrelated = ($null -ne $matchingSysmon)
}
}
}
# === Timeline Generation for Incident Response ===
function New-IncidentTimeline {
param(
[datetime]$StartTime,
[datetime]$EndTime,
[string]$TargetUser = "*",
[string]$TargetHost = $env:COMPUTERNAME
)
$timeline = @()
# Process creation events
$timeline += Get-WinEvent -FilterHashtable @{
LogName='Security'
ID=4688
StartTime=$StartTime
EndTime=$EndTime
} | ForEach-Object {
[PSCustomObject]@{
Timestamp = $_.TimeCreated
EventType = "ProcessCreation"
User = $_.Properties[1].Value
Details = "$($_.Properties[5].Value) - $($_.Properties[8].Value)"
Severity = "Info"
}
}
# Logon events
$timeline += Get-WinEvent -FilterHashtable @{
LogName='Security'
ID=4624,4625
StartTime=$StartTime
EndTime=$EndTime
} | ForEach-Object {
[PSCustomObject]@{
Timestamp = $_.TimeCreated
EventType = if ($_.Id -eq 4624) {"SuccessfulLogon"} else {"FailedLogon"}
User = $_.Properties[5].Value
Details = "LogonType: $($_.Properties[8].Value), Source: $($_.Properties[18].Value)"
Severity = if ($_.Id -eq 4625) {"Warning"} else {"Info"}
}
}
# PowerShell script execution
$timeline += Get-WinEvent -FilterHashtable @{
LogName='Microsoft-Windows-PowerShell/Operational'
ID=4104
StartTime=$StartTime
EndTime=$EndTime
} -ErrorAction SilentlyContinue | ForEach-Object {
[PSCustomObject]@{
Timestamp = $_.TimeCreated
EventType = "PowerShellExecution"
User = $_.UserId
Details = $_.Properties[2].Value.Substring(0, [Math]::Min(100, $_.Properties[2].Value.Length))
Severity = "Warning"
}
}
# Network connections (Sysmon 3)
$timeline += Get-WinEvent -FilterHashtable @{
LogName='Microsoft-Windows-Sysmon/Operational'
ID=3
StartTime=$StartTime
EndTime=$EndTime
} -ErrorAction SilentlyContinue | ForEach-Object {
[PSCustomObject]@{
Timestamp = $_.TimeCreated
EventType = "NetworkConnection"
User = $_.Properties[1].Value
Details = "$($_.Properties[4].Value) -> $($_.Properties[14].Value):$($_.Properties[16].Value)"
Severity = "Info"
}
}
$timeline | Sort-Object Timestamp | Format-Table -AutoSize
}Persistence Mechanism Hunter
# === Comprehensive Persistence Location Scanner ===
function Find-AllPersistence {
$results = @()
# Registry Run Keys
$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",
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunServices",
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunServicesOnce"
)
foreach ($key in $runKeys) {
if (Test-Path $key) {
$items = Get-ItemProperty -Path $key -ErrorAction SilentlyContinue
$items.PSObject.Properties | Where-Object {
$_.Name -notmatch "^PS" -and $_.Value
} | ForEach-Object {
$results += [PSCustomObject]@{
Type = "RegistryRun"
Location = $key
Name = $_.Name
Value = $_.Value
Suspicious = ($_.Value -match "powershell|cmd|wscript|temp|appdata")
}
}
}
}
# Startup Folders
$startupFolders = @(
"$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup",
"C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup",
"$env:USERPROFILE\Start Menu\Programs\Startup"
)
foreach ($folder in $startupFolders) {
if (Test-Path $folder) {
Get-ChildItem -Path $folder -ErrorAction SilentlyContinue | ForEach-Object {
$results += [PSCustomObject]@{
Type = "StartupFolder"
Location = $folder
Name = $_.Name
Value = $_.FullName
Suspicious = ($_.Extension -in @('.bat','.cmd','.vbs','.ps1','.lnk'))
}
}
}
}
# Scheduled Tasks
Get-ScheduledTask | Where-Object {
$_.State -ne "Disabled" -and
$_.TaskPath -notlike "\Microsoft\*"
} | ForEach-Object {
$action = $_.Actions.Execute
$results += [PSCustomObject]@{
Type = "ScheduledTask"
Location = $_.TaskPath
Name = $_.TaskName
Value = "$action $($_.Actions.Arguments)"
Suspicious = ($action -match "powershell|cmd|wscript|cscript|mshta")
}
}
# Services
Get-CimInstance Win32_Service | Where-Object {
$_.PathName -notmatch "C:\\Windows\\" -and
$_.PathName -notmatch "C:\\Program Files"
} | ForEach-Object {
$results += [PSCustomObject]@{
Type = "Service"
Location = "Services"
Name = $_.Name
Value = $_.PathName
Suspicious = $true
}
}
# WMI Event Subscriptions
Get-CimInstance -Namespace root/subscription -Class __EventFilter -ErrorAction SilentlyContinue |
ForEach-Object {
$results += [PSCustomObject]@{
Type = "WMIEventFilter"
Location = "WMI Subscription"
Name = $_.Name
Value = $_.Query
Suspicious = ($_.Query -match "powershell|cmd")
}
}
Get-CimInstance -Namespace root/subscription -Class __EventConsumer -ErrorAction SilentlyContinue |
ForEach-Object {
$results += [PSCustomObject]@{
Type = "WMIEventConsumer"
Location = "WMI Subscription"
Name = $_.Name
Value = if ($_.CommandLineTemplate) {$_.CommandLineTemplate} else {$_.ScriptText}
Suspicious = $true
}
}
# Browser extensions (common persistence)
$chromeExtPath = "$env:LOCALAPPDATA\Google\Chrome\User Data\Default\Extensions"
if (Test-Path $chromeExtPath) {
Get-ChildItem -Path $chromeExtPath -Directory | ForEach-Object {
$results += [PSCustomObject]@{
Type = "BrowserExtension"
Location = "Chrome"
Name = $_.Name
Value = $_.FullName
Suspicious = $false # Requires manual review
}
}
}
return $results
}
Find-AllPersistence | Where-Object {$_.Suspicious -eq $true} | Format-Table -AutoSizeMalware Analysis & Deobfuscation
# === PowerShell Deobfuscation Toolkit ===
function Deobfuscate-PowerShell {
param([string]$ObfuscatedScript)
# Step 1: Decode Base64 if present
if ($ObfuscatedScript -match "frombase64string\('([^']+)'\)" -or
$ObfuscatedScript -match 'frombase64string\("([^"]+)"\)') {
$base64 = $matches[1]
$decoded = [System.Text.Encoding]::Unicode.GetString([System.Convert]::FromBase64String($base64))
Write-Host "`n=== Base64 Decoded ===" -ForegroundColor Cyan
Write-Host $decoded
$ObfuscatedScript = $decoded
}
# Step 2: Decompress GZIP if present
if ($ObfuscatedScript -match "IO\.Compression\.GzipStream") {
Write-Host "`n=== GZIP Compression Detected ===" -ForegroundColor Yellow
}
# Step 3: Replace common obfuscation patterns
$patterns = @{
'\$\{([^\}]+)\}' = '$($1)' # ${var} to $(var)
'"\+"' = '' # Remove string concatenation
"'\+'" = ''
'\[char\](\d+)' = {[char][int]$args[0]} # [char]65 to 'A'
}
foreach ($pattern in $patterns.Keys) {
if ($ObfuscatedScript -match $pattern) {
Write-Host "`nFound obfuscation pattern: $pattern" -ForegroundColor Yellow
}
}
# Step 4: Identify suspicious cmdlets
$dangerousCmdlets = @(
"Invoke-Expression", "IEX", "Invoke-Command", "ICM",
"Invoke-WebRequest", "IWR", "Invoke-RestMethod", "IRM",
"Start-Process", "Start-Job", "New-Object Net.WebClient",
"DownloadString", "DownloadFile", "DownloadData",
"Reflection.Assembly", "Load", "System.Reflection",
"Runtime.InteropServices", "VirtualAlloc", "CreateThread"
)
Write-Host "`n=== Dangerous Cmdlets Found ===" -ForegroundColor Red
foreach ($cmdlet in $dangerousCmdlets) {
if ($ObfuscatedScript -match [regex]::Escape($cmdlet)) {
Write-Host "- $cmdlet" -ForegroundColor Red
}
}
# Step 5: Extract URLs/IPs
$urlPattern = 'https?://[^\s"\)'']+|(?:[0-9]{1,3}\.){3}[0-9]{1,3}'
$urls = [regex]::Matches($ObfuscatedScript, $urlPattern) |
Select-Object -ExpandProperty Value -Unique
if ($urls) {
Write-Host "`n=== URLs/IPs Extracted ===" -ForegroundColor Cyan
$urls | ForEach-Object {Write-Host $_ -ForegroundColor Green}
}
return $ObfuscatedScript
}
# === Extract IOCs from Scripts ===
function Extract-IOCs {
param([string]$ScriptContent)
$iocs = @{
URLs = @()
IPs = @()
FileHashes = @()
Domains = @()
FilePaths = @()
}
# URLs
$iocs.URLs = [regex]::Matches($ScriptContent, 'https?://[^\s"\)'']+') |
Select-Object -ExpandProperty Value -Unique
# IP Addresses
$iocs.IPs = [regex]::Matches($ScriptContent, '(?:[0-9]{1,3}\.){3}[0-9]{1,3}') |
Select-Object -ExpandProperty Value -Unique |
Where-Object {$_ -notmatch '^(127\.|10\.|172\.1[6-9]\.|172\.2[0-9]\.|172\.3[01]\.|192\.168\.)'}
# File hashes (MD5, SHA1, SHA256)
$iocs.FileHashes = [regex]::Matches($ScriptContent, '\b[A-Fa-f0-9]{32}\b|\b[A-Fa-f0-9]{40}\b|\b[A-Fa-f0-9]{64}\b') |
Select-Object -ExpandProperty Value -Unique
# File paths
$iocs.FilePaths = [regex]::Matches($ScriptContent, '[A-Z]:\\(?:[^\s"\''<>|?*]+\\)*[^\s"\''<>|?*]+') |
Select-Object -ExpandProperty Value -Unique
return $iocs
}
# === Analyze PowerShell Empire/Covenant Indicators ===
function Find-C2Indicators {
param([string]$ScriptBlock)
$indicators = @()
# Empire indicators
if ($ScriptBlock -match "defaultresponse|\/admin\/get\.php|SESSIONKEY|Empire") {
$indicators += "PowerShell Empire"
}
# Covenant indicators
if ($ScriptBlock -match "Covenant|GruntHTTP|/Grunt|elite") {
$indicators += "Covenant C2"
}
# Cobalt Strike indicators
if ($ScriptBlock -match "beacon|spawnas|powerpick|execute-assembly") {
$indicators += "Cobalt Strike"
}
# Metasploit indicators
if ($ScriptContent -match "meterpreter|ReflectivePEInjection|Invoke-Shellcode") {
$indicators += "Metasploit"
}
# Generic C2 patterns
if ($ScriptBlock -match "sleep.*rand|Start-Sleep.*Get-Random") {
$indicators += "Jitter/Sleep Pattern (C2 Beacon)"
}
if ($ScriptBlock -match "while.*true.*try.*catch") {
$indicators += "Infinite Loop with Error Handling (C2 Loop)"
}
return $indicators
}Fleet-Scale Operations
# === Parallel Threat Hunting Across Multiple Endpoints ===
function Invoke-FleetHunt {
param(
[Parameter(Mandatory=$true)]
[string[]]$ComputerNames,
[Parameter(Mandatory=$true)]
[scriptblock]$HuntScript,
[int]$ThrottleLimit = 10,
[PSCredential]$Credential
)
$results = $ComputerNames | ForEach-Object -Parallel {
$computer = $_
$script = $using:HuntScript
$cred = $using:Credential
try {
$params = @{
ComputerName = $computer
ScriptBlock = $script
ErrorAction = 'Stop'
}
if ($cred) {
$params.Credential = $cred
}
$result = Invoke-Command @params
[PSCustomObject]@{
Computer = $computer
Status = "Success"
Data = $result
Error = $null
}
} catch {
[PSCustomObject]@{
Computer = $computer
Status = "Failed"
Data = $null
Error = $_.Exception.Message
}
}
} -ThrottleLimit $ThrottleLimit
return $results
}
# Example: Hunt for Mimikatz across 100 endpoints
$endpoints = Get-ADComputer -Filter * | Select-Object -ExpandProperty Name -First 100
$mimikatzHunt = {
# Look for LSASS access in last hour
Get-WinEvent -FilterHashtable @{
LogName='Microsoft-Windows-Sysmon/Operational'
ID=10
StartTime=(Get-Date).AddHours(-1)
} -ErrorAction SilentlyContinue | Where-Object {
$_.Properties[4].Value -like "*lsass.exe*" -and
$_.Properties[6].Value -eq "0x1010"
}
}
$results = Invoke-FleetHunt -ComputerNames $endpoints -HuntScript $mimikatzHunt
# === Background Job Management for Long-Running Hunts ===
function Start-AsyncHunt {
param(
[scriptblock]$HuntScript,
[string]$JobName = "ThreatHunt_$(Get-Date -Format 'yyyyMMdd_HHmmss')"
)
$job = Start-Job -Name $JobName -ScriptBlock $HuntScript
Write-Host "Hunt job started: $JobName (ID: $($job.Id))" -ForegroundColor Green
Write-Host "Monitor with: Get-Job -Id $($job.Id) | Receive-Job -Keep"
return $job
}
# === Query EDR via API (Example: CrowdStrike) ===
function Search-CrowdStrikeHosts {
param(
[string]$Filter,
[string]$APIKey,
[int]$Limit = 100
)
$headers = @{
"Authorization" = "Bearer $APIKey"
"Content-Type" = "application/json"
}
$body = @{
filter = $Filter
limit = $Limit
} | ConvertTo-Json
$response = Invoke-RestMethod -Uri "https://api.crowdstrike.com/devices/queries/devices/v1" `
-Method Post -Headers $headers -Body $body
return $response
}SIEM Integration & Alert Enrichment
# === Splunk Query Automation ===
function Invoke-SplunkSearch {
param(
[string]$Query,
[string]$SplunkServer,
[PSCredential]$Credential,
[string]$EarliestTime = "-1h"
)
$base64Auth = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(
"$($Credential.UserName):$($Credential.GetNetworkCredential().Password)"
))
$headers = @{
"Authorization" = "Basic $base64Auth"
}
$searchParams = @{
search = "search $Query"
earliest_time = $EarliestTime
output_mode = "json"
}
$response = Invoke-RestMethod -Uri "https://$SplunkServer:8089/services/search/jobs/export" `
-Method Post -Headers $headers -Body $searchParams
return $response
}
# === Elastic/OpenSearch Query ===
function Search-ElasticSIEM {
param(
[string]$IndexPattern = "winlogbeat-*",
[hashtable]$Query,
[string]$ElasticURL,
[PSCredential]$Credential
)
$base64Auth = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(
"$($Credential.UserName):$($Credential.GetNetworkCredential().Password)"
))
$headers = @{
"Authorization" = "Basic $base64Auth"
"Content-Type" = "application/json"
}
$body = @{
query = $Query
size = 1000
} | ConvertTo-Json -Depth 10
$response = Invoke-RestMethod -Uri "$ElasticURL/$IndexPattern/_search" `
-Method Post -Headers $headers -Body $body
return $response.hits.hits
}
# === Automated Alert Enrichment ===
function Enrich-Alert {
param(
[string]$TargetHost,
[string]$TargetUser,
[string]$SourceIP
)
$enrichment = @{}
# Get host information
if ($TargetHost) {
try {
$comp = Get-ADComputer -Identity $TargetHost -Properties LastLogonDate, OperatingSystem
$enrichment.HostLastLogon = $comp.LastLogonDate
$enrichment.HostOS = $comp.OperatingSystem
} catch {
$enrichment.HostError = $_.Exception.Message
}
}
# Get user information
if ($TargetUser) {
try {
$user = Get-ADUser -Identity $TargetUser -Properties Department, Title, Manager
$enrichment.UserDepartment = $user.Department
$enrichment.UserTitle = $user.Title
$enrichment.UserManager = $user.Manager
} catch {
$enrichment.UserError = $_.Exception.Message
}
}
# IP geolocation and threat intel (example with AbuseIPDB)
if ($SourceIP) {
# Implement your threat intel lookup here
$enrichment.SourceIP = $SourceIP
$enrichment.ThreatIntel = "Check VirusTotal/AbuseIPDB/AlienVault OTX"
}
return $enrichment
}Performance & Production Tips
# === Optimize Large Dataset Processing ===
# BAD: Slow for large datasets
Get-WinEvent -LogName Security | Where-Object {$_.Id -eq 4688}
# GOOD: Filter at source
Get-WinEvent -FilterHashtable @{LogName='Security'; ID=4688}
# === Measure Script Performance ===
$elapsed = Measure-Command {
# Your hunting script here
Get-WinEvent -FilterHashtable @{LogName='Security'; ID=4688} -MaxEvents 10000
}
Write-Host "Execution time: $($elapsed.TotalSeconds) seconds"
# === Error Handling for Production ===
function Get-SafeWinEvent {
param($FilterHashtable)
try {
Get-WinEvent -FilterHashtable $FilterHashtable -ErrorAction Stop
} catch [System.Exception] {
Write-Warning "Failed to query event log: $($_.Exception.Message)"
return $null
}
}
# === Logging Your Detections ===
function Write-DetectionLog {
param(
[string]$Finding,
[string]$Severity,
[string]$LogPath = "C:\SOC\detection_log.json"
)
$entry = @{
Timestamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
Finding = $Finding
Severity = $Severity
Analyst = $env:USERNAME
Host = $env:COMPUTERNAME
} | ConvertTo-Json
Add-Content -Path $LogPath -Value $entry
}📚 Advanced Resources
Must-Read Documentation
MITRE ATT&CK: https://attack.mitre.org
Sigma Rules Repository: https://github.com/SigmaHQ/sigma
Sysmon Config: https://github.com/SwiftOnSecurity/sysmon-config
JPCERT Tool Analysis: https://jpcertcc.github.io/ToolAnalysisResultSheet
PowerShell Security Tools
PowerShell Empire: Study attack techniques
Invoke-Obfuscation: Learn obfuscation patterns
PSReflect: Understanding reflection-based attacks
PowerSploit: Offensive PowerShell (for detection building)
Practice Environments
Detection Lab: https://github.com/clong/DetectionLab
GOAD (Game of Active Directory): https://github.com/Orange-Cyberdefense/GOAD
Atomic Red Team: https://github.com/redcanaryco/atomic-red-team
⚡ Daily Habits for Mastery
Read one APT report per week - Extract TTPs and build detections
Analyse real malware samples - Understand attacker techniques
Automate repetitive tasks - Every manual step is a candidate for scripting
Share detections with team - Build your SOC's detection library
Measure everything - Track detection efficacy and false positive rates
Stay current - Follow @DanielBohannon, @HarmJ0y, @mattifestation on Twitter/X
Contribute to community - Publish Sigma rules, share detections
🛡️ Production Deployment Checklist
Before deploying scripts to production:
[ ] Comprehensive error handling implemented
[ ] Performance tested with large datasets
[ ] Logging and audit trail configured
[ ] Peer review completed
[ ] False positive rate measured and acceptable
[ ] Documentation written
[ ] Runbook created for SOC team
[ ] Integrated with ticketing/SOAR
[ ] Metrics and dashboards created
[ ] Incident response procedures updated
Remember: As a Senior SOC Analyst, you're not just detecting threats—you're building the detection infrastructure. Your PowerShell skills multiply your entire team's effectiveness.
Focus on automation, scalability, and sharing knowledge.
Hunt hard. Automate ruthlessly. Defend proactively. 🛡️
Last updated