What would you like to learn?

Try PowerShell, networks, XSS, or Step 138

Browse the full curriculum →

Penetration testing

Step 268. One HTB Medium Machine (Cumulative 3) — The Depth of Enumeration

Step 268Estimated practice · 2 days (2–3 hours a day)

Level 3 — Real-World CTF & Advanced Offensive Skills | Difficulty ★★★★☆ | Estimated time: 2 days (2–3 hours a day)

Prerequisites: Step 265–267 (2 cumulative Mediums + review). You start knowing which stage is weak from stuck-point statistics v1.

  • What you need: an HTB VPN environment, a filesystem where you’ll make a folder per machine, and Python 3 (for the hands-on vhost principle measurement).
  • ⚠️ All exercises in this chapter are for your own lab and legal platforms only. Applying them to unauthorized systems is a crime. HackTheBox (HTB) is a legal learning platform — today’s techniques are used on its machines and nowhere else.
  • Verification note: the vhost server and fuzzing-simulation output were measured on localhost on 2026-09-09. HTB machine attack scenes are screen examples.

One fact that Step 267’s stuck-point statistics point to — most of getting stuck on Medium is not technique but "not looking where you looked." The answer was usually inside the radius of the first recon; we just skimmed that radius shallowly and moved on. Today’s theme is the depth of enumeration — how to distinguish "wide" from "deep" and balance them, an organized set of second-stage enumeration techniques (vhosts, subdomains, hidden parameters), and an enumeration log system that records every discovery without leaks. The third Medium goes in carrying this system.


1. Learning Objectives

By the end of this chapter, you will be able to:

  • Distinguish enumeration’s "wide" from "deep," and choose the side that fits the kind of stuck
  • Explain the principle that a vhost is Host-header branching, with a localhost measurement
  • Execute the fuzzing principle of finding hidden vhosts by response-size/status differences
  • Know the list of second-stage enumeration techniques — subdomains, hidden parameters — and each one’s tools
  • Establish an enumeration log system with a machine-folder structure (01_scan–04_privesc) and a filename rule

2. Background Knowledge — Today’s Tools and Concepts

Today’s Tools at a Glance

Category Details
Language/environment HTB VPN + Kali + Python 3 (hands-on vhost principle)
Today’s commands gobuster vhost, ffuf -H "Host: FUZZ.domain", ffuf -u "...?FUZZ=1", nmap -oN, grep for rereads
Concepts needed Wide/deep distinction, Host headers and virtual hosts, response-difference-based fuzzing, the log system
Today’s deliverable Cumulative 3 machines rooted + enumeration log system (folders + filename rule) + a record of "what a log reread found"

2-1. Wide and Deep — Enumeration’s Two Axes

There are two kinds of holes in enumeration.

  • Wide (breadth): there’s a surface you haven’t looked at at all. You didn’t check UDP, you missed a non-standard port, you don’t know the subdomains. The prescription is "widen the scan’s scope."
  • Deep (depth): you looked at a surface shallowly. You did look at port 80, but the path scan ended at common.txt, or you didn’t sweep vhosts, or you didn’t dig parameters. The prescription is "dig one more layer into what you already saw."

When stuck, distinguish the two. "Is there a surface I haven’t seen yet?" → a wide problem. "Is there a layer I haven’t excavated on a surface I’ve seen?" → a deep problem. It’s a diagnosis one step finer than Step 265’s [enumeration/technique] distinction, and the deeper into Medium you go, the larger the deep side’s share becomes.

2-2. The vhost Principle — Same IP, the Host Header Separates Sites

The technology by which one server (one IP) serves several websites is the virtual host (vhost). The web server reads the request’s Host header to decide which site’s content to return — same IP and port, but a different Host yields a different site.

Why this matters in attacks: nmap tells you only "port 80 open" — it does not tell you how many sites live behind that port 80. Connect to target.htb and you get the public site; put dev.target.htb in as the Host and an internal site under development comes out. An entrance sitting on that hidden site is a classic Medium design.

