Pentest Methodology & Recon · intermediate · ~12 min
**What you will learn** - Enumerate the most common network services (HTTP, SMB, FTP, SSH, DNS, SNMP, NFS, databases) for the details that turn an open port into an understood service. - Choose the right tool for each service and know how *noisy* (loggable, active) each choice is. - Run enumeration inside authorization and scope: rate-limited, wordlist-controlled, and documented. - Read enumeration from the **defender's** side — what these probes look like in server and IDS logs, and how to detect and slow them down. - Verify a hardening change actually reduced what an enumerator can see (mitigation verification), not just assume it did. - Write an evidence trail (target, command, timestamp, result) you can drop straight into a report.
Security objective. The asset under discussion is a set of network services — the SMB share, the web app, the FTP server, the DNS server. The threat is an attacker who has already found open ports and now interrogates each service to find a foothold: a readable share, an anonymous login, a hidden admin directory, a leaky DNS zone. As the learner, you will practice enumeration the way an authorized tester does, and — just as important — learn to detect and blunt it as a defender: what to log, what abuse looks like, and how to confirm a fix.
Scanning (your prereq, Port, service, and OS scanning) answered what is listening? It produced a bare list: 22/tcp open, 445/tcp open, 80/tcp open. That list is nearly useless on its own. Enumeration is the next step: it answers what exactly is this, and what can I reach through it? An open SMB port is a shrug; an SMB port with a world-readable Backups share is a finding.
The field mantra is "enumerate, then enumerate again." Each service speaks its own protocol and hides its detail in a different place, so each needs its own probes and its own tool. This lesson connects directly to your scanning prereq — scanning hands you the port list; enumeration is what you do with it — and it feeds the next phases (vulnerability analysis and, in a lab, exploitation). Everything here is framed for systems you own or are explicitly authorized to test: localhost, containers, and intentionally-vulnerable lab VMs.
In authorized professional work — a scoped penetration test, a red-team engagement, an internal security assessment — footholds almost never come from the port list. They come from enumeration detail. A client rarely cares that port 445 is open; they care that an unauthenticated user can read \\server\HR-Payroll. Enumeration is where you find that.
The skill has two halves, and both are billable value:
| Habit | Why it matters professionally |
|---|---|
| Right tool per service | Efficient, repeatable, and defensible in a report. You are not guessing. |
| Awareness of noise | Directory and vhost brute-forcing is active and logged. Knowing this keeps you in scope, keeps rate limits sane, and lets you warn the client's SOC before you run it. |
The defensive payoff is just as real. The same knowledge that lets you enumerate a service tells a blue-teamer exactly what enumeration looks like in their logs — the burst of 404s, the SMB null-session, the AXFR request — so they can detect it, rate-limit it, and shrink the attack surface. On a professional engagement you are expected to hand back both: the findings and the detection guidance.
Definition. Scanning discovers which ports/services exist. Enumeration extracts actionable detail from each one: versions, shares, users, directories, records, credentials-in-the-open.
How it works. You take each open port and speak its protocol — HTTP to the web server, SMB to the file server, DNS to the resolver — asking questions the service is willing (or misconfigured) to answer.
When / when not. Enumerate every in-scope service; do not enumerate hosts or services outside your authorization, and do not brute-force against production without telling the owner and setting rate limits.
Pitfall. Stopping at the port list. "445 open" is not a finding. "445 open, Backups share readable by anonymous" is.
| Service (port) | What you enumerate | Insecure assumption it exploits |
|---|---|---|
| HTTP/S (80/443) | Directories, files, virtual hosts, tech stack, endpoints | "Nobody will guess /admin or /backup.zip" |
| SMB (445) | Shares, users, password policy | "Internal network is trusted" |
| FTP (21) | Anonymous login, readable/writable files | "Anonymous FTP is harmless" |
| SSH (22) | Version, allowed auth methods | "An old banner doesn't matter" |
| DNS (53) | Zone transfer (AXFR), record sweeps | "Anyone can copy our whole zone" |
| SNMP (161/udp) | Community strings, device/system inventory | "public is fine as a community string" |
| NFS (2049) | Exported shares you can mount | "Export to everyone is convenient" |
| Databases (3306/5432/1433) | Reachability, default creds, version | "The DB is behind a firewall so it's safe" |
Definitions of the less-obvious ones. A zone transfer (AXFR) asks a DNS server for its entire list of records at once — meant for server-to-server replication, dangerous when open to anyone. A community string is SNMP's simple shared password; the factory default public grants read access to a device's inventory. A null session is an SMB connection with an empty username and password.
Pitfall. Using one tool (say, Nmap) for everything. Nmap scripts help, but SMB shares are clearer with smbclient, DNS with dig, HTTP paths with ffuf. Match the tool to the protocol.
Definition. Directory and vhost discovery sends thousands of guesses. It is active: every request hits the server and lands in its access log.
How it works. A wordlist drives requests like GET /admin, GET /backup, GET /.git/; you keep the ones that don't return 404. That is a flood of requests from one source in a short window — a textbook detectable signature.
When / when not. In scope, rate-limited, and (on client systems) pre-announced to the SOC. Never as a "quick check" against something you don't have written permission to test.
Pitfall. Treating brute-forcing as passive recon. It is not. It is noisy, it is logged, and off-scope it can be a crime.
Definition. Every tool has a job and a noise level. Picking well makes you efficient and keeps you inside scope.
| Tool | Job | Noise |
|---|---|---|
| Nmap (+ NSE) | Port/service/OS scan, scripted checks | Medium — tunable with timing flags |
| Masscan | Very fast port sweeps | High — can saturate a network |
| Netcat | Manual connect, banner grab | Low — one connection |
| Gobuster / ffuf | Directory / file / vhost brute force | High — thousands of requests |
| WhatWeb / Wappalyzer | Web tech fingerprint | Low–medium |
| dig / nslookup | DNS queries, AXFR attempt | Low per query |
| curl | Craft raw HTTP requests | Low — you control each request |
Masscan caution. Masscan can send packets faster than a small network can absorb, effectively a self-inflicted denial of service. Use it only where you are explicitly authorized, and set --rate conservatively.
Pitfall. Reaching for the fastest/loudest tool by reflex. Speed you don't need is just noise that gets you noticed — or gets you out of scope.
AUTHORIZED LAB (localhost / container / vulnerable VM)
+-------------------------------------------------------------------+
| |
| Tester box === trust boundary (network) === |
| (you, in scope) --------------------+ |
| | enumeration probes | ENTRY POINTS / ASSETS |
| | (HTTP dir brute, SMB null, v |
| | AXFR, SNMP public, curl) +-----------------------------+ |
| | | 80/443 Web app + hidden | |
| | | dirs (asset) | |
| | | 445 SMB shares (asset) | |
| | | 53 DNS zone (asset) | |
| | | 161u SNMP inventory | |
| | | 3306 Database (asset) | |
| | +-----------------------------+ |
| | | |
| | v |
| | +------------------+ |
| +--------------------------->| Server LOGS / | <-- defender|
| every probe recorded | IDS / SIEM | detects |
| +------------------+ |
+-------------------------------------------------------------------+
Off this lab boundary = NOT authorized. Do not probe real hosts.
Knowledge check.
public dangerous? ("A default community string is harmless" — it grants anyone who can reach 161/udp read access to device inventory.)The shape of an enumeration command is: tool + target + what-to-ask-for + safety flags. Below are lab-safe skeletons. TARGET is a host you own — use 127.0.0.1, a container name, or a lab VM you set up; never a third party.
# HTTP directory discovery with an explicit, controlled rate.
# -w wordlist -u URL with FUZZ marker -mc match codes -rate cap -t threads
ffuf -w /usr/share/wordlists/dirb/common.txt \
-u http://TARGET/FUZZ \
-mc 200,204,301,302,401,403 \
-rate 50 -t 20
# SMB: list shares. -N = no password (tests for anonymous/null access).
smbclient -L //TARGET -N
# DNS: attempt a zone transfer (AXFR). Succeeds only if the server is misconfigured.
dig AXFR lab.example @TARGET
# HTTP banner / headers only, no body — quiet, precise.
curl -sS -I http://TARGET/
# Manual banner grab with netcat (one connection).
nc -vn TARGET 22
Key safety knobs to notice: -rate / --rate cap requests per second; -mc filters to interesting responses so you send fewer follow-ups; -N and AXFR are tests — a successful result is itself the finding. Always point these at a lab target, and log the exact command + timestamp you ran.
Scanning finds services. Enumeration interrogates each one for the detail that leads to a foothold.
The mantra is simple: "enumerate, then enumerate again."
| Tool | Job |
|---|---|
| Nmap | Port, service, and OS scanning; NSE scripts |
| Masscan | Very fast port sweeps — lab or authorized use only; can overwhelm networks |
| Netcat | Manual connect, banner grab, quick checks |
| Gobuster / ffuf | Directory, file, and vhost brute forcing |
| WhatWeb / Wappalyzer | Web technology fingerprinting |
| dig / nslookup | DNS queries |
| curl | Crafting raw HTTP requests |
Enumeration brute force (directories, vhosts) is active and noisy. Keep it in scope and rate-limited.
Masscan needs extra care. It can saturate a network, so use it only where you are authorized.
The example below walks the insecure -> secure -> verify shape for the most common enumeration finding: an HTTP server that leaks a sensitive file and a hidden admin path. Everything runs against a local lab you control.
A throwaway lab web root that exposes exactly what enumeration hunts for:
# Build an intentionally-vulnerable lab web root (localhost only).
mkdir -p /tmp/lab-web/admin
cd /tmp/lab-web
echo 'DB_PASSWORD=<development-placeholder>' > backup.env # secret-looking file left in web root
echo '<h1>Admin panel</h1>' > admin/index.html # hidden but unauthenticated
echo '<h1>Welcome</h1>' > index.html
# Serve on loopback ONLY (127.0.0.1), so nothing leaves this machine.
python3 -m http.server 8000 --bind 127.0.0.1
An authorized enumerator finds the leaks:
# Directory/file discovery, rate-limited, matching interesting codes only.
ffuf -w /usr/share/wordlists/dirb/common.txt \
-u http://127.0.0.1:8000/FUZZ \
-mc 200,301,403 -rate 50 -t 10
# -> finds admin (301) and backup.env (200)
curl -sS http://127.0.0.1:8000/backup.env
# -> DB_PASSWORD=<development-placeholder> <-- secret exposed: this is the finding
Why it's vulnerable: the assumption "nobody will guess these paths" is false — a wordlist guesses them in seconds — and there is no authentication on /admin and no rule preventing .env-style files from being served.
Don't rely on secrecy of the path. Remove the secret from the web root, require auth on admin, and block sensitive file types. Here is the same idea expressed as an Nginx config (concept-track, real directives):
server {
listen 127.0.0.1:8080;
root /tmp/lab-web-secure;
# 1) Never serve dotfiles or secret-bearing extensions.
location ~ /\.(?!well-known) { deny all; }
location ~* \.(env|bak|old|sql|key|pem)$ { deny all; }
# 2) Require authentication for the admin area.
location /admin/ {
auth_basic "restricted";
auth_basic_user_file /etc/nginx/.htpasswd; # created with htpasswd
}
# 3) Log everything so enumeration is detectable (see Security & safety).
access_log /var/log/nginx/lab_access.log;
}
And the operational fix that matters most: move the secret out of the web root entirely (into an environment variable or a secrets store), so no misconfiguration can serve it.
# BAD input must now be REJECTED:
curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/backup.env
# expected: 403 (blocked by the file-extension rule)
curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/admin/
# expected: 401 (auth now required)
# GOOD input must still be ACCEPTED:
curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/
# expected: 200 (normal page still served)
# GOOD input WITH credentials reaches admin:
curl -sS -o /dev/null -w '%{http_code}\n' -u admin:'<lab-password>' http://127.0.0.1:8080/admin/
# expected: 200
Expected output, read together: before the fix, backup.env returned 200 and printed the secret, and /admin/ returned 200 with no login. After the fix, backup.env -> 403, /admin/ -> 401, the home page still 200, and admin-with-credentials 200. That contrast is the mitigation verification: the fix rejects the two bad requests and still accepts the two legitimate ones.
Walkthrough of the insecure -> secure -> verify example.
Setup (insecure lab).
mkdir -p /tmp/lab-web/admin and the echo lines create three files: a normal index.html, a hidden admin/index.html with no auth, and backup.env containing a placeholder secret. This reproduces two classic leaks: a guessable admin path and a sensitive file left in the web root.python3 -m http.server 8000 --bind 127.0.0.1 serves them. --bind 127.0.0.1 is the safety control: the server is reachable only from this machine, so your lab never touches the outside network.Enumeration (the attack you are learning to detect).
ffuf ... -u http://127.0.0.1:8000/FUZZ replaces FUZZ with each wordlist entry. -mc 200,301,403 keeps only interesting responses; -rate 50 -t 10 caps the flood. It reports admin (a 301 redirect to the directory) and backup.env (200).curl -sS .../backup.env fetches the file and prints DB_PASSWORD=<development-placeholder>. The secret is exposed — that printed line is the finding you would document.Secure config.
location ~ /\.(?!well-known) denies dotfiles (like .env, .git) except .well-known. location ~* \.(env|bak|old|sql|key|pem)$ { deny all; } blocks secret-bearing extensions regardless of path.location /admin/ { auth_basic ...; } forces HTTP auth on the admin area; unauthenticated requests get 401.access_log ensures every probe is recorded — the detection half of the fix.Verification trace.
| Request | Before fix | After fix | Meaning |
|---|---|---|---|
GET /backup.env |
200 + secret |
403 |
Bad input rejected |
GET /admin/ |
200, no auth |
401 |
Bad input rejected |
GET / |
200 |
200 |
Good input still accepted |
GET /admin/ with creds |
(n/a) | 200 |
Authorized access works |
Reading the two right-hand columns together proves the mitigation: the two attacker requests now fail closed while the two legitimate requests still succeed. A fix you cannot demonstrate this way is a fix you cannot claim in a report.
1. Stopping at the port list.
smbclient -L //TARGET -N — and report the readable share and its contents.2. Brute-forcing at full speed on a client's production system.
ffuf with default threads and no rate cap against a live app.-rate, use a sensible wordlist, and notify the SOC of your source IP and window.3. Using one tool for every protocol.
smbclient/enum4linux for SMB, dig AXFR for DNS, ffuf for HTTP paths.4. Treating a scanner's silence as proof of security.
5. Running Masscan without care.
--rate, confirm authorization, and prefer Nmap unless you truly need the speed.ffuf/gobuster returns everything (every path looks like a hit).
200/301 for unknown paths (a catch-all or SPA). Add -mc to match real codes and -fs/-fc to filter the default response size/code. Confirm with curl -I http://TARGET/definitely-not-real to see the baseline.dig AXFR ... @TARGET returns "Transfer failed."
dig SOA lab.example @TARGET. If SOA works but AXFR is refused, the finding is "AXFR properly restricted."smbclient -L //TARGET -N gives NT_STATUS_ACCESS_DENIED.
curl hangs.
-v for the handshake, set --max-time 5, and reconfirm the port from your scan results.Nothing responds at all.
ss -tlnp / netstat)? Am I hitting 127.0.0.1 vs the container's IP? Is a host firewall dropping packets? Did I bind the service to loopback only?Questions to ask when enumeration "fails":
Security & safety — detection and logging.
Enumeration is active and loud; the defender's job is to see it and slow it down. When you build or assess these services, log with intent.
What to log for each enumeration-relevant event:
ACCESS_DENIED, AXFR refused, auth success/failure).What to NEVER log: passwords, session cookies or tokens, API keys, private keys, full credentials submitted in a request body, full payment card numbers (PANs), or unneeded PII. If you must reference a secret, log that it was present, not its value.
Events that signal abuse (enumeration in progress):
404s (and occasional 403/401) from one source in a short window -> directory/vhost brute force.AXFR request from a client that is not a configured secondary.public.How false positives arise: a legitimate uptime monitor or crawler can look like light brute force; a misconfigured internal replica may issue AXFR; a vulnerability scanner run by your own team will trip every one of these. That is why detections should correlate with known-good sources and scheduled scan windows before alerting a human — and why testers de-conflict with the SOC in advance.
Defensive controls that reduce what enumeration yields: authentication on sensitive paths, denying dotfiles/secret extensions, restricting AXFR to named secondaries, changing/removing default SNMP community strings, disabling anonymous FTP and SMB null sessions, least-privilege share permissions, and per-source rate limiting. Each control should be paired with a verification step (as in the code section) proving it now rejects the bad request while still allowing legitimate use.
Concrete authorized use case. During a scoped internal penetration test, a tester enumerates a file server: smbclient -L reveals a Finance share readable by anonymous users, and ffuf against the intranet portal uncovers /backups/ serving a database dump. Both become report findings with impact, evidence (command + timestamp + output), and remediation (fix share ACLs, remove the dump, block the path, add auth). The blue team uses the same detail to write detections for the next real attacker.
Professional best-practice habits.
public, anonymous FTP/SMB) off.| Level | What it looks like |
|---|---|
| Beginner | Enumerate one service at a time against a local lab, rate-limited, and record command + result. Recognise when a refusal is actually a good control. |
| Advanced | Chain enumeration across services with a correlation ID, tune wordlists to the target, de-conflict with the SOC, and deliver both findings and detection guidance. |
All tasks are lab-only: run against 127.0.0.1, a container, or an intentionally-vulnerable VM you own. Before starting, tick the authorization checklist. After each, do the DEFENSIVE conclusion: remediate and verify.
Authorization checklist (do this first, every time):
Beginner 1 — Rate-limited directory discovery.
ffuf with -mc and -rate 50; capture which paths return non-404.http://127.0.0.1:8000/FUZZ; output = list of hits with status codes.curl -I on a bogus path first.-mc filtering, tool noise.Beginner 2 — Banner and header enumeration.
curl -I on the web port and nc -vn on the SSH port; note versions/headers.Intermediate 1 — SMB share enumeration and lock-down.
smbclient -L //127.0.0.1 -N; record the result (shares listed vs. access denied).NT_STATUS_ACCESS_DENIED.Intermediate 2 — DNS zone-transfer test.
dig SOA lab.example @127.0.0.1, then attempt dig AXFR lab.example @127.0.0.1.Challenge — Multi-service enumeration report + detections.
Lab cleanup / reset (all tasks):
http.server; stop containers with docker stop/docker rm).rm -rf /tmp/lab-web /tmp/lab-web-secure.ss -tlnp.Main concepts. Scanning finds which services are open; enumeration extracts the actionable detail that becomes a foothold. Each service (HTTP, SMB, FTP, SSH, DNS, SNMP, NFS, databases) is its own puzzle with its own protocol, its own tool, and its own insecure assumption. Directory/vhost brute-forcing is active and logged — never passive.
Key syntax / commands. ffuf -w LIST -u http://TARGET/FUZZ -mc 200,301,403 -rate 50 (web paths), smbclient -L //TARGET -N (SMB shares), dig AXFR zone @TARGET (zone transfer), curl -sS -I (headers), nc -vn TARGET PORT (banner). Match the tool to the protocol and cap the rate.
Common mistakes. Stopping at the port list; brute-forcing full-speed on production; one tool for every protocol; treating a clean scan as proof of security (it isn't — nothing is "completely secure"); running Masscan carelessly.
What to remember. Enumerate, then enumerate again — but only where you're authorized (localhost / containers / lab VMs). A refused probe is often evidence of a working control. For every issue, deliver the full loop: what/why/impact -> secure fix -> mitigation verification (bad request now rejected, good request still accepted) -> detection & logging (log source, resource, result, decision, correlation id; never log secrets).