// PowerShell Basics
PowerShell is both a shell and a scripting language built on .NET. It's the primary administrative interface for Windows and is deeply integrated with Active Directory, Azure, and all Microsoft products � making it essential for both defenders and attackers.
| Concept | Description |
|---|---|
| Cmdlets | Verb-Noun named commands � Get-Process, Stop-Service, New-Item |
Pipeline (|) | Passes objects (not text) between commands � Get-Process | Where-Object CPU -gt 50 |
| Variables | $var = "value" � environment variables: $env:TEMP |
| Aliases | Short forms � ls = Get-ChildItem, ps = Get-Process, cat = Get-Content |
| Execution Policy | Controls whether scripts can run � Get-ExecutionPolicy / Set-ExecutionPolicy |
| Modules | Collections of cmdlets � Import-Module ActiveDirectory |
| Where-Object | Filters pipeline objects � | Where-Object { $_.Status -eq "Running" } |
| Select-Object | Selects specific properties � | Select-Object Name, CPU, Id |
| Sort-Object | Sorts pipeline output � | Sort-Object CPU -Descending |
| Format-Table | Displays output as table � | Format-Table -AutoSize |
// System Investigation
# All running processes with executable path
Get-Process | Select-Object Name, Id, CPU, Path | Sort-Object CPU -Descending | Format-Table -AutoSize
# Processes with no executable path (possible injection / fileless malware)
Get-Process | Where-Object { $_.Path -eq $null } | Select-Object Name, Id, CPU
# Recently created files in suspicious locations
Get-ChildItem -Path "$env:TEMP","$env:APPDATA","C:\ProgramData" -Recurse -ErrorAction SilentlyContinue |
Where-Object { $_.CreationTime -gt (Get-Date).AddDays(-1) } |
Select-Object FullName, CreationTime, Length
# Installed services and their binary paths
Get-CimInstance Win32_Service | Select-Object Name, State, PathName | Sort-Object State
# Startup programs (all hives)
Get-CimInstance Win32_StartupCommand | Select-Object Name, Command, Location, User
// Network Commands
# Active TCP connections with owning process
Get-NetTCPConnection -State Established |
Select-Object LocalAddress, LocalPort, RemoteAddress, RemotePort,
@{Name="Process";Expression={(Get-Process -Id $_.OwningProcess).Name}} |
Sort-Object RemoteAddress | Format-Table -AutoSize
# Listening ports and processes
Get-NetTCPConnection -State Listen |
Select-Object LocalAddress, LocalPort,
@{Name="Process";Expression={(Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).Name}} |
Format-Table -AutoSize
# DNS cache (recently resolved domains)
Get-DnsClientCache | Select-Object Entry, Data, TTL | Sort-Object Entry
# Network adapters and IPs
Get-NetIPAddress | Select-Object InterfaceAlias, AddressFamily, IPAddress | Format-Table
# ARP table
Get-NetNeighbor | Select-Object IPAddress, LinkLayerAddress, State | Format-Table
# Firewall rules
Get-NetFirewallRule -Enabled True | Select-Object DisplayName, Direction, Action, Profile | Format-Table
// Event Log Querying
PowerShell's Get-WinEvent is the fastest way to query Windows Event Logs directly on a host or remotely � much faster than Event Viewer for large log files.
# Failed logons in last hour (Event 4625)
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4625; StartTime=(Get-Date).AddHours(-1)} |
Select-Object TimeCreated, Message | Format-List
# Successful logons (Event 4624)
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4624; StartTime=(Get-Date).AddDays(-1)} |
ForEach-Object {
$xml = [xml]$_.ToXml()
[PSCustomObject]@{
Time = $_.TimeCreated
User = $xml.Event.EventData.Data | Where-Object Name -eq 'TargetUserName' | Select-Object -ExpandProperty '#text'
LogonType = $xml.Event.EventData.Data | Where-Object Name -eq 'LogonType' | Select-Object -ExpandProperty '#text'
SourceIP = $xml.Event.EventData.Data | Where-Object Name -eq 'IpAddress' | Select-Object -ExpandProperty '#text'
}
} | Format-Table -AutoSize
# PowerShell script block logging (Event 4104)
Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-PowerShell/Operational'; Id=4104} |
Select-Object TimeCreated, Message | Format-List
# New scheduled task created (Event 4698)
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4698} |
Select-Object TimeCreated, Message | Format-List
# Service installed (Event 7045)
Get-WinEvent -FilterHashtable @{LogName='System'; Id=7045} |
Select-Object TimeCreated, Message | Format-List
// Active Directory Queries
Requires the ActiveDirectory module: Import-Module ActiveDirectory
# Get user details
Get-ADUser -Identity jsmith -Properties * | Select-Object Name, SamAccountName, Enabled,
PasswordLastSet, LastLogonDate, LockedOut, MemberOf
# All members of Domain Admins
Get-ADGroupMember -Identity "Domain Admins" -Recursive | Select-Object Name, SamAccountName
# Recently created user accounts
Get-ADUser -Filter * -Properties Created | Where-Object { $_.Created -gt (Get-Date).AddDays(-7) } |
Select-Object Name, SamAccountName, Created | Sort-Object Created -Descending
# Accounts that never expire (risky service accounts)
Get-ADUser -Filter { PasswordNeverExpires -eq $true -and Enabled -eq $true } |
Select-Object Name, SamAccountName, PasswordLastSet
# Accounts with old passwords (90+ days)
Get-ADUser -Filter { Enabled -eq $true } -Properties PasswordLastSet |
Where-Object { $_.PasswordLastSet -lt (Get-Date).AddDays(-90) } |
Select-Object Name, SamAccountName, PasswordLastSet | Sort-Object PasswordLastSet
# Recently modified AD objects
Get-ADObject -Filter * -Properties WhenChanged |
Where-Object { $_.WhenChanged -gt (Get-Date).AddHours(-24) } |
Select-Object Name, ObjectClass, WhenChanged | Sort-Object WhenChanged -Descending
# Kerberoastable accounts (SPN set, enabled)
Get-ADUser -Filter { ServicePrincipalName -like "*" -and Enabled -eq $true } -Properties ServicePrincipalName |
Select-Object Name, SamAccountName, ServicePrincipalName
// Persistence Hunting
# Registry Run keys (all users and HKLM)
$paths = @(
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run",
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce",
"HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run",
"HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce"
)
foreach ($path in $paths) {
Write-Host "`n$path" -ForegroundColor Cyan
Get-ItemProperty -Path $path -ErrorAction SilentlyContinue
}
# Scheduled tasks (show full task and command)
Get-ScheduledTask | Where-Object { $_.State -ne 'Disabled' } |
Select-Object TaskName, TaskPath, State,
@{Name="Command";Expression={$_.Actions.Execute + " " + $_.Actions.Arguments}} |
Format-Table -AutoSize
# WMI persistence (event subscriptions)
Get-CimInstance -Namespace root/subscription -Class __EventFilter
Get-CimInstance -Namespace root/subscription -Class __EventConsumer
Get-CimInstance -Namespace root/subscription -Class __FilterToConsumerBinding
# Services running from unusual paths
Get-CimInstance Win32_Service |
Where-Object { $_.PathName -notmatch "System32|SysWOW64|Program Files" -and $_.State -eq "Running" } |
Select-Object Name, PathName, StartMode
# Startup folder contents
Get-ChildItem -Path "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup",
"C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup" -ErrorAction SilentlyContinue
// Remote Investigation
PowerShell Remoting (WinRM) lets you run commands on remote hosts without RDP � faster, less intrusive, and fully logged.
# Single command on remote host
Invoke-Command -ComputerName HOSTNAME -ScriptBlock { Get-Process | Where-Object { $_.CPU -gt 50 } }
# Interactive remote session
Enter-PSSession -ComputerName HOSTNAME
# Run against multiple hosts from a list
$hosts = "HOST1","HOST2","HOST3"
Invoke-Command -ComputerName $hosts -ScriptBlock {
Get-NetTCPConnection -State Established |
Where-Object { $_.RemoteAddress -notin "10.0.0.0/8","192.168.0.0/16" }
}
# Copy a file from a remote host
$session = New-PSSession -ComputerName HOSTNAME
Copy-Item -Path "C:\Windows\System32\winevt\Logs\Security.evtx" -Destination "C:\Evidence\" -FromSession $session
Remove-PSSession $session
WinRM generates Event 4624 (LogonType 3, network logon) on the target host, along with Microsoft-Windows-WinRM events. Your remote investigation commands are logged. This is expected and acceptable � but be aware the attacker may also be monitoring logs. Avoid running commands that would alert them (e.g. obviously stopping their persistence).
// Detecting Attack Patterns
# Find encoded PowerShell in running processes
Get-Process | ForEach-Object {
try {
$cmdline = (Get-CimInstance Win32_Process -Filter "ProcessId=$($_.Id)").CommandLine
if ($cmdline -match '-[Ee]nc') {
[PSCustomObject]@{Name=$_.Name; Id=$_.Id; CommandLine=$cmdline}
}
} catch {}
}
# Check for AMSI bypass attempts in PowerShell logs
Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-PowerShell/Operational'; Id=4104} |
Where-Object { $_.Message -match 'amsi|bypass|invoke-mimikatz|dump|sekurlsa' } |
Select-Object TimeCreated, Message
# Open admin shares indicating lateral movement
Get-SmbOpenFile | Select-Object ClientComputerName, ClientUserName, Path | Format-Table
# Recent SMB connections in event logs
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=5140} -MaxEvents 100 |
Select-Object TimeCreated, Message | Format-List
// Defensive Hardening
| Setting | Command | Purpose |
|---|---|---|
| Script Block Logging | Set-ItemProperty HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging -Name EnableScriptBlockLogging -Value 1 | Logs all PowerShell script block content � captures obfuscated and encoded commands after deobfuscation |
| Module Logging | Via GPO: Computer ? PowerShell ? Turn on Module Logging | Logs all PowerShell module activity |
| Transcription | Via GPO: PowerShell Transcription ? Turn on PowerShell Transcription | Saves full input/output of all PowerShell sessions to a text file |
| Constrained Language Mode | $ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage" | Restricts PowerShell to safe cmdlets � blocks many attack techniques |
| Execution Policy | Set-ExecutionPolicy -ExecutionPolicy AllSigned -Scope LocalMachine | Requires signed scripts � not a security boundary but raises the bar |
Script Block Logging is the most valuable defensive setting. Enable it via GPO across your environment. It captures all PowerShell commands � even those that are Base64-encoded or obfuscated � after PowerShell decodes them, giving you a clean view of what actually ran. Look for Event ID 4104 in the Microsoft-Windows-PowerShell/Operational log.