45+ boxes in. This is the enumeration workflow I’ve settled on. Not the “correct” one — the one that actually works under time pressure without missing things.

The Two-Pass Nmap Approach

I used to run one big nmap scan. Wasted time. Now I do two passes every single time.

Pass 1 — Fast TCP Scan

nmap -sC -sV -oN scans/initial.nmap $IP

This hits top 1000 ports. Takes maybe 30 seconds. Gives me something to work with immediately while the full scan runs.

Pass 2 — Full Port Scan (Background)

nmap -p- -sV -oN scans/allports.nmap $IP

Run this in another terminal. Catches the services hiding on weird high ports — and there’s almost always one. I’ve lost time on boxes because a web app was running on port 8443 or 50000 and I didn’t find it until 45 minutes in.

When I Also Run UDP

nmap -sU --top-ports 50 -oN scans/udp.nmap $IP

Only top 50. Full UDP scan takes forever and rarely pays off. The ports that matter:

  • 53 — DNS (zone transfers)
  • 161 — SNMP (community strings = free info)
  • 389 — LDAP
  • 123 — NTP (sometimes useful for domain enumeration)

Rule: If it’s an AD box, always run UDP. SNMP and DNS over UDP have given me footholds more than once.


Service-by-Service — What I Actually Check

The order matters. I work through services by likelihood of giving me a foothold.

HTTP/HTTPS (80, 443, 8080, 8443)

Web is almost always the attack surface. I hit it first.

Immediate checks:

  1. Browse the site manually. Read everything. Check source code.
  2. Check /robots.txt, /sitemap.xml
  3. Identify the tech stack — Wappalyzer or response headers
whatweb http://$IP

Directory brute force:

feroxbuster -u http://$IP -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x php,txt,html -o scans/ferox.txt

I use feroxbuster over gobuster now. Recursive by default, faster, better output.

Decision tree for extensions:

  • Linux box → -x php,txt,html,sh
  • Windows box → -x aspx,asp,txt,html,config
  • Identified framework → add framework-specific extensions (.jsp, .do, etc.)

Common mistakes I’ve made:

  • Not adding a hostname to /etc/hosts. Virtual hosting is everywhere. If you see a domain name anywhere — headers, page content, certificate — add it and scan again.
  • Only scanning one wordlist. If directory-list-2.3-medium finds nothing, try raft-medium-words.txt. Different lists catch different things.
  • Ignoring 403s. A 403 means something exists. Try bypasses, try adding extensions, try deeper enumeration on that path.

Subdomain/vhost enumeration:

ffuf -u http://$IP -H "Host: FUZZ.domain.htb" -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -fs <default_size>

The -fs flag is critical. Filter out the default response size or you’ll drown in false positives.


SMB (139, 445)

Second thing I check. SMB leaks info like crazy.

# Null session check
smbclient -N -L //$IP/

# Enum4linux (noisy but thorough)
enum4linux -a $IP

# List shares with credentials later
smbmap -H $IP
smbmap -H $IP -u 'guest' -p ''

What I’m looking for:

  • Readable shares — Download everything. smbget -R smb://$IP/sharename/
  • Writable shares — Potential for SCF file attacks, dropping payloads
  • User lists — RID brute forcing if null sessions work
# RID brute force for usernames
crackmapexec smb $IP -u '' -p '' --rid-brute

Gotcha: smbclient -N and smbmap behave differently with null sessions. If one fails, try the other. I’ve had boxes where smbclient shows nothing but smbmap with guest / empty password lists shares fine.


FTP (21)

Quick check. Either it’s useful or it’s not.

# Anonymous login
ftp $IP
# Username: anonymous
# Password: (blank or anything)

If anonymous works:

  • Download everything: mget *
  • Check if you can upload. Upload a test file. If it’s also serving HTTP, you might have a webshell path.
  • Check for hidden files: ls -la

If anonymous doesn’t work: Move on. Come back with creds later.


DNS (53)

Matters a lot on AD boxes. Less on standalone.

# Zone transfer attempt
dig axfr @$IP domain.htb

# Reverse lookup
dig -x $IP @$IP

# Any records
dig any domain.htb @$IP

Zone transfers are rare on real engagements but show up on HTB/OSCP boxes regularly. Always try it.

Subdomain brute force via DNS:

dnsenum --dnsserver $IP --enum -f /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt domain.htb

LDAP (389, 636)

If LDAP is open, it’s an AD box. Act accordingly.

# Anonymous bind
ldapsearch -x -H ldap://$IP -b "DC=domain,DC=htb"

# Get naming contexts first if you don't know the domain
ldapsearch -x -H ldap://$IP -s base namingcontexts

What you want from LDAP:

  • Full user list
  • Group memberships
  • Description fields (admins love putting passwords in descriptions)
  • Service accounts
# Pull all users
ldapsearch -x -H ldap://$IP -b "DC=domain,DC=htb" '(objectClass=person)' sAMAccountName description memberOf

