| name | blackbox-web-audit |
| description | Authorized black-box web/API audit WITHOUT test accounts — unauth probing discipline, unauth-IDOR rounds using leaked IDs, JS-bundle-first attack surface mapping, non-destructive gates, and local report delivery conventions (no zip, follow sibling example folder). Use when asked to audit/pentest a target where you hold no credentials, and when writing the local report deliverable afterwards. |
Black-Box Web Audit Without Credentials
Class workflow for authorized audits where you have NO test account (operator-requested or VDP scope). Complements the manually-authored bb-methodology (5-phase workflow) and bug-bounty-reporting-workflow (validation standard) — load those too; this skill adds the no-credentials specifics they lack.
Hard Constraints (non-destructive discipline)
- GET-only on anything state-changing-adjacent. Shutdown/reset/redeem endpoints: probe with GET even if the app uses POST; NEVER send the real state-changing verb on production (a
POST /api/version/shutdown was deliberately never sent).
- Fake IDs for write-path probes. Create-order/payment endpoints tested only with nonexistent UUIDs; expect-and-document 401.
- Credentials are always fake for rate-limit/login tests. A quarantine of your own IP after 429 is evidence the control works — record it as such.
- Embedded keys found in bundles get tested live against their real API before any claim (Firebase:
identitytoolkit.googleapis.com/v1/accounts:createAuthUri?key= → 400 "API key not valid" = revoked, informational only).
Pre-Flight: Authorization Check
Before any probing, check for a public bug bounty program (HackerOne/Bugcrowd/Intigriti) and /.well-known/security.txt on apex + www. If NEITHER exists, downgrade to passive/non-intrusive observation only (CT logs, header analysis, unauth GETs) and state that explicitly in the report's legal-notes section — findings from active testing of a no-program target are unreportable and create liability.
Attack Surface When You Have No Account
Subfinder/gau often return near-zero on these targets. Two maps work:
Map A — CT-log subdomain sweep (do this FIRST, ~2 min):
curl "https://crt.sh/?q=%25.DOMAIN&output=json" → on 502/malformed (frequent), fall back to https://api.certspotter.com/v1/issuances?domain=DOMAIN&include_subdomains=true&expand=dns_names (returned 24 hosts when crt.sh was down).
- Probe every host in one bash loop: status, redirect target,
<title>, Server/X-Powered-By headers; tee to recon/<target>/host_probe.txt.
- High-yield host classes:
staging-*/*dev* (note auth gates), mail.* (webmail brand/version), api-*/apps.* (swagger candidates), odd personal-looking names (leftover deployments).
- On ASP.NET Core hosts try
/swagger/index.html then /swagger/v1/swagger.json (also /swagger/doc.json). A 200 swagger.json with empty security/securitySchemes = Medium documentation-exposure finding; enumerate paths read-only, never send the documented POSTs.
Map B — the JS bundle when a SPA exists:
- Download all
/_next/static/chunks/*.js (or equivalent SPA bundles) to disk; never cat minified files — grep them.
- Extract endpoint inventory (regex
/api/[a-z0-9/_-]+, quoted AND backtick strings) → typically 50–100 endpoints from a ~1MB bundle.
- Probe each with GET, no auth. Verify content-type on EVERY probe: Next.js/SPA backends answer unknown
/api/* paths with HTTP 200 text/html (index fallback). Only application/json counts; HTML fallback ≠ open endpoint. This one check kills most false-positive "exposure" claims. Same trap via redirect chains: routers 307-normalize paths first (Go: /render/https://x → /render/https:/x), and following the redirect lands on the SPA catch-all 200 — a ffuf "hit" that looks like an open SSRF/render proxy. Before claiming ANY discovered path does something server-side, diff its body against the app's index.html baseline (byte size + <title>): identical shell = fallback route, not a feature (real case: /render/<url> flagged by ffuf, killed as SSRF after body-diff, 2026-08-23).
- Mine JS for: hardcoded config (
authMode, OIDC labels), sandbox/test features shipped to prod (simulate-payment, debug flags), embedded keys/client IDs.
- Rate-limit test: N sequential bad logins to
POST /api/auth/login → document lockout window (retryAfter) as a verified control.
Origin Exposure via SPF (Cloudflare bypass — highest-yield single technique)
When the target sits behind Cloudflare, mine DNS for the origin IP before scanning:
dig TXT domain / DoH — an SPF record with ip4: names the mail/origin server outright (v=spf1 ip4:ORIGIN ~all leaked it in one engagement).
- Verify multi-vhost serving:
curl -skI --resolve host:443:ORIGIN https://host/ for EVERY known hostname (apex, my., api., app., plus any gateway domains mined from JS bundles). One origin commonly serves all vhosts AND sibling products.
- Confirm via SNI cert mismatch:
openssl s_client -connect ORIGIN:443 -servername host returning a cert for a DIFFERENT domain = shared origin proven. nuclei flags this as mismatched-ssl-certificate.
- Impact chain once confirmed: full WAF/rate-limit/bot-protection bypass by pointing requests at the origin IP; often exposes internal-only services (admin panels, AI agent builders like Flowise — probe
/api/v1/version, /ping, get-upload-file?path= traversal variants, setup/signup takeover) and non-HTTP port clusters from a quick -p- nmap.
Also: /etc/hosts-pinning every discovered hostname to the origin IP makes all later
tooling (nuclei, ffuf) test origin-side behavior instead of Cloudflare edge.
Real IDs from ANY public leak make a meaningful object-access pass possible:
- Harvest real object UUIDs from public catalog/listing endpoints (e.g.
/api/voucher-groups leaking full catalog incl. hidden items).
- Replay GET-only against every ID-bearing variant: direct path
/api/x/{id}, query forms ?xId= / ?x_id=, sibling objects (orders, vouchers, user), admin-ish resources.
- Controls are mandatory: same request with fake/nonexistent ID (expect 404) proves the route resolves IDs; valid-ID-no-auth returning uniform 401 proves auth gating. 200-valid/404-fake differential on a gated endpoint = protection working, NOT IDOR.
- Write the negative matrix into the report (probe → status → verdict table in SUMMARY.md). Confirmed-no-IDOR with evidence demonstrates coverage; silence demonstrates nothing.
- If a per-ID detail endpoint IS open (by design), fold it into the parent disclosure finding rather than inventing an IDOR claim.
Complementary Negatives (run all, ~5 min)
- Parameter reflection: unique 8+ char canary through query params on main pages; count occurrences (0 expected on well-built SPAs).
- Open redirect:
?next=https://evil.com / ?redirect= — check Location header; fixed internal redirect = safe.
- Injection probes on path params (
', OR 1=1, traversal): expect uniform 404/400 (parameterized); varied error bodies = investigate.
- CORS: send foreign origins +
null; no reflection = clean (severity tiers live in bug-bounty-reporting-workflow).
- dalfox single-pass on parameterized pages for a scanner second-opinion.
Environment Pitfalls
- Cloudflare blocks default
python-urllib UA with 403 while curl/browser UA passes. Every urllib/requests-based PoC MUST set a browser User-Agent or it fails where your curl probes succeeded.
- No
dig/host/nslookup on many boxes → DNS-over-HTTPS via curl (https://dns.google/resolve?name=X&type=A).
- crt.sh intermittently returns malformed short responses → fall back to JS mining for subdomain/endpoint hints.
- ASP.NET edge can trap API probing in infinite 307 redirect loops (
Location: points at the same URL; -L never resolves). Diagnose by reading headers, not just following. Workarounds that flipped 307→real answers in one case: add Accept: application/json (→403 = WAF gate), try trailing slash, force --http1.1.
- ASP.NET hosts leak versions via headers even when pages are gated — capture
X-Powered-By, X-AspNet-Version, X-AspNetMvc-Version, and literal Server: Microsoft-IIS/8.0 (IIS 8.0 = Windows Server 2012, EOL Oct 2023 → version-exposure finding with upgrade recommendation).
- Missing binaries are setup issues, not findings — note once, work around, move on.
- Long inline bash gets hardline-blocked by the command parser. Compound one-liners carrying complex regex (brace quantifiers like
.{60}canary.{40}) or many chained statements can trip "BLOCKED (hardline): malformed executable payload". Reliable pattern that always worked: write_file a small dependency-free Python probe (urllib + http.cookiejar), then run python3 script.py. Keep loops/regex/context-extraction in the script; keep each terminal call short. This is an executor quirk, not "shell is broken".
- Laravel catch-all fallback route poisons naive probing. Unknown paths return HTTP 200 + homepage HTML (~same byte size every time, only canonical/og:url differ) instead of 404. So
/telescope, /horizon, .env.backup all "200 OK". Triage rule: 403 with tiny iso-8859-1 body = real Apache block; 200 + ~56KB text/html = catch-all, NOT exposure; only non-fallback sizes/content-types count as hits. Also expect soft-404 profile pages (/anggota/{invalid} → 200 empty shell) and broken routes that 500 into debug pages (see references/laravel-app-audit.md).
CMS Estates: WordPress + Moodle
When fingerprint shows wp-content/ / xmlrpc.php / /wp-json or a MoodleSession cookie, follow references/wp-moodle-deep-exploitation.md. Headline rules from that reference:
- Dual-vhost estates: probe apex AND origin vhosts separately (
curl --resolve host:443:<origin-ip>) — xmlrpc was CF-blocked (520) on apex but fully alive on the origin vhost; 520 ≠ downtime.
- readme.txt version enum is per-vhost: plugin
readme.txt was 404 for ALL plugins on origin but 200 via apex. Try both hosts before concluding "no readme".
- Moodle version: read top heading of
/UPGRADING.md (5.x) or /lib/upgrade.txt (≤4.4); anonymous config leak via POST /lib/ajax/service.php with tool_mobile_get_public_config; always try /info.php.
- Moodle login spray REQUIRES scraping the hidden
logintoken CSRF field first (GET login page with cookie jar → POST username/password/logintoken). Without it every attempt false-negatives as invalid-login even with correct credentials.
- Keep brute-force budgets exact and agreed up-front (e.g. 27 xmlrpc attempts, 3 Moodle logins), sequential with delay; uniform failures = no-lockout control verified.
Report Delivery (user preferences — hard rules)
- NEVER ship a zip archive. User explicitly rejected it ("kenapa bentuknya zip"). Plain individual files, ONE canonical location only.
- Before creating the report folder, look for a sibling example target and copy its structure exactly (e.g.
/workspace/reports/<previous-target>/). Canonical observed shape:
SUMMARY.md — emoji severity matrix (Severity | Title | CVSS | CWE | Endpoint | Link), PoC list, evidence list, verified-working-controls table, priority recommendations.
metadata.json — {target, scan_time, total_findings, severity_summary{CRITICAL..INFORMATIONAL}, findings[{file_name,title,severity,cvss,cwe,endpoint,last_modified}], pocs[], evidence_files[]}.
findings/[SEVERITY]_slug.md — Target/Severity/CVSS/CWE/Status header, raw request/response blocks, impact, verified-negatives, remediation.
pocs/*.py, evidence/*.
- Report language = whatever the CURRENT engagement specifies (this user alternates: English ("berbahasa inggris") and Bahasa Indonesia). Ask once if unspecified; never inherit the last session's choice.
- Execute every delivered PoC end-to-end before shipping. Both bugs found in the initial PoC (truncated-body slice breaking json.loads; leftover experimental line) surfaced only on first run.
- Post-engagement cleanup is a hard rule for this user. After the report folder is final, delete recon working dirs and /tmp probe artifacts (user: "abis ngapa-ngapain cache nya di bersihkan"). Copy anything worth keeping into
evidence/ FIRST, then rm -rf recon dir and tmp files; verify only deliverables remain.
- If a report-aggregator tool exists that regenerates SUMMARY.md from findings, run it BEFORE hand-writing the rich SUMMARY — the tool overwrites the file with its simple index format. Restore/enrich afterwards or skip the tool; never let the generated index be the shipped summary.
Follow-Up Rounds (user-mandated workflow)
When the operator asks for follow-up testing ("validasi exploit", round 2), resolve it into ONE explicitly-stated objective — no ambiguity — and restate it in the todo list before probing. Same non-destructive gates apply (GET-only on state-changing paths, fake credentials, small bursts). Every outcome must land in the report: positives AND negatives go into evidence/validation_roundN_notes.md (verdict matrix + per-test raw detail) and get reflected in SUMMARY.md rows (validated controls table grows; strengthened findings get updated evidence). A negative validation is coverage proof, not waste. Concrete patterns (timing enumeration, ID sweeps, SQLi canary battery, Accept:application/json debug trace) live in references/laravel-app-audit.md.
References
references/nextjs-gateway-case-study.md — worked example: hardened Next.js+Cloudflare AI gateway, 71-endpoint JS-mined surface, unauth-IDOR round with negative matrix, 1 Low + 5 Info outcome.
references/ai-reseller-storefront-case-study.md — worked example: SPF→origin multi-vhost bypass on a Cloudflare-fronted AI-credit reseller, Flowise fingerprinting, gateway-vs-console rate-limit differential, auditor-side artifact FPs (output-redaction masking, SPA-fallback SSRF), session-preserving auth testing under login cooldown.
references/laravel-app-audit.md — Laravel estate specifics: debug-page triage (APP_DEBUG vs Ignition vs catch-all), dual-mode HTML+JSON debug trace leak, response-signature table from an enterprise Laravel audit, login-throttle PoC pattern, validation-round patterns (timing enum, ID sweep, SQLi canary), safe trigger set.