1528 lines
60 KiB
Python
1528 lines
60 KiB
Python
|
|
"""
|
||
|
|
AUTARCH Metasploit Module
|
||
|
|
Enhanced interface for Metasploit Framework with module browser.
|
||
|
|
|
||
|
|
Provides easy access to MSF modules, exploits, and sessions.
|
||
|
|
Uses the centralized MSF interface from core/msf_interface.py.
|
||
|
|
Integrates with msf_terms.py and msf_modules.py for descriptions.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import sys
|
||
|
|
import os
|
||
|
|
import re
|
||
|
|
import json
|
||
|
|
import time
|
||
|
|
import socket
|
||
|
|
from pathlib import Path
|
||
|
|
from typing import Dict, List, Optional, Any
|
||
|
|
|
||
|
|
# Module metadata
|
||
|
|
DESCRIPTION = "Metasploit Framework interface"
|
||
|
|
AUTHOR = "darkHal"
|
||
|
|
VERSION = "2.0"
|
||
|
|
CATEGORY = "offense"
|
||
|
|
|
||
|
|
# Add parent directory to path
|
||
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||
|
|
|
||
|
|
from core.msf_interface import get_msf_interface
|
||
|
|
from core.banner import Colors, clear_screen, display_banner
|
||
|
|
from core.msf_terms import get_setting_info, get_setting_prompt, format_setting_help, validate_setting_value
|
||
|
|
from core.msf_modules import (
|
||
|
|
get_module_info as get_library_module_info,
|
||
|
|
get_module_description,
|
||
|
|
search_modules as library_search_modules,
|
||
|
|
get_modules_by_type,
|
||
|
|
format_module_help,
|
||
|
|
MSF_MODULES
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
class MSFMenu:
|
||
|
|
"""Enhanced Metasploit menu interface with module browser."""
|
||
|
|
|
||
|
|
# Module categories for browsing
|
||
|
|
MODULE_CATEGORIES = {
|
||
|
|
'scanners': {
|
||
|
|
'name': 'Scanners',
|
||
|
|
'description': 'Network and vulnerability scanners',
|
||
|
|
'types': ['auxiliary/scanner'],
|
||
|
|
'color': Colors.CYAN
|
||
|
|
},
|
||
|
|
'exploits': {
|
||
|
|
'name': 'Exploits',
|
||
|
|
'description': 'Remote and local exploits',
|
||
|
|
'types': ['exploit'],
|
||
|
|
'color': Colors.RED
|
||
|
|
},
|
||
|
|
'post': {
|
||
|
|
'name': 'Post-Exploitation',
|
||
|
|
'description': 'Post-exploitation modules',
|
||
|
|
'types': ['post'],
|
||
|
|
'color': Colors.MAGENTA
|
||
|
|
},
|
||
|
|
'payloads': {
|
||
|
|
'name': 'Payloads',
|
||
|
|
'description': 'Payload generators',
|
||
|
|
'types': ['payload'],
|
||
|
|
'color': Colors.YELLOW
|
||
|
|
},
|
||
|
|
'auxiliary': {
|
||
|
|
'name': 'Auxiliary',
|
||
|
|
'description': 'Other auxiliary modules',
|
||
|
|
'types': ['auxiliary'],
|
||
|
|
'color': Colors.GREEN
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
def __init__(self):
|
||
|
|
self.msf = get_msf_interface()
|
||
|
|
self.current_module = None
|
||
|
|
self.current_module_type = None
|
||
|
|
self.module_options = {}
|
||
|
|
|
||
|
|
# Global target settings - persist across module selections
|
||
|
|
self.global_settings = {
|
||
|
|
'RHOSTS': '',
|
||
|
|
'LHOST': '',
|
||
|
|
'LPORT': '4444',
|
||
|
|
}
|
||
|
|
|
||
|
|
def print_status(self, message: str, status: str = "info"):
|
||
|
|
"""Print a status message."""
|
||
|
|
colors = {"info": Colors.CYAN, "success": Colors.GREEN, "warning": Colors.YELLOW, "error": Colors.RED}
|
||
|
|
symbols = {"info": "*", "success": "+", "warning": "!", "error": "X"}
|
||
|
|
print(f"{colors.get(status, Colors.WHITE)}[{symbols.get(status, '*')}] {message}{Colors.RESET}")
|
||
|
|
|
||
|
|
def wrap_text(self, text: str, width: int = 60, indent: str = " ") -> str:
|
||
|
|
"""Word-wrap text with indent for subsequent lines."""
|
||
|
|
words = text.split()
|
||
|
|
lines = []
|
||
|
|
current_line = ""
|
||
|
|
|
||
|
|
for word in words:
|
||
|
|
if len(current_line) + len(word) + 1 <= width:
|
||
|
|
current_line += (" " if current_line else "") + word
|
||
|
|
else:
|
||
|
|
if current_line:
|
||
|
|
lines.append(current_line)
|
||
|
|
current_line = word
|
||
|
|
|
||
|
|
if current_line:
|
||
|
|
lines.append(current_line)
|
||
|
|
|
||
|
|
return f"\n{indent}".join(lines)
|
||
|
|
|
||
|
|
def ensure_connected(self) -> bool:
|
||
|
|
"""Ensure connected to MSF RPC."""
|
||
|
|
connected, msg = self.msf.ensure_connected()
|
||
|
|
if not connected:
|
||
|
|
print(f"\n{Colors.YELLOW}{msg}{Colors.RESET}")
|
||
|
|
print(f"{Colors.DIM}Make sure msfrpcd is running: msfrpcd -P <password> -S{Colors.RESET}")
|
||
|
|
return connected
|
||
|
|
|
||
|
|
def resolve_hostname(self, hostname: str) -> Optional[str]:
|
||
|
|
"""Resolve hostname to IP address."""
|
||
|
|
try:
|
||
|
|
# Check if it's already an IP
|
||
|
|
socket.inet_aton(hostname)
|
||
|
|
return hostname
|
||
|
|
except socket.error:
|
||
|
|
pass
|
||
|
|
|
||
|
|
# Try to resolve
|
||
|
|
try:
|
||
|
|
ip = socket.gethostbyname(hostname)
|
||
|
|
return ip
|
||
|
|
except socket.gaierror:
|
||
|
|
return None
|
||
|
|
|
||
|
|
def get_local_ip(self) -> str:
|
||
|
|
"""Get local IP address for LHOST."""
|
||
|
|
try:
|
||
|
|
# Connect to external address to get local IP
|
||
|
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||
|
|
s.connect(("8.8.8.8", 80))
|
||
|
|
local_ip = s.getsockname()[0]
|
||
|
|
s.close()
|
||
|
|
return local_ip
|
||
|
|
except Exception:
|
||
|
|
return "127.0.0.1"
|
||
|
|
|
||
|
|
# =========================================================================
|
||
|
|
# MAIN MENU
|
||
|
|
# =========================================================================
|
||
|
|
|
||
|
|
def show_main_menu(self):
|
||
|
|
"""Display MSF main menu."""
|
||
|
|
clear_screen()
|
||
|
|
display_banner()
|
||
|
|
|
||
|
|
print(f"{Colors.RED}{Colors.BOLD} Metasploit Framework{Colors.RESET}")
|
||
|
|
print(f"{Colors.DIM} {'─' * 50}{Colors.RESET}")
|
||
|
|
|
||
|
|
# Connection status
|
||
|
|
if self.msf.is_connected:
|
||
|
|
print(f" {Colors.GREEN}Status: Connected{Colors.RESET}")
|
||
|
|
else:
|
||
|
|
print(f" {Colors.YELLOW}Status: Disconnected{Colors.RESET}")
|
||
|
|
|
||
|
|
# Show current global settings
|
||
|
|
if any(self.global_settings.values()):
|
||
|
|
print()
|
||
|
|
if self.global_settings['RHOSTS']:
|
||
|
|
print(f" {Colors.CYAN}Target:{Colors.RESET} {self.global_settings['RHOSTS']}")
|
||
|
|
if self.global_settings['LHOST']:
|
||
|
|
print(f" {Colors.CYAN}LHOST:{Colors.RESET} {self.global_settings['LHOST']}")
|
||
|
|
if self.global_settings['LPORT'] and self.global_settings['LPORT'] != '4444':
|
||
|
|
print(f" {Colors.CYAN}LPORT:{Colors.RESET} {self.global_settings['LPORT']}")
|
||
|
|
|
||
|
|
# Current module
|
||
|
|
if self.current_module:
|
||
|
|
print(f" {Colors.YELLOW}Module:{Colors.RESET} {self.current_module_type}/{self.current_module}")
|
||
|
|
|
||
|
|
print()
|
||
|
|
print(f" {Colors.RED}[1]{Colors.RESET} Set Target {Colors.DIM}- Configure target & listener settings{Colors.RESET}")
|
||
|
|
print(f" {Colors.RED}[2]{Colors.RESET} Module Browser {Colors.DIM}- Browse modules by category{Colors.RESET}")
|
||
|
|
print(f" {Colors.RED}[3]{Colors.RESET} Search Modules {Colors.DIM}- Search all modules{Colors.RESET}")
|
||
|
|
print()
|
||
|
|
print(f" {Colors.RED}[4]{Colors.RESET} Current Module {Colors.DIM}- View/configure selected module{Colors.RESET}")
|
||
|
|
print(f" {Colors.RED}[5]{Colors.RESET} Run Module {Colors.DIM}- Execute current module{Colors.RESET}")
|
||
|
|
print()
|
||
|
|
print(f" {Colors.RED}[6]{Colors.RESET} Sessions {Colors.DIM}- View and interact with sessions{Colors.RESET}")
|
||
|
|
print(f" {Colors.RED}[7]{Colors.RESET} Jobs {Colors.DIM}- View running background jobs{Colors.RESET}")
|
||
|
|
print()
|
||
|
|
print(f" {Colors.RED}[8]{Colors.RESET} MSF Console {Colors.DIM}- Direct console access{Colors.RESET}")
|
||
|
|
print(f" {Colors.RED}[9]{Colors.RESET} Quick Scan {Colors.DIM}- Common scanners{Colors.RESET}")
|
||
|
|
print(f" {Colors.RED}[E]{Colors.RESET} Exploit Suggester {Colors.DIM}- Suggest exploits from vuln data{Colors.RESET}")
|
||
|
|
print()
|
||
|
|
print(f" {Colors.DIM}[0]{Colors.RESET} Back to Main Menu")
|
||
|
|
print()
|
||
|
|
|
||
|
|
# =========================================================================
|
||
|
|
# GLOBAL TARGET SETTINGS
|
||
|
|
# =========================================================================
|
||
|
|
|
||
|
|
def show_target_settings(self):
|
||
|
|
"""Configure global target settings."""
|
||
|
|
while True:
|
||
|
|
clear_screen()
|
||
|
|
display_banner()
|
||
|
|
|
||
|
|
print(f"{Colors.RED}{Colors.BOLD} Target Configuration{Colors.RESET}")
|
||
|
|
print(f"{Colors.DIM} Set target and listener options before selecting modules{Colors.RESET}")
|
||
|
|
print(f"{Colors.DIM} {'─' * 50}{Colors.RESET}")
|
||
|
|
print()
|
||
|
|
|
||
|
|
# Display current settings with term bank descriptions
|
||
|
|
rhosts_info = get_setting_info('RHOSTS')
|
||
|
|
lhost_info = get_setting_info('LHOST')
|
||
|
|
lport_info = get_setting_info('LPORT')
|
||
|
|
|
||
|
|
rhosts_val = self.global_settings['RHOSTS'] or f"{Colors.YELLOW}(not set){Colors.RESET}"
|
||
|
|
lhost_val = self.global_settings['LHOST'] or f"{Colors.YELLOW}(not set){Colors.RESET}"
|
||
|
|
lport_val = self.global_settings['LPORT'] or '4444'
|
||
|
|
|
||
|
|
print(f" {Colors.RED}[1]{Colors.RESET} RHOSTS = {rhosts_val}")
|
||
|
|
print(f" {Colors.DIM}{self.wrap_text(rhosts_info['description'])}{Colors.RESET}")
|
||
|
|
print()
|
||
|
|
print(f" {Colors.RED}[2]{Colors.RESET} LHOST = {lhost_val}")
|
||
|
|
print(f" {Colors.DIM}{self.wrap_text(lhost_info['description'])}{Colors.RESET}")
|
||
|
|
print()
|
||
|
|
print(f" {Colors.RED}[3]{Colors.RESET} LPORT = {lport_val}")
|
||
|
|
print(f" {Colors.DIM}{self.wrap_text(lport_info['description'])}{Colors.RESET}")
|
||
|
|
print()
|
||
|
|
print(f" {Colors.GREEN}[A]{Colors.RESET} Auto-detect LHOST")
|
||
|
|
print(f" {Colors.GREEN}[R]{Colors.RESET} Resolve hostname to IP")
|
||
|
|
print()
|
||
|
|
print(f" {Colors.DIM}[0]{Colors.RESET} Back")
|
||
|
|
print()
|
||
|
|
|
||
|
|
try:
|
||
|
|
choice = input(f"{Colors.WHITE} Select: {Colors.RESET}").strip().lower()
|
||
|
|
|
||
|
|
if choice == '0' or not choice:
|
||
|
|
break
|
||
|
|
elif choice == '1':
|
||
|
|
self._set_rhosts()
|
||
|
|
elif choice == '2':
|
||
|
|
self._set_lhost()
|
||
|
|
elif choice == '3':
|
||
|
|
self._set_lport()
|
||
|
|
elif choice == 'a':
|
||
|
|
self._auto_detect_lhost()
|
||
|
|
elif choice == 'r':
|
||
|
|
self._resolve_hostname()
|
||
|
|
|
||
|
|
except (EOFError, KeyboardInterrupt):
|
||
|
|
break
|
||
|
|
|
||
|
|
def _set_rhosts(self):
|
||
|
|
"""Set RHOSTS with validation and domain resolution."""
|
||
|
|
print()
|
||
|
|
print(format_setting_help('RHOSTS'))
|
||
|
|
print()
|
||
|
|
|
||
|
|
current = self.global_settings['RHOSTS']
|
||
|
|
prompt = f"Target [{current}]: " if current else "Target: "
|
||
|
|
value = input(f"{Colors.WHITE}{prompt}{Colors.RESET}").strip()
|
||
|
|
|
||
|
|
if not value and current:
|
||
|
|
return # Keep current
|
||
|
|
|
||
|
|
if value:
|
||
|
|
# Check if it's a hostname that needs resolution
|
||
|
|
if not any(c.isdigit() for c in value.split('/')[0].split('-')[0]):
|
||
|
|
# Looks like a hostname
|
||
|
|
print(f"{Colors.CYAN}[*] Resolving {value}...{Colors.RESET}")
|
||
|
|
ip = self.resolve_hostname(value)
|
||
|
|
if ip:
|
||
|
|
print(f"{Colors.GREEN}[+] Resolved to {ip}{Colors.RESET}")
|
||
|
|
use_ip = input(f"{Colors.WHITE}Use resolved IP? (y/n) [{Colors.GREEN}y{Colors.WHITE}]: {Colors.RESET}").strip().lower()
|
||
|
|
if use_ip != 'n':
|
||
|
|
value = ip
|
||
|
|
else:
|
||
|
|
print(f"{Colors.YELLOW}[!] Could not resolve hostname{Colors.RESET}")
|
||
|
|
|
||
|
|
# Validate
|
||
|
|
valid, msg = validate_setting_value('RHOSTS', value)
|
||
|
|
if valid:
|
||
|
|
self.global_settings['RHOSTS'] = value
|
||
|
|
self.print_status(f"RHOSTS => {value}", "success")
|
||
|
|
else:
|
||
|
|
self.print_status(msg, "warning")
|
||
|
|
self.global_settings['RHOSTS'] = value # Set anyway, user might know better
|
||
|
|
|
||
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
||
|
|
|
||
|
|
def _set_lhost(self):
|
||
|
|
"""Set LHOST."""
|
||
|
|
print()
|
||
|
|
print(format_setting_help('LHOST'))
|
||
|
|
print()
|
||
|
|
|
||
|
|
current = self.global_settings['LHOST']
|
||
|
|
auto_ip = self.get_local_ip()
|
||
|
|
|
||
|
|
print(f" {Colors.DIM}Detected local IP: {auto_ip}{Colors.RESET}")
|
||
|
|
prompt = f"LHOST [{current or auto_ip}]: "
|
||
|
|
value = input(f"{Colors.WHITE}{prompt}{Colors.RESET}").strip()
|
||
|
|
|
||
|
|
if not value:
|
||
|
|
value = current or auto_ip
|
||
|
|
|
||
|
|
if value:
|
||
|
|
self.global_settings['LHOST'] = value
|
||
|
|
self.print_status(f"LHOST => {value}", "success")
|
||
|
|
|
||
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
||
|
|
|
||
|
|
def _set_lport(self):
|
||
|
|
"""Set LPORT."""
|
||
|
|
print()
|
||
|
|
print(format_setting_help('LPORT'))
|
||
|
|
print()
|
||
|
|
|
||
|
|
current = self.global_settings['LPORT'] or '4444'
|
||
|
|
prompt = f"LPORT [{current}]: "
|
||
|
|
value = input(f"{Colors.WHITE}{prompt}{Colors.RESET}").strip()
|
||
|
|
|
||
|
|
if not value:
|
||
|
|
value = current
|
||
|
|
|
||
|
|
# Validate port
|
||
|
|
try:
|
||
|
|
port = int(value)
|
||
|
|
if 1 <= port <= 65535:
|
||
|
|
self.global_settings['LPORT'] = value
|
||
|
|
self.print_status(f"LPORT => {value}", "success")
|
||
|
|
else:
|
||
|
|
self.print_status("Port must be between 1 and 65535", "warning")
|
||
|
|
except ValueError:
|
||
|
|
self.print_status("Invalid port number", "warning")
|
||
|
|
|
||
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
||
|
|
|
||
|
|
def _auto_detect_lhost(self):
|
||
|
|
"""Auto-detect LHOST."""
|
||
|
|
ip = self.get_local_ip()
|
||
|
|
self.global_settings['LHOST'] = ip
|
||
|
|
self.print_status(f"LHOST => {ip} (auto-detected)", "success")
|
||
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
||
|
|
|
||
|
|
def _resolve_hostname(self):
|
||
|
|
"""Resolve a hostname to IP."""
|
||
|
|
print()
|
||
|
|
hostname = input(f"{Colors.WHITE}Hostname to resolve: {Colors.RESET}").strip()
|
||
|
|
|
||
|
|
if hostname:
|
||
|
|
print(f"{Colors.CYAN}[*] Resolving {hostname}...{Colors.RESET}")
|
||
|
|
ip = self.resolve_hostname(hostname)
|
||
|
|
if ip:
|
||
|
|
print(f"{Colors.GREEN}[+] {hostname} => {ip}{Colors.RESET}")
|
||
|
|
use_as_target = input(f"{Colors.WHITE}Use as RHOSTS? (y/n): {Colors.RESET}").strip().lower()
|
||
|
|
if use_as_target == 'y':
|
||
|
|
self.global_settings['RHOSTS'] = ip
|
||
|
|
self.print_status(f"RHOSTS => {ip}", "success")
|
||
|
|
else:
|
||
|
|
self.print_status(f"Could not resolve {hostname}", "error")
|
||
|
|
|
||
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
||
|
|
|
||
|
|
# =========================================================================
|
||
|
|
# MODULE BROWSER
|
||
|
|
# =========================================================================
|
||
|
|
|
||
|
|
def show_module_browser(self):
|
||
|
|
"""Browse modules by category."""
|
||
|
|
while True:
|
||
|
|
clear_screen()
|
||
|
|
display_banner()
|
||
|
|
|
||
|
|
print(f"{Colors.RED}{Colors.BOLD} Module Browser{Colors.RESET}")
|
||
|
|
print(f"{Colors.DIM} Browse Metasploit modules by category{Colors.RESET}")
|
||
|
|
print(f"{Colors.DIM} {'─' * 50}{Colors.RESET}")
|
||
|
|
print()
|
||
|
|
|
||
|
|
# Show categories
|
||
|
|
for i, (cat_id, cat_info) in enumerate(self.MODULE_CATEGORIES.items(), 1):
|
||
|
|
color = cat_info['color']
|
||
|
|
print(f" {color}[{i}]{Colors.RESET} {cat_info['name']}")
|
||
|
|
print(f" {Colors.DIM}{cat_info['description']}{Colors.RESET}")
|
||
|
|
|
||
|
|
print()
|
||
|
|
print(f" {Colors.DIM}[0]{Colors.RESET} Back")
|
||
|
|
print()
|
||
|
|
|
||
|
|
try:
|
||
|
|
choice = input(f"{Colors.WHITE} Select category: {Colors.RESET}").strip()
|
||
|
|
|
||
|
|
if choice == '0' or not choice:
|
||
|
|
break
|
||
|
|
|
||
|
|
try:
|
||
|
|
idx = int(choice) - 1
|
||
|
|
cat_ids = list(self.MODULE_CATEGORIES.keys())
|
||
|
|
if 0 <= idx < len(cat_ids):
|
||
|
|
self._browse_category(cat_ids[idx])
|
||
|
|
except ValueError:
|
||
|
|
pass
|
||
|
|
|
||
|
|
except (EOFError, KeyboardInterrupt):
|
||
|
|
break
|
||
|
|
|
||
|
|
def _browse_category(self, category: str):
|
||
|
|
"""Browse modules in a category with pagination."""
|
||
|
|
cat_info = self.MODULE_CATEGORIES.get(category)
|
||
|
|
if not cat_info:
|
||
|
|
return
|
||
|
|
|
||
|
|
# Get modules from library that match this category
|
||
|
|
modules = []
|
||
|
|
for path, info in MSF_MODULES.items():
|
||
|
|
for type_prefix in cat_info['types']:
|
||
|
|
if path.startswith(type_prefix):
|
||
|
|
modules.append({'path': path, **info})
|
||
|
|
break
|
||
|
|
|
||
|
|
# Also try to get from MSF if connected
|
||
|
|
if self.msf.is_connected:
|
||
|
|
for type_prefix in cat_info['types']:
|
||
|
|
if '/' in type_prefix:
|
||
|
|
# e.g., auxiliary/scanner
|
||
|
|
mtype = type_prefix.split('/')[0]
|
||
|
|
else:
|
||
|
|
mtype = type_prefix
|
||
|
|
|
||
|
|
msf_modules = self.msf.list_modules(mtype)
|
||
|
|
if msf_modules:
|
||
|
|
for mod_path in msf_modules[:50]: # Limit to avoid overwhelming
|
||
|
|
if mod_path not in [m['path'] for m in modules]:
|
||
|
|
# Add basic info for modules not in library
|
||
|
|
modules.append({
|
||
|
|
'path': mod_path,
|
||
|
|
'name': mod_path.split('/')[-1].replace('_', ' ').title(),
|
||
|
|
'description': 'Module from Metasploit (use "info" for details)',
|
||
|
|
'tags': []
|
||
|
|
})
|
||
|
|
|
||
|
|
if not modules:
|
||
|
|
self.print_status(f"No modules found in {cat_info['name']}", "warning")
|
||
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
||
|
|
return
|
||
|
|
|
||
|
|
# Pagination
|
||
|
|
page_size = 20
|
||
|
|
page = 0
|
||
|
|
total_pages = (len(modules) + page_size - 1) // page_size
|
||
|
|
|
||
|
|
while True:
|
||
|
|
clear_screen()
|
||
|
|
display_banner()
|
||
|
|
|
||
|
|
print(f"{cat_info['color']}{Colors.BOLD} {cat_info['name']}{Colors.RESET}")
|
||
|
|
print(f"{Colors.DIM} Page {page + 1} of {total_pages} ({len(modules)} modules){Colors.RESET}")
|
||
|
|
print(f"{Colors.DIM} {'─' * 50}{Colors.RESET}")
|
||
|
|
print()
|
||
|
|
|
||
|
|
# Display modules in 2 columns
|
||
|
|
start_idx = page * page_size
|
||
|
|
end_idx = min(start_idx + page_size, len(modules))
|
||
|
|
page_modules = modules[start_idx:end_idx]
|
||
|
|
|
||
|
|
# Split into two columns
|
||
|
|
half = (len(page_modules) + 1) // 2
|
||
|
|
col1 = page_modules[:half]
|
||
|
|
col2 = page_modules[half:]
|
||
|
|
|
||
|
|
for i in range(max(len(col1), len(col2))):
|
||
|
|
line = ""
|
||
|
|
|
||
|
|
# Column 1
|
||
|
|
if i < len(col1):
|
||
|
|
num = start_idx + i + 1
|
||
|
|
mod = col1[i]
|
||
|
|
name = mod.get('name', mod['path'].split('/')[-1])
|
||
|
|
if len(name) > 22:
|
||
|
|
name = name[:19] + "..."
|
||
|
|
line += f" {cat_info['color']}[{num:2}]{Colors.RESET} {name:22}"
|
||
|
|
else:
|
||
|
|
line += " " * 30
|
||
|
|
|
||
|
|
# Column 2
|
||
|
|
if i < len(col2):
|
||
|
|
num = start_idx + half + i + 1
|
||
|
|
mod = col2[i]
|
||
|
|
name = mod.get('name', mod['path'].split('/')[-1])
|
||
|
|
if len(name) > 22:
|
||
|
|
name = name[:19] + "..."
|
||
|
|
line += f" {cat_info['color']}[{num:2}]{Colors.RESET} {name:22}"
|
||
|
|
|
||
|
|
print(line)
|
||
|
|
|
||
|
|
print()
|
||
|
|
print(f" {Colors.DIM}[N]{Colors.RESET} Next page {Colors.DIM}[P]{Colors.RESET} Previous {Colors.DIM}[0]{Colors.RESET} Back")
|
||
|
|
print()
|
||
|
|
|
||
|
|
try:
|
||
|
|
choice = input(f"{Colors.WHITE} Select module: {Colors.RESET}").strip().lower()
|
||
|
|
|
||
|
|
if choice == '0' or not choice:
|
||
|
|
break
|
||
|
|
elif choice == 'n' and page < total_pages - 1:
|
||
|
|
page += 1
|
||
|
|
elif choice == 'p' and page > 0:
|
||
|
|
page -= 1
|
||
|
|
else:
|
||
|
|
try:
|
||
|
|
idx = int(choice) - 1
|
||
|
|
if 0 <= idx < len(modules):
|
||
|
|
self._show_module_details(modules[idx])
|
||
|
|
except ValueError:
|
||
|
|
pass
|
||
|
|
|
||
|
|
except (EOFError, KeyboardInterrupt):
|
||
|
|
break
|
||
|
|
|
||
|
|
def _show_module_details(self, module_info: Dict):
|
||
|
|
"""Show module details and offer to use it."""
|
||
|
|
clear_screen()
|
||
|
|
display_banner()
|
||
|
|
|
||
|
|
path = module_info['path']
|
||
|
|
name = module_info.get('name', path.split('/')[-1])
|
||
|
|
|
||
|
|
print(f"{Colors.RED}{Colors.BOLD} {name}{Colors.RESET}")
|
||
|
|
print(f"{Colors.DIM} {path}{Colors.RESET}")
|
||
|
|
print(f"{Colors.DIM} {'─' * 50}{Colors.RESET}")
|
||
|
|
print()
|
||
|
|
|
||
|
|
# Try to get full help from library
|
||
|
|
help_text = format_module_help(path)
|
||
|
|
if help_text:
|
||
|
|
print(help_text)
|
||
|
|
else:
|
||
|
|
# Fall back to basic info
|
||
|
|
desc = module_info.get('description', 'No description available')
|
||
|
|
print(f" {Colors.CYAN}Description:{Colors.RESET}")
|
||
|
|
# Word wrap description
|
||
|
|
words = desc.split()
|
||
|
|
line = " "
|
||
|
|
for word in words:
|
||
|
|
if len(line) + len(word) > 70:
|
||
|
|
print(line)
|
||
|
|
line = " "
|
||
|
|
line += word + " "
|
||
|
|
if line.strip():
|
||
|
|
print(line)
|
||
|
|
print()
|
||
|
|
|
||
|
|
if 'author' in module_info:
|
||
|
|
authors = module_info['author']
|
||
|
|
if isinstance(authors, list):
|
||
|
|
authors = ', '.join(authors)
|
||
|
|
print(f" {Colors.CYAN}Author:{Colors.RESET} {authors}")
|
||
|
|
|
||
|
|
if 'cve' in module_info and module_info['cve']:
|
||
|
|
print(f" {Colors.CYAN}CVE:{Colors.RESET} {module_info['cve']}")
|
||
|
|
|
||
|
|
if 'reliability' in module_info:
|
||
|
|
print(f" {Colors.CYAN}Reliability:{Colors.RESET} {module_info['reliability']}")
|
||
|
|
|
||
|
|
if 'notes' in module_info:
|
||
|
|
print()
|
||
|
|
print(f" {Colors.YELLOW}Notes:{Colors.RESET} {module_info['notes']}")
|
||
|
|
|
||
|
|
print()
|
||
|
|
print(f" {Colors.GREEN}[U]{Colors.RESET} Use this module")
|
||
|
|
print(f" {Colors.CYAN}[I]{Colors.RESET} Get info from MSF")
|
||
|
|
print(f" {Colors.DIM}[0]{Colors.RESET} Back")
|
||
|
|
print()
|
||
|
|
|
||
|
|
try:
|
||
|
|
choice = input(f"{Colors.WHITE} Select: {Colors.RESET}").strip().lower()
|
||
|
|
|
||
|
|
if choice == 'u':
|
||
|
|
self._select_module(path)
|
||
|
|
elif choice == 'i':
|
||
|
|
self._show_msf_info(path)
|
||
|
|
|
||
|
|
except (EOFError, KeyboardInterrupt):
|
||
|
|
pass
|
||
|
|
|
||
|
|
def _select_module(self, module_path: str):
|
||
|
|
"""Select a module and prepare it for execution."""
|
||
|
|
if not self.ensure_connected():
|
||
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
||
|
|
return
|
||
|
|
|
||
|
|
# Parse module type and name
|
||
|
|
parts = module_path.split('/', 1)
|
||
|
|
mtype = parts[0]
|
||
|
|
mname = parts[1] if len(parts) > 1 else module_path
|
||
|
|
|
||
|
|
self.print_status(f"Loading {module_path}...", "info")
|
||
|
|
|
||
|
|
# Get module info and options from MSF
|
||
|
|
info = self.msf.get_module_info(module_path)
|
||
|
|
options = self.msf.get_module_options(module_path)
|
||
|
|
|
||
|
|
if not options:
|
||
|
|
self.print_status(f"Failed to load module: {self.msf.last_error}", "error")
|
||
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
||
|
|
return
|
||
|
|
|
||
|
|
self.current_module = mname
|
||
|
|
self.current_module_type = mtype
|
||
|
|
self.module_options = {}
|
||
|
|
|
||
|
|
# Set defaults from module
|
||
|
|
for opt_name, opt_info in options.items():
|
||
|
|
if isinstance(opt_info, dict):
|
||
|
|
default = opt_info.get('default')
|
||
|
|
if default is not None and default != '':
|
||
|
|
self.module_options[opt_name] = default
|
||
|
|
|
||
|
|
# Apply global settings
|
||
|
|
if self.global_settings['RHOSTS'] and 'RHOSTS' in options:
|
||
|
|
self.module_options['RHOSTS'] = self.global_settings['RHOSTS']
|
||
|
|
if self.global_settings['RHOSTS'] and 'RHOST' in options:
|
||
|
|
self.module_options['RHOST'] = self.global_settings['RHOSTS']
|
||
|
|
if self.global_settings['LHOST'] and 'LHOST' in options:
|
||
|
|
self.module_options['LHOST'] = self.global_settings['LHOST']
|
||
|
|
if self.global_settings['LPORT'] and 'LPORT' in options:
|
||
|
|
self.module_options['LPORT'] = self.global_settings['LPORT']
|
||
|
|
|
||
|
|
self.print_status(f"Module loaded: {mtype}/{mname}", "success")
|
||
|
|
|
||
|
|
# Show what was auto-filled
|
||
|
|
auto_filled = []
|
||
|
|
if 'RHOSTS' in self.module_options or 'RHOST' in self.module_options:
|
||
|
|
target = self.module_options.get('RHOSTS') or self.module_options.get('RHOST')
|
||
|
|
if target:
|
||
|
|
auto_filled.append(f"Target: {target}")
|
||
|
|
if 'LHOST' in self.module_options and self.module_options['LHOST']:
|
||
|
|
auto_filled.append(f"LHOST: {self.module_options['LHOST']}")
|
||
|
|
|
||
|
|
if auto_filled:
|
||
|
|
print(f"{Colors.DIM} Auto-filled: {', '.join(auto_filled)}{Colors.RESET}")
|
||
|
|
|
||
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
||
|
|
|
||
|
|
def _show_msf_info(self, module_path: str):
|
||
|
|
"""Get and display module info from MSF."""
|
||
|
|
if not self.ensure_connected():
|
||
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
||
|
|
return
|
||
|
|
|
||
|
|
self.print_status(f"Fetching info for {module_path}...", "info")
|
||
|
|
|
||
|
|
info = self.msf.get_module_info(module_path)
|
||
|
|
if not info:
|
||
|
|
self.print_status(f"Failed to get info: {self.msf.last_error}", "error")
|
||
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
||
|
|
return
|
||
|
|
|
||
|
|
clear_screen()
|
||
|
|
display_banner()
|
||
|
|
|
||
|
|
print(f"{Colors.RED}{Colors.BOLD} Module Info (from MSF){Colors.RESET}")
|
||
|
|
print(f"{Colors.DIM} {'─' * 50}{Colors.RESET}")
|
||
|
|
print()
|
||
|
|
|
||
|
|
# Display info fields
|
||
|
|
fields = ['name', 'fullname', 'description', 'author', 'references', 'rank', 'platform', 'arch']
|
||
|
|
for field in fields:
|
||
|
|
if field in info and info[field]:
|
||
|
|
value = info[field]
|
||
|
|
if isinstance(value, list):
|
||
|
|
if field == 'references':
|
||
|
|
print(f" {Colors.CYAN}{field}:{Colors.RESET}")
|
||
|
|
for ref in value[:5]:
|
||
|
|
if isinstance(ref, (list, tuple)) and len(ref) >= 2:
|
||
|
|
print(f" - {ref[0]}: {ref[1]}")
|
||
|
|
else:
|
||
|
|
print(f" - {ref}")
|
||
|
|
else:
|
||
|
|
value = ', '.join(str(v) for v in value[:5])
|
||
|
|
print(f" {Colors.CYAN}{field}:{Colors.RESET} {value}")
|
||
|
|
elif field == 'description':
|
||
|
|
print(f" {Colors.CYAN}{field}:{Colors.RESET}")
|
||
|
|
# Word wrap
|
||
|
|
words = str(value).split()
|
||
|
|
line = " "
|
||
|
|
for word in words:
|
||
|
|
if len(line) + len(word) > 70:
|
||
|
|
print(line)
|
||
|
|
line = " "
|
||
|
|
line += word + " "
|
||
|
|
if line.strip():
|
||
|
|
print(line)
|
||
|
|
else:
|
||
|
|
print(f" {Colors.CYAN}{field}:{Colors.RESET} {value}")
|
||
|
|
|
||
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
||
|
|
|
||
|
|
# =========================================================================
|
||
|
|
# SEARCH MODULES
|
||
|
|
# =========================================================================
|
||
|
|
|
||
|
|
def search_modules(self):
|
||
|
|
"""Search for MSF modules."""
|
||
|
|
if not self.ensure_connected():
|
||
|
|
return
|
||
|
|
|
||
|
|
print(f"\n{Colors.BOLD}Search Metasploit Modules{Colors.RESET}")
|
||
|
|
print(f"{Colors.DIM}Examples: 'smb', 'apache', 'ssh', 'cve:2021', 'eternalblue'{Colors.RESET}\n")
|
||
|
|
|
||
|
|
query = input(f"{Colors.WHITE}Search: {Colors.RESET}").strip()
|
||
|
|
if not query:
|
||
|
|
return
|
||
|
|
|
||
|
|
self.print_status(f"Searching for '{query}'...", "info")
|
||
|
|
|
||
|
|
# Search both library and MSF
|
||
|
|
library_results = library_search_modules(query)
|
||
|
|
msf_results = self.msf.search_modules(query)
|
||
|
|
|
||
|
|
# Combine results, preferring library entries
|
||
|
|
combined = {}
|
||
|
|
for mod in library_results:
|
||
|
|
combined[mod['path']] = mod
|
||
|
|
|
||
|
|
if msf_results:
|
||
|
|
for mod in msf_results:
|
||
|
|
if isinstance(mod, dict):
|
||
|
|
fullname = mod.get('fullname', '')
|
||
|
|
if fullname and fullname not in combined:
|
||
|
|
combined[fullname] = {
|
||
|
|
'path': fullname,
|
||
|
|
'name': mod.get('name', fullname.split('/')[-1]),
|
||
|
|
'description': 'Module from Metasploit',
|
||
|
|
'rank': mod.get('rank', '')
|
||
|
|
}
|
||
|
|
elif isinstance(mod, str) and mod not in combined:
|
||
|
|
combined[mod] = {
|
||
|
|
'path': mod,
|
||
|
|
'name': mod.split('/')[-1],
|
||
|
|
'description': 'Module from Metasploit'
|
||
|
|
}
|
||
|
|
|
||
|
|
results = list(combined.values())
|
||
|
|
|
||
|
|
if not results:
|
||
|
|
self.print_status("No modules found", "warning")
|
||
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
||
|
|
return
|
||
|
|
|
||
|
|
# Display with pagination
|
||
|
|
page_size = 15
|
||
|
|
page = 0
|
||
|
|
total_pages = (len(results) + page_size - 1) // page_size
|
||
|
|
|
||
|
|
while True:
|
||
|
|
clear_screen()
|
||
|
|
display_banner()
|
||
|
|
|
||
|
|
print(f"{Colors.GREEN}{Colors.BOLD} Search Results: '{query}'{Colors.RESET}")
|
||
|
|
print(f"{Colors.DIM} Page {page + 1} of {total_pages} ({len(results)} found){Colors.RESET}")
|
||
|
|
print(f"{Colors.DIM} {'─' * 50}{Colors.RESET}")
|
||
|
|
print()
|
||
|
|
|
||
|
|
start_idx = page * page_size
|
||
|
|
end_idx = min(start_idx + page_size, len(results))
|
||
|
|
page_results = results[start_idx:end_idx]
|
||
|
|
|
||
|
|
for i, mod in enumerate(page_results, start_idx + 1):
|
||
|
|
name = mod.get('name', mod['path'].split('/')[-1])
|
||
|
|
path = mod['path']
|
||
|
|
if len(name) > 30:
|
||
|
|
name = name[:27] + "..."
|
||
|
|
print(f" {Colors.RED}[{i:2}]{Colors.RESET} {name}")
|
||
|
|
print(f" {Colors.DIM}{path}{Colors.RESET}")
|
||
|
|
|
||
|
|
print()
|
||
|
|
print(f" {Colors.DIM}[N]{Colors.RESET} Next {Colors.DIM}[P]{Colors.RESET} Previous {Colors.DIM}[0]{Colors.RESET} Back")
|
||
|
|
print()
|
||
|
|
|
||
|
|
try:
|
||
|
|
choice = input(f"{Colors.WHITE} Select: {Colors.RESET}").strip().lower()
|
||
|
|
|
||
|
|
if choice == '0' or not choice:
|
||
|
|
break
|
||
|
|
elif choice == 'n' and page < total_pages - 1:
|
||
|
|
page += 1
|
||
|
|
elif choice == 'p' and page > 0:
|
||
|
|
page -= 1
|
||
|
|
else:
|
||
|
|
try:
|
||
|
|
idx = int(choice) - 1
|
||
|
|
if 0 <= idx < len(results):
|
||
|
|
self._show_module_details(results[idx])
|
||
|
|
except ValueError:
|
||
|
|
pass
|
||
|
|
|
||
|
|
except (EOFError, KeyboardInterrupt):
|
||
|
|
break
|
||
|
|
|
||
|
|
# =========================================================================
|
||
|
|
# CURRENT MODULE MANAGEMENT
|
||
|
|
# =========================================================================
|
||
|
|
|
||
|
|
def show_current_module(self):
|
||
|
|
"""Show and configure current module options."""
|
||
|
|
if not self.current_module:
|
||
|
|
self.print_status("No module selected. Use Module Browser or Search first.", "warning")
|
||
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
||
|
|
return
|
||
|
|
|
||
|
|
if not self.ensure_connected():
|
||
|
|
return
|
||
|
|
|
||
|
|
full_path = f"{self.current_module_type}/{self.current_module}"
|
||
|
|
options = self.msf.get_module_options(full_path)
|
||
|
|
|
||
|
|
if not options:
|
||
|
|
self.print_status(f"Failed to get options: {self.msf.last_error}", "error")
|
||
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
||
|
|
return
|
||
|
|
|
||
|
|
while True:
|
||
|
|
clear_screen()
|
||
|
|
display_banner()
|
||
|
|
|
||
|
|
print(f"{Colors.RED}{Colors.BOLD} {full_path}{Colors.RESET}")
|
||
|
|
print(f"{Colors.DIM} {'─' * 50}{Colors.RESET}")
|
||
|
|
print()
|
||
|
|
|
||
|
|
# Separate required and optional
|
||
|
|
required = []
|
||
|
|
optional = []
|
||
|
|
|
||
|
|
for name, info in options.items():
|
||
|
|
if isinstance(info, dict):
|
||
|
|
is_required = info.get('required', False)
|
||
|
|
current_val = self.module_options.get(name, info.get('default', ''))
|
||
|
|
desc = info.get('desc', '')[:35]
|
||
|
|
|
||
|
|
entry = (name, current_val, desc, is_required)
|
||
|
|
if is_required:
|
||
|
|
required.append(entry)
|
||
|
|
else:
|
||
|
|
optional.append(entry)
|
||
|
|
|
||
|
|
# Show required first
|
||
|
|
if required:
|
||
|
|
print(f" {Colors.RED}Required Options:{Colors.RESET}")
|
||
|
|
for i, (name, val, desc, _) in enumerate(required, 1):
|
||
|
|
val_display = str(val) if val else f"{Colors.YELLOW}(not set){Colors.RESET}"
|
||
|
|
print(f" {Colors.CYAN}[{i}]{Colors.RESET} {name:18} = {val_display}")
|
||
|
|
|
||
|
|
# Get help from term bank
|
||
|
|
term_info = get_setting_info(name)
|
||
|
|
if term_info:
|
||
|
|
print(f" {Colors.DIM}{self.wrap_text(term_info['description'], width=55, indent=' ')}{Colors.RESET}")
|
||
|
|
else:
|
||
|
|
print(f" {Colors.DIM}{self.wrap_text(desc, width=55, indent=' ')}{Colors.RESET}")
|
||
|
|
print()
|
||
|
|
|
||
|
|
# Show optional (just first 8)
|
||
|
|
if optional:
|
||
|
|
print(f" {Colors.DIM}Optional (first 8):{Colors.RESET}")
|
||
|
|
for name, val, desc, _ in optional[:8]:
|
||
|
|
val_display = str(val)[:20] if val else ""
|
||
|
|
print(f" {Colors.DIM}{name:18} = {val_display}{Colors.RESET}")
|
||
|
|
print()
|
||
|
|
|
||
|
|
print(f" {Colors.GREEN}[S]{Colors.RESET} Set option")
|
||
|
|
print(f" {Colors.GREEN}[R]{Colors.RESET} Run module")
|
||
|
|
print(f" {Colors.CYAN}[A]{Colors.RESET} Show all options")
|
||
|
|
print()
|
||
|
|
print(f" {Colors.DIM}[0]{Colors.RESET} Back")
|
||
|
|
print()
|
||
|
|
|
||
|
|
try:
|
||
|
|
choice = input(f"{Colors.WHITE} Select: {Colors.RESET}").strip().lower()
|
||
|
|
|
||
|
|
if choice == '0' or not choice:
|
||
|
|
break
|
||
|
|
elif choice == 's':
|
||
|
|
self.set_option()
|
||
|
|
elif choice == 'r':
|
||
|
|
self.run_module()
|
||
|
|
break
|
||
|
|
elif choice == 'a':
|
||
|
|
self._show_all_options(options)
|
||
|
|
elif choice.isdigit():
|
||
|
|
idx = int(choice) - 1
|
||
|
|
if 0 <= idx < len(required):
|
||
|
|
self._set_specific_option(required[idx][0], options)
|
||
|
|
|
||
|
|
except (EOFError, KeyboardInterrupt):
|
||
|
|
break
|
||
|
|
|
||
|
|
def _show_all_options(self, options: Dict):
|
||
|
|
"""Show all module options."""
|
||
|
|
clear_screen()
|
||
|
|
display_banner()
|
||
|
|
|
||
|
|
full_path = f"{self.current_module_type}/{self.current_module}"
|
||
|
|
print(f"{Colors.BOLD}All Options for {full_path}{Colors.RESET}")
|
||
|
|
print(f"{Colors.DIM}{'─' * 60}{Colors.RESET}\n")
|
||
|
|
|
||
|
|
for name, info in sorted(options.items()):
|
||
|
|
if isinstance(info, dict):
|
||
|
|
is_required = info.get('required', False)
|
||
|
|
current_val = self.module_options.get(name, info.get('default', ''))
|
||
|
|
desc = info.get('desc', '')
|
||
|
|
|
||
|
|
req_marker = f"{Colors.RED}*{Colors.RESET}" if is_required else " "
|
||
|
|
val_display = str(current_val)[:30] if current_val else f"{Colors.DIM}(empty){Colors.RESET}"
|
||
|
|
|
||
|
|
print(f" {req_marker} {Colors.CYAN}{name:20}{Colors.RESET} = {val_display}")
|
||
|
|
if desc:
|
||
|
|
print(f" {Colors.DIM}{self.wrap_text(desc, width=55, indent=' ')}{Colors.RESET}")
|
||
|
|
|
||
|
|
print(f"\n{Colors.DIM}* = required{Colors.RESET}")
|
||
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
||
|
|
|
||
|
|
def _set_specific_option(self, opt_name: str, options: Dict):
|
||
|
|
"""Set a specific option with term bank help."""
|
||
|
|
print()
|
||
|
|
|
||
|
|
# Show help from term bank or module
|
||
|
|
term_info = get_setting_info(opt_name)
|
||
|
|
if term_info:
|
||
|
|
print(format_setting_help(opt_name))
|
||
|
|
elif opt_name in options:
|
||
|
|
opt_info = options[opt_name]
|
||
|
|
desc = opt_info.get('desc', 'No description')
|
||
|
|
print(f" {Colors.CYAN}{opt_name}:{Colors.RESET} {desc}")
|
||
|
|
print()
|
||
|
|
|
||
|
|
current = self.module_options.get(opt_name, '')
|
||
|
|
prompt = f"{opt_name} [{current}]: " if current else f"{opt_name}: "
|
||
|
|
value = input(f"{Colors.WHITE}{prompt}{Colors.RESET}").strip()
|
||
|
|
|
||
|
|
if value or not current:
|
||
|
|
self.module_options[opt_name] = value
|
||
|
|
self.print_status(f"{opt_name} => {value}", "success")
|
||
|
|
else:
|
||
|
|
self.print_status(f"{opt_name} unchanged", "info")
|
||
|
|
|
||
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
||
|
|
|
||
|
|
def set_option(self):
|
||
|
|
"""Set a module option."""
|
||
|
|
if not self.current_module:
|
||
|
|
self.print_status("No module selected.", "warning")
|
||
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
||
|
|
return
|
||
|
|
|
||
|
|
print(f"\n{Colors.BOLD}Set Option{Colors.RESET}")
|
||
|
|
print(f"{Colors.DIM}Common: RHOSTS, RPORT, LHOST, LPORT, PAYLOAD{Colors.RESET}\n")
|
||
|
|
|
||
|
|
opt_name = input(f"{Colors.WHITE}Option name: {Colors.RESET}").strip().upper()
|
||
|
|
if not opt_name:
|
||
|
|
return
|
||
|
|
|
||
|
|
# Show help
|
||
|
|
term_info = get_setting_info(opt_name)
|
||
|
|
if term_info:
|
||
|
|
print()
|
||
|
|
print(format_setting_help(opt_name))
|
||
|
|
print()
|
||
|
|
|
||
|
|
current = self.module_options.get(opt_name, '')
|
||
|
|
prompt = f"{Colors.WHITE}Value [{current}]: {Colors.RESET}" if current else f"{Colors.WHITE}Value: {Colors.RESET}"
|
||
|
|
opt_value = input(prompt).strip()
|
||
|
|
|
||
|
|
if opt_value or not current:
|
||
|
|
self.module_options[opt_name] = opt_value
|
||
|
|
self.print_status(f"{opt_name} => {opt_value}", "success")
|
||
|
|
else:
|
||
|
|
self.print_status(f"{opt_name} unchanged", "info")
|
||
|
|
|
||
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
||
|
|
|
||
|
|
def run_module(self):
|
||
|
|
"""Execute the current module."""
|
||
|
|
if not self.current_module:
|
||
|
|
self.print_status("No module selected.", "warning")
|
||
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
||
|
|
return
|
||
|
|
|
||
|
|
if not self.ensure_connected():
|
||
|
|
return
|
||
|
|
|
||
|
|
full_path = f"{self.current_module_type}/{self.current_module}"
|
||
|
|
|
||
|
|
print(f"\n{Colors.BOLD}Run Module: {full_path}{Colors.RESET}")
|
||
|
|
print(f"\n{Colors.CYAN}Options:{Colors.RESET}")
|
||
|
|
for k, v in self.module_options.items():
|
||
|
|
if v:
|
||
|
|
print(f" {k} = {v}")
|
||
|
|
|
||
|
|
confirm = input(f"\n{Colors.YELLOW}Execute? (y/n): {Colors.RESET}").strip().lower()
|
||
|
|
if confirm != 'y':
|
||
|
|
return
|
||
|
|
|
||
|
|
self.print_status("Executing module...", "info")
|
||
|
|
|
||
|
|
# Use job execution for exploits, console execution for auxiliary/scanners
|
||
|
|
if self.current_module_type in ['exploit', 'post']:
|
||
|
|
success, job_id, error = self.msf.execute_module_job(full_path, self.module_options)
|
||
|
|
if success:
|
||
|
|
self.print_status(f"Module running as Job {job_id}", "success")
|
||
|
|
else:
|
||
|
|
self.print_status(f"Execution failed: {error}", "error")
|
||
|
|
else:
|
||
|
|
result = self.msf.run_module(full_path, self.module_options, timeout=120)
|
||
|
|
self.msf.print_result(result, verbose=True)
|
||
|
|
|
||
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
||
|
|
|
||
|
|
# =========================================================================
|
||
|
|
# SESSIONS AND JOBS
|
||
|
|
# =========================================================================
|
||
|
|
|
||
|
|
def show_sessions(self):
|
||
|
|
"""Show active sessions."""
|
||
|
|
if not self.ensure_connected():
|
||
|
|
return
|
||
|
|
|
||
|
|
sessions = self.msf.list_sessions()
|
||
|
|
|
||
|
|
print(f"\n{Colors.BOLD}Active Sessions{Colors.RESET}")
|
||
|
|
print(f"{Colors.DIM}{'─' * 60}{Colors.RESET}\n")
|
||
|
|
|
||
|
|
if not sessions:
|
||
|
|
self.print_status("No active sessions", "info")
|
||
|
|
else:
|
||
|
|
for sid, info in sessions.items():
|
||
|
|
if isinstance(info, dict):
|
||
|
|
stype = info.get('type', 'shell')
|
||
|
|
target = info.get('target_host', 'unknown')
|
||
|
|
user = info.get('username', '')
|
||
|
|
via = info.get('via_exploit', '')[:30]
|
||
|
|
print(f" {Colors.GREEN}[{sid}]{Colors.RESET} {stype} @ {target}")
|
||
|
|
if user:
|
||
|
|
print(f" {Colors.DIM}User: {user}{Colors.RESET}")
|
||
|
|
if via:
|
||
|
|
print(f" {Colors.DIM}Via: {via}{Colors.RESET}")
|
||
|
|
else:
|
||
|
|
print(f" {Colors.GREEN}[{sid}]{Colors.RESET} {info}")
|
||
|
|
|
||
|
|
print()
|
||
|
|
sid = input(f"{Colors.WHITE}Interact with session (or Enter to skip): {Colors.RESET}").strip()
|
||
|
|
if sid and sid in sessions:
|
||
|
|
self.interact_session(sid)
|
||
|
|
|
||
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
||
|
|
|
||
|
|
def interact_session(self, session_id: str):
|
||
|
|
"""Interact with a session."""
|
||
|
|
print(f"\n{Colors.GREEN}Interacting with session {session_id}{Colors.RESET}")
|
||
|
|
print(f"{Colors.DIM}Type 'exit' to return to menu{Colors.RESET}\n")
|
||
|
|
|
||
|
|
while True:
|
||
|
|
try:
|
||
|
|
cmd = input(f"{Colors.RED}session({session_id})>{Colors.RESET} ").strip()
|
||
|
|
|
||
|
|
if cmd.lower() == 'exit':
|
||
|
|
break
|
||
|
|
|
||
|
|
if not cmd:
|
||
|
|
continue
|
||
|
|
|
||
|
|
self.msf.session_write(session_id, cmd)
|
||
|
|
time.sleep(1)
|
||
|
|
success, output = self.msf.session_read(session_id)
|
||
|
|
if success and output:
|
||
|
|
print(output)
|
||
|
|
|
||
|
|
except (EOFError, KeyboardInterrupt):
|
||
|
|
print()
|
||
|
|
break
|
||
|
|
except Exception as e:
|
||
|
|
self.print_status(f"Session error: {e}", "error")
|
||
|
|
break
|
||
|
|
|
||
|
|
def show_jobs(self):
|
||
|
|
"""Show running jobs."""
|
||
|
|
if not self.ensure_connected():
|
||
|
|
return
|
||
|
|
|
||
|
|
jobs = self.msf.list_jobs()
|
||
|
|
|
||
|
|
print(f"\n{Colors.BOLD}Running Jobs{Colors.RESET}")
|
||
|
|
print(f"{Colors.DIM}{'─' * 60}{Colors.RESET}\n")
|
||
|
|
|
||
|
|
if not jobs:
|
||
|
|
self.print_status("No running jobs", "info")
|
||
|
|
else:
|
||
|
|
for jid, jname in jobs.items():
|
||
|
|
print(f" {Colors.YELLOW}[{jid}]{Colors.RESET} {jname}")
|
||
|
|
|
||
|
|
print()
|
||
|
|
jid = input(f"{Colors.WHITE}Kill job (or Enter to skip): {Colors.RESET}").strip()
|
||
|
|
if jid and jid in jobs:
|
||
|
|
if self.msf.stop_job(jid):
|
||
|
|
self.print_status(f"Job {jid} stopped", "success")
|
||
|
|
else:
|
||
|
|
self.print_status(f"Failed to stop job {jid}", "error")
|
||
|
|
|
||
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
||
|
|
|
||
|
|
# =========================================================================
|
||
|
|
# CONSOLE AND QUICK SCAN
|
||
|
|
# =========================================================================
|
||
|
|
|
||
|
|
def console_command(self):
|
||
|
|
"""Run console commands directly."""
|
||
|
|
if not self.ensure_connected():
|
||
|
|
return
|
||
|
|
|
||
|
|
print(f"\n{Colors.BOLD}MSF Console{Colors.RESET}")
|
||
|
|
print(f"{Colors.DIM}Enter commands directly (type 'exit' to return){Colors.RESET}\n")
|
||
|
|
|
||
|
|
while True:
|
||
|
|
try:
|
||
|
|
cmd = input(f"{Colors.RED}msf>{Colors.RESET} ").strip()
|
||
|
|
|
||
|
|
if cmd.lower() == 'exit':
|
||
|
|
break
|
||
|
|
|
||
|
|
if not cmd:
|
||
|
|
continue
|
||
|
|
|
||
|
|
success, output = self.msf.run_console_command(cmd)
|
||
|
|
if output:
|
||
|
|
print(output)
|
||
|
|
|
||
|
|
except (EOFError, KeyboardInterrupt):
|
||
|
|
print()
|
||
|
|
break
|
||
|
|
|
||
|
|
def quick_scan(self):
|
||
|
|
"""Quick scanner with pre-set target."""
|
||
|
|
if not self.ensure_connected():
|
||
|
|
return
|
||
|
|
|
||
|
|
clear_screen()
|
||
|
|
display_banner()
|
||
|
|
|
||
|
|
print(f"{Colors.RED}{Colors.BOLD} Quick Scan{Colors.RESET}")
|
||
|
|
print(f"{Colors.DIM} {'─' * 50}{Colors.RESET}")
|
||
|
|
|
||
|
|
# Show current target
|
||
|
|
if self.global_settings['RHOSTS']:
|
||
|
|
print(f" {Colors.GREEN}Target: {self.global_settings['RHOSTS']}{Colors.RESET}")
|
||
|
|
else:
|
||
|
|
print(f" {Colors.YELLOW}Target: Not set (will prompt){Colors.RESET}")
|
||
|
|
print()
|
||
|
|
|
||
|
|
scanners = [
|
||
|
|
("auxiliary/scanner/portscan/tcp", "TCP Port Scanner", "Scan for open TCP ports"),
|
||
|
|
("auxiliary/scanner/smb/smb_version", "SMB Version", "Identify Windows/SMB version"),
|
||
|
|
("auxiliary/scanner/smb/smb_ms17_010", "MS17-010 Check", "Check for EternalBlue vulnerability"),
|
||
|
|
("auxiliary/scanner/ssh/ssh_version", "SSH Version", "Identify SSH server version"),
|
||
|
|
("auxiliary/scanner/http/http_version", "HTTP Version", "Identify web server version"),
|
||
|
|
("auxiliary/scanner/ftp/ftp_version", "FTP Version", "Identify FTP server version"),
|
||
|
|
]
|
||
|
|
|
||
|
|
for i, (mod, name, desc) in enumerate(scanners, 1):
|
||
|
|
print(f" {Colors.RED}[{i}]{Colors.RESET} {name}")
|
||
|
|
print(f" {Colors.DIM}{desc}{Colors.RESET}")
|
||
|
|
|
||
|
|
print(f"\n {Colors.DIM}[0]{Colors.RESET} Cancel\n")
|
||
|
|
|
||
|
|
choice = input(f"{Colors.WHITE} Select scanner: {Colors.RESET}").strip()
|
||
|
|
|
||
|
|
try:
|
||
|
|
idx = int(choice) - 1
|
||
|
|
if 0 <= idx < len(scanners):
|
||
|
|
scanner_mod, scanner_name, _ = scanners[idx]
|
||
|
|
|
||
|
|
# Get target
|
||
|
|
target = self.global_settings['RHOSTS']
|
||
|
|
if not target:
|
||
|
|
target = input(f"{Colors.WHITE}Target (IP/range): {Colors.RESET}").strip()
|
||
|
|
|
||
|
|
if not target:
|
||
|
|
return
|
||
|
|
|
||
|
|
options = {'RHOSTS': target, 'THREADS': '10'}
|
||
|
|
|
||
|
|
print(f"\n{Colors.CYAN}Running {scanner_name} against {target}...{Colors.RESET}")
|
||
|
|
|
||
|
|
result = self.msf.run_scanner(scanner_mod, target, options=options, timeout=120)
|
||
|
|
self.msf.print_result(result, verbose=False)
|
||
|
|
|
||
|
|
except (ValueError, IndexError):
|
||
|
|
pass
|
||
|
|
except Exception as e:
|
||
|
|
self.print_status(f"Scanner failed: {e}", "error")
|
||
|
|
|
||
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
||
|
|
|
||
|
|
# =========================================================================
|
||
|
|
# EXPLOIT SUGGESTER
|
||
|
|
# =========================================================================
|
||
|
|
|
||
|
|
def exploit_suggester(self):
|
||
|
|
"""Suggest exploits based on vulnerability scan results."""
|
||
|
|
clear_screen()
|
||
|
|
display_banner()
|
||
|
|
|
||
|
|
print(f"{Colors.RED}{Colors.BOLD} Exploit Suggester{Colors.RESET}")
|
||
|
|
print(f"{Colors.DIM} Suggest attack paths based on detected vulnerabilities{Colors.RESET}")
|
||
|
|
print(f"{Colors.DIM} {'─' * 50}{Colors.RESET}")
|
||
|
|
print()
|
||
|
|
|
||
|
|
print(f" {Colors.RED}[1]{Colors.RESET} Load vuln_correlator JSON")
|
||
|
|
print(f" {Colors.RED}[2]{Colors.RESET} Run fresh scan")
|
||
|
|
print(f" {Colors.RED}[3]{Colors.RESET} Manual service list")
|
||
|
|
print(f" {Colors.DIM}[0]{Colors.RESET} Back")
|
||
|
|
print()
|
||
|
|
|
||
|
|
choice = input(f"{Colors.WHITE} Select: {Colors.RESET}").strip()
|
||
|
|
|
||
|
|
services = []
|
||
|
|
cves = []
|
||
|
|
|
||
|
|
if choice == "1":
|
||
|
|
# Load from vuln correlator JSON
|
||
|
|
results_dir = Path("results")
|
||
|
|
json_files = sorted(results_dir.glob("vuln_correlator_*.json")) if results_dir.exists() else []
|
||
|
|
if not json_files:
|
||
|
|
self.print_status("No vuln correlator results found. Run OSINT > Vulnerability Correlator first.", "warning")
|
||
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
||
|
|
return
|
||
|
|
|
||
|
|
print(f"\n{Colors.CYAN}Available vuln reports:{Colors.RESET}")
|
||
|
|
for i, f in enumerate(json_files, 1):
|
||
|
|
print(f" {Colors.RED}[{i}]{Colors.RESET} {f.name}")
|
||
|
|
|
||
|
|
sel = input(f"\n{Colors.WHITE}Select: {Colors.RESET}").strip()
|
||
|
|
try:
|
||
|
|
idx = int(sel) - 1
|
||
|
|
with open(json_files[idx], 'r') as f:
|
||
|
|
data = json.load(f)
|
||
|
|
|
||
|
|
for corr in data.get('correlations', []):
|
||
|
|
svc = corr.get('service', {})
|
||
|
|
services.append(svc)
|
||
|
|
for cve in corr.get('cves', []):
|
||
|
|
cves.append(cve)
|
||
|
|
except (ValueError, IndexError, json.JSONDecodeError) as e:
|
||
|
|
self.print_status(f"Error loading file: {e}", "error")
|
||
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
||
|
|
return
|
||
|
|
|
||
|
|
elif choice == "2":
|
||
|
|
target = self.global_settings.get('RHOSTS', '') or input(f"{Colors.WHITE}Target: {Colors.RESET}").strip()
|
||
|
|
if not target:
|
||
|
|
return
|
||
|
|
|
||
|
|
self.print_status(f"Running nmap -sV on {target}...", "info")
|
||
|
|
import subprocess
|
||
|
|
try:
|
||
|
|
result = subprocess.run(f"nmap -sV -T4 {target}", shell=True, capture_output=True, text=True, timeout=300)
|
||
|
|
if result.returncode == 0:
|
||
|
|
port_re = re.compile(r'(\d+)/(tcp|udp)\s+open\s+(\S+)\s*(.*)')
|
||
|
|
for line in result.stdout.split('\n'):
|
||
|
|
m = port_re.match(line.strip())
|
||
|
|
if m:
|
||
|
|
parts = m.group(4).strip().split()
|
||
|
|
services.append({
|
||
|
|
'port': int(m.group(1)),
|
||
|
|
'service': parts[0] if parts else m.group(3),
|
||
|
|
'version': parts[1] if len(parts) > 1 else '',
|
||
|
|
'host': target,
|
||
|
|
})
|
||
|
|
except Exception as e:
|
||
|
|
self.print_status(f"Scan failed: {e}", "error")
|
||
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
||
|
|
return
|
||
|
|
|
||
|
|
elif choice == "3":
|
||
|
|
print(f"\n{Colors.DIM}Enter services (format: port service version), empty line to finish:{Colors.RESET}")
|
||
|
|
while True:
|
||
|
|
line = input(f"{Colors.WHITE} > {Colors.RESET}").strip()
|
||
|
|
if not line:
|
||
|
|
break
|
||
|
|
parts = line.split()
|
||
|
|
if len(parts) >= 2:
|
||
|
|
services.append({
|
||
|
|
'port': int(parts[0]) if parts[0].isdigit() else 0,
|
||
|
|
'service': parts[1],
|
||
|
|
'version': parts[2] if len(parts) > 2 else '',
|
||
|
|
})
|
||
|
|
else:
|
||
|
|
return
|
||
|
|
|
||
|
|
if not services:
|
||
|
|
self.print_status("No services to analyze", "warning")
|
||
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
||
|
|
return
|
||
|
|
|
||
|
|
# Try LLM-based suggestion first
|
||
|
|
suggestions = []
|
||
|
|
try:
|
||
|
|
from core.llm import get_llm
|
||
|
|
llm = get_llm()
|
||
|
|
if llm.is_loaded:
|
||
|
|
self.print_status("Using LLM for exploit analysis...", "info")
|
||
|
|
prompt = self._build_exploit_prompt(services, cves)
|
||
|
|
|
||
|
|
print(f"\n{Colors.CYAN}{'─' * 60}{Colors.RESET}")
|
||
|
|
print(f"{Colors.BOLD}Exploit Analysis{Colors.RESET}")
|
||
|
|
print(f"{Colors.CYAN}{'─' * 60}{Colors.RESET}\n")
|
||
|
|
|
||
|
|
# Stream response
|
||
|
|
full_response = ""
|
||
|
|
for token in llm.generate(prompt, stream=True, max_tokens=1024):
|
||
|
|
print(token, end='', flush=True)
|
||
|
|
full_response += token
|
||
|
|
print()
|
||
|
|
|
||
|
|
suggestions = self._parse_exploit_suggestions(full_response)
|
||
|
|
else:
|
||
|
|
raise Exception("LLM not loaded")
|
||
|
|
except Exception:
|
||
|
|
# Fallback: direct CVE-to-MSF mapping
|
||
|
|
self.print_status("Using direct CVE-to-MSF mapping (no LLM)...", "info")
|
||
|
|
suggestions = self._fallback_exploit_suggestions(services, cves)
|
||
|
|
|
||
|
|
# Display suggestions
|
||
|
|
if suggestions:
|
||
|
|
print(f"\n{Colors.CYAN}{'─' * 60}{Colors.RESET}")
|
||
|
|
print(f"{Colors.BOLD}Suggested Exploits{Colors.RESET}")
|
||
|
|
print(f"{Colors.CYAN}{'─' * 60}{Colors.RESET}\n")
|
||
|
|
|
||
|
|
for i, s in enumerate(suggestions, 1):
|
||
|
|
print(f" {Colors.RED}[{i}]{Colors.RESET} {s.get('module', 'N/A')}")
|
||
|
|
print(f" Target: {s.get('target', 'N/A')}")
|
||
|
|
if s.get('cve'):
|
||
|
|
print(f" CVE: {s['cve']}")
|
||
|
|
if s.get('reasoning'):
|
||
|
|
print(f" {Colors.DIM}{s['reasoning']}{Colors.RESET}")
|
||
|
|
print()
|
||
|
|
|
||
|
|
self._offer_autoload(suggestions)
|
||
|
|
else:
|
||
|
|
self.print_status("No matching exploits found in module library", "info")
|
||
|
|
|
||
|
|
input(f"\n{Colors.WHITE}Press Enter to continue...{Colors.RESET}")
|
||
|
|
|
||
|
|
def _build_exploit_prompt(self, services: list, cves: list) -> str:
|
||
|
|
"""Build LLM prompt for exploit suggestion."""
|
||
|
|
# Gather available MSF modules
|
||
|
|
relevant_modules = []
|
||
|
|
for svc in services:
|
||
|
|
svc_name = svc.get('service', '').lower()
|
||
|
|
results = library_search_modules(svc_name, max_results=5)
|
||
|
|
for mod in results:
|
||
|
|
if mod['path'] not in [m['path'] for m in relevant_modules]:
|
||
|
|
relevant_modules.append(mod)
|
||
|
|
|
||
|
|
# Also search by CVE
|
||
|
|
for cve in cves[:10]:
|
||
|
|
cve_id = cve.get('cve_id', '')
|
||
|
|
if cve_id:
|
||
|
|
results = library_search_modules(cve_id, max_results=3)
|
||
|
|
for mod in results:
|
||
|
|
if mod['path'] not in [m['path'] for m in relevant_modules]:
|
||
|
|
relevant_modules.append(mod)
|
||
|
|
|
||
|
|
prompt = "You are a penetration testing assistant. Based on the following target information, suggest the top 5 attack paths.\n\n"
|
||
|
|
prompt += "TARGET SERVICES:\n"
|
||
|
|
for svc in services:
|
||
|
|
prompt += f" - Port {svc.get('port', '?')}: {svc.get('service', '?')} {svc.get('version', '')}\n"
|
||
|
|
|
||
|
|
if cves:
|
||
|
|
prompt += "\nKNOWN VULNERABILITIES:\n"
|
||
|
|
for cve in cves[:15]:
|
||
|
|
prompt += f" - {cve.get('cve_id', '?')} ({cve.get('severity', '?')} {cve.get('cvss_score', '?')}): {(cve.get('description', '') or '')[:100]}\n"
|
||
|
|
|
||
|
|
if relevant_modules:
|
||
|
|
prompt += "\nAVAILABLE METASPLOIT MODULES:\n"
|
||
|
|
for mod in relevant_modules[:15]:
|
||
|
|
prompt += f" - {mod['path']}: {mod.get('name', '')}\n"
|
||
|
|
|
||
|
|
prompt += "\nFor each suggestion, provide: module path, target service, CVE (if applicable), and reasoning.\n"
|
||
|
|
prompt += "Format each as: RANK. MODULE_PATH | TARGET | CVE | REASONING\n"
|
||
|
|
|
||
|
|
return prompt
|
||
|
|
|
||
|
|
def _parse_exploit_suggestions(self, response: str) -> list:
|
||
|
|
"""Parse exploit suggestions from LLM response."""
|
||
|
|
suggestions = []
|
||
|
|
lines = response.split('\n')
|
||
|
|
|
||
|
|
for line in lines:
|
||
|
|
line = line.strip()
|
||
|
|
if not line:
|
||
|
|
continue
|
||
|
|
|
||
|
|
# Try to parse "N. module | target | cve | reasoning" format
|
||
|
|
if '|' in line and ('/' in line or 'exploit' in line.lower() or 'auxiliary' in line.lower()):
|
||
|
|
parts = [p.strip() for p in line.split('|')]
|
||
|
|
# Remove leading number
|
||
|
|
first = re.sub(r'^\d+\.\s*', '', parts[0])
|
||
|
|
|
||
|
|
suggestion = {
|
||
|
|
'module': first,
|
||
|
|
'target': parts[1] if len(parts) > 1 else '',
|
||
|
|
'cve': parts[2] if len(parts) > 2 else '',
|
||
|
|
'reasoning': parts[3] if len(parts) > 3 else '',
|
||
|
|
}
|
||
|
|
suggestions.append(suggestion)
|
||
|
|
|
||
|
|
return suggestions[:5]
|
||
|
|
|
||
|
|
def _fallback_exploit_suggestions(self, services: list, cves: list) -> list:
|
||
|
|
"""Fallback exploit suggestion using direct CVE-to-MSF mapping."""
|
||
|
|
suggestions = []
|
||
|
|
seen_modules = set()
|
||
|
|
|
||
|
|
# Search by CVE
|
||
|
|
for cve in cves[:20]:
|
||
|
|
cve_id = cve.get('cve_id', '')
|
||
|
|
if not cve_id:
|
||
|
|
continue
|
||
|
|
results = library_search_modules(cve_id, max_results=3)
|
||
|
|
for mod in results:
|
||
|
|
if mod['path'] not in seen_modules:
|
||
|
|
seen_modules.add(mod['path'])
|
||
|
|
suggestions.append({
|
||
|
|
'module': mod['path'],
|
||
|
|
'target': mod.get('name', ''),
|
||
|
|
'cve': cve_id,
|
||
|
|
'reasoning': f"CVSS {cve.get('cvss_score', '?')} - Direct CVE match",
|
||
|
|
})
|
||
|
|
|
||
|
|
# Search by service name
|
||
|
|
for svc in services:
|
||
|
|
svc_name = svc.get('service', '').lower()
|
||
|
|
if not svc_name:
|
||
|
|
continue
|
||
|
|
results = library_search_modules(svc_name, max_results=3)
|
||
|
|
for mod in results:
|
||
|
|
if mod['path'] not in seen_modules and mod['path'].startswith('exploit'):
|
||
|
|
seen_modules.add(mod['path'])
|
||
|
|
suggestions.append({
|
||
|
|
'module': mod['path'],
|
||
|
|
'target': f"{svc_name} on port {svc.get('port', '?')}",
|
||
|
|
'cve': ', '.join(mod.get('cve', []) or []),
|
||
|
|
'reasoning': f"Service match: {svc_name} {svc.get('version', '')}",
|
||
|
|
})
|
||
|
|
|
||
|
|
return suggestions[:5]
|
||
|
|
|
||
|
|
def _offer_autoload(self, suggestions: list):
|
||
|
|
"""Offer to auto-load a suggested module."""
|
||
|
|
choice = input(f"\n{Colors.WHITE}Load a module? (enter number or 0 to skip): {Colors.RESET}").strip()
|
||
|
|
if not choice or choice == '0':
|
||
|
|
return
|
||
|
|
|
||
|
|
try:
|
||
|
|
idx = int(choice) - 1
|
||
|
|
if 0 <= idx < len(suggestions):
|
||
|
|
module_path = suggestions[idx].get('module', '')
|
||
|
|
if module_path and '/' in module_path:
|
||
|
|
self.print_status(f"Loading {module_path}...", "info")
|
||
|
|
self._select_module(module_path)
|
||
|
|
except (ValueError, IndexError):
|
||
|
|
pass
|
||
|
|
|
||
|
|
# =========================================================================
|
||
|
|
# MAIN LOOP
|
||
|
|
# =========================================================================
|
||
|
|
|
||
|
|
def run(self):
|
||
|
|
"""Main loop."""
|
||
|
|
while True:
|
||
|
|
self.show_main_menu()
|
||
|
|
|
||
|
|
try:
|
||
|
|
choice = input(f"{Colors.WHITE} Select: {Colors.RESET}").strip()
|
||
|
|
|
||
|
|
if choice == "0":
|
||
|
|
break
|
||
|
|
elif choice == "1":
|
||
|
|
self.show_target_settings()
|
||
|
|
elif choice == "2":
|
||
|
|
self.show_module_browser()
|
||
|
|
elif choice == "3":
|
||
|
|
self.search_modules()
|
||
|
|
elif choice == "4":
|
||
|
|
self.show_current_module()
|
||
|
|
elif choice == "5":
|
||
|
|
self.run_module()
|
||
|
|
elif choice == "6":
|
||
|
|
self.show_sessions()
|
||
|
|
elif choice == "7":
|
||
|
|
self.show_jobs()
|
||
|
|
elif choice == "8":
|
||
|
|
self.console_command()
|
||
|
|
elif choice == "9":
|
||
|
|
self.quick_scan()
|
||
|
|
elif choice.lower() == "e":
|
||
|
|
self.exploit_suggester()
|
||
|
|
|
||
|
|
except (EOFError, KeyboardInterrupt):
|
||
|
|
print()
|
||
|
|
break
|
||
|
|
|
||
|
|
|
||
|
|
def run():
|
||
|
|
"""Module entry point."""
|
||
|
|
menu = MSFMenu()
|
||
|
|
menu.run()
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
run()
|