reverse-skills-max: initial public release (restored, redacted, hardened)
This commit is contained in:
379
reverse-skill-repo/burp-mcp-full/mcp-bridge.js
Normal file
379
reverse-skill-repo/burp-mcp-full/mcp-bridge.js
Normal file
@@ -0,0 +1,379 @@
|
||||
#!/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();
|
||||
Reference in New Issue
Block a user