Gotcha: Anonymous bind doesn’t always mean you get data. Some DCs allow bind but return nothing. Try with a valid user later.


Kerberos (88)

Kerberos open = AD box. Two things to do immediately without creds.

User enumeration:

kerbrute userenum -d domain.htb --dc $IP /usr/share/seclists/Usernames/xato-net-10-million-usernames.txt

This is not noisy in the way brute forcing is. It doesn’t generate failed logon events for valid users. Great for building a user list.

AS-REP Roasting (no creds needed):

impacket-GetNPUsers domain.htb/ -dc-ip $IP -usersfile users.txt -no-pass

If any account has “Do not require Kerberos preauthentication” set, you get a hash. Crack it.


MSSQL (1433)

# Default creds
impacket-mssqlclient sa:sa@$IP
impacket-mssqlclient sa:''@$IP

# With found creds later
impacket-mssqlclient domain.htb/user:password@$IP -windows-auth

If you get in:

  • xp_cmdshell for command execution (might need to enable it)
  • xp_dirtree for forced authentication / hash capture
  • Check linked servers — EXEC sp_linkedservers
-- Enable xp_cmdshell
EXEC sp_configure 'show advanced options', 1; RECONFIGURE;
EXEC sp_configure 'xp_cmdshell', 1; RECONFIGURE;
EXEC xp_cmdshell 'whoami';

SNMP (161/UDP)

Criminally underrated. SNMP with “public” community string dumps everything.

# Check community strings
onesixtyone -c /usr/share/seclists/Discovery/SNMP/snmp.txt $IP

# Walk the MIB tree
snmpwalk -v2c -c public $IP 1.3.6.1

Useful OIDs:

  • 1.3.6.1.2.1.25.4.2.1.2 — Running processes
  • 1.3.6.1.4.1.77.1.2.25 — User accounts
  • 1.3.6.1.2.1.25.6.3.1.2 — Installed software

I use snmpwalk with specific OIDs rather than walking the entire tree. Faster, less noise.

Gotcha: If SNMPv1/v2c is running, community strings are sent in cleartext. Default is almost always public. But also try private, manager, internal.


WinRM (5985, 5986)

Can’t do much with WinRM without creds, but note that it’s open. Once you have credentials:

evil-winrm -i $IP -u 'user' -p 'password'

If port 5985 is open and you have valid creds → instant shell. This is the go-to for lateral movement on Windows.


SSH (22)

Not much to enumerate without creds. I note the version (sometimes reveals OS version) and move on.

# Banner grab
nmap -sV -p22 $IP

Come back with found credentials. Try username reuse. Try keys found in other services.

One thing: If you find a private SSH key anywhere — web directory, SMB share, FTP — try it immediately against every user you know.


The Workflow in Practice

Here’s how the first 15 minutes on a box look:

  1. Terminal 1: Fast nmap scan → start working results immediately
  2. Terminal 2: Full port scan running in background
  3. Terminal 3: If HTTP found → browser open, whatweb, feroxbuster running
  4. Note-taking: Record every finding, every username, every potential credential
Target: $IP
Hostname: ???
OS: ???
Domain: ???

Open Ports:
-

Users Found:
-

Credentials:
-

Interesting Findings:
-

I keep this running note and update it constantly. Sounds basic. It’s the thing that saves me the most time.


When to Move On vs. Dig Deeper

This is the hardest skill and I’m still learning it.

Dig deeper when:

  • You found a custom web app (there’s almost certainly a vuln in it)
  • You found usernames but no passwords (password spraying time)
  • A service returned partial information (there’s probably more)
  • You have one set of creds but can’t get a shell (reuse them everywhere)

Move on when:

  • Default page, no custom content, directory busting returns nothing after two wordlists
  • Service requires authentication and you have zero creds
  • You’ve spent 30+ minutes on one service with nothing to show

The biggest time waster: Trying to exploit something before finishing enumeration. I’ve burned hours trying to get a foothold through one service while the actual path was through a port I hadn’t scanned yet.

Enumerate everything first. Exploit second. Every time I break this rule, I regret it.


Quick Reference — Port to Action

PortServiceFirst Move
21FTPAnonymous login
22SSHNote version, move on
25SMTPUser enumeration (VRFY/EXPN)
53DNSZone transfer
80/443HTTPBrowse, tech stack, directory brute
88KerberosUser enum, AS-REP Roast
110/143POP3/IMAPTry default creds
135MSRPCrpcclient null session
139/445SMBNull session, share listing
161SNMPCommunity string brute, walk
389/636LDAPAnonymous bind, dump users
1433MSSQLDefault creds (sa:sa)
3306MySQLDefault creds (root:root)
5985WinRMNeed creds → evil-winrm
5432PostgreSQLDefault creds (postgres:postgres)
6379RedisNo auth? → RCE
8080HTTP AltSame as 80

This workflow isn’t perfect. I update it after every box that teaches me something new. The core principle doesn’t change though: scan fast, enumerate wide, take notes, don’t exploit until you’ve seen the full picture.