Stealth Mode¶
Stealth mode is enabled by default for all engagements. It minimizes the detection footprint by mimicking real browser traffic patterns, enforcing low request rates, and avoiding signatures that trigger WAF rules. The only ways to disable stealth are the --fast and --bug-bounty flags.
Overview¶
| Setting | Stealth (Default) | --fast |
--bug-bounty |
|---|---|---|---|
| Rate limit | 1-3 req/sec | 50 req/sec | 50 req/sec |
| Concurrency | 2 threads | 25 threads | 25 threads |
| User-Agent | Chrome 133 | Tool default | Tool default |
| TLS fingerprint | Chrome via curl-impersonate | Standard curl | Standard curl |
| Jitter | 1-4s random delay | None | None |
| Wordlist shuffle | Yes | No | No |
| Phase breaks | 30s between phases | None | None |
| DoS templates | Excluded | Excluded | Included |
| Recon (Phase 1) | Full | Skipped | Full + expanded |
TLS Fingerprint Evasion¶
Standard curl's OpenSSL TLS fingerprint (JA3: 456523fc94726331a8d0512ce63e59e3) is on every major WAF blocklist. Stealth mode uses curl-impersonate to produce Chrome's exact JA3 hash at the TLS layer.
# Availability check (Docker container includes curl-impersonate)
if which curl_chrome131 >/dev/null 2>&1; then
CURL_CMD="curl_chrome131"
elif which curl-chrome >/dev/null 2>&1; then
CURL_CMD="curl-chrome"
else
CURL_CMD="curl" # Fallback with --tlsv1.3
fi
When curl-impersonate is unavailable, stealth mode falls back to standard curl with --tlsv1.3 to at least match modern browser TLS version negotiation.
Browser Impersonation Profiles¶
WAFs fingerprint HTTP header order, not just values. Chrome sends sec-ch-ua before User-Agent before Accept. Firefox does not send sec-ch-ua at all. A mismatched UA + header set triggers instant detection.
Chrome 133 Profile (Default)¶
CHROME_UA="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36"
# Headers in Chrome's exact sending order
STEALTH_HEADERS_CHROME=(
-H "sec-ch-ua: \"Chromium\";v=\"133\", \"Not?A_Brand\";v=\"99\""
-H "sec-ch-ua-mobile: ?0"
-H "sec-ch-ua-platform: \"macOS\""
-H "User-Agent: $CHROME_UA"
-H "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8"
-H "Accept-Language: en-US,en;q=0.9"
-H "Accept-Encoding: gzip, deflate, br"
-H "Connection: keep-alive"
-H "Upgrade-Insecure-Requests: 1"
-H "Sec-Fetch-Dest: document"
-H "Sec-Fetch-Mode: navigate"
-H "Sec-Fetch-Site: none"
-H "Sec-Fetch-User: ?1"
)
A separate STEALTH_HEADERS_CHROME_API profile is used for API/XHR requests, with Accept: application/json and Sec-Fetch-Mode: cors.
Firefox 133 Profile¶
Available as an alternative. Firefox does not send sec-ch-ua headers -- including them with a Firefox UA triggers detection.
Profile Selection¶
The active profile is automatically matched to the curl-impersonate binary in use:
curl_chrome131/curl-chrome--> Chrome headerscurl_ff133/curl-firefox--> Firefox headers- Standard
curl--> Chrome headers (best general compatibility)
Rate Limits¶
Global Limits¶
| Activity | Maximum Rate |
|---|---|
| Scanning | 10 req/sec |
| Fuzzing | 50 req/sec |
| Authentication | 1 req/sec |
Per-Tool Stealth Configuration¶
| Tool | Default Rate | Stealth Rate | Stealth Concurrency |
|---|---|---|---|
| nuclei | -rl 10 |
-rl 2 |
-c 2 |
| ffuf/ffufai | -rate 10 |
-rate 1 |
-t 1 |
| katana | default | -rl 2 -c 2 |
-d 3 (max depth) |
| httpx | default | -rl 2 -t 2 |
-- |
| naabu | -rate 100 |
-rate 10 |
-- |
| nikto | default | -maxtime 600s -Pause 2 |
-- |
| sqlmap | --delay=1 |
--delay=2 --time-sec=1 |
--threads=1 |
| dalfox | default | --delay 2000 |
--worker 1 |
| arjun | default | -t 1 --delay 2 |
-- |
| subfinder | default | -rate-limit 2 |
-- |
| curl loops | no delay | sleep $(shuf -i 1-4 -n 1) |
-- |
Tool-Specific Examples¶
# nuclei -- stealth mode
nuclei -rl 2 -c 2 -timeout 10 \
-H "User-Agent: $STEALTH_UA" \
-etags dos,fuzz \
-severity critical,high,medium \
-l targets.txt
# sqlmap -- stealth mode
sqlmap -u "TARGET" --batch --level=2 --risk=1 \
--technique=BT --threads=1 --delay=2 --time-sec=1 \
--random-agent --safe-url="TARGET_BASEURL" --safe-freq=5 \
--tamper=between,randomcase --output-dir=logs/sqlmap/
# ffuf -- stealth mode (with shuffled wordlist)
shuf wordlist.txt > /tmp/shuffled-wordlist.txt
ffuf -rate 1 -t 1 \
-H "User-Agent: $STEALTH_UA" \
-w /tmp/shuffled-wordlist.txt \
-u TARGET_URL/FUZZ
Timing Jitter¶
Instead of fixed delays, stealth mode uses random jitter between requests to simulate human browsing patterns.
# Base jitter: 1-4 seconds (random) between requests
stealth_delay() {
local min=$(( 1 * JITTER_MULT ))
local max=$(( 4 * JITTER_MULT ))
sleep $(python3 -c "import random; print(round(random.uniform($min, $max), 1))")
}
JITTER_MULT -- Parallel Agent Scaling¶
When multiple agents run concurrently against the same target (wave execution), each agent multiplies its jitter by JITTER_MULT to keep the combined request rate within stealth thresholds.
| Concurrent Agents | JITTER_MULT |
Per-Agent Jitter | Combined Rate |
|---|---|---|---|
| 1 (solo) | 1 | 1-4s | ~0.5 req/sec |
| 2 | 2 | 2-8s | ~0.5 req/sec |
| 3 | 3 | 3-12s | ~0.5 req/sec |
JITTER_MULT is set via environment variable by the wave dispatcher (see agent-dispatch.md). It affects:
- Inter-request jitter (
stealth_delay) - Rate limit backoff duration (
stealth_backoff) - All curl loops inside skills
# Rate limit backoff also scales with JITTER_MULT
stealth_backoff() {
local min=$(( 10 * JITTER_MULT ))
local max=$(( 30 * JITTER_MULT ))
sleep $(python3 -c "import random; print(round(random.uniform($min, $max), 1))")
}
WAF Detection and Response¶
The stealth_curl() function in the shared boilerplate automatically handles WAF responses:
| HTTP Code | Behavior |
|---|---|
| 429 | Log warning, backoff 10-30s (scaled by JITTER_MULT), retry once |
| 502/503 | Log warning, wait 5s, return response body |
| 3x 429 consecutive | Stop the current skill entirely |
# stealth_curl() handles this automatically:
# 1. Send request with Chrome UA + stealth headers + TLS 1.3
# 2. If 429: backoff, retry
# 3. If 503: warn, brief pause
# 4. Apply random jitter before next request
Wordlist Randomization¶
All stealth-mode fuzzing randomizes wordlists to avoid sequential access patterns detectable by WAFs:
stealth_shuffle_wordlist() {
local input="$1"
local output="/tmp/shuffled-$(basename "$input")"
shuf "$input" > "$output"
echo "$output"
}
# Usage
wordlist=$(stealth_shuffle_wordlist "wordlists/SecLists/Discovery/Web-Content/common.txt")
ffuf -w "$wordlist" -u TARGET_URL/FUZZ
Sequential directory brute-forcing (a, aa, aab, ...) is a well-known WAF signature. Shuffling eliminates this pattern.
Endpoint Exclusions¶
In stealth mode, the following endpoints are skipped during fuzzing and automated scanning to avoid triggering side effects:
STEALTH_EXCLUDE_ENDPOINTS=(
"/logout" "/signout"
"/api/*/delete" "/api/*/remove"
"/export/*" "/download/*"
"/admin/restart" "/admin/shutdown"
"/webhook/*" "/payment/*" "/checkout/*"
"/api/*/purge" "/cache/clear"
"/queue/*" "/cron/*" "/jobs/*"
)
For ffuf, these are converted to an exclude regex:
STEALTH_EXCLUDE_REGEX="(logout|signout|delete|remove|restart|shutdown|payment|checkout|purge|cache/clear)"
Phase Timing Distribution¶
Stealth mode inserts 30-second breaks between major phases to let traffic patterns normalize:
stealth_phase_break() {
echo "[stealth] Phase complete. Pausing 30s to normalize traffic patterns..."
sleep 30
}
Cookie Jar Persistence¶
Stealth mode maintains a persistent cookie jar to accumulate cookies like a real browser session (cf_clearance, _ga, csrftoken):
COOKIE_JAR="${EDIR:-/tmp}/logs/.cookie-jar"
# Initialize with a homepage visit (warms up WAF clearance cookies)
init_cookie_jar() {
if [ ! -s "$COOKIE_JAR" ]; then
$CURL_CMD -s -o /dev/null \
-b "$COOKIE_JAR" -c "$COOKIE_JAR" \
--max-time 15 "$target"
fi
}
DNS-over-HTTPS¶
Stealth mode routes DNS queries through Cloudflare's DoH endpoint to prevent DNS query leakage to network monitoring and IDS systems:
DOH_URL="https://cloudflare-dns.com/dns-query"
# Applied via: --doh-url "$DOH_URL" on stealth_curl calls
Comparing Modes¶
Default (Stealth ON)¶
- Chrome 133 User-Agent with correct header order
- curl-impersonate for TLS fingerprint evasion
- 1-3 req/sec with random jitter
- Shuffled wordlists
- DoS/fuzz nuclei templates excluded
- DNS-over-HTTPS enabled
- 30s phase breaks
- Cookie jar persistence
--fast (Stealth OFF, Recon Skipped)¶
- 50 req/sec, 25 threads
- Tool default User-Agents
- No jitter or phase breaks
- Phase 1 (recon) skipped entirely
- All other phases remain mandatory
- Mutually exclusive with
--bug-bounty
--bug-bounty (Stealth OFF, Expanded Recon)¶
- 50 req/sec, 25 threads
- Tool default User-Agents
- No jitter or phase breaks
- Expanded recon:
alterx,puredns,asnmap,uncover - All nuclei templates included (DoS and fuzz)
- sqlmap at
--risk=2 --level=3 --technique=BEUST - Mutually exclusive with
--fast
Integration Checklist¶
Every skill checks context.json for stealth mode at startup via the shared boilerplate:
stealth_enabled=true # Default
if [ -f "$CONTEXT_FILE" ]; then
stealth_enabled=$(python3 -c "
import json, os
ctx = json.load(open(os.environ.get('CONTEXT_FILE','context.json')))
print('true' if ctx.get('stealth',{}).get('enabled', True) else 'false')
")
fi
All curl calls are routed through stealth_curl(), which conditionally applies headers, jitter, rate limiting, and backoff based on this flag.