Pentest Methodology & Recon · intermediate · ~12 min
By the end of this lesson you will be able to: - Explain the three main TCP port states — **open**, **closed**, and **filtered** — and what each tells you about the host and any firewall in front of it. - Compare **TCP connect** and **SYN (half-open)** scans, and explain why **UDP** scanning is slow and ambiguous. - Run **service/version detection** (`nmap -sV`), **OS detection** (`nmap -O`), and **banner grabbing**, and read their output correctly. - Understand scanning from **both sides**: how an authorized tester maps a host, and how a defender detects and slows that same activity. - Set up a **lab-only** scanning target on your own machine and clean it up afterward. - State the authorization and logging rules that separate legitimate testing from an attack.
Security objective. In an authorized engagement, scanning is the step that turns a bare IP address into a map of services. The asset being examined is a host's exposed network surface — every listening port is a possible door. The threat you are modelling is an attacker who reaches those ports across a network boundary. As the tester you will detect which services are reachable and which versions they run; as the defender's ally you will learn what that same scan looks like in logs so it can be caught and slowed.
This lesson builds directly on your two prerequisites. From "TCP vs UDP, ports, and the ones to know" you already know that a port is a 16-bit number (0–65535), that TCP uses a three-way handshake (SYN → SYN-ACK → ACK) while UDP is connectionless, and which well-known ports (22 SSH, 80 HTTP, 443 HTTPS, 53 DNS) matter. Scanning is simply probing those ports and reading the reply. From "The phases of an engagement" you know scanning sits in the recon / enumeration phase, after scoping and authorization, before exploitation. Nothing here makes sense outside that authorized frame.
The standard tool is Nmap (Network Mapper). Scanning proceeds in stages: (1) discover which ports are open/closed/filtered, (2) fingerprint the service and its version, (3) guess the operating system, and (4) read service banners. The version is the crown jewel — it is what links a live service to a specific list of known weaknesses and drives everything that follows.
In real authorized work — a penetration test, a red-team engagement, or your own asset inventory — you cannot defend or test what you have not mapped. Scanning is the bridge between "we own this IP range" and "here are the exact services running, and which are outdated."
Version detection is the highest-value step. "Port 443 is open" tells you little. "Port 443 is nginx 1.14.0" tells you precisely which published vulnerabilities to check and which are irrelevant. Professionals use this to prioritise: an internet-facing, unpatched service outranks an internal one behind a firewall.
The defensive payoff is just as real. Blue teams run the same scans against their own estate to find shadow services — a forgotten database bound to a public interface, a debug port left open after a deploy. Knowing what a scan looks like from the network and in the logs lets defenders write detections for it. In professional practice the scanner and the detector are two views of one skill, and a good tester documents both: what was found, and what evidence the target should have recorded.
Definition. A scan classifies each probed TCP port into one of three states based on the reply it gets.
| State | What the host did | What it means |
|---|---|---|
| open | Replied with SYN-ACK | A service is listening and accepted the probe |
| closed | Replied with RST (reset) | The host is reachable, but nothing is listening on that port |
| filtered | No reply (or ICMP unreachable) | A firewall/packet filter likely dropped the probe |
How it works. For a TCP SYN probe: a SYN-ACK means a listener exists (open); an RST means the port is reachable but empty (closed); silence means something ate the packet (filtered). The difference between closed and filtered is the difference between "the door isn't there" and "someone is blocking the hallway."
When/when-not. These three cover most cases. Nmap also reports combined states like open|filtered (common in UDP, where silence is genuinely ambiguous) and unfiltered (reachable, state undetermined). Do not treat filtered as "secure" — it only means this probe got no answer.
Pitfall. Beginners read filtered as "nothing there." A filtered port can hide a very much alive service behind a firewall rule. Silence is not absence.
Definition. The technique used to probe a port. The main three:
| Type | Nmap flag | Handshake | Speed | Notes |
|---|---|---|---|---|
| TCP connect | -sT |
Full (SYN, SYN-ACK, ACK) | Slower | Uses the OS socket API; needs no special privilege; logged as a full connection |
| SYN / half-open | -sS |
Partial (SYN, SYN-ACK, then RST) | Faster | Never completes the handshake; needs raw-socket privilege (root/admin) |
| UDP | -sU |
None (UDP is connectionless) | Slow | "No reply" is ambiguous; relies on ICMP port-unreachable to detect closed |
How it works. A connect scan asks the operating system to open a real connection — reliable, but the target sees a completed session. A SYN scan sends the SYN, reads the SYN-ACK, then sends an RST to tear down before the handshake finishes — slightly quieter and faster because no full session is established. UDP has no handshake at all: an open UDP port often stays silent, and closed is inferred only when the host returns an ICMP "port unreachable." Because that ICMP is rate-limited by most kernels, UDP scans crawl.
When/when-not. SYN is the common default when you have privilege. Use connect when you cannot get raw sockets (e.g., an unprivileged shell). Scan UDP deliberately and narrowly — you rarely need all 65535 UDP ports; target 53, 123, 161, 500 and the like.
Pitfall. Calling a SYN scan "stealthy" oversells it. Modern IDS/IPS and even basic connection-tracking firewalls log SYN floods and incomplete handshakes readily. Stealth is relative, never absolute.
-sV)Definition. Probing an open port to identify the software and its version, not just "something is here."
How it works. Nmap connects, reads any banner the service volunteers, and if that is not conclusive it sends a series of protocol-specific probes and matches the responses against a signature database. Output looks like 22/tcp open ssh OpenSSH 8.9p1 Ubuntu.
When/when-not. Run it once you know which ports are open — it is slower than a plain port scan, so scope it to the open ports. It is the single most useful step for prioritising later work.
Pitfall. Version strings can be wrong or spoofed. A banner can be edited, back-ported patches change behaviour without changing the version number, and load balancers lie. Treat a detected version as a strong lead, not proof.
-O)Definition. Guessing the operating system from low-level TCP/IP stack behaviour — default TTL, TCP window size, the order and presence of TCP options, how the stack responds to unusual packets.
How it works. Different OS network stacks have measurable quirks. Nmap fingerprints them and reports a best match with a confidence figure. It needs at least one open and one closed port for a good guess.
When/when-not. Useful for inventory and for narrowing exploit choices. Skip it when it adds noise you cannot afford or when the answer is already known.
Pitfall. It is an educated guess, sometimes low-confidence. NAT, virtualization, and proxies routinely fool it. Never anchor a decision on OS detection alone.
Definition. The simplest version check: connect to a service and read the text it announces on connection.
How it works. Many services greet you with an identifying line — SSH sends SSH-2.0-OpenSSH_8.9p1, SMTP sends a 220 line naming the mailer, HTTP returns a Server: header. You can read it with netcat, curl, or, in this course's exercises, by parsing the banner string in C.
When/when-not. Great for a quick manual confirmation of what -sV reported. Not every service offers a banner, and banners can be suppressed or faked as a hardening measure.
Pitfall. Trusting a banner blindly. Hardened servers strip or alter banners precisely to mislead scanners.
TRUST BOUNDARY (the network edge / firewall)
===============================================
[ Attacker / Authorized tester ] <-- untrusted side
| probes (SYN / connect / UDP)
v
+-----------------------------------+
| FIREWALL / packet filter | <-- trust boundary #1
| drops? -> port shows 'filtered' | (decides reachability)
+-----------------------------------+
| allowed probes pass
v
+-----------------------------------+
| HOST (the protected asset) | <-- trust boundary #2
| |
| Entry points (listening ports): |
| 22/tcp ssh OpenSSH 8.9 |
| 80/tcp http nginx 1.14 |
| 5432/tcp postgres <-- should |
| NOT be here publicly |
+-----------------------------------+
|
v each open port = a possible door + a log source
[ Connection logs / IDS / firewall logs ] <-- detection lives here
The assets are the host and its listening services. The trust boundaries are the firewall (what is even reachable) and the host's own service authentication (what a reachable service will let you do). The entry points are the open ports. Detection lives in the firewall and connection logs.
Knowledge check.
filtered. Which trust boundary produced that result, and what insecure assumption would it be to conclude "no service is there"?5432/tcp open postgresql on a host meant to serve only web traffic. Which insecure assumption (about default binds / firewall rules) most likely caused this exposure, and where would you look in the logs to see when it started being reachable?The core command shape is nmap [scan-type] [detection] [timing] [ports] <target>. Every flag below is real Nmap syntax. Run these only against a target you own or are authorized to test (the examples use localhost / 127.0.0.1).
# Fast SYN scan of the default 1000 ports (needs root for raw sockets)
sudo nmap -sS 127.0.0.1
# Unprivileged full-connect scan (no root needed)
nmap -sT 127.0.0.1
# Service/version detection on specific open ports only
nmap -sV -p 22,80,443 127.0.0.1
# OS detection (needs root; needs 1 open + 1 closed port)
sudo nmap -O 127.0.0.1
# A narrow, deliberate UDP scan (slow; pick ports on purpose)
sudo nmap -sU -p 53,123,161 127.0.0.1
# Save evidence for a report: all formats, timestamped
nmap -sV -p 22,80 -oA scan_localhost_$(date +%F) 127.0.0.1
Annotations:
-sS SYN (half-open); -sT full connect; -sU UDP. Pick one scan type.-sV version detection; -O OS detection. Add on top of a scan type.-p 22,80,443 restrict to listed ports; -p- means all 65535 (slow — do it deliberately).-oA <base> writes .nmap, .gnmap, and .xml — your report evidence.printf '' | nc -w 3 127.0.0.1 22 or curl -sI http://127.0.0.1.Scanning answers three questions: which ports are open, what is listening, and what operating system runs underneath. Nmap is the standard tool for this.
A scan classifies each port:
Knowing that "port 80 is open" is not enough. You also want the exact software and version.
nmap -sV does this. It reads banners and matches response signatures to fingerprint the service. The version is what maps a service to its known vulnerabilities.
nmap -O guesses the operating system from subtle TCP/IP stack behaviors, such as default TTL values, window sizes, and the order of TCP options.
This is an educated guess, not a certainty.
Banner grabbing is the simplest version check. You connect to a service and read what it announces.
Many services state their exact version in this banner. An SSH or SMTP server, for example, often reveals its version on connect.
You can grab a banner with netcat or curl. In this course's exercises, you do it by parsing a banner string in C.
This example is entirely local and lab-safe: you stand up a service on your own loopback interface, scan it, then remove it. Loopback (127.0.0.1) traffic never leaves your machine.
Here is what an insecurely exposed service looks like, and why it is dangerous. Suppose an app developer starts a database and binds it to all interfaces:
# INSECURE: binds Postgres to 0.0.0.0 -> reachable from the whole network.
# Shown to illustrate the risk. Do NOT run this on a networked host.
# postgres -c listen_addresses='*' # 0.0.0.0:5432 exposed
A scan from anywhere on that network now sees:
PORT STATE SERVICE VERSION
5432/tcp open postgresql PostgreSQL 12.x
The insecure assumption: "it's just for local development, no one will connect." Binding to 0.0.0.0 exposes the port to every host that can route to the machine — the version is now advertised to any scanner, tying it to published vulnerabilities.
Bind the service to loopback only, and let the firewall default to deny. For the lab we will use a simple, safe HTTP service so nothing sensitive is involved:
# SECURE lab target: a throwaway web server bound to LOOPBACK ONLY.
# 127.0.0.1 is unreachable from any other host -> not network-exposed.
python3 -m http.server 8080 --bind 127.0.0.1
The general secure pattern for any real service:
127.0.0.1 (or a private management interface), never 0.0.0.0, unless the service is meant to be public.With the loopback server from Part 2 running in one terminal, in another terminal:
# (a) ACCEPT good/intended use: scanning the loopback address finds it.
nmap -sT -sV -p 8080 127.0.0.1
# Expected (abridged):
# PORT STATE SERVICE VERSION
# 8080/tcp open http SimpleHTTPServer 0.6 (Python 3.x)
# (b) REJECT exposure: probe the machine's OUTWARD IP for the same port.
# Because we bound to 127.0.0.1, it must NOT be reachable there.
MY_IP=$(hostname -I 2>/dev/null | awk '{print $1}')
nmap -sT -p 8080 "$MY_IP"
# Expected: 8080/tcp is 'closed' or 'filtered' on the external IP
# -> the service is NOT network-exposed. Fix verified.
What the output means. Test (a) confirms the intended path works: the service is running and version-detectable on loopback. Test (b) is the security assertion: the same port on the machine's routable address is not open, proving the bind restricted exposure. If (b) had shown open, the fix failed and the service is exposed.
# Stop the lab server: Ctrl-C in its terminal, or find and stop it:
pkill -f "http.server 8080" # ends the throwaway server
There is nothing else to undo — no data was created, and nothing left loopback.
Walking through the VERIFY block, the heart of the lesson:
python3 -m http.server 8080 --bind 127.0.0.1 — starts a minimal web server. The --bind 127.0.0.1 is the whole point: the listening socket is tied to the loopback address, so the kernel will only accept connections that originate on the same machine.nmap -sT -sV -p 8080 127.0.0.1 — a full-connect scan (-sT, no root needed) with version detection (-sV) against just port 8080 (-p 8080) on loopback. Nmap completes a TCP handshake to 8080, reads the HTTP response, and matches it to the SimpleHTTPServer signature.8080/tcp open http SimpleHTTPServer ... confirms state=open and a fingerprinted version. This is the "accepts good input" half: the service works as intended on its intended interface.MY_IP=$(hostname -I ... | awk '{print $1}') — captures the machine's first routable (non-loopback) IP into a variable, so the next scan targets the outward-facing address instead of loopback.nmap -sT -p 8080 "$MY_IP" — the same probe, now aimed at the routable IP.127.0.0.1, the kernel has no listener on MY_IP:8080. Nmap gets an RST (closed) or, if a host firewall drops it, silence (filtered). Either way the port is not open — the "rejects bad exposure" half.| Step | Target | Port state | Interpretation |
|---|---|---|---|
| Scan (a) | 127.0.0.1:8080 |
open | Intended local access works |
| Scan (b) | MY_IP:8080 |
closed / filtered | Not network-exposed — fix verified |
The contrast between the two rows is the verification: the service is reachable exactly where it should be and nowhere else.
1. Reading filtered as "nothing there."
filtered and concluding the port is safe/empty.filtered means the probe got no answer — usually a firewall dropped it. A live, vulnerable service can sit right behind that rule.filtered as "reachability blocked for this probe," not "absent." Retry with different techniques (-sT vs -sS), from an allowed vantage point, or check host-side.2. Believing a version banner is ground truth.
3. Assuming a SYN scan is invisible.
-sS and thinking no one will notice.4. Scanning something you are not authorized to scan.
localhost, containers, or intentionally-vulnerable VMs to practise.5. Blasting all 65535 UDP ports by reflex.
nmap -sU -p- on a whole range and waiting hours for ambiguous results.open|filtered is often the best you get; the ICMP rate-limit throttles you.open|filtered as "needs manual follow-up," not a finding."All ports show filtered." A local firewall (or the target's) is dropping probes. Check: are you scanning the right IP? Is a host firewall active (iptables/pf/Windows Firewall) blocking outbound or inbound? Try an allowed vantage point or -sT instead of -sS.
"Nmap says I need to be root." -sS, -sU, and -O need raw sockets. Either run with sudo on a machine you control, or fall back to -sT (connect scan) which uses the normal socket API and needs no privilege.
"-sV returns no version, just open." The service gave no banner and no probe matched. Increase intensity (-sV --version-intensity 9), grab the banner manually (nc/curl), and remember some services are deliberately silent.
"OS detection is low-confidence or wrong." You may be behind NAT, a proxy, or scanning a VM. -O needs at least one open and one closed port. Treat the result as a guess; corroborate with service versions.
"The scan is painfully slow." UDP is inherently slow. For TCP, narrow the port list (-p), and consider a faster timing template on a lab host (-T4). Do not crank timing on production — you may cause disruption.
Questions to ask when a scan surprises you:
Scanning is a two-sided skill. A professional tester documents what a scan should have left behind, and a defender uses exactly that to catch it.
What to log (for the defender / the target host):
What NEVER to log:
Which events signal abuse (scan indicators):
How false positives arise: legitimate monitoring, load balancers, uptime checkers, and vulnerability scanners your own team runs all look like scans. Backup jobs and service discovery can trip the same rules. Before alerting on a source, check whether it is a known internal scanner or health-check system — tune allowlists so real attacks are not buried under routine noise.
Authorization checklist (before any lab or scan):
localhost, a container, an intentionally-vulnerable VM, or a CTF — not a third party.-oA output) securely and share it only with authorized parties.Concrete authorized use case. A company hires you to test its external footprint. Scope is a signed list of IP ranges the company owns. You run a phased scan: a broad TCP SYN sweep of common ports across the range, then -sV on the open ports, then targeted UDP on services like DNS. You discover a 5432/tcp open postgresql on a host that should only serve HTTP — a database accidentally bound to a public interface after a deploy. You report it with evidence (the -oA output, timestamps), a remediation (rebind to loopback, add a deny rule), and a retest step. The blue team correlates your scan against their firewall logs to confirm their detection fired.
Professional best-practice habits:
filtered, open|filtered) as "needs follow-up," never as findings on their own.Beginner vs advanced:
| Beginner | Advanced | |
|---|---|---|
| Target | localhost, one VM |
Whole authorized ranges, phased |
| Scans | -sT, -sV on a few ports |
Tuned SYN/UDP, timing control, scripts |
| Interpretation | Read open/closed/filtered | Correlate versions to risk, weigh spoofing |
| Output | Read on screen | -oA evidence, integrate into a report |
| Mindset | "What is open?" | "What is exposed, how risky, and how is it detected?" |
All tasks are lab-only: use localhost, a container, or an intentionally-vulnerable VM you own. Never scan systems you are not authorized to test.
python3 -m http.server 8080 --bind 127.0.0.1). In another terminal run nmap -sT -p 8080,8081 127.0.0.1.open, 8081 closed (or filtered).-sT needs no root. Compare the two port states and explain the difference in one sentence.nmap -sV -p 8080 127.0.0.1. Then grab the banner manually with curl -sI http://127.0.0.1:8080.Server: header from curl.127.0.0.1:8080 (should be open) and the machine's routable IP on 8080 (should not be). Record both results.hostname -I.open.Main concepts. Scanning turns an IP into a service map. Each TCP port is open (SYN-ACK — a listener), closed (RST — reachable, empty), or filtered (no reply — likely firewalled). Scan types trade speed for noise: connect (-sT, no privilege, full handshake), SYN/half-open (-sS, faster, needs root), and UDP (-sU, slow and ambiguous). Layer detection on top: -sV fingerprints service/version — the highest-value step, since the version links a service to known weaknesses — -O guesses the OS from stack quirks, and banner grabbing reads what a service announces.
Key syntax. nmap -sT -sV -p 22,80,443 127.0.0.1; add -O for OS; -oA <base> to save evidence; nc/curl for manual banners.
Common mistakes. Reading filtered as "absent"; trusting a version banner as proof of exploitability; assuming SYN scans are invisible; scanning outside authorized scope; blasting all UDP ports by reflex.
What to remember. Distinguish reachable from present, and detected version from confirmed exploitable. Scanning is two-sided: know what you find and what evidence the target should log (timestamp, source, port, result, correlation id — never secrets). Only ever scan systems you own or are explicitly authorized to test, and always have a cleanup step. Nothing is ever "completely secure" — scanning tells you where to look next, on both offense and defense.