Skip to main content
See Security Labs

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

PowerShell object pipelineRemote command execution (Invoke-Command)Service and process enumeration at scaleWindows Event Log triage (Get-WinEvent 7045)File integrity hashing (Get-FileHash)

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

PowerShellInvoke-CommandGet-WinEventGet-FileHashOut-GridView

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-Process

2Deep 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 name
Select-Object -Property *dump every property on the pipeline object

3Launch 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
$ $NotepadProc

5Invoke 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 notepad

6Enumerate 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-Service

7Count 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-Object

8Filter 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 Running
Where-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-Object

10Out-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-GridView

11Live 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-GridView

12Export 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.csv

13Directory 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 dir

14Inspect 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 CreationTime

16Bootstrap 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'
$ $AlphaServers

17Invoke-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 host

18Negative 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-Table

19Fleet-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-Table

20Correlate 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 query
ID=7045Service Control Manager 'a service was installed' event
-MaxEvents 3cap results

21Hash 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

278 services enumerated on the admin workstation; 96 Running
broker.exe present on alpha-svr1/2/3.local under C:\Windows with matching 10/21/2023 timestamp
Event ID 7045 shows BrokerSvc installed as auto-start user-mode service under LocalSystem
SHA-256 of broker.exe: 646DF7C22A76C92CF6CD83A9B7970C95514047C9431B29909732C62F28963E31
Invoke-Command successfully executed against three hosts in parallel from a single console

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.

Evidence gallery