380 lines
18 KiB
JavaScript
380 lines
18 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* BurpSuite MCP Stdio Bridge
|
|
*
|
|
* Bridges the custom HTTP API (port 9876) to standard MCP JSON-RPC 2.0 stdio protocol.
|
|
* This allows any MCP client (Claude Code, Kiro, Cursor, Cline, etc.) to use all 78 Burp tools.
|
|
*
|
|
* Cross-platform: Works on Windows, Linux (Kali), macOS.
|
|
*
|
|
* Usage in MCP config (all platforms):
|
|
* { "command": "node", "args": ["<path-to-this-file>/mcp-bridge.js"] }
|
|
*
|
|
* Environment variables:
|
|
* BURP_MCP_HOST - Burp HTTP API host (default: 127.0.0.1)
|
|
* BURP_MCP_PORT - Burp HTTP API port (default: 9876)
|
|
*/
|
|
|
|
const http = require('http');
|
|
const readline = require('readline');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
|
|
const BURP_HOST = process.env.BURP_MCP_HOST || '127.0.0.1';
|
|
const BURP_PORT = parseInt(process.env.BURP_MCP_PORT || '9876', 10);
|
|
|
|
function resolveToken() {
|
|
if (process.env.BURP_MCP_TOKEN) return process.env.BURP_MCP_TOKEN;
|
|
try {
|
|
const tokenFile = path.join(os.homedir(), '.burp-mcp-token');
|
|
if (fs.existsSync(tokenFile)) return fs.readFileSync(tokenFile, 'utf8').trim();
|
|
} catch (e) { /* ignore */ }
|
|
return null;
|
|
}
|
|
|
|
const BURP_TOKEN = resolveToken();
|
|
const AUTH_HEADERS = BURP_TOKEN ? { 'Authorization': `Bearer ${BURP_TOKEN}` } : {};
|
|
|
|
let TOOLS = null;
|
|
let toolsRequest = null;
|
|
|
|
async function fetchTools() {
|
|
return new Promise((resolve, reject) => {
|
|
const req = http.request({
|
|
hostname: BURP_HOST, port: BURP_PORT, path: '/tools', method: 'GET',
|
|
headers: AUTH_HEADERS
|
|
}, (res) => {
|
|
let data = '';
|
|
res.on('data', chunk => data += chunk);
|
|
res.on('end', () => {
|
|
if (res.statusCode !== 200) {
|
|
reject(new Error(`Cannot fetch tools: HTTP ${res.statusCode} ${data.slice(0, 200)}`));
|
|
return;
|
|
}
|
|
try { resolve(JSON.parse(data)); } catch (e) { reject(e); }
|
|
});
|
|
});
|
|
req.on('error', reject);
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
function disconnectedResponse(id) {
|
|
return {
|
|
jsonrpc: '2.0',
|
|
id,
|
|
error: {
|
|
code: -32000,
|
|
message: `Burp MCP not connected at ${BURP_HOST}:${BURP_PORT}. Start Burp with the "MCP Full Control" extension loaded, then retry.`
|
|
}
|
|
};
|
|
}
|
|
|
|
async function ensureTools() {
|
|
if (TOOLS) return TOOLS;
|
|
if (!toolsRequest) {
|
|
toolsRequest = fetchTools()
|
|
.then(tools => {
|
|
TOOLS = tools;
|
|
process.stderr.write(`[burp-mcp-bridge] Connected to Burp. ${TOOLS.length} tools available.\n`);
|
|
return TOOLS;
|
|
})
|
|
.finally(() => {
|
|
toolsRequest = null;
|
|
});
|
|
}
|
|
return toolsRequest;
|
|
}
|
|
|
|
async function callTool(toolName, params) {
|
|
return new Promise((resolve, reject) => {
|
|
const body = JSON.stringify({ tool: toolName, params: params || {} });
|
|
const req = http.request({
|
|
hostname: BURP_HOST, port: BURP_PORT, path: '/', method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body), ...AUTH_HEADERS }
|
|
}, (res) => {
|
|
let data = '';
|
|
res.on('data', chunk => data += chunk);
|
|
res.on('end', () => {
|
|
if (res.statusCode === 403) {
|
|
reject(new Error('Burp MCP rejected the request: missing or invalid BURP_MCP_TOKEN. Set BURP_MCP_TOKEN to match the token in ~/.burp-mcp-token.'));
|
|
return;
|
|
}
|
|
try { resolve(JSON.parse(data)); } catch (e) { resolve({ error: data }); }
|
|
});
|
|
});
|
|
req.on('error', (err) => {
|
|
reject(new Error(
|
|
`Cannot reach Burp MCP at ${BURP_HOST}:${BURP_PORT} (${err.code || err.message}). ` +
|
|
`Ensure Burp Suite is running with the "MCP Full Control" extension loaded. ` +
|
|
`If the port differs, set BURP_MCP_PORT and -Dburp.mcp.port=<same> on Burp.`
|
|
));
|
|
});
|
|
req.write(body);
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
function buildToolDefinitions(toolNames) {
|
|
return toolNames.map(name => ({
|
|
name: `burp_${name}`,
|
|
description: getToolDescription(name),
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: getToolParams(name),
|
|
}
|
|
}));
|
|
}
|
|
|
|
function getToolDescription(name) {
|
|
const descriptions = {
|
|
proxy_history: 'Get Burp proxy history with optional filtering by URL, method, status code',
|
|
proxy_detail: 'Get full request/response details for a specific proxy history item by index',
|
|
proxy_websocket: 'Get WebSocket message history',
|
|
proxy_clear: 'Clear proxy history',
|
|
proxy_history_filtered: 'Filter proxy history by annotation color or notes',
|
|
send_request: 'Send an HTTP request through Burp and get the response',
|
|
send_to_repeater: 'Send a raw request to Burp Repeater tab',
|
|
repeater_send: 'Send a request and get response (like Repeater)',
|
|
repeater_modify_send: 'Modify headers/body of a request then send it',
|
|
send_to_intruder: 'Send a request to Burp Intruder',
|
|
intruder_attack: 'Run a numeric range brute force attack (synchronous)',
|
|
intruder_attack_async: 'Run a multi-threaded numeric range brute force attack',
|
|
intruder_attack_wordlist: 'Run a wordlist-based attack',
|
|
intruder_pitchfork: 'Run a Pitchfork attack (parallel multi-param)',
|
|
intruder_cluster_bomb: 'Run a Cluster Bomb attack (cartesian product multi-param)',
|
|
intruder_battering_ram: 'Run a Battering Ram attack (same payload all positions)',
|
|
intruder_with_options: 'Run attack with advanced options (throttle, encoding, grep, timing)',
|
|
sitemap: 'Get site map entries with optional URL prefix filter',
|
|
target_info: 'Get target information (hosts, technologies detected)',
|
|
intercept_toggle: 'Enable or disable proxy intercept',
|
|
encode: 'Encode a string (base64, url, hex)',
|
|
decode: 'Decode a string (base64, url)',
|
|
convert_request: 'Convert HTTP request method (e.g. GET to POST)',
|
|
export_request: 'Export a request as curl command',
|
|
generate_csrf_poc: 'Generate a CSRF proof-of-concept HTML page',
|
|
extract_from_response: 'Extract data from a response using regex',
|
|
payload_process: 'Process a payload (hash, encode, reverse, etc.)',
|
|
scan: 'Start a vulnerability scan',
|
|
scan_active: 'Start active scan on a specific request',
|
|
scan_results: 'Get scan results (discovered vulnerabilities)',
|
|
scan_issue_detail: 'Get detailed information about a specific scan issue',
|
|
crawl: 'Start crawling a URL (adds to scope)',
|
|
get_scope: 'Check if a URL is in Burp scope',
|
|
add_to_scope: 'Add a URL to Burp scope',
|
|
remove_from_scope: 'Remove a URL from Burp scope',
|
|
collaborator_generate: 'Generate Burp Collaborator payloads for OOB testing',
|
|
collaborator_poll: 'Poll for Collaborator interactions (DNS/HTTP callbacks)',
|
|
search_history: 'Search proxy history with regex (in URL, request, or response)',
|
|
highlight: 'Highlight a proxy history item with a color',
|
|
annotate: 'Add a note/annotation to a proxy history item',
|
|
compare: 'Compare two proxy history responses (diff)',
|
|
export_config: 'Export Burp project configuration as JSON',
|
|
import_config: 'Import Burp project configuration from JSON',
|
|
set_upstream_proxy: 'Set upstream proxy (SOCKS/HTTP) for all Burp traffic',
|
|
set_dns_override: 'Override DNS resolution for a hostname',
|
|
set_http2: 'Enable or disable HTTP/2',
|
|
cookie_jar: 'View cookies in Burp cookie jar (with optional domain filter)',
|
|
token_analysis: 'Analyze token entropy and randomness',
|
|
sequencer: 'Analyze a batch of tokens for randomness quality',
|
|
save_project: 'Save the current Burp project',
|
|
burp_version: 'Get Burp Suite version information',
|
|
add_issue: 'Manually add a vulnerability issue to the site map',
|
|
register_http_handler: 'Register an auto-modify rule for HTTP requests (add header or replace text)',
|
|
remove_http_handler: 'Remove/clear HTTP handler rules',
|
|
register_proxy_rule: 'Register a proxy intercept rule (intercept URLs containing a string)',
|
|
remove_proxy_rule: 'Remove/clear proxy intercept rules',
|
|
extensions_list: 'Get information about loaded extensions',
|
|
log: 'Write a message to Burp extension output log',
|
|
audit_log: 'View audit log entries',
|
|
privacy_mode: 'Set privacy mode (strict/off)',
|
|
scope_gate: 'Enable/disable scope gate',
|
|
inline_fuzzer: 'FUZZ marker fuzzing',
|
|
race_condition: 'Race condition testing',
|
|
access_control_sweep: 'Test different auth levels',
|
|
injection_probe: 'SQLi/SSTI/LFI probe',
|
|
jwt_attack: 'JWT attacks (alg:none)',
|
|
jwt_decode: 'Decode JWT tokens',
|
|
session_remove_rule: 'Remove session rule',
|
|
session_list_rules: 'List session rules',
|
|
session_create_rule: 'Create session handling rule',
|
|
passive_intel: 'Extract secrets from proxy history',
|
|
websocket_list: 'List active WebSockets',
|
|
websocket_close: 'Close WebSocket',
|
|
websocket_send_binary: 'Send binary on WebSocket',
|
|
websocket_send_text: 'Send text on WebSocket',
|
|
websocket_create: 'Create WebSocket connection',
|
|
send_request_parallel: 'Send parallel HTTP requests',
|
|
cookie_jar_set: 'Set cookie in Burp cookie jar',
|
|
};
|
|
return descriptions[name] || `Burp Suite tool: ${name}`;
|
|
}
|
|
|
|
function getToolParams(name) {
|
|
// Common parameter schemas
|
|
const schemas = {
|
|
proxy_history: { limit: {type:'number',description:'Max items (default 100)'}, offset: {type:'number'}, url_filter: {type:'string'}, method_filter: {type:'string'}, status_filter: {type:'number'} },
|
|
proxy_detail: { index: {type:'number',description:'History item index'} },
|
|
proxy_websocket: { limit: {type:'number'} },
|
|
proxy_history_filtered: { has_notes: {type:'string'}, color: {type:'string'}, limit: {type:'number'} },
|
|
send_request: { method: {type:'string'}, url: {type:'string',description:'Full URL'}, body: {type:'string'}, headers: {type:'object'} },
|
|
repeater_send: { request: {type:'string',description:'Raw HTTP request'}, host: {type:'string'}, port: {type:'number'}, https: {type:'boolean'} },
|
|
repeater_modify_send: { request: {type:'string'}, host: {type:'string'}, port: {type:'number'}, https: {type:'boolean'}, replace_header: {type:'object'}, add_header: {type:'object'}, replace_body: {type:'string'} },
|
|
send_to_repeater: { request: {type:'string'}, tab_name: {type:'string'} },
|
|
send_to_intruder: { request: {type:'string'} },
|
|
intruder_attack: { url_template: {type:'string',description:'URL with @@ placeholder'}, from: {type:'number'}, to: {type:'number'}, pad_digits: {type:'number'}, method: {type:'string'}, headers: {type:'object'}, success_length_not: {type:'number'}, success_contains: {type:'string'} },
|
|
intruder_attack_async: { url_template: {type:'string'}, from: {type:'number'}, to: {type:'number'}, pad_digits: {type:'number'}, method: {type:'string'}, headers: {type:'object'}, success_length_not: {type:'number'}, threads: {type:'number'} },
|
|
intruder_attack_wordlist: { url_template: {type:'string'}, wordlist: {type:'array',items:{type:'string'}}, method: {type:'string'}, headers: {type:'object'}, success_length_not: {type:'number'}, body_template: {type:'string'} },
|
|
intruder_pitchfork: { url_template: {type:'string'}, placeholders: {type:'object'}, method: {type:'string'}, headers: {type:'object'}, success_length_not: {type:'number'} },
|
|
intruder_cluster_bomb: { url_template: {type:'string'}, placeholders: {type:'object'}, method: {type:'string'}, headers: {type:'object'}, success_length_not: {type:'number'}, max_requests: {type:'number'} },
|
|
intruder_battering_ram: { url_template: {type:'string'}, wordlist: {type:'array',items:{type:'string'}}, placeholder: {type:'string'}, method: {type:'string'}, headers: {type:'object'}, success_length_not: {type:'number'} },
|
|
intruder_with_options: { url_template: {type:'string'}, from: {type:'number'}, to: {type:'number'}, pad_digits: {type:'number'}, method: {type:'string'}, headers: {type:'object'}, success_length_not: {type:'number'}, throttle_ms: {type:'number'}, payload_prefix: {type:'string'}, payload_suffix: {type:'string'}, payload_encoding: {type:'string'}, grep_extract: {type:'string'}, record_time: {type:'boolean'} },
|
|
sitemap: { url_prefix: {type:'string'}, limit: {type:'number'} },
|
|
target_info: { url: {type:'string'} },
|
|
intercept_toggle: { enable: {type:'boolean'} },
|
|
encode: { input: {type:'string'}, type: {type:'string',description:'base64, url, or hex'} },
|
|
decode: { input: {type:'string'}, type: {type:'string',description:'base64 or url'} },
|
|
convert_request: { request: {type:'string'}, convert_to: {type:'string'} },
|
|
export_request: { request: {type:'string'}, host: {type:'string'}, format: {type:'string',description:'curl or python'}, https: {type:'boolean'} },
|
|
generate_csrf_poc: { request: {type:'string'}, host: {type:'string'}, https: {type:'boolean'} },
|
|
extract_from_response: { index: {type:'number'}, regex: {type:'string'} },
|
|
payload_process: { input: {type:'string'}, operation: {type:'string',description:'base64_encode/decode, url_encode/decode, md5, sha1, sha256, hex_encode, lowercase, uppercase, reverse, length'} },
|
|
scan_active: { request: {type:'string'}, host: {type:'string'}, port: {type:'number'}, https: {type:'boolean'} },
|
|
scan_results: { limit: {type:'number'} },
|
|
scan_issue_detail: { index: {type:'number'} },
|
|
crawl: { url: {type:'string'} },
|
|
get_scope: { url: {type:'string'} },
|
|
add_to_scope: { url: {type:'string'} },
|
|
remove_from_scope: { url: {type:'string'} },
|
|
collaborator_generate: { count: {type:'number'} },
|
|
search_history: { regex: {type:'string'}, search_in: {type:'string',description:'url, request, or response'}, limit: {type:'number'} },
|
|
highlight: { index: {type:'number'}, color: {type:'string'} },
|
|
annotate: { index: {type:'number'}, note: {type:'string'} },
|
|
compare: { index1: {type:'number'}, index2: {type:'number'} },
|
|
import_config: { config: {type:'string'} },
|
|
set_upstream_proxy: { proxy_host: {type:'string'}, proxy_port: {type:'number'}, type: {type:'string'} },
|
|
set_dns_override: { hostname: {type:'string'}, ip: {type:'string'} },
|
|
set_http2: { enable: {type:'boolean'} },
|
|
cookie_jar: { limit: {type:'number'}, domain: {type:'string'} },
|
|
token_analysis: { tokens: {type:'array',items:{type:'string'}} },
|
|
sequencer: { tokens: {type:'array',items:{type:'string'}} },
|
|
add_issue: { name: {type:'string'}, url: {type:'string'}, detail: {type:'string'}, severity: {type:'string'}, confidence: {type:'string'} },
|
|
register_http_handler: { header_name: {type:'string'}, header_value: {type:'string'}, match: {type:'string'}, replace: {type:'string'} },
|
|
register_proxy_rule: { url_contains: {type:'string'} },
|
|
log: { message: {type:'string'}, level: {type:'string'} },
|
|
audit_log: { limit: {type:'number'} },
|
|
privacy_mode: { mode: {type:'string'} },
|
|
scope_gate: { action: {type:'string'} },
|
|
inline_fuzzer: { template: {type:'string'}, host: {type:'string'}, wordlist: {type:'array'} },
|
|
race_condition: { request: {type:'string'}, host: {type:'string'}, count: {type:'number'} },
|
|
access_control_sweep: { request: {type:'string'}, host: {type:'string'}, auth_headers: {type:'string'} },
|
|
injection_probe: { url: {type:'string'}, param: {type:'string'}, type: {type:'string'} },
|
|
jwt_attack: { token: {type:'string'}, attack: {type:'string'} },
|
|
jwt_decode: { token: {type:'string'} },
|
|
session_remove_rule: {},
|
|
session_list_rules: {},
|
|
session_create_rule: { find: {type:'string'}, replace: {type:'string'} },
|
|
passive_intel: { limit: {type:'number'} },
|
|
websocket_list: {},
|
|
websocket_close: { id: {type:'string'} },
|
|
websocket_send_binary: { id: {type:'string'}, data: {type:'string'} },
|
|
websocket_send_text: { id: {type:'string'}, text: {type:'string'} },
|
|
websocket_create: { host: {type:'string'}, port: {type:'number'} },
|
|
send_request_parallel: { requests: {type:'array'} },
|
|
cookie_jar_set: { url: {type:'string'}, name: {type:'string'}, value: {type:'string'} },
|
|
};
|
|
return schemas[name] || {};
|
|
}
|
|
|
|
async function handleRequest(msg) {
|
|
const { method, id, params } = msg;
|
|
|
|
switch (method) {
|
|
case 'initialize':
|
|
return { jsonrpc: '2.0', id, result: {
|
|
protocolVersion: '2024-11-05',
|
|
capabilities: { tools: {} },
|
|
serverInfo: { name: 'burpsuite-mcp', version: '2.0.0' }
|
|
}};
|
|
|
|
case 'notifications/initialized':
|
|
return null; // No response needed
|
|
|
|
case 'tools/list':
|
|
try {
|
|
const tools = await ensureTools();
|
|
return { jsonrpc: '2.0', id, result: { tools: buildToolDefinitions(tools) } };
|
|
} catch (err) {
|
|
return disconnectedResponse(id);
|
|
}
|
|
|
|
case 'tools/call': {
|
|
try {
|
|
await ensureTools();
|
|
} catch (err) {
|
|
return disconnectedResponse(id);
|
|
}
|
|
if (!params || typeof params.name !== 'string') {
|
|
return { jsonrpc: '2.0', id, error: { code: -32602, message: 'tools/call requires params.name' } };
|
|
}
|
|
const toolName = params.name.replace(/^burp_/, '');
|
|
const toolArgs = params.arguments || {};
|
|
try {
|
|
const result = await callTool(toolName, toolArgs);
|
|
return { jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } };
|
|
} catch (err) {
|
|
TOOLS = null;
|
|
return { jsonrpc: '2.0', id, error: { code: -1, message: err.message || 'Tool call failed' } };
|
|
}
|
|
}
|
|
|
|
default:
|
|
return { jsonrpc: '2.0', id, error: { code: -32601, message: `Method not found: ${method}` } };
|
|
}
|
|
}
|
|
|
|
// Main stdio loop
|
|
async function main() {
|
|
try {
|
|
await ensureTools();
|
|
} catch (e) {
|
|
process.stderr.write(`[burp-mcp-bridge] WARNING: Cannot connect to Burp at ${BURP_HOST}:${BURP_PORT}. Start Burp first.\n`);
|
|
}
|
|
|
|
const rl = readline.createInterface({ input: process.stdin, terminal: false });
|
|
let pending = 0;
|
|
let stdinClosed = false;
|
|
|
|
rl.on('line', (line) => {
|
|
if (!line.trim()) return;
|
|
let msg;
|
|
try {
|
|
msg = JSON.parse(line);
|
|
} catch (e) {
|
|
process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } }) + '\n');
|
|
return;
|
|
}
|
|
|
|
pending++;
|
|
handleRequest(msg)
|
|
.then(response => {
|
|
if (response) process.stdout.write(JSON.stringify(response) + '\n');
|
|
})
|
|
.catch(err => {
|
|
process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: msg.id ?? null, error: { code: -1, message: err.message || 'Handler error' } }) + '\n');
|
|
})
|
|
.finally(() => {
|
|
pending--;
|
|
if (stdinClosed && pending === 0) process.exit(0);
|
|
});
|
|
});
|
|
|
|
rl.on('close', () => {
|
|
stdinClosed = true;
|
|
if (pending === 0) process.exit(0);
|
|
});
|
|
}
|
|
|
|
main();
|