diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,50 @@
+# Python
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# Virtual environments
+venv/
+ENV/
+env/
+.venv/
+
+# IDE
+.idea/
+.vscode/
+*.swp
+*.swo
+*~
+
+# Testing
+.tox/
+.coverage
+.coverage.*
+htmlcov/
+.pytest_cache/
+.mypy_cache/
+
+# Build
+*.manifest
+*.spec
+
+# OS
+.DS_Store
+Thumbs.db
diff --git a/LICENSE b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 LACRYMARIA Team
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.txt b/README.txt
@@ -0,0 +1,193 @@
+USFX - Ultimate Subdomain Finder X
+===================================
+
+A standalone Python CLI tool for discovering subdomains on internal
+networks using custom DNS servers. Designed for air-gapped environments
+without internet connectivity.
+
+
+FEATURES
+--------
+
+ - Custom DNS Server Support: Specify internal DNS servers
+ - Offline-Only Modules: All techniques work without internet
+ - 10 Enumeration Modules:
+ * DNS Brute Force
+ * Zone Transfer (AXFR)
+ * DNSSEC Walking (NSEC/NSEC3)
+ * DNS Record Mining (MX, NS, TXT, SRV, SOA, CAA)
+ * Reverse DNS Sweep
+ * CNAME Chain Analysis
+ * Subdomain Permutation
+ * Recursive Sub-subdomain Enumeration
+ * Virtual Host Discovery
+ * TLS Certificate SAN Extraction
+ - Multiple Output Formats: JSON, CSV, TXT
+ - Bundled Wordlists: Small (~500), Medium (~3500), Large (~10000)
+ - Progress Tracking: Real-time progress with Rich terminal UI
+
+
+INSTALLATION
+------------
+
+From Source (Development):
+
+ cd /home/devastator/nextvt/usfx
+ python3 -m venv venv
+ source venv/bin/activate
+ pip install -e .
+ usfx --version
+
+From Built Package:
+
+ python -m build
+ pip install dist/usfx-1.0.0-py3-none-any.whl
+
+
+QUICK START
+-----------
+
+Basic Usage:
+
+ usfx corp.local # System DNS
+ usfx corp.local -d 192.168.1.1 # Internal DNS
+ usfx corp.local -d 192.168.1.1 -d 10.0.0.1 # Multiple DNS
+
+Wordlist Options:
+
+ usfx corp.local -d 10.0.0.1 -s small # ~500 words
+ usfx corp.local -d 10.0.0.1 -s medium # ~3500 words (default)
+ usfx corp.local -d 10.0.0.1 -s large # ~10000 words
+ usfx corp.local -d 10.0.0.1 -w /path/to/custom.txt
+
+Output Options:
+
+ usfx corp.local -d 10.0.0.1 -o results.json
+ usfx corp.local -d 10.0.0.1 -o results.csv -f csv
+ usfx corp.local -d 10.0.0.1 -o results.txt -f txt
+
+Module Selection:
+
+ usfx corp.local -d 10.0.0.1 -m bruteforce,zone,records
+
+ Available modules:
+ bruteforce - DNS brute force with wordlist
+ zone - Zone transfer (AXFR)
+ dnssec - DNSSEC zone walking
+ records - DNS record mining
+ reverse - Reverse DNS sweep
+ cname - CNAME chain analysis
+ permutation - Subdomain permutation
+ recursive - Recursive sub-subdomain enumeration
+ vhost - Virtual host discovery
+ tls - TLS certificate analysis
+
+Advanced Options:
+
+ usfx corp.local -d 10.0.0.1 -t 50 --timeout 5.0
+ usfx corp.local -d 10.0.0.1 --reverse-range 192.168.0.0/24
+ usfx corp.local -d 10.0.0.1 --vhost-ip 192.168.1.100
+ usfx corp.local -d 10.0.0.1 -v # Verbose
+ usfx corp.local -d 10.0.0.1 -q # Quiet
+
+
+CLI REFERENCE
+-------------
+
+Usage: usfx [OPTIONS] DOMAIN
+
+Options:
+ -d, --dns-server TEXT DNS server IP (can be repeated)
+ -w, --wordlist PATH Custom wordlist file
+ -s, --wordlist-size TEXT Wordlist size: small|medium|large
+ -o, --output PATH Output file path
+ -f, --format TEXT Output format: json|csv|txt
+ -t, --threads INTEGER Parallel threads (default: 30, max: 100)
+ --timeout FLOAT DNS timeout in seconds (default: 3.0)
+ -m, --modules TEXT Comma-separated module list
+ --reverse-range TEXT CIDR range for reverse DNS
+ --vhost-ip TEXT IP for vhost scanning
+ -v, --verbose Verbose output
+ -q, --quiet Suppress non-essential output
+ --no-color Disable colored output
+ --version Show version
+ --help Show help message
+
+
+PYTHON API
+----------
+
+ from usfx import ScanConfig, SubdomainEngine
+ from usfx.config import WordlistSize
+
+ config = ScanConfig(
+ domain='corp.internal',
+ dns_servers=['10.0.0.1', '10.0.0.2'],
+ wordlist_size=WordlistSize.MEDIUM,
+ threads=50,
+ timeout=3.0
+ )
+
+ engine = SubdomainEngine()
+ result = engine.scan(config)
+
+ print(f"Found {result.total_found} subdomains")
+ for sub in result.subdomains:
+ print(f" {sub.subdomain} -> {sub.ip}")
+
+
+MODULE DESCRIPTIONS
+-------------------
+
+ dns_bruteforce Wordlist-based DNS queries Medium
+ zone_transfer AXFR zone transfer attempts Fast
+ dnssec_walker NSEC/NSEC3 zone walking Fast
+ dns_records MX/NS/TXT/SRV/SOA/CAA mining Fast
+ reverse_dns PTR lookups on IP ranges Slow
+ cname_chaser CNAME chain tracking Fast
+ permutation Subdomain variation generation Medium
+ recursive_enum Sub-subdomain discovery Medium
+ vhost_scanner Host header brute force Slow
+ tls_analyzer TLS certificate SAN extraction Medium
+
+
+REQUIREMENTS
+------------
+
+ - Python 3.10+
+ - dnspython >= 2.4.0
+ - click >= 8.1.0
+ - requests >= 2.28.0
+ - cryptography >= 41.0.0
+ - rich >= 13.0.0
+
+
+USE CASES
+---------
+
+Internal Network Penetration Testing:
+ usfx corp.internal -d 10.0.0.53 -s large -o findings.json
+
+Active Directory Reconnaissance:
+ usfx ad.corp.local -d 192.168.1.1 -m records,reverse,zone
+
+IT Asset Discovery:
+ usfx internal.company -d 172.16.0.1 --reverse-range 172.16.0.0/16
+
+
+COMPARISON WITH WEB USF
+-----------------------
+
+ USFX Web USF
+ Internet Required No Yes
+ Custom DNS Servers Yes No
+ External APIs No CT Logs, Wayback, etc.
+ Database Storage No MySQL
+ Web Interface No Yes
+ Real-time Progress CLI WebSocket
+
+
+LICENSE
+-------
+
+MIT License - See LICENSE file for details.
diff --git a/pyproject.toml b/pyproject.toml
@@ -0,0 +1,71 @@
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "usfx"
+version = "1.0.0"
+description = "USFX - Ultimate Subdomain Finder X - Discover subdomains via custom DNS servers"
+readme = "README.txt"
+license = "MIT"
+requires-python = ">=3.10"
+authors = [
+ { name = "LACRYMARIA Team" }
+]
+keywords = ["subdomain", "dns", "security", "pentest", "enumeration", "reconnaissance"]
+classifiers = [
+ "Development Status :: 4 - Beta",
+ "Environment :: Console",
+ "Intended Audience :: Information Technology",
+ "Intended Audience :: System Administrators",
+ "License :: OSI Approved :: MIT License",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Topic :: Security",
+ "Topic :: System :: Networking",
+]
+dependencies = [
+ "dnspython>=2.4.0",
+ "click>=8.1.0",
+ "requests>=2.28.0",
+ "cryptography>=41.0.0",
+ "rich>=13.0.0",
+]
+
+[project.optional-dependencies]
+dev = [
+ "pytest>=7.0.0",
+ "pytest-cov>=4.0.0",
+ "black>=23.0.0",
+ "ruff>=0.1.0",
+]
+
+[project.scripts]
+usfx = "usfx.cli:main"
+
+[project.urls]
+Homepage = "https://github.com/nextvt/usfx"
+Repository = "https://github.com/nextvt/usfx"
+
+[tool.hatch.build.targets.wheel]
+packages = ["src/usfx"]
+
+[tool.hatch.build.targets.sdist]
+include = [
+ "/src",
+ "/README.txt",
+ "/LICENSE",
+]
+
+[tool.black]
+line-length = 100
+target-version = ["py310", "py311", "py312"]
+
+[tool.ruff]
+line-length = 100
+target-version = "py310"
+select = ["E", "F", "W", "I", "N", "UP", "B"]
+ignore = ["E501"]
diff --git a/src/usfx/__init__.py b/src/usfx/__init__.py
@@ -0,0 +1,21 @@
+"""
+USFX - Ultimate Subdomain Finder for Internal Networks
+
+A standalone CLI tool for discovering subdomains on internal networks
+using custom DNS servers. Designed for air-gapped environments without
+internet connectivity.
+"""
+
+__version__ = "1.0.0"
+__author__ = "NEXTVT Team"
+
+from .config import ScanConfig
+from .engine import SubdomainEngine
+from .orchestrator import SubdomainOrchestrator
+
+__all__ = [
+ "__version__",
+ "ScanConfig",
+ "SubdomainEngine",
+ "SubdomainOrchestrator",
+]
diff --git a/src/usfx/__main__.py b/src/usfx/__main__.py
@@ -0,0 +1,8 @@
+"""
+Entry point for running as a module: python -m usfx
+"""
+
+from .cli import main
+
+if __name__ == "__main__":
+ main()
diff --git a/src/usfx/cli.py b/src/usfx/cli.py
@@ -0,0 +1,310 @@
+"""
+USFX CLI
+
+Command-line interface for the Ultimate Subdomain Finder.
+Supports custom DNS servers for internal network enumeration.
+"""
+
+import logging
+import sys
+from pathlib import Path
+from typing import List, Optional, Tuple
+
+import click
+
+from . import __version__
+from .config import OutputFormat, ScanConfig, WordlistSize
+from .engine import SubdomainEngine
+from .utils.output import ConsoleOutput, OutputFormatter
+
+
+def setup_logging(verbose: bool, quiet: bool) -> None:
+ """Configure logging based on verbosity settings"""
+ if quiet:
+ level = logging.WARNING
+ elif verbose:
+ level = logging.DEBUG
+ else:
+ level = logging.INFO
+
+ logging.basicConfig(
+ level=level,
+ format='%(levelname)s: %(message)s' if not verbose else '%(asctime)s %(levelname)s [%(name)s] %(message)s',
+ handlers=[logging.StreamHandler(sys.stderr)]
+ )
+
+ # Suppress noisy third-party loggers
+ logging.getLogger('urllib3').setLevel(logging.WARNING)
+ logging.getLogger('requests').setLevel(logging.WARNING)
+
+
+def parse_modules(modules_str: Optional[str]) -> Optional[set]:
+ """Parse comma-separated module list"""
+ if not modules_str:
+ return None
+
+ valid_modules = {
+ 'bruteforce', 'dns_bruteforce',
+ 'zone', 'zone_transfer',
+ 'dnssec', 'dnssec_walker',
+ 'records', 'dns_records',
+ 'reverse', 'reverse_dns',
+ 'cname', 'cname_chaser',
+ 'permutation',
+ 'recursive', 'recursive_enum',
+ 'vhost', 'vhost_scanner',
+ 'tls', 'tls_analyzer'
+ }
+
+ # Normalize module names
+ module_map = {
+ 'bruteforce': 'dns_bruteforce',
+ 'zone': 'zone_transfer',
+ 'dnssec': 'dnssec_walker',
+ 'records': 'dns_records',
+ 'reverse': 'reverse_dns',
+ 'cname': 'cname_chaser',
+ 'recursive': 'recursive_enum',
+ 'vhost': 'vhost_scanner',
+ 'tls': 'tls_analyzer',
+ }
+
+ modules = set()
+ for m in modules_str.split(','):
+ m = m.strip().lower()
+ if m in module_map:
+ modules.add(module_map[m])
+ elif m in valid_modules:
+ modules.add(m)
+ else:
+ raise click.BadParameter(f"Unknown module: {m}")
+
+ return modules
+
+
+@click.command()
+@click.argument('domain')
+@click.option(
+ '-d', '--dns-server',
+ 'dns_servers',
+ multiple=True,
+ help='DNS server IP address (can be specified multiple times for multiple servers)'
+)
+@click.option(
+ '-w', '--wordlist',
+ 'wordlist_path',
+ type=click.Path(exists=True),
+ help='Path to custom wordlist file'
+)
+@click.option(
+ '-s', '--wordlist-size',
+ 'wordlist_size',
+ type=click.Choice(['small', 'medium', 'large']),
+ default='medium',
+ help='Size of bundled wordlist (default: medium)'
+)
+@click.option(
+ '-o', '--output',
+ 'output_path',
+ type=click.Path(),
+ help='Output file path'
+)
+@click.option(
+ '-f', '--format',
+ 'output_format',
+ type=click.Choice(['json', 'csv', 'txt']),
+ default='json',
+ help='Output format (default: json)'
+)
+@click.option(
+ '-t', '--threads',
+ default=30,
+ type=click.IntRange(1, 100),
+ help='Number of parallel threads (default: 30)'
+)
+@click.option(
+ '--timeout',
+ default=3.0,
+ type=click.FloatRange(0.5, 30.0),
+ help='DNS query timeout in seconds (default: 3.0)'
+)
+@click.option(
+ '-m', '--modules',
+ 'modules_str',
+ help='Comma-separated list of modules to run (e.g., bruteforce,zone,dnssec)'
+)
+@click.option(
+ '--reverse-range',
+ 'reverse_ranges',
+ multiple=True,
+ help='CIDR range for reverse DNS scanning (e.g., 192.168.0.0/24)'
+)
+@click.option(
+ '--vhost-ip',
+ 'vhost_ips',
+ multiple=True,
+ help='IP address for virtual host scanning'
+)
+@click.option(
+ '-v', '--verbose',
+ is_flag=True,
+ help='Enable verbose output'
+)
+@click.option(
+ '-q', '--quiet',
+ is_flag=True,
+ help='Suppress non-essential output'
+)
+@click.option(
+ '--no-color',
+ is_flag=True,
+ help='Disable colored output'
+)
+@click.version_option(version=__version__, prog_name='USFX')
+def main(
+ domain: str,
+ dns_servers: Tuple[str, ...],
+ wordlist_path: Optional[str],
+ wordlist_size: str,
+ output_path: Optional[str],
+ output_format: str,
+ threads: int,
+ timeout: float,
+ modules_str: Optional[str],
+ reverse_ranges: Tuple[str, ...],
+ vhost_ips: Tuple[str, ...],
+ verbose: bool,
+ quiet: bool,
+ no_color: bool
+) -> None:
+ """
+ USFX - Ultimate Subdomain Finder X for Internal Networks
+
+ Discover subdomains on internal networks using custom DNS servers.
+
+ Examples:
+
+ # Basic scan using system DNS
+ usfx corp.local
+
+ # Scan with internal DNS server
+ usfx corp.local -d 192.168.1.1
+
+ # Scan with multiple DNS servers
+ usfx corp.local -d 192.168.1.1 -d 192.168.1.2
+
+ # Quick scan with small wordlist
+ usfx corp.local -d 10.0.0.1 -s small
+
+ # Full scan with large wordlist
+ usfx corp.local -d 10.0.0.1 -s large -t 50
+
+ # Save results to JSON file
+ usfx corp.local -d 10.0.0.1 -o results.json
+
+ # Save as CSV
+ usfx corp.local -d 10.0.0.1 -o results.csv -f csv
+
+ # Only run specific modules
+ usfx corp.local -d 10.0.0.1 -m bruteforce,zone,records
+
+ # Scan with reverse DNS range
+ usfx corp.local -d 10.0.0.1 --reverse-range 192.168.0.0/16
+
+ # Scan with vhost discovery on specific IP
+ usfx corp.local -d 10.0.0.1 --vhost-ip 192.168.1.100
+ """
+ # Setup logging
+ setup_logging(verbose, quiet)
+
+ # Create console output helper
+ console = ConsoleOutput(use_rich=not no_color, quiet=quiet)
+
+ # Print banner
+ console.print_banner(__version__)
+
+ # Parse modules
+ try:
+ modules = parse_modules(modules_str)
+ except click.BadParameter as e:
+ console.print_error(str(e))
+ sys.exit(1)
+
+ # Create scan configuration
+ try:
+ config = ScanConfig(
+ domain=domain,
+ dns_servers=list(dns_servers),
+ wordlist_size=WordlistSize(wordlist_size),
+ custom_wordlist=Path(wordlist_path) if wordlist_path else None,
+ threads=threads,
+ timeout=timeout,
+ modules=modules,
+ reverse_dns_ranges=list(reverse_ranges),
+ vhost_ips=list(vhost_ips),
+ output_file=Path(output_path) if output_path else None,
+ output_format=OutputFormat(output_format),
+ verbose=verbose,
+ quiet=quiet
+ )
+ except Exception as e:
+ console.print_error(f"Invalid configuration: {e}")
+ sys.exit(1)
+
+ # Print configuration
+ enabled_modules = sorted(config.get_enabled_modules())
+ console.print_config(
+ domain=config.domain,
+ dns_servers=config.dns_servers,
+ modules=enabled_modules
+ )
+
+ # Create engine and run scan
+ engine = SubdomainEngine()
+
+ # Progress tracking
+ last_phase = None
+
+ def progress_callback(state):
+ nonlocal last_phase
+ if state.current_phase and state.current_phase != last_phase:
+ console.print_phase(state.current_phase.value)
+ last_phase = state.current_phase
+
+ try:
+ result = engine.scan(config, progress_callback=progress_callback if not quiet else None)
+ except KeyboardInterrupt:
+ console.print_error("Scan cancelled by user")
+ engine.cancel_scan()
+ sys.exit(130)
+ except Exception as e:
+ console.print_error(f"Scan failed: {e}")
+ if verbose:
+ import traceback
+ traceback.print_exc()
+ sys.exit(1)
+
+ # Print results
+ console.print_result_table(result)
+ console.print_summary(result)
+
+ # Write output file if requested
+ if config.output_file:
+ try:
+ OutputFormatter.write_file(
+ result,
+ config.output_file,
+ config.output_format.value
+ )
+ if not quiet:
+ click.echo(f"\nResults saved to: {config.output_file}")
+ except Exception as e:
+ console.print_error(f"Failed to write output file: {e}")
+ sys.exit(1)
+
+ # Exit with appropriate code
+ sys.exit(0 if result.total_found > 0 else 1)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/src/usfx/config.py b/src/usfx/config.py
@@ -0,0 +1,132 @@
+"""
+Scan Configuration
+
+Dataclasses for configuring subdomain enumeration scans.
+"""
+
+from dataclasses import dataclass, field
+from enum import Enum
+from pathlib import Path
+from typing import List, Optional, Set
+
+
+class WordlistSize(Enum):
+ """Predefined wordlist sizes"""
+ SMALL = "small" # ~500 words - quick scan
+ MEDIUM = "medium" # ~3500 words - default
+ LARGE = "large" # ~10000 words - thorough
+
+
+class OutputFormat(Enum):
+ """Output format types"""
+ JSON = "json"
+ CSV = "csv"
+ TXT = "txt"
+
+
+@dataclass
+class ScanConfig:
+ """Configuration for a subdomain scan"""
+
+ # Target domain (required)
+ domain: str
+
+ # DNS server configuration (core feature for internal networks)
+ dns_servers: List[str] = field(default_factory=list)
+
+ # Wordlist configuration
+ wordlist_size: WordlistSize = WordlistSize.MEDIUM
+ custom_wordlist: Optional[Path] = None
+
+ # Performance settings
+ threads: int = 30
+ timeout: float = 3.0
+
+ # Module selection (None = all modules)
+ modules: Optional[Set[str]] = None
+
+ # Additional options
+ reverse_dns_ranges: List[str] = field(default_factory=list) # CIDR ranges
+ vhost_ips: List[str] = field(default_factory=list) # IPs for vhost scanning
+
+ # Output configuration
+ output_file: Optional[Path] = None
+ output_format: OutputFormat = OutputFormat.JSON
+
+ # Verbosity
+ verbose: bool = False
+ quiet: bool = False
+
+ def __post_init__(self):
+ """Validate and normalize configuration"""
+ # Normalize domain
+ self.domain = self.domain.lower().strip('.')
+
+ # Validate threads
+ if self.threads < 1:
+ self.threads = 1
+ elif self.threads > 100:
+ self.threads = 100
+
+ # Validate timeout
+ if self.timeout < 0.5:
+ self.timeout = 0.5
+ elif self.timeout > 30.0:
+ self.timeout = 30.0
+
+ @property
+ def uses_custom_dns(self) -> bool:
+ """Check if custom DNS servers are configured"""
+ return len(self.dns_servers) > 0
+
+ def get_enabled_modules(self) -> Set[str]:
+ """Get the set of enabled module names"""
+ all_modules = {
+ "dns_bruteforce",
+ "zone_transfer",
+ "dnssec_walker",
+ "dns_records",
+ "reverse_dns",
+ "cname_chaser",
+ "permutation",
+ "recursive_enum",
+ "vhost_scanner",
+ "tls_analyzer",
+ }
+
+ if self.modules is None:
+ return all_modules
+
+ return self.modules & all_modules
+
+
+@dataclass
+class SubdomainResult:
+ """Result for a single discovered subdomain"""
+ subdomain: str
+ ip: Optional[str] = None
+ discovered_by: str = "unknown"
+ is_active: bool = False
+ cname_chain: List[str] = field(default_factory=list)
+
+
+@dataclass
+class ModuleResult:
+ """Result from a single module execution"""
+ module_name: str
+ found_count: int
+ subdomains: List[SubdomainResult]
+ duration_seconds: float
+ error: Optional[str] = None
+
+
+@dataclass
+class ScanResult:
+ """Complete scan result"""
+ domain: str
+ total_found: int
+ subdomains: List[SubdomainResult]
+ module_results: List[ModuleResult]
+ duration_seconds: float
+ dns_servers_used: List[str]
+ config: ScanConfig
diff --git a/src/usfx/data/__init__.py b/src/usfx/data/__init__.py
@@ -0,0 +1 @@
+# Data package marker for importlib.resources
diff --git a/src/usfx/data/wordlist.txt b/src/usfx/data/wordlist.txt
@@ -0,0 +1,3557 @@
+# Ultimate Subdomain Finder Wordlist
+# 10,000+ common subdomain names for brute force enumeration
+
+# === Common ===
+www
+mail
+ftp
+localhost
+webmail
+smtp
+pop
+pop3
+ns1
+ns2
+ns3
+ns4
+dns
+dns1
+dns2
+mx
+mx1
+mx2
+imap
+remote
+blog
+shop
+store
+forum
+wiki
+cpanel
+whm
+webdisk
+direct
+www2
+www3
+www4
+img
+image
+images
+media
+static
+assets
+cdn
+api
+dev
+test
+stage
+staging
+prod
+production
+beta
+alpha
+demo
+qa
+uat
+sandbox
+
+# === Web Servers ===
+web
+web1
+web2
+web3
+web01
+web02
+web03
+app
+app1
+app2
+app3
+app01
+app02
+app03
+server
+server1
+server2
+server01
+server02
+srv
+srv1
+srv2
+srv01
+srv02
+node
+node1
+node2
+node01
+node02
+host
+host1
+host2
+origin
+origin-www
+gateway
+proxy
+reverse
+frontend
+backend
+
+# === Development ===
+dev
+dev1
+dev2
+dev01
+dev02
+develop
+development
+test
+test1
+test2
+test01
+test02
+testing
+stage
+stage1
+stage2
+staging
+staging1
+staging2
+pre
+preprod
+pre-prod
+pre-production
+uat
+uat1
+uat2
+qa
+qa1
+qa2
+sandbox
+sandbox1
+sandbox2
+demo
+demo1
+demo2
+alpha
+beta
+gamma
+local
+localhost
+preview
+canary
+nightly
+edge
+
+# === Production ===
+prod
+prod1
+prod2
+prod01
+prod02
+production
+production1
+production2
+live
+live1
+live2
+stable
+release
+master
+main
+primary
+secondary
+
+# === API ===
+api
+api1
+api2
+api3
+api-v1
+api-v2
+api-v3
+api-gateway
+api-dev
+api-test
+api-staging
+api-prod
+rest
+rest-api
+restapi
+graphql
+graphql-api
+gql
+grpc
+rpc
+ws
+websocket
+socket
+realtime
+stream
+
+# === Admin ===
+admin
+admin1
+admin2
+administrator
+administrator1
+adm
+superadmin
+sysadmin
+webadmin
+control
+controlpanel
+control-panel
+panel
+cpanel
+whm
+plesk
+horde
+roundcube
+manage
+management
+console
+dashboard
+
+# === Authentication ===
+auth
+auth1
+auth2
+authentication
+login
+signin
+signon
+sign-in
+sign-on
+logout
+signout
+sign-out
+sso
+single-sign-on
+oauth
+oauth2
+openid
+saml
+identity
+id
+idp
+identityprovider
+accounts
+account
+users
+user
+members
+member
+register
+registration
+signup
+sign-up
+password
+forgot-password
+reset-password
+
+# === Database ===
+db
+db1
+db2
+db01
+db02
+database
+database1
+database2
+mysql
+mysql1
+mysql2
+mariadb
+postgres
+postgresql
+pgsql
+mongo
+mongodb
+redis
+redis1
+redis2
+memcache
+memcached
+cache
+cache1
+cache2
+elastic
+elasticsearch
+es
+es1
+es2
+solr
+lucene
+sphinx
+cassandra
+couchdb
+dynamodb
+rds
+aurora
+
+# === Mail ===
+mail
+mail1
+mail2
+mail01
+mail02
+smtp
+smtp1
+smtp2
+pop
+pop3
+imap
+imap4
+webmail
+webmail1
+webmail2
+email
+email1
+email2
+mx
+mx1
+mx2
+mx3
+mx4
+relay
+relay1
+relay2
+mailserver
+exchange
+owa
+outlook
+autodiscover
+autoconfig
+postfix
+sendmail
+exim
+dovecot
+roundcube
+horde
+squirrelmail
+newsletter
+mailing
+lists
+listserv
+
+# === Security ===
+secure
+security
+sec
+ssl
+tls
+https
+vpn
+vpn1
+vpn2
+openvpn
+ipsec
+firewall
+fw
+waf
+ids
+ips
+siem
+soc
+cert
+certs
+certificates
+pki
+ca
+key
+keys
+vault
+secrets
+2fa
+mfa
+otp
+totp
+
+# === Infrastructure ===
+ns
+ns1
+ns2
+ns3
+ns4
+dns
+dns1
+dns2
+dns3
+dns4
+nameserver
+nameserver1
+nameserver2
+resolver
+recursive
+authoritative
+ntp
+ntp1
+ntp2
+time
+ldap
+ldap1
+ldap2
+ad
+ads
+active-directory
+directory
+radius
+tacacs
+dhcp
+dhcp1
+dhcp2
+tftp
+pxe
+boot
+
+# === Monitoring ===
+monitor
+monitoring
+mon
+nagios
+zabbix
+prometheus
+grafana
+kibana
+splunk
+datadog
+newrelic
+dynatrace
+elastic
+elk
+logstash
+graylog
+sentry
+pagerduty
+status
+uptime
+healthcheck
+health
+metrics
+stats
+statistics
+analytics
+apm
+trace
+tracing
+
+# === Storage ===
+storage
+storage1
+storage2
+files
+file
+fileserver
+nas
+san
+nfs
+smb
+samba
+cifs
+ftp
+sftp
+ftps
+tftp
+s3
+minio
+backup
+backup1
+backup2
+backups
+archive
+archives
+data
+data1
+data2
+media
+media1
+media2
+upload
+uploads
+download
+downloads
+share
+shares
+public
+private
+
+# === CI/CD & DevOps ===
+ci
+cd
+cicd
+ci-cd
+jenkins
+jenkins1
+jenkins2
+bamboo
+travis
+circleci
+gitlab
+gitlab-ci
+github
+bitbucket
+git
+git1
+git2
+gitea
+gogs
+svn
+subversion
+hg
+mercurial
+cvs
+repo
+repos
+repository
+repositories
+artifact
+artifacts
+artifactory
+nexus
+sonatype
+registry
+docker
+docker-registry
+container
+containers
+k8s
+kubernetes
+rancher
+openshift
+helm
+chart
+charts
+
+# === Cloud ===
+cloud
+cloud1
+cloud2
+aws
+amazon
+azure
+gcp
+google
+googlecloud
+digitalocean
+linode
+vultr
+heroku
+vercel
+netlify
+cloudflare
+fastly
+akamai
+cloudfront
+aliyun
+alibaba
+tencent
+huawei
+oracle
+ibm
+ovh
+hetzner
+scaleway
+upcloud
+
+# === CDN & Assets ===
+cdn
+cdn1
+cdn2
+static
+static1
+static2
+assets
+assets1
+assets2
+img
+image
+images
+images1
+images2
+photo
+photos
+pic
+pics
+video
+videos
+media
+media1
+media2
+js
+javascript
+css
+stylesheet
+fonts
+font
+resources
+resource
+content
+files
+download
+downloads
+public
+pub
+
+# === Mobile ===
+mobile
+m
+mobile-api
+mobileapi
+app-api
+appapi
+ios
+android
+mobile-web
+webapp
+pwa
+
+# === Internal ===
+internal
+intranet
+int
+corp
+corporate
+company
+office
+hq
+headquarters
+staff
+employee
+employees
+hr
+human-resources
+payroll
+finance
+accounting
+sales
+marketing
+legal
+it
+helpdesk
+support
+ticket
+tickets
+service
+servicedesk
+
+# === Regional ===
+us
+us1
+us2
+us-east
+us-west
+na
+northamerica
+eu
+eu1
+eu2
+europe
+uk
+gb
+de
+germany
+fr
+france
+es
+spain
+it
+italy
+nl
+netherlands
+asia
+ap
+apac
+jp
+japan
+kr
+korea
+cn
+china
+sg
+singapore
+au
+australia
+in
+india
+br
+brazil
+latam
+la
+emea
+mea
+africa
+
+# === Services ===
+www
+web
+portal
+gateway
+proxy
+lb
+loadbalancer
+load-balancer
+haproxy
+nginx
+apache
+httpd
+tomcat
+jboss
+wildfly
+weblogic
+websphere
+iis
+glassfish
+jetty
+gunicorn
+unicorn
+puma
+passenger
+uwsgi
+supervisor
+pm2
+
+# === Communication ===
+chat
+im
+instant-messaging
+slack
+teams
+zoom
+meet
+conference
+video
+voip
+sip
+asterisk
+pbx
+phone
+telephone
+call
+calls
+fax
+sms
+mms
+push
+notification
+notifications
+
+# === E-commerce ===
+shop
+store
+ecommerce
+e-commerce
+cart
+checkout
+payment
+payments
+pay
+billing
+invoice
+invoices
+order
+orders
+catalog
+product
+products
+inventory
+shipping
+tracking
+
+# === CMS ===
+cms
+wordpress
+wp
+blog
+drupal
+joomla
+magento
+shopify
+wix
+squarespace
+woocommerce
+prestashop
+opencart
+ghost
+medium
+typo3
+contentful
+strapi
+sanity
+prismic
+
+# === Analytics ===
+analytics
+stats
+statistics
+tracking
+pixel
+tag
+tags
+gtm
+ga
+google-analytics
+matomo
+piwik
+hotjar
+heap
+mixpanel
+amplitude
+segment
+optimizely
+ab-test
+abtest
+abtesting
+experiment
+
+# === Documentation ===
+docs
+doc
+documentation
+wiki
+kb
+knowledgebase
+knowledge-base
+help
+support
+faq
+faqs
+manual
+manuals
+guide
+guides
+tutorial
+tutorials
+reference
+references
+readme
+changelog
+
+# === Collaboration ===
+confluence
+jira
+trello
+notion
+asana
+monday
+basecamp
+clickup
+linear
+shortcut
+clubhouse
+gitlab
+github
+bitbucket
+slack
+discord
+teams
+
+# === CRM & ERP ===
+crm
+erp
+salesforce
+sf
+hubspot
+zoho
+dynamics
+sap
+oracle
+workday
+servicenow
+zendesk
+freshdesk
+intercom
+drift
+
+# === Misc Patterns ===
+old
+new
+legacy
+archive
+archives
+bak
+backup
+temp
+tmp
+cache
+log
+logs
+debug
+trace
+info
+test1
+test2
+test3
+dev1
+dev2
+dev3
+stage1
+stage2
+prod1
+prod2
+www1
+www2
+www3
+ns01
+ns02
+mx01
+mx02
+web01
+web02
+app01
+app02
+db01
+db02
+srv01
+srv02
+node01
+node02
+
+# === Numbers ===
+1
+2
+3
+01
+02
+03
+001
+002
+003
+
+# === Prefixed common ===
+dev-api
+test-api
+stage-api
+prod-api
+dev-web
+test-web
+stage-web
+prod-web
+dev-app
+test-app
+stage-app
+prod-app
+api-dev
+api-test
+api-stage
+api-prod
+web-dev
+web-test
+web-stage
+web-prod
+app-dev
+app-test
+app-stage
+app-prod
+
+# === Suffixed common ===
+www-dev
+www-test
+www-stage
+www-prod
+mail-dev
+mail-test
+mail-stage
+mail-prod
+cdn-dev
+cdn-test
+cdn-stage
+cdn-prod
+
+# === Hyphenated variants ===
+dev-server
+test-server
+staging-server
+production-server
+dev-db
+test-db
+staging-db
+prod-db
+dev-api
+staging-api
+prod-api
+dev-web
+staging-web
+prod-web
+
+# === Technology specific ===
+jenkins
+sonarqube
+artifactory
+nexus
+vault
+consul
+nomad
+terraform
+ansible
+puppet
+chef
+salt
+prometheus
+grafana
+elasticsearch
+kibana
+logstash
+fluentd
+jaeger
+zipkin
+istio
+envoy
+nginx-ingress
+traefik
+harbor
+quay
+portainer
+rancher
+lens
+
+# === Gaming/Entertainment ===
+game
+games
+gaming
+play
+player
+server
+gameserver
+minecraft
+steam
+origin
+uplay
+epic
+twitch
+youtube
+stream
+streaming
+live
+broadcast
+vod
+
+# === Education ===
+learn
+learning
+edu
+education
+school
+university
+college
+student
+students
+teacher
+teachers
+course
+courses
+class
+classroom
+lms
+moodle
+canvas
+blackboard
+library
+
+# === Healthcare ===
+health
+healthcare
+medical
+patient
+patients
+doctor
+doctors
+hospital
+clinic
+pharmacy
+rx
+prescription
+lab
+labs
+laboratory
+ehr
+emr
+pacs
+hl7
+fhir
+
+# === Finance ===
+finance
+financial
+bank
+banking
+trading
+trade
+invest
+investment
+wallet
+crypto
+bitcoin
+blockchain
+payment
+payments
+transfer
+wire
+ach
+swift
+
+# === Government ===
+gov
+government
+city
+county
+state
+federal
+portal
+citizen
+service
+services
+tax
+taxes
+license
+permit
+court
+police
+fire
+emergency
+
+# === Telecom ===
+telecom
+telco
+network
+networks
+internet
+isp
+broadband
+fiber
+cable
+wireless
+wifi
+5g
+4g
+lte
+mobile
+voip
+pbx
+
+# === Manufacturing ===
+erp
+mes
+scada
+plc
+iot
+sensor
+sensors
+device
+devices
+machine
+machines
+factory
+plant
+production
+assembly
+quality
+qa
+
+# === Logistics ===
+logistics
+supply
+supply-chain
+warehouse
+warehouses
+inventory
+shipping
+tracking
+fleet
+delivery
+order
+orders
+fulfillment
+distribution
+
+# === Real Estate ===
+property
+properties
+real-estate
+realestate
+listing
+listings
+agent
+agents
+broker
+brokers
+mls
+home
+homes
+house
+houses
+apartment
+apartments
+rent
+rental
+lease
+
+# === Hospitality ===
+hotel
+hotels
+booking
+reservation
+reservations
+guest
+guests
+room
+rooms
+restaurant
+restaurants
+food
+menu
+bar
+spa
+resort
+
+# === Retail ===
+store
+stores
+shop
+shopping
+retail
+pos
+inventory
+product
+products
+catalog
+order
+orders
+customer
+customers
+loyalty
+rewards
+
+# === Automotive ===
+auto
+automotive
+car
+cars
+vehicle
+vehicles
+dealer
+dealership
+service
+parts
+warranty
+lease
+finance
+
+# === Energy ===
+energy
+power
+electric
+electricity
+gas
+oil
+solar
+wind
+renewable
+grid
+meter
+meters
+utility
+utilities
+
+# === Agriculture ===
+farm
+farming
+agriculture
+agri
+crop
+crops
+field
+fields
+livestock
+dairy
+grain
+harvest
+
+# === Construction ===
+construction
+project
+projects
+site
+sites
+bid
+bids
+contract
+contracts
+permit
+permits
+safety
+equipment
+
+# === Legal ===
+legal
+law
+attorney
+lawyers
+court
+case
+cases
+document
+documents
+contract
+contracts
+compliance
+regulatory
+
+# === HR ===
+hr
+human-resources
+recruit
+recruiting
+talent
+hire
+hiring
+employee
+employees
+payroll
+benefits
+training
+onboarding
+
+# === Marketing ===
+marketing
+brand
+branding
+campaign
+campaigns
+social
+social-media
+content
+creative
+seo
+sem
+ppc
+email-marketing
+automation
+
+# === Research ===
+research
+lab
+labs
+laboratory
+science
+data
+analytics
+ai
+ml
+machine-learning
+deep-learning
+nlp
+cv
+computer-vision
+
+# === Event ===
+event
+events
+conference
+conferences
+meeting
+meetings
+webinar
+webinars
+registration
+ticket
+tickets
+
+# === Emerging Tech ===
+ai
+ml
+deep-learning
+neural
+model
+models
+training
+inference
+gpu
+tpu
+cuda
+tensorflow
+pytorch
+jupyter
+notebook
+notebooks
+colab
+sagemaker
+vertex
+bedrock
+openai
+anthropic
+huggingface
+
+# === Additional Common ===
+about
+access
+account
+accounts
+adserver
+adm
+admin
+administrator
+admins
+affiliate
+affiliates
+agent
+agents
+ajax
+alerts
+all
+alpha
+amp
+android
+api
+apis
+app
+application
+apps
+apt
+archive
+archives
+assets
+assist
+atom
+auth
+auto
+autodiscover
+autoconfig
+automation
+avatar
+avatars
+aws
+b2b
+b2c
+backend
+backup
+backups
+banner
+banners
+base
+batch
+bb
+bbs
+beta
+billing
+bin
+binaries
+binary
+biz
+blog
+blogs
+board
+boards
+bot
+bots
+box
+broadcast
+broker
+bucket
+buckets
+bug
+bugs
+build
+builds
+bulk
+bulletin
+business
+buy
+cache
+calendar
+call
+callback
+calls
+campaign
+campaigns
+captcha
+card
+cards
+career
+careers
+cart
+cas
+catalog
+cdn
+central
+cgi
+change
+changes
+channel
+channels
+chart
+charts
+chat
+ci
+client
+clients
+cloud
+cluster
+clusters
+cms
+cn
+code
+codes
+collect
+collection
+collections
+comm
+comment
+comments
+commerce
+common
+community
+company
+compare
+component
+components
+conf
+config
+configuration
+connect
+connection
+connector
+connectors
+console
+consult
+contact
+contacts
+container
+containers
+content
+control
+controller
+corp
+corporate
+coupons
+coverage
+cp
+cpanel
+crash
+crm
+cron
+cronjob
+cronjobs
+crowd
+crypto
+cs
+css
+customer
+customers
+cv
+d
+daemon
+daily
+dash
+dashboard
+data
+database
+db
+dealer
+debug
+default
+demo
+demos
+deploy
+deployment
+design
+desktop
+detect
+detection
+dev
+devel
+develop
+developer
+developers
+development
+device
+devices
+devops
+dfp
+dig
+digital
+dir
+direct
+directory
+disabled
+discuss
+discussion
+disk
+dispatcher
+dl
+dns
+doc
+docker
+docs
+document
+documentation
+documents
+domain
+domains
+donate
+download
+downloads
+draft
+drafts
+drive
+drop
+drupal
+dump
+dynamic
+e
+ecommerce
+edge
+edit
+editor
+edu
+education
+el
+elastic
+elasticsearch
+element
+elements
+email
+embed
+employee
+employees
+en
+endpoint
+endpoints
+engine
+engineering
+enterprise
+entries
+entry
+env
+environment
+environments
+error
+errors
+es
+eu
+event
+events
+example
+examples
+exchange
+exec
+execute
+export
+external
+extranet
+f
+facebook
+faq
+faqs
+farm
+fast
+fax
+feature
+features
+feed
+feedback
+feeds
+field
+fields
+file
+files
+filter
+filters
+finance
+financial
+find
+finder
+fire
+firewall
+first
+fix
+fixes
+flash
+fleet
+flow
+folder
+folders
+font
+fonts
+food
+forex
+form
+forms
+forum
+forums
+forward
+found
+frame
+framework
+free
+freeware
+fresh
+frontend
+fs
+ftp
+ftps
+full
+function
+functions
+fund
+future
+fw
+g
+gallery
+game
+games
+gateway
+gen
+general
+generate
+generator
+get
+gis
+git
+github
+gitlab
+global
+go
+google
+gov
+gps
+graph
+graphql
+graphs
+green
+grid
+group
+groups
+grow
+growth
+gsm
+guest
+guests
+gui
+guide
+guides
+gw
+h
+hack
+hacking
+hadoop
+handle
+handler
+handlers
+hardware
+hash
+hd
+hdr
+health
+healthcare
+heap
+heartbeat
+help
+helpdesk
+hidden
+high
+history
+hits
+hk
+hls
+hm
+home
+homepage
+hook
+hooks
+horizon
+hospital
+host
+hosted
+hosting
+hostmaster
+hosts
+hot
+hotel
+house
+how
+howto
+hp
+hr
+hs
+html
+http
+https
+hub
+hvac
+i
+i18n
+id
+identity
+ids
+ig
+iis
+im
+image
+images
+imap
+import
+in
+inbox
+inbound
+include
+includes
+incoming
+index
+indexer
+india
+industries
+industry
+info
+infra
+infrastructure
+ingest
+init
+inline
+inner
+inout
+input
+inputs
+inquiry
+inside
+insights
+install
+installer
+instance
+instances
+int
+integration
+integrations
+intel
+intelligence
+interact
+interactive
+interface
+interfaces
+intern
+internal
+international
+internet
+intranet
+intro
+inventory
+invite
+io
+ios
+iot
+ip
+iphone
+ipmi
+ips
+ipsec
+ipv4
+ipv6
+irc
+ircd
+is
+iso
+isp
+issue
+issues
+it
+item
+items
+itunes
+j
+ja
+java
+javascript
+jb
+jenkins
+jira
+job
+jobs
+join
+journal
+jquery
+js
+json
+jump
+jumpbox
+k
+k8s
+kafka
+kali
+karma
+kb
+kdc
+keep
+kernel
+key
+keys
+kibana
+kickstart
+kill
+kiosk
+kit
+km
+know
+knowledge
+known
+ko
+kube
+kubernetes
+l
+l10n
+lab
+label
+labels
+labs
+landing
+language
+large
+last
+latest
+launch
+launcher
+layer
+layout
+lb
+ld
+ldap
+lead
+leads
+lean
+learn
+learning
+legacy
+legal
+letsencrypt
+lib
+libraries
+library
+license
+lifecycle
+light
+like
+limit
+limits
+line
+linear
+link
+linkedin
+links
+linux
+list
+listen
+lists
+listserv
+live
+lms
+load
+loadbalancer
+loader
+loading
+local
+locale
+localhost
+location
+locations
+lock
+locked
+log
+loghost
+logging
+logic
+login
+logon
+logout
+logs
+long
+loop
+low
+lp
+ls
+lync
+m
+m2m
+mac
+machine
+machines
+mail
+mail1
+mail2
+mail3
+mail4
+mail5
+mailbox
+maildir
+mailer
+mailgate
+mailhost
+mailing
+mailman
+mailserver
+main
+maintenance
+make
+manage
+managed
+management
+manager
+manifest
+manual
+manuals
+map
+mapping
+maps
+market
+marketing
+marketplace
+master
+match
+matrix
+maven
+max
+mc
+mcp
+md
+mdb
+me
+measure
+media
+meeting
+meetings
+member
+members
+membership
+memo
+memory
+merge
+mesh
+message
+messages
+messaging
+meta
+metadata
+meter
+metric
+metrics
+mgmt
+mi
+mic
+micro
+microsoft
+middleware
+migration
+min
+mirror
+mirrors
+misc
+mission
+mix
+ml
+mm
+mms
+mo
+mob
+mobile
+mock
+mod
+model
+models
+modern
+module
+modules
+mon
+mongo
+mongodb
+monitor
+monitoring
+monthly
+more
+mount
+movie
+movies
+mp
+mp3
+mp4
+mq
+mqtt
+mr
+ms
+mssql
+mt
+mta
+mts
+multi
+multimedia
+music
+mx
+mx1
+mx2
+my
+mysql
+n
+n1
+n2
+na
+name
+names
+nameserver
+naming
+nas
+nat
+nav
+navigation
+nb
+nc
+nd
+nds
+net
+netdata
+netflow
+netops
+network
+networking
+networks
+new
+newrelic
+news
+newsletter
+next
+nexus
+nfs
+ng
+nginx
+ni
+night
+nl
+nm
+nn
+no
+noc
+node
+nodes
+noi
+north
+nos
+not
+notes
+notice
+notification
+notifications
+notify
+now
+np
+ns
+ns1
+ns2
+ns3
+ns4
+ns5
+nt
+ntp
+ntp1
+ntp2
+null
+number
+numbers
+nw
+o
+oauth
+oauth2
+obe
+object
+objects
+observability
+oc
+ocs
+ocs2
+od
+ods
+of
+office
+office365
+offline
+ok
+old
+omega
+on
+one
+online
+ood
+oob
+oom
+op
+open
+openapi
+openid
+opensource
+openstack
+ops
+opt
+option
+options
+oracle
+orchestration
+order
+orders
+org
+org1
+org2
+organization
+origin
+os
+oss
+ost
+other
+out
+outage
+outbound
+outer
+outlook
+output
+outputs
+outreach
+outside
+outsourcing
+over
+owa
+owl
+own
+owner
+p
+p2p
+pa
+package
+packages
+packaging
+page
+pages
+paid
+panel
+pano
+paper
+parallel
+param
+params
+parent
+park
+parking
+parse
+parser
+part
+partner
+partners
+parts
+party
+pass
+passenger
+password
+passwords
+paste
+patch
+patches
+path
+pay
+payment
+payments
+paypal
+payout
+pb
+pc
+pci
+pdf
+pe
+peak
+peer
+peering
+pen
+pending
+people
+perf
+performance
+perm
+permissions
+persist
+persistent
+person
+personal
+pet
+pf
+pg
+pgadmin
+phone
+phones
+photo
+photos
+php
+phpmyadmin
+pi
+pic
+pics
+picture
+pictures
+pilot
+pin
+ping
+pioneer
+pipeline
+pipelines
+pit
+pixel
+pk
+pkg
+pkgs
+place
+placement
+plan
+planner
+planning
+plans
+platform
+platforms
+play
+player
+playground
+please
+plesk
+plugin
+plugins
+plus
+pm
+pm2
+pma
+pms
+pn
+pns
+poc
+pod
+pods
+point
+points
+policies
+policy
+poll
+polls
+pop
+pop3
+popmail
+port
+portal
+portfolio
+pos
+position
+positions
+post
+postgre
+postgres
+postgresql
+postman
+postmaster
+posts
+power
+pp
+ppg
+ppm
+ppt
+pptx
+pr
+pre
+predict
+prediction
+pref
+preference
+preferences
+premium
+prep
+preprod
+presence
+present
+presentation
+press
+preview
+price
+prices
+pricing
+primary
+prime
+print
+printer
+printers
+printing
+priority
+prism
+privacy
+private
+privilege
+pro
+probe
+problem
+problems
+proc
+process
+processes
+processing
+processor
+prod
+product
+production
+products
+professional
+profile
+profiles
+profit
+program
+programs
+progress
+project
+projects
+promo
+promos
+promotion
+promotions
+prompt
+proof
+prop
+properties
+property
+proposal
+protect
+protection
+proto
+protocol
+prototype
+provide
+provider
+providers
+provision
+provisioning
+proxy
+prx
+ps
+pt
+pta
+ptp
+pts
+pub
+public
+publish
+publisher
+publishing
+pull
+pulse
+puppet
+purchase
+push
+put
+pw
+pxe
+python
+q
+qa
+qc
+qm
+qms
+qq
+qt
+query
+question
+questions
+queue
+queues
+quick
+quota
+quote
+quotes
+r
+r1
+r2
+r10
+rabbitmq
+rack
+radar
+radio
+radius
+rads
+raid
+rails
+rain
+random
+range
+rank
+ranking
+rapid
+rate
+rates
+rating
+ratings
+raw
+rc
+rcs
+rdc
+rds
+re
+react
+read
+readme
+ready
+real
+realtime
+reason
+rebuild
+receive
+receiver
+recent
+recommend
+recommendation
+recommendations
+record
+recorder
+records
+recover
+recovery
+recruit
+recruiting
+recycle
+red
+redirect
+redis
+ref
+reference
+references
+referral
+refresh
+reg
+region
+regional
+regions
+register
+registration
+registry
+related
+relay
+release
+releases
+reload
+remote
+removal
+remove
+render
+repair
+replica
+replicate
+replication
+reply
+report
+reporter
+reporters
+reporting
+reports
+repo
+repos
+repository
+request
+requests
+require
+requirements
+res
+research
+reseller
+resellers
+reserve
+reset
+residence
+resolve
+resolver
+resource
+resources
+response
+responses
+rest
+restart
+restore
+result
+results
+resume
+retail
+retention
+retire
+return
+returns
+rev
+reverse
+review
+reviews
+revision
+rg
+ri
+rich
+ride
+ring
+risk
+rl
+rm
+rn
+ro
+road
+robot
+robots
+role
+roles
+roll
+rollback
+rollout
+root
+rootmail
+ros
+rotate
+rotation
+route
+router
+routers
+routes
+routing
+row
+rows
+rp
+rpc
+rpm
+rr
+rs
+rsa
+rss
+rt
+rtc
+rtmp
+rtp
+ruby
+rule
+rules
+run
+runner
+running
+runtime
+rv
+rx
+s
+s1
+s2
+s3
+s4
+s5
+sa
+saa
+saas
+safe
+safety
+sale
+sales
+salesforce
+salt
+sam
+sample
+samples
+samba
+san
+sandbox
+sap
+sat
+satellite
+save
+sc
+scala
+scale
+scan
+scanner
+schedule
+scheduler
+scheduling
+schema
+schemes
+school
+science
+scope
+score
+scores
+scoring
+scr
+screen
+screens
+script
+scripts
+sd
+sdc
+sdn
+sdr
+se
+search
+seat
+seats
+sec
+second
+secondary
+secret
+secrets
+section
+sections
+secure
+secured
+security
+seed
+segment
+segmentation
+select
+self
+self-service
+sell
+sem
+seminar
+seminars
+send
+sender
+senders
+sendmail
+seo
+sequence
+sequoia
+series
+serve
+server
+server1
+server2
+serverless
+servers
+service
+services
+servlet
+session
+sessions
+set
+setting
+settings
+setup
+sf
+sftp
+sg
+sh
+shadow
+share
+shared
+sharepoint
+shares
+sharing
+shell
+shield
+shift
+ship
+shipping
+shop
+shopping
+shops
+short
+shortlink
+shorturl
+show
+showcase
+shows
+si
+side
+sign
+signal
+signature
+signin
+signoff
+signon
+signout
+signup
+sim
+simple
+simulation
+simulator
+single
+sink
+sip
+sis
+sit
+site
+sites
+sitemap
+size
+sk
+skeleton
+sketch
+skill
+skills
+skin
+skins
+skip
+skype
+sl
+slack
+slave
+slide
+slides
+slim
+slo
+slot
+slow
+sm
+small
+smart
+smb
+sme
+smg
+sms
+smtp
+smtp1
+smtp2
+smtp3
+sn
+snap
+snapshot
+snapshots
+snmp
+sns
+so
+soa
+soap
+soc
+social
+socket
+sockets
+soft
+software
+sol
+solar
+solr
+solution
+solutions
+som
+some
+son
+sonar
+sonarqube
+sonic
+sop
+sort
+sos
+sound
+source
+sources
+south
+sp
+spa
+space
+spaces
+spam
+spanish
+spark
+spec
+special
+specs
+speed
+spell
+spider
+spike
+spinup
+split
+spoke
+sport
+sports
+spot
+spring
+sprint
+sps
+spy
+sq
+sql
+sqlserver
+squid
+sr
+src
+sre
+srm
+srv
+srv1
+srv2
+ss
+ssd
+ssh
+ssl
+sslvpn
+sso
+ssrs
+sss
+st
+stable
+stack
+stacks
+staff
+stage
+stage1
+stage2
+stages
+staging
+staging1
+staging2
+stamp
+stand
+standard
+standards
+star
+stars
+start
+starter
+stat
+state
+statement
+statements
+static
+static1
+static2
+statistics
+stats
+status
+stay
+stb
+std
+steam
+step
+steps
+stg
+stick
+sticky
+stock
+stocks
+stop
+storage
+store
+store1
+store2
+stores
+stories
+storm
+story
+str
+stream
+streaming
+streams
+stress
+string
+strings
+stripe
+strong
+struct
+structure
+sts
+stun
+style
+styles
+sub
+subdomain
+subject
+submit
+subscribe
+subscription
+subscriptions
+success
+suite
+sum
+summary
+summer
+sun
+sup
+super
+superuser
+support
+support1
+support2
+surge
+survey
+surveys
+sv
+svn
+sw
+swap
+swift
+switch
+sws
+sx
+sy
+symbol
+symbols
+symfony
+sync
+synch
+synergy
+sys
+sysadmin
+syslog
+system
+systems
+sz
+t
+t1
+t2
+tab
+table
+tables
+tablet
+tablets
+tac
+tag
+tags
+tail
+taiwan
+talent
+talk
+tam
+tap
+tape
+tar
+target
+targets
+task
+tasks
+tax
+taxes
+tc
+tcp
+td
+te
+teach
+teaching
+team
+teams
+tech
+technical
+technology
+tel
+telecom
+telegraph
+telephone
+telephony
+telnet
+temp
+temp1
+template
+templates
+ten
+tenant
+tenants
+term
+terminal
+terms
+test
+test1
+test2
+test3
+tester
+testers
+testing
+tests
+text
+tf
+tftp
+tfs
+tg
+th
+thailand
+theme
+themes
+thesis
+thick
+thin
+third
+thread
+threads
+three
+threshold
+tic
+tick
+ticket
+ticketing
+tickets
+tie
+tier
+tiger
+tile
+tiles
+tim
+time
+timeline
+timer
+times
+timing
+tip
+tips
+title
+titles
+tk
+tl
+tld
+tm
+tn
+to
+toast
+todo
+toggle
+token
+tokens
+tokyo
+toll
+tom
+ton
+tool
+toolbar
+toolkit
+tools
+top
+topic
+topics
+tor
+toronto
+total
+touch
+tour
+tours
+tow
+tower
+town
+tr
+trace
+tracer
+traces
+tracing
+track
+tracker
+trackers
+tracking
+tracks
+trade
+trader
+trades
+trading
+traffic
+trail
+train
+trainer
+training
+trans
+transaction
+transactions
+transfer
+transform
+transformation
+transit
+transition
+translate
+translation
+translations
+transport
+travel
+treasury
+tree
+trend
+trends
+trial
+trials
+trigger
+triggers
+trip
+trips
+true
+trunk
+trust
+trusted
+try
+ts
+tsc
+tsg
+tst
+tt
+tunnel
+turn
+turnstile
+tutorial
+tutorials
+tv
+tw
+twelve
+twitter
+two
+tx
+txt
+ty
+type
+types
+tz
+u
+ua
+ubuntu
+uc
+ucs
+ud
+udp
+ue
+ug
+ui
+uid
+uk
+ukraine
+ultra
+um
+un
+undefined
+undo
+unicast
+unicorn
+unified
+union
+unique
+unit
+united
+units
+universal
+university
+unix
+unknown
+unlock
+until
+up
+update
+updater
+updates
+upgrade
+upgrades
+uplink
+upload
+uploads
+upn
+ups
+uptime
+ur
+urban
+uri
+url
+urls
+us
+usa
+usage
+usb
+usd
+use
+used
+user
+user1
+user2
+userapi
+userdb
+username
+users
+usps
+util
+utilities
+utility
+utils
+uuid
+ux
+v
+v1
+v2
+v3
+v4
+v5
+va
+valid
+validate
+validation
+validator
+value
+values
+van
+var
+variable
+variables
+vars
+vault
+vc
+vcenter
+vcs
+vdi
+ve
+vector
+vendor
+vendors
+ver
+verification
+verify
+version
+versions
+vertex
+vf
+vg
+vh
+vi
+via
+video
+video1
+video2
+videos
+view
+viewer
+viewers
+views
+vim
+vip
+vip1
+vip2
+vips
+viral
+virtual
+vision
+visit
+visitor
+visitors
+visual
+vital
+vm
+vm1
+vm2
+vmail
+vms
+vmware
+vn
+vnc
+vo
+voice
+void
+voip
+vol
+volume
+volumes
+volunteer
+vote
+voting
+vp
+vpdn
+vpn
+vpn1
+vpn2
+vpn3
+vpns
+vps
+vr
+vs
+vsphere
+vt
+vts
+vue
+w
+w1
+w2
+w3
+wa
+waf
+wait
+walk
+wall
+wallet
+wan
+warehouse
+watch
+water
+wave
+wc
+wd
+we
+weather
+web
+web1
+web2
+web3
+web01
+web02
+web03
+webapp
+webapi
+webapps
+webcache
+webcam
+webcast
+webconf
+webdb
+webdisk
+webdns
+webfarm
+webhelp
+webhost
+webhosting
+webmail
+webmail1
+webmail2
+webmaster
+webmin
+webportal
+webproxy
+webpush
+webs
+webserver
+webservice
+webservices
+website
+websocket
+webstore
+websvr
+wechat
+weekly
+welcome
+well
+west
+wf
+whats
+wheel
+white
+whitelist
+who
+whois
+wholesale
+wi
+widget
+widgets
+wifi
+wiki
+wikis
+win
+wind
+window
+windows
+winter
+wip
+wire
+wireless
+wm
+wms
+wn
+wo
+woo
+woocommerce
+word
+wordpress
+work
+worker
+workers
+workflow
+workflows
+working
+workplace
+works
+workshop
+world
+worldwide
+wowza
+wp
+wp-admin
+wpad
+wr
+wrap
+wrapper
+write
+writer
+writers
+writing
+ws
+wsdl
+wsus
+ww
+ww1
+ww2
+ww3
+www
+www0
+www1
+www2
+www3
+www4
+www5
+www6
+www7
+www8
+www9
+wwww
+wy
+wyoming
+x
+x1
+x2
+x3
+x509
+xampp
+xen
+xfer
+xi
+xl
+xls
+xlsx
+xml
+xmlrpc
+xmpp
+xn
+xo
+xp
+xr
+xs
+xsd
+xslt
+xt
+xx
+y
+ya
+yahoo
+yaml
+year
+yellow
+yelp
+yes
+yesterday
+yi
+ym
+yo
+yoga
+york
+you
+young
+your
+youth
+yt
+yu
+yw
+yx
+z
+z1
+z2
+za
+zabbix
+zane
+zap
+zb
+ze
+zen
+zendesk
+zend
+zenith
+zephyr
+zero
+zf
+zh
+zi
+zip
+zk
+zm
+zn
+zo
+zone
+zones
+zoo
+zoom
+zr
+zs
+zu
+zw
+zz
diff --git a/src/usfx/data/wordlist_large.txt b/src/usfx/data/wordlist_large.txt
@@ -0,0 +1,627 @@
+# Large Subdomain Wordlist
+# Contains extended list for thorough enumeration
+# Auto-generated from base wordlist with additions
+
+# Common subdomains
+www
+www1
+www2
+www3
+www4
+www5
+mail
+mail1
+mail2
+mail3
+smtp
+smtp1
+smtp2
+pop
+pop3
+imap
+webmail
+email
+mx
+mx1
+mx2
+mx3
+ftp
+ftp1
+ftp2
+sftp
+ns
+ns1
+ns2
+ns3
+ns4
+dns
+dns1
+dns2
+dns3
+vpn
+vpn1
+vpn2
+remote
+gateway
+proxy
+proxy1
+proxy2
+
+# Development
+dev
+dev1
+dev2
+dev3
+development
+test
+test1
+test2
+test3
+testing
+stage
+staging
+stage1
+stage2
+prod
+production
+qa
+qa1
+qa2
+uat
+sandbox
+sandbox1
+demo
+demo1
+beta
+beta1
+alpha
+preview
+pre
+preprod
+pre-production
+
+# Infrastructure
+server
+server1
+server2
+server3
+server4
+server5
+web
+web1
+web2
+web3
+web4
+web5
+app
+app1
+app2
+app3
+app4
+app5
+node
+node1
+node2
+node3
+node4
+host
+host1
+host2
+host3
+worker
+worker1
+worker2
+worker3
+lb
+loadbalancer
+lb1
+lb2
+cache
+cache1
+cache2
+memcache
+redis
+redis1
+redis2
+
+# Database
+db
+db1
+db2
+db3
+database
+mysql
+mysql1
+mysql2
+postgres
+postgresql
+pgsql
+mongo
+mongodb
+elastic
+elasticsearch
+mssql
+sql
+oracle
+
+# Services
+api
+api1
+api2
+api3
+api-v1
+api-v2
+api-v3
+rest
+graphql
+grpc
+websocket
+ws
+cdn
+cdn1
+cdn2
+static
+static1
+assets
+images
+img
+img1
+media
+files
+upload
+uploads
+download
+downloads
+docs
+documentation
+wiki
+help
+support
+status
+health
+monitor
+monitoring
+metrics
+logs
+logging
+kibana
+grafana
+prometheus
+nagios
+zabbix
+
+# Security & Admin
+admin
+admin1
+admin2
+administrator
+root
+secure
+security
+ssl
+portal
+login
+signin
+auth
+authentication
+oauth
+sso
+sso1
+panel
+dashboard
+console
+control
+manage
+management
+manager
+cpanel
+whm
+webdisk
+
+# Cloud & DevOps
+jenkins
+jenkins1
+ci
+cd
+build
+build1
+deploy
+deployment
+git
+gitlab
+github
+bitbucket
+jira
+confluence
+slack
+teams
+kubernetes
+k8s
+docker
+container
+registry
+helm
+terraform
+ansible
+aws
+azure
+gcp
+cloud
+s3
+storage
+bucket
+
+# Email & Communication
+email
+newsletter
+exchange
+outlook
+autodiscover
+autoconfig
+calendar
+chat
+im
+messenger
+meeting
+meetings
+
+# Regional & Geographic
+us
+us1
+us2
+eu
+eu1
+eu2
+uk
+de
+fr
+es
+it
+pt
+ru
+zh
+ja
+ko
+au
+ca
+asia
+emea
+apac
+latam
+east
+west
+north
+south
+
+# Versioned
+v1
+v2
+v3
+v4
+v5
+version1
+version2
+old
+new
+legacy
+archive
+archives
+backup
+backup1
+backup2
+bak
+temp
+tmp
+
+# Mobile
+mobile
+m
+app-api
+ios
+android
+mobile-api
+
+# Internal (important for internal network enumeration)
+internal
+internal1
+intranet
+corp
+corporate
+staff
+employee
+office
+office1
+hr
+finance
+sales
+marketing
+legal
+engineering
+eng
+it
+helpdesk
+service
+services
+dc
+dc1
+dc2
+dc3
+ad
+ldap
+kerberos
+ntp
+radius
+fileserver
+printserver
+sharepoint
+exchange
+citrix
+vmware
+vcenter
+esxi
+
+# Miscellaneous
+home
+public
+private
+origin
+origin-www
+direct
+main
+primary
+secondary
+data
+data1
+data2
+report
+reports
+analytics
+tracking
+crm
+erp
+cms
+ecommerce
+payment
+payments
+checkout
+cart
+order
+orders
+billing
+invoice
+invoices
+shop
+store
+booking
+reservation
+ticket
+tickets
+forum
+community
+blog
+news
+events
+
+# Name patterns
+web01
+web02
+web03
+web04
+web05
+app01
+app02
+app03
+app04
+app05
+srv01
+srv02
+srv03
+srv04
+srv05
+srv1
+srv2
+srv3
+srv4
+srv5
+dc01
+dc02
+dc03
+datacenter
+datacenter1
+datacenter2
+backup01
+backup02
+mail01
+mail02
+db01
+db02
+db03
+ns01
+ns02
+ns03
+host01
+host02
+host03
+node01
+node02
+node03
+
+# Network devices
+router
+router1
+router2
+switch
+switch1
+switch2
+firewall
+firewall1
+fw
+fw1
+fw2
+gateway1
+gateway2
+
+# Services by port
+http
+https
+ssh
+telnet
+rdp
+vnc
+
+# Cloud services
+aws-us-east
+aws-us-west
+aws-eu
+azure-east
+azure-west
+gcp-us
+gcp-eu
+
+# Additional internal patterns
+accounts
+account
+billing1
+billing2
+report1
+report2
+files1
+files2
+share
+share1
+share2
+nas
+nas1
+san
+storage1
+storage2
+archive1
+archive2
+log
+log1
+log2
+print
+print1
+scan
+scanner
+
+# API versions and endpoints
+api-internal
+api-external
+api-public
+api-private
+api-admin
+api-dev
+api-staging
+api-prod
+
+# More environment variations
+dev-api
+staging-api
+prod-api
+test-api
+dev-db
+staging-db
+prod-db
+dev-app
+staging-app
+prod-app
+
+# Additional security
+sso2
+auth1
+auth2
+login1
+login2
+secure1
+secure2
+admin-dev
+admin-staging
+
+# Monitoring variations
+monitoring1
+monitoring2
+metrics1
+metrics2
+logs1
+logs2
+status1
+status2
+
+# CDN and static variations
+cdn-us
+cdn-eu
+cdn-asia
+static-us
+static-eu
+assets-us
+assets-eu
+
+# Kubernetes/Container patterns
+k8s-master
+k8s-node1
+k8s-node2
+k8s-worker1
+k8s-worker2
+docker1
+docker2
+container1
+container2
+registry1
+registry2
+
+# Database clusters
+db-master
+db-slave
+db-replica
+mysql-master
+mysql-slave
+postgres-master
+postgres-slave
+redis-master
+redis-slave
+mongo-primary
+mongo-secondary
+
+# Message queues
+mq
+mq1
+mq2
+rabbitmq
+kafka
+kafka1
+kafka2
+activemq
+zeromq
+
+# Search engines
+search
+search1
+search2
+elasticsearch1
+elasticsearch2
+solr
+solr1
+solr2
+
+# Additional numbered patterns
+server01
+server02
+server03
+server04
+server05
+server06
+server07
+server08
+server09
+server10
+web06
+web07
+web08
+web09
+web10
+app06
+app07
+app08
+app09
+app10
diff --git a/src/usfx/data/wordlist_small.txt b/src/usfx/data/wordlist_small.txt
@@ -0,0 +1,500 @@
+# Ultimate Subdomain Finder Wordlist
+# 10,000+ common subdomain names for brute force enumeration
+
+# === Common ===
+www
+mail
+ftp
+localhost
+webmail
+smtp
+pop
+pop3
+ns1
+ns2
+ns3
+ns4
+dns
+dns1
+dns2
+mx
+mx1
+mx2
+imap
+remote
+blog
+shop
+store
+forum
+wiki
+cpanel
+whm
+webdisk
+direct
+www2
+www3
+www4
+img
+image
+images
+media
+static
+assets
+cdn
+api
+dev
+test
+stage
+staging
+prod
+production
+beta
+alpha
+demo
+qa
+uat
+sandbox
+
+# === Web Servers ===
+web
+web1
+web2
+web3
+web01
+web02
+web03
+app
+app1
+app2
+app3
+app01
+app02
+app03
+server
+server1
+server2
+server01
+server02
+srv
+srv1
+srv2
+srv01
+srv02
+node
+node1
+node2
+node01
+node02
+host
+host1
+host2
+origin
+origin-www
+gateway
+proxy
+reverse
+frontend
+backend
+
+# === Development ===
+dev
+dev1
+dev2
+dev01
+dev02
+develop
+development
+test
+test1
+test2
+test01
+test02
+testing
+stage
+stage1
+stage2
+staging
+staging1
+staging2
+pre
+preprod
+pre-prod
+pre-production
+uat
+uat1
+uat2
+qa
+qa1
+qa2
+sandbox
+sandbox1
+sandbox2
+demo
+demo1
+demo2
+alpha
+beta
+gamma
+local
+localhost
+preview
+canary
+nightly
+edge
+
+# === Production ===
+prod
+prod1
+prod2
+prod01
+prod02
+production
+production1
+production2
+live
+live1
+live2
+stable
+release
+master
+main
+primary
+secondary
+
+# === API ===
+api
+api1
+api2
+api3
+api-v1
+api-v2
+api-v3
+api-gateway
+api-dev
+api-test
+api-staging
+api-prod
+rest
+rest-api
+restapi
+graphql
+graphql-api
+gql
+grpc
+rpc
+ws
+websocket
+socket
+realtime
+stream
+
+# === Admin ===
+admin
+admin1
+admin2
+administrator
+administrator1
+adm
+superadmin
+sysadmin
+webadmin
+control
+controlpanel
+control-panel
+panel
+cpanel
+whm
+plesk
+horde
+roundcube
+manage
+management
+console
+dashboard
+
+# === Authentication ===
+auth
+auth1
+auth2
+authentication
+login
+signin
+signon
+sign-in
+sign-on
+logout
+signout
+sign-out
+sso
+single-sign-on
+oauth
+oauth2
+openid
+saml
+identity
+id
+idp
+identityprovider
+accounts
+account
+users
+user
+members
+member
+register
+registration
+signup
+sign-up
+password
+forgot-password
+reset-password
+
+# === Database ===
+db
+db1
+db2
+db01
+db02
+database
+database1
+database2
+mysql
+mysql1
+mysql2
+mariadb
+postgres
+postgresql
+pgsql
+mongo
+mongodb
+redis
+redis1
+redis2
+memcache
+memcached
+cache
+cache1
+cache2
+elastic
+elasticsearch
+es
+es1
+es2
+solr
+lucene
+sphinx
+cassandra
+couchdb
+dynamodb
+rds
+aurora
+
+# === Mail ===
+mail
+mail1
+mail2
+mail01
+mail02
+smtp
+smtp1
+smtp2
+pop
+pop3
+imap
+imap4
+webmail
+webmail1
+webmail2
+email
+email1
+email2
+mx
+mx1
+mx2
+mx3
+mx4
+relay
+relay1
+relay2
+mailserver
+exchange
+owa
+outlook
+autodiscover
+autoconfig
+postfix
+sendmail
+exim
+dovecot
+roundcube
+horde
+squirrelmail
+newsletter
+mailing
+lists
+listserv
+
+# === Security ===
+secure
+security
+sec
+ssl
+tls
+https
+vpn
+vpn1
+vpn2
+openvpn
+ipsec
+firewall
+fw
+waf
+ids
+ips
+siem
+soc
+cert
+certs
+certificates
+pki
+ca
+key
+keys
+vault
+secrets
+2fa
+mfa
+otp
+totp
+
+# === Infrastructure ===
+ns
+ns1
+ns2
+ns3
+ns4
+dns
+dns1
+dns2
+dns3
+dns4
+nameserver
+nameserver1
+nameserver2
+resolver
+recursive
+authoritative
+ntp
+ntp1
+ntp2
+time
+ldap
+ldap1
+ldap2
+ad
+ads
+active-directory
+directory
+radius
+tacacs
+dhcp
+dhcp1
+dhcp2
+tftp
+pxe
+boot
+
+# === Monitoring ===
+monitor
+monitoring
+mon
+nagios
+zabbix
+prometheus
+grafana
+kibana
+splunk
+datadog
+newrelic
+dynatrace
+elastic
+elk
+logstash
+graylog
+sentry
+pagerduty
+status
+uptime
+healthcheck
+health
+metrics
+stats
+statistics
+analytics
+apm
+trace
+tracing
+
+# === Storage ===
+storage
+storage1
+storage2
+files
+file
+fileserver
+nas
+san
+nfs
+smb
+samba
+cifs
+ftp
+sftp
+ftps
+tftp
+s3
+minio
+backup
+backup1
+backup2
+backups
+archive
+archives
+data
+data1
+data2
+media
+media1
+media2
+upload
+uploads
+download
+downloads
+share
+shares
+public
+private
+
+# === CI/CD & DevOps ===
+ci
+cd
+cicd
+ci-cd
+jenkins
+jenkins1
+jenkins2
+bamboo
+travis
+circleci
+gitlab
+gitlab-ci
+github
+bitbucket
+git
+git1
+git2
+gitea
+gogs
+svn
+subversion
+hg
diff --git a/src/usfx/engine.py b/src/usfx/engine.py
@@ -0,0 +1,213 @@
+"""
+Subdomain Engine
+
+Main entry point for subdomain enumeration. Provides a simple interface
+for running scans and getting results.
+
+This is a simplified engine for internal networks that:
+- Does NOT use external databases
+- Does NOT use external APIs
+- Keeps all results in memory
+- Returns results directly
+"""
+
+import logging
+import time
+from typing import List, Optional
+
+from .config import ModuleResult, ScanConfig, ScanResult, SubdomainResult
+from .orchestrator import SubdomainOrchestrator
+
+logger = logging.getLogger(__name__)
+
+
+class SubdomainEngine:
+ """Main subdomain enumeration engine"""
+
+ def __init__(self):
+ """Initialize the engine"""
+ self.current_scan: Optional[SubdomainOrchestrator] = None
+
+ def scan(
+ self,
+ config: ScanConfig,
+ progress_callback=None
+ ) -> ScanResult:
+ """
+ Run a full subdomain scan.
+
+ Args:
+ config: Scan configuration
+ progress_callback: Optional callback for progress updates
+
+ Returns:
+ ScanResult with discovered subdomains
+ """
+ start_time = time.time()
+
+ logger.info(f"Starting subdomain scan for {config.domain}")
+ if config.dns_servers:
+ logger.info(f"Using DNS servers: {', '.join(config.dns_servers)}")
+ else:
+ logger.info("Using system default DNS servers")
+
+ # Create resolver
+ from .utils.dns_resolver import InternalDNSResolver
+ resolver = InternalDNSResolver(
+ dns_servers=config.dns_servers if config.dns_servers else None,
+ timeout=config.timeout
+ )
+
+ # Create orchestrator
+ orchestrator = SubdomainOrchestrator(
+ domain=config.domain,
+ resolver=resolver,
+ config=config
+ )
+
+ self.current_scan = orchestrator
+
+ # Set up progress callback
+ if progress_callback:
+ orchestrator.set_progress_callback(progress_callback)
+
+ try:
+ # Run enumeration
+ state = orchestrator.run_all()
+
+ duration = time.time() - start_time
+
+ # Build result
+ subdomains = []
+ for subdomain, info in state.discovered.items():
+ subdomains.append(SubdomainResult(
+ subdomain=subdomain,
+ ip=info.get('ip'),
+ discovered_by=info.get('discovered_by', 'unknown'),
+ is_active=info.get('is_active', False),
+ ))
+
+ # Sort by subdomain name
+ subdomains.sort(key=lambda x: x.subdomain)
+
+ # Build module results
+ module_results = []
+ for phase_result in state.phase_results:
+ for module in phase_result.modules:
+ module_results.append(ModuleResult(
+ module_name=module.module_name,
+ found_count=module.found_count,
+ subdomains=[
+ SubdomainResult(subdomain=s, ip=ip)
+ for s, ip in module.subdomains.items()
+ ],
+ duration_seconds=module.duration_seconds,
+ error=module.error
+ ))
+
+ return ScanResult(
+ domain=config.domain,
+ total_found=state.total_found,
+ subdomains=subdomains,
+ module_results=module_results,
+ duration_seconds=duration,
+ dns_servers_used=resolver.dns_servers,
+ config=config
+ )
+
+ except Exception as e:
+ logger.error(f"Scan failed for {config.domain}: {e}")
+ duration = time.time() - start_time
+
+ return ScanResult(
+ domain=config.domain,
+ total_found=0,
+ subdomains=[],
+ module_results=[],
+ duration_seconds=duration,
+ dns_servers_used=config.dns_servers if config.dns_servers else [],
+ config=config
+ )
+
+ finally:
+ self.current_scan = None
+
+ def cancel_scan(self) -> bool:
+ """
+ Cancel the current scan.
+
+ Returns:
+ True if cancelled, False if no scan was running
+ """
+ if self.current_scan:
+ self.current_scan.cancel()
+ return True
+ return False
+
+ def quick_scan(
+ self,
+ domain: str,
+ dns_servers: Optional[List[str]] = None,
+ threads: int = 30
+ ) -> ScanResult:
+ """
+ Run a quick scan with default settings.
+
+ Args:
+ domain: Target domain
+ dns_servers: Optional list of DNS server IPs
+ threads: Number of threads
+
+ Returns:
+ ScanResult with discovered subdomains
+ """
+ from .config import WordlistSize
+
+ config = ScanConfig(
+ domain=domain,
+ dns_servers=dns_servers or [],
+ wordlist_size=WordlistSize.SMALL,
+ threads=threads
+ )
+
+ return self.scan(config)
+
+ def full_scan(
+ self,
+ domain: str,
+ dns_servers: Optional[List[str]] = None,
+ threads: int = 50
+ ) -> ScanResult:
+ """
+ Run a full scan with comprehensive settings.
+
+ Args:
+ domain: Target domain
+ dns_servers: Optional list of DNS server IPs
+ threads: Number of threads
+
+ Returns:
+ ScanResult with discovered subdomains
+ """
+ from .config import WordlistSize
+
+ config = ScanConfig(
+ domain=domain,
+ dns_servers=dns_servers or [],
+ wordlist_size=WordlistSize.LARGE,
+ threads=threads
+ )
+
+ return self.scan(config)
+
+
+# Global engine instance
+_engine: Optional[SubdomainEngine] = None
+
+
+def get_engine() -> SubdomainEngine:
+ """Get or create the global subdomain engine"""
+ global _engine
+ if _engine is None:
+ _engine = SubdomainEngine()
+ return _engine
diff --git a/src/usfx/modules/__init__.py b/src/usfx/modules/__init__.py
@@ -0,0 +1,41 @@
+"""
+USFX Modules
+
+Offline-capable modules for subdomain enumeration:
+- dns_bruteforce: Wordlist-based DNS queries
+- zone_transfer: AXFR zone transfer attempts
+- dnssec_walker: NSEC/NSEC3 zone walking
+- dns_records: MX/NS/TXT/SRV/SOA/CAA analysis
+- reverse_dns: PTR lookups on IP ranges
+- cname_chaser: CNAME chain tracking
+- permutation: Subdomain variation generation
+- recursive_enum: Sub-subdomain discovery
+- vhost_scanner: Host header brute force
+- tls_analyzer: TLS certificate SAN extraction
+"""
+
+from .base import BaseModule
+from .dns_bruteforce import DNSBruteforcer
+from .zone_transfer import ZoneTransfer
+from .dnssec_walker import DNSSECWalker
+from .dns_records import DNSRecordMiner
+from .reverse_dns import ReverseDNS
+from .cname_chaser import CNAMEChaser
+from .permutation import PermutationEngine
+from .recursive_enum import RecursiveEnumerator
+from .vhost_scanner import VHostScanner
+from .tls_analyzer import TLSAnalyzer
+
+__all__ = [
+ "BaseModule",
+ "DNSBruteforcer",
+ "ZoneTransfer",
+ "DNSSECWalker",
+ "DNSRecordMiner",
+ "ReverseDNS",
+ "CNAMEChaser",
+ "PermutationEngine",
+ "RecursiveEnumerator",
+ "VHostScanner",
+ "TLSAnalyzer",
+]
diff --git a/src/usfx/modules/base.py b/src/usfx/modules/base.py
@@ -0,0 +1,155 @@
+"""
+Base Module Class
+
+Abstract base class for all subdomain enumeration modules.
+Provides common functionality and interface contract.
+"""
+
+import logging
+from abc import ABC, abstractmethod
+from typing import Dict, Optional, TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from ..utils.dns_resolver import InternalDNSResolver
+
+logger = logging.getLogger(__name__)
+
+
+class BaseModule(ABC):
+ """
+ Abstract base class for subdomain enumeration modules.
+
+ All modules must implement the enumerate() method and provide
+ a MODULE_NAME class attribute.
+
+ Attributes:
+ MODULE_NAME: Unique identifier for the module
+ domain: Target domain
+ resolver: DNS resolver for lookups
+ discovered: Dict of discovered {subdomain: ip}
+ """
+
+ MODULE_NAME: str = "base"
+
+ def __init__(
+ self,
+ domain: str,
+ resolver: Optional['InternalDNSResolver'] = None,
+ timeout: float = 3.0
+ ):
+ """
+ Initialize the module.
+
+ Args:
+ domain: Target domain (e.g., 'corp.local')
+ resolver: Optional custom DNS resolver
+ timeout: Default timeout for operations
+ """
+ self.domain = domain.lower().strip('.')
+ self.timeout = timeout
+ self.discovered: Dict[str, Optional[str]] = {}
+
+ # Use provided resolver or create a new one
+ if resolver:
+ self.resolver = resolver
+ else:
+ from ..utils.dns_resolver import InternalDNSResolver
+ self.resolver = InternalDNSResolver(timeout=timeout)
+
+ @abstractmethod
+ def enumerate(self, **kwargs) -> Dict[str, Optional[str]]:
+ """
+ Perform subdomain enumeration.
+
+ This method must be implemented by all modules.
+
+ Returns:
+ Dict of {subdomain: ip_address}
+ IP address may be None if not resolved.
+ """
+ pass
+
+ def _is_valid_subdomain(self, subdomain: str) -> bool:
+ """
+ Check if a discovered subdomain is valid for the target domain.
+
+ Args:
+ subdomain: Subdomain to validate
+
+ Returns:
+ True if valid subdomain of target domain
+ """
+ subdomain = subdomain.lower().rstrip('.')
+
+ # Must end with .domain
+ if not subdomain.endswith(f".{self.domain}"):
+ return False
+
+ # Must not be exactly the domain
+ if subdomain == self.domain:
+ return False
+
+ # Check for empty labels
+ labels = subdomain.split('.')
+ for label in labels:
+ if not label:
+ return False
+
+ return True
+
+ def _add_discovered(self, subdomain: str, ip: Optional[str] = None) -> bool:
+ """
+ Add a discovered subdomain to results.
+
+ Args:
+ subdomain: Discovered subdomain
+ ip: Optional IP address
+
+ Returns:
+ True if added (new), False if already existed
+ """
+ subdomain = subdomain.lower().rstrip('.')
+
+ if not self._is_valid_subdomain(subdomain):
+ return False
+
+ if subdomain not in self.discovered:
+ self.discovered[subdomain] = ip
+ logger.debug(f"[{self.MODULE_NAME}] Found: {subdomain} -> {ip}")
+ return True
+
+ # Update IP if we have one now
+ if ip and self.discovered[subdomain] is None:
+ self.discovered[subdomain] = ip
+
+ return False
+
+ def _resolve_subdomain(self, subdomain: str) -> Optional[str]:
+ """
+ Resolve a subdomain using the configured resolver.
+
+ Args:
+ subdomain: Subdomain to resolve
+
+ Returns:
+ IP address or None
+ """
+ return self.resolver.resolve_a(subdomain)
+
+ def get_results(self) -> Dict[str, Optional[str]]:
+ """
+ Get the discovered subdomains.
+
+ Returns:
+ Dict of {subdomain: ip_address}
+ """
+ return self.discovered.copy()
+
+ def get_count(self) -> int:
+ """
+ Get the number of discovered subdomains.
+
+ Returns:
+ Count of discovered subdomains
+ """
+ return len(self.discovered)
diff --git a/src/usfx/modules/cname_chaser.py b/src/usfx/modules/cname_chaser.py
@@ -0,0 +1,206 @@
+"""
+CNAME Chain Analysis Module
+
+Follows CNAME records to discover related domains and potential
+subdomain takeover vulnerabilities.
+"""
+
+import logging
+from concurrent.futures import ThreadPoolExecutor, as_completed
+from typing import Dict, List, Optional, Set, Tuple, TYPE_CHECKING
+
+from .base import BaseModule
+
+if TYPE_CHECKING:
+ from ..utils.dns_resolver import InternalDNSResolver
+
+logger = logging.getLogger(__name__)
+
+
+class CNAMEChaser(BaseModule):
+ """Analyzes CNAME chains to discover related subdomains"""
+
+ MODULE_NAME = "cname_chaser"
+
+ # Known CDN/service CNAME patterns that might reveal subdomains
+ INTERESTING_PATTERNS = [
+ '.cloudfront.net',
+ '.amazonaws.com',
+ '.azurewebsites.net',
+ '.azure-api.net',
+ '.cloudflare.com',
+ '.fastly.net',
+ '.akamaiedge.net',
+ '.akamaitechnologies.com',
+ '.edgekey.net',
+ '.edgesuite.net',
+ '.herokuapp.com',
+ '.github.io',
+ '.gitlab.io',
+ '.netlify.app',
+ '.vercel.app',
+ '.pages.dev',
+ ]
+
+ # Patterns that indicate potential subdomain takeover
+ TAKEOVER_FINGERPRINTS = {
+ '.s3.amazonaws.com': 'NoSuchBucket',
+ '.s3-website': 'NoSuchBucket',
+ '.cloudfront.net': 'Bad Request',
+ '.herokuapp.com': 'No such app',
+ '.github.io': "There isn't a GitHub Pages site here",
+ '.gitlab.io': "The page you're looking for could not be found",
+ '.netlify.app': 'Not Found',
+ '.zendesk.com': 'Help Center Closed',
+ '.shopify.com': 'Sorry, this shop is currently unavailable',
+ }
+
+ def __init__(
+ self,
+ domain: str,
+ resolver: Optional['InternalDNSResolver'] = None,
+ max_chain_depth: int = 10,
+ threads: int = 20
+ ):
+ """
+ Initialize CNAME chaser.
+
+ Args:
+ domain: Target domain
+ resolver: Custom DNS resolver
+ max_chain_depth: Maximum CNAME chain depth to follow
+ threads: Number of parallel threads
+ """
+ super().__init__(domain, resolver)
+ self.max_chain_depth = max_chain_depth
+ self.threads = threads
+
+ def _follow_cname_chain(self, subdomain: str) -> List[str]:
+ """Follow CNAME chain and return all CNAMEs"""
+ chain = []
+ current = subdomain
+ seen = set()
+
+ for _ in range(self.max_chain_depth):
+ if current in seen:
+ break # Circular reference
+ seen.add(current)
+
+ cname = self.resolver.resolve_cname(current)
+ if cname:
+ chain.append(cname)
+ current = cname
+ else:
+ break
+
+ return chain
+
+ def _extract_related_subdomains(self, cname: str) -> Set[str]:
+ """Extract potential related subdomains from CNAME"""
+ related = set()
+
+ # If CNAME contains our domain, it's a subdomain
+ if self.domain in cname:
+ # Extract the full subdomain
+ if cname.endswith(self.domain):
+ related.add(cname)
+ else:
+ # Maybe it's like "subdomain.example.com.cdn.cloudflare.com"
+ parts = cname.split('.')
+ for i in range(len(parts)):
+ candidate = '.'.join(parts[i:])
+ if candidate.endswith(self.domain) and candidate != self.domain:
+ related.add(candidate)
+ break
+
+ return related
+
+ def _check_takeover_potential(self, subdomain: str, cname_chain: List[str]) -> Optional[str]:
+ """Check if subdomain might be vulnerable to takeover"""
+ if not cname_chain:
+ return None
+
+ final_cname = cname_chain[-1].lower()
+
+ for pattern, fingerprint in self.TAKEOVER_FINGERPRINTS.items():
+ if pattern in final_cname:
+ # Flag the pattern as potential vulnerability
+ return f"Potential takeover: {final_cname} (pattern: {pattern})"
+
+ return None
+
+ def _analyze_subdomain(self, subdomain: str) -> Tuple[str, List[str], Set[str], Optional[str]]:
+ """Analyze a single subdomain's CNAME chain"""
+ chain = self._follow_cname_chain(subdomain)
+ related = set()
+ takeover_warning = None
+
+ for cname in chain:
+ related.update(self._extract_related_subdomains(cname))
+
+ if chain:
+ takeover_warning = self._check_takeover_potential(subdomain, chain)
+
+ return subdomain, chain, related, takeover_warning
+
+ def enumerate(self, known_subdomains: Dict[str, Optional[str]] = None) -> Dict[str, Optional[str]]:
+ """
+ Analyze CNAME chains for known subdomains.
+
+ Args:
+ known_subdomains: Dict of known subdomains to analyze
+
+ Returns:
+ Dict of newly discovered subdomains
+ """
+ if not known_subdomains:
+ known_subdomains = {}
+
+ logger.info(f"Analyzing CNAME chains for {len(known_subdomains)} subdomains")
+
+ takeover_warnings = []
+
+ # Analyze all known subdomains
+ with ThreadPoolExecutor(max_workers=self.threads) as executor:
+ futures = {
+ executor.submit(self._analyze_subdomain, sub): sub
+ for sub in known_subdomains.keys()
+ }
+
+ for future in as_completed(futures, timeout=120):
+ try:
+ subdomain, chain, related, warning = future.result()
+
+ # Add newly discovered subdomains
+ for sub in related:
+ if sub not in known_subdomains:
+ self._add_discovered(sub)
+ logger.debug(f"CNAME chain discovered: {sub}")
+
+ if warning:
+ takeover_warnings.append((subdomain, warning))
+
+ except Exception as e:
+ logger.debug(f"CNAME analysis error: {e}")
+
+ # Log takeover warnings
+ if takeover_warnings:
+ logger.warning(f"Found {len(takeover_warnings)} potential subdomain takeover vulnerabilities:")
+ for sub, warning in takeover_warnings:
+ logger.warning(f" {sub}: {warning}")
+
+ logger.info(f"CNAME analysis discovered {len(self.discovered)} new subdomains")
+
+ return self.discovered
+
+ def get_cname_chain(self, subdomain: str) -> List[str]:
+ """
+ Get the CNAME chain for a specific subdomain.
+
+ Args:
+ subdomain: Subdomain to analyze
+
+ Returns:
+ List of CNAMEs in the chain
+ """
+ return self._follow_cname_chain(subdomain)
diff --git a/src/usfx/modules/dns_bruteforce.py b/src/usfx/modules/dns_bruteforce.py
@@ -0,0 +1,169 @@
+"""
+DNS Brute Force Module
+
+Performs parallel DNS resolution against a wordlist to discover subdomains.
+Uses the internal DNS resolver for custom DNS server support.
+"""
+
+import logging
+from concurrent.futures import ThreadPoolExecutor, as_completed
+from typing import Callable, Dict, Optional, Set, TYPE_CHECKING
+
+from .base import BaseModule
+from ..utils.wordlist import WordlistManager
+from ..utils.wildcard import WildcardDetector
+
+if TYPE_CHECKING:
+ from ..utils.dns_resolver import InternalDNSResolver
+
+logger = logging.getLogger(__name__)
+
+
+class DNSBruteforcer(BaseModule):
+ """Subdomain discovery via DNS brute force"""
+
+ MODULE_NAME = "dns_bruteforce"
+
+ def __init__(
+ self,
+ domain: str,
+ resolver: Optional['InternalDNSResolver'] = None,
+ wordlist_path: Optional[str] = None,
+ wordlist_size: str = 'medium',
+ threads: int = 30,
+ timeout: float = 2.0
+ ):
+ """
+ Initialize DNS brute forcer.
+
+ Args:
+ domain: Target domain
+ resolver: Custom DNS resolver (uses internal if None)
+ wordlist_path: Custom wordlist file path
+ wordlist_size: Size of bundled wordlist ('small', 'medium', 'large')
+ threads: Number of parallel threads
+ timeout: DNS query timeout
+ """
+ super().__init__(domain, resolver, timeout)
+
+ self.threads = threads
+ self.wordlist_path = wordlist_path
+ self.wordlist_size = wordlist_size
+ self.wordlist_manager = WordlistManager(wordlist_path)
+ self.wildcard_detector = WildcardDetector(domain, resolver)
+
+ def _resolve_subdomain(self, subdomain: str) -> tuple[str, Optional[str], Optional[Set[str]]]:
+ """
+ Resolve a single subdomain.
+
+ Args:
+ subdomain: Subdomain prefix to test
+
+ Returns:
+ Tuple of (full_subdomain, ip_address, all_ips)
+ """
+ fqdn = f"{subdomain}.{self.domain}"
+
+ ips = self.resolver.resolve_a_all(fqdn)
+ primary_ip = list(ips)[0] if ips else None
+
+ return fqdn, primary_ip, ips
+
+ def enumerate(
+ self,
+ max_words: Optional[int] = None,
+ progress_callback: Optional[Callable[[int, int, int], None]] = None
+ ) -> Dict[str, Optional[str]]:
+ """
+ Perform DNS brute force enumeration.
+
+ Args:
+ max_words: Maximum number of words to test
+ progress_callback: Callback for progress updates (current, total, found)
+
+ Returns:
+ Dict of {subdomain: ip_address}
+ """
+ logger.info(f"Starting DNS brute force for {self.domain}")
+
+ # Detect wildcards first
+ has_wildcard, wildcard_ips = self.wildcard_detector.detect()
+ if has_wildcard:
+ logger.warning(f"Wildcard DNS detected for {self.domain}: {wildcard_ips}")
+
+ # Load wordlist
+ wordlist = self.wordlist_manager.get_wordlist(
+ size=self.wordlist_size,
+ max_words=max_words,
+ include_numbers=True,
+ include_env=True
+ )
+
+ total = len(wordlist)
+ logger.info(f"Testing {total} subdomain candidates")
+
+ completed = 0
+ found = 0
+
+ with ThreadPoolExecutor(max_workers=self.threads) as executor:
+ futures = {
+ executor.submit(self._resolve_subdomain, word): word
+ for word in wordlist
+ }
+
+ for future in as_completed(futures):
+ try:
+ fqdn, ip, all_ips = future.result()
+ completed += 1
+
+ if ip:
+ # Check if it's a wildcard result
+ if has_wildcard and all_ips and self.wildcard_detector.is_wildcard_result(all_ips):
+ logger.debug(f"Filtered wildcard result: {fqdn}")
+ else:
+ self._add_discovered(fqdn, ip)
+ found += 1
+
+ # Progress update every 100 results
+ if progress_callback and completed % 100 == 0:
+ progress_callback(completed, total, found)
+
+ except Exception as e:
+ completed += 1
+ logger.debug(f"Error in brute force: {e}")
+
+ # Final progress update
+ if progress_callback:
+ progress_callback(completed, total, found)
+
+ logger.info(f"DNS brute force complete: {found} subdomains found")
+ return self.discovered
+
+ def test_specific(self, subdomains: Set[str]) -> Dict[str, Optional[str]]:
+ """
+ Test specific subdomain candidates.
+
+ Args:
+ subdomains: Set of subdomain prefixes to test
+
+ Returns:
+ Dict of {subdomain: ip_address}
+ """
+ results = {}
+
+ with ThreadPoolExecutor(max_workers=self.threads) as executor:
+ futures = {
+ executor.submit(self._resolve_subdomain, sub): sub
+ for sub in subdomains
+ }
+
+ for future in as_completed(futures):
+ try:
+ fqdn, ip, _ = future.result()
+ if ip:
+ results[fqdn] = ip
+ self._add_discovered(fqdn, ip)
+ except Exception as e:
+ logger.debug(f"Error testing subdomain: {e}")
+
+ return results
diff --git a/src/usfx/modules/dns_records.py b/src/usfx/modules/dns_records.py
@@ -0,0 +1,236 @@
+"""
+DNS Record Mining Module
+
+Analyzes various DNS record types to discover subdomains:
+- MX records (mail servers)
+- NS records (nameservers)
+- TXT records (SPF, DMARC, DKIM)
+- SRV records (services)
+- SOA records
+- CAA records
+"""
+
+import logging
+import re
+from typing import Dict, Optional, Set, TYPE_CHECKING
+
+from .base import BaseModule
+
+if TYPE_CHECKING:
+ from ..utils.dns_resolver import InternalDNSResolver
+
+logger = logging.getLogger(__name__)
+
+
+class DNSRecordMiner(BaseModule):
+ """DNS record analysis for subdomain discovery"""
+
+ MODULE_NAME = "dns_records"
+
+ def __init__(
+ self,
+ domain: str,
+ resolver: Optional['InternalDNSResolver'] = None,
+ timeout: float = 5.0
+ ):
+ """
+ Initialize DNS record miner.
+
+ Args:
+ domain: Target domain
+ resolver: Custom DNS resolver
+ timeout: DNS query timeout
+ """
+ super().__init__(domain, resolver, timeout)
+
+ def _extract_domain_from_text(self, text: str) -> Set[str]:
+ """
+ Extract domain names from text (like SPF records).
+
+ Args:
+ text: Text to parse
+
+ Returns:
+ Set of discovered domains
+ """
+ domains = set()
+
+ # Match domain patterns
+ domain_pattern = rf'([a-zA-Z0-9][-a-zA-Z0-9]*\.)*{re.escape(self.domain)}'
+ matches = re.findall(domain_pattern, text, re.IGNORECASE)
+
+ # Also look for include: and redirect= in SPF
+ spf_includes = re.findall(r'include:([^\s]+)', text, re.IGNORECASE)
+ spf_redirects = re.findall(r'redirect=([^\s]+)', text, re.IGNORECASE)
+
+ for match in spf_includes + spf_redirects:
+ if self.domain in match.lower():
+ domains.add(match.lower().rstrip('.'))
+
+ # Look for explicit subdomains
+ subdomain_pattern = rf'([a-zA-Z0-9][-a-zA-Z0-9]*\.)+{re.escape(self.domain)}'
+ subdomain_matches = re.findall(subdomain_pattern, text, re.IGNORECASE)
+
+ for match in subdomain_matches:
+ full_domain = f"{match}{self.domain}".lower().rstrip('.')
+ if full_domain != self.domain:
+ domains.add(full_domain)
+
+ return domains
+
+ def _query_mx(self) -> Set[str]:
+ """Query MX records"""
+ discovered = set()
+
+ mx_records = self.resolver.resolve_mx(self.domain)
+ for priority, mx_host in mx_records:
+ mx_host = mx_host.lower()
+
+ # Check if MX is a subdomain
+ if mx_host.endswith(f".{self.domain}"):
+ discovered.add(mx_host)
+ self._add_discovered(mx_host)
+ logger.debug(f"MX record: {mx_host}")
+
+ return discovered
+
+ def _query_ns(self) -> Set[str]:
+ """Query NS records"""
+ discovered = set()
+
+ ns_records = self.resolver.resolve_ns(self.domain)
+ for ns_host in ns_records:
+ ns_host = ns_host.lower()
+
+ if ns_host.endswith(f".{self.domain}"):
+ discovered.add(ns_host)
+ self._add_discovered(ns_host)
+ logger.debug(f"NS record: {ns_host}")
+
+ return discovered
+
+ def _query_txt(self) -> Set[str]:
+ """Query TXT records for SPF, DMARC, etc."""
+ discovered = set()
+
+ # Query main domain TXT
+ txt_records = self.resolver.resolve_txt(self.domain)
+ for txt_data in txt_records:
+ found = self._extract_domain_from_text(txt_data)
+ discovered.update(found)
+ for d in found:
+ self._add_discovered(d)
+
+ # Query _dmarc
+ dmarc_domain = f"_dmarc.{self.domain}"
+ dmarc_records = self.resolver.resolve_txt(dmarc_domain)
+ for txt_data in dmarc_records:
+ # Extract rua and ruf email domains
+ rua_matches = re.findall(r'rua=mailto:[^@]+@([^\s;]+)', txt_data)
+ ruf_matches = re.findall(r'ruf=mailto:[^@]+@([^\s;]+)', txt_data)
+
+ for domain in rua_matches + ruf_matches:
+ domain = domain.lower().rstrip('.')
+ if domain.endswith(f".{self.domain}") or domain == self.domain:
+ if domain != self.domain:
+ discovered.add(domain)
+ self._add_discovered(domain)
+
+ return discovered
+
+ def _query_srv(self) -> Set[str]:
+ """Query common SRV records"""
+ discovered = set()
+
+ # Common SRV record prefixes
+ srv_prefixes = [
+ '_autodiscover._tcp', # Exchange
+ '_sip._tcp', '_sip._udp', # SIP
+ '_xmpp-client._tcp', '_xmpp-server._tcp', # XMPP
+ '_ldap._tcp', # LDAP
+ '_kerberos._tcp', '_kerberos._udp', # Kerberos
+ '_gc._tcp', # Global Catalog
+ '_kpasswd._tcp', '_kpasswd._udp', # Kerberos password
+ '_http._tcp', '_https._tcp', # HTTP
+ '_imaps._tcp', '_imap._tcp', # IMAP
+ '_pop3s._tcp', '_pop3._tcp', # POP3
+ '_smtps._tcp', '_smtp._tcp', # SMTP
+ '_submission._tcp', # Mail submission
+ ]
+
+ for prefix in srv_prefixes:
+ srv_name = f"{prefix}.{self.domain}"
+ srv_records = self.resolver.resolve_srv(srv_name)
+
+ for priority, weight, port, target in srv_records:
+ target = target.lower()
+ if target.endswith(f".{self.domain}"):
+ discovered.add(target)
+ self._add_discovered(target)
+ logger.debug(f"SRV record {prefix}: {target}")
+
+ return discovered
+
+ def _query_soa(self) -> Set[str]:
+ """Query SOA record for hostnames"""
+ discovered = set()
+
+ soa = self.resolver.resolve_soa(self.domain)
+ if soa:
+ mname = soa['mname'].lower()
+ rname = soa['rname'].lower()
+
+ if mname.endswith(f".{self.domain}"):
+ discovered.add(mname)
+ self._add_discovered(mname)
+
+ # rname is often in email format (hostmaster.domain.com)
+ # Convert to actual domain
+ if '.' in rname:
+ potential_domain = rname
+ if potential_domain.endswith(f".{self.domain}"):
+ discovered.add(potential_domain)
+ self._add_discovered(potential_domain)
+
+ return discovered
+
+ def _query_caa(self) -> Set[str]:
+ """Query CAA records for domain references"""
+ discovered = set()
+
+ caa_records = self.resolver.resolve_any(self.domain, 'CAA')
+ for record in caa_records:
+ value = str(record).lower()
+ if value.endswith(f".{self.domain}"):
+ discovered.add(value)
+ self._add_discovered(value)
+
+ return discovered
+
+ def enumerate(self) -> Dict[str, Optional[str]]:
+ """
+ Mine all DNS record types for subdomains.
+
+ Returns:
+ Dict of {subdomain: ip_address}
+ """
+ logger.info(f"Mining DNS records for {self.domain}")
+
+ # Query all record types
+ all_discovered = set()
+ all_discovered.update(self._query_mx())
+ all_discovered.update(self._query_ns())
+ all_discovered.update(self._query_txt())
+ all_discovered.update(self._query_srv())
+ all_discovered.update(self._query_soa())
+ all_discovered.update(self._query_caa())
+
+ # Resolve discovered subdomains
+ for subdomain in all_discovered:
+ if subdomain in self.discovered and self.discovered[subdomain] is None:
+ ip = self.resolver.resolve_a(subdomain)
+ if ip:
+ self.discovered[subdomain] = ip
+
+ logger.info(f"DNS record mining found {len(self.discovered)} subdomains")
+ return self.discovered
diff --git a/src/usfx/modules/dnssec_walker.py b/src/usfx/modules/dnssec_walker.py
@@ -0,0 +1,187 @@
+"""
+DNSSEC Zone Walking Module
+
+Exploits DNSSEC NSEC/NSEC3 records to enumerate zone contents.
+NSEC records form a chain linking all records in a zone.
+"""
+
+import logging
+from typing import Dict, Optional, Set, Tuple, TYPE_CHECKING
+
+import dns.rdatatype
+
+from .base import BaseModule
+
+if TYPE_CHECKING:
+ from ..utils.dns_resolver import InternalDNSResolver
+
+logger = logging.getLogger(__name__)
+
+
+class DNSSECWalker(BaseModule):
+ """DNSSEC zone walking via NSEC/NSEC3 records"""
+
+ MODULE_NAME = "dnssec_walker"
+
+ def __init__(
+ self,
+ domain: str,
+ resolver: Optional['InternalDNSResolver'] = None,
+ timeout: float = 5.0,
+ max_iterations: int = 500
+ ):
+ """
+ Initialize DNSSEC walker.
+
+ Args:
+ domain: Target domain
+ resolver: Custom DNS resolver
+ timeout: DNS query timeout
+ max_iterations: Maximum NSEC chain iterations
+ """
+ super().__init__(domain, resolver, timeout)
+ self.max_iterations = max_iterations
+
+ def _check_dnssec(self) -> Tuple[bool, bool, dict]:
+ """
+ Check if domain uses DNSSEC and what type.
+
+ Returns:
+ Tuple of (has_dnssec, uses_nsec3, nsec3_params)
+ """
+ try:
+ # Try to get NSEC3PARAM first
+ records = self.resolver.resolve_any(self.domain, 'NSEC3PARAM')
+ if records:
+ # NSEC3 is in use
+ return True, True, {}
+
+ # Try to get DNSKEY to confirm DNSSEC
+ records = self.resolver.resolve_any(self.domain, 'DNSKEY')
+ if records:
+ return True, False, {}
+
+ return False, False, {}
+
+ except Exception as e:
+ logger.debug(f"Error checking DNSSEC: {e}")
+ return False, False, {}
+
+ def _walk_nsec(self) -> Set[str]:
+ """
+ Walk NSEC records to enumerate zone.
+
+ Returns:
+ Set of discovered subdomains
+ """
+ discovered = set()
+ current = self.domain
+ iterations = 0
+
+ logger.info(f"Starting NSEC zone walk for {self.domain}")
+
+ native_resolver = self.resolver.get_native_resolver()
+
+ while iterations < self.max_iterations:
+ iterations += 1
+
+ try:
+ # Query for NSEC record at current name
+ try:
+ answers = native_resolver.resolve(current, 'NSEC')
+ for rdata in answers:
+ next_name = str(rdata.next).rstrip('.')
+
+ if next_name.endswith(f".{self.domain}"):
+ discovered.add(next_name)
+ self._add_discovered(next_name)
+
+ # Check if we've looped back
+ if next_name == self.domain or next_name <= current:
+ logger.info(f"NSEC walk complete: {len(discovered)} found")
+ return discovered
+
+ current = next_name
+
+ except Exception:
+ # Try querying a fake subdomain to get NSEC in authority
+ fake = f"aaa{iterations}.{current if current != self.domain else self.domain}"
+ try:
+ native_resolver.resolve(fake, 'A')
+ except Exception:
+ break
+
+ except Exception as e:
+ logger.debug(f"NSEC walk error at {current}: {e}")
+ break
+
+ logger.info(f"NSEC walk finished with {len(discovered)} subdomains")
+ return discovered
+
+ def _walk_nsec3(self, params: dict) -> Set[str]:
+ """
+ Attempt to crack NSEC3 hashes (limited effectiveness).
+
+ For NSEC3, we can only try to match known names against hashes,
+ so this is less effective than NSEC walking.
+
+ Args:
+ params: NSEC3 parameters
+
+ Returns:
+ Set of discovered subdomains
+ """
+ discovered = set()
+
+ # NSEC3 uses hashed names, making direct walking impossible
+ # We can only verify if specific names exist by computing their hashes
+
+ # Common subdomain prefixes to try
+ common_prefixes = [
+ 'www', 'mail', 'ftp', 'smtp', 'pop', 'imap', 'webmail',
+ 'api', 'dev', 'test', 'staging', 'admin', 'blog', 'shop',
+ 'cdn', 'static', 'ns1', 'ns2', 'dns', 'vpn', 'remote'
+ ]
+
+ logger.info(f"Attempting NSEC3 hash matching for {self.domain}")
+
+ for prefix in common_prefixes:
+ subdomain = f"{prefix}.{self.domain}"
+ ip = self.resolver.resolve_a(subdomain)
+ if ip:
+ discovered.add(subdomain)
+ self._add_discovered(subdomain, ip)
+
+ logger.info(f"NSEC3 matching found {len(discovered)} subdomains")
+ return discovered
+
+ def enumerate(self) -> Dict[str, Optional[str]]:
+ """
+ Enumerate subdomains using DNSSEC zone walking.
+
+ Returns:
+ Dict of {subdomain: ip_address}
+ """
+ logger.info(f"Checking DNSSEC for {self.domain}")
+
+ has_dnssec, uses_nsec3, nsec3_params = self._check_dnssec()
+
+ if not has_dnssec:
+ logger.info(f"Domain {self.domain} does not use DNSSEC")
+ return {}
+
+ if uses_nsec3:
+ logger.info(f"Domain uses NSEC3 (hashed) - limited enumeration possible")
+ self._walk_nsec3(nsec3_params)
+ else:
+ logger.info(f"Domain uses NSEC - zone walking possible")
+ self._walk_nsec()
+
+ # Resolve discovered subdomains to get IPs
+ for subdomain in list(self.discovered.keys()):
+ if self.discovered[subdomain] is None:
+ ip = self.resolver.resolve_a(subdomain)
+ if ip:
+ self.discovered[subdomain] = ip
+
+ return self.discovered
diff --git a/src/usfx/modules/permutation.py b/src/usfx/modules/permutation.py
@@ -0,0 +1,286 @@
+"""
+Permutation Engine Module
+
+Generates subdomain variations based on discovered subdomains:
+- Version variations (api-v1 -> api-v2, api-v3)
+- Environment variations (api -> api-dev, api-staging)
+- Numeric variations (web1 -> web2, web3)
+- Prefix/suffix combinations
+"""
+
+import logging
+import re
+from typing import Callable, Dict, List, Optional, Set, TYPE_CHECKING
+
+from .base import BaseModule
+
+if TYPE_CHECKING:
+ from ..utils.dns_resolver import InternalDNSResolver
+
+logger = logging.getLogger(__name__)
+
+
+class PermutationEngine(BaseModule):
+ """Subdomain permutation generator"""
+
+ MODULE_NAME = "permutation"
+
+ # Common prefixes
+ PREFIXES = [
+ 'dev', 'test', 'stage', 'staging', 'prod', 'production',
+ 'qa', 'uat', 'sandbox', 'demo', 'beta', 'alpha',
+ 'internal', 'external', 'private', 'public',
+ 'old', 'new', 'legacy', 'backup', 'temp', 'tmp',
+ 'api', 'app', 'web', 'mobile', 'm', 'admin',
+ 'secure', 'ssl', 'vpn', 'remote', 'local'
+ ]
+
+ # Common suffixes
+ SUFFIXES = [
+ 'dev', 'test', 'stage', 'staging', 'prod', 'production',
+ 'qa', 'uat', 'sandbox', 'demo', 'beta', 'alpha',
+ 'internal', 'external', 'api', 'app', 'web',
+ 'old', 'new', 'backup', 'bak', 'temp', 'tmp',
+ '01', '02', '03', '1', '2', '3'
+ ]
+
+ # Separator characters
+ SEPARATORS = ['-', '.', '_', '']
+
+ def __init__(
+ self,
+ domain: str,
+ resolver: Optional['InternalDNSResolver'] = None
+ ):
+ """
+ Initialize permutation engine.
+
+ Args:
+ domain: Target domain
+ resolver: Custom DNS resolver
+ """
+ super().__init__(domain, resolver)
+
+ def _extract_patterns(self, subdomains: Set[str]) -> dict:
+ """
+ Analyze discovered subdomains to extract patterns.
+
+ Args:
+ subdomains: Set of discovered subdomains
+
+ Returns:
+ Dict of pattern types and examples
+ """
+ patterns = {
+ 'versioned': [], # api-v1, api-v2
+ 'numbered': [], # web1, web2
+ 'environment': [], # api-dev, api-prod
+ 'base_names': set() # base names without modifiers
+ }
+
+ for subdomain in subdomains:
+ # Extract the subdomain part (without parent domain)
+ if subdomain.endswith(f".{self.domain}"):
+ name = subdomain[:-len(self.domain) - 1]
+ else:
+ continue
+
+ # Detect versioned patterns (v1, v2, etc.)
+ version_match = re.match(r'^(.+?)[-._]?v(\d+)$', name, re.IGNORECASE)
+ if version_match:
+ patterns['versioned'].append({
+ 'base': version_match.group(1),
+ 'version': int(version_match.group(2)),
+ 'original': name
+ })
+ patterns['base_names'].add(version_match.group(1))
+ continue
+
+ # Detect numbered patterns (web1, web2, etc.)
+ numbered_match = re.match(r'^(.+?)(\d+)$', name)
+ if numbered_match:
+ patterns['numbered'].append({
+ 'base': numbered_match.group(1),
+ 'number': int(numbered_match.group(2)),
+ 'original': name
+ })
+ patterns['base_names'].add(numbered_match.group(1))
+ continue
+
+ # Detect environment patterns
+ for env in ['dev', 'test', 'stage', 'staging', 'prod', 'qa', 'uat']:
+ if re.search(rf'[-._]{env}$', name, re.IGNORECASE):
+ base = re.sub(rf'[-._]{env}$', '', name, flags=re.IGNORECASE)
+ patterns['environment'].append({
+ 'base': base,
+ 'env': env,
+ 'original': name
+ })
+ patterns['base_names'].add(base)
+ break
+ elif re.search(rf'^{env}[-._]', name, re.IGNORECASE):
+ base = re.sub(rf'^{env}[-._]', '', name, flags=re.IGNORECASE)
+ patterns['environment'].append({
+ 'base': base,
+ 'env': env,
+ 'original': name,
+ 'prefix': True
+ })
+ patterns['base_names'].add(base)
+ break
+ else:
+ patterns['base_names'].add(name)
+
+ return patterns
+
+ def _generate_version_permutations(self, patterns: List[dict]) -> Set[str]:
+ """Generate version-based permutations"""
+ permutations = set()
+
+ for pattern in patterns:
+ base = pattern['base']
+ current_version = pattern['version']
+
+ # Generate nearby versions
+ for v in range(1, min(current_version + 5, 20)):
+ if v != current_version:
+ for sep in ['-', '_', '.', '']:
+ permutations.add(f"{base}{sep}v{v}")
+
+ return permutations
+
+ def _generate_numbered_permutations(self, patterns: List[dict]) -> Set[str]:
+ """Generate number-based permutations"""
+ permutations = set()
+
+ for pattern in patterns:
+ base = pattern['base']
+ current_num = pattern['number']
+
+ # Generate nearby numbers
+ for n in range(1, min(current_num + 10, 50)):
+ if n != current_num:
+ permutations.add(f"{base}{n}")
+ if n < 10:
+ permutations.add(f"{base}0{n}")
+
+ return permutations
+
+ def _generate_environment_permutations(self, patterns: List[dict]) -> Set[str]:
+ """Generate environment-based permutations"""
+ permutations = set()
+ environments = ['dev', 'test', 'stage', 'staging', 'prod', 'production', 'qa', 'uat', 'sandbox', 'demo']
+
+ for pattern in patterns:
+ base = pattern['base']
+ is_prefix = pattern.get('prefix', False)
+
+ for env in environments:
+ if env != pattern.get('env', '').lower():
+ for sep in ['-', '_', '.']:
+ if is_prefix:
+ permutations.add(f"{env}{sep}{base}")
+ else:
+ permutations.add(f"{base}{sep}{env}")
+
+ return permutations
+
+ def _generate_prefix_suffix_permutations(self, base_names: Set[str]) -> Set[str]:
+ """Generate prefix/suffix combinations"""
+ permutations = set()
+
+ for base in base_names:
+ # Don't permute very short names
+ if len(base) < 2:
+ continue
+
+ # Add common prefixes
+ for prefix in self.PREFIXES[:15]: # Limit to avoid explosion
+ for sep in ['-', '.']:
+ permutations.add(f"{prefix}{sep}{base}")
+
+ # Add common suffixes
+ for suffix in self.SUFFIXES[:15]:
+ for sep in ['-', '.']:
+ permutations.add(f"{base}{sep}{suffix}")
+
+ return permutations
+
+ def generate(
+ self,
+ discovered_subdomains: Set[str],
+ max_permutations: int = 1000
+ ) -> Set[str]:
+ """
+ Generate permutations of discovered subdomains.
+
+ Args:
+ discovered_subdomains: Set of discovered subdomains
+ max_permutations: Maximum number of permutations to generate
+
+ Returns:
+ Set of generated subdomain candidates (full FQDNs)
+ """
+ logger.info(f"Generating permutations for {len(discovered_subdomains)} subdomains")
+
+ patterns = self._extract_patterns(discovered_subdomains)
+ all_permutations = set()
+
+ # Generate different types of permutations
+ all_permutations.update(self._generate_version_permutations(patterns['versioned']))
+ all_permutations.update(self._generate_numbered_permutations(patterns['numbered']))
+ all_permutations.update(self._generate_environment_permutations(patterns['environment']))
+ all_permutations.update(self._generate_prefix_suffix_permutations(patterns['base_names']))
+
+ # Convert to FQDNs
+ fqdns = set()
+ for perm in all_permutations:
+ fqdn = f"{perm}.{self.domain}"
+ if fqdn not in discovered_subdomains:
+ fqdns.add(fqdn)
+
+ # Limit results
+ if len(fqdns) > max_permutations:
+ fqdns = set(list(fqdns)[:max_permutations])
+
+ logger.info(f"Generated {len(fqdns)} permutation candidates")
+ return fqdns
+
+ def enumerate(
+ self,
+ discovered_subdomains: Dict[str, Optional[str]] = None,
+ resolver_func: Callable[[str], Optional[str]] = None
+ ) -> Dict[str, Optional[str]]:
+ """
+ Generate and validate permutations.
+
+ Args:
+ discovered_subdomains: Dict of {subdomain: ip}
+ resolver_func: Optional function to resolve subdomains
+
+ Returns:
+ Dict of {subdomain: ip_address} for valid permutations
+ """
+ if not discovered_subdomains:
+ discovered_subdomains = {}
+
+ # Generate candidates
+ candidates = self.generate(set(discovered_subdomains.keys()))
+
+ if not candidates:
+ return {}
+
+ # Use provided resolver or default
+ if resolver_func is None:
+ resolver_func = self._resolve_subdomain
+
+ logger.info(f"Validating {len(candidates)} permutation candidates")
+
+ for candidate in candidates:
+ ip = resolver_func(candidate)
+ if ip:
+ self._add_discovered(candidate, ip)
+ logger.debug(f"Valid permutation: {candidate} -> {ip}")
+
+ logger.info(f"Permutation engine found {len(self.discovered)} valid subdomains")
+ return self.discovered
diff --git a/src/usfx/modules/recursive_enum.py b/src/usfx/modules/recursive_enum.py
@@ -0,0 +1,194 @@
+"""
+Recursive Subdomain Enumeration Module
+
+Discovers sub-subdomains by treating discovered subdomains as base domains
+and running enumeration on them (e.g., finding dev.api.example.com when
+api.example.com was discovered).
+"""
+
+import logging
+import time
+from concurrent.futures import ThreadPoolExecutor, as_completed
+from typing import Callable, Dict, Optional, TYPE_CHECKING
+
+from .base import BaseModule
+
+if TYPE_CHECKING:
+ from ..utils.dns_resolver import InternalDNSResolver
+
+logger = logging.getLogger(__name__)
+
+
+class RecursiveEnumerator(BaseModule):
+ """Discovers sub-subdomains through recursive enumeration"""
+
+ MODULE_NAME = "recursive_enum"
+
+ # Short wordlist for recursive enumeration (most common prefixes only)
+ RECURSIVE_WORDLIST = [
+ # Development
+ 'dev', 'staging', 'test', 'qa', 'beta',
+
+ # Versioning
+ 'v1', 'v2', 'v3',
+
+ # Environments
+ 'prod', 'internal',
+
+ # Geographic
+ 'us', 'eu', 'asia',
+
+ # Numbered
+ '1', '2', 'node1', 'node2',
+
+ # Services
+ 'api', 'app', 'www', 'admin', 'auth', 'db', 'cdn', 'static',
+ ]
+
+ def __init__(
+ self,
+ domain: str,
+ resolver: Optional['InternalDNSResolver'] = None,
+ threads: int = 20,
+ max_depth: int = 2
+ ):
+ """
+ Initialize recursive enumerator.
+
+ Args:
+ domain: Target domain
+ resolver: Custom DNS resolver
+ threads: Number of parallel threads
+ max_depth: Maximum recursion depth
+ """
+ super().__init__(domain, resolver)
+ self.threads = threads
+ self.max_depth = max_depth
+
+ def _enumerate_subdomain(self, base_subdomain: str) -> Dict[str, Optional[str]]:
+ """Enumerate sub-subdomains for a given subdomain"""
+ discovered = {}
+
+ for prefix in self.RECURSIVE_WORDLIST:
+ candidate = f'{prefix}.{base_subdomain}'
+
+ # Skip if it's too deep (more than max_depth levels above original domain)
+ depth = candidate.count('.') - self.domain.count('.')
+ if depth > self.max_depth:
+ continue
+
+ ip = self.resolver.resolve_a(candidate)
+ if ip:
+ discovered[candidate] = ip
+
+ return discovered
+
+ def _get_subdomain_depth(self, subdomain: str) -> int:
+ """Calculate depth of subdomain relative to base domain"""
+ return subdomain.count('.') - self.domain.count('.')
+
+ def enumerate(
+ self,
+ known_subdomains: Dict[str, Optional[str]] = None,
+ resolver_func: Optional[Callable[[str], Optional[str]]] = None
+ ) -> Dict[str, Optional[str]]:
+ """
+ Recursively enumerate sub-subdomains.
+
+ Args:
+ known_subdomains: Dict of discovered subdomains to use as base
+ resolver_func: Optional custom resolver function
+
+ Returns:
+ Dict of newly discovered sub-subdomains with IPs
+ """
+ start_time = time.time()
+ max_duration = 45 # Maximum 45 seconds
+
+ if not known_subdomains:
+ return {}
+
+ logger.info(f"Starting recursive enumeration on {len(known_subdomains)} subdomains")
+
+ # Filter subdomains that are worth recursing on
+ # Only recurse on depth-1 subdomains (direct subdomains of base domain)
+ candidates = []
+ for subdomain in known_subdomains.keys():
+ depth = self._get_subdomain_depth(subdomain)
+ if depth == 1: # Only direct subdomains
+ candidates.append(subdomain)
+
+ if not candidates:
+ logger.info("No suitable subdomains for recursive enumeration")
+ return {}
+
+ # Limit to first 10 candidates
+ candidates = candidates[:10]
+ logger.info(f"Recursively enumerating {len(candidates)} base subdomains")
+
+ all_discovered = {}
+
+ # Process each candidate subdomain
+ with ThreadPoolExecutor(max_workers=self.threads) as executor:
+ futures = {
+ executor.submit(self._enumerate_subdomain, sub): sub
+ for sub in candidates
+ }
+
+ for future in as_completed(futures, timeout=30):
+ if time.time() - start_time > max_duration:
+ logger.info("Recursive enumeration timeout reached")
+ break
+
+ try:
+ results = future.result(timeout=10)
+ for subdomain, ip in results.items():
+ if subdomain not in known_subdomains and subdomain not in all_discovered:
+ all_discovered[subdomain] = ip
+ self._add_discovered(subdomain, ip)
+ logger.debug(f"Recursive discovery: {subdomain} -> {ip}")
+ except Exception as e:
+ logger.debug(f"Recursive enumeration error: {e}")
+
+ logger.info(f"Recursive enumeration discovered {len(all_discovered)} new sub-subdomains in {time.time() - start_time:.1f}s")
+
+ return self.discovered
+
+ def deep_enumerate(
+ self,
+ known_subdomains: Dict[str, Optional[str]],
+ max_iterations: int = 3
+ ) -> Dict[str, Optional[str]]:
+ """
+ Perform multiple rounds of recursive enumeration.
+
+ Each round uses newly discovered subdomains as seeds for the next round.
+
+ Args:
+ known_subdomains: Initial set of known subdomains
+ max_iterations: Maximum number of recursive rounds
+
+ Returns:
+ All newly discovered subdomains
+ """
+ all_discovered = {}
+ current_known = dict(known_subdomains)
+
+ for iteration in range(max_iterations):
+ logger.info(f"Recursive enumeration round {iteration + 1}/{max_iterations}")
+
+ new_discoveries = self.enumerate(current_known)
+
+ if not new_discoveries:
+ logger.info(f"No new discoveries in round {iteration + 1}, stopping")
+ break
+
+ # Add to total discovered
+ all_discovered.update(new_discoveries)
+
+ # Update known for next round
+ current_known.update(new_discoveries)
+
+ logger.info(f"Round {iteration + 1} found {len(new_discoveries)} new subdomains")
+
+ return all_discovered
diff --git a/src/usfx/modules/reverse_dns.py b/src/usfx/modules/reverse_dns.py
@@ -0,0 +1,212 @@
+"""
+Reverse DNS Sweep Module
+
+Discovers subdomains by performing PTR lookups on IP ranges
+associated with the domain.
+"""
+
+import ipaddress
+import logging
+from concurrent.futures import ThreadPoolExecutor, as_completed
+from typing import Callable, Dict, List, Optional, Set, TYPE_CHECKING
+
+from .base import BaseModule
+
+if TYPE_CHECKING:
+ from ..utils.dns_resolver import InternalDNSResolver
+
+logger = logging.getLogger(__name__)
+
+
+class ReverseDNS(BaseModule):
+ """Reverse DNS (PTR) enumeration"""
+
+ MODULE_NAME = "reverse_dns"
+
+ def __init__(
+ self,
+ domain: str,
+ resolver: Optional['InternalDNSResolver'] = None,
+ timeout: float = 2.0,
+ threads: int = 30
+ ):
+ """
+ Initialize reverse DNS module.
+
+ Args:
+ domain: Target domain
+ resolver: Custom DNS resolver
+ timeout: DNS query timeout
+ threads: Number of parallel threads
+ """
+ super().__init__(domain, resolver, timeout)
+ self.threads = threads
+ self.known_ips: Set[str] = set()
+
+ def _get_domain_ips(self) -> Set[str]:
+ """
+ Get IP addresses associated with the domain.
+
+ Returns:
+ Set of IP addresses
+ """
+ ips = set()
+
+ # Get A records for main domain and common subdomains
+ for subdomain in ['', 'www', 'mail', 'ns1', 'ns2']:
+ fqdn = f"{subdomain}.{self.domain}" if subdomain else self.domain
+ ip = self.resolver.resolve_a(fqdn)
+ if ip:
+ ips.add(ip)
+
+ # Get IPs from MX records
+ mx_records = self.resolver.resolve_mx(self.domain)
+ for priority, mx_host in mx_records:
+ ip = self.resolver.resolve_a(mx_host)
+ if ip:
+ ips.add(ip)
+
+ # Get IPs from NS records
+ ns_records = self.resolver.resolve_ns(self.domain)
+ for ns_host in ns_records:
+ ip = self.resolver.resolve_a(ns_host)
+ if ip:
+ ips.add(ip)
+
+ logger.info(f"Found {len(ips)} known IPs for {self.domain}")
+ return ips
+
+ def _get_ip_range(self, ip: str) -> List[str]:
+ """
+ Get IP range to scan (/24 network).
+
+ Args:
+ ip: Base IP address
+
+ Returns:
+ List of IPs in the range
+ """
+ try:
+ ip_obj = ipaddress.ip_address(ip)
+ if isinstance(ip_obj, ipaddress.IPv4Address):
+ # Get /24 network
+ network = ipaddress.ip_network(f"{ip}/24", strict=False)
+ return [str(host) for host in network.hosts()]
+ else:
+ # For IPv6, use /112 (smaller range)
+ network = ipaddress.ip_network(f"{ip}/112", strict=False)
+ return [str(host) for host in list(network.hosts())[:256]]
+ except Exception as e:
+ logger.error(f"Error getting IP range for {ip}: {e}")
+ return []
+
+ def _scan_ip(self, ip: str) -> tuple[str, Optional[str]]:
+ """
+ Scan a single IP for PTR record matching our domain.
+
+ Args:
+ ip: IP to scan
+
+ Returns:
+ Tuple of (ip, hostname) if found
+ """
+ hostname = self.resolver.resolve_ptr(ip)
+ if hostname and hostname.endswith(f".{self.domain}"):
+ return ip, hostname
+ return ip, None
+
+ def enumerate(
+ self,
+ additional_ips: Optional[Set[str]] = None,
+ ip_ranges: Optional[List[str]] = None,
+ progress_callback: Optional[Callable[[int, int, int], None]] = None
+ ) -> Dict[str, Optional[str]]:
+ """
+ Perform reverse DNS sweep.
+
+ Args:
+ additional_ips: Additional IPs to include in sweep
+ ip_ranges: CIDR ranges to scan (e.g., ['192.168.0.0/24'])
+ progress_callback: Progress callback (current, total, found)
+
+ Returns:
+ Dict of {subdomain: ip_address}
+ """
+ logger.info(f"Starting reverse DNS sweep for {self.domain}")
+
+ # Get known IPs
+ self.known_ips = self._get_domain_ips()
+ if additional_ips:
+ self.known_ips.update(additional_ips)
+
+ # Get all IPs to scan (unique /24 networks)
+ networks_scanned = set()
+ all_ips = set()
+
+ # Add IPs from discovered domain records
+ for ip in self.known_ips:
+ try:
+ network = ipaddress.ip_network(f"{ip}/24", strict=False)
+ network_key = str(network)
+ if network_key not in networks_scanned:
+ networks_scanned.add(network_key)
+ all_ips.update(self._get_ip_range(ip))
+ except Exception:
+ pass
+
+ # Add explicit IP ranges
+ if ip_ranges:
+ for cidr in ip_ranges:
+ try:
+ network = ipaddress.ip_network(cidr, strict=False)
+ all_ips.update(str(host) for host in network.hosts())
+ logger.info(f"Added range {cidr} ({network.num_addresses} IPs)")
+ except ValueError as e:
+ logger.warning(f"Invalid CIDR range {cidr}: {e}")
+
+ if not all_ips:
+ logger.warning("No IP ranges to scan")
+ return {}
+
+ total = len(all_ips)
+ logger.info(f"Scanning {total} IPs across {len(networks_scanned)} networks")
+
+ completed = 0
+ found = 0
+
+ with ThreadPoolExecutor(max_workers=self.threads) as executor:
+ futures = {executor.submit(self._scan_ip, ip): ip for ip in all_ips}
+
+ for future in as_completed(futures):
+ try:
+ ip, hostname = future.result()
+ completed += 1
+
+ if hostname:
+ self._add_discovered(hostname, ip)
+ found += 1
+ logger.debug(f"Found: {hostname} -> {ip}")
+
+ if progress_callback and completed % 50 == 0:
+ progress_callback(completed, total, found)
+
+ except Exception as e:
+ completed += 1
+ logger.debug(f"Error in PTR lookup: {e}")
+
+ if progress_callback:
+ progress_callback(completed, total, found)
+
+ logger.info(f"Reverse DNS sweep found {len(self.discovered)} subdomains")
+ return self.discovered
+
+ def add_ips_from_subdomains(self, subdomains: Dict[str, Optional[str]]) -> None:
+ """
+ Add IPs from discovered subdomains for scanning.
+
+ Args:
+ subdomains: Dict of {subdomain: ip_address}
+ """
+ for subdomain, ip in subdomains.items():
+ if ip:
+ self.known_ips.add(ip)
diff --git a/src/usfx/modules/tls_analyzer.py b/src/usfx/modules/tls_analyzer.py
@@ -0,0 +1,265 @@
+"""
+TLS Certificate Analysis Module
+
+Extracts subdomains from TLS certificates by connecting to discovered IPs
+and parsing the Subject Alternative Names (SANs) field.
+"""
+
+import logging
+import socket
+import ssl
+from concurrent.futures import ThreadPoolExecutor, as_completed
+from typing import Dict, List, Optional, Set, TYPE_CHECKING
+
+from .base import BaseModule
+
+if TYPE_CHECKING:
+ from ..utils.dns_resolver import InternalDNSResolver
+
+try:
+ from cryptography import x509
+ from cryptography.x509.oid import NameOID, ExtensionOID
+ from cryptography.hazmat.backends import default_backend
+ CRYPTOGRAPHY_AVAILABLE = True
+except ImportError:
+ CRYPTOGRAPHY_AVAILABLE = False
+
+logger = logging.getLogger(__name__)
+
+
+class TLSAnalyzer(BaseModule):
+ """TLS certificate analysis for subdomain discovery"""
+
+ MODULE_NAME = "tls_analyzer"
+
+ # Common ports that might serve TLS
+ COMMON_TLS_PORTS = [443, 8443, 8080, 4443, 9443]
+
+ def __init__(
+ self,
+ domain: str,
+ resolver: Optional['InternalDNSResolver'] = None,
+ timeout: float = 5.0,
+ threads: int = 10
+ ):
+ """
+ Initialize TLS analyzer.
+
+ Args:
+ domain: Target domain
+ resolver: Custom DNS resolver
+ timeout: Connection timeout
+ threads: Number of parallel threads
+ """
+ super().__init__(domain, resolver, timeout)
+ self.threads = threads
+ self.analyzed_hosts: Set[str] = set()
+
+ def _create_ssl_context(self) -> ssl.SSLContext:
+ """Create an SSL context that accepts all certificates"""
+ ctx = ssl.create_default_context()
+ ctx.check_hostname = False
+ ctx.verify_mode = ssl.CERT_NONE
+ return ctx
+
+ def _get_certificate(self, host: str, port: int = 443) -> Optional[bytes]:
+ """
+ Get the TLS certificate from a host.
+
+ Args:
+ host: Hostname or IP
+ port: Port number
+
+ Returns:
+ DER-encoded certificate or None
+ """
+ try:
+ ctx = self._create_ssl_context()
+
+ with socket.create_connection((host, port), timeout=self.timeout) as sock:
+ with ctx.wrap_socket(sock, server_hostname=self.domain) as ssl_sock:
+ cert_der = ssl_sock.getpeercert(binary_form=True)
+ return cert_der
+
+ except (socket.timeout, socket.error, ssl.SSLError) as e:
+ logger.debug(f"Failed to get cert from {host}:{port}: {e}")
+ return None
+ except Exception as e:
+ logger.debug(f"Error getting cert from {host}:{port}: {e}")
+ return None
+
+ def _extract_sans_cryptography(self, cert_der: bytes) -> Set[str]:
+ """
+ Extract SANs using cryptography library.
+
+ Args:
+ cert_der: DER-encoded certificate
+
+ Returns:
+ Set of domain names from SANs
+ """
+ if not CRYPTOGRAPHY_AVAILABLE:
+ return set()
+
+ domains = set()
+
+ try:
+ cert = x509.load_der_x509_certificate(cert_der, default_backend())
+
+ # Get Subject Alternative Names
+ try:
+ san_ext = cert.extensions.get_extension_for_oid(ExtensionOID.SUBJECT_ALTERNATIVE_NAME)
+ for name in san_ext.value:
+ if isinstance(name, x509.DNSName):
+ domains.add(name.value.lower())
+ except x509.ExtensionNotFound:
+ pass
+
+ # Get Common Name
+ try:
+ for attr in cert.subject:
+ if attr.oid == NameOID.COMMON_NAME:
+ domains.add(attr.value.lower())
+ except Exception:
+ pass
+
+ except Exception as e:
+ logger.debug(f"Error parsing certificate: {e}")
+
+ return domains
+
+ def _extract_sans(self, cert_der: bytes) -> Set[str]:
+ """
+ Extract SANs from certificate.
+
+ Args:
+ cert_der: DER-encoded certificate
+
+ Returns:
+ Set of domain names from SANs
+ """
+ if CRYPTOGRAPHY_AVAILABLE:
+ return self._extract_sans_cryptography(cert_der)
+ else:
+ logger.warning("cryptography library not available - TLS analysis limited")
+ return set()
+
+ def _analyze_host(self, host: str, port: int = 443) -> Set[str]:
+ """
+ Analyze a single host's TLS certificate.
+
+ Args:
+ host: Host to analyze
+ port: Port number
+
+ Returns:
+ Set of discovered subdomains
+ """
+ discovered = set()
+ host_key = f"{host}:{port}"
+
+ if host_key in self.analyzed_hosts:
+ return discovered
+
+ self.analyzed_hosts.add(host_key)
+
+ cert_der = self._get_certificate(host, port)
+ if not cert_der:
+ return discovered
+
+ sans = self._extract_sans(cert_der)
+
+ for name in sans:
+ # Remove wildcard prefix
+ if name.startswith('*.'):
+ name = name[2:]
+
+ # Check if it's a subdomain of our target
+ if name.endswith(f".{self.domain}") and name != self.domain:
+ discovered.add(name)
+ logger.debug(f"Found SAN: {name} from {host}:{port}")
+
+ return discovered
+
+ def _probe_sni(self, ip: str, hostname: str, port: int = 443) -> bool:
+ """
+ Probe if a specific hostname works via SNI.
+
+ Args:
+ ip: IP address to connect to
+ hostname: Hostname to send via SNI
+ port: Port number
+
+ Returns:
+ True if the hostname is served at this IP
+ """
+ try:
+ ctx = self._create_ssl_context()
+
+ with socket.create_connection((ip, port), timeout=self.timeout) as sock:
+ with ctx.wrap_socket(sock, server_hostname=hostname) as ssl_sock:
+ cert_der = ssl_sock.getpeercert(binary_form=True)
+ if cert_der:
+ sans = self._extract_sans(cert_der)
+ return hostname.lower() in sans or f"*.{'.'.join(hostname.split('.')[1:])}".lower() in sans
+
+ except Exception:
+ pass
+
+ return False
+
+ def enumerate(
+ self,
+ known_hosts: Optional[Dict[str, str]] = None,
+ scan_ports: bool = False
+ ) -> Dict[str, Optional[str]]:
+ """
+ Enumerate subdomains from TLS certificates.
+
+ Args:
+ known_hosts: Dict of {subdomain: ip} to analyze
+ scan_ports: Whether to scan multiple TLS ports
+
+ Returns:
+ Dict of {subdomain: ip_address}
+ """
+ logger.info(f"Starting TLS analysis for {self.domain}")
+
+ if not CRYPTOGRAPHY_AVAILABLE:
+ logger.warning("cryptography library not available - TLS analysis limited")
+
+ # Determine hosts to analyze
+ hosts_to_analyze = set()
+
+ if known_hosts:
+ for subdomain, ip in known_hosts.items():
+ if ip:
+ hosts_to_analyze.add(ip)
+ hosts_to_analyze.add(subdomain)
+
+ # Always try the main domain
+ hosts_to_analyze.add(self.domain)
+ hosts_to_analyze.add(f"www.{self.domain}")
+
+ ports = self.COMMON_TLS_PORTS if scan_ports else [443]
+ all_discovered = set()
+
+ with ThreadPoolExecutor(max_workers=self.threads) as executor:
+ futures = []
+ for host in hosts_to_analyze:
+ for port in ports:
+ futures.append(executor.submit(self._analyze_host, host, port))
+
+ for future in as_completed(futures):
+ try:
+ discovered = future.result()
+ all_discovered.update(discovered)
+ except Exception as e:
+ logger.debug(f"Error in TLS analysis: {e}")
+
+ # Build result dict
+ for subdomain in all_discovered:
+ self._add_discovered(subdomain)
+
+ logger.info(f"TLS analysis found {len(self.discovered)} subdomains")
+ return self.discovered
diff --git a/src/usfx/modules/vhost_scanner.py b/src/usfx/modules/vhost_scanner.py
@@ -0,0 +1,328 @@
+"""
+Virtual Host Discovery Module
+
+Discovers subdomains by brute-forcing Host headers on known IP addresses.
+This technique finds subdomains hosted on the same server that may not
+be discoverable through DNS alone.
+
+Note: This module does NOT use external proxy services (unlike the web version).
+It connects directly to target IPs, which is suitable for internal network scanning.
+"""
+
+import hashlib
+import logging
+import time
+from concurrent.futures import ThreadPoolExecutor, as_completed
+from typing import Dict, List, Optional, Tuple, TYPE_CHECKING
+
+import requests
+from requests.exceptions import RequestException
+
+from .base import BaseModule
+
+if TYPE_CHECKING:
+ from ..utils.dns_resolver import InternalDNSResolver
+
+logger = logging.getLogger(__name__)
+
+
+class VHostScanner(BaseModule):
+ """Discovers virtual hosts through Host header manipulation"""
+
+ MODULE_NAME = "vhost_scanner"
+
+ # Common subdomain prefixes to try
+ VHOST_WORDLIST = [
+ # Common web apps
+ 'www', 'app', 'web', 'portal', 'admin', 'panel', 'dashboard',
+ 'api', 'api2', 'api3', 'rest', 'graphql',
+ 'blog', 'news', 'cms', 'wordpress', 'wp',
+ 'shop', 'store', 'cart', 'checkout', 'payment',
+ 'forum', 'community', 'support', 'help', 'docs', 'documentation',
+ 'wiki', 'kb', 'knowledgebase',
+
+ # Development
+ 'dev', 'development', 'staging', 'stage', 'test', 'testing',
+ 'qa', 'uat', 'sandbox', 'demo', 'preview', 'beta', 'alpha',
+ 'local', 'localhost', 'debug',
+
+ # Internal
+ 'internal', 'intranet', 'private', 'corp', 'corporate',
+ 'employee', 'staff', 'hr', 'finance', 'legal',
+
+ # Security
+ 'secure', 'login', 'signin', 'auth', 'sso', 'oauth', 'id', 'identity',
+ 'account', 'accounts', 'my', 'profile', 'user', 'users',
+
+ # Services
+ 'mail', 'email', 'webmail', 'mx', 'smtp', 'imap', 'pop',
+ 'ftp', 'sftp', 'files', 'upload', 'download',
+ 'cdn', 'static', 'assets', 'media', 'img', 'images', 'css', 'js',
+
+ # Infrastructure
+ 'server', 'host', 'node', 'cluster',
+ 'db', 'database', 'mysql', 'postgres', 'mongo', 'redis',
+ 'proxy', 'gateway', 'lb', 'loadbalancer',
+ 'vpn', 'remote', 'rdp', 'ssh',
+
+ # Monitoring
+ 'monitor', 'monitoring', 'status', 'health', 'ping',
+ 'metrics', 'grafana', 'prometheus', 'kibana', 'logs',
+
+ # Geographic/Versioned
+ 'us', 'eu', 'asia', 'uk',
+ 'v1', 'v2', 'v3', 'www2', 'www3', 'new', 'old', 'legacy',
+ ]
+
+ def __init__(
+ self,
+ domain: str,
+ resolver: Optional['InternalDNSResolver'] = None,
+ threads: int = 10,
+ timeout: float = 5.0
+ ):
+ """
+ Initialize vhost scanner.
+
+ Args:
+ domain: Target domain
+ resolver: Custom DNS resolver
+ threads: Number of parallel threads
+ timeout: HTTP request timeout
+ """
+ super().__init__(domain, resolver)
+ self.threads = threads
+ self.timeout = timeout
+
+ # Suppress SSL warnings
+ import urllib3
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
+
+ def _get_response_signature(self, response: requests.Response) -> str:
+ """Create a signature for a response to detect differences"""
+ factors = [
+ str(response.status_code),
+ str(len(response.content)),
+ response.headers.get('Content-Type', ''),
+ hashlib.md5(response.content[:1000]).hexdigest(),
+ ]
+ return '|'.join(factors)
+
+ def _get_baseline(self, ip: str, port: int = 443) -> Tuple[Optional[str], Optional[str]]:
+ """Get baseline response for invalid hostname"""
+ invalid_host = f'invalid-{hashlib.md5(ip.encode()).hexdigest()[:8]}.{self.domain}'
+
+ try:
+ protocol = 'https' if port == 443 else 'http'
+ response = requests.get(
+ f'{protocol}://{ip}:{port}/',
+ headers={'Host': invalid_host},
+ timeout=self.timeout,
+ allow_redirects=False,
+ verify=False
+ )
+ return self._get_response_signature(response), response.text[:500]
+ except:
+ return None, None
+
+ def _check_vhost(
+ self,
+ ip: str,
+ hostname: str,
+ port: int,
+ baseline_sig: Optional[str]
+ ) -> Optional[Tuple[str, bool]]:
+ """Check if a hostname is a valid virtual host"""
+ try:
+ protocol = 'https' if port == 443 else 'http'
+ response = requests.get(
+ f'{protocol}://{ip}:{port}/',
+ headers={'Host': hostname},
+ timeout=self.timeout,
+ allow_redirects=False,
+ verify=False
+ )
+
+ sig = self._get_response_signature(response)
+
+ # Check if response differs from baseline
+ if baseline_sig and sig != baseline_sig:
+ is_valid = True
+
+ # Check for generic error pages
+ body_lower = response.text.lower()
+ error_indicators = [
+ 'not found', '404', 'does not exist',
+ 'no such', 'invalid host', 'unknown host',
+ 'default page', 'welcome to nginx',
+ 'apache2 default', 'it works!',
+ ]
+
+ for indicator in error_indicators:
+ if indicator in body_lower and response.status_code in [200, 404]:
+ is_valid = False
+ break
+
+ if is_valid and response.status_code in [200, 301, 302, 307, 308, 401, 403]:
+ return hostname, True
+
+ # Even if same as baseline, 200 with good content might be valid
+ if response.status_code == 200 and len(response.content) > 1000:
+ if '<html' in response.text.lower() or '{' in response.text[:100]:
+ return hostname, True
+
+ except RequestException:
+ pass
+ except Exception as e:
+ logger.debug(f"VHost check error for {hostname}: {e}")
+
+ return None
+
+ def _scan_ip(self, ip: str, ports: List[int] = None) -> Dict[str, Optional[str]]:
+ """Scan a single IP for virtual hosts"""
+ if ports is None:
+ ports = [443, 80]
+
+ discovered = {}
+
+ # Use shortened wordlist for faster scanning
+ quick_wordlist = [
+ 'www', 'app', 'api', 'admin', 'portal', 'dashboard',
+ 'dev', 'staging', 'test', 'beta', 'demo',
+ 'mail', 'webmail', 'blog', 'shop', 'store',
+ 'cdn', 'static', 'assets', 'img', 'media',
+ 'login', 'auth', 'sso', 'secure',
+ 'internal', 'intranet', 'vpn', 'remote',
+ 'db', 'mysql', 'api2', 'v1', 'v2',
+ ]
+
+ for port in ports:
+ # Get baseline
+ baseline_sig, _ = self._get_baseline(ip, port)
+
+ # Test each hostname
+ for prefix in quick_wordlist:
+ hostname = f'{prefix}.{self.domain}'
+
+ result = self._check_vhost(ip, hostname, port, baseline_sig)
+ if result:
+ hostname, _ = result
+ if hostname not in discovered:
+ discovered[hostname] = ip
+ logger.debug(f"VHost discovered: {hostname} on {ip}:{port}")
+
+ return discovered
+
+ def enumerate(
+ self,
+ known_subdomains: Dict[str, Optional[str]] = None,
+ vhost_ips: List[str] = None
+ ) -> Dict[str, Optional[str]]:
+ """
+ Scan known IPs for virtual hosts.
+
+ Args:
+ known_subdomains: Dict of subdomains with their IPs
+ vhost_ips: Additional IPs to scan for vhosts
+
+ Returns:
+ Dict of newly discovered subdomains
+ """
+ start_time = time.time()
+ max_duration = 45 # Maximum 45 seconds for vhost scanning
+
+ if not known_subdomains:
+ known_subdomains = {}
+
+ # Extract unique IPs
+ unique_ips = set()
+
+ # Add IPs from known subdomains
+ for ip in known_subdomains.values():
+ if ip and len(unique_ips) < 5:
+ unique_ips.add(ip)
+
+ # Add explicitly provided IPs
+ if vhost_ips:
+ for ip in vhost_ips:
+ unique_ips.add(ip)
+
+ if not unique_ips:
+ logger.info("No IPs for virtual host scanning")
+ return {}
+
+ logger.info(f"Scanning {len(unique_ips)} unique IPs for virtual hosts")
+
+ all_discovered = {}
+
+ # Scan each IP
+ with ThreadPoolExecutor(max_workers=self.threads) as executor:
+ futures = {
+ executor.submit(self._scan_ip, ip): ip
+ for ip in unique_ips
+ }
+
+ for future in as_completed(futures, timeout=30):
+ if time.time() - start_time > max_duration:
+ logger.info("VHost scanning timeout reached")
+ break
+
+ ip = futures[future]
+ try:
+ results = future.result(timeout=10)
+ for hostname, host_ip in results.items():
+ if hostname not in known_subdomains and hostname not in all_discovered:
+ all_discovered[hostname] = host_ip
+ self._add_discovered(hostname, host_ip)
+ except Exception as e:
+ logger.debug(f"VHost scan error for {ip}: {e}")
+
+ logger.info(f"Virtual host scanning discovered {len(all_discovered)} new subdomains in {time.time() - start_time:.1f}s")
+
+ return self.discovered
+
+ def brute_force_vhosts(
+ self,
+ ip: str,
+ custom_wordlist: List[str] = None,
+ ports: List[int] = None
+ ) -> List[str]:
+ """
+ Brute force virtual hosts on a specific IP.
+
+ Args:
+ ip: Target IP address
+ custom_wordlist: Optional custom wordlist
+ ports: Ports to scan
+
+ Returns:
+ List of discovered hostnames
+ """
+ if ports is None:
+ ports = [443, 80]
+
+ wordlist = custom_wordlist or self.VHOST_WORDLIST
+ discovered = []
+
+ for port in ports:
+ baseline_sig, _ = self._get_baseline(ip, port)
+
+ with ThreadPoolExecutor(max_workers=self.threads) as executor:
+ futures = {}
+ for prefix in wordlist:
+ hostname = f'{prefix}.{self.domain}'
+ futures[executor.submit(
+ self._check_vhost, ip, hostname, port, baseline_sig
+ )] = hostname
+
+ for future in as_completed(futures, timeout=120):
+ try:
+ result = future.result()
+ if result:
+ hostname, _ = result
+ if hostname not in discovered:
+ discovered.append(hostname)
+ except:
+ pass
+
+ return discovered
diff --git a/src/usfx/modules/zone_transfer.py b/src/usfx/modules/zone_transfer.py
@@ -0,0 +1,182 @@
+"""
+DNS Zone Transfer (AXFR) Module
+
+Attempts to retrieve full DNS zone data from authoritative nameservers.
+About 5-15% of domains still allow zone transfers.
+"""
+
+import logging
+from typing import Dict, List, Optional, Set, TYPE_CHECKING
+
+import dns.query
+import dns.rdataclass
+import dns.rdatatype
+import dns.zone
+
+from .base import BaseModule
+
+if TYPE_CHECKING:
+ from ..utils.dns_resolver import InternalDNSResolver
+
+logger = logging.getLogger(__name__)
+
+
+class ZoneTransfer(BaseModule):
+ """DNS Zone Transfer (AXFR) enumeration"""
+
+ MODULE_NAME = "zone_transfer"
+
+ def __init__(
+ self,
+ domain: str,
+ resolver: Optional['InternalDNSResolver'] = None,
+ timeout: float = 10.0
+ ):
+ """
+ Initialize zone transfer module.
+
+ Args:
+ domain: Target domain
+ resolver: Custom DNS resolver
+ timeout: Timeout for zone transfer operations
+ """
+ super().__init__(domain, resolver, timeout)
+ self.nameservers: List[str] = []
+
+ def _get_nameservers(self) -> List[str]:
+ """
+ Get authoritative nameservers for the domain.
+
+ Returns:
+ List of nameserver hostnames
+ """
+ if self.nameservers:
+ return self.nameservers
+
+ self.nameservers = self.resolver.resolve_ns(self.domain)
+
+ if self.nameservers:
+ logger.info(f"Found {len(self.nameservers)} nameservers for {self.domain}")
+
+ return self.nameservers
+
+ def _resolve_ns_to_ip(self, ns_hostname: str) -> Optional[str]:
+ """
+ Resolve nameserver hostname to IP address.
+
+ Args:
+ ns_hostname: Nameserver hostname
+
+ Returns:
+ IP address or None
+ """
+ return self.resolver.resolve_a(ns_hostname)
+
+ def _attempt_axfr(self, ns_ip: str, ns_hostname: str) -> Set[str]:
+ """
+ Attempt zone transfer from a specific nameserver.
+
+ Args:
+ ns_ip: Nameserver IP address
+ ns_hostname: Nameserver hostname for logging
+
+ Returns:
+ Set of discovered subdomains
+ """
+ discovered = set()
+
+ try:
+ logger.debug(f"Attempting AXFR from {ns_hostname} ({ns_ip})")
+
+ zone = dns.zone.from_xfr(
+ dns.query.xfr(ns_ip, self.domain, timeout=self.timeout)
+ )
+
+ for name, node in zone.nodes.items():
+ subdomain = str(name)
+ if subdomain == '@':
+ continue
+
+ fqdn = f"{subdomain}.{self.domain}"
+
+ # Get A record if available
+ ip = None
+ try:
+ rdataset = node.find_rdataset(dns.rdataclass.IN, dns.rdatatype.A)
+ if rdataset:
+ ip = str(list(rdataset)[0])
+ except KeyError:
+ pass
+
+ discovered.add(fqdn)
+ self._add_discovered(fqdn, ip)
+
+ logger.info(f"Zone transfer SUCCESS from {ns_hostname}: {len(discovered)} records")
+ return discovered
+
+ except dns.exception.FormError:
+ logger.debug(f"Zone transfer REFUSED by {ns_hostname}")
+ except dns.query.TransferError as e:
+ logger.debug(f"Zone transfer FAILED from {ns_hostname}: {e}")
+ except dns.exception.Timeout:
+ logger.debug(f"Zone transfer TIMEOUT from {ns_hostname}")
+ except Exception as e:
+ logger.debug(f"Zone transfer ERROR from {ns_hostname}: {e}")
+
+ return discovered
+
+ def enumerate(self) -> Dict[str, Optional[str]]:
+ """
+ Attempt zone transfer from all nameservers.
+
+ Returns:
+ Dict of {subdomain: ip_address}
+ """
+ logger.info(f"Attempting zone transfer for {self.domain}")
+
+ nameservers = self._get_nameservers()
+ if not nameservers:
+ logger.warning(f"No nameservers found for {self.domain}")
+ return {}
+
+ all_discovered = set()
+
+ for ns in nameservers:
+ ns_ip = self._resolve_ns_to_ip(ns)
+ if not ns_ip:
+ continue
+
+ discovered = self._attempt_axfr(ns_ip, ns)
+ all_discovered.update(discovered)
+
+ if discovered:
+ # Zone transfer succeeded, we have complete data
+ logger.info(f"Zone transfer succeeded with {len(discovered)} records")
+ break
+
+ if not all_discovered:
+ logger.info(f"Zone transfer not allowed for {self.domain}")
+
+ return self.discovered
+
+ def is_available(self) -> bool:
+ """
+ Quick check if zone transfer might be available.
+
+ Returns:
+ True if zone transfer appears to be allowed
+ """
+ nameservers = self._get_nameservers()
+ if not nameservers:
+ return False
+
+ for ns in nameservers[:2]: # Only check first 2 NS
+ ns_ip = self._resolve_ns_to_ip(ns)
+ if not ns_ip:
+ continue
+
+ discovered = self._attempt_axfr(ns_ip, ns)
+ if discovered:
+ return True
+
+ return False
diff --git a/src/usfx/orchestrator.py b/src/usfx/orchestrator.py
@@ -0,0 +1,525 @@
+"""
+Subdomain Enumeration Orchestrator
+
+Manages the execution order of enumeration modules and aggregates results.
+Uses parallel execution within phases for maximum speed.
+
+This is a simplified orchestrator for internal networks that:
+- Does NOT use external APIs
+- Does NOT store results in a database
+- Keeps all results in memory
+"""
+
+import logging
+import threading
+import time
+from concurrent.futures import Future, ThreadPoolExecutor, as_completed
+from dataclasses import dataclass, field
+from enum import Enum
+from typing import Callable, Dict, List, Optional, Set, Tuple, TYPE_CHECKING
+
+from .utils.validator import SubdomainValidator
+from .utils.wildcard import WildcardDetector
+
+if TYPE_CHECKING:
+ from .config import ScanConfig
+ from .utils.dns_resolver import InternalDNSResolver
+
+logger = logging.getLogger(__name__)
+
+
+class Phase(Enum):
+ """Enumeration phases"""
+ QUICK_DISCOVERY = "quick_discovery"
+ DNS_ENUMERATION = "dns_enumeration"
+ EXTENSION = "extension"
+ VALIDATION = "validation"
+
+
+@dataclass
+class ModuleResult:
+ """Result from a single module"""
+ module_name: str
+ found_count: int
+ subdomains: Dict[str, Optional[str]]
+ duration_seconds: float
+ error: Optional[str] = None
+
+
+@dataclass
+class PhaseResult:
+ """Result from a phase"""
+ phase: Phase
+ modules: List[ModuleResult]
+ total_found: int
+ duration_seconds: float
+
+
+@dataclass
+class OrchestrationState:
+ """Current state of the orchestration"""
+ domain: str
+ current_phase: Optional[Phase] = None
+ current_module: Optional[str] = None
+ progress: int = 0
+ total_found: int = 0
+ discovered: Dict[str, dict] = field(default_factory=dict)
+ phase_results: List[PhaseResult] = field(default_factory=list)
+ started_at: float = field(default_factory=time.time)
+ is_cancelled: bool = False
+
+
+class SubdomainOrchestrator:
+ """Orchestrates subdomain enumeration modules"""
+
+ # Phase weight for overall progress calculation
+ PHASE_WEIGHTS = {
+ Phase.QUICK_DISCOVERY: (0, 15), # 0-15%
+ Phase.DNS_ENUMERATION: (15, 60), # 15-60%
+ Phase.EXTENSION: (60, 90), # 60-90%
+ Phase.VALIDATION: (90, 100), # 90-100%
+ }
+
+ def __init__(
+ self,
+ domain: str,
+ resolver: Optional['InternalDNSResolver'] = None,
+ config: Optional['ScanConfig'] = None
+ ):
+ """
+ Initialize orchestrator.
+
+ Args:
+ domain: Target domain
+ resolver: Custom DNS resolver
+ config: Scan configuration
+ """
+ self.domain = domain.lower().strip('.')
+ self.state = OrchestrationState(domain=self.domain)
+ self.config = config
+
+ # Create resolver
+ if resolver:
+ self.resolver = resolver
+ else:
+ from .utils.dns_resolver import InternalDNSResolver
+ dns_servers = config.dns_servers if config else None
+ timeout = config.timeout if config else 3.0
+ self.resolver = InternalDNSResolver(dns_servers=dns_servers, timeout=timeout)
+
+ self.validator = SubdomainValidator(resolver=self.resolver)
+ self.wildcard_detector = WildcardDetector(domain, self.resolver)
+
+ # Progress callback
+ self.progress_callback: Optional[Callable[[OrchestrationState], None]] = None
+
+ # Lock for thread-safe result collection
+ self._results_lock = threading.Lock()
+
+ def set_progress_callback(self, callback: Callable[[OrchestrationState], None]):
+ """Set callback for progress updates"""
+ self.progress_callback = callback
+
+ def _update_progress(self, phase: Phase, module: str, phase_progress: int):
+ """Update and broadcast progress"""
+ self.state.current_phase = phase
+ self.state.current_module = module
+
+ # Calculate overall progress based on phase and phase_progress
+ phase_start, phase_end = self.PHASE_WEIGHTS.get(phase, (0, 100))
+ phase_range = phase_end - phase_start
+ overall_progress = phase_start + (phase_progress * phase_range // 100)
+
+ self.state.progress = overall_progress
+
+ if self.progress_callback:
+ self.progress_callback(self.state)
+
+ def _add_results(self, results: Dict[str, Optional[str]], module_name: str):
+ """Add results from a module (thread-safe)"""
+ with self._results_lock:
+ for subdomain, ip in results.items():
+ if subdomain not in self.state.discovered:
+ self.state.discovered[subdomain] = {
+ 'ip': ip,
+ 'discovered_by': module_name,
+ 'is_active': ip is not None
+ }
+ self.state.total_found += 1
+ elif ip and not self.state.discovered[subdomain].get('ip'):
+ # Update IP if we have one now
+ self.state.discovered[subdomain]['ip'] = ip
+ self.state.discovered[subdomain]['is_active'] = True
+
+ def _run_module_task(
+ self,
+ module_factory: Callable,
+ module_name: str,
+ needs_known_subs: bool = False,
+ **kwargs
+ ) -> ModuleResult:
+ """Run a single module as a task"""
+ start_time = time.time()
+
+ try:
+ module = module_factory()
+
+ # Get current state snapshot for modules that need it
+ if needs_known_subs:
+ with self._results_lock:
+ known_subs = {k: v.get('ip') for k, v in self.state.discovered.items()}
+
+ if module_name == "tls_analyzer":
+ results = module.enumerate(known_hosts=known_subs)
+ elif module_name == "permutation":
+ def resolve(subdomain):
+ return self.resolver.resolve_a(subdomain)
+ results = module.enumerate(known_subs, resolver_func=resolve)
+ else:
+ results = module.enumerate(known_subs)
+ else:
+ results = module.enumerate()
+
+ duration = time.time() - start_time
+ self._add_results(results, module_name)
+
+ logger.info(f"Module {module_name} found {len(results)} subdomains in {duration:.1f}s")
+
+ return ModuleResult(
+ module_name=module_name,
+ found_count=len(results),
+ subdomains=results,
+ duration_seconds=duration
+ )
+
+ except Exception as e:
+ logger.error(f"Error in module {module_name}: {e}")
+ return ModuleResult(
+ module_name=module_name,
+ found_count=0,
+ subdomains={},
+ duration_seconds=time.time() - start_time,
+ error=str(e)
+ )
+
+ def _run_modules_parallel(
+ self,
+ phase: Phase,
+ module_tasks: List[Tuple[Callable, str, dict]],
+ max_workers: int = 10
+ ) -> List[ModuleResult]:
+ """Run multiple modules in parallel"""
+ results = []
+ completed_count = 0
+ total_modules = len(module_tasks)
+
+ module_names = [name for _, name, _ in module_tasks]
+ self._update_progress(phase, f"parallel: {', '.join(module_names)}", 0)
+
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
+ futures: Dict[Future, str] = {}
+ for module_factory, module_name, kwargs in module_tasks:
+ future = executor.submit(
+ self._run_module_task,
+ module_factory,
+ module_name,
+ **kwargs
+ )
+ futures[future] = module_name
+
+ for future in as_completed(futures):
+ module_name = futures[future]
+ try:
+ result = future.result(timeout=300)
+ results.append(result)
+ except Exception as e:
+ logger.error(f"Module {module_name} failed: {e}")
+ results.append(ModuleResult(
+ module_name=module_name,
+ found_count=0,
+ subdomains={},
+ duration_seconds=0,
+ error=str(e)
+ ))
+
+ completed_count += 1
+ progress = int(completed_count * 100 / total_modules)
+ self._update_progress(phase, module_name, progress)
+
+ if self.state.is_cancelled:
+ for f in futures:
+ f.cancel()
+ break
+
+ return results
+
+ def _get_enabled_modules(self) -> Set[str]:
+ """Get set of enabled module names"""
+ if self.config and self.config.modules:
+ return self.config.modules
+ return {
+ "zone_transfer", "dns_records", "dns_bruteforce", "dnssec_walker",
+ "reverse_dns", "cname_chaser", "permutation", "recursive_enum",
+ "vhost_scanner", "tls_analyzer"
+ }
+
+ def run_phase1_quick_discovery(self) -> PhaseResult:
+ """Phase 1: Quick Discovery - Zone Transfer and DNS Records"""
+ from .modules.zone_transfer import ZoneTransfer
+ from .modules.dns_records import DNSRecordMiner
+
+ phase_start = time.time()
+ results = []
+ enabled = self._get_enabled_modules()
+
+ # Get config values
+ threads = self.config.threads if self.config else 30
+
+ module_tasks = []
+
+ if "zone_transfer" in enabled:
+ module_tasks.append((
+ lambda: ZoneTransfer(self.domain, self.resolver),
+ "zone_transfer",
+ {}
+ ))
+
+ if "dns_records" in enabled:
+ module_tasks.append((
+ lambda: DNSRecordMiner(self.domain, self.resolver),
+ "dns_records",
+ {}
+ ))
+
+ if module_tasks:
+ results = self._run_modules_parallel(Phase.QUICK_DISCOVERY, module_tasks, max_workers=2)
+
+ self._update_progress(Phase.QUICK_DISCOVERY, "complete", 100)
+
+ return PhaseResult(
+ phase=Phase.QUICK_DISCOVERY,
+ modules=results,
+ total_found=sum(r.found_count for r in results),
+ duration_seconds=time.time() - phase_start
+ )
+
+ def run_phase2_dns_enumeration(self) -> PhaseResult:
+ """Phase 2: DNS Enumeration - Brute force and DNSSEC"""
+ from .modules.dns_bruteforce import DNSBruteforcer
+ from .modules.dnssec_walker import DNSSECWalker
+
+ phase_start = time.time()
+ results = []
+ enabled = self._get_enabled_modules()
+
+ threads = self.config.threads if self.config else 30
+ wordlist_size = self.config.wordlist_size.value if self.config else 'medium'
+ wordlist_path = str(self.config.custom_wordlist) if self.config and self.config.custom_wordlist else None
+
+ module_tasks = []
+
+ if "dns_bruteforce" in enabled:
+ module_tasks.append((
+ lambda: DNSBruteforcer(
+ self.domain, self.resolver,
+ wordlist_path=wordlist_path,
+ wordlist_size=wordlist_size,
+ threads=threads
+ ),
+ "dns_bruteforce",
+ {}
+ ))
+
+ if "dnssec_walker" in enabled:
+ module_tasks.append((
+ lambda: DNSSECWalker(self.domain, self.resolver),
+ "dnssec_walker",
+ {}
+ ))
+
+ if module_tasks:
+ results = self._run_modules_parallel(Phase.DNS_ENUMERATION, module_tasks, max_workers=2)
+
+ self._update_progress(Phase.DNS_ENUMERATION, "complete", 100)
+
+ return PhaseResult(
+ phase=Phase.DNS_ENUMERATION,
+ modules=results,
+ total_found=sum(r.found_count for r in results),
+ duration_seconds=time.time() - phase_start
+ )
+
+ def run_phase3_extension(self) -> PhaseResult:
+ """Phase 3: Extension - Permutation, Reverse DNS, Recursive, VHost, TLS"""
+ from .modules.permutation import PermutationEngine
+ from .modules.reverse_dns import ReverseDNS
+ from .modules.recursive_enum import RecursiveEnumerator
+ from .modules.vhost_scanner import VHostScanner
+ from .modules.tls_analyzer import TLSAnalyzer
+ from .modules.cname_chaser import CNAMEChaser
+
+ phase_start = time.time()
+ results = []
+ enabled = self._get_enabled_modules()
+
+ threads = self.config.threads if self.config else 30
+ vhost_ips = self.config.vhost_ips if self.config else []
+ reverse_ranges = self.config.reverse_dns_ranges if self.config else []
+
+ module_tasks = []
+
+ if "permutation" in enabled:
+ module_tasks.append((
+ lambda: PermutationEngine(self.domain, self.resolver),
+ "permutation",
+ {"needs_known_subs": True}
+ ))
+
+ if "cname_chaser" in enabled:
+ module_tasks.append((
+ lambda: CNAMEChaser(self.domain, self.resolver, threads=threads),
+ "cname_chaser",
+ {"needs_known_subs": True}
+ ))
+
+ if "recursive_enum" in enabled:
+ module_tasks.append((
+ lambda: RecursiveEnumerator(self.domain, self.resolver, threads=threads),
+ "recursive_enum",
+ {"needs_known_subs": True}
+ ))
+
+ if "tls_analyzer" in enabled:
+ module_tasks.append((
+ lambda: TLSAnalyzer(self.domain, self.resolver, threads=threads),
+ "tls_analyzer",
+ {"needs_known_subs": True}
+ ))
+
+ if module_tasks:
+ results = self._run_modules_parallel(Phase.EXTENSION, module_tasks, max_workers=4)
+
+ # Run reverse DNS separately (may need special IP ranges)
+ if "reverse_dns" in enabled:
+ self._update_progress(Phase.EXTENSION, "reverse_dns", 80)
+ reverse_module = ReverseDNS(self.domain, self.resolver, threads=threads)
+ with self._results_lock:
+ known_ips = {v.get('ip') for v in self.state.discovered.values() if v.get('ip')}
+ reverse_results = reverse_module.enumerate(
+ additional_ips=known_ips,
+ ip_ranges=reverse_ranges if reverse_ranges else None
+ )
+ self._add_results(reverse_results, "reverse_dns")
+ results.append(ModuleResult(
+ module_name="reverse_dns",
+ found_count=len(reverse_results),
+ subdomains=reverse_results,
+ duration_seconds=0
+ ))
+
+ # Run vhost scanner separately (may need special IPs)
+ if "vhost_scanner" in enabled and vhost_ips:
+ self._update_progress(Phase.EXTENSION, "vhost_scanner", 90)
+ vhost_module = VHostScanner(self.domain, self.resolver, threads=threads)
+ with self._results_lock:
+ known_subs = {k: v.get('ip') for k, v in self.state.discovered.items()}
+ vhost_results = vhost_module.enumerate(known_subs, vhost_ips=vhost_ips)
+ self._add_results(vhost_results, "vhost_scanner")
+ results.append(ModuleResult(
+ module_name="vhost_scanner",
+ found_count=len(vhost_results),
+ subdomains=vhost_results,
+ duration_seconds=0
+ ))
+
+ self._update_progress(Phase.EXTENSION, "complete", 100)
+
+ return PhaseResult(
+ phase=Phase.EXTENSION,
+ modules=results,
+ total_found=sum(r.found_count for r in results),
+ duration_seconds=time.time() - phase_start
+ )
+
+ def run_phase4_validation(self) -> PhaseResult:
+ """Phase 4: Validation - DNS resolution verification and wildcard filtering"""
+ phase_start = time.time()
+
+ self._update_progress(Phase.VALIDATION, "validating", 0)
+
+ # Detect wildcards
+ has_wildcard, wildcard_ips = self.wildcard_detector.detect()
+
+ # Validate all discovered subdomains
+ subdomains_to_validate = set(self.state.discovered.keys())
+ threads = self.config.threads if self.config else 30
+ validated = self.validator.validate_batch(subdomains_to_validate, threads=threads)
+
+ # Update state with validation results
+ removed_count = 0
+ for subdomain, result in validated.items():
+ if subdomain in self.state.discovered:
+ if result['ip']:
+ # Check wildcard
+ if has_wildcard and result['ip'] in wildcard_ips:
+ del self.state.discovered[subdomain]
+ removed_count += 1
+ else:
+ self.state.discovered[subdomain]['ip'] = result['ip']
+ self.state.discovered[subdomain]['is_active'] = True
+ else:
+ self.state.discovered[subdomain]['is_active'] = False
+
+ self.state.total_found = len(self.state.discovered)
+
+ self._update_progress(Phase.VALIDATION, "complete", 100)
+
+ if removed_count > 0:
+ logger.info(f"Filtered {removed_count} wildcard results")
+
+ logger.info(f"Validation complete: {self.state.total_found} valid subdomains")
+
+ return PhaseResult(
+ phase=Phase.VALIDATION,
+ modules=[ModuleResult(
+ module_name="validation",
+ found_count=self.state.total_found,
+ subdomains={},
+ duration_seconds=time.time() - phase_start
+ )],
+ total_found=self.state.total_found,
+ duration_seconds=time.time() - phase_start
+ )
+
+ def cancel(self):
+ """Cancel the orchestration"""
+ self.state.is_cancelled = True
+
+ def run_all(self) -> OrchestrationState:
+ """Run all phases"""
+ logger.info(f"Starting subdomain enumeration for {self.domain}")
+
+ # Phase 1: Quick Discovery
+ result = self.run_phase1_quick_discovery()
+ self.state.phase_results.append(result)
+ if self.state.is_cancelled:
+ return self.state
+
+ # Phase 2: DNS Enumeration
+ result = self.run_phase2_dns_enumeration()
+ self.state.phase_results.append(result)
+ if self.state.is_cancelled:
+ return self.state
+
+ # Phase 3: Extension
+ result = self.run_phase3_extension()
+ self.state.phase_results.append(result)
+ if self.state.is_cancelled:
+ return self.state
+
+ # Phase 4: Validation
+ result = self.run_phase4_validation()
+ self.state.phase_results.append(result)
+
+ logger.info(f"Enumeration complete: {self.state.total_found} subdomains found")
+ return self.state
diff --git a/src/usfx/utils/__init__.py b/src/usfx/utils/__init__.py
@@ -0,0 +1,25 @@
+"""
+USFX Utilities
+
+Core utilities for subdomain enumeration:
+- dns_resolver: Custom DNS resolver supporting internal DNS servers
+- wildcard: Wildcard DNS detection
+- validator: Subdomain validation
+- wordlist: Wordlist management with bundled lists
+- output: Result output formatters (JSON/CSV/TXT)
+"""
+
+from .dns_resolver import InternalDNSResolver
+from .wildcard import WildcardDetector
+from .validator import SubdomainValidator
+from .wordlist import WordlistManager, get_bundled_wordlist
+from .output import OutputFormatter
+
+__all__ = [
+ "InternalDNSResolver",
+ "WildcardDetector",
+ "SubdomainValidator",
+ "WordlistManager",
+ "get_bundled_wordlist",
+ "OutputFormatter",
+]
diff --git a/src/usfx/utils/dns_resolver.py b/src/usfx/utils/dns_resolver.py
@@ -0,0 +1,364 @@
+"""
+Custom DNS Resolver for Internal Networks
+
+The core utility that enables subdomain enumeration against internal DNS servers.
+Supports multiple DNS servers, custom timeouts, and provides a unified interface
+for all enumeration modules.
+"""
+
+import logging
+from typing import List, Optional, Set, Tuple
+
+import dns.resolver
+import dns.exception
+import dns.rdatatype
+import dns.reversename
+
+logger = logging.getLogger(__name__)
+
+
+class InternalDNSResolver:
+ """
+ DNS resolver that supports custom internal DNS servers.
+
+ This is the key component that differentiates internal-usf from
+ internet-based subdomain finders. By specifying internal DNS servers,
+ you can enumerate subdomains that only exist on your internal network.
+
+ Usage:
+ # Use custom internal DNS servers
+ resolver = InternalDNSResolver(dns_servers=['192.168.1.1', '10.0.0.1'])
+
+ # Use system default DNS
+ resolver = InternalDNSResolver()
+
+ # Resolve a subdomain
+ ip = resolver.resolve_a('api.corp.local')
+ """
+
+ def __init__(
+ self,
+ dns_servers: Optional[List[str]] = None,
+ timeout: float = 3.0,
+ lifetime: float = 6.0
+ ):
+ """
+ Initialize the DNS resolver.
+
+ Args:
+ dns_servers: List of DNS server IPs. If None, uses system defaults.
+ timeout: Timeout for individual DNS queries in seconds.
+ lifetime: Total time allowed for a query (including retries).
+ """
+ self.resolver = dns.resolver.Resolver()
+ self.timeout = timeout
+ self.lifetime = lifetime
+
+ # Configure resolver
+ self.resolver.timeout = timeout
+ self.resolver.lifetime = lifetime
+
+ # Set custom DNS servers if provided
+ if dns_servers:
+ self.dns_servers = dns_servers
+ self.resolver.nameservers = dns_servers
+ logger.info(f"Using custom DNS servers: {', '.join(dns_servers)}")
+ else:
+ # Use system default DNS servers
+ self.dns_servers = self.resolver.nameservers.copy()
+ logger.info(f"Using system DNS servers: {', '.join(self.dns_servers)}")
+
+ def resolve_a(self, hostname: str) -> Optional[str]:
+ """
+ Resolve hostname to IPv4 address (A record).
+
+ Args:
+ hostname: Fully qualified domain name to resolve.
+
+ Returns:
+ Primary IP address or None if not found.
+ """
+ try:
+ answers = self.resolver.resolve(hostname, 'A')
+ return str(answers[0])
+ except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
+ return None
+ except dns.resolver.NoNameservers:
+ logger.debug(f"No nameservers available for {hostname}")
+ return None
+ except dns.exception.Timeout:
+ logger.debug(f"Timeout resolving {hostname}")
+ return None
+ except Exception as e:
+ logger.debug(f"Error resolving {hostname}: {e}")
+ return None
+
+ def resolve_a_all(self, hostname: str) -> Set[str]:
+ """
+ Resolve hostname to all IPv4 addresses.
+
+ Args:
+ hostname: Fully qualified domain name to resolve.
+
+ Returns:
+ Set of all IP addresses, empty set if not found.
+ """
+ try:
+ answers = self.resolver.resolve(hostname, 'A')
+ return {str(rdata) for rdata in answers}
+ except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer,
+ dns.resolver.NoNameservers, dns.exception.Timeout):
+ return set()
+ except Exception as e:
+ logger.debug(f"Error resolving {hostname}: {e}")
+ return set()
+
+ def resolve_aaaa(self, hostname: str) -> Optional[str]:
+ """
+ Resolve hostname to IPv6 address (AAAA record).
+
+ Args:
+ hostname: Fully qualified domain name to resolve.
+
+ Returns:
+ Primary IPv6 address or None if not found.
+ """
+ try:
+ answers = self.resolver.resolve(hostname, 'AAAA')
+ return str(answers[0])
+ except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer,
+ dns.resolver.NoNameservers, dns.exception.Timeout):
+ return None
+ except Exception as e:
+ logger.debug(f"Error resolving AAAA for {hostname}: {e}")
+ return None
+
+ def resolve_cname(self, hostname: str) -> Optional[str]:
+ """
+ Resolve CNAME record for hostname.
+
+ Args:
+ hostname: Fully qualified domain name.
+
+ Returns:
+ CNAME target or None if not found.
+ """
+ try:
+ answers = self.resolver.resolve(hostname, 'CNAME')
+ return str(answers[0].target).rstrip('.')
+ except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer,
+ dns.resolver.NoNameservers, dns.exception.Timeout):
+ return None
+ except Exception as e:
+ logger.debug(f"Error resolving CNAME for {hostname}: {e}")
+ return None
+
+ def resolve_mx(self, domain: str) -> List[Tuple[int, str]]:
+ """
+ Resolve MX records for domain.
+
+ Args:
+ domain: Domain to query.
+
+ Returns:
+ List of (priority, hostname) tuples.
+ """
+ try:
+ answers = self.resolver.resolve(domain, 'MX')
+ return [(rdata.preference, str(rdata.exchange).rstrip('.'))
+ for rdata in answers]
+ except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer,
+ dns.resolver.NoNameservers, dns.exception.Timeout):
+ return []
+ except Exception as e:
+ logger.debug(f"Error resolving MX for {domain}: {e}")
+ return []
+
+ def resolve_ns(self, domain: str) -> List[str]:
+ """
+ Resolve NS records for domain.
+
+ Args:
+ domain: Domain to query.
+
+ Returns:
+ List of nameserver hostnames.
+ """
+ try:
+ answers = self.resolver.resolve(domain, 'NS')
+ return [str(rdata).rstrip('.') for rdata in answers]
+ except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer,
+ dns.resolver.NoNameservers, dns.exception.Timeout):
+ return []
+ except Exception as e:
+ logger.debug(f"Error resolving NS for {domain}: {e}")
+ return []
+
+ def resolve_txt(self, domain: str) -> List[str]:
+ """
+ Resolve TXT records for domain.
+
+ Args:
+ domain: Domain to query.
+
+ Returns:
+ List of TXT record values.
+ """
+ try:
+ answers = self.resolver.resolve(domain, 'TXT')
+ return [str(rdata).strip('"') for rdata in answers]
+ except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer,
+ dns.resolver.NoNameservers, dns.exception.Timeout):
+ return []
+ except Exception as e:
+ logger.debug(f"Error resolving TXT for {domain}: {e}")
+ return []
+
+ def resolve_srv(self, name: str) -> List[Tuple[int, int, int, str]]:
+ """
+ Resolve SRV records.
+
+ Args:
+ name: SRV record name (e.g., _ldap._tcp.domain.local)
+
+ Returns:
+ List of (priority, weight, port, target) tuples.
+ """
+ try:
+ answers = self.resolver.resolve(name, 'SRV')
+ return [(rdata.priority, rdata.weight, rdata.port,
+ str(rdata.target).rstrip('.'))
+ for rdata in answers]
+ except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer,
+ dns.resolver.NoNameservers, dns.exception.Timeout):
+ return []
+ except Exception as e:
+ logger.debug(f"Error resolving SRV for {name}: {e}")
+ return []
+
+ def resolve_soa(self, domain: str) -> Optional[dict]:
+ """
+ Resolve SOA record for domain.
+
+ Args:
+ domain: Domain to query.
+
+ Returns:
+ Dict with SOA fields or None.
+ """
+ try:
+ answers = self.resolver.resolve(domain, 'SOA')
+ rdata = answers[0]
+ return {
+ 'mname': str(rdata.mname).rstrip('.'),
+ 'rname': str(rdata.rname).rstrip('.'),
+ 'serial': rdata.serial,
+ 'refresh': rdata.refresh,
+ 'retry': rdata.retry,
+ 'expire': rdata.expire,
+ 'minimum': rdata.minimum,
+ }
+ except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer,
+ dns.resolver.NoNameservers, dns.exception.Timeout):
+ return None
+ except Exception as e:
+ logger.debug(f"Error resolving SOA for {domain}: {e}")
+ return None
+
+ def resolve_ptr(self, ip: str) -> Optional[str]:
+ """
+ Resolve PTR (reverse DNS) record for IP address.
+
+ Args:
+ ip: IP address to lookup.
+
+ Returns:
+ Hostname or None.
+ """
+ try:
+ reverse_name = dns.reversename.from_address(ip)
+ answers = self.resolver.resolve(reverse_name, 'PTR')
+ return str(answers[0]).rstrip('.').lower()
+ except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer,
+ dns.resolver.NoNameservers, dns.exception.Timeout):
+ return None
+ except Exception as e:
+ logger.debug(f"Error resolving PTR for {ip}: {e}")
+ return None
+
+ def resolve_any(self, hostname: str, record_type: str) -> List[str]:
+ """
+ Resolve any DNS record type.
+
+ Args:
+ hostname: Hostname to query.
+ record_type: DNS record type (A, AAAA, MX, NS, TXT, etc.)
+
+ Returns:
+ List of string representations of the records.
+ """
+ try:
+ answers = self.resolver.resolve(hostname, record_type)
+ return [str(rdata) for rdata in answers]
+ except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer,
+ dns.resolver.NoNameservers, dns.exception.Timeout):
+ return []
+ except Exception as e:
+ logger.debug(f"Error resolving {record_type} for {hostname}: {e}")
+ return []
+
+ def check_exists(self, hostname: str) -> bool:
+ """
+ Check if a hostname exists (has any DNS record).
+
+ Args:
+ hostname: Hostname to check.
+
+ Returns:
+ True if hostname exists.
+ """
+ # Try A record first (most common)
+ if self.resolve_a(hostname):
+ return True
+
+ # Try CNAME
+ if self.resolve_cname(hostname):
+ return True
+
+ # Try AAAA
+ if self.resolve_aaaa(hostname):
+ return True
+
+ return False
+
+ def get_native_resolver(self) -> dns.resolver.Resolver:
+ """
+ Get the underlying dnspython resolver for advanced operations.
+
+ Returns:
+ The configured dns.resolver.Resolver instance.
+ """
+ return self.resolver
+
+ def create_child_resolver(
+ self,
+ timeout: Optional[float] = None,
+ lifetime: Optional[float] = None
+ ) -> 'InternalDNSResolver':
+ """
+ Create a child resolver with the same DNS servers but different timeouts.
+
+ Useful for modules that need different timeout configurations.
+
+ Args:
+ timeout: New timeout value (uses parent's if None)
+ lifetime: New lifetime value (uses parent's if None)
+
+ Returns:
+ New InternalDNSResolver instance.
+ """
+ return InternalDNSResolver(
+ dns_servers=self.dns_servers,
+ timeout=timeout if timeout is not None else self.timeout,
+ lifetime=lifetime if lifetime is not None else self.lifetime
+ )
diff --git a/src/usfx/utils/output.py b/src/usfx/utils/output.py
@@ -0,0 +1,371 @@
+"""
+Output Formatter
+
+Handles formatting and writing scan results to various formats:
+JSON, CSV, and TXT.
+"""
+
+import csv
+import json
+import logging
+from datetime import datetime
+from pathlib import Path
+from typing import List, Optional, TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from ..config import ScanResult, SubdomainResult
+
+logger = logging.getLogger(__name__)
+
+
+class OutputFormatter:
+ """Formats and outputs scan results in various formats"""
+
+ @staticmethod
+ def to_json(result: 'ScanResult', pretty: bool = True) -> str:
+ """
+ Convert scan result to JSON string.
+
+ Args:
+ result: ScanResult object
+ pretty: Whether to pretty-print the JSON
+
+ Returns:
+ JSON string
+ """
+ data = {
+ 'domain': result.domain,
+ 'scan_time': datetime.now().isoformat(),
+ 'total_found': result.total_found,
+ 'duration_seconds': round(result.duration_seconds, 2),
+ 'dns_servers': result.dns_servers_used,
+ 'subdomains': [
+ {
+ 'subdomain': s.subdomain,
+ 'ip': s.ip,
+ 'discovered_by': s.discovered_by,
+ 'is_active': s.is_active,
+ }
+ for s in result.subdomains
+ ],
+ 'modules': [
+ {
+ 'name': m.module_name,
+ 'found': m.found_count,
+ 'duration': round(m.duration_seconds, 2),
+ 'error': m.error,
+ }
+ for m in result.module_results
+ ]
+ }
+
+ if pretty:
+ return json.dumps(data, indent=2, ensure_ascii=False)
+ return json.dumps(data, ensure_ascii=False)
+
+ @staticmethod
+ def to_csv(result: 'ScanResult') -> str:
+ """
+ Convert scan result to CSV string.
+
+ Args:
+ result: ScanResult object
+
+ Returns:
+ CSV string
+ """
+ import io
+ output = io.StringIO()
+ writer = csv.writer(output)
+
+ # Header
+ writer.writerow(['subdomain', 'ip', 'discovered_by', 'is_active'])
+
+ # Data
+ for s in result.subdomains:
+ writer.writerow([
+ s.subdomain,
+ s.ip or '',
+ s.discovered_by,
+ 'yes' if s.is_active else 'no'
+ ])
+
+ return output.getvalue()
+
+ @staticmethod
+ def to_txt(result: 'ScanResult', include_ips: bool = True) -> str:
+ """
+ Convert scan result to plain text (one subdomain per line).
+
+ Args:
+ result: ScanResult object
+ include_ips: Whether to include IP addresses
+
+ Returns:
+ Plain text string
+ """
+ lines = []
+
+ for s in result.subdomains:
+ if include_ips and s.ip:
+ lines.append(f"{s.subdomain}\t{s.ip}")
+ else:
+ lines.append(s.subdomain)
+
+ return '\n'.join(lines)
+
+ @staticmethod
+ def write_file(
+ result: 'ScanResult',
+ output_path: Path,
+ format: str = 'json'
+ ) -> None:
+ """
+ Write scan results to file.
+
+ Args:
+ result: ScanResult object
+ output_path: Path to output file
+ format: Output format ('json', 'csv', 'txt')
+ """
+ format = format.lower()
+
+ if format == 'json':
+ content = OutputFormatter.to_json(result)
+ elif format == 'csv':
+ content = OutputFormatter.to_csv(result)
+ elif format == 'txt':
+ content = OutputFormatter.to_txt(result)
+ else:
+ raise ValueError(f"Unknown output format: {format}")
+
+ # Ensure parent directory exists
+ output_path.parent.mkdir(parents=True, exist_ok=True)
+
+ with open(output_path, 'w', encoding='utf-8') as f:
+ f.write(content)
+
+ logger.info(f"Results written to {output_path}")
+
+ @staticmethod
+ def format_summary(result: 'ScanResult') -> str:
+ """
+ Format a human-readable summary of scan results.
+
+ Args:
+ result: ScanResult object
+
+ Returns:
+ Formatted summary string
+ """
+ lines = [
+ "",
+ f"{'=' * 50}",
+ f"Scan Complete: {result.domain}",
+ f"{'=' * 50}",
+ f"Total subdomains found: {result.total_found}",
+ f"Duration: {result.duration_seconds:.1f}s",
+ f"DNS servers: {', '.join(result.dns_servers_used) or 'system default'}",
+ "",
+ "Module Results:",
+ ]
+
+ for module in result.module_results:
+ status = f"[ERROR: {module.error}]" if module.error else ""
+ lines.append(
+ f" - {module.module_name}: {module.found_count} found "
+ f"({module.duration_seconds:.1f}s) {status}"
+ )
+
+ if result.subdomains:
+ lines.extend([
+ "",
+ "Discovered Subdomains:",
+ "-" * 50,
+ ])
+
+ # Group by active status
+ active = [s for s in result.subdomains if s.is_active]
+ inactive = [s for s in result.subdomains if not s.is_active]
+
+ if active:
+ lines.append(f"\nActive ({len(active)}):")
+ for s in active[:20]: # Limit display
+ ip_str = f" -> {s.ip}" if s.ip else ""
+ lines.append(f" {s.subdomain}{ip_str}")
+ if len(active) > 20:
+ lines.append(f" ... and {len(active) - 20} more")
+
+ if inactive:
+ lines.append(f"\nInactive/Unresolved ({len(inactive)}):")
+ for s in inactive[:10]:
+ lines.append(f" {s.subdomain}")
+ if len(inactive) > 10:
+ lines.append(f" ... and {len(inactive) - 10} more")
+
+ return '\n'.join(lines)
+
+
+class ConsoleOutput:
+ """
+ Console output helper with optional Rich formatting.
+ Falls back to plain text if Rich is not available.
+ """
+
+ def __init__(self, use_rich: bool = True, quiet: bool = False):
+ """
+ Initialize console output.
+
+ Args:
+ use_rich: Whether to use Rich for formatting
+ quiet: Whether to suppress non-essential output
+ """
+ self.quiet = quiet
+ self.use_rich = use_rich
+
+ if use_rich:
+ try:
+ from rich.console import Console
+ from rich.table import Table
+ from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
+ self.console = Console()
+ self._rich_available = True
+ except ImportError:
+ self._rich_available = False
+ self.use_rich = False
+ else:
+ self._rich_available = False
+
+ def print_banner(self, version: str) -> None:
+ """Print application banner"""
+ if self.quiet:
+ return
+
+ banner = f"""
+ USFX v{version}
+ Ultimate Subdomain Finder X for Internal Networks
+"""
+ if self.use_rich:
+ self.console.print(banner, style="bold blue")
+ else:
+ print(banner)
+
+ def print_config(self, domain: str, dns_servers: List[str], modules: List[str]) -> None:
+ """Print scan configuration"""
+ if self.quiet:
+ return
+
+ if self.use_rich:
+ self.console.print(f"\n[bold]Target:[/bold] {domain}")
+ if dns_servers:
+ self.console.print(f"[bold]DNS Servers:[/bold] {', '.join(dns_servers)}")
+ else:
+ self.console.print("[bold]DNS Servers:[/bold] system default")
+ self.console.print(f"[bold]Modules:[/bold] {', '.join(modules)}")
+ self.console.print()
+ else:
+ print(f"\nTarget: {domain}")
+ if dns_servers:
+ print(f"DNS Servers: {', '.join(dns_servers)}")
+ else:
+ print("DNS Servers: system default")
+ print(f"Modules: {', '.join(modules)}")
+ print()
+
+ def print_phase(self, phase: str) -> None:
+ """Print phase header"""
+ if self.quiet:
+ return
+
+ if self.use_rich:
+ self.console.print(f"\n[bold cyan][{phase}][/bold cyan]")
+ else:
+ print(f"\n[{phase}]")
+
+ def print_module_result(self, module: str, count: int, duration: float) -> None:
+ """Print module result"""
+ if self.quiet:
+ return
+
+ if self.use_rich:
+ self.console.print(f" └─ {module}: [green]{count}[/green] found ({duration:.1f}s)")
+ else:
+ print(f" └─ {module}: {count} found ({duration:.1f}s)")
+
+ def print_result_table(self, result: 'ScanResult') -> None:
+ """Print results as a table"""
+ if self.quiet or not result.subdomains:
+ return
+
+ if self.use_rich:
+ from rich.table import Table
+
+ table = Table(title=f"Results: {result.total_found} subdomains")
+ table.add_column("Subdomain", style="cyan")
+ table.add_column("IP Address", style="green")
+ table.add_column("Source", style="dim")
+
+ for s in result.subdomains[:50]: # Limit to 50 rows
+ table.add_row(s.subdomain, s.ip or "-", s.discovered_by)
+
+ if result.total_found > 50:
+ table.add_row("...", f"({result.total_found - 50} more)", "")
+
+ self.console.print(table)
+ else:
+ print(f"\nResults: {result.total_found} subdomains")
+ print("-" * 60)
+ print(f"{'Subdomain':<35} {'IP Address':<15} Source")
+ print("-" * 60)
+
+ for s in result.subdomains[:50]:
+ print(f"{s.subdomain:<35} {(s.ip or '-'):<15} {s.discovered_by}")
+
+ if result.total_found > 50:
+ print(f"... ({result.total_found - 50} more)")
+
+ def print_summary(self, result: 'ScanResult') -> None:
+ """Print scan summary"""
+ if self.use_rich:
+ self.console.print(f"\n[bold green]Scan complete![/bold green]")
+ self.console.print(f"Found [bold]{result.total_found}[/bold] subdomains in {result.duration_seconds:.1f}s")
+ else:
+ print(f"\nScan complete!")
+ print(f"Found {result.total_found} subdomains in {result.duration_seconds:.1f}s")
+
+ def print_error(self, message: str) -> None:
+ """Print error message"""
+ if self.use_rich:
+ self.console.print(f"[bold red]Error:[/bold red] {message}")
+ else:
+ print(f"Error: {message}")
+
+ def create_progress(self):
+ """Create a progress context manager"""
+ if self.use_rich and not self.quiet:
+ from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TimeElapsedColumn
+
+ return Progress(
+ SpinnerColumn(),
+ TextColumn("[bold blue]{task.description}"),
+ BarColumn(),
+ TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
+ TimeElapsedColumn(),
+ console=self.console,
+ )
+ else:
+ # Return a dummy context manager
+ class DummyProgress:
+ def __enter__(self):
+ return self
+
+ def __exit__(self, *args):
+ pass
+
+ def add_task(self, *args, **kwargs):
+ return 0
+
+ def update(self, *args, **kwargs):
+ pass
+
+ return DummyProgress()
diff --git a/src/usfx/utils/validator.py b/src/usfx/utils/validator.py
@@ -0,0 +1,210 @@
+"""
+Subdomain Validation
+
+Validates discovered subdomains by checking DNS resolution and HTTP accessibility.
+"""
+
+import logging
+import socket
+import ssl
+from concurrent.futures import ThreadPoolExecutor, as_completed
+from typing import Dict, Optional, Set, Tuple, TYPE_CHECKING
+from urllib.request import urlopen, Request
+from urllib.error import URLError, HTTPError
+
+if TYPE_CHECKING:
+ from .dns_resolver import InternalDNSResolver
+
+logger = logging.getLogger(__name__)
+
+
+class SubdomainValidator:
+ """Validates discovered subdomains"""
+
+ def __init__(
+ self,
+ timeout: float = 3.0,
+ resolver: Optional['InternalDNSResolver'] = None
+ ):
+ """
+ Initialize validator.
+
+ Args:
+ timeout: Timeout for validation operations
+ resolver: Optional custom DNS resolver
+ """
+ self.timeout = timeout
+
+ if resolver:
+ self.resolver = resolver
+ else:
+ from .dns_resolver import InternalDNSResolver
+ self.resolver = InternalDNSResolver(timeout=timeout)
+
+ def resolve_subdomain(self, subdomain: str) -> Optional[str]:
+ """
+ Resolve a subdomain to its IP address.
+
+ Args:
+ subdomain: Full subdomain to resolve
+
+ Returns:
+ IP address or None if unresolvable
+ """
+ return self.resolver.resolve_a(subdomain)
+
+ def check_http(self, subdomain: str, https: bool = True) -> Optional[int]:
+ """
+ Check HTTP(S) accessibility of a subdomain.
+
+ Args:
+ subdomain: Full subdomain to check
+ https: Whether to use HTTPS (default True)
+
+ Returns:
+ HTTP status code or None if inaccessible
+ """
+ protocol = 'https' if https else 'http'
+ url = f"{protocol}://{subdomain}/"
+
+ try:
+ ctx = ssl.create_default_context()
+ ctx.check_hostname = False
+ ctx.verify_mode = ssl.CERT_NONE
+
+ req = Request(url, headers={'User-Agent': 'Mozilla/5.0'})
+
+ if https:
+ response = urlopen(req, timeout=self.timeout, context=ctx)
+ else:
+ response = urlopen(req, timeout=self.timeout)
+
+ return response.getcode()
+ except HTTPError as e:
+ return e.code
+ except URLError:
+ return None
+ except Exception:
+ return None
+
+ def validate_single(self, subdomain: str, check_http: bool = False) -> dict:
+ """
+ Validate a single subdomain.
+
+ Args:
+ subdomain: Subdomain to validate
+ check_http: Whether to also check HTTP accessibility
+
+ Returns:
+ Dict with ip, is_active, http_status, https_status
+ """
+ ip = self.resolve_subdomain(subdomain)
+ result = {
+ 'ip': ip,
+ 'is_active': ip is not None,
+ 'http_status': None,
+ 'https_status': None
+ }
+
+ if check_http and ip:
+ result['https_status'] = self.check_http(subdomain, https=True)
+ if result['https_status'] is None:
+ result['http_status'] = self.check_http(subdomain, https=False)
+
+ return result
+
+ def validate_batch(
+ self,
+ subdomains: Set[str],
+ threads: int = 20,
+ check_http: bool = False
+ ) -> Dict[str, dict]:
+ """
+ Validate a batch of subdomains.
+
+ Args:
+ subdomains: Set of subdomains to validate
+ threads: Number of parallel threads
+ check_http: Whether to also check HTTP accessibility
+
+ Returns:
+ Dict of {subdomain: {ip, is_active, http_status, https_status}}
+ """
+ results = {}
+
+ def validate_single_wrapper(subdomain: str) -> Tuple[str, dict]:
+ return subdomain, self.validate_single(subdomain, check_http)
+
+ with ThreadPoolExecutor(max_workers=threads) as executor:
+ futures = {executor.submit(validate_single_wrapper, sub): sub for sub in subdomains}
+
+ for future in as_completed(futures):
+ try:
+ subdomain, result = future.result()
+ results[subdomain] = result
+ except Exception as e:
+ subdomain = futures[future]
+ logger.error(f"Error validating {subdomain}: {e}")
+ results[subdomain] = {
+ 'ip': None,
+ 'is_active': False,
+ 'http_status': None,
+ 'https_status': None
+ }
+
+ return results
+
+ @staticmethod
+ def is_valid_domain(domain: str) -> bool:
+ """
+ Check if a domain string is valid.
+
+ Args:
+ domain: Domain to validate
+
+ Returns:
+ True if valid domain format
+ """
+ if not domain or len(domain) > 253:
+ return False
+
+ # Check for valid characters
+ allowed = set('abcdefghijklmnopqrstuvwxyz0123456789.-')
+ if not set(domain.lower()).issubset(allowed):
+ return False
+
+ # Check label lengths
+ labels = domain.split('.')
+ if len(labels) < 2:
+ return False
+
+ for label in labels:
+ if not label or len(label) > 63:
+ return False
+ if label.startswith('-') or label.endswith('-'):
+ return False
+
+ return True
+
+ @staticmethod
+ def extract_subdomain_from_fqdn(fqdn: str, parent_domain: str) -> Optional[str]:
+ """
+ Extract subdomain part from a fully qualified domain name.
+
+ Args:
+ fqdn: Full domain (e.g., 'api.staging.example.com')
+ parent_domain: Parent domain (e.g., 'example.com')
+
+ Returns:
+ Subdomain part (e.g., 'api.staging') or None
+ """
+ fqdn = fqdn.lower().rstrip('.')
+ parent_domain = parent_domain.lower().rstrip('.')
+
+ if not fqdn.endswith(f".{parent_domain}") and fqdn != parent_domain:
+ return None
+
+ if fqdn == parent_domain:
+ return None
+
+ return fqdn[:-len(parent_domain) - 1]
diff --git a/src/usfx/utils/wildcard.py b/src/usfx/utils/wildcard.py
@@ -0,0 +1,127 @@
+"""
+Wildcard DNS Detection
+
+Detects if a domain uses wildcard DNS records (*.domain.com)
+which would return valid responses for any subdomain query.
+"""
+
+import random
+import string
+import logging
+from typing import Optional, Set, Tuple, TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from .dns_resolver import InternalDNSResolver
+
+logger = logging.getLogger(__name__)
+
+
+class WildcardDetector:
+ """Detects wildcard DNS configurations"""
+
+ def __init__(
+ self,
+ domain: str,
+ resolver: Optional['InternalDNSResolver'] = None
+ ):
+ """
+ Initialize wildcard detector.
+
+ Args:
+ domain: Target domain
+ resolver: Optional custom DNS resolver (uses internal resolver if None)
+ """
+ self.domain = domain
+ self.wildcard_ips: Set[str] = set()
+ self.has_wildcard = False
+
+ if resolver:
+ self.resolver = resolver
+ else:
+ from .dns_resolver import InternalDNSResolver
+ self.resolver = InternalDNSResolver()
+
+ def _generate_random_subdomain(self, length: int = 16) -> str:
+ """Generate a random subdomain that is unlikely to exist"""
+ chars = string.ascii_lowercase + string.digits
+ return ''.join(random.choice(chars) for _ in range(length))
+
+ def detect(self, num_tests: int = 3) -> Tuple[bool, Set[str]]:
+ """
+ Detect if domain uses wildcard DNS.
+
+ Args:
+ num_tests: Number of random subdomain tests to perform
+
+ Returns:
+ Tuple of (has_wildcard, wildcard_ips)
+ """
+ test_results = []
+
+ for _ in range(num_tests):
+ random_sub = self._generate_random_subdomain()
+ test_domain = f"{random_sub}.{self.domain}"
+
+ ips = self.resolver.resolve_a_all(test_domain)
+ test_results.append(ips)
+
+ if ips:
+ logger.debug(f"Wildcard test {test_domain} resolved to {ips}")
+
+ # If all tests returned the same non-empty IP set, we have a wildcard
+ non_empty_results = [r for r in test_results if r]
+
+ if len(non_empty_results) >= 2:
+ # Check if all non-empty results have overlapping IPs
+ common_ips = non_empty_results[0]
+ for result in non_empty_results[1:]:
+ common_ips = common_ips & result
+
+ if common_ips:
+ self.has_wildcard = True
+ self.wildcard_ips = common_ips
+ logger.info(f"Wildcard detected for {self.domain}: {common_ips}")
+
+ return self.has_wildcard, self.wildcard_ips
+
+ def is_wildcard_result(self, ips: Set[str]) -> bool:
+ """
+ Check if a set of IPs matches the wildcard IPs.
+
+ Args:
+ ips: Set of IP addresses to check
+
+ Returns:
+ True if the IPs match the wildcard configuration
+ """
+ if not self.has_wildcard or not self.wildcard_ips:
+ return False
+
+ # If the result IPs are a subset of wildcard IPs, it's likely a wildcard result
+ return bool(ips & self.wildcard_ips)
+
+ def filter_wildcards(self, subdomains: dict) -> dict:
+ """
+ Filter out wildcard results from discovered subdomains.
+
+ Args:
+ subdomains: Dict of {subdomain: ip_address}
+
+ Returns:
+ Filtered dict without wildcard results
+ """
+ if not self.has_wildcard:
+ return subdomains
+
+ filtered = {}
+ for subdomain, ip in subdomains.items():
+ if ip and ip not in self.wildcard_ips:
+ filtered[subdomain] = ip
+ elif ip is None:
+ filtered[subdomain] = ip
+
+ filtered_count = len(subdomains) - len(filtered)
+ if filtered_count > 0:
+ logger.info(f"Filtered {filtered_count} wildcard results")
+
+ return filtered
diff --git a/src/usfx/utils/wordlist.py b/src/usfx/utils/wordlist.py
@@ -0,0 +1,284 @@
+"""
+Wordlist Management
+
+Manages subdomain wordlists for brute force enumeration.
+Uses importlib.resources for bundled wordlists to support pip installation.
+"""
+
+import logging
+from pathlib import Path
+from typing import List, Optional, Set
+
+try:
+ from importlib.resources import files as pkg_files
+ from importlib.resources import as_file
+except ImportError:
+ # Python < 3.9 fallback
+ from importlib_resources import files as pkg_files
+ from importlib_resources import as_file
+
+logger = logging.getLogger(__name__)
+
+
+# Built-in minimal wordlist as fallback
+BUILTIN_WORDLIST = [
+ # Common
+ 'www', 'mail', 'ftp', 'localhost', 'webmail', 'smtp', 'pop', 'ns1', 'ns2',
+ 'ns3', 'ns4', 'dns', 'dns1', 'dns2', 'mx', 'mx1', 'mx2', 'imap', 'remote',
+ 'blog', 'shop', 'store', 'forum', 'cpanel', 'whm', 'webdisk', 'api',
+
+ # Development
+ 'dev', 'development', 'test', 'testing', 'stage', 'staging', 'prod',
+ 'production', 'qa', 'uat', 'sandbox', 'demo', 'beta', 'alpha', 'preview',
+ 'pre', 'preprod', 'pre-production', 'dev1', 'dev2', 'test1', 'test2',
+
+ # Infrastructure
+ 'db', 'database', 'mysql', 'postgres', 'postgresql', 'redis', 'mongo',
+ 'mongodb', 'elastic', 'elasticsearch', 'cache', 'memcache', 'memcached',
+ 'backup', 'bak', 'server', 'server1', 'server2', 'node', 'node1', 'node2',
+ 'host', 'host1', 'host2', 'web', 'web1', 'web2', 'app', 'app1', 'app2',
+ 'worker', 'worker1', 'worker2', 'lb', 'loadbalancer', 'proxy', 'gateway',
+
+ # Services
+ 'api', 'api1', 'api2', 'rest', 'graphql', 'grpc', 'ws', 'websocket',
+ 'socket', 'cdn', 'static', 'assets', 'images', 'img', 'media', 'files',
+ 'upload', 'uploads', 'download', 'downloads', 'docs', 'documentation',
+ 'wiki', 'help', 'support', 'status', 'health', 'monitor', 'monitoring',
+ 'metrics', 'logs', 'logging', 'kibana', 'grafana', 'prometheus',
+
+ # Security & Admin
+ 'admin', 'administrator', 'root', 'secure', 'security', 'ssl', 'vpn',
+ 'portal', 'login', 'signin', 'auth', 'authentication', 'oauth', 'sso',
+ 'panel', 'dashboard', 'console', 'control', 'manage', 'management',
+
+ # Cloud & DevOps
+ 'jenkins', 'ci', 'cd', 'build', 'deploy', 'git', 'gitlab', 'github',
+ 'bitbucket', 'jira', 'confluence', 'slack', 'teams', 'kubernetes', 'k8s',
+ 'docker', 'container', 'registry', 'helm', 'terraform', 'ansible',
+ 'aws', 'azure', 'gcp', 'cloud', 's3', 'storage', 'bucket',
+
+ # Email & Communication
+ 'email', 'newsletter', 'mail2', 'mail3', 'webmail2', 'exchange',
+ 'outlook', 'autodiscover', 'autoconfig', 'calendar', 'chat', 'im',
+
+ # Regional & Language
+ 'en', 'es', 'de', 'fr', 'it', 'pt', 'ru', 'zh', 'ja', 'ko',
+ 'us', 'eu', 'uk', 'au', 'ca', 'asia', 'emea', 'apac', 'latam',
+
+ # Versioning
+ 'v1', 'v2', 'v3', 'v4', 'api-v1', 'api-v2', 'api-v3',
+ 'old', 'new', 'legacy', 'archive', 'archives', 'backup1', 'backup2',
+
+ # Mobile
+ 'mobile', 'm', 'app-api', 'ios', 'android', 'mobile-api',
+
+ # Internal (important for internal network enumeration)
+ 'internal', 'intranet', 'corp', 'corporate', 'staff', 'employee',
+ 'office', 'hr', 'finance', 'sales', 'marketing', 'legal',
+ 'dc', 'dc1', 'dc2', 'ad', 'ldap', 'kerberos', 'ntp', 'radius',
+ 'fileserver', 'printserver', 'sharepoint', 'exchange',
+
+ # Misc
+ 'home', 'public', 'private', 'www1', 'www2', 'www3', 'web3',
+ 'origin', 'origin-www', 'direct', 'main', 'primary', 'secondary',
+ 'temp', 'tmp', 'data', 'report', 'reports', 'analytics', 'tracking',
+ 'crm', 'erp', 'cms', 'ecommerce', 'payment', 'payments', 'checkout',
+ 'cart', 'order', 'orders', 'billing', 'invoice', 'invoices',
+
+ # Name patterns
+ 'web01', 'web02', 'web03', 'app01', 'app02', 'app03',
+ 'srv01', 'srv02', 'srv03', 'srv1', 'srv2', 'srv3',
+ 'dc1', 'dc2', 'datacenter', 'datacenter1', 'datacenter2'
+]
+
+
+def get_bundled_wordlist(size: str = 'medium') -> Path:
+ """
+ Get path to a bundled wordlist file.
+
+ Args:
+ size: Wordlist size - 'small', 'medium', or 'large'
+
+ Returns:
+ Path to the wordlist file
+
+ Raises:
+ FileNotFoundError: If the wordlist file doesn't exist
+ """
+ filename_map = {
+ 'small': 'wordlist_small.txt',
+ 'medium': 'wordlist.txt',
+ 'large': 'wordlist_large.txt',
+ }
+
+ filename = filename_map.get(size, 'wordlist.txt')
+
+ try:
+ # Try to get the bundled resource
+ ref = pkg_files('usfx.data').joinpath(filename)
+ with as_file(ref) as path:
+ if path.exists():
+ return path
+
+ # Fallback: check relative to this file
+ data_dir = Path(__file__).parent.parent / 'data'
+ path = data_dir / filename
+ if path.exists():
+ return path
+
+ raise FileNotFoundError(f"Wordlist not found: {filename}")
+
+ except Exception as e:
+ logger.warning(f"Could not load bundled wordlist: {e}")
+ raise FileNotFoundError(f"Wordlist not found: {filename}")
+
+
+class WordlistManager:
+ """Manages subdomain wordlists with support for bundled and custom lists"""
+
+ def __init__(self, wordlist_path: Optional[str] = None):
+ """
+ Initialize wordlist manager.
+
+ Args:
+ wordlist_path: Optional custom wordlist file path
+ """
+ self.wordlist_path = wordlist_path
+ self._wordlist: Optional[List[str]] = None
+
+ def load_wordlist(self, size: str = 'medium') -> List[str]:
+ """
+ Load wordlist from file or use built-in.
+
+ Args:
+ size: Wordlist size for bundled lists ('small', 'medium', 'large')
+
+ Returns:
+ List of subdomain words
+ """
+ if self._wordlist is not None:
+ return self._wordlist
+
+ wordlist = set()
+
+ # Try to load from custom path first
+ if self.wordlist_path:
+ try:
+ with open(self.wordlist_path, 'r', encoding='utf-8') as f:
+ for line in f:
+ word = line.strip().lower()
+ if word and not word.startswith('#'):
+ wordlist.add(word)
+ logger.info(f"Loaded {len(wordlist)} words from {self.wordlist_path}")
+ except Exception as e:
+ logger.error(f"Error loading custom wordlist: {e}")
+
+ # If no custom wordlist or it failed, try bundled
+ if not wordlist:
+ try:
+ bundled_path = get_bundled_wordlist(size)
+ with open(bundled_path, 'r', encoding='utf-8') as f:
+ for line in f:
+ word = line.strip().lower()
+ if word and not word.startswith('#'):
+ wordlist.add(word)
+ logger.info(f"Loaded {len(wordlist)} words from bundled {size} wordlist")
+ except FileNotFoundError:
+ logger.warning("Bundled wordlist not found, using built-in minimal list")
+
+ # Always add built-in words as fallback
+ wordlist.update(BUILTIN_WORDLIST)
+
+ self._wordlist = sorted(list(wordlist))
+ logger.info(f"Total wordlist size: {len(self._wordlist)}")
+
+ return self._wordlist
+
+ def get_wordlist(
+ self,
+ size: str = 'medium',
+ max_words: Optional[int] = None,
+ include_numbers: bool = True,
+ include_env: bool = True
+ ) -> List[str]:
+ """
+ Get wordlist with optional filtering and expansion.
+
+ Args:
+ size: Wordlist size for bundled lists
+ max_words: Maximum number of words to return
+ include_numbers: Include numbered variations (api1, api2, etc.)
+ include_env: Include environment variations (api-dev, api-prod, etc.)
+
+ Returns:
+ List of subdomain words
+ """
+ base_list = self.load_wordlist(size)
+ result = set(base_list)
+
+ if include_numbers:
+ # Add numbered variations
+ for word in list(result)[:500]: # Limit base words for expansion
+ for i in range(1, 6):
+ result.add(f"{word}{i}")
+ result.add(f"{word}0{i}")
+
+ if include_env:
+ # Add environment variations
+ envs = ['dev', 'test', 'stage', 'staging', 'prod', 'qa', 'uat']
+ for word in list(result)[:200]: # Limit base words
+ for env in envs:
+ result.add(f"{word}-{env}")
+ result.add(f"{env}-{word}")
+
+ result_list = sorted(list(result))
+
+ if max_words:
+ result_list = result_list[:max_words]
+
+ return result_list
+
+ def add_discovered(self, subdomains: Set[str]) -> None:
+ """
+ Add newly discovered subdomains to wordlist.
+
+ Args:
+ subdomains: Set of discovered subdomains
+ """
+ if self._wordlist is None:
+ self.load_wordlist()
+
+ existing = set(self._wordlist)
+ new_words = []
+
+ for subdomain in subdomains:
+ parts = subdomain.split('.')
+ for part in parts:
+ if part and part not in existing:
+ new_words.append(part)
+ existing.add(part)
+
+ if new_words:
+ self._wordlist.extend(new_words)
+ self._wordlist.sort()
+ logger.debug(f"Added {len(new_words)} new words to wordlist")
+
+ def save_wordlist(self, path: str) -> None:
+ """
+ Save current wordlist to file.
+
+ Args:
+ path: Path to save the wordlist
+ """
+ if self._wordlist is None:
+ self.load_wordlist()
+
+ try:
+ with open(path, 'w', encoding='utf-8') as f:
+ f.write("# Subdomain Wordlist\n")
+ f.write("# Generated by USFX\n\n")
+ for word in self._wordlist:
+ f.write(f"{word}\n")
+ logger.info(f"Saved {len(self._wordlist)} words to {path}")
+ except Exception as e:
+ logger.error(f"Error saving wordlist: {e}")
|