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:
- Port discovery — finding which of the 65,535 TCP ports are open
- Service detection (
-sV) — fingerprinting what's running on each open port - 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:
# 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 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:
- Check if rustscan is installed. If yes, use it. If not, fall back to nmap with
--min-rate 1000. - Run rustscan in greppable mode to get the port list.
- Parse the output — rustscan's
-gformat isip -> [port1,port2,...], so we extract the bracket contents and normalize. - Feed those ports to nmap for the actual service detection, version fingerprinting, and OS detection.
Here's the core logic, stripped down:
# 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_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:
# 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:
# 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.
$ 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