The discovery method is substitution — send candidate names in the Host header and look for the one whose status code or size differs from the baseline. Nonexistent names all return the same default response, so the one different response is itself "a vhost that exists." You reproduce this principle yourself in 3-1.

2-3. The Second-Stage Enumeration List — The List for After the First Pass

Basic recon (nmap TCP, path scan) is done and no entrance is visible? Digest this list from the top.

Technique What it finds Tool examples
UDP scan Services missed by TCP-only (SNMP, etc.) nmap -sU --top-ports 100
vhost fuzzing Other sites behind the same IP gobuster vhost, ffuf -H "Host: FUZZ.domain"
Subdomain enumeration Services on separate hosts ffuf, amass, certificate transparency logs (crt.sh)
Bigger/different path lists Paths not in common.txt directory-list-2.3-medium, the raft family
Extension scan .bak, .old, .zip, .conf backup files gobuster -x bak,old,zip,conf
Hidden parameters Undocumented GET/POST arguments ffuf -u "URL?FUZZ=1", arjun
Source reading Endpoints/comments/credentials inside JS Browser dev tools, the linkfinder family

This list’s value is the definition of "done everything." Only when every row of this table has a check when you’re stuck do you earn the right to suspect technique shortage.

2-4. The Enumeration Log System — Recording So Discoveries Don’t Leak

The deeper you look, the more output there is. Let output just flow across the screen, and later you have no way to prove "I think I saw that line." Hence the need for a system of folders and names.

htb/machine03/
├── 01_scan/     ← raw scans like nmap (20260909_nmap_full-tcp.txt)
├── 02_enum/     ← path/vhost/service enumeration results
├── 03_exploit/  ← intrusion attempts and evidence
├── 04_privesc/  ← escalation enumeration (linPEAS etc.) and attempts
└── timeline.txt ← stage transitions and stuck records

The filename rule is date_tool_target20260909_ffuf_vhost.txt. Only with a rule does later grep work. And the reread routine for when you’re stuck: reread the logs from the top in the order nmap output → web paths → version info. Recording "what I missed and found on a reread" is one of today’s completion conditions.


3. Follow Along

3-1. Hands-On — Stand Up a vhost Server Yourself and Fuzz It

Before using tools, build the principle by hand. Save the file below as vhost_enum268.py — a standard-library-only server that returns different sites depending on the Host header, plus a fuzzing client.

"""vhost (virtual host) hands-on — same IP, the Host header separates sites."""
import http.server
import threading
import urllib.request

HOST, PORT = "127.0.0.1", 18090

PAGES = {
    "main.lab": (200, "<h1>Welcome to main.lab</h1><p>public site</p>"),
    "dev.lab": (200, "<h1>dev.lab staging</h1>"
                     "<p>internal build 0.9.3</p>"
                     "<a href="/internal-docs">docs</a>"),
}
DEFAULT = (200, "<h1>Welcome to main.lab</h1><p>public site</p>")

class VHostHandler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        host = self.headers.get("Host", "").split(":")[0]
        status, body = PAGES.get(host, DEFAULT)
        data = body.encode()
        self.send_response(status)
        self.send_header("Content-Type", "text/html")
        self.send_header("Content-Length", str(len(data)))
        self.end_headers()
        self.wfile.write(data)
    def log_message(self, fmt, *args):
        pass

def get(path="/", host_header=None):
    req = urllib.request.Request(f"http://{HOST}:{PORT}{path}")
    if host_header:
        req.add_header("Host", host_header)
    with urllib.request.urlopen(req, timeout=5) as r:
        return r.status, len(r.read())

