From 8263f5cfa9150235ddd041fa05b9796ed9af11bc Mon Sep 17 00:00:00 2001 From: overcuriousity Date: Tue, 9 Sep 2025 13:55:05 +0200 Subject: [PATCH] flask app --- README.md | 107 ++- requirements.txt | 4 + src/__init__.py | 20 + src/certificate_checker.py | 122 +++ src/config.py | 45 ++ src/data_structures.py | 142 ++++ src/dns_resolver.py | 143 ++++ src/main.py | 107 +++ src/reconnaissance.py | 191 +++++ src/report_generator.py | 111 +++ src/shodan_client.py | 105 +++ src/tld_fetcher.py | 68 ++ src/virustotal_client.py | 100 +++ src/web_app.py | 139 ++++ static/script.js | 280 +++++++ static/style.css | 258 +++++++ templates/index.html | 80 ++ tlds_cache.txt | 1440 ++++++++++++++++++++++++++++++++++++ 18 files changed, 3461 insertions(+), 1 deletion(-) create mode 100644 requirements.txt create mode 100644 src/__init__.py create mode 100644 src/certificate_checker.py create mode 100644 src/config.py create mode 100644 src/data_structures.py create mode 100644 src/dns_resolver.py create mode 100644 src/main.py create mode 100644 src/reconnaissance.py create mode 100644 src/report_generator.py create mode 100644 src/shodan_client.py create mode 100644 src/tld_fetcher.py create mode 100644 src/virustotal_client.py create mode 100644 src/web_app.py create mode 100644 static/script.js create mode 100644 static/style.css create mode 100644 templates/index.html create mode 100644 tlds_cache.txt diff --git a/README.md b/README.md index f412655..15156bd 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,107 @@ -# dnsrecon +# DNS Reconnaissance Tool +A comprehensive DNS reconnaissance tool designed for investigators to gather intelligence on hostnames and IP addresses through multiple data sources. + +## Features + +- **DNS Resolution**: Query multiple DNS servers (1.1.1.1, 8.8.8.8, 9.9.9.9) +- **TLD Expansion**: Automatically try all IANA TLDs for hostname-only inputs +- **Certificate Transparency**: Query crt.sh for SSL certificate information +- **Recursive Discovery**: Automatically discover and analyze subdomains +- **External Intelligence**: Optional Shodan and VirusTotal integration +- **Multiple Interfaces**: Both CLI and web interface available +- **Comprehensive Reports**: JSON and text output formats + +## Installation + +```bash +# Clone or create the project structure +mkdir dns-recon-tool && cd dns-recon-tool + +# Install dependencies +pip install -r requirements.txt +``` + +## Usage + +### Command Line Interface + +```bash +# Basic domain scan +python -m src.main example.com + +# Try all TLDs for hostname +python -m src.main example + +# With API keys and custom depth +python -m src.main example.com --shodan-key YOUR_KEY --virustotal-key YOUR_KEY --max-depth 3 + +# Save reports +python -m src.main example.com --output results + +# JSON only output +python -m src.main example.com --json-only +``` + +### Web Interface + +```bash +# Start web server +python -m src.main --web + +# Custom port +python -m src.main --web --port 8080 +``` + +Then open http://localhost:5000 in your browser. + +## Configuration + +The tool uses the following default settings: +- DNS Servers: 1.1.1.1, 8.8.8.8, 9.9.9.9 +- Max Recursion Depth: 2 +- Rate Limits: DNS (10/s), crt.sh (2/s), Shodan (0.5/s), VirusTotal (0.25/s) + +## API Keys + +For enhanced reconnaissance, obtain API keys from: +- [Shodan](https://shodan.io) - Port scanning and service detection +- [VirusTotal](https://virustotal.com) - Security analysis and reputation + +## Output + +The tool generates two types of reports: + +### JSON Report +Complete machine-readable data including: +- All discovered hostnames and IPs +- DNS records by type +- Certificate information +- External service results +- Metadata and timing + +### Text Report +Human-readable summary with: +- Executive summary +- Hostnames by discovery depth +- IP address analysis +- DNS record details +- Certificate analysis +- Security findings + +## Architecture + +``` +src/ +├── main.py # CLI entry point +├── web_app.py # Flask web interface +├── config.py # Configuration management +├── data_structures.py # Data models +├── dns_resolver.py # DNS functionality +├── certificate_checker.py # crt.sh integration +├── shodan_client.py # Shodan API +├── virustotal_client.py # VirusTotal API +├── tld_fetcher.py # IANA TLD handling +├── reconnaissance.py # Main logic +└── report_generator.py # Report generation +``` \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c61e49a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +requests>=2.31.0 +flask>=2.3.0 +dnspython>=2.4.0 +click>=8.1.0 \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..defb0ae --- /dev/null +++ b/src/__init__.py @@ -0,0 +1,20 @@ +# File: src/__init__.py +"""DNS Reconnaissance Tool Package.""" + +__version__ = "1.0.0" +__author__ = "DNS Recon Tool" +__email__ = "" +__description__ = "A comprehensive DNS reconnaissance tool for investigators" + +from .main import main +from .config import Config +from .reconnaissance import ReconnaissanceEngine +from .data_structures import ReconData + +__all__ = [ + 'main', + 'Config', + 'ReconnaissanceEngine', + 'ReconData' +] + diff --git a/src/certificate_checker.py b/src/certificate_checker.py new file mode 100644 index 0000000..0cb2766 --- /dev/null +++ b/src/certificate_checker.py @@ -0,0 +1,122 @@ +# File: src/certificate_checker.py +"""Certificate transparency log checker using crt.sh.""" + +import requests +import json +import time +from datetime import datetime +from typing import List, Optional, Set +from .data_structures import Certificate +from .config import Config + +class CertificateChecker: + """Check certificates using crt.sh.""" + + CRT_SH_URL = "https://crt.sh/" + + def __init__(self, config: Config): + self.config = config + self.last_request = 0 + + def _rate_limit(self): + """Apply rate limiting for crt.sh.""" + now = time.time() + time_since_last = now - self.last_request + min_interval = 1.0 / self.config.CRT_SH_RATE_LIMIT + + if time_since_last < min_interval: + time.sleep(min_interval - time_since_last) + + self.last_request = time.time() + + def get_certificates(self, domain: str) -> List[Certificate]: + """Get certificates for a domain from crt.sh.""" + certificates = [] + + # Query for the domain + certificates.extend(self._query_crt_sh(domain)) + + # Also query for wildcard certificates + certificates.extend(self._query_crt_sh(f"%.{domain}")) + + # Remove duplicates based on certificate ID + unique_certs = {cert.id: cert for cert in certificates} + return list(unique_certs.values()) + + def _query_crt_sh(self, query: str) -> List[Certificate]: + """Query crt.sh API with retry logic.""" + certificates = [] + self._rate_limit() + + max_retries = 3 + for attempt in range(max_retries): + try: + params = { + 'q': query, + 'output': 'json' + } + + response = requests.get( + self.CRT_SH_URL, + params=params, + timeout=self.config.HTTP_TIMEOUT + ) + + if response.status_code == 200: + data = response.json() + for cert_data in data: + try: + certificate = Certificate( + id=cert_data.get('id'), + issuer=cert_data.get('issuer_name', ''), + subject=cert_data.get('name_value', ''), + not_before=datetime.fromisoformat( + cert_data.get('not_before', '').replace('Z', '+00:00') + ), + not_after=datetime.fromisoformat( + cert_data.get('not_after', '').replace('Z', '+00:00') + ), + is_wildcard='*.' in cert_data.get('name_value', '') + ) + certificates.append(certificate) + except (ValueError, TypeError): + continue # Skip malformed certificate data + return certificates # Success, exit retry loop + + except requests.exceptions.RequestException as e: + print(f"Error querying crt.sh for {query} (attempt {attempt+1}/{max_retries}): {e}") + if attempt < max_retries - 1: + time.sleep(2) # Wait 2 seconds before retrying + continue + + return certificates # Return what we have after all retries + + def extract_subdomains_from_certificates(self, certificates: List[Certificate]) -> Set[str]: + """Extract subdomains from certificate subjects.""" + subdomains = set() + + for cert in certificates: + # Parse subject field for domain names + subjects = cert.subject.split('\n') + for subject in subjects: + subject = subject.strip() + + # Skip wildcard domains for recursion + if not subject.startswith('*.'): + if self._is_valid_domain(subject): + subdomains.add(subject.lower()) + + return subdomains + + def _is_valid_domain(self, domain: str) -> bool: + """Basic domain validation.""" + if not domain or '.' not in domain: + return False + + # Remove common prefixes + domain = domain.lower() + if domain.startswith('www.'): + domain = domain[4:] + + # Basic validation + return len(domain) > 0 and len(domain) < 255 \ No newline at end of file diff --git a/src/config.py b/src/config.py new file mode 100644 index 0000000..aa6fce5 --- /dev/null +++ b/src/config.py @@ -0,0 +1,45 @@ +# File: src/config.py +"""Configuration settings for the reconnaissance tool.""" + +import os +from dataclasses import dataclass +from typing import List, Optional + +@dataclass +class Config: + """Configuration class for the reconnaissance tool.""" + + # DNS servers to query + DNS_SERVERS: List[str] = None + + # API keys + shodan_key: Optional[str] = None + virustotal_key: Optional[str] = None + + # Rate limiting (requests per second) + DNS_RATE_LIMIT: float = 10.0 + CRT_SH_RATE_LIMIT: float = 2.0 + SHODAN_RATE_LIMIT: float = 0.5 + VIRUSTOTAL_RATE_LIMIT: float = 0.25 + + # Recursive depth + max_depth: int = 2 + + # Timeouts + DNS_TIMEOUT: int = 5 + HTTP_TIMEOUT: int = 20 + + def __post_init__(self): + if self.DNS_SERVERS is None: + self.DNS_SERVERS = ['1.1.1.1', '8.8.8.8', '9.9.9.9'] + + @classmethod + def from_args(cls, shodan_key: Optional[str] = None, + virustotal_key: Optional[str] = None, + max_depth: int = 2) -> 'Config': + """Create config from command line arguments.""" + return cls( + shodan_key=shodan_key, + virustotal_key=virustotal_key, + max_depth=max_depth + ) diff --git a/src/data_structures.py b/src/data_structures.py new file mode 100644 index 0000000..b953ffb --- /dev/null +++ b/src/data_structures.py @@ -0,0 +1,142 @@ +# File: src/data_structures.py +"""Data structures for storing reconnaissance results.""" + +from dataclasses import dataclass, field +from typing import Dict, List, Set, Optional, Any +from datetime import datetime +import json + +@dataclass +class DNSRecord: + """DNS record information.""" + record_type: str + value: str + ttl: Optional[int] = None + +@dataclass +class Certificate: + """Certificate information from crt.sh.""" + id: int + issuer: str + subject: str + not_before: datetime + not_after: datetime + is_wildcard: bool = False + +@dataclass +class ShodanResult: + """Shodan scan result.""" + ip: str + ports: List[int] + services: Dict[str, Any] + organization: Optional[str] = None + country: Optional[str] = None + +@dataclass +class VirusTotalResult: + """VirusTotal scan result.""" + resource: str # IP or domain + positives: int + total: int + scan_date: datetime + permalink: str + +@dataclass +class ReconData: + """Main data structure for reconnaissance results.""" + + # Core data + hostnames: Set[str] = field(default_factory=set) + ip_addresses: Set[str] = field(default_factory=set) + + # DNS information + dns_records: Dict[str, List[DNSRecord]] = field(default_factory=dict) + reverse_dns: Dict[str, str] = field(default_factory=dict) + + # Certificate information + certificates: Dict[str, List[Certificate]] = field(default_factory=dict) + + # External service results + shodan_results: Dict[str, ShodanResult] = field(default_factory=dict) + virustotal_results: Dict[str, VirusTotalResult] = field(default_factory=dict) + + # Metadata + start_time: datetime = field(default_factory=datetime.now) + end_time: Optional[datetime] = None + depth_map: Dict[str, int] = field(default_factory=dict) # Track recursion depth + + def add_hostname(self, hostname: str, depth: int = 0) -> None: + """Add a hostname to the dataset.""" + self.hostnames.add(hostname.lower()) + self.depth_map[hostname.lower()] = depth + + def add_ip_address(self, ip: str) -> None: + """Add an IP address to the dataset.""" + self.ip_addresses.add(ip) + + def add_dns_record(self, hostname: str, record: DNSRecord) -> None: + """Add a DNS record for a hostname.""" + hostname = hostname.lower() + if hostname not in self.dns_records: + self.dns_records[hostname] = [] + self.dns_records[hostname].append(record) + + def get_new_subdomains(self, max_depth: int) -> Set[str]: + """Get subdomains that haven't been processed yet and are within depth limit.""" + new_domains = set() + for hostname in self.hostnames: + if (hostname not in self.dns_records and + self.depth_map.get(hostname, 0) < max_depth): + new_domains.add(hostname) + return new_domains + + def to_dict(self) -> dict: + """Export data as a serializable dictionary.""" + return { + 'hostnames': list(self.hostnames), + 'ip_addresses': list(self.ip_addresses), + 'dns_records': { + host: [{'type': r.record_type, 'value': r.value, 'ttl': r.ttl} + for r in records] + for host, records in self.dns_records.items() + }, + 'reverse_dns': self.reverse_dns, + 'certificates': { + host: [{ + 'id': cert.id, + 'issuer': cert.issuer, + 'subject': cert.subject, + 'not_before': cert.not_before.isoformat(), + 'not_after': cert.not_after.isoformat(), + 'is_wildcard': cert.is_wildcard + } for cert in certs] + for host, certs in self.certificates.items() + }, + 'shodan_results': { + ip: { + 'ports': result.ports, + 'services': result.services, + 'organization': result.organization, + 'country': result.country + } for ip, result in self.shodan_results.items() + }, + 'virustotal_results': { + resource: { + 'positives': result.positives, + 'total': result.total, + 'scan_date': result.scan_date.isoformat(), + 'permalink': result.permalink + } for resource, result in self.virustotal_results.items() + }, + 'metadata': { + 'start_time': self.start_time.isoformat(), + 'end_time': self.end_time.isoformat() if self.end_time else None, + 'total_hostnames': len(self.hostnames), + 'total_ips': len(self.ip_addresses) + } + } + + def to_json(self) -> str: + """Export data as JSON.""" + # Now uses the to_dict method + return json.dumps(self.to_dict(), indent=2, default=str) \ No newline at end of file diff --git a/src/dns_resolver.py b/src/dns_resolver.py new file mode 100644 index 0000000..adddd0c --- /dev/null +++ b/src/dns_resolver.py @@ -0,0 +1,143 @@ +# File: src/dns_resolver.py +"""DNS resolution functionality.""" + +import dns.resolver +import dns.reversename +import dns.query +import dns.zone +from typing import List, Dict, Optional, Set +import socket +import time +from .data_structures import DNSRecord, ReconData +from .config import Config + +class DNSResolver: + """DNS resolution and record lookup.""" + + # All DNS record types to query + RECORD_TYPES = [ + 'A', 'AAAA', 'MX', 'NS', 'TXT', 'CNAME', 'SOA', 'PTR', + 'SRV', 'CAA', 'DNSKEY', 'DS', 'RRSIG', 'NSEC', 'NSEC3' + ] + + def __init__(self, config: Config): + self.config = config + self.last_request = 0 + + def _rate_limit(self): + """Apply rate limiting.""" + now = time.time() + time_since_last = now - self.last_request + min_interval = 1.0 / self.config.DNS_RATE_LIMIT + + if time_since_last < min_interval: + time.sleep(min_interval - time_since_last) + + self.last_request = time.time() + + def resolve_hostname(self, hostname: str) -> List[str]: + """Resolve hostname to IP addresses.""" + ips = [] + + for dns_server in self.config.DNS_SERVERS: + self._rate_limit() + resolver = dns.resolver.Resolver() + resolver.nameservers = [dns_server] + resolver.timeout = self.config.DNS_TIMEOUT + + try: + # Try A records + answers = resolver.resolve(hostname, 'A') + for answer in answers: + ips.append(str(answer)) + except Exception: + pass + + try: + # Try AAAA records + answers = resolver.resolve(hostname, 'AAAA') + for answer in answers: + ips.append(str(answer)) + except Exception: + pass + + return list(set(ips)) # Remove duplicates + + def get_all_dns_records(self, hostname: str) -> List[DNSRecord]: + """Get all DNS records for a hostname.""" + records = [] + + for record_type in self.RECORD_TYPES: + for dns_server in self.config.DNS_SERVERS: + self._rate_limit() + resolver = dns.resolver.Resolver() + resolver.nameservers = [dns_server] + resolver.timeout = self.config.DNS_TIMEOUT + + try: + answers = resolver.resolve(hostname, record_type) + for answer in answers: + records.append(DNSRecord( + record_type=record_type, + value=str(answer), + ttl=answers.ttl + )) + except Exception: + continue + + return records + + def reverse_dns_lookup(self, ip: str) -> Optional[str]: + """Perform reverse DNS lookup.""" + try: + self._rate_limit() + return socket.gethostbyaddr(ip)[0] + except Exception: + return None + + def extract_subdomains_from_dns(self, records: List[DNSRecord]) -> Set[str]: + """Extract potential subdomains from DNS records.""" + subdomains = set() + + for record in records: + value = record.value.lower() + + # Extract from CNAME, NS, and correctly from MX records + if record.record_type == 'MX': + # MX record values are like: "10 mail.example.com." + # We need to extract the hostname part. + parts = value.split() + if len(parts) == 2: + hostname = parts[1].rstrip('.') + if self._is_valid_hostname(hostname): + subdomains.add(hostname) + elif record.record_type in ['CNAME', 'NS']: + # These records are just the hostname + hostname = value.rstrip('.') + if self._is_valid_hostname(hostname): + subdomains.add(hostname) + + # Extract from TXT records (sometimes contain domain references) + elif record.record_type == 'TXT': + # Look for domain-like strings in TXT records + parts = value.split() + for part in parts: + if '.' in part and not part.startswith('http'): + clean_part = part.strip('",\'()[]{}') + if self._is_valid_hostname(clean_part): + subdomains.add(clean_part) + + return subdomains + + def _is_valid_hostname(self, hostname: str) -> bool: + """Basic hostname validation.""" + if not hostname or len(hostname) > 255: + return False + + # Must contain at least one dot + if '.' not in hostname: + return False + + # Basic character check + allowed_chars = set('abcdefghijklmnopqrstuvwxyz0123456789.-') + return all(c in allowed_chars for c in hostname.lower()) \ No newline at end of file diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..29eea5e --- /dev/null +++ b/src/main.py @@ -0,0 +1,107 @@ +# File: src/main.py +"""Main CLI interface for the reconnaissance tool.""" + +import click +import json +import sys +from pathlib import Path +from .config import Config +from .reconnaissance import ReconnaissanceEngine +from .report_generator import ReportGenerator +from .web_app import create_app + +@click.command() +@click.argument('target', required=False) +@click.option('--web', is_flag=True, help='Start web interface instead of CLI') +@click.option('--shodan-key', help='Shodan API key') +@click.option('--virustotal-key', help='VirusTotal API key') +@click.option('--max-depth', default=2, help='Maximum recursion depth (default: 2)') +@click.option('--output', '-o', help='Output file prefix (will create .json and .txt files)') +@click.option('--json-only', is_flag=True, help='Only output JSON') +@click.option('--text-only', is_flag=True, help='Only output text report') +@click.option('--port', default=5000, help='Port for web interface (default: 5000)') +def main(target, web, shodan_key, virustotal_key, max_depth, output, json_only, text_only, port): + """DNS Reconnaissance Tool + + Examples: + recon example.com # Scan example.com + recon example # Try example.* for all TLDs + recon example.com --max-depth 3 # Deeper recursion + recon --web # Start web interface + """ + + if web: + # Start web interface + app = create_app(Config.from_args(shodan_key, virustotal_key, max_depth)) + app.run(host='0.0.0.0', port=port, debug=True) + return + + if not target: + click.echo("Error: TARGET is required for CLI mode. Use --web for web interface.") + sys.exit(1) + + # Create configuration + config = Config.from_args(shodan_key, virustotal_key, max_depth) + + # Initialize reconnaissance engine + engine = ReconnaissanceEngine(config) + + # Set up progress callback + def progress_callback(message, percentage=None): + if percentage: + click.echo(f"[{percentage:3d}%] {message}") + else: + click.echo(f" {message}") + + engine.set_progress_callback(progress_callback) + + # Run reconnaissance + click.echo(f"Starting reconnaissance for: {target}") + click.echo(f"Max recursion depth: {max_depth}") + if shodan_key: + click.echo("✓ Shodan integration enabled") + if virustotal_key: + click.echo("✓ VirusTotal integration enabled") + click.echo("") + + try: + data = engine.run_reconnaissance(target) + + # Generate reports + report_gen = ReportGenerator(data) + + if output: + # Save to files + if not text_only: + json_file = f"{output}.json" + with open(json_file, 'w') as f: + f.write(data.to_json()) + click.echo(f"JSON report saved to: {json_file}") + + if not json_only: + text_file = f"{output}.txt" + with open(text_file, 'w') as f: + f.write(report_gen.generate_text_report()) + click.echo(f"Text report saved to: {text_file}") + + else: + # Output to stdout + if json_only: + click.echo(data.to_json()) + elif text_only: + click.echo(report_gen.generate_text_report()) + else: + # Default: show text report + click.echo(report_gen.generate_text_report()) + click.echo(f"\nTo get JSON output, use: --json-only") + click.echo(f"To save reports, use: --output filename") + + except KeyboardInterrupt: + click.echo("\nReconnaissance interrupted by user.") + sys.exit(1) + except Exception as e: + click.echo(f"Error during reconnaissance: {e}") + sys.exit(1) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/src/reconnaissance.py b/src/reconnaissance.py new file mode 100644 index 0000000..fa96ce0 --- /dev/null +++ b/src/reconnaissance.py @@ -0,0 +1,191 @@ +# File: src/reconnaissance.py +"""Main reconnaissance logic.""" + +import threading +import concurrent.futures +from datetime import datetime +from typing import Set, List, Optional +from .data_structures import ReconData +from .config import Config +from .dns_resolver import DNSResolver +from .certificate_checker import CertificateChecker +from .shodan_client import ShodanClient +from .virustotal_client import VirusTotalClient +from .tld_fetcher import TLDFetcher + +class ReconnaissanceEngine: + """Main reconnaissance engine.""" + + def __init__(self, config: Config): + self.config = config + # self.data = ReconData() # <-- REMOVED FROM HERE + + # Initialize clients + self.dns_resolver = DNSResolver(config) + self.cert_checker = CertificateChecker(config) + self.tld_fetcher = TLDFetcher() + + # Optional clients + self.shodan_client = None + if config.shodan_key: + self.shodan_client = ShodanClient(config.shodan_key, config) + + self.virustotal_client = None + if config.virustotal_key: + self.virustotal_client = VirusTotalClient(config.virustotal_key, config) + + # Progress tracking + self.progress_callback = None + self._lock = threading.Lock() + + def set_progress_callback(self, callback): + """Set callback for progress updates.""" + self.progress_callback = callback + + def _update_progress(self, message: str, percentage: int = None): + """Update progress if callback is set.""" + if self.progress_callback: + self.progress_callback(message, percentage) + + def run_reconnaissance(self, target: str) -> ReconData: + """Run full reconnaissance on target.""" + self.data = ReconData() + self.data.start_time = datetime.now() + + try: + # Determine if target is hostname.tld or just hostname + if '.' in target: + self._update_progress(f"Starting reconnaissance for {target}", 0) + self.data.add_hostname(target, 0) + initial_targets = {target} + else: + self._update_progress(f"Expanding {target} to all TLDs", 5) + initial_targets = self._expand_hostname_to_tlds(target) + + self._update_progress("Resolving initial targets", 10) + + # Process all targets recursively + self._process_targets_recursively(initial_targets) + + # Final external lookups + self._update_progress("Performing external service lookups", 90) + self._perform_external_lookups() + + self._update_progress("Reconnaissance complete", 100) + + finally: + self.data.end_time = datetime.now() + + return self.data + + def _expand_hostname_to_tlds(self, hostname: str) -> Set[str]: + """Expand hostname to all possible TLDs.""" + tlds = self.tld_fetcher.get_tlds() + targets = set() + + for i, tld in enumerate(tlds): + full_hostname = f"{hostname}.{tld}" + + # Quick check if domain resolves + ips = self.dns_resolver.resolve_hostname(full_hostname) + if ips: + self.data.add_hostname(full_hostname, 0) + targets.add(full_hostname) + for ip in ips: + self.data.add_ip_address(ip) + + # Progress update every 100 TLDs + if i % 100 == 0: + progress = 5 + int((i / len(tlds)) * 5) # 5-10% range + self._update_progress(f"Checked {i}/{len(tlds)} TLDs, found {len(targets)} valid domains", progress) + + return targets + + def _process_targets_recursively(self, targets: Set[str]): + """Process targets with recursive subdomain discovery.""" + current_depth = 0 + + while current_depth <= self.config.max_depth and targets: + self._update_progress(f"Processing depth {current_depth}", 15 + (current_depth * 25)) + + new_targets = set() + + for target in targets: + # DNS resolution and record gathering + self._process_single_target(target, current_depth) + + # Extract new subdomains + if current_depth < self.config.max_depth: + new_subdomains = self._extract_new_subdomains(target) + for subdomain in new_subdomains: + self.data.add_hostname(subdomain, current_depth + 1) + new_targets.add(subdomain) + + targets = new_targets + current_depth += 1 + + def _process_single_target(self, hostname: str, depth: int): + """Process a single target hostname.""" + # Get all DNS records + dns_records = self.dns_resolver.get_all_dns_records(hostname) + for record in dns_records: + self.data.add_dns_record(hostname, record) + + # Extract IP addresses from A and AAAA records + if record.record_type in ['A', 'AAAA']: + self.data.add_ip_address(record.value) + + # Get certificates + certificates = self.cert_checker.get_certificates(hostname) + if certificates: + self.data.certificates[hostname] = certificates + + def _extract_new_subdomains(self, hostname: str) -> Set[str]: + """Extract new subdomains from DNS records and certificates.""" + new_subdomains = set() + + # From DNS records + if hostname in self.data.dns_records: + dns_subdomains = self.dns_resolver.extract_subdomains_from_dns( + self.data.dns_records[hostname] + ) + new_subdomains.update(dns_subdomains) + + # From certificates + if hostname in self.data.certificates: + cert_subdomains = self.cert_checker.extract_subdomains_from_certificates( + self.data.certificates[hostname] + ) + new_subdomains.update(cert_subdomains) + + # Filter out already known hostnames + return new_subdomains - self.data.hostnames + + def _perform_external_lookups(self): + """Perform Shodan and VirusTotal lookups.""" + # Reverse DNS for all IPs + for ip in self.data.ip_addresses: + reverse = self.dns_resolver.reverse_dns_lookup(ip) + if reverse: + self.data.reverse_dns[ip] = reverse + + # Shodan lookups + if self.shodan_client: + for ip in self.data.ip_addresses: + result = self.shodan_client.lookup_ip(ip) + if result: + self.data.shodan_results[ip] = result + + # VirusTotal lookups + if self.virustotal_client: + # Check IPs + for ip in self.data.ip_addresses: + result = self.virustotal_client.lookup_ip(ip) + if result: + self.data.virustotal_results[ip] = result + + # Check domains + for hostname in self.data.hostnames: + result = self.virustotal_client.lookup_domain(hostname) + if result: + self.data.virustotal_results[hostname] = result diff --git a/src/report_generator.py b/src/report_generator.py new file mode 100644 index 0000000..f9f8315 --- /dev/null +++ b/src/report_generator.py @@ -0,0 +1,111 @@ +# File: src/report_generator.py +"""Generate reports from reconnaissance data.""" + +from datetime import datetime +from typing import Dict, Any +from .data_structures import ReconData + +class ReportGenerator: + """Generate various report formats.""" + + def __init__(self, data: ReconData): + self.data = data + + def generate_text_report(self) -> str: + """Generate comprehensive text report.""" + report = [] + + # Header + report.append("="*80) + report.append("DNS RECONNAISSANCE REPORT") + report.append("="*80) + report.append(f"Start Time: {self.data.start_time}") + report.append(f"End Time: {self.data.end_time}") + if self.data.end_time: + duration = self.data.end_time - self.data.start_time + report.append(f"Duration: {duration}") + report.append("") + + # Summary + report.append("SUMMARY") + report.append("-" * 40) + report.append(f"Total Hostnames Discovered: {len(self.data.hostnames)}") + report.append(f"Total IP Addresses Found: {len(self.data.ip_addresses)}") + report.append(f"Total DNS Records: {sum(len(records) for records in self.data.dns_records.values())}") + report.append(f"Total Certificates Found: {sum(len(certs) for certs in self.data.certificates.values())}") + report.append("") + + # Hostnames by depth + report.append("HOSTNAMES BY DISCOVERY DEPTH") + report.append("-" * 40) + depth_groups = {} + for hostname, depth in self.data.depth_map.items(): + if depth not in depth_groups: + depth_groups[depth] = [] + depth_groups[depth].append(hostname) + + for depth in sorted(depth_groups.keys()): + report.append(f"Depth {depth}: {len(depth_groups[depth])} hostnames") + for hostname in sorted(depth_groups[depth]): + report.append(f" - {hostname}") + report.append("") + + # IP Addresses + report.append("IP ADDRESSES") + report.append("-" * 40) + for ip in sorted(self.data.ip_addresses): + report.append(f"{ip}") + if ip in self.data.reverse_dns: + report.append(f" Reverse DNS: {self.data.reverse_dns[ip]}") + if ip in self.data.shodan_results: + shodan = self.data.shodan_results[ip] + report.append(f" Shodan: {len(shodan.ports)} open ports") + if shodan.organization: + report.append(f" Organization: {shodan.organization}") + if shodan.country: + report.append(f" Country: {shodan.country}") + report.append("") + + # DNS Records + report.append("DNS RECORDS") + report.append("-" * 40) + for hostname in sorted(self.data.dns_records.keys()): + report.append(f"{hostname}:") + records_by_type = {} + for record in self.data.dns_records[hostname]: + if record.record_type not in records_by_type: + records_by_type[record.record_type] = [] + records_by_type[record.record_type].append(record) + + for record_type in sorted(records_by_type.keys()): + report.append(f" {record_type}:") + for record in records_by_type[record_type]: + report.append(f" {record.value}") + report.append("") + + # Certificates + if self.data.certificates: + report.append("CERTIFICATES") + report.append("-" * 40) + for hostname in sorted(self.data.certificates.keys()): + report.append(f"{hostname}:") + for cert in self.data.certificates[hostname]: + report.append(f" Certificate ID: {cert.id}") + report.append(f" Issuer: {cert.issuer}") + report.append(f" Valid From: {cert.not_before}") + report.append(f" Valid Until: {cert.not_after}") + if cert.is_wildcard: + report.append(f" Type: Wildcard Certificate") + report.append("") + + # Security Analysis + if self.data.virustotal_results: + report.append("SECURITY ANALYSIS") + report.append("-" * 40) + for resource, result in self.data.virustotal_results.items(): + if result.positives > 0: + report.append(f"⚠️ {resource}: {result.positives}/{result.total} detections") + report.append(f" Scan Date: {result.scan_date}") + report.append(f" Report: {result.permalink}") + + return "\n".join(report) \ No newline at end of file diff --git a/src/shodan_client.py b/src/shodan_client.py new file mode 100644 index 0000000..ec5fdb5 --- /dev/null +++ b/src/shodan_client.py @@ -0,0 +1,105 @@ +# File: src/shodan_client.py +"""Shodan API integration.""" + +import requests +import time +from typing import Optional, Dict, Any, List +from .data_structures import ShodanResult +from .config import Config + +class ShodanClient: + """Shodan API client.""" + + BASE_URL = "https://api.shodan.io" + + def __init__(self, api_key: str, config: Config): + self.api_key = api_key + self.config = config + self.last_request = 0 + + def _rate_limit(self): + """Apply rate limiting for Shodan.""" + now = time.time() + time_since_last = now - self.last_request + min_interval = 1.0 / self.config.SHODAN_RATE_LIMIT + + if time_since_last < min_interval: + time.sleep(min_interval - time_since_last) + + self.last_request = time.time() + + def lookup_ip(self, ip: str) -> Optional[ShodanResult]: + """Lookup IP address information.""" + self._rate_limit() + + try: + url = f"{self.BASE_URL}/shodan/host/{ip}" + params = {'key': self.api_key} + + response = requests.get(url, params=params, timeout=self.config.HTTP_TIMEOUT) + + if response.status_code == 200: + data = response.json() + + ports = [] + services = {} + + for service in data.get('data', []): + port = service.get('port') + if port: + ports.append(port) + services[str(port)] = { + 'product': service.get('product', ''), + 'version': service.get('version', ''), + 'banner': service.get('data', '').strip()[:200] # Limit banner size + } + + return ShodanResult( + ip=ip, + ports=sorted(list(set(ports))), + services=services, + organization=data.get('org'), + country=data.get('country_name') + ) + + elif response.status_code == 404: + return None # IP not found in Shodan + else: + print(f"Shodan API error for {ip}: {response.status_code}") + return None + + except Exception as e: + print(f"Error querying Shodan for {ip}: {e}") + return None + + def search_domain(self, domain: str) -> List[str]: + """Search for IPs associated with a domain.""" + self._rate_limit() + + try: + url = f"{self.BASE_URL}/shodan/host/search" + params = { + 'key': self.api_key, + 'query': f'hostname:{domain}', + 'limit': 100 + } + + response = requests.get(url, params=params, timeout=self.config.HTTP_TIMEOUT) + + if response.status_code == 200: + data = response.json() + ips = [] + + for match in data.get('matches', []): + ip = match.get('ip_str') + if ip: + ips.append(ip) + + return list(set(ips)) + else: + print(f"Shodan search error for {domain}: {response.status_code}") + return [] + + except Exception as e: + print(f"Error searching Shodan for {domain}: {e}") + return [] \ No newline at end of file diff --git a/src/tld_fetcher.py b/src/tld_fetcher.py new file mode 100644 index 0000000..7f13e58 --- /dev/null +++ b/src/tld_fetcher.py @@ -0,0 +1,68 @@ +# File: src/tld_fetcher.py +"""Fetch and cache IANA TLD list.""" + +import requests +from typing import List, Set +import os +import time + +class TLDFetcher: + """Fetches and caches IANA TLD list.""" + + IANA_TLD_URL = "https://data.iana.org/TLD/tlds-alpha-by-domain.txt" + CACHE_FILE = "tlds_cache.txt" + CACHE_DURATION = 86400 # 24 hours in seconds + + def __init__(self): + self._tlds: Optional[Set[str]] = None + + def get_tlds(self) -> Set[str]: + """Get list of TLDs, using cache if available.""" + if self._tlds is None: + self._tlds = self._load_tlds() + return self._tlds + + def _load_tlds(self) -> Set[str]: + """Load TLDs from cache or fetch from IANA.""" + if self._is_cache_valid(): + return self._load_from_cache() + return self._fetch_and_cache() + + def _is_cache_valid(self) -> bool: + """Check if cache file exists and is recent.""" + if not os.path.exists(self.CACHE_FILE): + return False + + cache_age = time.time() - os.path.getmtime(self.CACHE_FILE) + return cache_age < self.CACHE_DURATION + + def _load_from_cache(self) -> Set[str]: + """Load TLDs from cache file.""" + with open(self.CACHE_FILE, 'r') as f: + return set(line.strip().lower() for line in f if not line.startswith('#')) + + def _fetch_and_cache(self) -> Set[str]: + """Fetch TLDs from IANA and cache them.""" + try: + response = requests.get(self.IANA_TLD_URL, timeout=10) + response.raise_for_status() + + tlds = set() + for line in response.text.split('\n'): + line = line.strip().lower() + if line and not line.startswith('#'): + tlds.add(line) + + # Cache the results + with open(self.CACHE_FILE, 'w') as f: + f.write(response.text) + + return tlds + + except Exception as e: + print(f"Failed to fetch TLD list: {e}") + # Return a minimal set if fetch fails + return { + 'com', 'org', 'net', 'edu', 'gov', 'mil', 'int', + 'co.uk', 'org.uk', 'ac.uk', 'de', 'fr', 'it', 'nl', 'be' + } \ No newline at end of file diff --git a/src/virustotal_client.py b/src/virustotal_client.py new file mode 100644 index 0000000..40d084c --- /dev/null +++ b/src/virustotal_client.py @@ -0,0 +1,100 @@ +# File: src/virustotal_client.py +"""VirusTotal API integration.""" + +import requests +import time +from datetime import datetime +from typing import Optional +from .data_structures import VirusTotalResult +from .config import Config + +class VirusTotalClient: + """VirusTotal API client.""" + + BASE_URL = "https://www.virustotal.com/vtapi/v2" + + def __init__(self, api_key: str, config: Config): + self.api_key = api_key + self.config = config + self.last_request = 0 + + def _rate_limit(self): + """Apply rate limiting for VirusTotal.""" + now = time.time() + time_since_last = now - self.last_request + min_interval = 1.0 / self.config.VIRUSTOTAL_RATE_LIMIT + + if time_since_last < min_interval: + time.sleep(min_interval - time_since_last) + + self.last_request = time.time() + + def lookup_ip(self, ip: str) -> Optional[VirusTotalResult]: + """Lookup IP address reputation.""" + self._rate_limit() + + try: + url = f"{self.BASE_URL}/ip-address/report" + params = { + 'apikey': self.api_key, + 'ip': ip + } + + response = requests.get(url, params=params, timeout=self.config.HTTP_TIMEOUT) + + if response.status_code == 200: + data = response.json() + + if data.get('response_code') == 1: + return VirusTotalResult( + resource=ip, + positives=data.get('detected_urls', []) and len([ + url for url in data.get('detected_urls', []) + if url.get('positives', 0) > 0 + ]) or 0, + total=len(data.get('detected_urls', [])), + scan_date=datetime.fromisoformat( + data.get('scan_date', datetime.now().isoformat()) + ) if data.get('scan_date') else datetime.now(), + permalink=data.get('permalink', '') + ) + + except Exception as e: + print(f"Error querying VirusTotal for {ip}: {e}") + + return None + + def lookup_domain(self, domain: str) -> Optional[VirusTotalResult]: + """Lookup domain reputation.""" + self._rate_limit() + + try: + url = f"{self.BASE_URL}/domain/report" + params = { + 'apikey': self.api_key, + 'domain': domain + } + + response = requests.get(url, params=params, timeout=self.config.HTTP_TIMEOUT) + + if response.status_code == 200: + data = response.json() + + if data.get('response_code') == 1: + return VirusTotalResult( + resource=domain, + positives=data.get('detected_urls', []) and len([ + url for url in data.get('detected_urls', []) + if url.get('positives', 0) > 0 + ]) or 0, + total=len(data.get('detected_urls', [])), + scan_date=datetime.fromisoformat( + data.get('scan_date', datetime.now().isoformat()) + ) if data.get('scan_date') else datetime.now(), + permalink=data.get('permalink', '') + ) + + except Exception as e: + print(f"Error querying VirusTotal for {domain}: {e}") + + return None diff --git a/src/web_app.py b/src/web_app.py new file mode 100644 index 0000000..6b39297 --- /dev/null +++ b/src/web_app.py @@ -0,0 +1,139 @@ +# File: src/web_app.py +"""Flask web application for reconnaissance tool.""" + +from flask import Flask, render_template, request, jsonify, send_from_directory +import threading +import time +from .config import Config +from .reconnaissance import ReconnaissanceEngine +from .report_generator import ReportGenerator + +# Global variables for tracking ongoing scans +active_scans = {} +scan_lock = threading.Lock() + +def create_app(config: Config): + """Create Flask application.""" + app = Flask(__name__, + template_folder='../templates', + static_folder='../static') + + app.config['SECRET_KEY'] = 'recon-tool-secret-key' + + @app.route('/') + def index(): + """Main page.""" + return render_template('index.html') + + @app.route('/api/scan', methods=['POST']) + def start_scan(): + """Start a new reconnaissance scan.""" + data = request.get_json() + target = data.get('target') + scan_config = Config.from_args( + shodan_key=data.get('shodan_key'), + virustotal_key=data.get('virustotal_key'), + max_depth=data.get('max_depth', 2) + ) + + if not target: + return jsonify({'error': 'Target is required'}), 400 + + # Generate scan ID + scan_id = f"{target}_{int(time.time())}" + + # Initialize scan data + with scan_lock: + active_scans[scan_id] = { + 'status': 'starting', + 'progress': 0, + 'message': 'Initializing...', + 'data': None, + 'error': None + } + + # Start reconnaissance in background thread + thread = threading.Thread( + target=run_reconnaissance_background, + args=(scan_id, target, scan_config) + ) + thread.daemon = True + thread.start() + + return jsonify({'scan_id': scan_id}) + + @app.route('/api/scan//status') + def get_scan_status(scan_id): + """Get scan status and progress.""" + with scan_lock: + if scan_id not in active_scans: + return jsonify({'error': 'Scan not found'}), 404 + + scan_data = active_scans[scan_id].copy() + + # Convert ReconData object to a dict to make it JSON serializable + if scan_data.get('data'): + scan_data['data'] = scan_data['data'].to_dict() + + return jsonify(scan_data) + + @app.route('/api/scan//report') + def get_scan_report(scan_id): + """Get scan report.""" + with scan_lock: + if scan_id not in active_scans: + return jsonify({'error': 'Scan not found'}), 404 + + scan_data = active_scans[scan_id] + + if scan_data['status'] != 'completed' or not scan_data['data']: + return jsonify({'error': 'Scan not completed'}), 400 + + # Generate report + report_gen = ReportGenerator(scan_data['data']) + + return jsonify({ + 'json_report': scan_data['data'].to_dict(), # Use to_dict for a clean JSON object + 'text_report': report_gen.generate_text_report() + }) + + return app + +def run_reconnaissance_background(scan_id: str, target: str, config: Config): + """Run reconnaissance in background thread.""" + + def update_progress(message: str, percentage: int = None): + """Update scan progress.""" + with scan_lock: + if scan_id in active_scans: + active_scans[scan_id]['message'] = message + if percentage is not None: + active_scans[scan_id]['progress'] = percentage + + try: + # Initialize engine + engine = ReconnaissanceEngine(config) + engine.set_progress_callback(update_progress) + + # Update status + with scan_lock: + active_scans[scan_id]['status'] = 'running' + + # Run reconnaissance + data = engine.run_reconnaissance(target) + + # Update with results + with scan_lock: + active_scans[scan_id]['status'] = 'completed' + active_scans[scan_id]['progress'] = 100 + active_scans[scan_id]['message'] = 'Reconnaissance completed' + active_scans[scan_id]['data'] = data + + except Exception as e: + # Handle errors + with scan_lock: + active_scans[scan_id]['status'] = 'error' + active_scans[scan_id]['error'] = str(e) + active_scans[scan_id]['message'] = f'Error: {str(e)}' + + diff --git a/static/script.js b/static/script.js new file mode 100644 index 0000000..453b233 --- /dev/null +++ b/static/script.js @@ -0,0 +1,280 @@ +// DNS Reconnaissance Tool - Frontend JavaScript + +class ReconTool { + constructor() { + this.currentScanId = null; + this.pollInterval = null; + this.currentReport = null; + this.init(); + } + + init() { + this.bindEvents(); + } + + bindEvents() { + // Start scan button + document.getElementById('startScan').addEventListener('click', () => { + this.startScan(); + }); + + // New scan button + document.getElementById('newScan').addEventListener('click', () => { + this.resetToForm(); + }); + + // Report view toggles + document.getElementById('showJson').addEventListener('click', () => { + this.showReport('json'); + }); + + document.getElementById('showText').addEventListener('click', () => { + this.showReport('text'); + }); + + // Download buttons + document.getElementById('downloadJson').addEventListener('click', () => { + this.downloadReport('json'); + }); + + document.getElementById('downloadText').addEventListener('click', () => { + this.downloadReport('text'); + }); + + // Enter key in target field + document.getElementById('target').addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + this.startScan(); + } + }); + } + + async startScan() { + const target = document.getElementById('target').value.trim(); + + if (!target) { + alert('Please enter a target domain or hostname'); + return; + } + + const scanData = { + target: target, + max_depth: parseInt(document.getElementById('maxDepth').value), + shodan_key: document.getElementById('shodanKey').value.trim() || null, + virustotal_key: document.getElementById('virustotalKey').value.trim() || null + }; + + try { + // Show progress section + this.showProgressSection(); + this.updateProgress(0, 'Starting scan...'); + + const response = await fetch('/api/scan', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(scanData) + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + + if (result.error) { + throw new Error(result.error); + } + + this.currentScanId = result.scan_id; + this.startPolling(); + + } catch (error) { + this.showError(`Failed to start scan: ${error.message}`); + } + } + + startPolling() { + // Poll every 2 seconds for updates + this.pollInterval = setInterval(() => { + this.checkScanStatus(); + }, 2000); + + // Also check immediately + this.checkScanStatus(); + } + + async checkScanStatus() { + if (!this.currentScanId) { + return; + } + + try { + const response = await fetch(`/api/scan/${this.currentScanId}/status`); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const status = await response.json(); + + if (status.error) { + throw new Error(status.error); + } + + // Update progress + this.updateProgress(status.progress, status.message); + + // Check if completed + if (status.status === 'completed') { + this.stopPolling(); + await this.loadScanReport(); + } else if (status.status === 'error') { + this.stopPolling(); + throw new Error(status.error || 'Scan failed'); + } + + } catch (error) { + this.stopPolling(); + this.showError(`Error checking scan status: ${error.message}`); + } + } + + async loadScanReport() { + try { + const response = await fetch(`/api/scan/${this.currentScanId}/report`); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const report = await response.json(); + + if (report.error) { + throw new Error(report.error); + } + + this.currentReport = report; + this.showResultsSection(); + this.showReport('text'); // Default to text view + + } catch (error) { + this.showError(`Error loading report: ${error.message}`); + } + } + + stopPolling() { + if (this.pollInterval) { + clearInterval(this.pollInterval); + this.pollInterval = null; + } + } + + showProgressSection() { + document.getElementById('scanForm').style.display = 'none'; + document.getElementById('progressSection').style.display = 'block'; + document.getElementById('resultsSection').style.display = 'none'; + } + + showResultsSection() { + document.getElementById('scanForm').style.display = 'none'; + document.getElementById('progressSection').style.display = 'none'; + document.getElementById('resultsSection').style.display = 'block'; + } + + resetToForm() { + this.stopPolling(); + this.currentScanId = null; + this.currentReport = null; + + document.getElementById('scanForm').style.display = 'block'; + document.getElementById('progressSection').style.display = 'none'; + document.getElementById('resultsSection').style.display = 'none'; + + // Clear form + document.getElementById('target').value = ''; + document.getElementById('shodanKey').value = ''; + document.getElementById('virustotalKey').value = ''; + document.getElementById('maxDepth').value = '2'; + } + + updateProgress(percentage, message) { + const progressFill = document.getElementById('progressFill'); + const progressMessage = document.getElementById('progressMessage'); + + progressFill.style.width = `${percentage || 0}%`; + progressMessage.textContent = message || 'Processing...'; + } + + showError(message) { + // Update progress section to show error + this.updateProgress(0, `Error: ${message}`); + + // Also alert the user + alert(`Error: ${message}`); + } + + showReport(type) { + if (!this.currentReport) { + return; + } + + const reportContent = document.getElementById('reportContent'); + const showJsonBtn = document.getElementById('showJson'); + const showTextBtn = document.getElementById('showText'); + + if (type === 'json') { + // Show JSON report + try { + const jsonData = JSON.parse(this.currentReport.json_report); + reportContent.textContent = JSON.stringify(jsonData, null, 2); + } catch (e) { + reportContent.textContent = this.currentReport.json_report; + } + + showJsonBtn.classList.add('active'); + showTextBtn.classList.remove('active'); + } else { + // Show text report + reportContent.textContent = this.currentReport.text_report; + + showTextBtn.classList.add('active'); + showJsonBtn.classList.remove('active'); + } + } + + downloadReport(type) { + if (!this.currentReport) { + return; + } + + let content, filename, mimeType; + + if (type === 'json') { + content = this.currentReport.json_report; + filename = `recon-report-${this.currentScanId}.json`; + mimeType = 'application/json'; + } else { + content = this.currentReport.text_report; + filename = `recon-report-${this.currentScanId}.txt`; + mimeType = 'text/plain'; + } + + // Create download link + const blob = new Blob([content], { type: mimeType }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + } +} + +// Initialize the application when DOM is loaded +document.addEventListener('DOMContentLoaded', () => { + new ReconTool(); +}); \ No newline at end of file diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..4dc777a --- /dev/null +++ b/static/style.css @@ -0,0 +1,258 @@ +/* + ███████╗██████╗ ███████╗ ██████╗████████╗ ██████╗ ██████╗ ██╗ ██╗███████╗ + ██╔════╝██╔══██╗██╔════╝██╔═══██╗╚══██╔══╝ ██╔═══██╗██╔═══██╗╚██╗██╔╝██╔════╝ + ███████╗██████╔╝█████╗ ██║ ██║ ██║ ██║ ██║██║ ██║ ╚███╔╝ ███████╗ + ╚════██║██╔══██╗██╔══╝ ██║ ██║ ██║ ██║ ██║██║ ██║ ██╔██╗ ╚════██║ + ███████║██║ ██║███████╗╚██████╔╝ ██║ ╚██████╔╝╚██████╔╝██╔╝ ██╗███████║ + ╚══════╝╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ + + TACTICAL THEME - DNS RECONNAISSANCE INTERFACE + STYLE OVERRIDE +*/ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Roboto Mono', 'Lucida Console', Monaco, monospace; + line-height: 1.6; + color: #c7c7c7; /* Light grey for readability */ + /* Dark, textured background for a gritty feel */ + background-color: #1a1a1a; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3E%3Cpath fill='%23333333' fill-opacity='0.4' d='M1 3h1v1H1V3zm2-2h1v1H3V1z'%3E%3C/path%3E%3C/svg%3E"); + min-height: 100vh; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 20px; +} + +header { + text-align: center; + color: #e0e0e0; + margin-bottom: 40px; + border-bottom: 1px solid #444; + padding-bottom: 20px; +} + +header h1 { + font-family: 'Special Elite', 'Courier New', monospace; /* Stencil / Typewriter font */ + font-size: 2.8rem; + color: #00ff41; /* Night-vision green */ + text-shadow: 0 0 5px rgba(0, 255, 65, 0.5); + margin-bottom: 10px; + letter-spacing: 2px; +} + +header p { + font-size: 1.1rem; + color: #a0a0a0; +} + +.scan-form, .progress-section, .results-section { + background: #2a2a2a; /* Dark charcoal */ + border-radius: 4px; /* Sharper edges */ + border: 1px solid #444; + box-shadow: inset 0 0 15px rgba(0,0,0,0.5); + padding: 30px; + margin-bottom: 25px; +} + +.scan-form h2, .progress-section h2, .results-section h2 { + margin-bottom: 20px; + color: #e0e0e0; + border-bottom: 1px solid #555; + padding-bottom: 10px; + text-transform: uppercase; /* Military style */ + letter-spacing: 1px; +} + +.form-group { + margin-bottom: 20px; +} + +.form-group label { + display: block; + margin-bottom: 8px; + font-weight: 600; + color: #b0b0b0; + text-transform: uppercase; + font-size: 0.9rem; +} + +.form-group input, .form-group select { + width: 100%; + padding: 12px; + background: #1a1a1a; + border: 1px solid #555; + border-radius: 2px; + font-size: 16px; + color: #00ff41; /* Green text for input fields */ + font-family: 'Roboto Mono', monospace; + transition: all 0.2s ease-in-out; +} + +.form-group input:focus, .form-group select:focus { + outline: none; + border-color: #ff9900; /* Amber focus color */ + box-shadow: 0 0 5px rgba(255, 153, 0, 0.5); +} + +.api-keys { + background: rgba(0,0,0,0.3); + padding: 20px; + border-radius: 4px; + border: 1px solid #444; + margin: 20px 0; +} + +.api-keys h3 { + margin-bottom: 15px; + color: #c7c7c7; +} + +.btn-primary, .btn-secondary { + padding: 12px 24px; + border: 1px solid #666; + border-radius: 2px; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease-in-out; + margin-right: 10px; + margin-bottom: 10px; + text-transform: uppercase; + letter-spacing: 1px; +} + +.btn-primary { + background: #2c5c34; /* Dark military green */ + color: #e0e0e0; + border-color: #3b7b46; +} + +.btn-primary:hover { + background: #3b7b46; /* Lighter green on hover */ + color: #fff; + border-color: #4cae5c; +} + +.btn-secondary { + background: #4a4a4a; /* Dark grey */ + color: #c7c7c7; + border-color: #666; +} + +.btn-secondary:hover { + background: #5a5a5a; +} + +.btn-secondary.active { + background: #6a4f2a; /* Amber/Brown for active state */ + color: #fff; + border-color: #ff9900; +} + +.progress-bar { + width: 100%; + height: 20px; + background: #1a1a1a; + border: 1px solid #555; + border-radius: 2px; + overflow: hidden; + margin-bottom: 15px; + padding: 2px; +} + +.progress-fill { + height: 100%; + background: #ff9900; /* Solid amber progress fill */ + width: 0%; + transition: width 0.3s ease; + border-radius: 0; +} + +#progressMessage { + font-weight: 500; + color: #a0a0a0; + margin-bottom: 20px; +} + +.scan-controls { + text-align: center; +} + +.results-controls { + margin-bottom: 20px; + text-align: center; +} + +.report-container { + background: #0a0a0a; /* Near-black terminal background */ + border-radius: 4px; + border: 1px solid #333; + padding: 20px; + max-height: 600px; + overflow-y: auto; + box-shadow: inset 0 0 10px #000; +} + +#reportContent { + color: #00ff41; /* Classic terminal green */ + font-family: 'Courier New', monospace; + font-size: 14px; + line-height: 1.4; + white-space: pre-wrap; + word-wrap: break-word; +} + +/* Responsive design adjustments */ +@media (max-width: 768px) { + .container { + padding: 10px; + } + + header h1 { + font-size: 2.2rem; + } + + .scan-form, .progress-section, .results-section { + padding: 20px; + } + + .btn-primary, .btn-secondary { + width: 100%; + margin-right: 0; + } + + .results-controls { + display: flex; + flex-wrap: wrap; + justify-content: center; + } + + .results-controls button { + flex: 1; + min-width: 120px; + } +} + +/* Tactical loading spinner */ +.loading { + display: inline-block; + width: 20px; + height: 20px; + border: 3px solid rgba(199, 199, 199, 0.3); + border-radius: 50%; + border-top-color: #00ff41; /* Night-vision green spinner */ + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..eff9007 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,80 @@ + + + + + + DNS Reconnaissance Tool + + + +
+
+

