kavklaw@llm $ cat web-enumeration-guide.md
โฑ๏ธ 30 min read ยท Complete web testing methodology from first HTTP request to finding every attack surface
sudo apt install seclists on Kali/Debian)Kali Linux has all of these preinstalled. On Debian/Ubuntu: sudo apt install curl gobuster whatweb seclists. For ffuf, grab the binary from GitHub releases.
Got a web target and need to enumerate it right now? Here's the essential checklist:
# 1. First look โ what's on the page?
curl -I http://target.htb # Headers
curl -s http://target.htb | head -100 # First 100 lines of source
# 2. Technology fingerprinting
whatweb http://target.htb
# 3. Directory brute force
gobuster dir -u http://target.htb -w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt -t 50
# 4. Subdomain/vhost discovery
gobuster vhost -u http://target.htb -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt --append-domain
# 5. Check common files
curl -s http://target.htb/robots.txt
curl -s http://target.htb/sitemap.xml
curl -s http://target.htb/.git/HEAD
curl -s http://target.htb/.env
That's the speed run. Below is each technique broken down in detail.
Web enumeration is the systematic process of gathering information about a web application and its infrastructure. It sits between port scanning (which found the web service) and exploitation (where you attack it). Good enumeration is the difference between finding a vulnerability in 5 minutes and banging your head against the wall for hours.
At its core, the web is a conversation between your browser (the client) and a remote computer (the server). Your browser sends an HTTP request โ essentially a structured text message saying "GET me the page at /login" โ and the server sends back an HTTP response containing the page content, a status code (200 = OK, 404 = not found, 500 = server error), and headers with metadata. Every tool in this guide โ curl, gobuster, ffuf, whatweb โ is just sending HTTP requests and analyzing the responses. Understanding this request-response cycle is fundamental because every web vulnerability comes from how the server processes your requests.
DNS (Domain Name System) is the internet's phone book โ it translates human-readable names like target.htb into IP addresses like 10.10.10.100. A subdomain is a prefix added to the main domain, like admin.target.htb or dev.target.htb, and it can point to a completely different application running on the same or different server. Subdomains are critical in pentesting because organizations often run development, staging, and internal applications on subdomains with weaker security than the main site. Many CTF challenges hide the real vulnerable application on a subdomain that's only discoverable through enumeration.
The goal is to map out the application's attack surface (every entry point where data you control gets processed by the server). The more entry points you find, the more chances you have to find a vulnerability:
Web enumeration is iterative. Each finding can open new avenues. Finding a subdomain leads to a new application. Finding a JavaScript file reveals API endpoints. Finding an API endpoint reveals parameters. It's a tree you keep expanding until you've mapped everything.
Before any tools, start with a simple HTTP request and pay attention to everything:
# GET request with full headers
curl -v http://target.htb/ 2>&1
# What to observe:
# * Connected to target.htb (10.10.10.100) port 80
# > GET / HTTP/1.1
# > Host: target.htb
# > User-Agent: curl/7.88.1
# < HTTP/1.1 200 OK
# < Server: Apache/2.4.41 (Ubuntu) โ Server software + OS
# < X-Powered-By: PHP/7.4.3 โ Backend language + version
# < Set-Cookie: PHPSESSID=abc123... โ Session management
# < Content-Type: text/html; charset=UTF-8
# Headers-only request (faster)
curl -I http://target.htb/
# Follow redirects
curl -L -I http://target.htb/
# View the page source
curl -s http://target.htb/ | less
# Check different HTTP methods โ HTTP defines several "verbs" for different actions:
# GET = retrieve data, POST = submit data, PUT = upload/replace, DELETE = remove
# OPTIONS = ask what methods are allowed, TRACE = echo request back (debug)
curl -X OPTIONS http://target.htb/ -v
curl -X PUT http://target.htb/test.txt -d "test" -v # โ ๏ธ Can write files if PUT is enabled โ authorized tests only
curl -X TRACE http://target.htb/ -v
๐ก Pro Tip: Always add the target hostname to/etc/hostsbefore testing. Many web applications behave differently based on the Host header.echo "10.10.10.100 target.htb" | sudo tee -a /etc/hosts. If you discover virtual hosts later, add those too.
Knowing what technologies a web application uses tells you what vulnerabilities to look for, what default files to check, and what exploits might apply.
# WhatWeb โ identifies technologies from HTTP responses
whatweb http://target.htb
# Output: http://target.htb [200 OK] Apache[2.4.41], Country[US],
# HTTPServer[Ubuntu Linux][Apache/2.4.41 (Ubuntu)], IP[10.10.10.100],
# JQuery, PHP[7.4.3], Title[My Website], WordPress[5.7.2],
# X-Powered-By[PHP/7.4.3]
# Verbose output with all plugins
whatweb -v http://target.htb
# Aggressive mode (makes additional requests)
whatweb -a 3 http://target.htb
# Scan multiple targets
whatweb http://target.htb http://target.htb:8080
Wappalyzer is a browser extension that identifies technologies on web pages. It detects:
Install the Wappalyzer browser extension and just visit the page โ it automatically analyzes technologies and shows them in the toolbar icon.
# BuiltWith โ technology lookup (web-based)
# https://builtwith.com/target.htb
# Netcraft โ site report
# https://sitereport.netcraft.com/?url=target.htb
# Command-line alternative: httpx
httpx -u http://target.htb -tech-detect -status-code -title -web-server
# Nuclei technology detection
nuclei -u http://target.htb -t technologies/
# Check HTTP response headers for clues
curl -I http://target.htb/ 2>/dev/null | grep -iE "server|x-powered|x-aspnet|x-generator"
# Error page fingerprinting โ trigger a 404
curl http://target.htb/nonexistent_page_12345 -v
# Apache default 404 looks different from Nginx, IIS, Tomcat, etc.
# Custom error pages โ the application may hide the server identity
# Check for framework-specific files
curl -s http://target.htb/wp-login.php # WordPress
curl -s http://target.htb/administrator/ # Joomla
curl -s http://target.htb/user/login # Drupal
curl -s http://target.htb/admin/ # Generic admin panel
curl -s http://target.htb/elmah.axd # ASP.NET error log
curl -s http://target.htb/web.config # IIS config
curl -s http://target.htb/server-status # Apache server-status
# PHP info page (sometimes left exposed)
curl -s http://target.htb/phpinfo.php
curl -s http://target.htb/info.php
HTTP headers leak a tremendous amount of information. Both response headers from the server and request headers you can manipulate are important.
curl -I http://target.htb/ 2>/dev/null
# Server identification
Server: Apache/2.4.41 (Ubuntu) # Web server + OS
X-Powered-By: PHP/7.4.3 # Backend language
X-AspNet-Version: 4.0.30319 # ASP.NET version
X-Generator: WordPress 5.7.2 # CMS
# Security headers (or lack thereof) โ missing ones are findings in a pentest!
X-Frame-Options: DENY # Clickjacking protection (prevents embedding in iframes)
X-Content-Type-Options: nosniff # MIME sniffing protection
X-XSS-Protection: 1; mode=block # XSS (Cross-Site Scripting) filter
Content-Security-Policy: ... # CSP โ restricts which resources the page can load
Strict-Transport-Security: ... # HSTS โ forces browser to always use HTTPS
Access-Control-Allow-Origin: * # CORS (Cross-Origin Resource Sharing) โ controls
# which other websites can make requests to this one
# Missing security headers = findings in a pentest!
# Interesting response headers
Set-Cookie: session=abc; HttpOnly; Secure # Cookie attributes
X-Request-ID: abc-123-def # Request tracking
X-Debug: true # Debug mode!
X-Backend-Server: app-server-01 # Internal infrastructure
# Check all ports for different headers
for port in 80 443 8080 8443; do
echo "=== Port $port ==="
curl -I http://target.htb:$port/ 2>/dev/null | head -10
done
# Test for virtual hosts via Host header
curl -H "Host: admin.target.htb" http://10.10.10.100/
curl -H "Host: dev.target.htb" http://10.10.10.100/
# X-Forwarded-For โ a header that proxy servers add to indicate the original
# client IP. If the app trusts this header, you can spoof your IP to bypass
# restrictions that only allow access from localhost or internal networks
curl -H "X-Forwarded-For: 127.0.0.1" http://target.htb/admin/
curl -H "X-Real-IP: 127.0.0.1" http://target.htb/admin/
curl -H "X-Originating-IP: 127.0.0.1" http://target.htb/admin/
curl -H "X-Custom-IP-Authorization: 127.0.0.1" http://target.htb/admin/
# User-Agent testing
curl -A "Mozilla/5.0" http://target.htb/ # Standard browser
curl -A "Googlebot/2.1" http://target.htb/ # Google crawler
curl -A "" http://target.htb/ # Empty UA
# Accept header testing
curl -H "Accept: application/json" http://target.htb/api/
curl -H "Accept: application/xml" http://target.htb/api/
Always read the HTML source code. Don't just look at the rendered page โ view the raw source for hidden information.
# Download and examine the page source
curl -s http://target.htb/ > source.html
# HTML comments (developers leave notes, credentials, TODOs)
grep -n "<!--" source.html
# Common finds:
# <!-- TODO: remove before production -->
# <!-- admin:password123 -->
# <!-- Debug mode enabled -->
# <!-- Old login at /login_backup.php -->
# Hidden form fields
grep -i "hidden" source.html
# <input type="hidden" name="debug" value="0">
# <input type="hidden" name="role" value="user">
# โ Try changing these: debug=1, role=admin
# Links and paths
grep -oP 'href="[^"]*"' source.html | sort -u
grep -oP 'src="[^"]*"' source.html | sort -u
grep -oP 'action="[^"]*"' source.html | sort -u
# Email addresses
grep -oP '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}' source.html
# Embedded JavaScript with interesting data
grep -i "var \|const \|let \|api\|token\|key\|secret\|password\|endpoint" source.html
# Base64 encoded strings
grep -oP '[A-Za-z0-9+/]{20,}={0,2}' source.html | while read b64; do
decoded=$(echo "$b64" | base64 -d 2>/dev/null)
if [ $? -eq 0 ]; then echo "$b64 โ $decoded"; fi
done
# robots.txt โ tells crawlers what NOT to index (= interesting paths)
curl -s http://target.htb/robots.txt
# Disallow: /admin/
# Disallow: /backup/
# Disallow: /internal/api/
# โ These are all worth investigating!
# sitemap.xml โ full site structure
curl -s http://target.htb/sitemap.xml
# .git directory โ exposed source code!
curl -s http://target.htb/.git/HEAD
# If this returns "ref: refs/heads/main" โ source code is exposed
# Use git-dumper to extract:
git-dumper http://target.htb/.git/ ./dumped_repo
# .env file โ environment variables (credentials, API keys)
curl -s http://target.htb/.env
# DB_PASSWORD=supersecret
# API_KEY=sk-live-abc123
# JWT_SECRET=mysecretkey
# .htaccess โ Apache configuration
curl -s http://target.htb/.htaccess
# Other sensitive files
curl -s http://target.htb/crossdomain.xml
curl -s http://target.htb/clientaccesspolicy.xml
curl -s http://target.htb/security.txt
curl -s http://target.htb/.well-known/security.txt
curl -s http://target.htb/composer.json
curl -s http://target.htb/package.json
curl -s http://target.htb/Gruntfile.js
curl -s http://target.htb/config.php.bak
curl -s http://target.htb/web.config
curl -s http://target.htb/WEB-INF/web.xml
whatweb http://target.htb as one of your first commands on any web target. Wappalyzer (browser extension) provides similar capability in the GUI.$ -u http://target.htb -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt --append-domain
gobuster vhost fuzzes the Host header to discover virtual hosts sharing the same IP. This is crucial in CTFs where the main site is often a decoy and the real vulnerable app runs on a subdomain like dev.target.htb or admin.target.htb. Always fuzz for vhosts on every web target. ffuf can also do this with -H "Host: FUZZ.target.htb".$ curl -s http://target.htb/.git/HEAD
ref: refs/heads/main
$ curl -I http://target.htb/.env
HTTP/1.1 200 OK
Content-Type: text/plain
$ curl -s http://target.htb/.env
DB_HOST=localhost
DB_USER=appuser
DB_PASS=S3cretDbP@ss!
JWT_SECRET=my-super-secret-jwt-key-2024
๐ก Pro Tip: Exposed.gitdirectories are one of the most impactful findings in real-world pentesting. With the full source code, you can find hardcoded credentials, understand application logic, find hidden endpoints, and discover vulnerabilities. Always check/.git/HEADon every web target.
JavaScript files are a goldmine. They contain API routes, authentication logic, hidden endpoints, debug functions, and sometimes hardcoded credentials or API keys.
# Extract all JS file references from the page
curl -s http://target.htb/ | grep -oP 'src="[^"]*\.js[^"]*"' | sort -u
# src="/static/js/main.js"
# src="/static/js/app.bundle.js"
# src="/assets/js/api-client.js"
# Download all JS files
for js in $(curl -s http://target.htb/ | grep -oP 'src="\K[^"]*\.js[^"]*'); do
echo "Downloading: $js"
curl -s "http://target.htb$js" -o "$(basename $js)"
done
# Check for sourcemaps (unminified source code!)
curl -s http://target.htb/static/js/main.js | tail -1
# //# sourceMappingURL=main.js.map
curl -s http://target.htb/static/js/main.js.map | python3 -m json.tool
# Search for API endpoints
grep -oP '["'\'']/api/[^"'\'']*' *.js | sort -u
# "/api/v1/users"
# "/api/v1/admin/settings" โ Hidden admin API!
# "/api/internal/debug" โ Debug endpoint!
# Search for hardcoded credentials/keys
grep -iP '(api[_-]?key|token|secret|password|auth|bearer)\s*[:=]' *.js
# const API_KEY = "sk-live-abc123def456"
# let adminToken = "eyJhbGciOiJ..."
# Search for interesting URLs/endpoints
grep -oP 'https?://[^"'\''\\s]*' *.js | sort -u
# Search for hidden routes (React/Angular/Vue router)
grep -oP 'path:\s*["'\'']/[^"'\'']*' *.js | sort -u
# path: "/admin/dashboard"
# path: "/debug/phpinfo"
# path: "/secret-page"
# Beautify minified JS
# Install js-beautify: npm install -g js-beautify
js-beautify main.js > main.beautified.js
# Use LinkFinder to extract endpoints automatically
python3 linkfinder.py -i http://target.htb/static/js/main.js -o cli
# Source maps contain the ORIGINAL unminified source code
# Check for .map files
curl -s http://target.htb/static/js/main.js.map -o main.js.map
# Extract source files from map
# Install: npm install -g sourcemapper
sourcemapper -url http://target.htb/static/js/main.js.map -output extracted/
# Now you have the full React/Angular/Vue source code!
find extracted/ -name "*.js" -exec grep -l "password\|secret\|admin" {} \;
๐ก Pro Tip: Modern web apps (React, Angular, Vue) compile everything into bundle files. These bundles contain ALL client-side logic, route definitions, and API calls. A single app.bundle.js can reveal dozens of hidden endpoints. Always download and analyze every JS file, and always check for source maps.
Cookies reveal session management details and can expose vulnerabilities:
# View cookies set by the server
curl -v http://target.htb/login 2>&1 | grep "Set-Cookie"
# Set-Cookie: PHPSESSID=abc123; path=/; HttpOnly
# Set-Cookie: user_role=dXNlcg==; path=/
# Check cookie attributes
# HttpOnly โ not accessible via JavaScript (XSS can't steal it)
# Secure โ only sent over HTTPS
# SameSite โ CSRF protection (Strict/Lax/None)
# Path โ which paths receive the cookie
# Domain โ which domains receive the cookie
# Decode Base64 cookies
echo "dXNlcg==" | base64 -d
# "user" โ Can we change this to "admin"?
# JWT token inspection
# If the cookie looks like: eyJhbGciOiJ... it's a JWT (three base64url parts separated by dots)
# Decode at jwt.io, or from the command line (JWT uses base64url, needs padding fix):
echo "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" | tr '_-' '/+' | base64 -d 2>/dev/null
# {"alg":"HS256","typ":"JWT"}
# Try modifying cookies
curl -b "user_role=YWRtaW4=" http://target.htb/dashboard
# "YWRtaW4=" = base64("admin") โ does it work?
# Flask session cookies (signed but not encrypted)
# Install: pip install flask-unsign
flask-unsign --decode --cookie 'eyJ1c2VyIjoiZ3Vlc3QifQ.Xt0...'
# {'user': 'guest'}
flask-unsign --unsign --cookie 'eyJ1c2VyIjoiZ3Vlc3QifQ.Xt0...' \
--wordlist /usr/share/wordlists/rockyou.txt
# Found secret key: 'supersecret'
flask-unsign --sign --cookie '{"user":"admin"}' --secret 'supersecret'
# Forged admin cookie!
Directory brute forcing discovers hidden pages, backup files, admin panels, and configuration files that aren't linked from the main site.
# Gobuster โ directory mode
gobuster dir -u http://target.htb \
-w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt \
-t 50 -o gobuster_dirs.txt
# With file extensions
gobuster dir -u http://target.htb \
-w /usr/share/seclists/Discovery/Web-Content/raft-medium-files.txt \
-x php,html,txt,bak,old,zip,config \
-t 50 -o gobuster_files.txt
# ffuf โ faster and more flexible
ffuf -u http://target.htb/FUZZ \
-w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt \
-t 50 -o ffuf_results.json -of json
# With extensions
ffuf -u http://target.htb/FUZZ \
-w /usr/share/seclists/Discovery/Web-Content/raft-medium-words.txt \
-e .php,.html,.txt,.bak,.zip,.config,.env \
-t 50
# Gobuster does NOT support recursive scanning in dir mode
# Use feroxbuster instead โ it's recursive by default
feroxbuster -u http://target.htb \
-w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt \
-t 50 -x php,html,txt
# feroxbuster will automatically scan discovered directories like /admin/, /api/, etc.
# Filter by response size (exclude common 404 pages)
ffuf -u http://target.htb/FUZZ \
-w /usr/share/wordlists/dirb/big.txt \
-fs 1234 # Filter out responses of size 1234 bytes (the 404 page)
Small/fast (start here):
/usr/share/seclists/Discovery/Web-Content/common.txt โ 4,700 entries/usr/share/wordlists/dirb/common.txt โ 4,614 entriesMedium (good balance):
/usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt โ 30,000 entries/usr/share/seclists/Discovery/Web-Content/raft-medium-files.txt โ 17,000 entriesLarge (thorough):
/usr/share/seclists/Discovery/Web-Content/raft-large-directories.txt โ 62,000 entries/usr/share/seclists/Discovery/Web-Content/directory-list-2.3-big.txt โ 1.2M entriesTechnology-specific:
/usr/share/seclists/Discovery/Web-Content/apache.txt
/usr/share/seclists/Discovery/Web-Content/nginx.txt
/usr/share/seclists/Discovery/Web-Content/iis.txt
/usr/share/seclists/Discovery/Web-Content/tomcat.txt
/usr/share/seclists/Discovery/Web-Content/CGIs.txt
A CMS (Content Management System) is software for building websites without writing code from scratch, like WordPress, Joomla, or Drupal. They're common targets because they have well-known vulnerabilities (especially through plugins), default files, and specialized scanning tools that automate finding weaknesses.
# Detection
curl -s http://target.htb/ | grep -i "wp-content\|wordpress"
curl -s http://target.htb/wp-login.php
curl -s http://target.htb/wp-json/wp/v2/users # Enumerate users!
# WPScan โ thorough WordPress scanner
wpscan --url http://target.htb/ --enumerate u,p,t,tt
# u = users, p = plugins, t = themes, tt = timthumbs
# Aggressive plugin detection
wpscan --url http://target.htb/ --enumerate ap --plugins-detection aggressive
# With API token (for vulnerability data)
wpscan --url http://target.htb/ --enumerate vp,vt \
--api-token YOUR_TOKEN_HERE
# Brute force WordPress login
wpscan --url http://target.htb/ -U admin -P /usr/share/wordlists/rockyou.txt
# Manual enumeration
curl -s http://target.htb/wp-json/wp/v2/users | python3 -m json.tool
# Returns user IDs, names, slugs
curl -s "http://target.htb/?author=1" # User enumeration via author IDs
# Check WordPress version
curl -s http://target.htb/readme.html | grep -i version
curl -s http://target.htb/feed/ | grep generator
# Detection
curl -s http://target.htb/ | grep -i "joomla"
curl -s http://target.htb/administrator/
curl -s http://target.htb/configuration.php~ # Backup config?
# JoomScan โ Joomla scanner
joomscan -u http://target.htb/
# Version detection
curl -s http://target.htb/administrator/manifests/files/joomla.xml | grep version
curl -s http://target.htb/language/en-GB/en-GB.xml | grep version
# Detection
curl -s http://target.htb/ | grep -i "drupal"
curl -s http://target.htb/CHANGELOG.txt | head -5 # Version!
curl -s http://target.htb/core/CHANGELOG.txt | head -5
# Droopescan
droopescan scan drupal -u http://target.htb/
# Known Drupal vulns
# Drupalgeddon (CVE-2014-3704) โ SQL injection
# Drupalgeddon2 (CVE-2018-7600) โ RCE
# Drupalgeddon3 (CVE-2018-7602) โ RCE
An API (Application Programming Interface) is a set of URLs that the website's frontend uses to talk to the backend. For example, /api/v1/users might return a list of users in JSON format. Modern web apps are API-driven, meaning the real logic lives in API endpoints rather than traditional HTML pages. Discovering these endpoints reveals attack surfaces that directory brute forcing often misses.
# Check for API documentation
curl -s http://target.htb/api/
curl -s http://target.htb/api/docs
curl -s http://target.htb/api/v1/
curl -s http://target.htb/swagger.json
curl -s http://target.htb/swagger/
curl -s http://target.htb/openapi.json
curl -s http://target.htb/api-docs/
curl -s http://target.htb/v2/api-docs
curl -s http://target.htb/graphql # GraphQL endpoint
curl -s http://target.htb/graphiql # GraphQL IDE
# Fuzz API endpoints
ffuf -u http://target.htb/api/v1/FUZZ \
-w /usr/share/seclists/Discovery/Web-Content/api/objects.txt \
-t 50
# Fuzz API versions
ffuf -u http://target.htb/api/FUZZ/users \
-w <(seq 1 20 | sed 's/^/v/') \
-t 10
# Test HTTP methods on discovered endpoints
for method in GET POST PUT DELETE PATCH OPTIONS; do
echo "=== $method ==="
curl -X $method http://target.htb/api/v1/users -s -o /dev/null -w "%{http_code}\n"
done
GraphQL is an alternative to REST APIs where you send a query describing exactly what data you want. The powerful thing for attackers: GraphQL has an "introspection" feature that lets you ask the API to describe its entire structure, including all available data types and fields.
# Introspection query โ get the full schema
curl -s -X POST http://target.htb/graphql \
-H "Content-Type: application/json" \
-d '{"query":"{__schema{types{name,fields{name,type{name}}}}}"}'
# Simplified introspection
curl -s -X POST http://target.htb/graphql \
-H "Content-Type: application/json" \
-d '{"query":"{__schema{queryType{fields{name}}}}"}'
# Tools for GraphQL
# graphql-voyager โ visual schema explorer
# graphql-path-enum โ find sensitive paths
# InQL โ Burp extension for GraphQL
# Arjun โ parameter discovery tool
arjun -u http://target.htb/api/endpoint
# ffuf parameter fuzzing
ffuf -u "http://target.htb/page?FUZZ=test" \
-w /usr/share/seclists/Discovery/Web-Content/burp-parameter-names.txt \
-fs 1234 # Filter default response size
# POST parameter fuzzing
ffuf -u http://target.htb/api/endpoint \
-X POST -H "Content-Type: application/json" \
-d '{"FUZZ":"test"}' \
-w /usr/share/seclists/Discovery/Web-Content/burp-parameter-names.txt \
-fs 1234
Virtual hosts (vhosts) allow multiple websites to run on a single IP address โ this is how most web servers in the real world operate, since IP addresses are expensive and limited. The web server looks at the Host header in your HTTP request to decide which site to show you. This means admin.target.htb and dev.target.htb could both point to the same IP but show completely different websites with entirely different codebases and security postures. Discovering vhosts reveals hidden applications the main site never links to โ and in CTFs, this is often where the actual vulnerability lives.
# Gobuster vhost mode
gobuster vhost -u http://target.htb \
-w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
--append-domain
# ffuf vhost discovery
ffuf -u http://target.htb/ \
-H "Host: FUZZ.target.htb" \
-w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
-fs 1234 # Filter the default vhost response size
# Manual vhost testing
curl -H "Host: admin.target.htb" http://10.10.10.100/ -s | head -20
curl -H "Host: dev.target.htb" http://10.10.10.100/ -s | head -20
curl -H "Host: staging.target.htb" http://10.10.10.100/ -s | head -20
curl -H "Host: internal.target.htb" http://10.10.10.100/ -s | head -20
curl -H "Host: api.target.htb" http://10.10.10.100/ -s | head -20
# DNS subdomain enumeration
# If the target has real DNS (not just /etc/hosts)
gobuster dns -d target.htb -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt
# Add discovered vhosts to /etc/hosts
echo "10.10.10.100 admin.target.htb dev.target.htb" | sudo tee -a /etc/hosts
$
wpscan --url http://target.htb/ --enumerate u,p,t scans for users (u), plugins (p), and themes (t). WPScan is the gold standard for WordPress testing. For vulnerable plugins/themes, add --enumerate vp,vt with an API token (--api-token). WordPress plugins are the most common attack vector โ many have known RCE vulnerabilities. Also try /wp-json/wp/v2/users for user enumeration.X-Forwarded-For: 127.0.0.1 tricks the app into thinking the request comes from localhost. Also try X-Real-IP, X-Originating-IP, and X-Custom-IP-Authorization headers.echo "10.10.10.100 target.htb" | sudo tee -a /etc/hosts. Many web applications use virtual hosting and return different content based on the Host header. Browsing by IP might show a default page, while using the hostname shows the actual application. This is especially important in HTB/CTF environments where vhosts are common.๐ก Pro Tip: Virtual host discovery is important in CTFs and HTB machines. When you get a box with a web server, the main site might be a dead end. The real vulnerable application is often on a subdomain. Always fuzz for vhosts. If ffuf shows many results with the same size, use -fs SIZE to filter out the default response.
A WAF (Web Application Firewall) is a security layer that sits between you and the web server. It inspects your requests and blocks anything that looks malicious (like SQL injection attempts or directory brute forcing). Detecting a WAF early helps you adjust your approach, for example by slowing down scans or encoding your payloads differently.
# wafw00f โ WAF detection tool
wafw00f http://target.htb
# Manual WAF detection โ send a malicious request and observe:
curl "http://target.htb/?id=1' OR 1=1--" -v
# Nmap WAF detection scripts
nmap -p 80 --script=http-waf-detect,http-waf-fingerprint target.htb
WAF responses often include: 403 Forbidden with custom error page, different response headers, modified response body ("Access Denied"), or redirect to a CAPTCHA challenge page.
Common WAF signatures in headers (useful heuristics, but not guarantees โ headers can be stripped, spoofed, or hidden behind CDNs):
cf-ray, cf-cache-status โ Cloudflarex-sucuri-id โ Sucurix-akamai-transformed โ Akamaix-cdn: Served-By-Zenedge โ Zenedgex-protected-by: Sqreen โ Sqreen# If there's a WAF, consider:
# 1. Finding the origin IP (bypass Cloudflare/CDN)
# - Check DNS history: https://securitytrails.com
# - Check Censys/Shodan for the origin IP
# - Check mail headers (MX records reveal origin)
# 2. Adjusting your requests
# - Slow down scanning (reduce threads)
# - Rotate User-Agent strings
# - Use different encoding for payloads
# - Try HTTP parameter pollution
# 3. Using WAF bypass techniques (varies by WAF)
# - URL encoding, double encoding
# - Case variation
# - Comment injection in SQL
# - Alternative syntax for commands
Here's the complete web enumeration methodology I use on every engagement. Follow this order โ each step feeds into the next.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ WEB ENUMERATION METHODOLOGY โ
โโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โผ
Phase 1: FIRST CONTACT โโโ curl headers, whatweb,
(5 min) add to /etc/hosts
โ
โผ
Phase 2: QUICK WINS โโโโโ robots.txt, .git/HEAD,
(5 min) .env, source code review
โ
โผ
Phase 3: DIRECTORY โโโโโโโ gobuster/ffuf + vhost
ENUM (parallel) discovery (background)
โ
โผ
Phase 4: DEEP ANALYSIS โโ JS analysis, cookies,
(while scans run) CMS scanning, API discovery
โ
โผ
Phase 5: ITERATE โโโโโโโโโโ New finding? Go back to
Phase 2 for that target!
# Add hostname to /etc/hosts
echo "10.10.10.100 target.htb" | sudo tee -a /etc/hosts
# Quick look at the target
curl -I http://target.htb/
curl -s http://target.htb/ | head -200
whatweb http://target.htb
# Check all web ports
for port in 80 443 8080 8443 3000 5000 8000 9090; do
code=$(curl -s -o /dev/null -w "%{http_code}" http://target.htb:$port/ 2>/dev/null)
[ "$code" != "000" ] && echo "Port $port: HTTP $code"
done
# Check common files
for file in robots.txt sitemap.xml .git/HEAD .env .htaccess crossdomain.xml security.txt; do
code=$(curl -s -o /dev/null -w "%{http_code}" http://target.htb/$file)
[ "$code" == "200" ] && echo "FOUND: /$file"
done
# View source โ look for comments, hidden fields, JS files
curl -s http://target.htb/ | grep -i "