847 lines
27 KiB
Python
847 lines
27 KiB
Python
|
|
"""
|
||
|
|
AUTARCH Metasploit Interface
|
||
|
|
Centralized high-level interface for all Metasploit operations.
|
||
|
|
|
||
|
|
This module provides a clean API for executing MSF modules, handling
|
||
|
|
connection management, output parsing, and error recovery.
|
||
|
|
|
||
|
|
Usage:
|
||
|
|
from core.msf_interface import get_msf_interface, MSFResult
|
||
|
|
|
||
|
|
msf = get_msf_interface()
|
||
|
|
result = msf.run_module('auxiliary/scanner/portscan/tcp', {'RHOSTS': '192.168.1.1'})
|
||
|
|
|
||
|
|
if result.success:
|
||
|
|
for finding in result.findings:
|
||
|
|
print(finding)
|
||
|
|
"""
|
||
|
|
|
||
|
|
import re
|
||
|
|
import time
|
||
|
|
from dataclasses import dataclass, field
|
||
|
|
from typing import Dict, List, Optional, Any, Tuple
|
||
|
|
from enum import Enum
|
||
|
|
|
||
|
|
# Import the low-level MSF components
|
||
|
|
from core.msf import get_msf_manager, MSFError, MSFManager
|
||
|
|
from core.banner import Colors
|
||
|
|
|
||
|
|
|
||
|
|
class MSFStatus(Enum):
|
||
|
|
"""Status of an MSF operation."""
|
||
|
|
SUCCESS = "success"
|
||
|
|
PARTIAL = "partial" # Some results but also errors
|
||
|
|
FAILED = "failed"
|
||
|
|
AUTH_ERROR = "auth_error"
|
||
|
|
CONNECTION_ERROR = "connection_error"
|
||
|
|
TIMEOUT = "timeout"
|
||
|
|
NOT_CONNECTED = "not_connected"
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class MSFResult:
|
||
|
|
"""Result from an MSF module execution."""
|
||
|
|
status: MSFStatus
|
||
|
|
module: str
|
||
|
|
target: str = ""
|
||
|
|
|
||
|
|
# Raw and cleaned output
|
||
|
|
raw_output: str = ""
|
||
|
|
cleaned_output: str = ""
|
||
|
|
|
||
|
|
# Parsed results
|
||
|
|
findings: List[str] = field(default_factory=list) # [+] lines
|
||
|
|
info: List[str] = field(default_factory=list) # [*] lines
|
||
|
|
errors: List[str] = field(default_factory=list) # [-] lines
|
||
|
|
warnings: List[str] = field(default_factory=list) # [!] lines
|
||
|
|
|
||
|
|
# For scan results
|
||
|
|
open_ports: List[Dict] = field(default_factory=list) # [{port, service, state}]
|
||
|
|
services: List[Dict] = field(default_factory=list) # [{name, version, info}]
|
||
|
|
|
||
|
|
# Metadata
|
||
|
|
execution_time: float = 0.0
|
||
|
|
error_count: int = 0
|
||
|
|
|
||
|
|
@property
|
||
|
|
def success(self) -> bool:
|
||
|
|
return self.status in (MSFStatus.SUCCESS, MSFStatus.PARTIAL)
|
||
|
|
|
||
|
|
def get_summary(self) -> str:
|
||
|
|
"""Get a brief summary of the result."""
|
||
|
|
if self.status == MSFStatus.SUCCESS:
|
||
|
|
return f"Success: {len(self.findings)} findings"
|
||
|
|
elif self.status == MSFStatus.PARTIAL:
|
||
|
|
return f"Partial: {len(self.findings)} findings, {self.error_count} errors"
|
||
|
|
elif self.status == MSFStatus.AUTH_ERROR:
|
||
|
|
return "Authentication token expired"
|
||
|
|
elif self.status == MSFStatus.CONNECTION_ERROR:
|
||
|
|
return "Connection to MSF failed"
|
||
|
|
elif self.status == MSFStatus.TIMEOUT:
|
||
|
|
return "Module execution timed out"
|
||
|
|
else:
|
||
|
|
return f"Failed: {self.errors[0] if self.errors else 'Unknown error'}"
|
||
|
|
|
||
|
|
|
||
|
|
class MSFInterface:
|
||
|
|
"""High-level interface for Metasploit operations."""
|
||
|
|
|
||
|
|
# Patterns to filter from output (banner noise, Easter eggs, etc.)
|
||
|
|
SKIP_PATTERNS = [
|
||
|
|
'metasploit', '=[ ', '+ -- --=[', 'Documentation:',
|
||
|
|
'Rapid7', 'Open Source', 'MAGIC WORD', 'PERMISSION DENIED',
|
||
|
|
'access security', 'access:', 'Ready...', 'Alpha E',
|
||
|
|
'Version 4.0', 'System Security Interface', 'Metasploit Park',
|
||
|
|
'exploits -', 'auxiliary -', 'payloads', 'encoders -',
|
||
|
|
'evasion', 'nops -', 'post -', 'msf6', 'msf5', 'msf >',
|
||
|
|
]
|
||
|
|
|
||
|
|
# Patterns indicating specific result types
|
||
|
|
PORT_PATTERN = re.compile(
|
||
|
|
r'(\d{1,5})/(tcp|udp)\s+(open|closed|filtered)?\s*(\S+)?',
|
||
|
|
re.IGNORECASE
|
||
|
|
)
|
||
|
|
SERVICE_PATTERN = re.compile(
|
||
|
|
r'\[\+\].*?(\d+\.\d+\.\d+\.\d+):(\d+)\s*[-:]\s*(.+)',
|
||
|
|
re.IGNORECASE
|
||
|
|
)
|
||
|
|
VERSION_PATTERN = re.compile(
|
||
|
|
r'(?:version|running|server)[\s:]+([^\n\r]+)',
|
||
|
|
re.IGNORECASE
|
||
|
|
)
|
||
|
|
|
||
|
|
def __init__(self):
|
||
|
|
self._manager: Optional[MSFManager] = None
|
||
|
|
self._last_error: Optional[str] = None
|
||
|
|
|
||
|
|
@property
|
||
|
|
def manager(self) -> MSFManager:
|
||
|
|
"""Get or create the MSF manager."""
|
||
|
|
if self._manager is None:
|
||
|
|
self._manager = get_msf_manager()
|
||
|
|
return self._manager
|
||
|
|
|
||
|
|
@property
|
||
|
|
def is_connected(self) -> bool:
|
||
|
|
"""Check if connected to MSF RPC."""
|
||
|
|
return self.manager.is_connected
|
||
|
|
|
||
|
|
@property
|
||
|
|
def last_error(self) -> Optional[str]:
|
||
|
|
"""Get the last error message."""
|
||
|
|
return self._last_error
|
||
|
|
|
||
|
|
def ensure_connected(self, password: str = None, auto_prompt: bool = True) -> Tuple[bool, str]:
|
||
|
|
"""Ensure we have a valid connection to MSF RPC.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
password: Optional password to use for connection.
|
||
|
|
auto_prompt: If True, prompt for password if needed.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Tuple of (success, message).
|
||
|
|
"""
|
||
|
|
# Check if already connected
|
||
|
|
if self.is_connected:
|
||
|
|
# Verify the connection is actually valid with a test request
|
||
|
|
try:
|
||
|
|
self.manager.rpc.get_version()
|
||
|
|
return True, "Connected"
|
||
|
|
except Exception as e:
|
||
|
|
error_str = str(e)
|
||
|
|
if 'Invalid Authentication Token' in error_str:
|
||
|
|
# Token expired, need to reconnect
|
||
|
|
pass
|
||
|
|
else:
|
||
|
|
self._last_error = error_str
|
||
|
|
return False, f"Connection test failed: {error_str}"
|
||
|
|
|
||
|
|
# Need to connect or reconnect
|
||
|
|
try:
|
||
|
|
# Disconnect existing stale connection
|
||
|
|
if self.manager.rpc:
|
||
|
|
try:
|
||
|
|
self.manager.rpc.disconnect()
|
||
|
|
except:
|
||
|
|
pass
|
||
|
|
|
||
|
|
# Get password from settings or parameter
|
||
|
|
settings = self.manager.get_settings()
|
||
|
|
connect_password = password or settings.get('password')
|
||
|
|
|
||
|
|
if not connect_password and auto_prompt:
|
||
|
|
print(f"{Colors.YELLOW}[!] MSF RPC password required{Colors.RESET}")
|
||
|
|
connect_password = input(f" Password: ").strip()
|
||
|
|
|
||
|
|
if not connect_password:
|
||
|
|
self._last_error = "No password provided"
|
||
|
|
return False, "No password provided"
|
||
|
|
|
||
|
|
# Connect
|
||
|
|
self.manager.connect(connect_password)
|
||
|
|
return True, "Connected successfully"
|
||
|
|
|
||
|
|
except MSFError as e:
|
||
|
|
self._last_error = str(e)
|
||
|
|
return False, f"MSF Error: {e}"
|
||
|
|
except Exception as e:
|
||
|
|
self._last_error = str(e)
|
||
|
|
return False, f"Connection failed: {e}"
|
||
|
|
|
||
|
|
def _run_console_command(self, commands: str, timeout: int = 120) -> Tuple[str, Optional[str]]:
|
||
|
|
"""Execute commands via MSF console and capture output.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
commands: Newline-separated commands to run.
|
||
|
|
timeout: Maximum wait time in seconds.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Tuple of (output, error_message).
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
# Create console
|
||
|
|
console = self.manager.rpc._request("console.create")
|
||
|
|
console_id = console.get("id")
|
||
|
|
|
||
|
|
if not console_id:
|
||
|
|
return "", "Failed to create console"
|
||
|
|
|
||
|
|
try:
|
||
|
|
# Wait for console to initialize and consume banner
|
||
|
|
time.sleep(2)
|
||
|
|
self.manager.rpc._request("console.read", [console_id])
|
||
|
|
|
||
|
|
# Send commands one at a time
|
||
|
|
for cmd in commands.strip().split('\n'):
|
||
|
|
cmd = cmd.strip()
|
||
|
|
if cmd:
|
||
|
|
self.manager.rpc._request("console.write", [console_id, cmd + "\n"])
|
||
|
|
time.sleep(0.3)
|
||
|
|
|
||
|
|
# Collect output
|
||
|
|
output = ""
|
||
|
|
waited = 0
|
||
|
|
idle_count = 0
|
||
|
|
|
||
|
|
while waited < timeout:
|
||
|
|
time.sleep(1)
|
||
|
|
waited += 1
|
||
|
|
|
||
|
|
result = self.manager.rpc._request("console.read", [console_id])
|
||
|
|
new_data = result.get("data", "")
|
||
|
|
|
||
|
|
if new_data:
|
||
|
|
output += new_data
|
||
|
|
idle_count = 0
|
||
|
|
else:
|
||
|
|
idle_count += 1
|
||
|
|
|
||
|
|
# Stop if not busy and idle for 3+ seconds
|
||
|
|
if not result.get("busy", False) and idle_count >= 3:
|
||
|
|
break
|
||
|
|
|
||
|
|
# Check for timeout
|
||
|
|
if waited >= timeout:
|
||
|
|
return output, "Execution timed out"
|
||
|
|
|
||
|
|
return output, None
|
||
|
|
|
||
|
|
finally:
|
||
|
|
# Clean up console
|
||
|
|
try:
|
||
|
|
self.manager.rpc._request("console.destroy", [console_id])
|
||
|
|
except:
|
||
|
|
pass
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
error_str = str(e)
|
||
|
|
if 'Invalid Authentication Token' in error_str:
|
||
|
|
return "", "AUTH_ERROR"
|
||
|
|
return "", f"Console error: {e}"
|
||
|
|
|
||
|
|
def _clean_output(self, raw_output: str) -> str:
|
||
|
|
"""Remove banner noise and clean up MSF output.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
raw_output: Raw console output.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Cleaned output string.
|
||
|
|
"""
|
||
|
|
lines = []
|
||
|
|
for line in raw_output.split('\n'):
|
||
|
|
line_stripped = line.strip()
|
||
|
|
|
||
|
|
# Skip empty lines
|
||
|
|
if not line_stripped:
|
||
|
|
continue
|
||
|
|
|
||
|
|
# Skip banner/noise patterns
|
||
|
|
skip = False
|
||
|
|
for pattern in self.SKIP_PATTERNS:
|
||
|
|
if pattern.lower() in line_stripped.lower():
|
||
|
|
skip = True
|
||
|
|
break
|
||
|
|
|
||
|
|
if skip:
|
||
|
|
continue
|
||
|
|
|
||
|
|
# Skip prompt lines
|
||
|
|
if line_stripped.startswith('>') and len(line_stripped) < 5:
|
||
|
|
continue
|
||
|
|
|
||
|
|
# Skip set confirmations (we already show these)
|
||
|
|
if ' => ' in line_stripped and any(
|
||
|
|
line_stripped.startswith(opt) for opt in
|
||
|
|
['RHOSTS', 'RHOST', 'PORTS', 'LHOST', 'LPORT', 'THREADS']
|
||
|
|
):
|
||
|
|
continue
|
||
|
|
|
||
|
|
lines.append(line)
|
||
|
|
|
||
|
|
return '\n'.join(lines)
|
||
|
|
|
||
|
|
def _parse_output(self, cleaned_output: str, module_path: str) -> Dict[str, Any]:
|
||
|
|
"""Parse cleaned output into structured data.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
cleaned_output: Cleaned console output.
|
||
|
|
module_path: The module that was run (for context).
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Dictionary with parsed results.
|
||
|
|
"""
|
||
|
|
result = {
|
||
|
|
'findings': [],
|
||
|
|
'info': [],
|
||
|
|
'errors': [],
|
||
|
|
'warnings': [],
|
||
|
|
'open_ports': [],
|
||
|
|
'services': [],
|
||
|
|
'error_count': 0,
|
||
|
|
}
|
||
|
|
|
||
|
|
is_scanner = 'scanner' in module_path.lower()
|
||
|
|
is_portscan = 'portscan' in module_path.lower()
|
||
|
|
|
||
|
|
for line in cleaned_output.split('\n'):
|
||
|
|
line_stripped = line.strip()
|
||
|
|
|
||
|
|
# Categorize by prefix
|
||
|
|
if '[+]' in line:
|
||
|
|
result['findings'].append(line_stripped)
|
||
|
|
|
||
|
|
# Try to extract port/service info from scanner results
|
||
|
|
if is_scanner:
|
||
|
|
# Look for IP:port patterns
|
||
|
|
service_match = self.SERVICE_PATTERN.search(line)
|
||
|
|
if service_match:
|
||
|
|
ip, port, info = service_match.groups()
|
||
|
|
result['services'].append({
|
||
|
|
'ip': ip,
|
||
|
|
'port': int(port),
|
||
|
|
'info': info.strip()
|
||
|
|
})
|
||
|
|
|
||
|
|
# Look for "open" port mentions
|
||
|
|
if is_portscan and 'open' in line.lower():
|
||
|
|
port_match = re.search(r':(\d+)\s', line)
|
||
|
|
if port_match:
|
||
|
|
result['open_ports'].append({
|
||
|
|
'port': int(port_match.group(1)),
|
||
|
|
'state': 'open'
|
||
|
|
})
|
||
|
|
|
||
|
|
elif '[-]' in line or 'Error:' in line:
|
||
|
|
# Count NoMethodError and similar spam but don't store each one
|
||
|
|
if 'NoMethodError' in line or 'undefined method' in line:
|
||
|
|
result['error_count'] += 1
|
||
|
|
else:
|
||
|
|
result['errors'].append(line_stripped)
|
||
|
|
|
||
|
|
elif '[!]' in line:
|
||
|
|
result['warnings'].append(line_stripped)
|
||
|
|
|
||
|
|
elif '[*]' in line:
|
||
|
|
result['info'].append(line_stripped)
|
||
|
|
|
||
|
|
return result
|
||
|
|
|
||
|
|
def run_module(
|
||
|
|
self,
|
||
|
|
module_path: str,
|
||
|
|
options: Dict[str, Any] = None,
|
||
|
|
timeout: int = 120,
|
||
|
|
auto_reconnect: bool = True
|
||
|
|
) -> MSFResult:
|
||
|
|
"""Execute an MSF module and return parsed results.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
module_path: Full module path (e.g., 'auxiliary/scanner/portscan/tcp').
|
||
|
|
options: Module options dictionary.
|
||
|
|
timeout: Maximum execution time in seconds.
|
||
|
|
auto_reconnect: If True, attempt to reconnect on auth errors.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
MSFResult with parsed output.
|
||
|
|
"""
|
||
|
|
options = options or {}
|
||
|
|
target = options.get('RHOSTS', options.get('RHOST', ''))
|
||
|
|
start_time = time.time()
|
||
|
|
|
||
|
|
# Ensure connected
|
||
|
|
connected, msg = self.ensure_connected()
|
||
|
|
if not connected:
|
||
|
|
return MSFResult(
|
||
|
|
status=MSFStatus.NOT_CONNECTED,
|
||
|
|
module=module_path,
|
||
|
|
target=target,
|
||
|
|
errors=[msg]
|
||
|
|
)
|
||
|
|
|
||
|
|
# Build console commands
|
||
|
|
commands = f"use {module_path}\n"
|
||
|
|
for key, value in options.items():
|
||
|
|
commands += f"set {key} {value}\n"
|
||
|
|
commands += "run"
|
||
|
|
|
||
|
|
# Execute
|
||
|
|
raw_output, error = self._run_console_command(commands, timeout)
|
||
|
|
|
||
|
|
# Handle auth error with reconnect
|
||
|
|
if error == "AUTH_ERROR" and auto_reconnect:
|
||
|
|
connected, msg = self.ensure_connected()
|
||
|
|
if connected:
|
||
|
|
raw_output, error = self._run_console_command(commands, timeout)
|
||
|
|
else:
|
||
|
|
return MSFResult(
|
||
|
|
status=MSFStatus.AUTH_ERROR,
|
||
|
|
module=module_path,
|
||
|
|
target=target,
|
||
|
|
errors=["Session expired and reconnection failed"]
|
||
|
|
)
|
||
|
|
|
||
|
|
# Handle other errors
|
||
|
|
if error and error != "AUTH_ERROR":
|
||
|
|
if "timed out" in error.lower():
|
||
|
|
status = MSFStatus.TIMEOUT
|
||
|
|
else:
|
||
|
|
status = MSFStatus.FAILED
|
||
|
|
return MSFResult(
|
||
|
|
status=status,
|
||
|
|
module=module_path,
|
||
|
|
target=target,
|
||
|
|
raw_output=raw_output,
|
||
|
|
errors=[error]
|
||
|
|
)
|
||
|
|
|
||
|
|
# Clean and parse output
|
||
|
|
cleaned = self._clean_output(raw_output)
|
||
|
|
parsed = self._parse_output(cleaned, module_path)
|
||
|
|
|
||
|
|
execution_time = time.time() - start_time
|
||
|
|
|
||
|
|
# Determine status
|
||
|
|
if parsed['error_count'] > 0 and not parsed['findings']:
|
||
|
|
status = MSFStatus.FAILED
|
||
|
|
elif parsed['error_count'] > 0:
|
||
|
|
status = MSFStatus.PARTIAL
|
||
|
|
elif parsed['findings'] or parsed['info']:
|
||
|
|
status = MSFStatus.SUCCESS
|
||
|
|
else:
|
||
|
|
status = MSFStatus.SUCCESS # No output isn't necessarily an error
|
||
|
|
|
||
|
|
return MSFResult(
|
||
|
|
status=status,
|
||
|
|
module=module_path,
|
||
|
|
target=target,
|
||
|
|
raw_output=raw_output,
|
||
|
|
cleaned_output=cleaned,
|
||
|
|
findings=parsed['findings'],
|
||
|
|
info=parsed['info'],
|
||
|
|
errors=parsed['errors'],
|
||
|
|
warnings=parsed['warnings'],
|
||
|
|
open_ports=parsed['open_ports'],
|
||
|
|
services=parsed['services'],
|
||
|
|
execution_time=execution_time,
|
||
|
|
error_count=parsed['error_count']
|
||
|
|
)
|
||
|
|
|
||
|
|
def run_scanner(
|
||
|
|
self,
|
||
|
|
module_path: str,
|
||
|
|
target: str,
|
||
|
|
ports: str = None,
|
||
|
|
options: Dict[str, Any] = None,
|
||
|
|
timeout: int = 120
|
||
|
|
) -> MSFResult:
|
||
|
|
"""Convenience method for running scanner modules.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
module_path: Scanner module path.
|
||
|
|
target: Target IP or range (RHOSTS).
|
||
|
|
ports: Port specification (optional).
|
||
|
|
options: Additional options.
|
||
|
|
timeout: Maximum execution time.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
MSFResult with scan results.
|
||
|
|
"""
|
||
|
|
opts = {'RHOSTS': target}
|
||
|
|
if ports:
|
||
|
|
opts['PORTS'] = ports
|
||
|
|
if options:
|
||
|
|
opts.update(options)
|
||
|
|
|
||
|
|
return self.run_module(module_path, opts, timeout)
|
||
|
|
|
||
|
|
def get_module_info(self, module_path: str) -> Optional[Dict[str, Any]]:
|
||
|
|
"""Get information about a module.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
module_path: Full module path.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Module info dictionary or None.
|
||
|
|
"""
|
||
|
|
connected, _ = self.ensure_connected(auto_prompt=False)
|
||
|
|
if not connected:
|
||
|
|
return None
|
||
|
|
|
||
|
|
try:
|
||
|
|
# Determine module type from path
|
||
|
|
parts = module_path.split('/')
|
||
|
|
if len(parts) < 2:
|
||
|
|
return None
|
||
|
|
|
||
|
|
module_type = parts[0]
|
||
|
|
module_name = '/'.join(parts[1:])
|
||
|
|
|
||
|
|
info = self.manager.rpc.get_module_info(module_type, module_name)
|
||
|
|
return {
|
||
|
|
'name': info.name,
|
||
|
|
'description': info.description,
|
||
|
|
'author': info.author,
|
||
|
|
'type': info.type,
|
||
|
|
'rank': info.rank,
|
||
|
|
'references': info.references
|
||
|
|
}
|
||
|
|
except Exception as e:
|
||
|
|
self._last_error = str(e)
|
||
|
|
return None
|
||
|
|
|
||
|
|
def get_module_options(self, module_path: str) -> Optional[Dict[str, Any]]:
|
||
|
|
"""Get available options for a module.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
module_path: Full module path.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Options dictionary or None.
|
||
|
|
"""
|
||
|
|
connected, _ = self.ensure_connected(auto_prompt=False)
|
||
|
|
if not connected:
|
||
|
|
return None
|
||
|
|
|
||
|
|
try:
|
||
|
|
parts = module_path.split('/')
|
||
|
|
if len(parts) < 2:
|
||
|
|
return None
|
||
|
|
|
||
|
|
module_type = parts[0]
|
||
|
|
module_name = '/'.join(parts[1:])
|
||
|
|
|
||
|
|
return self.manager.rpc.get_module_options(module_type, module_name)
|
||
|
|
except Exception as e:
|
||
|
|
self._last_error = str(e)
|
||
|
|
return None
|
||
|
|
|
||
|
|
def search_modules(self, query: str) -> List[str]:
|
||
|
|
"""Search for modules matching a query.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
query: Search query.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
List of matching module paths.
|
||
|
|
"""
|
||
|
|
connected, _ = self.ensure_connected(auto_prompt=False)
|
||
|
|
if not connected:
|
||
|
|
return []
|
||
|
|
|
||
|
|
try:
|
||
|
|
results = self.manager.rpc.search_modules(query)
|
||
|
|
# Results are typically dicts with 'fullname' key
|
||
|
|
if isinstance(results, list):
|
||
|
|
return [r.get('fullname', r) if isinstance(r, dict) else str(r) for r in results]
|
||
|
|
return []
|
||
|
|
except Exception as e:
|
||
|
|
self._last_error = str(e)
|
||
|
|
return []
|
||
|
|
|
||
|
|
def list_modules(self, module_type: str = None) -> List[str]:
|
||
|
|
"""List available modules by type.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
module_type: Filter by type (exploit, auxiliary, post, payload, encoder, nop).
|
||
|
|
If None, returns all modules.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
List of module paths.
|
||
|
|
"""
|
||
|
|
connected, _ = self.ensure_connected(auto_prompt=False)
|
||
|
|
if not connected:
|
||
|
|
return []
|
||
|
|
|
||
|
|
try:
|
||
|
|
return self.manager.rpc.list_modules(module_type)
|
||
|
|
except Exception as e:
|
||
|
|
self._last_error = str(e)
|
||
|
|
return []
|
||
|
|
|
||
|
|
def list_sessions(self) -> Dict[str, Any]:
|
||
|
|
"""List active MSF sessions.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Dictionary of session IDs to session info.
|
||
|
|
"""
|
||
|
|
connected, _ = self.ensure_connected(auto_prompt=False)
|
||
|
|
if not connected:
|
||
|
|
return {}
|
||
|
|
|
||
|
|
try:
|
||
|
|
return self.manager.rpc.list_sessions()
|
||
|
|
except Exception as e:
|
||
|
|
self._last_error = str(e)
|
||
|
|
return {}
|
||
|
|
|
||
|
|
def list_jobs(self) -> Dict[str, Any]:
|
||
|
|
"""List running MSF jobs.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Dictionary of job IDs to job info.
|
||
|
|
"""
|
||
|
|
connected, _ = self.ensure_connected(auto_prompt=False)
|
||
|
|
if not connected:
|
||
|
|
return {}
|
||
|
|
|
||
|
|
try:
|
||
|
|
return self.manager.rpc.list_jobs()
|
||
|
|
except Exception as e:
|
||
|
|
self._last_error = str(e)
|
||
|
|
return {}
|
||
|
|
|
||
|
|
def stop_job(self, job_id: str) -> bool:
|
||
|
|
"""Stop a running job.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
job_id: Job ID to stop.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
True if stopped successfully.
|
||
|
|
"""
|
||
|
|
connected, _ = self.ensure_connected(auto_prompt=False)
|
||
|
|
if not connected:
|
||
|
|
return False
|
||
|
|
|
||
|
|
try:
|
||
|
|
return self.manager.rpc.stop_job(job_id)
|
||
|
|
except Exception as e:
|
||
|
|
self._last_error = str(e)
|
||
|
|
return False
|
||
|
|
|
||
|
|
def execute_module_job(
|
||
|
|
self,
|
||
|
|
module_path: str,
|
||
|
|
options: Dict[str, Any] = None
|
||
|
|
) -> Tuple[bool, Optional[str], Optional[str]]:
|
||
|
|
"""Execute a module as a background job (non-blocking).
|
||
|
|
|
||
|
|
This is different from run_module() which uses console and captures output.
|
||
|
|
Use this for exploits and long-running modules that should run in background.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
module_path: Full module path.
|
||
|
|
options: Module options.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Tuple of (success, job_id, error_message).
|
||
|
|
"""
|
||
|
|
connected, msg = self.ensure_connected()
|
||
|
|
if not connected:
|
||
|
|
return False, None, msg
|
||
|
|
|
||
|
|
try:
|
||
|
|
parts = module_path.split('/')
|
||
|
|
if len(parts) < 2:
|
||
|
|
return False, None, "Invalid module path"
|
||
|
|
|
||
|
|
module_type = parts[0]
|
||
|
|
module_name = '/'.join(parts[1:])
|
||
|
|
|
||
|
|
result = self.manager.rpc.execute_module(module_type, module_name, options or {})
|
||
|
|
|
||
|
|
job_id = result.get('job_id')
|
||
|
|
if job_id is not None:
|
||
|
|
return True, str(job_id), None
|
||
|
|
else:
|
||
|
|
# Check for error in result
|
||
|
|
error = result.get('error_message') or result.get('error') or "Unknown error"
|
||
|
|
return False, None, str(error)
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
self._last_error = str(e)
|
||
|
|
return False, None, str(e)
|
||
|
|
|
||
|
|
def session_read(self, session_id: str) -> Tuple[bool, str]:
|
||
|
|
"""Read from a session shell.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
session_id: Session ID.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Tuple of (success, output).
|
||
|
|
"""
|
||
|
|
connected, _ = self.ensure_connected(auto_prompt=False)
|
||
|
|
if not connected:
|
||
|
|
return False, ""
|
||
|
|
|
||
|
|
try:
|
||
|
|
output = self.manager.rpc.session_shell_read(session_id)
|
||
|
|
return True, output
|
||
|
|
except Exception as e:
|
||
|
|
self._last_error = str(e)
|
||
|
|
return False, ""
|
||
|
|
|
||
|
|
def session_write(self, session_id: str, command: str) -> bool:
|
||
|
|
"""Write a command to a session shell.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
session_id: Session ID.
|
||
|
|
command: Command to execute.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
True if written successfully.
|
||
|
|
"""
|
||
|
|
connected, _ = self.ensure_connected(auto_prompt=False)
|
||
|
|
if not connected:
|
||
|
|
return False
|
||
|
|
|
||
|
|
try:
|
||
|
|
return self.manager.rpc.session_shell_write(session_id, command)
|
||
|
|
except Exception as e:
|
||
|
|
self._last_error = str(e)
|
||
|
|
return False
|
||
|
|
|
||
|
|
def session_stop(self, session_id: str) -> bool:
|
||
|
|
"""Stop/kill a session.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
session_id: Session ID.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
True if stopped successfully.
|
||
|
|
"""
|
||
|
|
connected, _ = self.ensure_connected(auto_prompt=False)
|
||
|
|
if not connected:
|
||
|
|
return False
|
||
|
|
|
||
|
|
try:
|
||
|
|
return self.manager.rpc.session_stop(session_id)
|
||
|
|
except Exception as e:
|
||
|
|
self._last_error = str(e)
|
||
|
|
return False
|
||
|
|
|
||
|
|
def run_console_command(self, command: str, timeout: int = 30) -> Tuple[bool, str]:
|
||
|
|
"""Run a raw console command and return output.
|
||
|
|
|
||
|
|
This is a lower-level method for direct console access.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
command: Console command to run.
|
||
|
|
timeout: Timeout in seconds.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Tuple of (success, output).
|
||
|
|
"""
|
||
|
|
connected, msg = self.ensure_connected()
|
||
|
|
if not connected:
|
||
|
|
return False, msg
|
||
|
|
|
||
|
|
try:
|
||
|
|
output = self.manager.rpc.run_console_command(command, timeout=timeout)
|
||
|
|
return True, output
|
||
|
|
except Exception as e:
|
||
|
|
self._last_error = str(e)
|
||
|
|
return False, str(e)
|
||
|
|
|
||
|
|
def print_result(self, result: MSFResult, verbose: bool = False):
|
||
|
|
"""Print a formatted result to the console.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
result: MSFResult to print.
|
||
|
|
verbose: If True, show all output including info lines.
|
||
|
|
"""
|
||
|
|
print(f"\n{Colors.CYAN}Module Output:{Colors.RESET}")
|
||
|
|
print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}")
|
||
|
|
|
||
|
|
if result.status == MSFStatus.NOT_CONNECTED:
|
||
|
|
print(f" {Colors.RED}[X] Not connected to Metasploit{Colors.RESET}")
|
||
|
|
if result.errors:
|
||
|
|
print(f" {result.errors[0]}")
|
||
|
|
elif result.status == MSFStatus.AUTH_ERROR:
|
||
|
|
print(f" {Colors.RED}[X] Authentication failed{Colors.RESET}")
|
||
|
|
elif result.status == MSFStatus.TIMEOUT:
|
||
|
|
print(f" {Colors.YELLOW}[!] Execution timed out{Colors.RESET}")
|
||
|
|
else:
|
||
|
|
# Print findings (green)
|
||
|
|
for line in result.findings:
|
||
|
|
print(f" {Colors.GREEN}{line}{Colors.RESET}")
|
||
|
|
|
||
|
|
# Print info (cyan) - only in verbose mode
|
||
|
|
if verbose:
|
||
|
|
for line in result.info:
|
||
|
|
print(f" {Colors.CYAN}{line}{Colors.RESET}")
|
||
|
|
|
||
|
|
# Print warnings (yellow)
|
||
|
|
for line in result.warnings:
|
||
|
|
print(f" {Colors.YELLOW}{line}{Colors.RESET}")
|
||
|
|
|
||
|
|
# Print errors (dim)
|
||
|
|
for line in result.errors:
|
||
|
|
print(f" {Colors.DIM}{line}{Colors.RESET}")
|
||
|
|
|
||
|
|
# Summarize error count if high
|
||
|
|
if result.error_count > 0:
|
||
|
|
print(f"\n {Colors.YELLOW}[!] {result.error_count} errors occurred during execution{Colors.RESET}")
|
||
|
|
|
||
|
|
print(f"{Colors.DIM}{'─' * 50}{Colors.RESET}")
|
||
|
|
|
||
|
|
# Print summary
|
||
|
|
if result.execution_time > 0:
|
||
|
|
print(f" {Colors.DIM}Time: {result.execution_time:.1f}s{Colors.RESET}")
|
||
|
|
print(f" {Colors.DIM}Status: {result.get_summary()}{Colors.RESET}")
|
||
|
|
|
||
|
|
# Print parsed port/service info if available
|
||
|
|
if result.open_ports:
|
||
|
|
print(f"\n {Colors.GREEN}Open Ports:{Colors.RESET}")
|
||
|
|
for port_info in result.open_ports:
|
||
|
|
print(f" {port_info['port']}/tcp - {port_info.get('state', 'open')}")
|
||
|
|
|
||
|
|
if result.services:
|
||
|
|
print(f"\n {Colors.GREEN}Services Detected:{Colors.RESET}")
|
||
|
|
for svc in result.services:
|
||
|
|
print(f" {svc['ip']}:{svc['port']} - {svc['info']}")
|
||
|
|
|
||
|
|
|
||
|
|
# Global instance
|
||
|
|
_msf_interface: Optional[MSFInterface] = None
|
||
|
|
|
||
|
|
|
||
|
|
def get_msf_interface() -> MSFInterface:
|
||
|
|
"""Get the global MSF interface instance."""
|
||
|
|
global _msf_interface
|
||
|
|
if _msf_interface is None:
|
||
|
|
_msf_interface = MSFInterface()
|
||
|
|
return _msf_interface
|