Enumeration Methodology
This is the routine I run on every target before I even think about a shell. The whole point of this phase is to map the attack surface so the way in basically jumps out at me. Skip it and you end up staring at the screen wondering why nothing works.
A lot of this is distilled from reading a ton of writeups (0xdf and IppSec especially). The tools change box to box, but the habits below are the ones that keep paying off. Go read those blogs after you try a box yourself. You will start noticing the same moves again and again, and that pattern recognition is the actual skill.
If I’m stuck, it almost always means I did not enumerate hard enough. Back up a step and dig again.
Phase 0: set up shop
Keep everything for one target in one place. Future me always says thanks, and so does the report.
export IP=10.10.10.10 # set it once, use it everywhere
mkdir -p $IP/{scans,loot,exploits,www}
cd $IP
Quick ping to check it is alive, and the TTL leaks the OS:
ping -c 2 $IP
# TTL around 64 = probably Linux
# TTL around 128 = probably Windows
Take notes as you go, not at the end. A running log of every command and every interesting line of output is what lets you retrace your steps when something finally clicks three hours later. Screenshot anything visual. This is also literally the raw material for the report.
Phase 1: find the open ports
Two passes. A fast one to see what is open, then a slow one that only pokes the ports that actually answered. No sense scanning all 65k ports twice.
# fast: every TCP port, just tell me what is open
nmap -p- --min-rate=1000 -T4 $IP -oN scans/all-ports.txt
# pull the open ports into a variable, then deep scan only those
ports=$(grep -oP '^\d+(?=/tcp)' scans/all-ports.txt | paste -sd,)
nmap -p$ports -sC -sV -oA scans/services $IP
-oA saves all three formats (normal, greppable, XML). The XML is handy if you ever want to feed it to other tools.
Do not sleep on UDP. It is easy to forget and it loves to hide stuff. The usual suspects are 53 (DNS), 69 (TFTP), 123 (NTP), 161 (SNMP), and 500 (IKE):
sudo nmap -sU --top-ports 100 -oN scans/udp.txt $IP
A few things people miss here:
rustscanis a great fast front end if the box is slow to scan. It finds open ports quick and hands them to nmap.- If a port shows as
filtered, a firewall may be dropping probes. Try again later or with different timing before writing it off. - Full
-A(aggressive: OS detection, traceroute, scripts) is fine on a lab box but noisy. On a real engagement dial it back.
Before moving on, write down for each open port: what service, what exact version, and one thing you want to find out about it. Those version strings feed straight into the next steps.
Phase 2: work every service
Go port by port. This is where boxes are won. Here are the moves I run per service, pulled from the patterns that show up constantly in writeups.
21 FTP
Try an anonymous login first. Free files more often than you would think.
ftp $IP # user: anonymous, pass: anything
# once in: ls -la, then 'binary' and 'get' anything interesting
Note the banner and version for searchsploit. Some FTP servers let you reach the web root, so an upload here can turn into code execution on port 80.
22 SSH
You rarely break SSH itself. Note the version (it dates the OS), note which auth methods are allowed, and park it. The second you find creds anywhere else, SSH is your shell. Watch for private keys in loot.
25 / 465 / 587 SMTP
Mail servers can leak usernames.
smtp-user-enum -M VRFY -U users.txt -t $IP
53 DNS
If it is a DNS server, try a zone transfer. When it works you get the whole map for free.
dig axfr @$IP target.htb
80 / 443 HTTP(S)
This gets its own phase below. It is the most common way in, so it earns the most time.
88 Kerberos
Kerberos on 88 means you are looking at a domain controller. Switch to the AD playbook. You can enumerate valid usernames before you have any creds:
kerbrute userenum -d target.htb --dc $IP users.txt
111 / 2049 NFS
Check for exported shares. Sometimes mountable with no auth.
showmount -e $IP
139 / 445 SMB
Huge one. Run the whole battery.
netexec smb $IP # banner, OS, domain, signing
netexec smb $IP -u '' -p '' # null session?
netexec smb $IP -u 'guest' -p '' # guest?
smbmap -H $IP # what shares, what access
smbclient -L //$IP/ -N # list shares, no password
enum4linux-ng -A $IP # kitchen sink
If you get any foothold, list users by cycling RIDs with rpcclient:
rpcclient -U "" -N $IP
> enumdomusers
> queryuser 0x1f4
Readable shares get pulled down and grepped for creds. Do not skim them.
161 UDP SNMP
Underrated. Community strings are often left at defaults.
onesixtyone -c /usr/share/seclists/Discovery/SNMP/common-snmp-community-strings.txt $IP
snmpwalk -v2c -c public $IP # if 'public' works, walk the whole tree
SNMP can dump running processes (with full command lines, sometimes including passwords), listening ports, and user accounts.
389 / 636 LDAP
Try an anonymous bind and dump what you can.
ldapsearch -x -H ldap://$IP -s base namingcontexts
ldapsearch -x -H ldap://$IP -b "DC=target,DC=htb"
3306 / 5432 / 1433 databases
Test default creds and ask why it is even exposed to you. root with no password still happens.
3389 RDP
Note it and come back once you have creds. Great for credential reuse checks with netexec.
5985 / 5986 WinRM
Windows remote management. The moment you have valid Windows creds, this is often your shell.
netexec winrm $IP -u user -p pass # test creds
evil-winrm -i $IP -u user -p pass # get the shell
6379 Redis
Frequently exposed with no auth. Connect and look around.
redis-cli -h $IP
> info
> keys *
5900 VNC
Remote desktop. Test for no-auth or weak passwords, then connect with a viewer.
9200 Elasticsearch / 27017 MongoDB / 11211 memcached
Data stores that are regularly left open. Hit the REST API or client and dump what is there. Secrets and creds love to sit in databases.
curl http://$IP:9200/_cat/indices # elasticsearch
1521 Oracle / 873 rsync / 79 finger / 1099 Java RMI
Less common but they show up. rsync can expose modules with rsync $IP::. finger leaks users. Note the versions and look them up.
8080 / 8000 / 8443 and other high web ports
Treat every one of these as its own website and run the full web phase against each. Dev servers, admin panels, and Tomcat or Jenkins instances live up here all the time.
Phase 3: the web, in depth
Web is where most footholds hide, so slow down here. The number one mistake is running a directory brute force and calling it a day. Look at the thing like a human first.
Look with your own eyes
Open the page. Actually use the app. What is it for? What stack is it on? Any login, search, upload, or contact form is a potential door. Then dig into the parts a browser hides:
- View source. Read the HTML comments. Devs leave creds, paths, and TODOs in there constantly.
- Open the JavaScript files. They often list API endpoints, hidden parameters, and sometimes hardcoded secrets or keys.
- Check
robots.txtandsitemap.xml. They point at paths someone wanted hidden. - Look at the response headers and cookies.
Server,X-Powered-By, and cookie names likePHPSESSIDorJSESSIONIDfingerprint the stack fast. - Favicon can identify a known app by its hash if you are unsure what you are looking at.
whatweb http://$IP
curl -sI http://$IP # just the headers
The /etc/hosts habit
If the site redirects you to a name like target.htb, or a cert shows a hostname, the box is doing name-based virtual hosting. Add it to /etc/hosts and browse by name, or you will see a boring default page and miss the real site.
echo "$IP target.htb" | sudo tee -a /etc/hosts
Content discovery
Brute force directories and files, with extensions that match the stack.
feroxbuster -u http://$IP -w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt -x php,txt,html,bak
Notes from experience:
- feroxbuster recurses by default, which is what you want.
- Try more than one wordlist.
raft-*lists are strong general picks. - A
403 Forbiddenis not a dead end. The path exists. Sometimes you can bypass it (trailing slash,%2e, different method,X-Forwarded-For). - Hunt for leftover files:
.bak,.old,~,.swp, andindex.php.bakstyle backups often hold source with secrets. - Check for an exposed
.gitfolder. If it is there,git-dumperrebuilds the whole repo, and you get the source and history.
Virtual host fuzzing
Different vhosts can serve totally different sites on the same IP. Fuzz the Host header and filter out the default response size.
ffuf -u http://$IP -H "Host: FUZZ.target.htb" \
-w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
-fs <size-of-default-response>
Known apps
If it is WordPress, Joomla, a Git server, a Jenkins, whatever, match the exact version to public advisories. Run the app-specific tooling (like wpscan for WordPress). Default and weak creds on admin panels are a classic first step.
Hidden parameters and APIs
The page you see is rarely the whole app.
- Fuzz for hidden GET/POST parameters with
arjun. A single undocumented parameter can be the whole box (think LFI or command injection). - If you spot
/api, look for/api/swagger,/swagger-ui,/openapi.json, or a GraphQL endpoint at/graphql. These document the app’s real attack surface for you. - When you find an input, run the quick probes before anything heavy: a single quote for SQLi,
../../etc/passwdfor LFI, a backtick or semicolon for command injection, and your own listener URL for SSRF.
Automate the boring part
On a box with lots of ports, an automation wrapper saves time and stops you forgetting a service. autorecon and nmapAutomator run the standard scans and per-service scripts for you in the background while you start looking at the web. Let them run, but still read every result yourself. Automation finds the ports, you find the path.
TLS certificates
On HTTPS, read the cert. It often carries hostnames and email addresses that become vhosts or usernames.
openssl s_client -connect $IP:443 2>/dev/null | openssl x509 -noout -text | grep -A1 "Subject Alternative Name"
Phase 4: turn notes into a plan
Take everything you found and make a ranked to-do list. For each idea, write what you need and how likely it is to pan out.
- Anonymous FTP had a config file with a hostname, add it to
/etc/hosts - Web app version X.Y, check
searchsploitand Google for a matching CVE - Readable SMB share, pull it down and grep for creds
- Found user
j.smith, start a username list for password attacks later - JS file referenced
/api/v1/, go fuzz that
Hit the highest-confidence path first. If it flops, the rest of the list is still sitting there waiting.
When I hit a wall
Nine times out of ten the answer was in output I skimmed too fast. Run down this list before rage-quitting:
- Did I scan all the ports, not just the top 1000?
- Did I bother with UDP?
- Did I try every service with default, anonymous, and guest creds?
- Did I actually read every file I can already reach, all the way through, including HTML comments and JS?
- Did I add every hostname and vhost I found to
/etc/hostsand re-run web enum against each one? - Did I check for
.git, backup files, androbots.txt? - Am I matching exact version numbers to public exploits?
- Did I reuse every credential I found against every service (password reuse is everywhere)?
Go slow here so you can go fast later.
Habits worth stealing from the writeup crowd
- Enumerate, do not guess. If you are trying random exploits, you skipped a step.
- Every new piece of info (a username, a hostname, a version) should send you back to re-enumerate with it.
- Keep a creds list and a users list per box, and spray them across services.
- When something works, note exactly why. That “why” is your next note, and it is what makes the next box faster.
Appendix: port to first move
A glance table for when a scan comes back. Not exhaustive, just my “start here” per port.
| Port | Service | First move |
|---|---|---|
| 21 | FTP | anonymous login, grab files, note version |
| 22 | SSH | note version, hold for creds, look for keys |
| 23 | Telnet | try default creds, note it is cleartext |
| 25/465/587 | SMTP | user enum with VRFY |
| 53 | DNS | zone transfer, subdomain brute |
| 79 | finger | enumerate users |
| 80/443 | HTTP(S) | full web phase |
| 88 | Kerberos | it is a DC, kerbrute user enum |
| 110/143 | POP3/IMAP | try creds, read mail |
| 111/2049 | RPC/NFS | showmount, mount shares |
| 135/139/445 | RPC/SMB | netexec, smbmap, enum4linux-ng, RID cycle |
| 161 (udp) | SNMP | onesixtyone, snmpwalk |
| 389/636 | LDAP | anonymous bind, dump naming context |
| 512-514 | r-services | check for trust-based access |
| 873 | rsync | list modules with rsync $IP:: |
| 1099 | Java RMI | check for deserialization exploits |
| 1433/3306/5432 | MSSQL/MySQL/Postgres | default creds, why exposed |
| 1521 | Oracle | SID enum, default creds |
| 2049 | NFS | showmount -e |
| 3128 | Squid proxy | proxy to reach internal services |
| 3389 | RDP | note for credential reuse |
| 5900 | VNC | no-auth or weak password |
| 5985/5986 | WinRM | evil-winrm once you have creds |
| 6379 | Redis | connect unauth, dump keys |
| 8080/8000/8443 | alt web | treat as its own website |
| 9200 | Elasticsearch | REST API, dump indices |
| 11211 | memcached | dump cached data |
| 27017 | MongoDB | connect unauth, dump collections |
Where to go deeper
- 0xdf’s blog. Read the enumeration section of any box, then try to predict his next move before you scroll.
- IppSec’s videos. Watch how he pivots from one finding to the next in real time.
- HackTricks. The per-port and “Pentesting Web” pages are a checklist you will keep open.