""" Masscan Penetration Testing Module This module uses Masscan for high-speed Internet-wide port scanning. """ # Copyright (c) 2025 FuzzingLabs # # Licensed under the Business Source License 1.1 (BSL). See the LICENSE file # at the root of this repository for details. # # After the Change Date (four years from publication), this version of the # Licensed Work will be made available under the Apache License, Version 2.0. # See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0 # # Additional attribution and requirements are provided in the NOTICE file. import asyncio import json from pathlib import Path from typing import Dict, Any, List import subprocess import logging from ..base import BaseModule, ModuleMetadata, ModuleFinding, ModuleResult from . import register_module logger = logging.getLogger(__name__) @register_module class MasscanModule(BaseModule): """Masscan high-speed port scanner module""" def get_metadata(self) -> ModuleMetadata: """Get module metadata""" return ModuleMetadata( name="masscan", version="1.3.2", description="High-speed Internet-wide port scanner for large-scale network discovery", author="FuzzForge Team", category="penetration_testing", tags=["port-scan", "network", "discovery", "high-speed", "mass-scan"], input_schema={ "type": "object", "properties": { "targets": { "type": "array", "items": {"type": "string"}, "description": "List of targets (IP addresses, CIDR ranges, domains)" }, "target_file": { "type": "string", "description": "File containing targets to scan" }, "ports": { "type": "string", "default": "1-1000", "description": "Port range or specific ports to scan" }, "top_ports": { "type": "integer", "description": "Scan top N most common ports" }, "rate": { "type": "integer", "default": 1000, "description": "Packet transmission rate (packets/second)" }, "max_rate": { "type": "integer", "description": "Maximum packet rate limit" }, "connection_timeout": { "type": "integer", "default": 10, "description": "Connection timeout in seconds" }, "wait_time": { "type": "integer", "default": 10, "description": "Time to wait for responses (seconds)" }, "retries": { "type": "integer", "default": 0, "description": "Number of retries for failed connections" }, "randomize_hosts": { "type": "boolean", "default": True, "description": "Randomize host order" }, "source_ip": { "type": "string", "description": "Source IP address to use" }, "source_port": { "type": "string", "description": "Source port range to use" }, "interface": { "type": "string", "description": "Network interface to use" }, "router_mac": { "type": "string", "description": "Router MAC address" }, "exclude_targets": { "type": "array", "items": {"type": "string"}, "description": "Targets to exclude from scanning" }, "exclude_file": { "type": "string", "description": "File containing targets to exclude" }, "ping": { "type": "boolean", "default": False, "description": "Include ping scan" }, "banners": { "type": "boolean", "default": False, "description": "Grab banners from services" }, "http_user_agent": { "type": "string", "description": "HTTP User-Agent string for banner grabbing" } } }, output_schema={ "type": "object", "properties": { "findings": { "type": "array", "items": { "type": "object", "properties": { "host": {"type": "string"}, "port": {"type": "integer"}, "protocol": {"type": "string"}, "state": {"type": "string"}, "banner": {"type": "string"} } } } } } ) def validate_config(self, config: Dict[str, Any]) -> bool: """Validate configuration""" targets = config.get("targets", []) target_file = config.get("target_file") if not targets and not target_file: raise ValueError("Either 'targets' or 'target_file' must be specified") rate = config.get("rate", 1000) if rate <= 0 or rate > 10000000: # Masscan limit raise ValueError("Rate must be between 1 and 10,000,000 packets/second") return True async def execute(self, config: Dict[str, Any], workspace: Path) -> ModuleResult: """Execute Masscan port scanning""" self.start_timer() try: # Validate inputs self.validate_config(config) self.validate_workspace(workspace) logger.info("Running Masscan high-speed port scan") # Prepare target specification target_args = self._prepare_targets(config, workspace) if not target_args: logger.info("No targets specified for scanning") return self.create_result( findings=[], status="success", summary={"total_findings": 0, "targets_scanned": 0} ) # Run Masscan scan findings = await self._run_masscan_scan(target_args, config, workspace) # Create summary target_count = len(config.get("targets", [])) if config.get("targets") else 1 summary = self._create_summary(findings, target_count) logger.info(f"Masscan found {len(findings)} open ports") return self.create_result( findings=findings, status="success", summary=summary ) except Exception as e: logger.error(f"Masscan module failed: {e}") return self.create_result( findings=[], status="failed", error=str(e) ) def _prepare_targets(self, config: Dict[str, Any], workspace: Path) -> List[str]: """Prepare target arguments for masscan""" target_args = [] # Add targets from list targets = config.get("targets", []) for target in targets: target_args.extend(["-t", target]) # Add targets from file target_file = config.get("target_file") if target_file: target_path = workspace / target_file if target_path.exists(): target_args.extend(["-iL", str(target_path)]) else: raise FileNotFoundError(f"Target file not found: {target_file}") return target_args async def _run_masscan_scan(self, target_args: List[str], config: Dict[str, Any], workspace: Path) -> List[ModuleFinding]: """Run Masscan scan""" findings = [] try: # Build masscan command cmd = ["masscan"] # Add target arguments cmd.extend(target_args) # Add port specification if config.get("top_ports"): # Masscan doesn't have built-in top ports, use common ports top_ports = self._get_top_ports(config["top_ports"]) cmd.extend(["-p", top_ports]) else: ports = config.get("ports", "1-1000") cmd.extend(["-p", ports]) # Add rate limiting rate = config.get("rate", 1000) cmd.extend(["--rate", str(rate)]) # Add max rate if specified max_rate = config.get("max_rate") if max_rate: cmd.extend(["--max-rate", str(max_rate)]) # Add connection timeout connection_timeout = config.get("connection_timeout", 10) cmd.extend(["--connection-timeout", str(connection_timeout)]) # Add wait time wait_time = config.get("wait_time", 10) cmd.extend(["--wait", str(wait_time)]) # Add retries retries = config.get("retries", 0) if retries > 0: cmd.extend(["--retries", str(retries)]) # Add randomization if config.get("randomize_hosts", True): cmd.append("--randomize-hosts") # Add source IP source_ip = config.get("source_ip") if source_ip: cmd.extend(["--source-ip", source_ip]) # Add source port source_port = config.get("source_port") if source_port: cmd.extend(["--source-port", source_port]) # Add interface interface = config.get("interface") if interface: cmd.extend(["-e", interface]) # Add router MAC router_mac = config.get("router_mac") if router_mac: cmd.extend(["--router-mac", router_mac]) # Add exclude targets exclude_targets = config.get("exclude_targets", []) for exclude in exclude_targets: cmd.extend(["--exclude", exclude]) # Add exclude file exclude_file = config.get("exclude_file") if exclude_file: exclude_path = workspace / exclude_file if exclude_path.exists(): cmd.extend(["--excludefile", str(exclude_path)]) # Add ping scan if config.get("ping", False): cmd.append("--ping") # Add banner grabbing if config.get("banners", False): cmd.append("--banners") # Add HTTP User-Agent user_agent = config.get("http_user_agent") if user_agent: cmd.extend(["--http-user-agent", user_agent]) # Set output format to JSON output_file = workspace / "masscan_results.json" cmd.extend(["-oJ", str(output_file)]) logger.debug(f"Running command: {' '.join(cmd)}") # Run masscan process = await asyncio.create_subprocess_exec( *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, cwd=workspace ) stdout, stderr = await process.communicate() # Parse results from JSON file if output_file.exists(): findings = self._parse_masscan_json(output_file, workspace) else: # Try to parse stdout if no file was created if stdout: findings = self._parse_masscan_output(stdout.decode(), workspace) else: error_msg = stderr.decode() logger.error(f"Masscan scan failed: {error_msg}") except Exception as e: logger.warning(f"Error running Masscan scan: {e}") return findings def _get_top_ports(self, count: int) -> str: """Get top N common ports for masscan""" # Common ports based on Nmap's top ports list top_ports = [ 80, 23, 443, 21, 22, 25, 53, 110, 111, 995, 993, 143, 993, 995, 587, 465, 109, 88, 53, 135, 139, 445, 993, 995, 143, 25, 110, 465, 587, 993, 995, 80, 8080, 443, 8443, 8000, 8888, 8880, 2222, 9999, 3389, 5900, 5901, 1433, 3306, 5432, 1521, 50000, 1494, 554, 37, 79, 82, 5060, 50030 ] # Take first N unique ports selected_ports = list(dict.fromkeys(top_ports))[:count] return ",".join(map(str, selected_ports)) def _parse_masscan_json(self, json_file: Path, workspace: Path) -> List[ModuleFinding]: """Parse Masscan JSON output into findings""" findings = [] try: with open(json_file, 'r') as f: content = f.read().strip() # Masscan outputs JSONL format (one JSON object per line) for line in content.split('\n'): if not line.strip(): continue try: result = json.loads(line) finding = self._process_masscan_result(result) if finding: findings.append(finding) except json.JSONDecodeError: continue except Exception as e: logger.warning(f"Error parsing Masscan JSON: {e}") return findings def _parse_masscan_output(self, output: str, workspace: Path) -> List[ModuleFinding]: """Parse Masscan text output into findings""" findings = [] try: for line in output.split('\n'): if not line.strip() or line.startswith('#'): continue # Parse format: "open tcp 80 1.2.3.4" parts = line.split() if len(parts) >= 4 and parts[0] == "open": protocol = parts[1] port = int(parts[2]) ip = parts[3] result = { "ip": ip, "ports": [{"port": port, "proto": protocol, "status": "open"}] } finding = self._process_masscan_result(result) if finding: findings.append(finding) except Exception as e: logger.warning(f"Error parsing Masscan output: {e}") return findings def _process_masscan_result(self, result: Dict) -> ModuleFinding: """Process a single Masscan result into a finding""" try: ip_address = result.get("ip", "") ports_data = result.get("ports", []) if not ip_address or not ports_data: return None # Process first port (Masscan typically reports one port per result) port_data = ports_data[0] port_number = port_data.get("port", 0) protocol = port_data.get("proto", "tcp") status = port_data.get("status", "open") service = port_data.get("service", {}) banner = service.get("banner", "") if service else "" # Only report open ports if status != "open": return None # Determine severity based on port severity = self._get_port_severity(port_number) # Get category category = self._get_port_category(port_number) # Create description description = f"Open port {port_number}/{protocol} on {ip_address}" if banner: description += f" (Banner: {banner[:100]})" # Create finding finding = self.create_finding( title=f"Open Port: {port_number}/{protocol}", description=description, severity=severity, category=category, file_path=None, # Network scan, no file recommendation=self._get_port_recommendation(port_number, banner), metadata={ "host": ip_address, "port": port_number, "protocol": protocol, "status": status, "banner": banner, "service_info": service } ) return finding except Exception as e: logger.warning(f"Error processing Masscan result: {e}") return None def _get_port_severity(self, port: int) -> str: """Determine severity based on port number""" # High risk ports (commonly exploited or sensitive services) high_risk_ports = [21, 23, 135, 139, 445, 1433, 1521, 3389, 5900, 6379, 27017] # Medium risk ports (network services that could be risky if misconfigured) medium_risk_ports = [22, 25, 53, 110, 143, 993, 995, 3306, 5432] # Web ports are generally lower risk but still noteworthy web_ports = [80, 443, 8080, 8443, 8000, 8888] if port in high_risk_ports: return "high" elif port in medium_risk_ports: return "medium" elif port in web_ports: return "low" elif port < 1024: # Well-known ports return "medium" else: return "low" def _get_port_category(self, port: int) -> str: """Determine category based on port number""" if port in [80, 443, 8080, 8443, 8000, 8888]: return "web_services" elif port == 22: return "remote_access" elif port in [20, 21]: return "file_transfer" elif port in [25, 110, 143, 587, 993, 995]: return "email_services" elif port in [1433, 3306, 5432, 1521, 27017, 6379]: return "database_services" elif port == 3389: return "remote_desktop" elif port == 53: return "dns_services" elif port in [135, 139, 445]: return "windows_services" elif port in [23, 5900]: return "insecure_protocols" else: return "network_services" def _get_port_recommendation(self, port: int, banner: str) -> str: """Generate recommendation based on port and banner""" # Port-specific recommendations recommendations = { 21: "FTP service detected. Consider using SFTP instead for secure file transfer.", 22: "SSH service detected. Ensure strong authentication and key-based access.", 23: "Telnet service detected. Replace with SSH for secure remote access.", 25: "SMTP service detected. Ensure proper authentication and encryption.", 53: "DNS service detected. Verify it's not an open resolver.", 80: "HTTP service detected. Consider upgrading to HTTPS.", 110: "POP3 service detected. Consider using secure alternatives like IMAPS.", 135: "Windows RPC service exposed. Restrict access if not required.", 139: "NetBIOS service detected. Ensure proper access controls.", 143: "IMAP service detected. Consider using encrypted IMAPS.", 445: "SMB service detected. Ensure latest patches and access controls.", 443: "HTTPS service detected. Verify SSL/TLS configuration.", 993: "IMAPS service detected. Verify certificate configuration.", 995: "POP3S service detected. Verify certificate configuration.", 1433: "SQL Server detected. Ensure strong authentication and network restrictions.", 1521: "Oracle DB detected. Ensure proper security configuration.", 3306: "MySQL service detected. Secure with strong passwords and access controls.", 3389: "RDP service detected. Use strong passwords and consider VPN access.", 5432: "PostgreSQL detected. Ensure proper authentication and access controls.", 5900: "VNC service detected. Use strong passwords and encryption.", 6379: "Redis service detected. Configure authentication and access controls.", 8080: "HTTP proxy/web service detected. Verify if exposure is intended.", 8443: "HTTPS service on non-standard port. Verify certificate configuration." } recommendation = recommendations.get(port, f"Port {port} is open. Verify if this service is required and properly secured.") # Add banner-specific advice if banner: banner_lower = banner.lower() if "default" in banner_lower or "admin" in banner_lower: recommendation += " Default credentials may be in use - change immediately." elif any(version in banner_lower for version in ["1.0", "2.0", "old", "legacy"]): recommendation += " Service version appears outdated - consider upgrading." return recommendation def _create_summary(self, findings: List[ModuleFinding], targets_count: int) -> Dict[str, Any]: """Create analysis summary""" severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0} category_counts = {} port_counts = {} host_counts = {} protocol_counts = {"tcp": 0, "udp": 0} for finding in findings: # Count by severity severity_counts[finding.severity] += 1 # Count by category category = finding.category category_counts[category] = category_counts.get(category, 0) + 1 # Count by port port = finding.metadata.get("port") if port: port_counts[port] = port_counts.get(port, 0) + 1 # Count by host host = finding.metadata.get("host", "unknown") host_counts[host] = host_counts.get(host, 0) + 1 # Count by protocol protocol = finding.metadata.get("protocol", "tcp") if protocol in protocol_counts: protocol_counts[protocol] += 1 return { "total_findings": len(findings), "targets_scanned": targets_count, "severity_counts": severity_counts, "category_counts": category_counts, "protocol_counts": protocol_counts, "unique_hosts": len(host_counts), "top_ports": dict(sorted(port_counts.items(), key=lambda x: x[1], reverse=True)[:10]), "host_counts": dict(sorted(host_counts.items(), key=lambda x: x[1], reverse=True)[:10]) }