Here's something that took me way too long to figure out: the default OSCP enumeration workflow is broken. Not conceptually — the methodology is solid. The tooling is just painfully slow when you need it to be fast.

Everyone learning for the OSCP starts the same way. You spin up a target, type nmap -p- -sV -sC target, and wait. And wait. On a good day that's 15-20 minutes. On a machine with aggressive rate limiting or a /24 range, you're looking at an hour. During the exam, with the clock running, that wait is brutal.

So I built a script to fix it. Six versions later, the approach has changed enough that I think it's worth writing up — not just the "what" but the "why."

The Typical Approach (and Why It's Slow)

The standard nmap full-port scan does a lot of work you don't actually need at the discovery stage. When you run nmap -p- -sV -sC, it's doing three things at once:

  1. Port discovery — finding which of the 65,535 TCP ports are open
  2. Service detection (-sV) — fingerprinting what's running on each open port
  3. Script scanning (-sC) — running default NSE scripts against detected services

The problem is that steps 2 and 3 are running against all 65,535 ports simultaneously. Nmap has to send probes, wait for responses, handle retransmissions, and do version detection on every single port — even the 65,500+ that are closed. The SYN scan itself is actually pretty fast. It's the combined overhead that kills you.

The key insight: Separate port discovery from service enumeration. Find which ports are open first (fast), then throw the heavy stuff at only those ports (targeted).

This isn't a new idea. Plenty of people do this manually with a two-phase nmap approach:

two-phase nmap
# Phase 1: Fast port discovery (SYN scan, no scripts)
nmap -p- -sS -T4 --min-rate 1000 --open target -oN ports.txt

# Phase 2: Targeted deep scan on found ports
nmap -sV -sC -O -p 22,80,443,8080 target -oN services.txt

This is already way better. Phase 1 finishes in a couple minutes because it's just doing raw SYN probes. Phase 2 is fast because it's only hitting 4 ports instead of 65,535. But we can do better.

Enter Rustscan

Rustscan is a port scanner written in Rust that does exactly one thing: find open ports, stupidly fast. It uses async I/O to blast out connection attempts and can scan all 65,535 ports in under 10 seconds on a good connection.

The idea isn't to replace nmap — nmap's service detection and NSE scripts are irreplaceable. The idea is to use rustscan as a front-end for port discovery, then hand the results to nmap for the deep work.

rustscan + nmap pipeline
# Rustscan finds ports, pipes to nmap for service detection
rustscan -a 10.10.10.50 --ulimit 5000 -- -sC -sV

# Or grab just the ports (greppable output)
rustscan -a 10.10.10.50 --ulimit 5000 -g
10.10.10.50 -> [22,80,139,445,3306]

That -g flag is the useful one. It gives you a clean list of open ports that you can parse and feed into whatever comes next. The entire scan takes seconds instead of minutes.

How the Script Handles It

In version 6.0 of the enumeration script, Phase 2 (port scanning) works like this:

  1. Check if rustscan is installed. If yes, use it. If not, fall back to nmap with --min-rate 1000.
  2. Run rustscan in greppable mode to get the port list.
  3. Parse the output — rustscan's -g format is ip -> [port1,port2,...], so we extract the bracket contents and normalize.
  4. Feed those ports to nmap for the actual service detection, version fingerprinting, and OS detection.

Here's the core logic, stripped down:

oscp-network-enum-v2.sh — Phase 2
# Try rustscan first (speed-first approach)
if [ "$HAS_RUSTSCAN" = "true" ]; then
    RUSTSCAN_OUTPUT=$(rustscan -a "$host" --ulimit 5000 -g 2>/dev/null)

    if [ -n "$RUSTSCAN_OUTPUT" ]; then
        # Parse: "ip -> [port1,port2,port3]"
        RUSTSCAN_PORTS=$(echo "$RUSTSCAN_OUTPUT" | \
            grep -oE '\[.*\]' | tr -d '[]' | \
            tr ',' '\n' | sort -nu)
        TCP_PORTS=$(echo "$RUSTSCAN_PORTS" | \
            tr '\n' ',' | sed 's/,$//')
    else
        # Fallback to nmap if rustscan returns nothing
        _nmap_tcp_scan "$host"
    fi
else
    # No rustscan available, nmap fallback
    _nmap_tcp_scan "$host"
fi

# Now hit only the discovered ports with the heavy stuff
nmap -sV -sC -O --osscan-guess -p "$TCP_PORTS" \
    --version-intensity 7 "$host"

A few things worth noting:

  • --ulimit 5000 — Rustscan's file descriptor limit. Higher = faster but more aggressive. 5000 is a good balance for exam-like environments where you don't want to DoS the target.
  • The fallback matters. Not every exam environment will have rustscan installed (it's not in the default Kali repos). The script detects this at startup and silently falls back to nmap's -p- -sS -T4 --min-rate 1000.
  • --version-intensity 7 — Default is 7, but being explicit about it means the targeted nmap scan does thorough version detection. This is where you want to spend time, not on port discovery.

The Nmap Fallback

When rustscan isn't available, the fallback is still the two-phase approach, just slower:

nmap fallback
_nmap_tcp_scan() {
    if [ "$QUICK_SCAN" = "true" ]; then
        # Quick mode: only top 1000 ports
        nmap -sS -T4 --top-ports 1000 --open "$host"
    else
        # Full mode: all 65535 ports
        nmap -p- -sS -T4 --min-rate 1000 --open "$host"
    fi

    # Parse open ports from nmap output
    grep "^[0-9]*/tcp.*open" tcp_scan.txt | \
        awk '{print $1}' | cut -d'/' -f1 | \
        sort -nu
}

The --quick flag is there for when you just need the common ports fast — good for initial triage on a /24 range. The full mode does the same -p- scan but with --min-rate 1000 to keep things moving.

Phase 1: Host Discovery

Before port scanning, the script does a quick host discovery pass. This is pretty standard stuff:

Phase 1 — host discovery
# For CIDR ranges: nmap ping sweep
nmap -sn -T4 192.168.1.0/24

# For single hosts: quick ICMP check
if ping -c 2 -W 1 $TARGET; then
    # Host is up, proceed normally
    USE_PN=""
else
    # Host doesn't respond to ping
    # Could be a firewall, use -Pn
    USE_PN="-Pn"
fi

The important bit here is the -Pn fallback. A lot of OSCP machines block ICMP, so if ping fails, the script doesn't bail — it sets a flag to tell nmap to skip host discovery and scan anyway. Simple, but easy to forget when you're doing this manually at 3 AM during the exam.

Real Numbers

Here's what this actually looks like in practice on a typical HTB/OSCP-style machine:

timing comparison
# The old way:
$ time nmap -p- -sV -sC 10.10.10.50
Nmap done: 1 IP address (1 host up) scanned in 847.23 seconds
# ~14 minutes

# The new way:
$ time rustscan -a 10.10.10.50 --ulimit 5000 -g
10.10.10.50 -> [22,80,139,445,3306]
# ~3 seconds

$ time nmap -sV -sC -O -p 22,80,139,445,3306 10.10.10.50
Nmap done: 1 IP address (1 host up) scanned in 28.41 seconds
# ~30 seconds

# Total: ~33 seconds vs ~14 minutes
# That's a 25x speedup.

On the exam, where you've got 5-6 machines and 24 hours, saving 13 minutes per box adds up to over an hour of extra time. That's the difference between finishing your report at midnight vs 1 AM. Or more realistically, the difference between having time to try that one weird exploit path vs. running out of time.

Checklist Mode

Version 6.0 also added something I've found really useful during the exam: checklist mode. Instead of running enumeration automatically, it takes the discovered ports and prints out the manual commands you'd want to run for each service.

checklist mode
$ sudo ./oscp-network-enum-v2.sh 10.10.10.50 -c

========================================
 Discovered Ports: 22,80,139,445,3306
 Target: 10.10.10.50
========================================

[Port 22 - SSH]
  # Banner grab
  nc -nv 10.10.10.50 22
  # Auth with key
  chmod 600 id_rsa && ssh -i id_rsa user@10.10.10.50
  # Brute force
  hydra -l user -P /usr/share/wordlists/rockyou.txt ssh://10.10.10.50

[Port 80 - HTTP/S]
  # Technology detection
  whatweb -a 3 http://10.10.10.50:80
  # Directory brute
  gobuster dir -u http://10.10.10.50:80 -w /usr/share/wordlists/dirb/common.txt -x php,txt,html,bak -t 50
  feroxbuster -u http://10.10.10.50:80 -w raft-medium-directories.txt -x php,txt,html
  # Vhost enum
  gobuster vhost -u http://10.10.10.50:80 -w subdomains-top1million-5000.txt --append-domain

[Port 139/445 - SMB]
  # Anonymous access
  smbclient -L //10.10.10.50 -N
  smbmap -H 10.10.10.50
  # Enum4linux
  enum4linux-ng -A 10.10.10.50
  # RID brute (user enum)
  nxc smb 10.10.10.50 -u '' -p '' --rid-brute 10000

The point isn't automation for automation's sake. It's about not having to remember the exact syntax for enum4linux-ng or the right wordlist path for gobuster when you're six hours into the exam and running on caffeine. The script discovers ports, then hands you a cheat sheet customized to exactly what's running.

A note on exam rules: Make sure any scripts or tools you bring into the OSCP exam are permitted. As of this writing, rustscan and custom bash scripts are allowed. Automated exploitation tools (like sqlmap outside of the allowed use case) are not. Always check the latest exam guide.

What's Next

This post covered the "front door" — getting from zero to a list of open ports and services as fast as possible. In Part 2, I'll break down what the script does once it knows what's running: service-specific deep enumeration for SMB, HTTP, LDAP, Kerberos, and every other protocol you'll see on exam day.

The full script is open source: github.com/mouteee/oscp-enumeration

OSCP Enumeration Series

Part 1 The Speed Problem READING
Part 2 Deep Enumeration — What to Hit When Ports Talk Back SOON
Part 3 Post-Exploitation — The Part Nobody Preps For SOON