bashRecon & Automation#fuzzing
1002 linhas•04_dirs.sh
04_dirs.sh — Advanced Web & Parameter Fuzzing
Módulo de fuzzing de alta precisão com calibração anti-falso positivo, Host header fuzzing (Vhosts), brute-force de extensões e teste automatizado de parâmetros para injeções.
Destaques Operacionais
- Calibração automática inteligente: detecta respostas 404 customizadas e filtra tamanhos de página dinâmicos
- Fuzzing de Virtual Hosts (Vhosts) via cabeçalho Host
- Parameter discovery com injeção automática de testes para LFI, XSS, SQLi, SSTI, CMDI e SSRF
- Higienização de sequências de escape ANSI em todos os relatórios gerados
Como Executar
# Execução standalone com menu interativo de wordlists: ./04_dirs.sh 10.10.10.1 ./output/10.10.10.1
* Ajuste as permissões de execução com chmod +x 04_dirs.sh se necessário.
Código Fonte — 04_dirs.sh
bash#!/bin/bash
# ═══════════════════════════════════════════════════════
# MÓDULO 04 — Fuzzing (Dirs, Vhosts, Subdomínios, Extensões)
# Uso: ./04_dirs.sh <alvo> <output_dir> [auto]
# Se passar "auto" como 3º arg, roda tudo sem menu (FULL RECON)
# ═══════════════════════════════════════════════════════
set -uo pipefail
RED='\033[0;31m'; GRN='\033[0;32m'; YLW='\033[1;33m'
BLU='\033[0;34m'; CYN='\033[0;36m'; MAG='\033[0;35m'
RST='\033[0m'; BOLD='\033[1m'
info() { echo -e "${BLU}[*]${RST} $1"; }
ok() { echo -e "${GRN}[+]${RST} $1"; }
warn() { echo -e "${YLW}[!]${RST} $1"; }
fail() { echo -e "${RED}[-]${RST} $1"; }
has() { command -v "$1" &>/dev/null; }
is_ip() { [[ "$1" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; }
# Limpar códigos ANSI de arquivo
strip_ansi() { sed -i 's/\x1b\[[0-9;]*[a-zA-Z]//g' "$1" 2>/dev/null; }
TARGET="${1:?Uso: $0 <alvo> <output_dir> [auto]}"
OUTDIR="${2:?Uso: $0 <alvo> <output_dir>}/dirs"
AUTO_MODE="${3:-}"
mkdir -p "$OUTDIR"
CLEAN=$(echo "$TARGET" | sed 's|https\?://||;s|/.*||;s|:.*||')
# Detectar URL
if curl -sk --connect-timeout 3 "https://${CLEAN}" -o /dev/null 2>/dev/null; then
BASE_URL="https://${CLEAN}"
else
BASE_URL="http://${CLEAN}"
fi
# ── Wordlists disponíveis ──
declare -A WL_MAP
wl_idx=0
add_wl() {
local label="$1" path="$2" type="$3"
if [[ -f "$path" ]]; then
wl_idx=$((wl_idx + 1))
WL_MAP["${wl_idx}_path"]="$path"
WL_MAP["${wl_idx}_label"]="$label"
WL_MAP["${wl_idx}_type"]="$type"
WL_MAP["${wl_idx}_size"]="$(wc -l < "$path" 2>/dev/null)"
fi
}
# Custom do user (legado — caminhos preservados; ignorados se não existirem)
add_wl "raft-large-dirs" "$HOME/Aulas/txts_uteis/raft-large-directories-lowercase.txt" "dirs"
add_wl "big" "$HOME/Aulas/txts_uteis/big.txt" "dirs"
add_wl "common" "$HOME/Aulas/txts_uteis/common.txt" "dirs"
add_wl "raft-large-files" "$HOME/Aulas/txts_uteis/raft-large-files-lowercase.txt" "files"
add_wl "raft-small-extensions" "$HOME/Aulas/txts_uteis/raft-small-extensions.txt" "ext"
add_wl "subdomains-5k" "$HOME/Aulas/txts_uteis/subdomains-top1million-5000.txt" "subs"
# Kali defaults (legado — ignorados se não existirem)
add_wl "dirbuster-medium" "/usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt" "dirs"
add_wl "dirb-common" "/usr/share/wordlists/dirb/common.txt" "dirs"
# Arch Linux / seclists — caminhos verificados em /usr/share/seclists (instalado via pacman)
add_wl "dirbuster-medium-sl" "/usr/share/seclists/Discovery/Web-Content/DirBuster-2007_directory-list-2.3-medium.txt" "dirs"
add_wl "dirbuster-medium-lc" "/usr/share/seclists/Discovery/Web-Content/DirBuster-2007_directory-list-lowercase-2.3-medium.txt" "dirs"
add_wl "raft-large-dirs-sl" "/usr/share/seclists/Discovery/Web-Content/raft-large-directories-lowercase.txt" "dirs"
add_wl "raft-medium-dirs-sl" "/usr/share/seclists/Discovery/Web-Content/raft-medium-directories-lowercase.txt" "dirs"
add_wl "raft-medium-dirs" "/usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt" "dirs"
add_wl "raft-small-dirs-sl" "/usr/share/seclists/Discovery/Web-Content/raft-small-directories-lowercase.txt" "dirs"
add_wl "big-sl" "/usr/share/seclists/Discovery/Web-Content/big.txt" "dirs"
add_wl "common-sl" "/usr/share/seclists/Discovery/Web-Content/common.txt" "dirs"
add_wl "raft-large-files-sl" "/usr/share/seclists/Discovery/Web-Content/raft-large-files-lowercase.txt" "files"
add_wl "raft-medium-files-sl" "/usr/share/seclists/Discovery/Web-Content/raft-medium-files-lowercase.txt" "files"
add_wl "raft-small-ext-sl" "/usr/share/seclists/Discovery/Web-Content/raft-small-extensions.txt" "ext"
add_wl "raft-large-ext-sl" "/usr/share/seclists/Discovery/Web-Content/raft-large-extensions.txt" "ext"
add_wl "subs-5k-sl" "/usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt" "subs"
add_wl "subs-20k-sl" "/usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt" "subs"
add_wl "burp-params-sl" "/usr/share/seclists/Discovery/Web-Content/burp-parameter-names.txt" "params"
add_wl "top-app-params-sl" "/usr/share/seclists/Discovery/Web-Content/url-params_from-top-55-most-popular-apps.txt" "params"
WL_TOTAL=$wl_idx
# ═══════════════════════════════════════════
# CALIBRAÇÃO
# ═══════════════════════════════════════════
calibrate() {
info "Calibrando..."
CALIBRATION_SIZES=()
for i in 1 2 3; do
rand_path=$(tr -dc 'a-z0-9' < /dev/urandom | head -c 16)
resp=$(curl -sk --connect-timeout 5 -w '\n__HTTP_CODE__%{http_code}' "${BASE_URL}/${rand_path}" 2>/dev/null)
code=$(echo "$resp" | grep '__HTTP_CODE__' | sed 's/__HTTP_CODE__//')
resp=$(echo "$resp" | grep -v '__HTTP_CODE__')
sz=${#resp}
wc_w=$(echo "$resp" | wc -w | tr -d ' ')
CALIBRATION_SIZES+=("$sz")
echo -e " teste ${i}: code=${code} size=${sz} words=${wc_w}"
done
FILTER_ARGS=""
if [[ "${CALIBRATION_SIZES[0]}" == "${CALIBRATION_SIZES[1]}" && "${CALIBRATION_SIZES[1]}" == "${CALIBRATION_SIZES[2]}" ]]; then
FILTER_ARGS="-fs ${CALIBRATION_SIZES[0]}"
ok "404 customizado (size=${CALIBRATION_SIZES[0]}). Filtrando."
else
FILTER_ARGS="-ac"
ok "Usando auto-calibrate."
fi
}
# ═══════════════════════════════════════════
# CRAWLER — katana + gau + fallback curl
# ═══════════════════════════════════════════
# Detectar binários (compatível com sudo)
_real_home="${REAL_HOME:-$HOME}"
_find_bin() {
command -v "$1" 2>/dev/null && return
[[ -x "${_real_home}/go/bin/$1" ]] && echo "${_real_home}/go/bin/$1" && return
[[ -x "${_real_home}/.local/bin/$1" ]] && echo "${_real_home}/.local/bin/$1" && return
echo ""
}
scan_crawl() {
local max_depth="${1:-3}"
echo -e "\n${BOLD}══ CRAWLER ══${RST}"
info "Alvo: ${BASE_URL}"
local urls_file="${OUTDIR}/crawl_urls.txt"
local params_file="${OUTDIR}/crawl_params.txt"
local endpoints_file="${OUTDIR}/crawl_endpoints.txt"
local js_file="${OUTDIR}/crawl_js.txt"
> "$urls_file"
> "$params_file"
> "$endpoints_file"
> "$js_file"
local katana_bin=$(_find_bin katana)
local gau_bin=$(_find_bin gau)
local used_tools=""
# ── KATANA — Crawler ativo ──
if [[ -n "$katana_bin" ]]; then
info "katana — crawling ativo (profundidade: ${max_depth}, 20 threads)..."
used_tools="katana"
$katana_bin -u "$BASE_URL" \
-d "$max_depth" \
-jc \
-kf all \
-ef css,png,jpg,jpeg,gif,svg,ico,woff,woff2,ttf,eot,mp4,mp3 \
-c 20 \
-timeout 10 \
-silent \
-no-color \
-o "${OUTDIR}/katana_raw.txt" \
2>/dev/null || true
if [[ -s "${OUTDIR}/katana_raw.txt" ]]; then
local kt_count=$(wc -l < "${OUTDIR}/katana_raw.txt")
ok "katana: ${kt_count} URLs"
cat "${OUTDIR}/katana_raw.txt" >> "$urls_file"
else
warn "katana não retornou resultados."
fi
fi
# ── GAU — URLs históricas ──
if [[ -n "$gau_bin" ]]; then
info "gau — buscando URLs históricas (Wayback, CommonCrawl)..."
used_tools="${used_tools:+$used_tools + }gau"
timeout 90 $gau_bin --threads 5 "$CLEAN" 2>/dev/null \
| grep -viE '\.(css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot|mp4|mp3|pdf|zip|tar|gz)(\?|$)' \
| sort -u > "${OUTDIR}/gau_crawl.txt" || true
if [[ -s "${OUTDIR}/gau_crawl.txt" ]]; then
local gau_count=$(wc -l < "${OUTDIR}/gau_crawl.txt")
ok "gau: ${gau_count} URLs históricas"
cat "${OUTDIR}/gau_crawl.txt" >> "$urls_file"
else
warn "gau não retornou resultados."
fi
fi
# ── FALLBACK: curl crawl ──
if [[ -z "$katana_bin" && -z "$gau_bin" ]]; then
warn "Nem katana nem gau disponíveis. Usando crawler básico (curl)..."
used_tools="curl"
local visited="${OUTDIR}/.crawl_visited"
local queue="${OUTDIR}/.crawl_queue"
> "$visited"
echo "${BASE_URL}/" > "$queue"
local d=0
while [[ $d -lt $max_depth ]] && [[ -s "$queue" ]]; do
d=$((d + 1))
info "Profundidade ${d}/${max_depth}..."
local next_q="${OUTDIR}/.crawl_next"
> "$next_q"
while IFS= read -r url; do
grep -qxF "$url" "$visited" 2>/dev/null && continue
echo "$url" >> "$visited"
body=$(curl -sk --connect-timeout 5 --max-time 10 -L "$url" 2>/dev/null) || continue
echo "$body" | grep -oP '(href|src|action)\s*=\s*["'"'"']\K[^"'"'"'#]+' 2>/dev/null | while IFS= read -r link; do
case "$link" in
http://*|https://*) echo "$link" | grep -qi "$CLEAN" && echo "$link" >> "$urls_file" ;;
/*) echo "${BASE_URL}${link}" >> "$urls_file" ;;
esac
done
done < "$queue"
sort -u "$next_q" -o "$queue" 2>/dev/null
done
rm -f "$visited" "$queue" "${OUTDIR}/.crawl_next"
fi
# ── PÓS-PROCESSAMENTO unificado ──
local raw_count=$(wc -l < "$urls_file" 2>/dev/null || echo 0)
# Deduplicar
sort -u "$urls_file" -o "$urls_file" 2>/dev/null
# Filtrar assets que passaram
grep -viE '\.(css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot)(\?|$)' \
"$urls_file" > "${urls_file}.tmp" 2>/dev/null
mv "${urls_file}.tmp" "$urls_file" 2>/dev/null
# uro — remover URLs redundantes (mesma page, params diferentes)
local uro_bin=$(_find_bin uro)
if [[ -n "$uro_bin" ]]; then
local before_uro=$(wc -l < "$urls_file" 2>/dev/null || echo 0)
$uro_bin < "$urls_file" > "${urls_file}.uro" 2>/dev/null
mv "${urls_file}.uro" "$urls_file" 2>/dev/null
local after_uro=$(wc -l < "$urls_file" 2>/dev/null || echo 0)
local removed=$((before_uro - after_uro))
[[ $removed -gt 0 ]] && ok "uro: removeu ${removed} URLs redundantes (${before_uro} → ${after_uro})"
used_tools="${used_tools:+$used_tools + }uro"
fi
# Extrair URLs com parâmetros
grep '?' "$urls_file" 2>/dev/null | sort -u > "$params_file"
# Extrair nomes de parâmetros únicos
grep -oP '[?&]\K[^=]+' "$params_file" 2>/dev/null | sort -u > "$endpoints_file"
# Extrair JS files
grep -iE '\.js(\?|$)' "$urls_file" 2>/dev/null | sort -u > "$js_file"
local url_count=$(wc -l < "$urls_file" 2>/dev/null || echo 0)
local param_count=$(wc -l < "$params_file" 2>/dev/null || echo 0)
local endpoint_count=$(wc -l < "$endpoints_file" 2>/dev/null || echo 0)
local js_count=$(wc -l < "$js_file" 2>/dev/null || echo 0)
echo ""
echo -e " ${BOLD}Ferramentas: ${used_tools}${RST}"
echo -e " ${GRN}URLs totais:${RST} ${url_count}"
echo -e " ${RED}Com parâmetros:${RST} ${param_count}"
echo -e " ${CYN}Parâmetros únicos:${RST} ${endpoint_count}"
echo -e " ${YLW}Arquivos JS:${RST} ${js_count}"
if [[ $url_count -gt 0 ]]; then
echo -e "\n ${BOLD}Top URLs:${RST}"
head -15 "$urls_file" | while IFS= read -r u; do
echo -e " ${GRN}→${RST} $u"
done
[[ $url_count -gt 15 ]] && echo -e " ${YLW}... +$((url_count - 15)) mais${RST}"
fi
if [[ $param_count -gt 0 ]]; then
echo -e "\n ${BOLD}URLs com parâmetros (prontas pra fuzzing):${RST}"
head -10 "$params_file" | while IFS= read -r u; do
echo -e " ${RED}⚡${RST} $u"
done
fi
ok "→ crawl_urls.txt, crawl_params.txt, crawl_endpoints.txt, crawl_js.txt"
ok "Concluído."
}
# ═══════════════════════════════════════════
# FUNÇÕES DE SCAN
# ═══════════════════════════════════════════
scan_dirs() {
local wl="$1"
echo -e "\n${BOLD}══ DIRETÓRIOS ══${RST}"
info "Wordlist: $(basename "$wl") ($(wc -l < "$wl") entradas)"
if has ffuf; then
# shellcheck disable=SC2086
ffuf -u "${BASE_URL}/FUZZ" -w "$wl" \
-mc 200,204,301,302,307,401,405 -fc 404 \
-ac ${FILTER_ARGS} -t 50 -c \
-o "${OUTDIR}/ffuf_dirs.json" -of json \
| tee "${OUTDIR}/ffuf_dirs.txt" 2>/dev/null
elif has gobuster; then
gobuster dir -u "$BASE_URL" -w "$wl" \
-s "200,204,301,302,307,401,405" -b "404,403" \
-t 50 -o "${OUTDIR}/gobuster_dirs.txt" \
--no-error 2>/dev/null || true
fi
ok "Concluído."
}
scan_vhosts() {
local wl="$1"
if is_ip "$CLEAN"; then warn "Alvo é IP, vhosts não se aplica."; return; fi
echo -e "\n${BOLD}══ VHOSTS ══${RST}"
info "Wordlist: $(basename "$wl") ($(wc -l < "$wl") entradas)"
if has ffuf; then
rand_vhost=$(tr -dc 'a-z' < /dev/urandom | head -c 12)
vhost_size=$(curl -sk -o /dev/null -w "%{size_download}" -H "Host: ${rand_vhost}.${CLEAN}" "$BASE_URL" 2>/dev/null)
info "Filtro: size=${vhost_size}"
ffuf -u "$BASE_URL" -H "Host: FUZZ.${CLEAN}" -w "$wl" \
-mc 200,301,302,307 -fs "$vhost_size" \
-ac -t 50 -c \
-o "${OUTDIR}/ffuf_vhosts.json" \
-of json 2>/dev/null | tee "${OUTDIR}/ffuf_vhosts.txt" 2>/dev/null || true
else
warn "ffuf necessário para vhosts."
fi
ok "Concluído."
}
scan_subdomains() {
local wl="$1"
if is_ip "$CLEAN"; then warn "Alvo é IP, subdomínios não se aplica."; return; fi
echo -e "\n${BOLD}══ SUBDOMÍNIOS DNS ══${RST}"
info "Wordlist: $(basename "$wl") ($(wc -l < "$wl") entradas)"
# Wildcard check
rand_sub=$(tr -dc 'a-z' < /dev/urandom | head -c 14)
wildcard_ip=$(dig +short "${rand_sub}.${CLEAN}" A 2>/dev/null | head -1)
[[ -n "$wildcard_ip" ]] && warn "Wildcard: *.${CLEAN} → ${wildcard_ip}"
if has gobuster; then
local wild_arg=""
[[ -n "$wildcard_ip" ]] && wild_arg="--wildcard"
# shellcheck disable=SC2086
timeout 120 gobuster dns -d "$CLEAN" -w "$wl" \
-t 50 ${wild_arg} \
-o "${OUTDIR}/dns_brute.txt" \
--no-error 2>/dev/null || true
else
info "dig fallback (max 500)..."
> "${OUTDIR}/dns_brute.txt"
local c=0
while IFS= read -r sub && [[ $c -lt 500 ]]; do
[[ -z "$sub" || "$sub" == \#* ]] && continue
ip=$(dig +short +time=1 +tries=1 "${sub}.${CLEAN}" A 2>/dev/null | head -1)
if [[ -n "$ip" && "$ip" != "$wildcard_ip" ]]; then
echo "${sub}.${CLEAN} → ${ip}" >> "${OUTDIR}/dns_brute.txt"
echo -e " ${GRN}✔${RST} ${sub}.${CLEAN} → ${ip}"
fi
c=$((c + 1))
done < "$wl"
fi
if [[ -s "${OUTDIR}/dns_brute.txt" ]]; then
found=$(wc -l < "${OUTDIR}/dns_brute.txt")
echo -e " ${GRN}Encontrados: ${found}${RST}"
head -15 "${OUTDIR}/dns_brute.txt" | while read -r l; do echo -e " ${GRN}→${RST} $l"; done
fi
ok "Concluído."
}
scan_extensions() {
local wl="$1"
local exts="${2:-.php,.html,.txt,.bak,.old,.conf,.xml,.json,.sql,.zip,.tar.gz,.log}"
echo -e "\n${BOLD}══ ARQUIVOS COM EXTENSÕES ══${RST}"
info "Wordlist: $(basename "$wl")"
info "Extensões: ${exts:0:60}..."
if has ffuf; then
# shellcheck disable=SC2086
ffuf -u "${BASE_URL}/FUZZ" -w "$wl" \
-e "$exts" \
-mc 200,204,301,302,307,401 -fc 404 \
-ac ${FILTER_ARGS} -t 50 -c \
-o "${OUTDIR}/ffuf_files.json" -of json \
| tee "${OUTDIR}/ffuf_files.txt" 2>/dev/null
elif has gobuster; then
gobuster dir -u "$BASE_URL" -w "$wl" \
-x "${exts//./}" \
-s "200,204,301,302,307,401" -b "404,403" \
-t 50 -o "${OUTDIR}/gobuster_files.txt" \
--no-error 2>/dev/null || true
fi
ok "Concluído."
}
scan_files_only() {
local wl="$1"
echo -e "\n${BOLD}══ WORDLIST DE ARQUIVOS ══${RST}"
info "Wordlist: $(basename "$wl") ($(wc -l < "$wl") entradas)"
if has ffuf; then
# shellcheck disable=SC2086
ffuf -u "${BASE_URL}/FUZZ" -w "$wl" \
-mc 200,204,301,302,307,401 -fc 404 \
-ac ${FILTER_ARGS} -t 50 -c \
-o "${OUTDIR}/ffuf_files_raft.json" -of json \
| tee "${OUTDIR}/ffuf_files_raft.txt" 2>/dev/null
fi
ok "Concluído."
}
scan_params() {
local target_url="$1"
local wl="$2"
if ! has ffuf; then
fail "ffuf necessário para parameter fuzzing."
return 1
fi
local threads="${THREADS_FFUF:-50}"
echo -e "\n${BOLD}══ PARAMETER FUZZING ══${RST}"
info "URL: ${target_url}"
info "Wordlist: $(basename "$wl") ($(wc -l < "$wl") entradas)"
info "Threads: ${threads}"
# ── FASE A: Descobrir parâmetros GET ──
info "Fase A — Descobrindo parâmetros GET ocultos..."
baseline_size=$(curl -sk -o /dev/null -w "%{size_download}" --connect-timeout "${TIMEOUT_CURL:-5}" "$target_url" 2>/dev/null)
info "Baseline size: ${baseline_size} bytes"
# Rate limiting adaptativo
local ffuf_rate_args=""
if [[ "${RATE_LIMIT:-0}" -gt 0 ]]; then
ffuf_rate_args="-rate ${RATE_LIMIT}"
fi
ffuf -u "${target_url}?FUZZ=test123" \
-w "$wl" \
-mc all \
-fs "$baseline_size" \
-ac \
-t "$threads" -c $ffuf_rate_args \
-timeout "${TIMEOUT_FFUF:-10}" \
-o "${OUTDIR}/ffuf_params_discovery.json" \
-of json 2>/dev/null | tee "${OUTDIR}/ffuf_params_discovery.txt" 2>/dev/null || true
# Verificar se WAF bloqueou (muitos 429/403)
if [[ "${RATE_LIMIT_ADAPTIVE:-true}" == "true" && -s "${OUTDIR}/ffuf_params_discovery.txt" ]]; then
local waf_hits
waf_hits=$(grep -c '\[429\]\|\[403\]' "${OUTDIR}/ffuf_params_discovery.txt" 2>/dev/null || echo 0)
if [[ $waf_hits -gt 10 ]]; then
local new_threads="${RATE_LIMIT_MIN_THREADS:-5}"
warn "WAF detectado (${waf_hits}x 429/403) — reduzindo threads: ${threads} → ${new_threads}"
threads=$new_threads
# Re-rodar com menos threads
ffuf -u "${target_url}?FUZZ=test123" \
-w "$wl" -mc all -fs "$baseline_size" -ac \
-t "$new_threads" -c -rate 10 \
-timeout "${TIMEOUT_FFUF:-10}" \
-o "${OUTDIR}/ffuf_params_discovery.json" \
-of json 2>/dev/null | tee "${OUTDIR}/ffuf_params_discovery.txt" 2>/dev/null || true
ok "Re-scan com ${new_threads} threads concluído."
fi
fi
# Extrair parâmetros encontrados
local params_found=()
local params_file="${OUTDIR}/params_found_raw.txt"
> "$params_file"
# Texto do ffuf (mais confiável)
if [[ -s "${OUTDIR}/ffuf_params_discovery.txt" ]]; then
sed 's/\x1b\[[0-9;]*[a-zA-Z]//g' "${OUTDIR}/ffuf_params_discovery.txt" 2>/dev/null \
| grep '\[Status:' | awk '{print $1}' | sort -u >> "$params_file"
fi
# JSON fallback
if [[ -s "${OUTDIR}/ffuf_params_discovery.json" ]]; then
grep -oP '"FUZZ"\s*:\s*"\K[^"]+' "${OUTDIR}/ffuf_params_discovery.json" 2>/dev/null \
| sort -u >> "$params_file"
fi
sort -u "$params_file" -o "$params_file"
while IFS= read -r p; do
[[ -n "$p" && "$p" != "FUZZ" ]] && params_found+=("$p")
done < "$params_file"
if [[ ${#params_found[@]} -eq 0 ]]; then
warn "Nenhum parâmetro oculto encontrado na fase A."
echo -e " ${BLU}Dica: tente em páginas específicas (.php, /api/, /search)${RST}"
ok "Concluído."
return 0
fi
ok "Parâmetros encontrados: ${#params_found[@]}"
printf '%s\n' "${params_found[@]}" > "${OUTDIR}/params_found.txt"
for p in "${params_found[@]}"; do
echo -e " ${GRN}→${RST} ${p}"
done
# ── FASE B: Testar injeções GET ──
echo ""
info "Fase B — Testando injeções GET nos ${#params_found[@]} parâmetros..."
declare -A PAYLOADS
PAYLOADS[LFI]="../../../../etc/passwd"
PAYLOADS[LFI2]="....//....//....//etc/passwd"
PAYLOADS[XSS]="<script>alert(1)</script>"
PAYLOADS[XSS2]="'\"><img src=x onerror=alert(1)>"
PAYLOADS[SQLi]="' OR '1'='1"
PAYLOADS[SQLi2]="1' AND SLEEP(2)-- -"
PAYLOADS[SQLi3]="\" OR \"\"=\""
PAYLOADS[SSRF]="http://127.0.0.1:80"
PAYLOADS[SSRF2]="http://169.254.169.254/latest/meta-data/"
PAYLOADS[SSTI]="{{7*7}}"
PAYLOADS[CMDI]=";id"
PAYLOADS[IDOR]="1"
PAYLOADS[REDIRECT]="https://evil.com"
> "${OUTDIR}/params_vulns.txt"
_test_payload() {
local param="$1" vuln_type="$2" payload="$3" method="$4" resp="$5"
local is_vuln=false vuln_evidence=""
case "$vuln_type" in
LFI|LFI2)
echo "$resp" | grep -q "root:x:0:0\|root:.*:/bin/" && { is_vuln=true; vuln_evidence="/etc/passwd no response!"; } ;;
XSS|XSS2)
echo "$resp" | grep -q "<script>alert(1)</script>\|onerror=alert" && { is_vuln=true; vuln_evidence="payload refletido sem sanitização"; } ;;
SQLi|SQLi2|SQLi3)
if echo "$resp" | grep -qi "sql syntax\|mysql\|sqlite\|postgresql\|ORA-\|unterminated\|ODBC"; then
is_vuln=true; vuln_evidence="erro SQL no response"
fi ;;
SSTI)
echo "$resp" | grep -q "49" && { is_vuln=true; vuln_evidence="{{7*7}}=49 refletido!"; } ;;
CMDI)
echo "$resp" | grep -q "uid=\|gid=" && { is_vuln=true; vuln_evidence="output de comando detectado!"; } ;;
SSRF|SSRF2)
;; # Precisa de out-of-band
esac
if $is_vuln; then
echo -e " ${RED}🔴 ${method} ${vuln_type}: POSSÍVEL VULN! ${vuln_evidence}${RST}"
echo "[VULN] method=${method} param=${param} type=${vuln_type} evidence=${vuln_evidence}" >> "${OUTDIR}/params_vulns.txt"
fi
}
for param in "${params_found[@]}"; do
echo -e "\n ${CYN}── ${param} ──${RST}"
normal_resp=$(curl -sk --connect-timeout "${TIMEOUT_CURL:-5}" "${target_url}?${param}=normaltest123" 2>/dev/null)
normal_size=${#normal_resp}
for vuln_type in "${!PAYLOADS[@]}"; do
payload="${PAYLOADS[$vuln_type]}"
encoded=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${payload}'))" 2>/dev/null || echo "$payload")
test_resp=$(curl -sk --connect-timeout "${TIMEOUT_CURL:-5}" "${target_url}?${param}=${encoded}" 2>/dev/null)
test_size=${#test_resp}
_test_payload "$param" "$vuln_type" "$payload" "GET" "$test_resp"
# Size diff check (genérico)
if [[ $((test_size - normal_size)) -gt 500 || $((test_size - normal_size)) -lt -500 ]]; then
echo -e " ${YLW}🟡 GET ${vuln_type}: size diff (${normal_size}→${test_size})${RST}"
echo "[INTERESTING] method=GET param=${param} type=${vuln_type} evidence=size_diff(${normal_size}→${test_size})" >> "${OUTDIR}/params_vulns.txt"
fi
done
done
# ── FASE C: Testar injeções POST ──
echo ""
info "Fase C — Testando injeções POST..."
for param in "${params_found[@]}"; do
echo -e "\n ${MAG}── POST: ${param} ──${RST}"
# Baseline POST
post_normal=$(curl -sk --connect-timeout "${TIMEOUT_CURL:-5}" \
-X POST -d "${param}=normaltest123" "$target_url" 2>/dev/null)
post_normal_size=${#post_normal}
for vuln_type in "${!PAYLOADS[@]}"; do
payload="${PAYLOADS[$vuln_type]}"
encoded=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${payload}'))" 2>/dev/null || echo "$payload")
# POST urlencoded
post_resp=$(curl -sk --connect-timeout "${TIMEOUT_CURL:-5}" \
-X POST -d "${param}=${encoded}" "$target_url" 2>/dev/null)
_test_payload "$param" "$vuln_type" "$payload" "POST" "$post_resp"
# POST JSON
json_resp=$(curl -sk --connect-timeout "${TIMEOUT_CURL:-5}" \
-X POST -H "Content-Type: application/json" \
-d "{\"${param}\": \"${payload}\"}" "$target_url" 2>/dev/null)
_test_payload "$param" "$vuln_type" "$payload" "POST-JSON" "$json_resp"
done
done
# ── Resumo ──
echo ""
if [[ -s "${OUTDIR}/params_vulns.txt" ]]; then
local get_vulns=$(grep -c "method=GET.*\[VULN\]" "${OUTDIR}/params_vulns.txt" 2>/dev/null || echo 0)
local post_vulns=$(grep -c "method=POST" "${OUTDIR}/params_vulns.txt" 2>/dev/null || echo 0)
local total_vulns=$(grep -c "\[VULN\]" "${OUTDIR}/params_vulns.txt" 2>/dev/null || echo 0)
local interesting=$(grep -c "\[INTERESTING\]" "${OUTDIR}/params_vulns.txt" 2>/dev/null || echo 0)
echo -e " ${RED}🔴 Vulneráveis: ${total_vulns}${RST} (GET: ${get_vulns}, POST: ${post_vulns})"
echo -e " ${YLW}🟡 Interessantes: ${interesting}${RST}"
ok "→ params_vulns.txt"
else
ok "Nenhuma injeção detectada."
fi
ok "→ params_found.txt"
ok "Concluído."
}
# ═══════════════════════════════════════════
# MENU — Listar wordlists
# ═══════════════════════════════════════════
show_wordlists() {
echo ""
echo -e "${BOLD} Wordlists disponíveis:${RST}"
for i in $(seq 1 "$WL_TOTAL"); do
local label="${WL_MAP["${i}_label"]}"
local sz="${WL_MAP["${i}_size"]}"
local tp="${WL_MAP["${i}_type"]}"
local color="$RST"
case "$tp" in
dirs) color="$GRN" ;;
subs) color="$CYN" ;;
files) color="$MAG" ;;
ext) color="$YLW" ;;
params) color="$RED" ;;
esac
printf " ${color}%2d${RST}) %-25s %6s linhas [%s]\n" "$i" "$label" "$sz" "$tp"
done
echo ""
}
show_menu() {
echo ""
echo -e "${BOLD}═══════════════════════════════════════${RST}"
echo -e "${CYN} FUZZING — ${YLW}${BASE_URL}${RST}"
echo -e "${BOLD}═══════════════════════════════════════${RST}"
echo ""
echo -e " ${GRN}1${RST}) Diretórios"
echo -e " ${GRN}2${RST}) Vhosts"
echo -e " ${GRN}3${RST}) Subdomínios DNS"
echo -e " ${GRN}4${RST}) Extensões (dirs + extensões)"
echo -e " ${GRN}5${RST}) Arquivos (wordlist de filenames)"
echo -e " ${GRN}6${RST}) ${RED}Parâmetros${RST} (GET param fuzzing + injeções)"
echo -e " ${GRN}7${RST}) ${CYN}Crawler${RST} (descobrir endpoints automaticamente)"
echo -e " ${GRN}8${RST}) ${MAG}FULL${RST} (tudo sequencial)"
echo ""
echo -e " ${GRN}w${RST}) Ver wordlists disponíveis"
echo -e " ${GRN}q${RST}) Voltar"
echo ""
}
pick_wordlist() {
local default_type="$1"
show_wordlists
# Sugerir wordlist default baseado no tipo
local suggested=""
for i in $(seq 1 "$WL_TOTAL"); do
if [[ "${WL_MAP["${i}_type"]}" == "$default_type" ]]; then
suggested="$i"
break
fi
done
read -rp "$(echo -e "${YLW}[?]${RST} Wordlist [Enter=${suggested:-1}]: ")" choice
choice="${choice:-$suggested}"
choice="${choice:-1}"
if [[ -n "${WL_MAP["${choice}_path"]+x}" ]]; then
PICKED_WL="${WL_MAP["${choice}_path"]}"
ok "Usando: $(basename "$PICKED_WL")"
else
fail "Opção inválida, usando default."
PICKED_WL="${WL_MAP["1_path"]}"
fi
}
# ═══════════════════════════════════════════
# MODO AUTO (chamado pelo FULL RECON)
# ═══════════════════════════════════════════
run_auto() {
echo -e "\n${CYN}━━━ 📂 MÓDULO 04 — Fuzzing (AUTO) ━━━${RST}\n"
calibrate
echo ""
# Pegar primeiro wordlist de cada tipo
local dir_wl="" sub_wl="" ext_wl="" files_wl=""
for i in $(seq 1 "$WL_TOTAL"); do
case "${WL_MAP["${i}_type"]}" in
dirs) [[ -z "$dir_wl" ]] && dir_wl="${WL_MAP["${i}_path"]}" ;;
subs) [[ -z "$sub_wl" ]] && sub_wl="${WL_MAP["${i}_path"]}" ;;
ext) [[ -z "$ext_wl" ]] && ext_wl="${WL_MAP["${i}_path"]}" ;;
files) [[ -z "$files_wl" ]] && files_wl="${WL_MAP["${i}_path"]}" ;;
esac
done
[[ -n "$dir_wl" ]] && scan_dirs "$dir_wl"
[[ -n "$sub_wl" ]] && scan_vhosts "$sub_wl"
[[ -n "$sub_wl" ]] && scan_subdomains "$sub_wl"
if [[ -n "$ext_wl" && -n "$dir_wl" ]]; then
local exts
exts=$(head -50 "$ext_wl" | tr '\n' ',' | sed 's/,$//')
scan_extensions "$dir_wl" "$exts"
elif [[ -n "$dir_wl" ]]; then
scan_extensions "$dir_wl"
fi
[[ -n "$files_wl" ]] && scan_files_only "$files_wl"
# Crawler
scan_crawl 2
# Parameter fuzzing automático (usar URLs crawleadas se existirem)
local param_wl=""
for i in $(seq 1 "$WL_TOTAL"); do
[[ "${WL_MAP["${i}_type"]}" == "params" ]] && { param_wl="${WL_MAP["${i}_path"]}"; break; }
done
if [[ -n "$param_wl" ]]; then
# Se crawler encontrou URLs com parâmetros, testar cada uma
if [[ -s "${OUTDIR}/crawl_params.txt" ]]; then
info "Testando parâmetros nas URLs crawleadas..."
while IFS= read -r crawled_url; do
# Extrair base URL (sem parâmetros)
local base
base=$(echo "$crawled_url" | sed 's|?.*||')
scan_params "$base" "$param_wl"
done < <(head -5 "${OUTDIR}/crawl_params.txt")
else
scan_params "${BASE_URL}/" "$param_wl"
fi
fi
}
# ═══════════════════════════════════════════
# MODO INTERATIVO
# ═══════════════════════════════════════════
run_interactive() {
echo -e "\n${CYN}━━━ 📂 MÓDULO 04 — Fuzzing ━━━${RST}"
echo -e "${BLU} Alvo: ${BASE_URL}${RST}"
calibrate
while true; do
show_menu
read -rp "$(echo -e "${BLU}[fuzzing]${RST} Opção: ")" opt
case "$opt" in
1)
pick_wordlist "dirs"
scan_dirs "$PICKED_WL"
;;
2)
pick_wordlist "subs"
scan_vhosts "$PICKED_WL"
;;
3)
pick_wordlist "subs"
scan_subdomains "$PICKED_WL"
;;
4)
pick_wordlist "dirs"
local dir_picked="$PICKED_WL"
# Perguntar extensões
local ext_wl_found=""
for i in $(seq 1 "$WL_TOTAL"); do
[[ "${WL_MAP["${i}_type"]}" == "ext" ]] && ext_wl_found="${WL_MAP["${i}_path"]}"
done
echo -e "\n${YLW}[?]${RST} Como quer buscar extensões?"
echo -e " ${GRN}1${RST}) Usar wordlist ($(basename "${ext_wl_found:-nenhuma}"))"
echo -e " ${GRN}2${RST}) Digitar extensões manualmente"
echo -e " ${GRN}3${RST}) Sem extensões (só diretórios)"
read -rp "$(echo -e "${YLW}[?]${RST} Opção [1]: ")" ext_choice
ext_choice="${ext_choice:-1}"
case "$ext_choice" in
1)
if [[ -n "$ext_wl_found" ]]; then
local exts
exts=$(head -50 "$ext_wl_found" | tr '\n' ',' | sed 's/,$//')
scan_extensions "$dir_picked" "$exts"
else
warn "Nenhuma wordlist de extensões encontrada."
scan_dirs "$dir_picked"
fi
;;
2)
read -rp "$(echo -e "${YLW}[?]${RST} Extensões (ex: .php,.html,.txt): ")" custom_ext
if [[ -n "$custom_ext" ]]; then
scan_extensions "$dir_picked" "$custom_ext"
else
warn "Nenhuma extensão digitada. Rodando só diretórios."
scan_dirs "$dir_picked"
fi
;;
3)
info "Sem extensões — rodando só diretórios."
scan_dirs "$dir_picked"
;;
*)
warn "Opção inválida. Rodando só diretórios."
scan_dirs "$dir_picked"
;;
esac
;;
5)
pick_wordlist "files"
scan_files_only "$PICKED_WL"
;;
6)
echo -e "\n${YLW}[?]${RST} URL alvo para param fuzzing (ex: ${BASE_URL}/index.php)"
echo -e " ${BLU}Dica: use uma página que aceite parâmetros${RST}"
read -rp "$(echo -e "${YLW}[?]${RST} URL [Enter=${BASE_URL}/]: ")" param_url
param_url="${param_url:-${BASE_URL}/}"
pick_wordlist "params"
scan_params "$param_url" "$PICKED_WL"
;;
7)
echo -e "\n${YLW}[?]${RST} Profundidade do crawl (1-5) [2]: "
read -rp " > " crawl_depth
crawl_depth="${crawl_depth:-2}"
scan_crawl "$crawl_depth"
;;
8)
run_auto
;;
w)
show_wordlists
;;
q)
break
;;
*)
warn "Opção inválida."
;;
esac
done
}
# ═══════════════════════════════════════════
# RESUMO
# ═══════════════════════════════════════════
show_summary() {
echo ""
echo -e "${BOLD}══ RESUMO ══${RST}"
local total=0
for f in "${OUTDIR}"/*.txt "${OUTDIR}"/*.json; do
if [[ -f "$f" ]] && [[ -s "$f" ]]; then
local count
count=$(wc -l < "$f")
total=$((total + count))
echo -e " ${GRN}→${RST} $(basename "$f"): ${count} linhas"
fi
done
ok "Total: ${total} resultados"
}
# Gerar HTML report
generate_html_report() {
HELPERS="$(dirname "$0")/_html_helpers.sh"
[[ ! -f "$HELPERS" ]] && return
source "$HELPERS"
HTML="${OUTDIR}/report.html"
html_head "📂 Directory & Param Fuzzing — ${CLEAN}" > "$HTML"
# Stats
local dir_count=0 crawl_count=0 param_count=0 vuln_count=0 js_count=0
for f in "${OUTDIR}"/ffuf_dirs.txt "${OUTDIR}"/gobuster_dirs.txt; do
[[ -f "$f" ]] && dir_count=$((dir_count + $(wc -l < "$f" 2>/dev/null || echo 0)))
done
[[ -f "${OUTDIR}/crawl_urls.txt" ]] && crawl_count=$(wc -l < "${OUTDIR}/crawl_urls.txt" 2>/dev/null || echo 0)
[[ -f "${OUTDIR}/crawl_params.txt" ]] && param_count=$(wc -l < "${OUTDIR}/crawl_params.txt" 2>/dev/null || echo 0)
[[ -f "${OUTDIR}/params_vulns.txt" ]] && vuln_count=$(grep -c "\[VULN\]" "${OUTDIR}/params_vulns.txt" 2>/dev/null || echo 0)
[[ -f "${OUTDIR}/crawl_js.txt" ]] && js_count=$(wc -l < "${OUTDIR}/crawl_js.txt" 2>/dev/null || echo 0)
cat >> "$HTML" <<EOF
<h1>📂 Directory & Param Fuzzing</h1>
<p class="meta">Alvo: <strong>${BASE_URL}</strong> — $(date '+%Y-%m-%d %H:%M:%S')</p>
<div class="stats">
$(html_stat "$dir_count" "Diretórios" "green")
$(html_stat "$crawl_count" "Crawl URLs" "blue")
$(html_stat "$param_count" "Com parâmetros" "yellow")
$(html_stat "$vuln_count" "Vulns param" "red")
$(html_stat "$js_count" "Arquivos JS" "cyan")
</div>
EOF
# Diretórios encontrados
for f in ffuf_dirs gobuster_dirs; do
if [[ -s "${OUTDIR}/${f}.txt" ]]; then
local tool_name
[[ "$f" == ffuf* ]] && tool_name="ffuf" || tool_name="gobuster"
html_section_start "📁 Diretórios (${tool_name})" >> "$HTML"
html_file_pre "${OUTDIR}/${f}.txt" 100 >> "$HTML"
html_section_end >> "$HTML"
fi
done
# Vhosts
if [[ -s "${OUTDIR}/ffuf_vhosts.txt" ]]; then
html_section_start "🌐 Virtual Hosts" >> "$HTML"
html_file_pre "${OUTDIR}/ffuf_vhosts.txt" 50 >> "$HTML"
html_section_end >> "$HTML"
fi
# Crawler URLs
if [[ -s "${OUTDIR}/crawl_urls.txt" ]]; then
html_section_start "🕷️ Crawler URLs (${crawl_count})" >> "$HTML"
html_file_pre "${OUTDIR}/crawl_urls.txt" 100 >> "$HTML"
html_section_end >> "$HTML"
fi
# URLs com parâmetros
if [[ -s "${OUTDIR}/crawl_params.txt" ]]; then
html_section_start "⚡ URLs com Parâmetros (${param_count})" >> "$HTML"
echo '<table><tr><th>#</th><th>URL</th></tr>' >> "$HTML"
local i=0
while IFS= read -r url && [[ $i -lt 80 ]]; do
i=$((i+1))
echo "<tr><td>${i}</td><td><a href=\"${url}\" target=\"_blank\">$(echo "$url" | _esc)</a></td></tr>" >> "$HTML"
done < "${OUTDIR}/crawl_params.txt"
echo '</table>' >> "$HTML"
html_section_end >> "$HTML"
fi
# Param vulns
if [[ -s "${OUTDIR}/params_vulns.txt" ]]; then
html_section_start "🔴 Vulnerabilidades em Parâmetros (${vuln_count})" >> "$HTML"
echo '<table><tr><th>Tipo</th><th>Método</th><th>Parâmetro</th><th>Evidência</th></tr>' >> "$HTML"
while IFS= read -r line; do
local tag method param vuln_type evidence badge_color
tag=$(echo "$line" | grep -oP '^\[\K[^\]]+')
method=$(echo "$line" | grep -oP 'method=\K\S+')
param=$(echo "$line" | grep -oP 'param=\K\S+')
vuln_type=$(echo "$line" | grep -oP 'type=\K\S+')
evidence=$(echo "$line" | grep -oP 'evidence=\K.*')
[[ "$tag" == "VULN" ]] && badge_color="red" || badge_color="yellow"
echo "<tr><td>$(html_badge "${vuln_type:-?}" "$badge_color")</td><td>$(html_badge "${method:-?}" "blue")</td><td><strong>${param}</strong></td><td>$(echo "$evidence" | _esc)</td></tr>" >> "$HTML"
done < "${OUTDIR}/params_vulns.txt"
echo '</table>' >> "$HTML"
html_section_end >> "$HTML"
fi
# JS Files
if [[ -s "${OUTDIR}/crawl_js.txt" ]]; then
html_section_start "📜 Arquivos JavaScript (${js_count})" >> "$HTML"
html_file_pre "${OUTDIR}/crawl_js.txt" 50 >> "$HTML"
html_section_end >> "$HTML"
fi
# Extensions
if [[ -s "${OUTDIR}/ffuf_extensions.txt" ]]; then
html_section_start "📄 Extensões" >> "$HTML"
html_file_pre "${OUTDIR}/ffuf_extensions.txt" 80 >> "$HTML"
html_section_end >> "$HTML"
fi
html_foot >> "$HTML"
ok "→ report.html"
}
# ── Main ──
if [[ "$AUTO_MODE" == "auto" ]]; then
run_auto
else
run_interactive
fi
# Limpar ANSI de TODOS os arquivos de output
info "Limpando output files..."
for f in "${OUTDIR}"/*.txt; do
[[ -f "$f" ]] && strip_ansi "$f"
done
show_summary
generate_html_report
echo -e "\n${GRN}━━━ Módulo 04 concluído ━━━${RST}"