SEC401 - Windows Security
Lab 5.4 - Using PowerShell for Speed and Scale
Solo, Lab
Focus: Windows Security
Level: SEC401
Date: Apr 2026
Artifacts: Sanitized PowerShell console screenshots from a 3-host alpha-svr lab fleet
TL;DR
- •Pipelined Get-Process/Get-Service with Where-Object, Measure-Object, Out-GridView, Export-CSV
- •Used Invoke-Command with Get-Credential to run remote queries against alpha-svr1/2/3.local
- •Hunted a rogue BrokerSvc (broker.exe, LocalSystem, auto-start) and captured its SHA-256
Skills demonstrated
Note: Course-provided PCAPs and lab instructions are not shared. Only my own captures and sanitized notes are published.
Why this matters
Windows attackers live on the box with the same tools defenders use. Fluency in PowerShell — pipelines, remoting, Get-WinEvent, Get-FileHash — is what lets a blue-team analyst triage a 3-host (or 3000-host) incident without drowning in RDP sessions or GUI clicks. This lab builds exactly that muscle.
Context
This lab demonstrates how PowerShell scales Windows administration and incident response from a single host to a fleet: working with objects through the pipeline, filtering and measuring results, driving remote command execution with Invoke-Command, and using that same remoting surface to hunt a suspicious service (broker.exe/BrokerSvc) deployed across three servers.
Tools used
Steps taken
1Process overview with Get-Process
Baseline enumeration of every running process with handles, memory (PM/WS), CPU seconds, PID, and ProcessName. This is the PowerShell equivalent of tasklist, but every row is a live object you can pipe into further filters.
$ Get-Process2Deep property view on a single process
Piped one process into Select-Object -Property * to expose every property the object exposes — FileVersion, Path, Company, HandleCount, WorkingSet, VirtualMemorySize, BasePriority. This is how you learn what you can filter on before writing a Where-Object clause.
$ Get-Process -Name explorer | Select-Object -Property *-Namematch by process nameSelect-Object -Property *dump every property on the pipeline object3Launch and inspect a process
Started Notepad with Start-Process, then introspected it with Select-Object *. Confirmed the AppX path under C:\Program Files\WindowsApps — useful detail when triaging whether a running binary is the Microsoft-signed Store build or a sideloaded copy.
$ Start-Process notepad.exe
$ Get-Process -Name notepad | Select-Object *4Capture a process into a variable
Stored the Notepad process object in $NotepadProc. Variables in PowerShell hold live objects (not strings), so $NotepadProc carries every method and property the process exposes — which is why the next step works.
$ $NotepadProc = Get-Process -Name notepad
$ $NotepadProc5Invoke a method on the stored object
Called .kill() on the stored object to terminate Notepad, then re-queried Get-Process to confirm the process is gone (ObjectNotFound error proves the kill succeeded). This pattern — capture, act, re-verify — is the bread-and-butter of automated incident response.
$ $NotepadProc.kill()
$ Get-Process -Name notepad6Enumerate Windows services
Get-Service returns every service with Status, Name, and DisplayName. Same object-pipeline story as Get-Process — downstream cmdlets operate on service objects, not parsed text.
$ Get-Service7Count services with Measure-Object
Piped Get-Service to Measure-Object — 278 services installed on this host. Measure-Object is the PowerShell analog to wc -l, except it counts pipeline objects, not lines of text.
$ Get-Service | Measure-Object8Filter services to only those Running
Where-Object -Property Status -like Running narrows the pipeline to active services. Same object flowing through: Get-Service produces, Where-Object filters.
$ Get-Service | Where-Object -Property Status -like RunningWhere-Objectfilter pipeline objects by a predicate-Property Statusproperty to test-like Runningcomparison (-like is case-insensitive wildcard)9Count the running services
Chained the same filter into Measure-Object — 96 of 278 services are Running. Two cmdlets, one pipeline, zero intermediate files.
$ Get-Service | Where-Object -Property Status -like Running | Measure-Object10Out-GridView for interactive triage
Piped Get-Service to Out-GridView — a sortable, filterable GUI grid. Out-GridView is a triage tool: you can click-filter to a subset, then send the selection back to the pipeline for further processing.
$ Get-Service | Out-GridView11Live filter inside Out-GridView
Added a 'Status contains Running' criteria inside Out-GridView to narrow the grid interactively. Useful when you want to poke around without writing the full Where-Object in advance.
$ Get-Service | Out-GridView12Export to CSV and open in ISE
Dumped every service object to Services.csv with Export-Csv, then opened it in the PowerShell ISE for inspection. Export-Csv serializes every property of every pipeline object — great for offline analysis or evidence preservation.
$ Get-Service | Export-CSV -Path Services.csv
$ ise .\Services.csv13Directory listing and alias discovery
Used dir to list the lab directory, then Get-Alias dir to confirm dir is just an alias for Get-ChildItem. Knowing the underlying cmdlet is what lets you pipe dir into object-aware cmdlets like Sort-Object.
$ dir
$ Get-Alias dir14Inspect a file as an object
Piped one CSV into Format-List * to expose every property on the FileSystemInfo object — PSPath, VersionInfo, BaseName, Length. Same object-pipeline mental model as processes and services: a file is an object with properties, not just a name.
$ dir .\Services.csv | Format-List *15Sort directory listing by CreationTime
Piped dir into Sort-Object CreationTime — Services.csv sorts last because it was just created, while the original .ps1 scripts share an older 12/16/2023 timestamp.
$ dir | Sort-Object CreationTime16Bootstrap the fleet and load the server list
Ran start-servers.ps1 to bring the alpha-svr fleet online, then loaded the server list into a typed array with [string[]]$AlphaServers = Get-Content. Typing matters: [string[]] tells Invoke-Command to treat $AlphaServers as a list of computer names, not one long string.
$ ./start-servers.ps1
$ [string[]]$AlphaServers = Get-Content -Path 'C:\sec401\labs\5.4\alpha-servers.txt'
$ $AlphaServers17Invoke-Command across the fleet with credentials
Captured credentials with Get-Credential, then ran Get-CimInstance Win32_OperatingSystem remotely on all three alpha-svr hosts in one call. The output is a single table with a PSComputerName column — Invoke-Command returns deserialized objects from every remote host, merged into one pipeline.
$ $creds = Get-Credential
$ invoke-command -Authentication Basic -Credential $creds -ComputerName $AlphaServers -command { Get-CimInstance Win32_OperatingSystem | Select-Object CSName, Caption } | Format-Table-Authentication Basicsimple auth (lab only — use Kerberos/CredSSP in prod)-CredentialPSCredential object from Get-Credential-ComputerNamearray of targets-command { ... }scriptblock executed on every remote host18Negative control: probe for a file that doesn't exist
Ran Get-ChildItem C:\Windows\System32\proxy.exe across the fleet — all three hosts returned PathNotFound. This is a deliberate negative control: it proves Invoke-Command is routing to all three hosts and that the hunt query below isn't silently failing.
$ invoke-command -Authentication Basic -Credential $creds -ComputerName $AlphaServers -command { Get-ChildItem C:\Windows\System32\proxy.exe } | Format-Table19Fleet-wide enumeration of C:\Windows\*.exe
Listed every EXE directly under C:\Windows on all three hosts. The output reveals the same five binaries on each host — bfsvc.exe, notepad.exe, regedit.exe, write.exe (expected Windows binaries) plus broker.exe with a 10/21/2023 timestamp. broker.exe is not a default Windows binary at that path and shows up on every host — a strong IOC signal.
$ invoke-command -Authentication Basic -Credential $creds -ComputerName $AlphaServers -command { Get-ChildItem C:\Windows\*.exe } | Format-Table20Correlate with Event ID 7045 (service installed)
Entered a remote session on alpha-svr3 and queried the System log for Event ID 7045 (Service Control Manager: a service was installed). Got a direct match: BrokerSvc, c:\Windows\broker.exe, user mode service, auto start, running as LocalSystem. That's the full install record — who installed it (SCM context), when (TimeCreated 12/12/2023), and with what privileges (LocalSystem = full admin on the box).
$ Get-WinEvent -FilterHashtable @{LogName='System'; ID=7045} -MaxEvents 3 | format-list-FilterHashtableserver-side XPath-equivalent filter (fast)LogNamewhich log to queryID=7045Service Control Manager 'a service was installed' event-MaxEvents 3cap results21Hash the suspicious binary for IOC sharing
Ran Get-FileHash -Algorithm SHA256 against C:\Windows\broker.exe on the remote host. SHA-256: 646DF7C22A76C92CF6CD83A9B7970C95514047C9431B29909732C62F28963E31. That hash is the shareable IOC: feed it to VirusTotal, add it to a SIEM watchlist, or block it with Defender ASR — it's what turns this single-lab finding into fleet-wide detection content.
$ Get-FileHash -Algorithm SHA256 C:\Windows\broker.exe-Algorithm SHA256hash algorithm (MD5/SHA1/SHA256/SHA512 supported)Key findings
Outcome / Lessons learned
Demonstrated the full arc of PowerShell for Windows incident response: local enumeration with pipelines, interactive triage with Out-GridView, remote execution across a 3-host fleet with Invoke-Command, and a concrete hunt that surfaced a rogue BrokerSvc running broker.exe as LocalSystem — complete with a SHA-256 suitable for distribution as an IOC.
Ship the SHA-256 to the SIEM and EDR as a detection. Pull the full Event ID 7045 history across the fleet to identify every host where BrokerSvc was installed, not just the three in the lab. Quarantine broker.exe, capture a memory image of any host where it's running, and pivot to 4697 (Security log) and Sysmon Event ID 1/7 for process creation and image-load context. Rotate any credentials that could have been harvested from the LocalSystem-privileged service.
Security controls relevant
- PowerShell remoting over WinRM (constrained to signed scriptblocks in prod)
- Service installation auditing (Event ID 7045, 4697)
- File integrity monitoring + SHA-256 IOC sharing
- Endpoint detection (Defender for Endpoint, Sysmon)
- Least-privilege service accounts (no LocalSystem for custom services)
What I took away from this
The object pipeline is the thing that matters in PowerShell, and it's the part that trips up people coming from bash. Get-Service isn't returning lines of text, it's returning ServiceController objects — which is why Where-Object -Property Status works without any parsing and why Export-Csv can serialize every property automatically. Once you internalize that, you stop writing awk-style text hacks and start chaining cmdlets.
Invoke-Command is where PowerShell stops being a shell and starts being a fleet tool. Being able to fire the same scriptblock at three — or three thousand — hosts and get back one merged object pipeline is what makes hunting feasible at scale. The negative control here (probing a path that doesn't exist) is the muscle I'd want every analyst to build: before you trust a hunt query, confirm it's actually reaching every target.
The broker.exe finding is a good real-world shape. The binary sits in C:\Windows (not System32), shows up identically on every host, has a matching 7045 event, and runs as LocalSystem. None of those signals alone would be conclusive, but together they turn a generic 'enumerate EXEs' query into a clean IOC with a hash you can distribute. That's the workflow — enumerate, correlate, hash, share — and PowerShell gives you all of it in one console.