def main():
    srv = http.server.HTTPServer((HOST, PORT), VHostHandler)
    threading.Thread(target=srv.serve_forever, daemon=True).start()
    print(f"[vhost server up] {HOST}:{PORT} — the site branches by Host header")

    print("n=== Scene 1: access with no Host header ===")
    s, n = get("/")
    print(f"HTTP {s}, {n} bytes")

    print("n=== Scene 2: access with Host: dev.lab ===")
    s, n = get("/", "dev.lab")
    print(f"HTTP {s}, {n} bytes")

    print("n=== Scene 3: vhost fuzzing simulation (substitute candidate names, discover by response-size difference) ===")
    _, base = get("/", "nonexistent-zz9.lab")
    print(f"baseline (nonexistent name) response: {base} bytes")
    for name in ["www", "api", "dev", "dev.lab", "admin", "mail", "main.lab"]:
        s, n = get("/", name)
        mark = "  <-- different response size: a separate site exists!" if n != base else ""
        print(f"Host: {name:<12} HTTP {s}  {n:>4} bytes{mark}")

    srv.shutdown()
    print("n[done] measurement complete")

if __name__ == "__main__":
    main()

Input:

python -u vhost_enum268.py

Output (measured 2026-09-09):

[vhost server up] 127.0.0.1:18090 — the site branches by Host header

=== Scene 1: access with no Host header ===
HTTP 200, 46 bytes

=== Scene 2: access with Host: dev.lab ===
HTTP 200, 84 bytes

=== Scene 3: vhost fuzzing simulation (substitute candidate names, discover by response-size difference) ===
baseline (nonexistent name) response: 46 bytes
Host: www          HTTP 200    46 bytes
Host: api          HTTP 200    46 bytes
Host: dev          HTTP 200    46 bytes
Host: dev.lab      HTTP 200    84 bytes  <-- different response size: a separate site exists!
Host: admin        HTTP 200    46 bytes
Host: mail         HTTP 200    46 bytes
Host: main.lab     HTTP 200    46 bytes

[done] measurement complete

How to read it: these three scenes are all of vhost fuzzing. In scenes 1 and 2, the same address (127.0.0.1:18090) returned different sites (46 bytes / 84 bytes) depending on the Host value — what nmap can see is not this far but only "18090 open." Scene 3 is the discovery method — nonexistent names all returned the default response (46 bytes), while only dev.lab differed at 84 bytes. Fuzzing is the work of finding "one different response," which is why measuring the baseline response’s size (or status code) first is mandatory.

3-2. Moving to Real Tools — ffuf and gobuster’s vhost Modes

Now that you’ve seen the principle, tool output reads clearly (screen example):

ffuf -u http://$TARGET -H "Host: FUZZ.target.htb" 
     -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt 
     -fs 46

Screen example:

dev                     [Status: 200, Size: 8431, Words: 1205, Lines: 210]

How to read it: -fs 46 is a filter saying "hide the size-46 ones (the default response)" — the same job as measuring the baseline size in 3-1’s scene 3. Some tools measure the baseline automatically (-ac), but knowing the principle explains why the filter is needed and why only a single line survives in the result. gobuster vhost -u http://target.htb -w list is a tool doing the same job.

3-3. The Rest of Second-Stage Enumeration — Parameters and Extensions

Even a page where the entrance was found has more layers (screen example):

# hidden parameters — finding undocumented GET arguments
ffuf -u "http://$TARGET/page.php?FUZZ=1" -w params.txt -fs 0

# backup/config files — rerun the path scan with different extensions
gobuster dir -u http://$TARGET -w common.txt -x bak,old,zip,conf

How to read it: parameter fuzzing finds "argument names that change the response" — the classic Medium device where ?debug=1 shows a different screen. Extension scans find files a developer left behind, like index.php.bak — an entrance where the source downloads in plaintext. Both are deep-side techniques, "digging one more layer into a surface already seen."

3-4. The Log System in Practice — Make the Folders Before You Start

The third machine goes in with the system in place (screen example):

