From 23434bb5f1fc026683b87742c6582a65cdf06381 Mon Sep 17 00:00:00 2001 From: giveen Date: Wed, 14 Jan 2026 13:54:53 -0700 Subject: [PATCH] =?UTF-8?q?mcp:=20Metasploit=20parity=20with=20Hexstrike?= =?UTF-8?q?=20=E2=80=94=20auto-start=20msfrpcd=20(no=20sudo),=20HTTP=20tra?= =?UTF-8?q?nsport,=20adapter=20updates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pentestagent/mcp/mcp_servers.json | 6 +- pentestagent/mcp/metasploit_adapter.py | 113 ++++++++++++++++++++++++- scripts/setup.ps1 | 22 +++-- scripts/setup.sh | 17 ++-- 4 files changed, 142 insertions(+), 16 deletions(-) diff --git a/pentestagent/mcp/mcp_servers.json b/pentestagent/mcp/mcp_servers.json index dd8f080..f8fe4db 100644 --- a/pentestagent/mcp/mcp_servers.json +++ b/pentestagent/mcp/mcp_servers.json @@ -18,7 +18,11 @@ "args": [ "third_party/MetasploitMCP/MetasploitMCP.py", "--transport", - "stdio" + "http", + "--host", + "127.0.0.1", + "--port", + "7777" ], "description": "Metasploit MCP (vendored) - local server", "timeout": 300, diff --git a/pentestagent/mcp/metasploit_adapter.py b/pentestagent/mcp/metasploit_adapter.py index bcb3716..d86ec85 100644 --- a/pentestagent/mcp/metasploit_adapter.py +++ b/pentestagent/mcp/metasploit_adapter.py @@ -44,21 +44,106 @@ class MetasploitAdapter: server_script: Optional[Path] = None, cwd: Optional[Path] = None, env: Optional[dict] = None, + transport: str = "http", ) -> None: self.host = host self.port = int(port) self.python_cmd = python_cmd + # Vendored project uses 'MetasploitMCP.py' as the main entrypoint self.server_script = ( - server_script or Path("third_party/MetasploitMCP/metasploit_mcp.py") + server_script or Path("third_party/MetasploitMCP/MetasploitMCP.py") ) self.cwd = cwd or Path.cwd() self.env = {**os.environ, **(env or {})} + self.transport = transport self._process: Optional[asyncio.subprocess.Process] = None self._reader_task: Optional[asyncio.Task] = None + self._msfrpcd_proc: Optional[asyncio.subprocess.Process] = None def _build_command(self): - return [self.python_cmd, str(self.server_script), "--port", str(self.port)] + cmd = [self.python_cmd, str(self.server_script)] + # Prefer explicit transport when starting vendored server from adapter + if self.transport: + cmd += ["--transport", str(self.transport)] + # When running HTTP, ensure host/port are provided + if str(self.transport).lower() in ("http", "sse"): + cmd += ["--host", str(self.host), "--port", str(self.port)] + else: + # For other transports, allow default args + cmd += ["--port", str(self.port)] + return cmd + + async def _start_msfrpcd_if_needed(self) -> None: + """Start `msfrpcd` if it's not already reachable at MSF_SERVER:MSF_PORT. + + This starts `msfrpcd` as a child process (no sudo) using MSF_* env + values if available. It's intentionally conservative: if the RPC + endpoint is already listening we won't try to start a new daemon. + """ + try: + msf_server = str(self.env.get("MSF_SERVER", "127.0.0.1")) + msf_port = int(self.env.get("MSF_PORT", 55553)) + except Exception: + msf_server = "127.0.0.1" + msf_port = 55553 + + # Quick socket check to see if msfrpcd is already listening + import socket + + try: + with socket.create_connection((msf_server, msf_port), timeout=1): + return + except Exception: + pass + + # If msfrpcd not available on path, skip starting + if not shutil.which("msfrpcd"): + return + + msf_user = str(self.env.get("MSF_USER", "msf")) + msf_password = str(self.env.get("MSF_PASSWORD", "")) + msf_ssl = str(self.env.get("MSF_SSL", "false")).lower() in ("1", "true", "yes", "y") + + # Build args for msfrpcd (no sudo). Use -S (SSL optional) flag only if requested. + args = ["msfrpcd", "-U", msf_user, "-P", msf_password, "-a", msf_server, "-p", str(msf_port)] + if msf_ssl: + args.append("-S") + + try: + resolved = shutil.which("msfrpcd") or "msfrpcd" + self._msfrpcd_proc = await asyncio.create_subprocess_exec( + resolved, + *args[1:], + cwd=str(self.cwd), + env=self.env, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.STDOUT, + start_new_session=True, + ) + # Start reader to capture msfrpcd logs + loop = asyncio.get_running_loop() + loop.create_task(self._capture_msfrpcd_output()) + # Give msfrpcd a moment to start + await asyncio.sleep(0.5) + except Exception: + return + + async def _capture_msfrpcd_output(self) -> None: + if not self._msfrpcd_proc or not self._msfrpcd_proc.stdout: + return + try: + with LOG_FILE.open("ab") as fh: + while True: + line = await self._msfrpcd_proc.stdout.readline() + if not line: + break + fh.write(b"[msfrpcd] " + line) + fh.flush() + except asyncio.CancelledError: + pass + except Exception: + pass async def start(self, background: bool = True, timeout: int = 30) -> bool: """Start the vendored Metasploit MCP server. @@ -74,6 +159,13 @@ class MetasploitAdapter: if self._process and self._process.returncode is None: return await self.health_check(timeout=1) + # If running in HTTP/SSE mode, ensure msfrpcd is started and reachable + if str(self.transport).lower() in ("http", "sse"): + try: + await self._start_msfrpcd_if_needed() + except Exception: + pass + cmd = self._build_command() resolved = shutil.which(self.python_cmd) or self.python_cmd @@ -147,6 +239,23 @@ class MetasploitAdapter: except Exception: pass + # Stop msfrpcd if we started it + try: + msf_proc = self._msfrpcd_proc + if msf_proc: + try: + msf_proc.terminate() + await asyncio.wait_for(msf_proc.wait(), timeout=timeout) + except asyncio.TimeoutError: + try: + msf_proc.kill() + except Exception: + pass + except Exception: + pass + finally: + self._msfrpcd_proc = None + def stop_sync(self, timeout: int = 5) -> None: proc = self._process if not proc: diff --git a/scripts/setup.ps1 b/scripts/setup.ps1 index d06bbae..fa8397e 100644 --- a/scripts/setup.ps1 +++ b/scripts/setup.ps1 @@ -152,18 +152,24 @@ if (Test-Path -Path $msReq) { # Optionally auto-start msfrpcd if configured in .env if (($env:LAUNCH_METASPLOIT_MCP -eq 'true') -and ($env:MSF_PASSWORD)) { - if (Get-Command bash -ErrorAction SilentlyContinue) { - $msfUser = if ($env:MSF_USER) { $env:MSF_USER } else { 'msf' } - $msfServer = if ($env:MSF_SERVER) { $env:MSF_SERVER } else { '127.0.0.1' } - $msfPort = if ($env:MSF_PORT) { $env:MSF_PORT } else { '55553' } - Write-Host "Attempting to start msfrpcd (user=$msfUser, host=$msfServer, port=$msfPort)..." + $msfUser = if ($env:MSF_USER) { $env:MSF_USER } else { 'msf' } + $msfServer = if ($env:MSF_SERVER) { $env:MSF_SERVER } else { '127.0.0.1' } + $msfPort = if ($env:MSF_PORT) { $env:MSF_PORT } else { '55553' } + Write-Host "Starting msfrpcd (user=$msfUser, host=$msfServer, port=$msfPort) without sudo (background)..." + # Start msfrpcd without sudo; if it's already running the cmd will fail harmlessly. + if (Get-Command msfrpcd -ErrorAction SilentlyContinue) { try { - & bash -lc "sudo msfrpcd -U $msfUser -P '$($env:MSF_PASSWORD)' -a $msfServer -p $msfPort -S" + if ($env:MSF_SSL -eq 'true' -or $env:MSF_SSL -eq '1') { + Start-Process -FilePath msfrpcd -ArgumentList "-U", $msfUser, "-P", $env:MSF_PASSWORD, "-a", $msfServer, "-p", $msfPort, "-S" -NoNewWindow -WindowStyle Hidden + } else { + Start-Process -FilePath msfrpcd -ArgumentList "-U", $msfUser, "-P", $env:MSF_PASSWORD, "-a", $msfServer, "-p", $msfPort -NoNewWindow -WindowStyle Hidden + } + Write-Host "msfrpcd start requested; check with: netstat -an | Select-String $msfPort" } catch { - Write-Host "Warning: Failed to start msfrpcd via bash: $($_.Exception.Message)" -ForegroundColor Yellow + Write-Host "Warning: Failed to start msfrpcd: $($_.Exception.Message)" -ForegroundColor Yellow } } else { - Write-Host "Warning: Cannot auto-start msfrpcd: 'bash' not available. Start msfrpcd manually with msfrpcd -U -P -a -p -S" -ForegroundColor Yellow + Write-Host "msfrpcd not found; please install Metasploit Framework to enable Metasploit RPC." -ForegroundColor Yellow } } diff --git a/scripts/setup.sh b/scripts/setup.sh index f4b98e4..e74ecb6 100644 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -126,7 +126,7 @@ if [ -f "third_party/MetasploitMCP/requirements.txt" ]; then fi # Optionally auto-start Metasploit RPC daemon if configured -# Requires `msfrpcd` (from metasploit-framework) and sudo to run as a service. +# Start `msfrpcd` without sudo if LAUNCH_METASPLOIT_MCP=true and MSF_PASSWORD is set. if [ "${LAUNCH_METASPLOIT_MCP,,}" = "true" ] && [ -n "${MSF_PASSWORD:-}" ]; then if command -v msfrpcd >/dev/null 2>&1; then MSF_USER="${MSF_USER:-msf}" @@ -134,11 +134,18 @@ if [ "${LAUNCH_METASPLOIT_MCP,,}" = "true" ] && [ -n "${MSF_PASSWORD:-}" ]; then MSF_PORT="${MSF_PORT:-55553}" MSF_SSL="${MSF_SSL:-false}" echo "Starting msfrpcd (user=${MSF_USER}, host=${MSF_SERVER}, port=${MSF_PORT})..." - if sudo -n true 2>/dev/null; then - sudo msfrpcd -U "$MSF_USER" -P "$MSF_PASSWORD" -a "$MSF_SERVER" -p "$MSF_PORT" -S || echo "Warning: msfrpcd failed to start." + # Start msfrpcd as a background process without sudo. The daemon will bind to the loopback + # interface and does not require root privileges on modern systems for ephemeral ports. + msfrpcd_cmd=$(command -v msfrpcd || true) + if [ -n "$msfrpcd_cmd" ]; then + if [ "${MSF_SSL,,}" = "true" ] || [ "${MSF_SSL}" = "1" ]; then + "$msfrpcd_cmd" -U "$MSF_USER" -P "$MSF_PASSWORD" -a "$MSF_SERVER" -p "$MSF_PORT" -S &>/dev/null & + else + "$msfrpcd_cmd" -U "$MSF_USER" -P "$MSF_PASSWORD" -a "$MSF_SERVER" -p "$MSF_PORT" &>/dev/null & + fi + echo "msfrpcd started (check with: ss -ltn | grep $MSF_PORT)" else - echo "msfrpcd requires sudo. You may be prompted for your password to start it interactively." - sudo msfrpcd -U "$MSF_USER" -P "$MSF_PASSWORD" -a "$MSF_SERVER" -p "$MSF_PORT" -S || echo "Failed to start msfrpcd. Start it manually with: sudo msfrpcd -U $MSF_USER -P -a $MSF_SERVER -p $MSF_PORT -S" + echo "msfrpcd not found; please install Metasploit Framework to enable Metasploit RPC." fi else echo "msfrpcd not found; please install Metasploit Framework to enable Metasploit RPC."