🔍 DNS Reconnaissance Tool

+

Comprehensive domain and IP intelligence gathering

+
+ +
+

Start New Scan

+ +
+ + +
+ +
+ + +
+ +
+

Optional API Keys

+
+ + +
+ +
+ + +
+
+ + +
+ + + + +
+ + + + \ No newline at end of file diff --git a/tlds_cache.txt b/tlds_cache.txt new file mode 100644 index 0000000..0c9dc64 --- /dev/null +++ b/tlds_cache.txt @@ -0,0 +1,1440 @@ +# Version 2025090900, Last Updated Tue Sep 9 07:07:01 2025 UTC +AAA +AARP +ABB +ABBOTT +ABBVIE +ABC +ABLE +ABOGADO +ABUDHABI +AC +ACADEMY +ACCENTURE +ACCOUNTANT +ACCOUNTANTS +ACO +ACTOR +AD +ADS +ADULT +AE +AEG +AERO +AETNA +AF +AFL +AFRICA +AG +AGAKHAN +AGENCY +AI +AIG +AIRBUS +AIRFORCE +AIRTEL +AKDN +AL +ALIBABA +ALIPAY +ALLFINANZ +ALLSTATE +ALLY +ALSACE +ALSTOM +AM +AMAZON +AMERICANEXPRESS +AMERICANFAMILY +AMEX +AMFAM +AMICA +AMSTERDAM +ANALYTICS +ANDROID +ANQUAN +ANZ +AO +AOL +APARTMENTS +APP +APPLE +AQ +AQUARELLE +AR +ARAB +ARAMCO +ARCHI +ARMY +ARPA +ART +ARTE +AS +ASDA +ASIA +ASSOCIATES +AT +ATHLETA +ATTORNEY +AU +AUCTION +AUDI +AUDIBLE +AUDIO +AUSPOST +AUTHOR +AUTO +AUTOS +AW +AWS +AX +AXA +AZ +AZURE +BA +BABY +BAIDU +BANAMEX +BAND +BANK +BAR +BARCELONA +BARCLAYCARD +BARCLAYS +BAREFOOT +BARGAINS +BASEBALL +BASKETBALL +BAUHAUS +BAYERN +BB +BBC +BBT +BBVA +BCG +BCN +BD +BE +BEATS +BEAUTY +BEER +BERLIN +BEST +BESTBUY +BET +BF +BG +BH +BHARTI +BI +BIBLE +BID +BIKE +BING +BINGO +BIO +BIZ +BJ +BLACK +BLACKFRIDAY +BLOCKBUSTER +BLOG +BLOOMBERG +BLUE +BM +BMS +BMW +BN +BNPPARIBAS +BO +BOATS +BOEHRINGER +BOFA +BOM +BOND +BOO +BOOK +BOOKING +BOSCH +BOSTIK +BOSTON +BOT +BOUTIQUE +BOX +BR +BRADESCO +BRIDGESTONE +BROADWAY +BROKER +BROTHER +BRUSSELS +BS +BT +BUILD +BUILDERS +BUSINESS +BUY +BUZZ +BV +BW +BY +BZ +BZH +CA +CAB +CAFE +CAL +CALL +CALVINKLEIN +CAM +CAMERA +CAMP +CANON +CAPETOWN +CAPITAL +CAPITALONE +CAR +CARAVAN +CARDS +CARE +CAREER +CAREERS +CARS +CASA +CASE +CASH +CASINO +CAT +CATERING +CATHOLIC +CBA +CBN +CBRE +CC +CD +CENTER +CEO +CERN +CF +CFA +CFD +CG +CH +CHANEL +CHANNEL +CHARITY +CHASE +CHAT +CHEAP +CHINTAI +CHRISTMAS +CHROME +CHURCH +CI +CIPRIANI +CIRCLE +CISCO +CITADEL +CITI +CITIC +CITY +CK +CL +CLAIMS +CLEANING +CLICK +CLINIC +CLINIQUE +CLOTHING +CLOUD +CLUB +CLUBMED +CM +CN +CO +COACH +CODES +COFFEE +COLLEGE +COLOGNE +COM +COMMBANK +COMMUNITY +COMPANY +COMPARE +COMPUTER +COMSEC +CONDOS +CONSTRUCTION +CONSULTING +CONTACT +CONTRACTORS +COOKING +COOL +COOP +CORSICA +COUNTRY +COUPON +COUPONS +COURSES +CPA +CR +CREDIT +CREDITCARD +CREDITUNION +CRICKET +CROWN +CRS +CRUISE +CRUISES +CU +CUISINELLA +CV +CW +CX +CY +CYMRU +CYOU +CZ +DAD +DANCE +DATA +DATE +DATING +DATSUN +DAY +DCLK +DDS +DE +DEAL +DEALER +DEALS +DEGREE +DELIVERY +DELL +DELOITTE +DELTA +DEMOCRAT +DENTAL +DENTIST +DESI +DESIGN +DEV +DHL +DIAMONDS +DIET +DIGITAL +DIRECT +DIRECTORY +DISCOUNT +DISCOVER +DISH +DIY +DJ +DK +DM +DNP +DO +DOCS +DOCTOR +DOG +DOMAINS +DOT +DOWNLOAD +DRIVE +DTV +DUBAI +DUNLOP +DUPONT +DURBAN +DVAG +DVR +DZ +EARTH +EAT +EC +ECO +EDEKA +EDU +EDUCATION +EE +EG +EMAIL +EMERCK +ENERGY +ENGINEER +ENGINEERING +ENTERPRISES +EPSON +EQUIPMENT +ER +ERICSSON +ERNI +ES +ESQ +ESTATE +ET +EU +EUROVISION +EUS +EVENTS +EXCHANGE +EXPERT +EXPOSED +EXPRESS +EXTRASPACE +FAGE +FAIL +FAIRWINDS +FAITH +FAMILY +FAN +FANS +FARM +FARMERS +FASHION +FAST +FEDEX +FEEDBACK +FERRARI +FERRERO +FI +FIDELITY +FIDO +FILM +FINAL +FINANCE +FINANCIAL +FIRE +FIRESTONE +FIRMDALE +FISH +FISHING +FIT +FITNESS +FJ +FK +FLICKR +FLIGHTS +FLIR +FLORIST +FLOWERS +FLY +FM +FO +FOO +FOOD +FOOTBALL +FORD +FOREX +FORSALE +FORUM +FOUNDATION +FOX +FR +FREE +FRESENIUS +FRL +FROGANS +FRONTIER +FTR +FUJITSU +FUN +FUND +FURNITURE +FUTBOL +FYI +GA +GAL +GALLERY +GALLO +GALLUP +GAME +GAMES +GAP +GARDEN +GAY +GB +GBIZ +GD +GDN +GE +GEA +GENT +GENTING +GEORGE +GF +GG +GGEE +GH +GI +GIFT +GIFTS +GIVES +GIVING +GL +GLASS +GLE +GLOBAL +GLOBO +GM +GMAIL +GMBH +GMO +GMX +GN +GODADDY +GOLD +GOLDPOINT +GOLF +GOO +GOODYEAR +GOOG +GOOGLE +GOP +GOT +GOV +GP +GQ +GR +GRAINGER +GRAPHICS +GRATIS +GREEN +GRIPE +GROCERY +GROUP +GS +GT +GU +GUCCI +GUGE +GUIDE +GUITARS +GURU +GW +GY +HAIR +HAMBURG +HANGOUT +HAUS +HBO +HDFC +HDFCBANK +HEALTH +HEALTHCARE +HELP +HELSINKI +HERE +HERMES +HIPHOP +HISAMITSU +HITACHI +HIV +HK +HKT +HM +HN +HOCKEY +HOLDINGS +HOLIDAY +HOMEDEPOT +HOMEGOODS +HOMES +HOMESENSE +HONDA +HORSE +HOSPITAL +HOST +HOSTING +HOT +HOTELS +HOTMAIL +HOUSE +HOW +HR +HSBC +HT +HU +HUGHES +HYATT +HYUNDAI +IBM +ICBC +ICE +ICU +ID +IE +IEEE +IFM +IKANO +IL +IM +IMAMAT +IMDB +IMMO +IMMOBILIEN +IN +INC +INDUSTRIES +INFINITI +INFO +ING +INK +INSTITUTE +INSURANCE +INSURE +INT +INTERNATIONAL +INTUIT +INVESTMENTS +IO +IPIRANGA +IQ +IR +IRISH +IS +ISMAILI +IST +ISTANBUL +IT +ITAU +ITV +JAGUAR +JAVA +JCB +JE +JEEP +JETZT +JEWELRY +JIO +JLL +JM +JMP +JNJ +JO +JOBS +JOBURG +JOT +JOY +JP +JPMORGAN +JPRS +JUEGOS +JUNIPER +KAUFEN +KDDI +KE +KERRYHOTELS +KERRYPROPERTIES +KFH +KG +KH +KI +KIA +KIDS +KIM +KINDLE +KITCHEN +KIWI +KM +KN +KOELN +KOMATSU +KOSHER +KP +KPMG +KPN +KR +KRD +KRED +KUOKGROUP +KW +KY +KYOTO +KZ +LA +LACAIXA +LAMBORGHINI +LAMER +LAND +LANDROVER +LANXESS +LASALLE +LAT +LATINO +LATROBE +LAW +LAWYER +LB +LC +LDS +LEASE +LECLERC +LEFRAK +LEGAL +LEGO +LEXUS +LGBT +LI +LIDL +LIFE +LIFEINSURANCE +LIFESTYLE +LIGHTING +LIKE +LILLY +LIMITED +LIMO +LINCOLN +LINK +LIVE +LIVING +LK +LLC +LLP +LOAN +LOANS +LOCKER +LOCUS +LOL +LONDON +LOTTE +LOTTO +LOVE +LPL +LPLFINANCIAL +LR +LS +LT +LTD +LTDA +LU +LUNDBECK +LUXE +LUXURY +LV +LY +MA +MADRID +MAIF +MAISON +MAKEUP +MAN +MANAGEMENT +MANGO +MAP +MARKET +MARKETING +MARKETS +MARRIOTT +MARSHALLS +MATTEL +MBA +MC +MCKINSEY +MD +ME +MED +MEDIA +MEET +MELBOURNE +MEME +MEMORIAL +MEN +MENU +MERCKMSD +MG +MH +MIAMI +MICROSOFT +MIL +MINI +MINT +MIT +MITSUBISHI +MK +ML +MLB +MLS +MM +MMA +MN +MO +MOBI +MOBILE +MODA +MOE +MOI +MOM +MONASH +MONEY +MONSTER +MORMON +MORTGAGE +MOSCOW +MOTO +MOTORCYCLES +MOV +MOVIE +MP +MQ +MR +MS +MSD +MT +MTN +MTR +MU +MUSEUM +MUSIC +MV +MW +MX +MY +MZ +NA +NAB +NAGOYA +NAME +NAVY +NBA +NC +NE +NEC +NET +NETBANK +NETFLIX +NETWORK +NEUSTAR +NEW +NEWS +NEXT +NEXTDIRECT +NEXUS +NF +NFL +NG +NGO +NHK +NI +NICO +NIKE +NIKON +NINJA +NISSAN +NISSAY +NL +NO +NOKIA +NORTON +NOW +NOWRUZ +NOWTV +NP +NR +NRA +NRW +NTT +NU +NYC +NZ +OBI +OBSERVER +OFFICE +OKINAWA +OLAYAN +OLAYANGROUP +OLLO +OM +OMEGA +ONE +ONG +ONL +ONLINE +OOO +OPEN +ORACLE +ORANGE +ORG +ORGANIC +ORIGINS +OSAKA +OTSUKA +OTT +OVH +PA +PAGE +PANASONIC +PARIS +PARS +PARTNERS +PARTS +PARTY +PAY +PCCW +PE +PET +PF +PFIZER +PG +PH +PHARMACY +PHD +PHILIPS +PHONE +PHOTO +PHOTOGRAPHY +PHOTOS +PHYSIO +PICS +PICTET +PICTURES +PID +PIN +PING +PINK +PIONEER +PIZZA +PK +PL +PLACE +PLAY +PLAYSTATION +PLUMBING +PLUS +PM +PN +PNC +POHL +POKER +POLITIE +PORN +POST +PR +PRAXI +PRESS +PRIME +PRO +PROD +PRODUCTIONS +PROF +PROGRESSIVE +PROMO +PROPERTIES +PROPERTY +PROTECTION +PRU +PRUDENTIAL +PS +PT +PUB +PW +PWC +PY +QA +QPON +QUEBEC +QUEST +RACING +RADIO +RE +READ +REALESTATE +REALTOR +REALTY +RECIPES +RED +REDUMBRELLA +REHAB +REISE +REISEN +REIT +RELIANCE +REN +RENT +RENTALS +REPAIR +REPORT +REPUBLICAN +REST +RESTAURANT +REVIEW +REVIEWS +REXROTH +RICH +RICHARDLI +RICOH +RIL +RIO +RIP +RO +ROCKS +RODEO +ROGERS +ROOM +RS +RSVP +RU +RUGBY +RUHR +RUN +RW +RWE +RYUKYU +SA +SAARLAND +SAFE +SAFETY +SAKURA +SALE +SALON +SAMSCLUB +SAMSUNG +SANDVIK +SANDVIKCOROMANT +SANOFI +SAP +SARL +SAS +SAVE +SAXO +SB +SBI +SBS +SC +SCB +SCHAEFFLER +SCHMIDT +SCHOLARSHIPS +SCHOOL +SCHULE +SCHWARZ +SCIENCE +SCOT +SD +SE +SEARCH +SEAT +SECURE +SECURITY +SEEK +SELECT +SENER +SERVICES +SEVEN +SEW +SEX +SEXY +SFR +SG +SH +SHANGRILA +SHARP +SHELL +SHIA +SHIKSHA +SHOES +SHOP +SHOPPING +SHOUJI +SHOW +SI +SILK +SINA +SINGLES +SITE +SJ +SK +SKI +SKIN +SKY +SKYPE +SL +SLING +SM +SMART +SMILE +SN +SNCF +SO +SOCCER +SOCIAL +SOFTBANK +SOFTWARE +SOHU +SOLAR +SOLUTIONS +SONG +SONY +SOY +SPA +SPACE +SPORT +SPOT +SR +SRL +SS +ST +STADA +STAPLES +STAR +STATEBANK +STATEFARM +STC +STCGROUP +STOCKHOLM +STORAGE +STORE +STREAM +STUDIO +STUDY +STYLE +SU +SUCKS +SUPPLIES +SUPPLY +SUPPORT +SURF +SURGERY +SUZUKI +SV +SWATCH +SWISS +SX +SY +SYDNEY +SYSTEMS +SZ +TAB +TAIPEI +TALK +TAOBAO +TARGET +TATAMOTORS +TATAR +TATTOO +TAX +TAXI +TC +TCI +TD +TDK +TEAM +TECH +TECHNOLOGY +TEL +TEMASEK +TENNIS +TEVA +TF +TG +TH +THD +THEATER +THEATRE +TIAA +TICKETS +TIENDA +TIPS +TIRES +TIROL +TJ +TJMAXX +TJX +TK +TKMAXX +TL +TM +TMALL +TN +TO +TODAY +TOKYO +TOOLS +TOP +TORAY +TOSHIBA +TOTAL +TOURS +TOWN +TOYOTA +TOYS +TR +TRADE +TRADING +TRAINING +TRAVEL +TRAVELERS +TRAVELERSINSURANCE +TRUST +TRV +TT +TUBE +TUI +TUNES +TUSHU +TV +TVS +TW +TZ +UA +UBANK +UBS +UG +UK +UNICOM +UNIVERSITY +UNO +UOL +UPS +US +UY +UZ +VA +VACATIONS +VANA +VANGUARD +VC +VE +VEGAS +VENTURES +VERISIGN +VERSICHERUNG +VET +VG +VI +VIAJES +VIDEO +VIG +VIKING +VILLAS +VIN +VIP +VIRGIN +VISA +VISION +VIVA +VIVO +VLAANDEREN +VN +VODKA +VOLVO +VOTE +VOTING +VOTO +VOYAGE +VU +WALES +WALMART +WALTER +WANG +WANGGOU +WATCH +WATCHES +WEATHER +WEATHERCHANNEL +WEBCAM +WEBER +WEBSITE +WED +WEDDING +WEIBO +WEIR +WF +WHOSWHO +WIEN +WIKI +WILLIAMHILL +WIN +WINDOWS +WINE +WINNERS +WME +WOLTERSKLUWER +WOODSIDE +WORK +WORKS +WORLD +WOW +WS +WTC +WTF +XBOX +XEROX +XIHUAN +XIN +XN--11B4C3D +XN--1CK2E1B +XN--1QQW23A +XN--2SCRJ9C +XN--30RR7Y +XN--3BST00M +XN--3DS443G +XN--3E0B707E +XN--3HCRJ9C +XN--3PXU8K +XN--42C2D9A +XN--45BR5CYL +XN--45BRJ9C +XN--45Q11C +XN--4DBRK0CE +XN--4GBRIM +XN--54B7FTA0CC +XN--55QW42G +XN--55QX5D +XN--5SU34J936BGSG +XN--5TZM5G +XN--6FRZ82G +XN--6QQ986B3XL +XN--80ADXHKS +XN--80AO21A +XN--80AQECDR1A +XN--80ASEHDB +XN--80ASWG +XN--8Y0A063A +XN--90A3AC +XN--90AE +XN--90AIS +XN--9DBQ2A +XN--9ET52U +XN--9KRT00A +XN--B4W605FERD +XN--BCK1B9A5DRE4C +XN--C1AVG +XN--C2BR7G +XN--CCK2B3B +XN--CCKWCXETD +XN--CG4BKI +XN--CLCHC0EA0B2G2A9GCD +XN--CZR694B +XN--CZRS0T +XN--CZRU2D +XN--D1ACJ3B +XN--D1ALF +XN--E1A4C +XN--ECKVDTC9D +XN--EFVY88H +XN--FCT429K +XN--FHBEI +XN--FIQ228C5HS +XN--FIQ64B +XN--FIQS8S +XN--FIQZ9S +XN--FJQ720A +XN--FLW351E +XN--FPCRJ9C3D +XN--FZC2C9E2C +XN--FZYS8D69UVGM +XN--G2XX48C +XN--GCKR3F0F +XN--GECRJ9C +XN--GK3AT1E +XN--H2BREG3EVE +XN--H2BRJ9C +XN--H2BRJ9C8C +XN--HXT814E +XN--I1B6B1A6A2E +XN--IMR513N +XN--IO0A7I +XN--J1AEF +XN--J1AMH +XN--J6W193G +XN--JLQ480N2RG +XN--JVR189M +XN--KCRX77D1X4A +XN--KPRW13D +XN--KPRY57D +XN--KPUT3I +XN--L1ACC +XN--LGBBAT1AD8J +XN--MGB9AWBF +XN--MGBA3A3EJT +XN--MGBA3A4F16A +XN--MGBA7C0BBN0A +XN--MGBAAM7A8H +XN--MGBAB2BD +XN--MGBAH1A3HJKRD +XN--MGBAI9AZGQP6J +XN--MGBAYH7GPA +XN--MGBBH1A +XN--MGBBH1A71E +XN--MGBC0A9AZCG +XN--MGBCA7DZDO +XN--MGBCPQ6GPA1A +XN--MGBERP4A5D4AR +XN--MGBGU82A +XN--MGBI4ECEXP +XN--MGBPL2FH +XN--MGBT3DHD +XN--MGBTX2B +XN--MGBX4CD0AB +XN--MIX891F +XN--MK1BU44C +XN--MXTQ1M +XN--NGBC5AZD +XN--NGBE9E0A +XN--NGBRX +XN--NODE +XN--NQV7F +XN--NQV7FS00EMA +XN--NYQY26A +XN--O3CW4H +XN--OGBPF8FL +XN--OTU796D +XN--P1ACF +XN--P1AI +XN--PGBS0DH +XN--PSSY2U +XN--Q7CE6A +XN--Q9JYB4C +XN--QCKA1PMC +XN--QXA6A +XN--QXAM +XN--RHQV96G +XN--ROVU88B +XN--RVC1E0AM3E +XN--S9BRJ9C +XN--SES554G +XN--T60B56A +XN--TCKWE +XN--TIQ49XQYJ +XN--UNUP4Y +XN--VERMGENSBERATER-CTB +XN--VERMGENSBERATUNG-PWB +XN--VHQUV +XN--VUQ861B +XN--W4R85EL8FHU5DNRA +XN--W4RS40L +XN--WGBH1C +XN--WGBL6A +XN--XHQ521B +XN--XKC2AL3HYE2A +XN--XKC2DL3A5EE0H +XN--Y9A3AQ +XN--YFRO4I67O +XN--YGBI2AMMX +XN--ZFR164B +XXX +XYZ +YACHTS +YAHOO +YAMAXUN +YANDEX +YE +YODOBASHI +YOGA +YOKOHAMA +YOU +YOUTUBE +YT +YUN +ZA +ZAPPOS +ZARA +ZERO +ZIP +ZM +ZONE +ZUERICH +ZW