mkdir -p htb/machine03/{01_scan,02_enum,03_exploit,04_privesc}
cd htb/machine03
date '+start: %F %H:%M' | tee timeline.txt
nmap -sV -p- $TARGET -oN 01_scan/$(date +%Y%m%d)_nmap_full-tcp.txt
nmap -sU --top-ports 100 $TARGET -oN 01_scan/$(date +%Y%m%d)_nmap_udp-top100.txt

Attach -oN/-o (ffuf takes -o results.json) to every enumeration command, and save web requests/responses as a Burp project.

How to read it: the tedium lasts only the first two days. The system’s worth appears the moment "where did I save that result" disappears on day three. And this folder becomes, as-is, the skeleton of a Step 266-style review and the final write-up.

3-5. The Reread Routine When Stuck — The Log Has the Answer

When stuck, rereading comes before new scans. Decide the order in advance.

# reread routine: logs from the top, in scan → path → version order
grep -iE "open|http-title" 01_scan/*.txt
grep -iE "301|302|401|403" 02_enum/*.txt   # non-200 responses are often the clue
grep -iE "Server:|X-Powered-By|version" 02_enum/*.txt

When you find something on a reread, record it — "missed, then found on a log reread: ____". As this one line accumulates, you’ll see "what kind of lines do I miss often."

How to read it: 301/302 is a redirect (follow it to a new surface), 403 is evidence of existence (a bypass target), 401 is an authentication point (where credentials get spent). Aim the rereading eye at "non-200 responses" — on the first pass, everyone looks only at 200s.


4. Missions & Exercises

Mission — Break Cumulative Machine 3 and Establish the Enumeration Log System

  1. Run 3-1’s vhost measurement script and capture the three scenes (default/dev/fuzzing discovery). Add a third site to PAGES and confirm fuzzing finds that one too.
  2. Start your third cumulative Medium with 3-4’s folder system — every scan/enumeration output must remain as a file following the date_tool_target rule.
  3. Every time you get stuck during the attack, write the 2-1 distinction (wide/deep), and digest rows of 2-3’s second-stage enumeration table starting with the ones not yet done.
  4. After root, record "what I missed and found on a log reread" in 3-5’s format — if there was none, record "passed without a reread" and write why.
  5. Formally incorporate this enumeration system into your routine document (v3).

Exercises

Exercise 1. Explain why vhost fuzzing "measures the baseline response’s size first," using scene 3 of the 3-1 measurement as grounds.

Exercise 2. Explain the difference between "wide" and "deep" with one representative technique each, and write the judgment criterion for which side to check first when stuck.

Exercise 3. Explain why the reread routine looks at non-200 responses (301, 401, 403) first, together with each code’s meaning.

Exercise 4. Without a filename rule (date_tool_target), what problems arise once the logs pile up? Explain from the perspective of grep-based rereading.


Answers & completion criteria · expand/collapse

5. Model Answers & Completion Criteria

Mission Model Answer

Item 1: the 2026-09-09 measurement confirmed the three scenes — no Host: 46 bytes, dev.lab: 84 bytes, and in fuzzing only dev.lab differed in size and got discovered. For the site-addition verification, add "api.lab": (200, "...") to PAGES and add api.lab to the candidate list; that line should then show in the fuzzing results with a size different from the baseline.

Example shape of items 2–3 records (screen example):

htb/machine03/01_scan/20260909_nmap_full-tcp.txt  (22, 80, 3000)
htb/machine03/02_enum/20260909_ffuf_vhost.txt     (found dev.target.htb)
htb/machine03/02_enum/20260909_gobuster_dev.txt   (found /staging, /.git)
timeline.txt: 14:20 stuck [deep] — checked 2nd-stage table → found 'hidden parameters' not yet done
              14:35 ffuf parameter fuzzing → source exposure at ?debug=1 → breakthrough

Item 4 example: "Missed, then found on a reread: I’d passed the first nmap’s port 3000 as ‘miscellaneous port,’ but on the reread I checked http-title and reclassified it as a second surface — that was the entrance."

How to verify: ① are the vhost measurement captures and the site-addition verification present? ② Does the machine folder have the 4 subfolders and rule-following filenames? ③ Do stuck records carry the [wide/deep] distinction? ④ Is there a "found on a reread" or a "passed without reread + reason"? ⑤ Has an enumeration-system section been added to routine document v3?

Exercise Answers

Answer 1. Fuzzing is the work of finding "a response different from the baseline," so without a baseline, "different" cannot be defined. In scene 3, nonexistent names all returned 46 bytes (the default site), and because the baseline was set at 46, the 84-byte dev.lab stood out. Without measuring the baseline, every response looks like a 200 and you can’t tell a real vhost apart — the real tools’ -fs (size filter) is exactly the automation of this baseline measurement.

Answer 2. Wide is "looking at surfaces you haven’t seen," represented by the UDP scan; deep is "digging further into surfaces you’ve seen," represented by vhost fuzzing or parameter fuzzing. Judgment criterion: "is there an axis missing from the scan’s scope (TCP only, top ports only)?" → wide first. "I’ve seen every surface but did I see each shallowly?" → deep first. On Medium, deep is the more common cause, but a wide hole (UDP unscanned) can never be filled by digging deep, so the checklist order wide → deep is safer.

Answer 3. A 200 is a "normal page," most of which you already saw on the first pass, while the remaining codes are uncharted territory easily ignored on the first pass. 301/302 is a signpost to another surface (the redirect destination is an undiscovered area), 401 is an authentication point (a candidate consumer for credentials you’ve obtained), 403 is "exists but denied" — evidence a file is there and a target for a bypass hypothesis. A reread must use eyes different from the first pass, and collecting "everything that isn’t 200" is those different eyes.

Answer 4. Without a rule in filenames, "which file had that result" depends on memory, and once logs number in the dozens, rereading itself becomes impossible. With a rule, you can mechanically narrow the search range by folder and pattern, as in grep -iE "open" 01_scan/*.txt — the date tells the when, the tool name the command, and the target what was looked at, all from the filename alone. Rereading is searching, and searching is fast only where an index exists. The filename rule is that index.

Completion Criteria Checklist

  • [ ] I confirmed the vhost principle (same IP, Host-header branching) hands-on
  • [ ] I can explain that vhost fuzzing is "finding a response different from the baseline"
  • [ ] I know what each row of the second-stage enumeration table finds
  • [ ] I ran my third cumulative machine with the folder system (01_scan–04_privesc)
  • [ ] Every output remains as a file under the date_tool_target rule
  • [ ] I tagged stuck points with the [wide/deep] distinction
  • [ ] I recorded "what a log reread found" (or the reason I passed without one)
  • [ ] I incorporated the enumeration system into routine document v3

6. Common Pitfalls & Fixes

Wall 1. "The vhost server won’t come up / curl can’t connect"

Symptom (measured 2026-09-09, curl to a port with no server running):

curl: (7) Failed to connect to 127.0.0.1 port 18099 after 2032 ms: Could not connect to server

Cause: you reached the address, but no program is listening on that port — the server isn’t up yet, it already exited, or the port number differs. Same interpretation as Step 163 Wall 1.
Fix: check that the terminal running the script shows the "[vhost server up]" line, and connect only while the script is alive in that terminal. The script shuts itself down at the end of main(), so curl experiments must happen while it’s running.

Wall 2. "Fuzzing returns everything in the same size — I can’t find anything"

Symptom: hundreds of candidates all come back with the same Size (screen example):

www                     [Status: 200, Size: 15324, ...]
api                     [Status: 200, Size: 15324, ...]
admin                   [Status: 200, Size: 15324, ...]

Cause: one of two things. ① No baseline filter (-fs/-ac), so the default response is printed for everything — that output is "a graveyard of nonexistent names." ② The target is a server that ignores Host and serves the same page (no virtual hosts in use).
Fix: measure the baseline size with one nonexistent name and hide it with -fs size — the same procedure as 3-1’s scene 3. If nothing survives the filter, "this server has no vhosts" is enumeration’s conclusion. You didn’t fail to find — you confirmed absence.

Wall 3. "I found a vhost, but opening it shows a weird page"

Symptom: you discovered dev.target.htb, but opening it in a browser gives a DNS error.
Cause: that name doesn’t exist in public DNS — you discovered it by Host-header substitution, but the browser can’t resolve the name to an IP.
Fix: add TARGET_IP dev.target.htb to /etc/hosts (on Windows, C:WindowsSystem32driversetchosts). For curl experiments, -H "Host: dev.target.htb" is enough; for browser exploration, a hosts entry is more convenient.

Wall 4. "I kept logs but can’t find things later"

Symptom: 40 files in the folder, and you don’t know which one had the vhost results.
Cause: you saved them as out1.txt, scan2.txt with no filename rule — the volume of logs outgrew your search ability.
Fix: exactly as Exercise 4’s answer. Rename to date_tool_target starting now, and reread by narrowing to a folder before grep. Half of "keeping logs" is saving; the other half is saving so you can find it.

Wall 5. "I did all the second-stage enumeration and there’s still no entrance"

Symptom: every row of 2-3’s table is checked, yet no progress.
Cause: only now have you reached the point where technique shortage (Step 265’s distinction) may legitimately be suspected. Or the checks were hasty — "ran it" and "ran it with a meaningful list" are different.
Fix: check two things. ① Was each row’s wordlist sufficient (one pass with common.txt is close to "didn’t do it" — a full pass reaches the medium list)? ② If still nothing, switch to research — reread the starring service’s HackTricks page (Step 267). The depth of enumeration has an end, and confirming that end is also skill.


7. Summary

Today’s Concepts

Concept One-line explanation
Wide / deep Seeing unseen surfaces / digging deeper into seen ones — the two prescriptions for stuck
Virtual host (vhost) A structure where one IP separates sites by Host header
vhost fuzzing The technique of substituting candidate names and finding a response (size/status) different from baseline
Baseline response The response of a "nonexistent name" — fuzzing’s filter criterion (-fs)
Hidden parameters Undocumented GET/POST arguments — debug, admin, etc.
Enumeration log system 01_scan–04_privesc folders + date_tool_target filenames
Reread routine The procedure of rereading logs in scan → path → version order when stuck

Today’s Commands & Tools

Command What it does
ffuf -u http://$TARGET -H "Host: FUZZ.target.htb" -w list -fs size vhost fuzzing (filter by baseline size)
gobuster vhost -u http://target.htb -w list Another tool for vhost fuzzing
ffuf -u "http://$TARGET/page.php?FUZZ=1" -w params.txt Hidden-parameter discovery
gobuster dir -u URL -w list -x bak,old,zip,conf Backup/config file scan
grep -iE "301|302|401|403" 02_enum/*.txt Reread — collecting non-200 responses
python -u vhost_enum268.py Hands-on model of the vhost principle

An Instinct More Important Than Commands

Today’s measurement has one key point — the same address returned different worlds by a single Host header, and those worlds’ existence was revealed by "one response different from the baseline." The depth of enumeration is, in the end, this — eyes that know how many more layers lie behind the visible, and a list that checks those layers mechanically.

And trust the log system. Getting stuck late in Medium is not "not knowing" but "missing," and the prescription for missing is faster when you reread what you kept rather than look anew. The statistics and logs of 3 cumulative machines are in place — from the next chapter begins the stretch of vulnerability combination, chaining these discoveries as each other’s keys.


Once every box is checked, Step 268 is complete.

ONE STEP FURTHER

Finished this lesson?

Check the completion criteria, then mark your progress.

Something wrong with this page or a link? Let us know.
Next