recon.sh
A modular automation script built in Bash designed to streamline initial reconnaissance, asset discovery, and active/passive information gathering during infrastructure penetration tests.
#!/bin/bash
# recon.sh — automated enumeration script
# Usage: ./recon.sh <IP> [domain]
# Example: ./recon.sh 10.10.10.10 target.htb
# ─── usage ──────────────────────────────────────────────────────────────────
usage() {
cat <<EOF
Usage: $0 <IP> [domain]
Arguments:
IP Target IP address (required)
domain Target hostname, e.g. target.htb (optional — enables
/etc/hosts entry, ffuf subdomain fuzzing, and vhost fuzzing)
Example:
$0 10.10.10.10 target.htb
EOF
}
if [[ "$1" == "-h" || "$1" == "--help" ]]; then
usage
exit 0
fi
IP=$1
DOMAIN=${2:-""}
OUT="recon_${IP}"
DIR_WORDLIST="/usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt"
SUB_WORDLIST="/usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt"
# ─── colours ────────────────────────────────────────────────────────────────
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
CYAN='\033[0;36m'; NC='\033[0m'
banner() { echo -e "\n${CYAN}[*] $1${NC}"; }
ok() { echo -e "${GREEN}[+] $1${NC}"; }
warn() { echo -e "${YELLOW}[!] $1${NC}"; }
err() { echo -e "${RED}[-] $1${NC}"; }
# ─── sanity checks ──────────────────────────────────────────────────────────
if [[ -z "$IP" ]]; then
err "Missing target IP"
usage
exit 1
fi
for tool in nmap gobuster ffuf; do
if ! command -v "$tool" &>/dev/null; then
warn "$tool not found — skipping its steps"
fi
done
mkdir -p "$OUT"
banner "Output dir: $OUT"
# ─── /etc/hosts entry ───────────────────────────────────────────────────────
# Add it if missing, leave it if already correctly mapped, or auto-correct
# the IP if stale (common on HTB — boxes get reset and reassigned a new IP
# under the same hostname).
if [[ -n "$DOMAIN" ]]; then
if grep -qE "^${IP}[[:space:]].*${DOMAIN}([[:space:]]|$)" /etc/hosts 2>/dev/null; then
warn "$IP -> $DOMAIN already correctly mapped in /etc/hosts"
elif grep -qE "[[:space:]]${DOMAIN}([[:space:]]|$)" /etc/hosts 2>/dev/null; then
banner "$DOMAIN exists in /etc/hosts pointing at a different (stale) IP — updating to $IP"
sudo sed -i -E "s/^[0-9.]+([[:space:]]+.*\b${DOMAIN}\b.*)$/${IP}\1/" /etc/hosts
ok "Updated $DOMAIN -> $IP"
else
banner "Adding $IP $DOMAIN to /etc/hosts"
echo "$IP $DOMAIN" | sudo tee -a /etc/hosts
ok "Added"
fi
fi
# ─── 1. nmap — two-phase scan ───────────────────────────────────────────────
# Phase 1: fast full-port sweep, no version/script detection, just to find
# which ports are open. Phase 2 then runs -sV -sC only against those ports.
# This avoids running heavyweight scripts against all 65535 ports.
banner "nmap — phase 1: fast full-port discovery"
nmap -Pn -sS --min-rate=5000 -p- --open \
-oN "${OUT}/nmap_discovery.txt" \
"$IP" | tee "${OUT}/nmap_discovery_live.txt"
OPEN_PORTS=$(grep -oP '^\d+(?=/tcp\s+open)' "${OUT}/nmap_discovery.txt" | paste -sd, -)
if [[ -z "$OPEN_PORTS" ]]; then
err "No open ports found in phase 1 — skipping phase 2 and web enumeration"
HTTP_PORTS=""
else
ok "Open ports: $OPEN_PORTS"
banner "nmap — phase 2: version/script scan on discovered ports"
nmap -Pn -sV -sC -p "$OPEN_PORTS" \
-oN "${OUT}/nmap_full.txt" \
-oX "${OUT}/nmap_full.xml" \
"$IP" | tee "${OUT}/nmap_live.txt"
ok "nmap done -> ${OUT}/nmap_full.txt"
# detect open HTTP/HTTPS ports from nmap output.
# Whitelist exact service names rather than substring-matching "http" —
# nmap labels some non-web services with "http" in the name too, e.g.
# "http-rpc-epmap" (port 593, Windows RPC-over-HTTP endpoint mapper) and
# its dynamic high-port pair. A substring match wrongly treats those as
# websites and wastes gobuster/ffuf time dialing RPC ports for content.
HTTP_PORTS=$(awk '$2=="open" {
svc=$3
if (svc=="http" || svc=="https" || svc=="ssl/http" || \
svc=="http-alt" || svc=="https-alt" || svc=="http-proxy") {
split($1,a,"/"); print a[1]
}
}' "${OUT}/nmap_live.txt")
if [[ -z "$HTTP_PORTS" ]]; then
warn "No HTTP ports detected among open ports"
fi
fi
# ─── 2. gobuster dir ────────────────────────────────────────────────────────
# detect_extensions: fires one lightweight HEAD request at the target and
# maps the Server/X-Powered-By headers to a relevant extension list, so we
# don't waste gobuster's time brute-forcing .aspx against an nginx+PHP box
# or .php against IIS.
detect_extensions() {
local target="$1"
local headers
headers=$(curl -sk -I -m 5 "$target" 2>/dev/null)
if [[ -z "$headers" ]]; then
echo "php,html,txt,bak" # no response — fall back to the common default
return
fi
if grep -qi "IIS" <<<"$headers"; then
echo "asp,aspx,config,txt"
elif grep -qi "X-Powered-By: *PHP\|PHP/" <<<"$headers"; then
echo "php,phtml,html,txt,bak"
elif grep -qi "Werkzeug\|gunicorn\|X-Powered-By: *Express" <<<"$headers"; then
echo "py,js,json,html,txt"
elif grep -qi "Tomcat\|Jetty\|JSESSIONID" <<<"$headers"; then
echo "jsp,do,action,html"
else
echo "php,html,txt,bak" # apache/nginx with no clear stack signal — safe default
fi
}
if [[ -n "$HTTP_PORTS" ]] && command -v gobuster &>/dev/null && [[ -f "$DIR_WORDLIST" ]]; then
for PORT in $HTTP_PORTS; do
SCHEME="http"; [[ "$PORT" == "443" ]] && SCHEME="https"
TARGET="${SCHEME}://${DOMAIN:-$IP}:${PORT}"
EXTRA_FLAGS=()
[[ "$SCHEME" == "https" ]] && EXTRA_FLAGS+=(-k)
EXTENSIONS="php,html,txt,bak"
if command -v curl &>/dev/null; then
EXTENSIONS=$(detect_extensions "$TARGET")
fi
ok "Detected extension set for port $PORT: $EXTENSIONS"
banner "gobuster dir -> $TARGET"
gobuster dir \
-u "$TARGET" \
-w "$DIR_WORDLIST" \
-t 50 \
-x "$EXTENSIONS" \
--no-error \
"${EXTRA_FLAGS[@]}" \
-o "${OUT}/gobuster_${PORT}.txt" \
2>&1 | tee -a "${OUT}/gobuster_${PORT}_live.txt"
ok "gobuster done -> ${OUT}/gobuster_${PORT}.txt"
done
elif [[ -z "$HTTP_PORTS" ]]; then
warn "Skipping gobuster (no HTTP ports found)"
else
warn "Skipping gobuster (binary or wordlist missing)"
fi
# ─── 3. ffuf subdomain fuzzing ──────────────────────────────────────────────
if [[ -z "$DOMAIN" ]]; then
warn "No domain supplied — skipping ffuf subdomain fuzz"
elif ! command -v ffuf &>/dev/null; then
warn "ffuf not found — skipping subdomain fuzz"
elif [[ ! -f "$SUB_WORDLIST" ]]; then
warn "Subdomain wordlist not found at $SUB_WORDLIST — skipping"
warn "Install seclists: sudo apt install seclists"
else
banner "ffuf — subdomain fuzzing on $DOMAIN"
ffuf \
-w "$SUB_WORDLIST":FUZZ \
-u "http://${DOMAIN}" \
-H "Host: FUZZ.${DOMAIN}" \
-t 100 \
-fc 301,302,400,404 \
-o "${OUT}/ffuf_subdomains.json" \
-of json \
2>&1 | tee "${OUT}/ffuf_subdomains_live.txt"
ok "ffuf done -> ${OUT}/ffuf_subdomains.json"
fi
# ─── 4. ffuf vhost fuzzing (alternative to subdomain) ──────────────────────
if [[ -n "$DOMAIN" ]] && [[ -n "$HTTP_PORTS" ]] && command -v ffuf &>/dev/null && [[ -f "$SUB_WORDLIST" ]]; then
for PORT in $HTTP_PORTS; do
SCHEME="http"; [[ "$PORT" == "443" ]] && SCHEME="https"
banner "ffuf — vhost fuzz on port $PORT"
ffuf \
-w "$SUB_WORDLIST":FUZZ \
-u "${SCHEME}://${IP}:${PORT}" \
-H "Host: FUZZ.${DOMAIN}" \
-t 100 \
-fc 302,400,404 \
-o "${OUT}/ffuf_vhost_${PORT}.json" \
-of json \
2>&1 | tee "${OUT}/ffuf_vhost_${PORT}_live.txt"
ok "vhost fuzz done -> ${OUT}/ffuf_vhost_${PORT}.json"
done
fi
# ─── summary ────────────────────────────────────────────────────────────────
echo ""
echo -e "${GREEN}══════════════════════════════════════${NC}"
echo -e "${GREEN} Recon complete — all output in: ${OUT}/${NC}"
echo -e "${GREEN}══════════════════════════════════════${NC}"
ls -lh "${OUT}/"