Closing the Front Door: Auditing Your Windows Network Attack Surface
Every listening port is a door an attacker can knock on. Most Windows machines expose far more than they need to - RDP, SMB, WinRM, leftover dev servers - and the inbound firewall rules that should fence them off drift open over time. Here is how to enumerate every listening service, judge what actually needs to be reachable, and audit the firewall posture that decides who gets to try.
Every defensive control on a Windows machine — patching, EDR, credential hardening — assumes the attacker first has to reach something. The network attack surface is that “something”: the set of services bound to a network interface, waiting for a connection. It is the part of the machine an attacker can touch before they have any code execution at all, which makes it the first thing worth shrinking and the last thing most people audit. A box can be fully patched, EDR-protected, and still be one exposed, unauthenticated service away from compromise. The job here is to enumerate every door, decide which ones genuinely need to be open, and confirm the firewall actually fences off the rest.
Step 1: Enumerate Everything That Is Listening
You cannot defend a port you do not know is open. The ground truth lives in the TCP/UDP listener table, and the only entry that matters is one bound to a routable address. A service listening on 127.0.0.1 is reachable only from the machine itself; a service on 0.0.0.0 or :: (or a specific LAN IP) is reachable from the network. That distinction is the whole game, and it is the column people skip.
# Every listening socket, with the owning process and its path:
Get-NetTCPConnection -State Listen |
Select-Object LocalAddress, LocalPort,
@{n='Proc';e={ (Get-Process -Id $_.OwningProcess -EA SilentlyContinue).Name }},
@{n='Path';e={ (Get-Process -Id $_.OwningProcess -EA SilentlyContinue).Path }} |
Sort-Object LocalPort
# The classic, still useful for UDP + a quick PID join:
netstat -ano -p TCP | Select-String LISTENING
Read the LocalAddress column first, not the port. A development server on 127.0.0.1:5173 is harmless; the same server on 0.0.0.0:5173 is your source tree served to the LAN. The most common real exposures are not exotic: a debug build bound to all interfaces, a database that shipped listening on 0.0.0.0, a forgotten python -m http.server, or a management agent that opened a port the vendor never documented.
Step 2: Judge What Actually Needs to Be Reachable
For each network-bound listener, ask one question: does anything legitimately connect to this over the network? Most answers are no. A handful of Windows services are exposed by default and deserve specific scrutiny because attackers know them cold:
- RDP (3389) — remote desktop. Convenient and relentlessly brute-forced. If it must be open, it should be gated by Network Level Authentication, scoped to specific source addresses, and never exposed to the internet directly. An internet-facing 3389 is a ransomware on-ramp, full stop.
- SMB (445) — file sharing. The protocol behind EternalBlue and most lateral movement. It belongs on internal segments only, never the perimeter, and with SMB signing required.
- WinRM (5985/5986) — remote management. Powerful by design; 5985 is plaintext HTTP. If you use it, prefer 5986 (HTTPS) and restrict the source hosts.
- WMI / RPC (135 + the dynamic ephemeral range) — remote administration plumbing that doubles as a lateral-movement channel.
- NetBIOS / LLMNR / mDNS (137-139, 5353, 5355) — legacy name resolution that enables credential-relay and poisoning attacks (Responder territory). Almost always safe to disable on modern networks.
The principle is least exposure: a service that does not need a network listener should not have one, and a service that needs to be reachable by three machines should not be reachable by the whole subnet. Every port you close is an entire class of remote attack that no longer applies to you — no patch cadence to keep up with, no zero-day to fear, because the door isn’t there.
Step 3: Audit the Firewall That Is Supposed to Stop Them
Closing a service is the strongest fix, but you cannot always close everything — so the inbound firewall is the control that decides who gets to try. The failure mode here is rarely “the firewall is off.” It is drift: an installer that punched an Any-scope allow rule, a long-disabled rule someone re-enabled to debug something and forgot, a profile (Domain/Private/Public) with weaker defaults that kicks in the moment the laptop joins coffee-shop Wi-Fi.
# Is the firewall actually on for every profile?
Get-NetFirewallProfile |
Select-Object Name, Enabled, DefaultInboundAction
# Every ENABLED inbound ALLOW rule, with the ports and scope it opens:
Get-NetFirewallRule -Direction Inbound -Action Allow -Enabled True |
Get-NetFirewallPortFilter | # join to see LocalPort
Select-Object InstanceID, Protocol, LocalPort
Three things decide whether the firewall is doing its job: every profile is enabled with a default inbound action of Block; the Public profile is at least as strict as Private (it is the profile that protects you on untrusted networks); and every enabled allow rule is scoped to the narrowest remote address and port set it can be. A rule that opens a port to Any remote address when the real need is one subnet is not a firewall rule, it is a comment that says “allow.”
The Catch: This Surface Drifts
The reason network exposure is dangerous is not that it is hard to fix — closing a port or tightening a rule takes seconds. It is that the surface is not static. Every software install, every Windows feature toggle, every “let me just open this to test” quietly changes the listener table and the rule set. The machine you hardened in January is not the machine you have in June. A one-time audit is a photograph of a moving target, which is why this work has to be baselined and re-checked, not done once.
That is exactly the kind of broad, mechanical enumeration that belongs in a posture audit rather than your afternoon. On the machine in front of you, the full sweep — listeners mapped to processes, exposed-service checks, firewall profile and rule analysis — runs free, at full depth, and finishes in seconds:
# Audit listeners, exposed services, and firewall posture - full depth:
winsentinel --audit
# Capture the network surface as a baseline you can diff against next week:
winsentinel --audit --format json --out network-baseline.json
# Remediate the exposures that don't belong:
winsentinel --fix-it
None of this needs a CVE feed. An exposed RDP endpoint, a database bound to 0.0.0.0, a wide-open firewall rule — these are configuration state, invisible to a vulnerability scanner that only asks “is the software patched?” The software can be perfectly current and the door still wide open. That gap, between a patched machine and a reachable one, is precisely what a configuration audit is built to read.
Where It Becomes an Org Problem
On a single machine, enumerating listeners and tightening the firewall is genuinely complete — every check above runs free, at full depth, on the box you own. The shape of the problem changes at scale. The danger to an organization is not one laptop with RDP open; it is that you cannot see which of your machines drifted. A single GPO change, a mass software rollout, or one well-meaning admin re-enabling a rule can swing the exposed surface across hundreds of endpoints at once, and you would never know from any one box.
That is the boundary where fleet orchestration earns its keep. WinSentinel Pro runs the same network audit — identical depth to the free single-machine scan; Pro does not unlock extra checks — on an agent on each endpoint, reporting into a central node. What it adds is the org-level answer: a fleet rollup that says “9 of your 140 machines are exposing SMB to a routable address” in one view instead of 140 separate scans; drift alerts when a rollout opens a new listener or firewall rule across the fleet; and compliance evidence that your endpoints’ network posture stayed locked down over an audit window. Single-machine hardening is free and total. The “which of my machines did that change just expose?” question, answered as one report, is the org problem Pro exists to solve.
The Takeaway
An attacker’s first move is to find a service that will talk to them. The fewer that will, the smaller every later step of the intrusion becomes — you cannot exploit, brute-force, or relay against a port that isn’t open. Treat your listening sockets the way you treat your service inventory: enumerated, justified, and baselined, with a firewall that blocks by default and allows only what is named and scoped. Close the doors that don’t need to be open, fence the ones that do, and re-check it on a schedule — because the surface will keep trying to drift back open.
# See what's reachable on this machine right now, then close it:
winsentinel --audit
winsentinel --fix-it
An attacker only needs one reachable service. Knowing every one of yours — before they do — is how you make sure that service isn’t there to find.