Back to Projects

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}/"