""" AUTARCH Metasploit Integration Interface for Metasploit Framework via RPC """ import json import http.client import ssl import socket import subprocess import time import os import signal from typing import Optional, Dict, List, Any, Tuple from dataclasses import dataclass # msgpack is optional - MSF features disabled without it try: import msgpack MSGPACK_AVAILABLE = True except ImportError: msgpack = None MSGPACK_AVAILABLE = False from .config import get_config from .banner import Colors class MSFError(Exception): """Exception raised for Metasploit-related errors.""" pass def check_msgpack(): """Check if msgpack is available, raise error if not.""" if not MSGPACK_AVAILABLE: raise MSFError( "msgpack module not installed. Install with: pip install msgpack" ) @dataclass class MSFModule: """Information about a Metasploit module.""" type: str # exploit, auxiliary, post, payload, encoder, nop name: str fullname: str description: str rank: str = "" author: List[str] = None references: List[str] = None class MetasploitRPC: """Client for Metasploit RPC API.""" def __init__( self, host: str = "127.0.0.1", port: int = 55553, username: str = "msf", password: str = None, ssl: bool = True ): """Initialize MSF RPC client. Args: host: MSF RPC host address. port: MSF RPC port (default 55553). username: RPC username. password: RPC password. ssl: Use SSL connection. """ self.host = host self.port = port self.username = username self.password = password self.use_ssl = ssl self.token: Optional[str] = None self._connected = False @property def is_connected(self) -> bool: """Check if connected to MSF RPC.""" return self._connected and self.token is not None def _decode_bytes(self, obj): """Recursively decode bytes to strings in msgpack responses. Args: obj: Object to decode (dict, list, bytes, or other). Returns: Decoded object with all bytes converted to strings. """ if isinstance(obj, bytes): return obj.decode('utf-8', errors='replace') elif isinstance(obj, dict): return { self._decode_bytes(k): self._decode_bytes(v) for k, v in obj.items() } elif isinstance(obj, list): return [self._decode_bytes(item) for item in obj] elif isinstance(obj, tuple): return tuple(self._decode_bytes(item) for item in obj) else: return obj def _request(self, method: str, params: List = None) -> Dict[str, Any]: """Make an RPC request to Metasploit. Args: method: RPC method name. params: Method parameters. Returns: Response dictionary. Raises: MSFError: If request fails. """ check_msgpack() # Ensure msgpack is available params = params or [] # Add token to authenticated requests if self.token and method != "auth.login": params = [self.token] + params # Build request request_data = msgpack.packb([method] + params) try: if self.use_ssl: context = ssl.create_default_context() context.check_hostname = False context.verify_mode = ssl.CERT_NONE conn = http.client.HTTPSConnection( self.host, self.port, context=context, timeout=30 ) else: conn = http.client.HTTPConnection(self.host, self.port, timeout=30) headers = { "Content-Type": "binary/message-pack", "Content-Length": str(len(request_data)) } conn.request("POST", "/api/", request_data, headers) response = conn.getresponse() if response.status != 200: raise MSFError(f"HTTP error: {response.status} {response.reason}") response_data = response.read() result = msgpack.unpackb(response_data, raw=False, strict_map_key=False) # Recursively normalize bytes to strings throughout the response result = self._decode_bytes(result) if isinstance(result, dict) and result.get("error"): raise MSFError(f"MSF error: {result.get('error_message', 'Unknown error')}") return result except ConnectionRefusedError: raise MSFError(f"Connection refused to {self.host}:{self.port}. Is msfrpcd running?") except Exception as e: if isinstance(e, MSFError): raise raise MSFError(f"RPC request failed: {e}") finally: try: conn.close() except: pass def connect(self, password: str = None) -> bool: """Connect and authenticate to MSF RPC. Args: password: RPC password (uses stored password if not provided). Returns: True if connected successfully. Raises: MSFError: If connection fails. """ password = password or self.password if not password: raise MSFError("No password provided for MSF RPC") try: result = self._request("auth.login", [self.username, password]) if result.get("result") == "success": self.token = result.get("token") self._connected = True return True else: raise MSFError("Authentication failed") except MSFError: self._connected = False self.token = None raise def disconnect(self): """Disconnect from MSF RPC.""" if self.token: try: self._request("auth.logout", [self.token]) except: pass self.token = None self._connected = False def get_version(self) -> Dict[str, str]: """Get Metasploit version info. Returns: Dictionary with version information. """ if not self.is_connected: raise MSFError("Not connected to MSF RPC") return self._request("core.version") def list_modules(self, module_type: str = None) -> List[str]: """List available modules. Args: module_type: Filter by type (exploit, auxiliary, post, payload, encoder, nop). Returns: List of module names. """ if not self.is_connected: raise MSFError("Not connected to MSF RPC") # Map module types to their API method names # The MSF RPC API uses module.exploits, module.auxiliary, etc. type_to_method = { "exploit": "module.exploits", "auxiliary": "module.auxiliary", "post": "module.post", "payload": "module.payloads", "encoder": "module.encoders", "nop": "module.nops", } if module_type: method = type_to_method.get(module_type) if not method: raise MSFError(f"Unknown module type: {module_type}") result = self._request(method) return result.get("modules", []) else: # Get all module types all_modules = [] for mtype in ["exploit", "auxiliary", "post", "payload"]: try: method = type_to_method.get(mtype) result = self._request(method) modules = result.get("modules", []) all_modules.extend([f"{mtype}/{m}" for m in modules]) except: pass return all_modules def search_modules(self, query: str) -> List[Dict[str, Any]]: """Search for modules matching a query. Args: query: Search query string. Returns: List of matching modules. """ if not self.is_connected: raise MSFError("Not connected to MSF RPC") result = self._request("module.search", [query]) return result if isinstance(result, list) else [] def get_module_info(self, module_type: str, module_name: str) -> MSFModule: """Get detailed information about a module. Args: module_type: Module type (exploit, auxiliary, etc.). module_name: Module name. Returns: MSFModule with module details. """ if not self.is_connected: raise MSFError("Not connected to MSF RPC") result = self._request("module.info", [module_type, module_name]) return MSFModule( type=module_type, name=result.get("name", module_name), fullname=f"{module_type}/{module_name}", description=result.get("description", ""), rank=result.get("rank", ""), author=result.get("author", []), references=result.get("references", []) ) def get_module_options(self, module_type: str, module_name: str) -> Dict[str, Any]: """Get available options for a module. Args: module_type: Module type. module_name: Module name. Returns: Dictionary of options and their details. """ if not self.is_connected: raise MSFError("Not connected to MSF RPC") return self._request("module.options", [module_type, module_name]) def execute_module( self, module_type: str, module_name: str, options: Dict[str, Any] = None ) -> Dict[str, Any]: """Execute a module with given options. Args: module_type: Module type. module_name: Module name. options: Module options dictionary. Returns: Execution result with job_id. """ if not self.is_connected: raise MSFError("Not connected to MSF RPC") options = options or {} return self._request("module.execute", [module_type, module_name, options]) def list_jobs(self) -> Dict[str, Any]: """List running jobs. Returns: Dictionary of job IDs and info. """ if not self.is_connected: raise MSFError("Not connected to MSF RPC") return self._request("job.list") def get_job_info(self, job_id: str) -> Dict[str, Any]: """Get information about a job. Args: job_id: Job ID. Returns: Job information dictionary. """ if not self.is_connected: raise MSFError("Not connected to MSF RPC") return self._request("job.info", [job_id]) def stop_job(self, job_id: str) -> bool: """Stop a running job. Args: job_id: Job ID to stop. Returns: True if stopped successfully. """ if not self.is_connected: raise MSFError("Not connected to MSF RPC") result = self._request("job.stop", [job_id]) return result.get("result") == "success" def list_sessions(self) -> Dict[str, Any]: """List active sessions. Returns: Dictionary of session IDs and info. """ if not self.is_connected: raise MSFError("Not connected to MSF RPC") return self._request("session.list") def session_shell_read(self, session_id: str) -> str: """Read output from a shell session. Args: session_id: Session ID. Returns: Shell output string. """ if not self.is_connected: raise MSFError("Not connected to MSF RPC") result = self._request("session.shell_read", [session_id]) return result.get("data", "") def session_shell_write(self, session_id: str, command: str) -> bool: """Write a command to a shell session. Args: session_id: Session ID. command: Command to execute. Returns: True if written successfully. """ if not self.is_connected: raise MSFError("Not connected to MSF RPC") result = self._request("session.shell_write", [session_id, command + "\n"]) return result.get("write_count", 0) > 0 def session_stop(self, session_id: str) -> bool: """Stop/kill a session. Args: session_id: Session ID to stop. Returns: True if stopped successfully. """ if not self.is_connected: raise MSFError("Not connected to MSF RPC") result = self._request("session.stop", [session_id]) return result.get("result") == "success" def run_console_command(self, command: str) -> str: """Run a command in MSF console. Args: command: Console command to run. Returns: Command output. """ if not self.is_connected: raise MSFError("Not connected to MSF RPC") # Create console console = self._request("console.create") console_id = console.get("id") try: # Write command self._request("console.write", [console_id, command + "\n"]) # Read output (with retries for async commands) import time output = "" for _ in range(10): time.sleep(0.5) result = self._request("console.read", [console_id]) output += result.get("data", "") if not result.get("busy", False): break return output finally: # Destroy console try: self._request("console.destroy", [console_id]) except: pass class MSFManager: """High-level manager for Metasploit integration.""" def __init__(self): self.config = get_config() self.rpc: Optional[MetasploitRPC] = None self._server_process: Optional[subprocess.Popen] = None def _ensure_config_section(self): """Ensure MSF config section exists.""" if not self.config.config.has_section('msf'): self.config.config['msf'] = { 'host': '127.0.0.1', 'port': '55553', 'username': 'msf', 'password': '', 'ssl': 'true', 'autoconnect': 'true' } self.config.save() def detect_server(self) -> Tuple[bool, Optional[str]]: """Detect if msfrpcd is already running. Returns: Tuple of (is_running, pid or None) """ settings = self.get_settings() host = settings['host'] port = settings['port'] # First try socket connection to check if port is open try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(2) result = sock.connect_ex((host, port)) sock.close() if result == 0: # Port is open, try to find the process pid = self._find_msfrpcd_pid() return True, pid except Exception: pass # Also check for running msfrpcd process even if port check failed pid = self._find_msfrpcd_pid() if pid: return True, pid return False, None def _find_msfrpcd_pid(self) -> Optional[str]: """Find the PID of running msfrpcd process. Returns: PID as string, or None if not found """ try: # Use pgrep to find msfrpcd result = subprocess.run( ['pgrep', '-f', 'msfrpcd'], capture_output=True, text=True, timeout=5 ) if result.returncode == 0 and result.stdout.strip(): # Return first PID found pids = result.stdout.strip().split('\n') return pids[0] if pids else None except (subprocess.TimeoutExpired, FileNotFoundError): pass # Fallback: check /proc on Linux try: for pid_dir in os.listdir('/proc'): if pid_dir.isdigit(): try: cmdline_path = f'/proc/{pid_dir}/cmdline' with open(cmdline_path, 'r') as f: cmdline = f.read() if 'msfrpcd' in cmdline: return pid_dir except (IOError, PermissionError): continue except Exception: pass return None def kill_server(self, use_sudo: bool = True) -> bool: """Kill any running msfrpcd server. Args: use_sudo: Use sudo for killing (needed if server was started with sudo) Returns: True if server was killed or no server was running """ is_running, pid = self.detect_server() if not is_running: return True # Disconnect our client first if connected if self.is_connected: self.disconnect() # Kill the process if pid: try: # Try without sudo first os.kill(int(pid), signal.SIGTERM) # Wait a bit for graceful shutdown time.sleep(1) # Check if still running, force kill if needed try: os.kill(int(pid), 0) # Check if process exists os.kill(int(pid), signal.SIGKILL) time.sleep(0.5) except ProcessLookupError: pass # Process already dead return True except PermissionError: # Process owned by root, need sudo if use_sudo: try: subprocess.run(['sudo', 'kill', '-TERM', str(pid)], timeout=5) time.sleep(1) # Check if still running try: os.kill(int(pid), 0) subprocess.run(['sudo', 'kill', '-KILL', str(pid)], timeout=5) except ProcessLookupError: pass return True except Exception as e: print(f"{Colors.RED}[X] Failed to kill msfrpcd with sudo (PID {pid}): {e}{Colors.RESET}") return False else: print(f"{Colors.RED}[X] Failed to kill msfrpcd (PID {pid}): Permission denied{Colors.RESET}") return False except ProcessLookupError: return True # Already dead # Try pkill as fallback (with sudo if needed) try: if use_sudo: subprocess.run(['sudo', 'pkill', '-f', 'msfrpcd'], timeout=5) else: subprocess.run(['pkill', '-f', 'msfrpcd'], timeout=5) time.sleep(1) return True except Exception: pass return False def start_server(self, username: str, password: str, host: str = "127.0.0.1", port: int = 55553, use_ssl: bool = True, use_sudo: bool = True) -> bool: """Start the msfrpcd server with given credentials. Args: username: RPC username password: RPC password host: Host to bind to port: Port to listen on use_ssl: Whether to use SSL use_sudo: Run msfrpcd with sudo (required for raw socket modules like SYN scan) Returns: True if server started successfully """ # Build msfrpcd command from core.paths import find_tool msfrpcd_bin = find_tool('msfrpcd') or 'msfrpcd' cmd = [ msfrpcd_bin, '-U', username, '-P', password, '-a', host, '-p', str(port), '-f' # Run in foreground (we'll background it ourselves) ] if not use_ssl: cmd.append('-S') # Disable SSL # Prepend sudo if requested if use_sudo: cmd = ['sudo'] + cmd try: # Start msfrpcd in background self._server_process = subprocess.Popen( cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True # Detach from our process group ) # Wait for server to start (check port becomes available) max_wait = 30 # seconds start_time = time.time() port_open = False while time.time() - start_time < max_wait: try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(1) result = sock.connect_ex((host, port)) sock.close() if result == 0: port_open = True break except Exception: pass time.sleep(0.5) if not port_open: print(f"{Colors.YELLOW}[!] Server started but port not responding after {max_wait}s{Colors.RESET}") return False # Port is open, but server needs time to initialize RPC layer # msfrpcd can take 5-10 seconds to fully initialize on some systems print(f"{Colors.DIM} Waiting for RPC initialization...{Colors.RESET}") time.sleep(5) # Give server time to fully initialize # Try a test connection to verify server is really ready for attempt in range(10): try: test_rpc = MetasploitRPC( host=host, port=port, username=username, password=password, ssl=use_ssl ) test_rpc.connect(password) test_rpc.disconnect() return True except MSFError as e: if attempt < 9: time.sleep(2) continue except Exception: if attempt < 9: time.sleep(2) continue # Server started but auth still failing - return true anyway # The server IS running, caller can retry connection print(f"{Colors.YELLOW}[!] Server running but authentication not ready - try connecting manually{Colors.RESET}") return True except FileNotFoundError: print(f"{Colors.RED}[X] msfrpcd not found. Is Metasploit installed?{Colors.RESET}") return False except Exception as e: print(f"{Colors.RED}[X] Failed to start msfrpcd: {e}{Colors.RESET}") return False def get_settings(self) -> Dict[str, Any]: """Get current MSF settings.""" self._ensure_config_section() return { 'host': self.config.get('msf', 'host', '127.0.0.1'), 'port': self.config.get_int('msf', 'port', 55553), 'username': self.config.get('msf', 'username', 'msf'), 'password': self.config.get('msf', 'password', ''), 'ssl': self.config.get_bool('msf', 'ssl', True), 'autoconnect': self.config.get_bool('msf', 'autoconnect', True) } def save_settings(self, host: str, port: int, username: str, password: str, use_ssl: bool): """Save MSF settings.""" self._ensure_config_section() self.config.set('msf', 'host', host) self.config.set('msf', 'port', port) self.config.set('msf', 'username', username) self.config.set('msf', 'password', password) self.config.set('msf', 'ssl', str(use_ssl).lower()) self.config.save() def connect(self, password: str = None) -> MetasploitRPC: """Connect to Metasploit RPC. Args: password: RPC password (uses saved if not provided). Returns: Connected MetasploitRPC instance. """ settings = self.get_settings() password = password or settings['password'] self.rpc = MetasploitRPC( host=settings['host'], port=settings['port'], username=settings['username'], password=password, ssl=settings['ssl'] ) self.rpc.connect(password) return self.rpc def disconnect(self): """Disconnect from Metasploit RPC.""" if self.rpc: self.rpc.disconnect() self.rpc = None @property def is_connected(self) -> bool: """Check if connected to MSF.""" return self.rpc is not None and self.rpc.is_connected def autoconnect(self) -> bool: """Perform automatic MSF server detection and connection on startup. Flow: 1. Scan for existing msfrpcd server 2. If found: kill it, ask for new credentials, restart with new creds 3. If not found: ask for credentials, start server 4. Connect to the server Returns: True if successfully connected to MSF """ settings = self.get_settings() print(f"\n{Colors.CYAN}[*] Metasploit Auto-Connect{Colors.RESET}") print(f"{Colors.DIM} {'─' * 40}{Colors.RESET}") # Step 1: Detect existing server print(f"\n{Colors.WHITE} Scanning for existing MSF RPC server...{Colors.RESET}") is_running, pid = self.detect_server() if is_running: print(f"{Colors.YELLOW} [!] Found existing msfrpcd server{Colors.RESET}", end="") if pid: print(f" (PID: {pid})") else: print() # Kill existing server (use sudo in case it was started with sudo) print(f"{Colors.WHITE} Stopping existing server...{Colors.RESET}") if not self.kill_server(use_sudo=True): print(f"{Colors.RED} [X] Failed to stop existing server{Colors.RESET}") print(f"{Colors.DIM} You may need to manually run: sudo pkill -f msfrpcd{Colors.RESET}") return False print(f"{Colors.GREEN} [+] Server stopped{Colors.RESET}") else: print(f"{Colors.DIM} No existing server detected{Colors.RESET}") # Step 2: Ask for credentials print(f"\n{Colors.BOLD} Configure MSF RPC Credentials{Colors.RESET}") print(f"{Colors.DIM} These credentials will be used for the new server{Colors.RESET}\n") try: default_user = settings.get('username', 'msf') default_host = settings.get('host', '127.0.0.1') default_port = settings.get('port', 55553) username = input(f" Username [{default_user}]: ").strip() if not username: username = default_user password = input(f" Password (required): ").strip() if not password: print(f"{Colors.RED} [X] Password is required{Colors.RESET}") return False host_input = input(f" Host [{default_host}]: ").strip() host = host_input if host_input else default_host port_input = input(f" Port [{default_port}]: ").strip() try: port = int(port_input) if port_input else default_port except ValueError: port = default_port ssl_input = input(f" Use SSL (y/n) [y]: ").strip().lower() use_ssl = ssl_input != 'n' # Ask about sudo - default to yes for full module support print(f"\n{Colors.DIM} Note: Running with sudo enables raw socket modules (SYN scan, etc.){Colors.RESET}") sudo_input = input(f" Run with sudo (y/n) [y]: ").strip().lower() use_sudo = sudo_input != 'n' except (EOFError, KeyboardInterrupt): print(f"\n{Colors.YELLOW} [!] Setup cancelled{Colors.RESET}") return False # Save settings self.save_settings(host, port, username, password, use_ssl) # Step 3: Start server if use_sudo: print(f"\n{Colors.WHITE} Starting msfrpcd server with sudo...{Colors.RESET}") print(f"{Colors.DIM} (You may be prompted for your password){Colors.RESET}") else: print(f"\n{Colors.WHITE} Starting msfrpcd server...{Colors.RESET}") if not self.start_server(username, password, host, port, use_ssl, use_sudo): print(f"{Colors.RED} [X] Failed to start msfrpcd server{Colors.RESET}") return False print(f"{Colors.GREEN} [+] Server started on {host}:{port}{Colors.RESET}") # Step 4: Connect print(f"{Colors.WHITE} Connecting to server...{Colors.RESET}") try: self.connect(password) version = self.rpc.get_version() print(f"{Colors.GREEN} [+] Connected to Metasploit {version.get('version', 'Unknown')}{Colors.RESET}") return True except MSFError as e: print(f"{Colors.RED} [X] Connection failed: {e}{Colors.RESET}") return False def set_autoconnect(self, enabled: bool): """Enable or disable autoconnect on startup.""" self._ensure_config_section() self.config.set('msf', 'autoconnect', str(enabled).lower()) self.config.save() # Global MSF manager instance _msf_manager: Optional[MSFManager] = None def get_msf_manager() -> MSFManager: """Get the global MSF manager instance.""" global _msf_manager if _msf_manager is None: _msf_manager = MSFManager() return _msf_manager def msf_startup_autoconnect(skip_if_disabled: bool = True) -> bool: """Perform MSF autoconnect during application startup. This is the main entry point for the autoconnect feature. Call this during application initialization. Args: skip_if_disabled: If True, skip autoconnect if disabled in config Returns: True if connected successfully, False otherwise """ msf = get_msf_manager() settings = msf.get_settings() # Check if autoconnect is enabled if skip_if_disabled and not settings.get('autoconnect', True): print(f"{Colors.DIM} MSF autoconnect disabled in settings{Colors.RESET}") return False # Check if msgpack is available if not MSGPACK_AVAILABLE: print(f"{Colors.YELLOW}[!] msgpack not installed - MSF features disabled{Colors.RESET}") print(f"{Colors.DIM} Install with: pip install msgpack{Colors.RESET}") return False return msf.autoconnect() def msf_quick_connect(username: str = None, password: str = None, host: str = "127.0.0.1", port: int = 55553, use_ssl: bool = True, kill_existing: bool = True, use_sudo: bool = True) -> bool: """Quick non-interactive MSF server setup and connection. Useful for scripting or when credentials are already known. Args: username: RPC username (default: msf) password: RPC password (required) host: Host address port: RPC port use_ssl: Use SSL connection kill_existing: Kill any existing msfrpcd server first use_sudo: Run msfrpcd with sudo (required for raw socket modules) Returns: True if connected successfully """ if not password: print(f"{Colors.RED}[X] Password required for msf_quick_connect{Colors.RESET}") return False if not MSGPACK_AVAILABLE: print(f"{Colors.RED}[X] msgpack not installed{Colors.RESET}") return False username = username or "msf" msf = get_msf_manager() # Kill existing if requested if kill_existing: is_running, _ = msf.detect_server() if is_running: print(f"{Colors.WHITE}[*] Stopping existing msfrpcd...{Colors.RESET}") msf.kill_server(use_sudo=use_sudo) # Save and start msf.save_settings(host, port, username, password, use_ssl) print(f"{Colors.WHITE}[*] Starting msfrpcd{' with sudo' if use_sudo else ''}...{Colors.RESET}") if not msf.start_server(username, password, host, port, use_ssl, use_sudo): return False print(f"{Colors.WHITE}[*] Connecting...{Colors.RESET}") try: msf.connect(password) print(f"{Colors.GREEN}[+] Connected to Metasploit{Colors.RESET}") return True except MSFError as e: print(f"{Colors.RED}[X] Connection failed: {e}{Colors.RESET}") return False