Initial commit

This commit is contained in:
Tanguy Duhamel
2025-09-29 21:26:41 +02:00
parent ecf8d49dde
commit 0547b78429
208 changed files with 72069 additions and 53 deletions
@@ -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())
}