mirror of
https://github.com/FuzzingLabs/fuzzforge_ai.git
synced 2026-07-11 16:26:32 +02:00
Initial commit
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
"""
|
||||
Penetration Testing Modules
|
||||
|
||||
This package contains modules for penetration testing and vulnerability assessment.
|
||||
|
||||
Available modules:
|
||||
- Nuclei: Fast and customizable vulnerability scanner
|
||||
- Nmap: Network discovery and security auditing
|
||||
- Masscan: High-speed Internet-wide port scanner
|
||||
- SQLMap: Automatic SQL injection detection and exploitation
|
||||
"""
|
||||
# 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.
|
||||
|
||||
|
||||
from typing import List, Type
|
||||
from ..base import BaseModule
|
||||
|
||||
# Module registry for automatic discovery
|
||||
PENETRATION_TESTING_MODULES: List[Type[BaseModule]] = []
|
||||
|
||||
def register_module(module_class: Type[BaseModule]):
|
||||
"""Register a penetration testing module"""
|
||||
PENETRATION_TESTING_MODULES.append(module_class)
|
||||
return module_class
|
||||
|
||||
def get_available_modules() -> List[Type[BaseModule]]:
|
||||
"""Get all available penetration testing modules"""
|
||||
return PENETRATION_TESTING_MODULES.copy()
|
||||
|
||||
# Import modules to trigger registration
|
||||
from .nuclei import NucleiModule
|
||||
from .nmap import NmapModule
|
||||
from .masscan import MasscanModule
|
||||
from .sqlmap import SQLMapModule
|
||||
@@ -0,0 +1,607 @@
|
||||
"""
|
||||
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])
|
||||
}
|
||||
@@ -0,0 +1,710 @@
|
||||
"""
|
||||
Nmap Penetration Testing Module
|
||||
|
||||
This module uses Nmap for network discovery, port scanning, and security auditing.
|
||||
"""
|
||||
# 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
|
||||
import xml.etree.ElementTree as ET
|
||||
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 NmapModule(BaseModule):
|
||||
"""Nmap network discovery and security auditing module"""
|
||||
|
||||
def get_metadata(self) -> ModuleMetadata:
|
||||
"""Get module metadata"""
|
||||
return ModuleMetadata(
|
||||
name="nmap",
|
||||
version="7.94",
|
||||
description="Network discovery and security auditing using Nmap",
|
||||
author="FuzzForge Team",
|
||||
category="penetration_testing",
|
||||
tags=["network", "port-scan", "discovery", "security-audit", "service-detection"],
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"targets": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "List of targets (IP addresses, domains, CIDR ranges)"
|
||||
},
|
||||
"target_file": {
|
||||
"type": "string",
|
||||
"description": "File containing targets to scan"
|
||||
},
|
||||
"scan_type": {
|
||||
"type": "string",
|
||||
"enum": ["syn", "tcp", "udp", "ack", "window", "maimon"],
|
||||
"default": "syn",
|
||||
"description": "Type of scan to perform"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"service_detection": {
|
||||
"type": "boolean",
|
||||
"default": True,
|
||||
"description": "Enable service version detection"
|
||||
},
|
||||
"os_detection": {
|
||||
"type": "boolean",
|
||||
"default": False,
|
||||
"description": "Enable OS detection (requires root)"
|
||||
},
|
||||
"script_scan": {
|
||||
"type": "boolean",
|
||||
"default": True,
|
||||
"description": "Enable default NSE scripts"
|
||||
},
|
||||
"scripts": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Specific NSE scripts to run"
|
||||
},
|
||||
"script_categories": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "NSE script categories to run (safe, vuln, etc.)"
|
||||
},
|
||||
"timing_template": {
|
||||
"type": "string",
|
||||
"enum": ["paranoid", "sneaky", "polite", "normal", "aggressive", "insane"],
|
||||
"default": "normal",
|
||||
"description": "Timing template (0-5)"
|
||||
},
|
||||
"max_retries": {
|
||||
"type": "integer",
|
||||
"default": 1,
|
||||
"description": "Maximum number of retries"
|
||||
},
|
||||
"host_timeout": {
|
||||
"type": "integer",
|
||||
"default": 300,
|
||||
"description": "Host timeout in seconds"
|
||||
},
|
||||
"min_rate": {
|
||||
"type": "integer",
|
||||
"description": "Minimum packet rate (packets/second)"
|
||||
},
|
||||
"max_rate": {
|
||||
"type": "integer",
|
||||
"description": "Maximum packet rate (packets/second)"
|
||||
},
|
||||
"stealth": {
|
||||
"type": "boolean",
|
||||
"default": False,
|
||||
"description": "Enable stealth scanning options"
|
||||
},
|
||||
"skip_discovery": {
|
||||
"type": "boolean",
|
||||
"default": False,
|
||||
"description": "Skip host discovery (treat all as online)"
|
||||
}
|
||||
}
|
||||
},
|
||||
output_schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"findings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"host": {"type": "string"},
|
||||
"port": {"type": "integer"},
|
||||
"service": {"type": "string"},
|
||||
"state": {"type": "string"},
|
||||
"version": {"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")
|
||||
|
||||
scan_type = config.get("scan_type", "syn")
|
||||
valid_scan_types = ["syn", "tcp", "udp", "ack", "window", "maimon"]
|
||||
if scan_type not in valid_scan_types:
|
||||
raise ValueError(f"Invalid scan type: {scan_type}. Valid: {valid_scan_types}")
|
||||
|
||||
timing = config.get("timing_template", "normal")
|
||||
valid_timings = ["paranoid", "sneaky", "polite", "normal", "aggressive", "insane"]
|
||||
if timing not in valid_timings:
|
||||
raise ValueError(f"Invalid timing template: {timing}. Valid: {valid_timings}")
|
||||
|
||||
return True
|
||||
|
||||
async def execute(self, config: Dict[str, Any], workspace: Path) -> ModuleResult:
|
||||
"""Execute Nmap network scanning"""
|
||||
self.start_timer()
|
||||
|
||||
try:
|
||||
# Validate inputs
|
||||
self.validate_config(config)
|
||||
self.validate_workspace(workspace)
|
||||
|
||||
logger.info("Running Nmap network scan")
|
||||
|
||||
# Prepare target file
|
||||
target_file = await self._prepare_targets(config, workspace)
|
||||
if not target_file:
|
||||
logger.info("No targets specified for scanning")
|
||||
return self.create_result(
|
||||
findings=[],
|
||||
status="success",
|
||||
summary={"total_findings": 0, "hosts_scanned": 0}
|
||||
)
|
||||
|
||||
# Run Nmap scan
|
||||
findings = await self._run_nmap_scan(target_file, 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"Nmap found {len(findings)} results")
|
||||
|
||||
return self.create_result(
|
||||
findings=findings,
|
||||
status="success",
|
||||
summary=summary
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Nmap module failed: {e}")
|
||||
return self.create_result(
|
||||
findings=[],
|
||||
status="failed",
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
async def _prepare_targets(self, config: Dict[str, Any], workspace: Path) -> Path:
|
||||
"""Prepare target file for scanning"""
|
||||
targets = config.get("targets", [])
|
||||
target_file = config.get("target_file")
|
||||
|
||||
if target_file:
|
||||
# Use existing target file
|
||||
target_path = workspace / target_file
|
||||
if target_path.exists():
|
||||
return target_path
|
||||
else:
|
||||
raise FileNotFoundError(f"Target file not found: {target_file}")
|
||||
|
||||
if targets:
|
||||
# Create temporary target file
|
||||
target_path = workspace / "nmap_targets.txt"
|
||||
with open(target_path, 'w') as f:
|
||||
for target in targets:
|
||||
f.write(f"{target}\n")
|
||||
return target_path
|
||||
|
||||
return None
|
||||
|
||||
async def _run_nmap_scan(self, target_file: Path, config: Dict[str, Any], workspace: Path) -> List[ModuleFinding]:
|
||||
"""Run Nmap scan"""
|
||||
findings = []
|
||||
|
||||
try:
|
||||
# Build nmap command
|
||||
cmd = ["nmap"]
|
||||
|
||||
# Add scan type
|
||||
scan_type = config.get("scan_type", "syn")
|
||||
scan_type_map = {
|
||||
"syn": "-sS",
|
||||
"tcp": "-sT",
|
||||
"udp": "-sU",
|
||||
"ack": "-sA",
|
||||
"window": "-sW",
|
||||
"maimon": "-sM"
|
||||
}
|
||||
cmd.append(scan_type_map[scan_type])
|
||||
|
||||
# Add port specification
|
||||
if config.get("top_ports"):
|
||||
cmd.extend(["--top-ports", str(config["top_ports"])])
|
||||
else:
|
||||
ports = config.get("ports", "1-1000")
|
||||
cmd.extend(["-p", ports])
|
||||
|
||||
# Add service detection
|
||||
if config.get("service_detection", True):
|
||||
cmd.append("-sV")
|
||||
|
||||
# Add OS detection
|
||||
if config.get("os_detection", False):
|
||||
cmd.append("-O")
|
||||
|
||||
# Add script scanning
|
||||
if config.get("script_scan", True):
|
||||
cmd.append("-sC")
|
||||
|
||||
# Add specific scripts
|
||||
scripts = config.get("scripts", [])
|
||||
if scripts:
|
||||
cmd.extend(["--script", ",".join(scripts)])
|
||||
|
||||
# Add script categories
|
||||
script_categories = config.get("script_categories", [])
|
||||
if script_categories:
|
||||
cmd.extend(["--script", ",".join(script_categories)])
|
||||
|
||||
# Add timing template
|
||||
timing = config.get("timing_template", "normal")
|
||||
timing_map = {
|
||||
"paranoid": "-T0",
|
||||
"sneaky": "-T1",
|
||||
"polite": "-T2",
|
||||
"normal": "-T3",
|
||||
"aggressive": "-T4",
|
||||
"insane": "-T5"
|
||||
}
|
||||
cmd.append(timing_map[timing])
|
||||
|
||||
# Add retry options
|
||||
max_retries = config.get("max_retries", 1)
|
||||
cmd.extend(["--max-retries", str(max_retries)])
|
||||
|
||||
# Add timeout
|
||||
host_timeout = config.get("host_timeout", 300)
|
||||
cmd.extend(["--host-timeout", f"{host_timeout}s"])
|
||||
|
||||
# Add rate limiting
|
||||
if config.get("min_rate"):
|
||||
cmd.extend(["--min-rate", str(config["min_rate"])])
|
||||
|
||||
if config.get("max_rate"):
|
||||
cmd.extend(["--max-rate", str(config["max_rate"])])
|
||||
|
||||
# Add stealth options
|
||||
if config.get("stealth", False):
|
||||
cmd.extend(["-f", "--randomize-hosts"])
|
||||
|
||||
# Skip host discovery if requested
|
||||
if config.get("skip_discovery", False):
|
||||
cmd.append("-Pn")
|
||||
|
||||
# Add output format
|
||||
output_file = workspace / "nmap_results.xml"
|
||||
cmd.extend(["-oX", str(output_file)])
|
||||
|
||||
# Add targets from file
|
||||
cmd.extend(["-iL", str(target_file)])
|
||||
|
||||
# Add verbose and reason flags
|
||||
cmd.extend(["-v", "--reason"])
|
||||
|
||||
logger.debug(f"Running command: {' '.join(cmd)}")
|
||||
|
||||
# Run nmap
|
||||
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 XML file
|
||||
if output_file.exists():
|
||||
findings = self._parse_nmap_xml(output_file, workspace)
|
||||
else:
|
||||
error_msg = stderr.decode()
|
||||
logger.error(f"Nmap scan failed: {error_msg}")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error running Nmap scan: {e}")
|
||||
|
||||
return findings
|
||||
|
||||
def _parse_nmap_xml(self, xml_file: Path, workspace: Path) -> List[ModuleFinding]:
|
||||
"""Parse Nmap XML output into findings"""
|
||||
findings = []
|
||||
|
||||
try:
|
||||
tree = ET.parse(xml_file)
|
||||
root = tree.getroot()
|
||||
|
||||
# Process each host
|
||||
for host_elem in root.findall(".//host"):
|
||||
# Get host information
|
||||
host_status = host_elem.find("status")
|
||||
if host_status is None or host_status.get("state") != "up":
|
||||
continue
|
||||
|
||||
# Get IP address
|
||||
address_elem = host_elem.find("address[@addrtype='ipv4']")
|
||||
if address_elem is None:
|
||||
address_elem = host_elem.find("address[@addrtype='ipv6']")
|
||||
|
||||
if address_elem is None:
|
||||
continue
|
||||
|
||||
ip_address = address_elem.get("addr")
|
||||
|
||||
# Get hostname if available
|
||||
hostname = ""
|
||||
hostnames_elem = host_elem.find("hostnames")
|
||||
if hostnames_elem is not None:
|
||||
hostname_elem = hostnames_elem.find("hostname")
|
||||
if hostname_elem is not None:
|
||||
hostname = hostname_elem.get("name", "")
|
||||
|
||||
# Get OS information
|
||||
os_info = self._extract_os_info(host_elem)
|
||||
|
||||
# Process ports
|
||||
ports_elem = host_elem.find("ports")
|
||||
if ports_elem is not None:
|
||||
for port_elem in ports_elem.findall("port"):
|
||||
finding = self._process_port(port_elem, ip_address, hostname, os_info)
|
||||
if finding:
|
||||
findings.append(finding)
|
||||
|
||||
# Process host scripts
|
||||
host_scripts = host_elem.find("hostscript")
|
||||
if host_scripts is not None:
|
||||
for script_elem in host_scripts.findall("script"):
|
||||
finding = self._process_host_script(script_elem, ip_address, hostname)
|
||||
if finding:
|
||||
findings.append(finding)
|
||||
|
||||
except ET.ParseError as e:
|
||||
logger.warning(f"Failed to parse Nmap XML: {e}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Error processing Nmap results: {e}")
|
||||
|
||||
return findings
|
||||
|
||||
def _extract_os_info(self, host_elem) -> Dict[str, Any]:
|
||||
"""Extract OS information from host element"""
|
||||
os_info = {}
|
||||
|
||||
os_elem = host_elem.find("os")
|
||||
if os_elem is not None:
|
||||
osmatch_elem = os_elem.find("osmatch")
|
||||
if osmatch_elem is not None:
|
||||
os_info["name"] = osmatch_elem.get("name", "")
|
||||
os_info["accuracy"] = osmatch_elem.get("accuracy", "0")
|
||||
|
||||
return os_info
|
||||
|
||||
def _process_port(self, port_elem, ip_address: str, hostname: str, os_info: Dict) -> ModuleFinding:
|
||||
"""Process a port element into a finding"""
|
||||
try:
|
||||
port_id = port_elem.get("portid")
|
||||
protocol = port_elem.get("protocol")
|
||||
|
||||
# Get state
|
||||
state_elem = port_elem.find("state")
|
||||
if state_elem is None:
|
||||
return None
|
||||
|
||||
state = state_elem.get("state")
|
||||
reason = state_elem.get("reason", "")
|
||||
|
||||
# Only report open ports
|
||||
if state != "open":
|
||||
return None
|
||||
|
||||
# Get service information
|
||||
service_elem = port_elem.find("service")
|
||||
service_name = ""
|
||||
service_version = ""
|
||||
service_product = ""
|
||||
service_extra = ""
|
||||
|
||||
if service_elem is not None:
|
||||
service_name = service_elem.get("name", "")
|
||||
service_version = service_elem.get("version", "")
|
||||
service_product = service_elem.get("product", "")
|
||||
service_extra = service_elem.get("extrainfo", "")
|
||||
|
||||
# Determine severity based on service
|
||||
severity = self._get_port_severity(int(port_id), service_name)
|
||||
|
||||
# Get category
|
||||
category = self._get_port_category(int(port_id), service_name)
|
||||
|
||||
# Create description
|
||||
desc_parts = [f"Open port {port_id}/{protocol}"]
|
||||
if service_name:
|
||||
desc_parts.append(f"running {service_name}")
|
||||
if service_product:
|
||||
desc_parts.append(f"({service_product}")
|
||||
if service_version:
|
||||
desc_parts.append(f"version {service_version}")
|
||||
desc_parts.append(")")
|
||||
|
||||
description = " ".join(desc_parts)
|
||||
|
||||
# Process port scripts
|
||||
script_results = []
|
||||
script_elems = port_elem.findall("script")
|
||||
for script_elem in script_elems:
|
||||
script_id = script_elem.get("id", "")
|
||||
script_output = script_elem.get("output", "")
|
||||
if script_output:
|
||||
script_results.append({"id": script_id, "output": script_output})
|
||||
|
||||
# Create finding
|
||||
finding = self.create_finding(
|
||||
title=f"Open Port: {port_id}/{protocol}",
|
||||
description=description,
|
||||
severity=severity,
|
||||
category=category,
|
||||
file_path=None, # Network scan, no file
|
||||
recommendation=self._get_port_recommendation(int(port_id), service_name, script_results),
|
||||
metadata={
|
||||
"host": ip_address,
|
||||
"hostname": hostname,
|
||||
"port": int(port_id),
|
||||
"protocol": protocol,
|
||||
"state": state,
|
||||
"reason": reason,
|
||||
"service_name": service_name,
|
||||
"service_version": service_version,
|
||||
"service_product": service_product,
|
||||
"service_extra": service_extra,
|
||||
"os_info": os_info,
|
||||
"script_results": script_results
|
||||
}
|
||||
)
|
||||
|
||||
return finding
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error processing port: {e}")
|
||||
return None
|
||||
|
||||
def _process_host_script(self, script_elem, ip_address: str, hostname: str) -> ModuleFinding:
|
||||
"""Process a host script result into a finding"""
|
||||
try:
|
||||
script_id = script_elem.get("id", "")
|
||||
script_output = script_elem.get("output", "")
|
||||
|
||||
if not script_output or not script_id:
|
||||
return None
|
||||
|
||||
# Determine if this is a security issue
|
||||
severity = self._get_script_severity(script_id, script_output)
|
||||
|
||||
if severity == "info":
|
||||
# Skip informational scripts
|
||||
return None
|
||||
|
||||
category = self._get_script_category(script_id)
|
||||
|
||||
finding = self.create_finding(
|
||||
title=f"Host Script Result: {script_id}",
|
||||
description=script_output.strip(),
|
||||
severity=severity,
|
||||
category=category,
|
||||
file_path=None,
|
||||
recommendation=self._get_script_recommendation(script_id, script_output),
|
||||
metadata={
|
||||
"host": ip_address,
|
||||
"hostname": hostname,
|
||||
"script_id": script_id,
|
||||
"script_output": script_output.strip()
|
||||
}
|
||||
)
|
||||
|
||||
return finding
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error processing host script: {e}")
|
||||
return None
|
||||
|
||||
def _get_port_severity(self, port: int, service: str) -> str:
|
||||
"""Determine severity based on port and service"""
|
||||
# High risk ports
|
||||
high_risk_ports = [21, 23, 135, 139, 445, 1433, 1521, 3389, 5432, 5900, 6379]
|
||||
# Medium risk ports
|
||||
medium_risk_ports = [22, 25, 53, 110, 143, 993, 995]
|
||||
# Web ports are generally lower risk
|
||||
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, service: str) -> str:
|
||||
"""Determine category based on port and service"""
|
||||
service_lower = service.lower()
|
||||
|
||||
if service_lower in ["http", "https"] or port in [80, 443, 8080, 8443]:
|
||||
return "web_services"
|
||||
elif service_lower in ["ssh"] or port == 22:
|
||||
return "remote_access"
|
||||
elif service_lower in ["ftp", "ftps"] or port in [20, 21]:
|
||||
return "file_transfer"
|
||||
elif service_lower in ["smtp", "pop3", "imap"] or port in [25, 110, 143, 587, 993, 995]:
|
||||
return "email_services"
|
||||
elif service_lower in ["mysql", "postgresql", "mssql", "oracle"] or port in [1433, 3306, 5432, 1521]:
|
||||
return "database_services"
|
||||
elif service_lower in ["rdp"] or port == 3389:
|
||||
return "remote_desktop"
|
||||
elif service_lower in ["dns"] or port == 53:
|
||||
return "dns_services"
|
||||
elif port in [135, 139, 445]:
|
||||
return "windows_services"
|
||||
else:
|
||||
return "network_services"
|
||||
|
||||
def _get_script_severity(self, script_id: str, output: str) -> str:
|
||||
"""Determine severity for script results"""
|
||||
script_lower = script_id.lower()
|
||||
output_lower = output.lower()
|
||||
|
||||
# High severity indicators
|
||||
if any(term in script_lower for term in ["vuln", "exploit", "backdoor"]):
|
||||
return "high"
|
||||
if any(term in output_lower for term in ["vulnerable", "exploit", "critical"]):
|
||||
return "high"
|
||||
|
||||
# Medium severity indicators
|
||||
if any(term in script_lower for term in ["auth", "brute", "enum"]):
|
||||
return "medium"
|
||||
if any(term in output_lower for term in ["anonymous", "default", "weak"]):
|
||||
return "medium"
|
||||
|
||||
# Everything else is informational
|
||||
return "info"
|
||||
|
||||
def _get_script_category(self, script_id: str) -> str:
|
||||
"""Determine category for script results"""
|
||||
script_lower = script_id.lower()
|
||||
|
||||
if "vuln" in script_lower:
|
||||
return "vulnerability_detection"
|
||||
elif "auth" in script_lower or "brute" in script_lower:
|
||||
return "authentication_testing"
|
||||
elif "enum" in script_lower:
|
||||
return "information_gathering"
|
||||
elif "ssl" in script_lower or "tls" in script_lower:
|
||||
return "ssl_tls_testing"
|
||||
else:
|
||||
return "service_detection"
|
||||
|
||||
def _get_port_recommendation(self, port: int, service: str, scripts: List[Dict]) -> str:
|
||||
"""Generate recommendation for open port"""
|
||||
# Check for script-based issues
|
||||
for script in scripts:
|
||||
script_id = script.get("id", "")
|
||||
if "vuln" in script_id.lower():
|
||||
return "Vulnerability detected by NSE scripts. Review and patch the service."
|
||||
|
||||
# Port-specific recommendations
|
||||
if port == 21:
|
||||
return "FTP service detected. Consider using SFTP instead for secure file transfer."
|
||||
elif port == 23:
|
||||
return "Telnet service detected. Use SSH instead for secure remote access."
|
||||
elif port == 135:
|
||||
return "Windows RPC service exposed. Restrict access if not required."
|
||||
elif port in [139, 445]:
|
||||
return "SMB/NetBIOS services detected. Ensure proper access controls and patch levels."
|
||||
elif port == 1433:
|
||||
return "SQL Server detected. Ensure strong authentication and network restrictions."
|
||||
elif port == 3389:
|
||||
return "RDP service detected. Use strong passwords and consider VPN access."
|
||||
elif port in [80, 443]:
|
||||
return "Web service detected. Ensure regular security updates and proper configuration."
|
||||
else:
|
||||
return f"Open port {port} detected. Verify if this service is required and properly secured."
|
||||
|
||||
def _get_script_recommendation(self, script_id: str, output: str) -> str:
|
||||
"""Generate recommendation for script results"""
|
||||
if "vuln" in script_id.lower():
|
||||
return "Vulnerability detected. Apply security patches and updates."
|
||||
elif "auth" in script_id.lower():
|
||||
return "Authentication issue detected. Review and strengthen authentication mechanisms."
|
||||
elif "ssl" in script_id.lower():
|
||||
return "SSL/TLS configuration issue. Update SSL configuration and certificates."
|
||||
else:
|
||||
return "Review the script output and address any security concerns identified."
|
||||
|
||||
def _create_summary(self, findings: List[ModuleFinding], hosts_count: int) -> Dict[str, Any]:
|
||||
"""Create analysis summary"""
|
||||
severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0}
|
||||
category_counts = {}
|
||||
port_counts = {}
|
||||
service_counts = {}
|
||||
host_counts = {}
|
||||
|
||||
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 service
|
||||
service = finding.metadata.get("service_name", "unknown")
|
||||
service_counts[service] = service_counts.get(service, 0) + 1
|
||||
|
||||
# Count by host
|
||||
host = finding.metadata.get("host", "unknown")
|
||||
host_counts[host] = host_counts.get(host, 0) + 1
|
||||
|
||||
return {
|
||||
"total_findings": len(findings),
|
||||
"hosts_scanned": hosts_count,
|
||||
"severity_counts": severity_counts,
|
||||
"category_counts": category_counts,
|
||||
"unique_hosts": len(host_counts),
|
||||
"top_ports": dict(sorted(port_counts.items(), key=lambda x: x[1], reverse=True)[:10]),
|
||||
"top_services": dict(sorted(service_counts.items(), key=lambda x: x[1], reverse=True)[:10]),
|
||||
"host_counts": dict(sorted(host_counts.items(), key=lambda x: x[1], reverse=True)[:5])
|
||||
}
|
||||
@@ -0,0 +1,501 @@
|
||||
"""
|
||||
Nuclei Penetration Testing Module
|
||||
|
||||
This module uses Nuclei to perform fast and customizable vulnerability scanning
|
||||
using community-powered templates.
|
||||
"""
|
||||
# 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 NucleiModule(BaseModule):
|
||||
"""Nuclei fast vulnerability scanner module"""
|
||||
|
||||
def get_metadata(self) -> ModuleMetadata:
|
||||
"""Get module metadata"""
|
||||
return ModuleMetadata(
|
||||
name="nuclei",
|
||||
version="3.1.0",
|
||||
description="Fast and customizable vulnerability scanner using community-powered templates",
|
||||
author="FuzzForge Team",
|
||||
category="penetration_testing",
|
||||
tags=["vulnerability", "scanner", "web", "network", "templates"],
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"targets": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "List of targets (URLs, domains, IP addresses)"
|
||||
},
|
||||
"target_file": {
|
||||
"type": "string",
|
||||
"description": "File containing targets to scan"
|
||||
},
|
||||
"templates": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Specific templates to use"
|
||||
},
|
||||
"template_directory": {
|
||||
"type": "string",
|
||||
"description": "Directory containing custom templates"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Template tags to include"
|
||||
},
|
||||
"exclude_tags": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Template tags to exclude"
|
||||
},
|
||||
"severity": {
|
||||
"type": "array",
|
||||
"items": {"type": "string", "enum": ["critical", "high", "medium", "low", "info"]},
|
||||
"default": ["critical", "high", "medium"],
|
||||
"description": "Severity levels to include"
|
||||
},
|
||||
"concurrency": {
|
||||
"type": "integer",
|
||||
"default": 25,
|
||||
"description": "Number of concurrent threads"
|
||||
},
|
||||
"rate_limit": {
|
||||
"type": "integer",
|
||||
"default": 150,
|
||||
"description": "Rate limit (requests per second)"
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"default": 10,
|
||||
"description": "Timeout for requests (seconds)"
|
||||
},
|
||||
"retries": {
|
||||
"type": "integer",
|
||||
"default": 1,
|
||||
"description": "Number of retries for failed requests"
|
||||
},
|
||||
"update_templates": {
|
||||
"type": "boolean",
|
||||
"default": False,
|
||||
"description": "Update templates before scanning"
|
||||
},
|
||||
"disable_clustering": {
|
||||
"type": "boolean",
|
||||
"default": False,
|
||||
"description": "Disable template clustering"
|
||||
},
|
||||
"no_interactsh": {
|
||||
"type": "boolean",
|
||||
"default": True,
|
||||
"description": "Disable interactsh server for OAST testing"
|
||||
}
|
||||
}
|
||||
},
|
||||
output_schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"findings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"template_id": {"type": "string"},
|
||||
"name": {"type": "string"},
|
||||
"severity": {"type": "string"},
|
||||
"host": {"type": "string"},
|
||||
"matched_at": {"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")
|
||||
|
||||
severity_levels = config.get("severity", [])
|
||||
valid_severities = ["critical", "high", "medium", "low", "info"]
|
||||
for severity in severity_levels:
|
||||
if severity not in valid_severities:
|
||||
raise ValueError(f"Invalid severity: {severity}. Valid: {valid_severities}")
|
||||
|
||||
return True
|
||||
|
||||
async def execute(self, config: Dict[str, Any], workspace: Path) -> ModuleResult:
|
||||
"""Execute Nuclei vulnerability scanning"""
|
||||
self.start_timer()
|
||||
|
||||
try:
|
||||
# Validate inputs
|
||||
self.validate_config(config)
|
||||
self.validate_workspace(workspace)
|
||||
|
||||
logger.info("Running Nuclei vulnerability scan")
|
||||
|
||||
# Update templates if requested
|
||||
if config.get("update_templates", False):
|
||||
await self._update_templates(workspace)
|
||||
|
||||
# Prepare target file
|
||||
target_file = await self._prepare_targets(config, workspace)
|
||||
if not target_file:
|
||||
logger.info("No targets specified for scanning")
|
||||
return self.create_result(
|
||||
findings=[],
|
||||
status="success",
|
||||
summary={"total_findings": 0, "targets_scanned": 0}
|
||||
)
|
||||
|
||||
# Run Nuclei scan
|
||||
findings = await self._run_nuclei_scan(target_file, config, workspace)
|
||||
|
||||
# Create summary
|
||||
summary = self._create_summary(findings, len(config.get("targets", [])))
|
||||
|
||||
logger.info(f"Nuclei found {len(findings)} vulnerabilities")
|
||||
|
||||
return self.create_result(
|
||||
findings=findings,
|
||||
status="success",
|
||||
summary=summary
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Nuclei module failed: {e}")
|
||||
return self.create_result(
|
||||
findings=[],
|
||||
status="failed",
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
async def _update_templates(self, workspace: Path):
|
||||
"""Update Nuclei templates"""
|
||||
try:
|
||||
logger.info("Updating Nuclei templates...")
|
||||
cmd = ["nuclei", "-update-templates"]
|
||||
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
*cmd,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
cwd=workspace
|
||||
)
|
||||
|
||||
stdout, stderr = await process.communicate()
|
||||
|
||||
if process.returncode == 0:
|
||||
logger.info("Templates updated successfully")
|
||||
else:
|
||||
logger.warning(f"Template update failed: {stderr.decode()}")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error updating templates: {e}")
|
||||
|
||||
async def _prepare_targets(self, config: Dict[str, Any], workspace: Path) -> Path:
|
||||
"""Prepare target file for scanning"""
|
||||
targets = config.get("targets", [])
|
||||
target_file = config.get("target_file")
|
||||
|
||||
if target_file:
|
||||
# Use existing target file
|
||||
target_path = workspace / target_file
|
||||
if target_path.exists():
|
||||
return target_path
|
||||
else:
|
||||
raise FileNotFoundError(f"Target file not found: {target_file}")
|
||||
|
||||
if targets:
|
||||
# Create temporary target file
|
||||
target_path = workspace / "nuclei_targets.txt"
|
||||
with open(target_path, 'w') as f:
|
||||
for target in targets:
|
||||
f.write(f"{target}\n")
|
||||
return target_path
|
||||
|
||||
return None
|
||||
|
||||
async def _run_nuclei_scan(self, target_file: Path, config: Dict[str, Any], workspace: Path) -> List[ModuleFinding]:
|
||||
"""Run Nuclei scan"""
|
||||
findings = []
|
||||
|
||||
try:
|
||||
# Build nuclei command
|
||||
cmd = ["nuclei", "-l", str(target_file)]
|
||||
|
||||
# Add output format
|
||||
cmd.extend(["-json"])
|
||||
|
||||
# Add templates
|
||||
templates = config.get("templates", [])
|
||||
if templates:
|
||||
cmd.extend(["-t", ",".join(templates)])
|
||||
|
||||
# Add template directory
|
||||
template_dir = config.get("template_directory")
|
||||
if template_dir:
|
||||
cmd.extend(["-t", template_dir])
|
||||
|
||||
# Add tags
|
||||
tags = config.get("tags", [])
|
||||
if tags:
|
||||
cmd.extend(["-tags", ",".join(tags)])
|
||||
|
||||
# Add exclude tags
|
||||
exclude_tags = config.get("exclude_tags", [])
|
||||
if exclude_tags:
|
||||
cmd.extend(["-exclude-tags", ",".join(exclude_tags)])
|
||||
|
||||
# Add severity
|
||||
severity_levels = config.get("severity", ["critical", "high", "medium"])
|
||||
cmd.extend(["-severity", ",".join(severity_levels)])
|
||||
|
||||
# Add concurrency
|
||||
concurrency = config.get("concurrency", 25)
|
||||
cmd.extend(["-c", str(concurrency)])
|
||||
|
||||
# Add rate limit
|
||||
rate_limit = config.get("rate_limit", 150)
|
||||
cmd.extend(["-rl", str(rate_limit)])
|
||||
|
||||
# Add timeout
|
||||
timeout = config.get("timeout", 10)
|
||||
cmd.extend(["-timeout", str(timeout)])
|
||||
|
||||
# Add retries
|
||||
retries = config.get("retries", 1)
|
||||
cmd.extend(["-retries", str(retries)])
|
||||
|
||||
# Add other flags
|
||||
if config.get("disable_clustering", False):
|
||||
cmd.append("-no-color")
|
||||
|
||||
if config.get("no_interactsh", True):
|
||||
cmd.append("-no-interactsh")
|
||||
|
||||
# Add silent flag for JSON output
|
||||
cmd.append("-silent")
|
||||
|
||||
logger.debug(f"Running command: {' '.join(cmd)}")
|
||||
|
||||
# Run nuclei
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
*cmd,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
cwd=workspace
|
||||
)
|
||||
|
||||
stdout, stderr = await process.communicate()
|
||||
|
||||
# Parse results
|
||||
if process.returncode == 0 or stdout:
|
||||
findings = self._parse_nuclei_output(stdout.decode(), workspace)
|
||||
else:
|
||||
error_msg = stderr.decode()
|
||||
logger.error(f"Nuclei scan failed: {error_msg}")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error running Nuclei scan: {e}")
|
||||
|
||||
return findings
|
||||
|
||||
def _parse_nuclei_output(self, output: str, workspace: Path) -> List[ModuleFinding]:
|
||||
"""Parse Nuclei JSON output into findings"""
|
||||
findings = []
|
||||
|
||||
if not output.strip():
|
||||
return findings
|
||||
|
||||
try:
|
||||
# Parse each line as JSON (JSONL format)
|
||||
for line in output.strip().split('\n'):
|
||||
if not line.strip():
|
||||
continue
|
||||
|
||||
result = json.loads(line)
|
||||
|
||||
# Extract information
|
||||
template_id = result.get("template-id", "")
|
||||
template_name = result.get("info", {}).get("name", "")
|
||||
severity = result.get("info", {}).get("severity", "medium")
|
||||
host = result.get("host", "")
|
||||
matched_at = result.get("matched-at", "")
|
||||
description = result.get("info", {}).get("description", "")
|
||||
reference = result.get("info", {}).get("reference", [])
|
||||
classification = result.get("info", {}).get("classification", {})
|
||||
extracted_results = result.get("extracted-results", [])
|
||||
|
||||
# Map severity to our standard levels
|
||||
finding_severity = self._map_severity(severity)
|
||||
|
||||
# Get category based on template
|
||||
category = self._get_category(template_id, template_name, classification)
|
||||
|
||||
# Create finding
|
||||
finding = self.create_finding(
|
||||
title=f"Nuclei Detection: {template_name}",
|
||||
description=description or f"Vulnerability detected using template {template_id}",
|
||||
severity=finding_severity,
|
||||
category=category,
|
||||
file_path=None, # Nuclei scans network targets
|
||||
recommendation=self._get_recommendation(template_id, template_name, reference),
|
||||
metadata={
|
||||
"template_id": template_id,
|
||||
"template_name": template_name,
|
||||
"nuclei_severity": severity,
|
||||
"host": host,
|
||||
"matched_at": matched_at,
|
||||
"classification": classification,
|
||||
"reference": reference,
|
||||
"extracted_results": extracted_results
|
||||
}
|
||||
)
|
||||
findings.append(finding)
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.warning(f"Failed to parse Nuclei output: {e}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Error processing Nuclei results: {e}")
|
||||
|
||||
return findings
|
||||
|
||||
def _map_severity(self, nuclei_severity: str) -> str:
|
||||
"""Map Nuclei severity to our standard severity levels"""
|
||||
severity_map = {
|
||||
"critical": "critical",
|
||||
"high": "high",
|
||||
"medium": "medium",
|
||||
"low": "low",
|
||||
"info": "info"
|
||||
}
|
||||
return severity_map.get(nuclei_severity.lower(), "medium")
|
||||
|
||||
def _get_category(self, template_id: str, template_name: str, classification: Dict) -> str:
|
||||
"""Determine finding category based on template and classification"""
|
||||
template_lower = f"{template_id} {template_name}".lower()
|
||||
|
||||
# Use classification if available
|
||||
cwe_id = classification.get("cwe-id")
|
||||
if cwe_id:
|
||||
# Map common CWE IDs to categories
|
||||
if cwe_id in ["CWE-79", "CWE-80"]:
|
||||
return "cross_site_scripting"
|
||||
elif cwe_id in ["CWE-89"]:
|
||||
return "sql_injection"
|
||||
elif cwe_id in ["CWE-22", "CWE-23"]:
|
||||
return "path_traversal"
|
||||
elif cwe_id in ["CWE-352"]:
|
||||
return "csrf"
|
||||
elif cwe_id in ["CWE-601"]:
|
||||
return "redirect"
|
||||
|
||||
# Analyze template content
|
||||
if any(term in template_lower for term in ["xss", "cross-site"]):
|
||||
return "cross_site_scripting"
|
||||
elif any(term in template_lower for term in ["sql", "injection"]):
|
||||
return "sql_injection"
|
||||
elif any(term in template_lower for term in ["lfi", "rfi", "file", "path", "traversal"]):
|
||||
return "file_inclusion"
|
||||
elif any(term in template_lower for term in ["rce", "command", "execution"]):
|
||||
return "remote_code_execution"
|
||||
elif any(term in template_lower for term in ["auth", "login", "bypass"]):
|
||||
return "authentication_bypass"
|
||||
elif any(term in template_lower for term in ["disclosure", "exposure", "leak"]):
|
||||
return "information_disclosure"
|
||||
elif any(term in template_lower for term in ["config", "misconfiguration"]):
|
||||
return "misconfiguration"
|
||||
elif any(term in template_lower for term in ["cve-"]):
|
||||
return "known_vulnerability"
|
||||
else:
|
||||
return "web_vulnerability"
|
||||
|
||||
def _get_recommendation(self, template_id: str, template_name: str, references: List) -> str:
|
||||
"""Generate recommendation based on template"""
|
||||
# Use references if available
|
||||
if references:
|
||||
ref_text = ", ".join(references[:3]) # Limit to first 3 references
|
||||
return f"Review the vulnerability and apply appropriate fixes. References: {ref_text}"
|
||||
|
||||
# Generate based on template type
|
||||
template_lower = f"{template_id} {template_name}".lower()
|
||||
|
||||
if "xss" in template_lower:
|
||||
return "Implement proper input validation and output encoding to prevent XSS attacks."
|
||||
elif "sql" in template_lower:
|
||||
return "Use parameterized queries and input validation to prevent SQL injection."
|
||||
elif "lfi" in template_lower or "rfi" in template_lower:
|
||||
return "Validate and sanitize file paths. Avoid dynamic file includes with user input."
|
||||
elif "rce" in template_lower:
|
||||
return "Sanitize user input and avoid executing system commands with user-controlled data."
|
||||
elif "auth" in template_lower:
|
||||
return "Review authentication mechanisms and implement proper access controls."
|
||||
elif "exposure" in template_lower or "disclosure" in template_lower:
|
||||
return "Restrict access to sensitive information and implement proper authorization."
|
||||
elif "cve-" in template_lower:
|
||||
return "Update the affected software to the latest version to patch known vulnerabilities."
|
||||
else:
|
||||
return f"Review and remediate the security issue identified by template {template_id}."
|
||||
|
||||
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 = {}
|
||||
template_counts = {}
|
||||
host_counts = {}
|
||||
|
||||
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 template
|
||||
template_id = finding.metadata.get("template_id", "unknown")
|
||||
template_counts[template_id] = template_counts.get(template_id, 0) + 1
|
||||
|
||||
# Count by host
|
||||
host = finding.metadata.get("host", "unknown")
|
||||
host_counts[host] = host_counts.get(host, 0) + 1
|
||||
|
||||
return {
|
||||
"total_findings": len(findings),
|
||||
"targets_scanned": targets_count,
|
||||
"severity_counts": severity_counts,
|
||||
"category_counts": category_counts,
|
||||
"top_templates": dict(sorted(template_counts.items(), key=lambda x: x[1], reverse=True)[:10]),
|
||||
"affected_hosts": len(host_counts),
|
||||
"host_counts": dict(sorted(host_counts.items(), key=lambda x: x[1], reverse=True)[:10])
|
||||
}
|
||||
@@ -0,0 +1,671 @@
|
||||
"""
|
||||
SQLMap Penetration Testing Module
|
||||
|
||||
This module uses SQLMap for automatic SQL injection detection and exploitation.
|
||||
"""
|
||||
# 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 SQLMapModule(BaseModule):
|
||||
"""SQLMap automatic SQL injection detection and exploitation module"""
|
||||
|
||||
def get_metadata(self) -> ModuleMetadata:
|
||||
"""Get module metadata"""
|
||||
return ModuleMetadata(
|
||||
name="sqlmap",
|
||||
version="1.7.11",
|
||||
description="Automatic SQL injection detection and exploitation tool",
|
||||
author="FuzzForge Team",
|
||||
category="penetration_testing",
|
||||
tags=["sql-injection", "web", "database", "vulnerability", "exploitation"],
|
||||
input_schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"target_url": {
|
||||
"type": "string",
|
||||
"description": "Target URL to test for SQL injection"
|
||||
},
|
||||
"target_file": {
|
||||
"type": "string",
|
||||
"description": "File containing URLs to test"
|
||||
},
|
||||
"request_file": {
|
||||
"type": "string",
|
||||
"description": "Load HTTP request from file (Burp log, etc.)"
|
||||
},
|
||||
"data": {
|
||||
"type": "string",
|
||||
"description": "Data string to be sent through POST"
|
||||
},
|
||||
"cookie": {
|
||||
"type": "string",
|
||||
"description": "HTTP Cookie header value"
|
||||
},
|
||||
"user_agent": {
|
||||
"type": "string",
|
||||
"description": "HTTP User-Agent header value"
|
||||
},
|
||||
"referer": {
|
||||
"type": "string",
|
||||
"description": "HTTP Referer header value"
|
||||
},
|
||||
"headers": {
|
||||
"type": "object",
|
||||
"description": "Additional HTTP headers"
|
||||
},
|
||||
"method": {
|
||||
"type": "string",
|
||||
"enum": ["GET", "POST", "PUT", "DELETE", "PATCH"],
|
||||
"default": "GET",
|
||||
"description": "HTTP method to use"
|
||||
},
|
||||
"testable_parameters": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Comma-separated list of testable parameter(s)"
|
||||
},
|
||||
"skip_parameters": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Parameters to skip during testing"
|
||||
},
|
||||
"dbms": {
|
||||
"type": "string",
|
||||
"enum": ["mysql", "postgresql", "oracle", "mssql", "sqlite", "access", "firebird", "sybase", "db2", "hsqldb", "h2"],
|
||||
"description": "Force back-end DBMS to provided value"
|
||||
},
|
||||
"level": {
|
||||
"type": "integer",
|
||||
"enum": [1, 2, 3, 4, 5],
|
||||
"default": 1,
|
||||
"description": "Level of tests to perform (1-5)"
|
||||
},
|
||||
"risk": {
|
||||
"type": "integer",
|
||||
"enum": [1, 2, 3],
|
||||
"default": 1,
|
||||
"description": "Risk of tests to perform (1-3)"
|
||||
},
|
||||
"technique": {
|
||||
"type": "array",
|
||||
"items": {"type": "string", "enum": ["B", "E", "U", "S", "T", "Q"]},
|
||||
"description": "SQL injection techniques to use (B=Boolean, E=Error, U=Union, S=Stacked, T=Time, Q=Inline)"
|
||||
},
|
||||
"time_sec": {
|
||||
"type": "integer",
|
||||
"default": 5,
|
||||
"description": "Seconds to delay DBMS response for time-based blind SQL injection"
|
||||
},
|
||||
"union_cols": {
|
||||
"type": "string",
|
||||
"description": "Range of columns to test for UNION query SQL injection"
|
||||
},
|
||||
"threads": {
|
||||
"type": "integer",
|
||||
"default": 1,
|
||||
"description": "Maximum number of concurrent HTTP requests"
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"default": 30,
|
||||
"description": "Seconds to wait before timeout connection"
|
||||
},
|
||||
"retries": {
|
||||
"type": "integer",
|
||||
"default": 3,
|
||||
"description": "Retries when connection timeouts"
|
||||
},
|
||||
"randomize": {
|
||||
"type": "boolean",
|
||||
"default": True,
|
||||
"description": "Randomly change value of given parameter(s)"
|
||||
},
|
||||
"safe_url": {
|
||||
"type": "string",
|
||||
"description": "URL to visit frequently during testing"
|
||||
},
|
||||
"safe_freq": {
|
||||
"type": "integer",
|
||||
"description": "Test requests between visits to safe URL"
|
||||
},
|
||||
"crawl": {
|
||||
"type": "integer",
|
||||
"description": "Crawl website starting from target URL (depth)"
|
||||
},
|
||||
"forms": {
|
||||
"type": "boolean",
|
||||
"default": False,
|
||||
"description": "Parse and test forms on target URL"
|
||||
},
|
||||
"batch": {
|
||||
"type": "boolean",
|
||||
"default": True,
|
||||
"description": "Never ask for user input, use default behavior"
|
||||
},
|
||||
"cleanup": {
|
||||
"type": "boolean",
|
||||
"default": True,
|
||||
"description": "Clean up files used by SQLMap"
|
||||
},
|
||||
"check_waf": {
|
||||
"type": "boolean",
|
||||
"default": False,
|
||||
"description": "Check for existence of WAF/IPS protection"
|
||||
},
|
||||
"tamper": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Use tamper scripts to modify requests"
|
||||
}
|
||||
}
|
||||
},
|
||||
output_schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"findings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {"type": "string"},
|
||||
"parameter": {"type": "string"},
|
||||
"technique": {"type": "string"},
|
||||
"dbms": {"type": "string"},
|
||||
"payload": {"type": "string"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
def validate_config(self, config: Dict[str, Any]) -> bool:
|
||||
"""Validate configuration"""
|
||||
target_url = config.get("target_url")
|
||||
target_file = config.get("target_file")
|
||||
request_file = config.get("request_file")
|
||||
|
||||
if not any([target_url, target_file, request_file]):
|
||||
raise ValueError("Either 'target_url', 'target_file', or 'request_file' must be specified")
|
||||
|
||||
level = config.get("level", 1)
|
||||
if level not in [1, 2, 3, 4, 5]:
|
||||
raise ValueError("Level must be between 1 and 5")
|
||||
|
||||
risk = config.get("risk", 1)
|
||||
if risk not in [1, 2, 3]:
|
||||
raise ValueError("Risk must be between 1 and 3")
|
||||
|
||||
return True
|
||||
|
||||
async def execute(self, config: Dict[str, Any], workspace: Path) -> ModuleResult:
|
||||
"""Execute SQLMap SQL injection testing"""
|
||||
self.start_timer()
|
||||
|
||||
try:
|
||||
# Validate inputs
|
||||
self.validate_config(config)
|
||||
self.validate_workspace(workspace)
|
||||
|
||||
logger.info("Running SQLMap SQL injection scan")
|
||||
|
||||
# Run SQLMap scan
|
||||
findings = await self._run_sqlmap_scan(config, workspace)
|
||||
|
||||
# Create summary
|
||||
summary = self._create_summary(findings)
|
||||
|
||||
logger.info(f"SQLMap found {len(findings)} SQL injection vulnerabilities")
|
||||
|
||||
return self.create_result(
|
||||
findings=findings,
|
||||
status="success",
|
||||
summary=summary
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"SQLMap module failed: {e}")
|
||||
return self.create_result(
|
||||
findings=[],
|
||||
status="failed",
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
async def _run_sqlmap_scan(self, config: Dict[str, Any], workspace: Path) -> List[ModuleFinding]:
|
||||
"""Run SQLMap scan"""
|
||||
findings = []
|
||||
|
||||
try:
|
||||
# Build sqlmap command
|
||||
cmd = ["sqlmap"]
|
||||
|
||||
# Add target specification
|
||||
target_url = config.get("target_url")
|
||||
if target_url:
|
||||
cmd.extend(["-u", target_url])
|
||||
|
||||
target_file = config.get("target_file")
|
||||
if target_file:
|
||||
target_path = workspace / target_file
|
||||
if target_path.exists():
|
||||
cmd.extend(["-m", str(target_path)])
|
||||
else:
|
||||
raise FileNotFoundError(f"Target file not found: {target_file}")
|
||||
|
||||
request_file = config.get("request_file")
|
||||
if request_file:
|
||||
request_path = workspace / request_file
|
||||
if request_path.exists():
|
||||
cmd.extend(["-r", str(request_path)])
|
||||
else:
|
||||
raise FileNotFoundError(f"Request file not found: {request_file}")
|
||||
|
||||
# Add HTTP options
|
||||
data = config.get("data")
|
||||
if data:
|
||||
cmd.extend(["--data", data])
|
||||
|
||||
cookie = config.get("cookie")
|
||||
if cookie:
|
||||
cmd.extend(["--cookie", cookie])
|
||||
|
||||
user_agent = config.get("user_agent")
|
||||
if user_agent:
|
||||
cmd.extend(["--user-agent", user_agent])
|
||||
|
||||
referer = config.get("referer")
|
||||
if referer:
|
||||
cmd.extend(["--referer", referer])
|
||||
|
||||
headers = config.get("headers", {})
|
||||
for key, value in headers.items():
|
||||
cmd.extend(["--header", f"{key}: {value}"])
|
||||
|
||||
method = config.get("method")
|
||||
if method and method != "GET":
|
||||
cmd.extend(["--method", method])
|
||||
|
||||
# Add parameter options
|
||||
testable_params = config.get("testable_parameters", [])
|
||||
if testable_params:
|
||||
cmd.extend(["-p", ",".join(testable_params)])
|
||||
|
||||
skip_params = config.get("skip_parameters", [])
|
||||
if skip_params:
|
||||
cmd.extend(["--skip", ",".join(skip_params)])
|
||||
|
||||
# Add injection options
|
||||
dbms = config.get("dbms")
|
||||
if dbms:
|
||||
cmd.extend(["--dbms", dbms])
|
||||
|
||||
level = config.get("level", 1)
|
||||
cmd.extend(["--level", str(level)])
|
||||
|
||||
risk = config.get("risk", 1)
|
||||
cmd.extend(["--risk", str(risk)])
|
||||
|
||||
techniques = config.get("technique", [])
|
||||
if techniques:
|
||||
cmd.extend(["--technique", "".join(techniques)])
|
||||
|
||||
time_sec = config.get("time_sec", 5)
|
||||
cmd.extend(["--time-sec", str(time_sec)])
|
||||
|
||||
union_cols = config.get("union_cols")
|
||||
if union_cols:
|
||||
cmd.extend(["--union-cols", union_cols])
|
||||
|
||||
# Add performance options
|
||||
threads = config.get("threads", 1)
|
||||
cmd.extend(["--threads", str(threads)])
|
||||
|
||||
timeout = config.get("timeout", 30)
|
||||
cmd.extend(["--timeout", str(timeout)])
|
||||
|
||||
retries = config.get("retries", 3)
|
||||
cmd.extend(["--retries", str(retries)])
|
||||
|
||||
# Add request options
|
||||
if config.get("randomize", True):
|
||||
cmd.append("--randomize")
|
||||
|
||||
safe_url = config.get("safe_url")
|
||||
if safe_url:
|
||||
cmd.extend(["--safe-url", safe_url])
|
||||
|
||||
safe_freq = config.get("safe_freq")
|
||||
if safe_freq:
|
||||
cmd.extend(["--safe-freq", str(safe_freq)])
|
||||
|
||||
# Add crawling options
|
||||
crawl_depth = config.get("crawl")
|
||||
if crawl_depth:
|
||||
cmd.extend(["--crawl", str(crawl_depth)])
|
||||
|
||||
if config.get("forms", False):
|
||||
cmd.append("--forms")
|
||||
|
||||
# Add behavioral options
|
||||
if config.get("batch", True):
|
||||
cmd.append("--batch")
|
||||
|
||||
if config.get("cleanup", True):
|
||||
cmd.append("--cleanup")
|
||||
|
||||
if config.get("check_waf", False):
|
||||
cmd.append("--check-waf")
|
||||
|
||||
# Add tamper scripts
|
||||
tamper_scripts = config.get("tamper", [])
|
||||
if tamper_scripts:
|
||||
cmd.extend(["--tamper", ",".join(tamper_scripts)])
|
||||
|
||||
# Set output directory
|
||||
output_dir = workspace / "sqlmap_output"
|
||||
output_dir.mkdir(exist_ok=True)
|
||||
cmd.extend(["--output-dir", str(output_dir)])
|
||||
|
||||
# Add format for easier parsing
|
||||
cmd.append("--flush-session") # Start fresh
|
||||
cmd.append("--fresh-queries") # Ignore previous results
|
||||
|
||||
logger.debug(f"Running command: {' '.join(cmd)}")
|
||||
|
||||
# Run sqlmap
|
||||
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 output directory
|
||||
findings = self._parse_sqlmap_output(output_dir, stdout.decode(), workspace)
|
||||
|
||||
# Log results
|
||||
if findings:
|
||||
logger.info(f"SQLMap detected {len(findings)} SQL injection vulnerabilities")
|
||||
else:
|
||||
logger.info("No SQL injection vulnerabilities found")
|
||||
# Check for errors
|
||||
stderr_text = stderr.decode()
|
||||
if stderr_text:
|
||||
logger.warning(f"SQLMap warnings/errors: {stderr_text}")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error running SQLMap scan: {e}")
|
||||
|
||||
return findings
|
||||
|
||||
def _parse_sqlmap_output(self, output_dir: Path, stdout: str, workspace: Path) -> List[ModuleFinding]:
|
||||
"""Parse SQLMap output into findings"""
|
||||
findings = []
|
||||
|
||||
try:
|
||||
# Look for session files in output directory
|
||||
session_files = list(output_dir.glob("**/*.sqlite"))
|
||||
log_files = list(output_dir.glob("**/*.log"))
|
||||
|
||||
# Parse stdout for injection information
|
||||
findings.extend(self._parse_stdout_output(stdout))
|
||||
|
||||
# Parse log files for additional details
|
||||
for log_file in log_files:
|
||||
findings.extend(self._parse_log_file(log_file))
|
||||
|
||||
# If we have session files, we can extract more detailed information
|
||||
# For now, we'll rely on stdout parsing
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error parsing SQLMap output: {e}")
|
||||
|
||||
return findings
|
||||
|
||||
def _parse_stdout_output(self, stdout: str) -> List[ModuleFinding]:
|
||||
"""Parse SQLMap stdout for SQL injection findings"""
|
||||
findings = []
|
||||
|
||||
try:
|
||||
lines = stdout.split('\n')
|
||||
current_url = None
|
||||
current_parameter = None
|
||||
current_technique = None
|
||||
current_dbms = None
|
||||
injection_found = False
|
||||
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
|
||||
# Extract URL being tested
|
||||
if "testing URL" in line or "testing connection to the target URL" in line:
|
||||
# Extract URL from line
|
||||
if "'" in line:
|
||||
url_start = line.find("'") + 1
|
||||
url_end = line.find("'", url_start)
|
||||
if url_end > url_start:
|
||||
current_url = line[url_start:url_end]
|
||||
|
||||
# Extract parameter being tested
|
||||
elif "testing parameter" in line or "testing" in line and "parameter" in line:
|
||||
if "'" in line:
|
||||
param_parts = line.split("'")
|
||||
if len(param_parts) >= 2:
|
||||
current_parameter = param_parts[1]
|
||||
|
||||
# Detect SQL injection found
|
||||
elif any(indicator in line.lower() for indicator in [
|
||||
"parameter appears to be vulnerable",
|
||||
"injectable",
|
||||
"parameter is vulnerable"
|
||||
]):
|
||||
injection_found = True
|
||||
|
||||
# Extract technique information
|
||||
elif "Type:" in line:
|
||||
current_technique = line.replace("Type:", "").strip()
|
||||
|
||||
# Extract database information
|
||||
elif "back-end DBMS:" in line.lower():
|
||||
current_dbms = line.split(":")[-1].strip()
|
||||
|
||||
# Extract payload information
|
||||
elif "Payload:" in line:
|
||||
payload = line.replace("Payload:", "").strip()
|
||||
|
||||
# Create finding if we have injection
|
||||
if injection_found and current_url and current_parameter:
|
||||
finding = self._create_sqlmap_finding(
|
||||
current_url, current_parameter, current_technique,
|
||||
current_dbms, payload
|
||||
)
|
||||
if finding:
|
||||
findings.append(finding)
|
||||
|
||||
# Reset state
|
||||
injection_found = False
|
||||
current_technique = None
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error parsing SQLMap stdout: {e}")
|
||||
|
||||
return findings
|
||||
|
||||
def _parse_log_file(self, log_file: Path) -> List[ModuleFinding]:
|
||||
"""Parse SQLMap log file for additional findings"""
|
||||
findings = []
|
||||
|
||||
try:
|
||||
with open(log_file, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Look for injection indicators in log
|
||||
if "injectable" in content.lower() or "vulnerable" in content.lower():
|
||||
# Could parse more detailed information from log
|
||||
# For now, we'll rely on stdout parsing
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error parsing log file {log_file}: {e}")
|
||||
|
||||
return findings
|
||||
|
||||
def _create_sqlmap_finding(self, url: str, parameter: str, technique: str, dbms: str, payload: str) -> ModuleFinding:
|
||||
"""Create a ModuleFinding for SQL injection"""
|
||||
try:
|
||||
# Map technique to readable description
|
||||
technique_map = {
|
||||
"boolean-based blind": "Boolean-based blind SQL injection",
|
||||
"time-based blind": "Time-based blind SQL injection",
|
||||
"error-based": "Error-based SQL injection",
|
||||
"UNION query": "UNION-based SQL injection",
|
||||
"stacked queries": "Stacked queries SQL injection",
|
||||
"inline query": "Inline query SQL injection"
|
||||
}
|
||||
|
||||
technique_desc = technique_map.get(technique, technique or "SQL injection")
|
||||
|
||||
# Create description
|
||||
description = f"SQL injection vulnerability detected in parameter '{parameter}' using {technique_desc}"
|
||||
if dbms:
|
||||
description += f" against {dbms} database"
|
||||
|
||||
# Determine severity based on technique
|
||||
severity = self._get_injection_severity(technique, dbms)
|
||||
|
||||
# Create finding
|
||||
finding = self.create_finding(
|
||||
title=f"SQL Injection: {parameter}",
|
||||
description=description,
|
||||
severity=severity,
|
||||
category="sql_injection",
|
||||
file_path=None, # Web application testing
|
||||
recommendation=self._get_sqlinjection_recommendation(technique, dbms),
|
||||
metadata={
|
||||
"url": url,
|
||||
"parameter": parameter,
|
||||
"technique": technique,
|
||||
"dbms": dbms,
|
||||
"payload": payload[:500] if payload else "", # Limit payload length
|
||||
"injection_type": technique_desc
|
||||
}
|
||||
)
|
||||
|
||||
return finding
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error creating SQLMap finding: {e}")
|
||||
return None
|
||||
|
||||
def _get_injection_severity(self, technique: str, dbms: str) -> str:
|
||||
"""Determine severity based on injection technique and database"""
|
||||
if not technique:
|
||||
return "high" # Any SQL injection is serious
|
||||
|
||||
technique_lower = technique.lower()
|
||||
|
||||
# Critical severity for techniques that allow easy data extraction
|
||||
if any(term in technique_lower for term in ["union", "error-based"]):
|
||||
return "critical"
|
||||
|
||||
# High severity for techniques that allow some data extraction
|
||||
elif any(term in technique_lower for term in ["boolean-based", "time-based"]):
|
||||
return "high"
|
||||
|
||||
# Stacked queries are very dangerous as they allow multiple statements
|
||||
elif "stacked" in technique_lower:
|
||||
return "critical"
|
||||
|
||||
else:
|
||||
return "high"
|
||||
|
||||
def _get_sqlinjection_recommendation(self, technique: str, dbms: str) -> str:
|
||||
"""Generate recommendation for SQL injection"""
|
||||
base_recommendation = "Implement parameterized queries/prepared statements and input validation to prevent SQL injection attacks."
|
||||
|
||||
if technique:
|
||||
technique_lower = technique.lower()
|
||||
if "union" in technique_lower:
|
||||
base_recommendation += " The UNION-based injection allows direct data extraction - immediate remediation required."
|
||||
elif "error-based" in technique_lower:
|
||||
base_recommendation += " Error-based injection reveals database structure - disable error messages in production."
|
||||
elif "time-based" in technique_lower:
|
||||
base_recommendation += " Time-based injection allows blind data extraction - implement query timeout limits."
|
||||
elif "stacked" in technique_lower:
|
||||
base_recommendation += " Stacked queries injection allows multiple SQL statements - extremely dangerous, fix immediately."
|
||||
|
||||
if dbms:
|
||||
dbms_lower = dbms.lower()
|
||||
if "mysql" in dbms_lower:
|
||||
base_recommendation += " For MySQL: disable LOAD_FILE and INTO OUTFILE if not needed."
|
||||
elif "postgresql" in dbms_lower:
|
||||
base_recommendation += " For PostgreSQL: review user privileges and disable unnecessary functions."
|
||||
elif "mssql" in dbms_lower:
|
||||
base_recommendation += " For SQL Server: disable xp_cmdshell and review extended stored procedures."
|
||||
|
||||
return base_recommendation
|
||||
|
||||
def _create_summary(self, findings: List[ModuleFinding]) -> Dict[str, Any]:
|
||||
"""Create analysis summary"""
|
||||
severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0}
|
||||
technique_counts = {}
|
||||
dbms_counts = {}
|
||||
parameter_counts = {}
|
||||
url_counts = {}
|
||||
|
||||
for finding in findings:
|
||||
# Count by severity
|
||||
severity_counts[finding.severity] += 1
|
||||
|
||||
# Count by technique
|
||||
technique = finding.metadata.get("technique", "unknown")
|
||||
technique_counts[technique] = technique_counts.get(technique, 0) + 1
|
||||
|
||||
# Count by DBMS
|
||||
dbms = finding.metadata.get("dbms", "unknown")
|
||||
if dbms != "unknown":
|
||||
dbms_counts[dbms] = dbms_counts.get(dbms, 0) + 1
|
||||
|
||||
# Count by parameter
|
||||
parameter = finding.metadata.get("parameter", "unknown")
|
||||
parameter_counts[parameter] = parameter_counts.get(parameter, 0) + 1
|
||||
|
||||
# Count by URL
|
||||
url = finding.metadata.get("url", "unknown")
|
||||
url_counts[url] = url_counts.get(url, 0) + 1
|
||||
|
||||
return {
|
||||
"total_findings": len(findings),
|
||||
"severity_counts": severity_counts,
|
||||
"technique_counts": technique_counts,
|
||||
"dbms_counts": dbms_counts,
|
||||
"vulnerable_parameters": list(parameter_counts.keys()),
|
||||
"vulnerable_urls": len(url_counts),
|
||||
"most_common_techniques": dict(sorted(technique_counts.items(), key=lambda x: x[1], reverse=True)[:5]),
|
||||
"affected_databases": list(dbms_counts.keys())
|
||||
}
|
||||
Reference in New Issue
Block a user