From 131543d1483448f18c55b8cfcc6e33c682d006dc Mon Sep 17 00:00:00 2001 From: n0mad1k Date: Thu, 30 Apr 2026 16:45:41 -0400 Subject: [PATCH] Fix WEBRUNNER: attack_box format, self-contained CIDR, tools submenu, Tor, teardown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - deploy_webrunner.py: full rewrite following attack_box pattern exactly — auto-generate engagement name when blank, select_provider+gather_provider_config per provider (handles region selection), separate teardown_after_scan (default=yes) from enhanced_opsec, use_tor prompt, bundled YAML defaults, setup_logging+confirm_action+show_naming_relationship - utils/cidr_resolver.py: self-contained RIR delegation file downloader/parser — downloads APNIC/ARIN/RIPE/LACNIC/AFRINIC, caches 24h in ~/.cache/c2itall/cidr/, no geo-scout dep - utils/chunk_utils.py: remove geo-scout _try_load_geo_scout() dependency, use cidr_resolver - modules/webrunner/inputs/countries.yaml: bundled default country list (no external dep) - modules/webrunner/inputs/targets.yaml: bundled default scan targets and ports - deploy.py: move WEBRUNNER from main menu (item 14) to tools submenu (item 10) - configure_node.yml: install tor+proxychains4, start tor service, configure proxychains, wait for circuit establishment when use_tor=true - run_scan.yml: prefix command with proxychains4 when use_tor=true - providers/webrunner.yml: add Play 4 teardown — Linode DELETE + AWS terminate-instances using hostvars instance IDs stored during provisioning, conditional on teardown_after_scan --- deploy.py | 6 +- modules/webrunner/deploy_webrunner.py | 364 ++++++++++++--------- modules/webrunner/inputs/countries.yaml | 36 ++ modules/webrunner/inputs/targets.yaml | 55 ++++ modules/webrunner/tasks/configure_node.yml | 38 +++ modules/webrunner/tasks/run_scan.yml | 3 +- providers/webrunner.yml | 40 +++ utils/chunk_utils.py | 40 +-- utils/cidr_resolver.py | 114 +++++++ 9 files changed, 502 insertions(+), 194 deletions(-) create mode 100644 modules/webrunner/inputs/countries.yaml create mode 100644 modules/webrunner/inputs/targets.yaml create mode 100644 utils/cidr_resolver.py diff --git a/deploy.py b/deploy.py index 902424e..322cd3d 100755 --- a/deploy.py +++ b/deploy.py @@ -47,7 +47,6 @@ def main_menu(): print(f"11) Deploy Privacy Server {COLORS['GREEN']}(Phantom — VPN, DNS, Matrix, etc.){COLORS['RESET']}") print(f"12) Tools & Utilities") print(f"13) Cleanup & Teardown") - print(f"14) WEBRUNNER {COLORS['CYAN']}(Distributed Geo-Targeted Recon){COLORS['RESET']}") print(f"\n99) Exit") choice = input(f"\nSelect an option: ") @@ -75,8 +74,6 @@ def main_menu(): tools_menu() elif choice == "13": cleanup_menu() - elif choice == "14": - deploy_webrunner() elif choice == "99": print(f"\n{COLORS['GREEN']}Exiting C2ingRed. Goodbye!{COLORS['RESET']}") sys.exit(0) @@ -247,6 +244,7 @@ def tools_menu(): print(f"7) Ops Dashboard {COLORS['CYAN']}(Real-Time Engagement Monitor){COLORS['RESET']}") print(f"8) Claude Bot {COLORS['CYAN']}(Matrix-Claude Code Bridge){COLORS['RESET']}") print(f"9) Chaos C2 {COLORS['CYAN']}(Deploy / Manage Chaos teamserver){COLORS['RESET']}") + print(f"10) WEBRUNNER {COLORS['CYAN']}(Distributed Geo-Targeted Recon){COLORS['RESET']}") print(f"99) Return to Main Menu") choice = input(f"\nSelect an option: ") @@ -279,6 +277,8 @@ def tools_menu(): chaos_module = import_module_from_path('deploy_chaos', chaos_module_path) if chaos_module: chaos_module.chaos_menu() + elif choice == "10": + deploy_webrunner() elif choice == "99": return else: diff --git a/modules/webrunner/deploy_webrunner.py b/modules/webrunner/deploy_webrunner.py index 3470dd5..2baeb06 100644 --- a/modules/webrunner/deploy_webrunner.py +++ b/modules/webrunner/deploy_webrunner.py @@ -15,12 +15,13 @@ if _base not in sys.path: sys.path.insert(0, _base) from utils.common import ( - COLORS, clear_screen, print_banner, wait_for_input, - get_public_ip, load_vars_file, + COLORS, clear_screen, print_banner, generate_deployment_id, + setup_logging, get_public_ip, confirm_action, wait_for_input, + load_vars_file, ) +from utils.provider_utils import select_provider, gather_provider_config from utils.ssh_utils import generate_ssh_key -from utils.name_generator import generate_deployment_id -from utils.naming_utils import get_deployment_name_with_options +from utils.naming_utils import get_deployment_name_with_options, show_naming_relationship from utils.deployment_engine import execute_playbook, set_provider_environment from utils.chunk_utils import get_country_cidrs, chunk_cidrs, ip_count from utils.provider_rates import ( @@ -28,7 +29,7 @@ from utils.provider_rates import ( build_estimate_table, fmt_hours, fmt_ip_count, ) -GEO_SCOUT_INPUTS = Path(__file__).parents[3] / 'geo-scout' / 'inputs' +WEBRUNNER_INPUTS = Path(__file__).parent / 'inputs' SUPPORTED_PROVIDERS = ['linode', 'aws', 'flokinet'] PROVIDER_LABELS = {'linode': 'Linode', 'aws': 'AWS', 'flokinet': 'FlokiNET'} @@ -50,7 +51,6 @@ def webrunner_menu(): config = gather_webrunner_parameters() if config: execute_webrunner_deployment(config) - wait_for_input() elif choice == "99": break else: @@ -58,61 +58,12 @@ def webrunner_menu(): wait_for_input() -# ── parameter gathering ─────────────────────────────────────────────────────── - -def _select_countries() -> tuple[list[str], dict[str, list], str]: - print(f"\n{COLORS['BLUE']}Country Selection:{COLORS['RESET']}") - print(f"1) Use geo-scout countries.yaml") - print(f"2) Enter country codes manually") - - choice = input("Select [1]: ").strip() or "1" - exclude_map: dict[str, list] = {} - - if choice == "1": - default_path = str(GEO_SCOUT_INPUTS / 'countries.yaml') - raw = input(f"Path [{default_path}]: ").strip() - path = Path(raw) if raw else GEO_SCOUT_INPUTS / 'countries.yaml' - - if not path.exists(): - print(f"{COLORS['RED']}File not found: {path}{COLORS['RESET']}") - return [], {}, "" - - import yaml - with open(path) as f: - data = yaml.safe_load(f) - - countries = data.get('countries', []) - codes = [c['code'].upper() for c in countries] - for c in countries: - if c.get('exclude_cidrs'): - exclude_map[c['code'].upper()] = c['exclude_cidrs'] - return codes, exclude_map, str(path) - - raw = input("Country codes (comma-separated, e.g. RU,IR,CN): ").strip() - codes = [c.strip().upper() for c in raw.split(',') if c.strip()] - return codes, exclude_map, "manual" - - -def _select_scan_mode() -> str: - print(f"\n{COLORS['BLUE']}Scan Mode:{COLORS['RESET']}") - modes = list(SCAN_MODES.items()) - for i, (key, val) in enumerate(modes, 1): - print(f"{i}) {key:<20} {val['desc']}") - - choice = input("Select [1]: ").strip() or "1" - try: - idx = int(choice) - 1 - if 0 <= idx < len(modes): - return modes[idx][0] - except ValueError: - pass - return 'geo-scout' - +# ── helpers ─────────────────────────────────────────────────────────────────── def _select_providers() -> list[str]: print(f"\n{COLORS['BLUE']}Provider Selection (multi-select):{COLORS['RESET']}") for i, key in enumerate(SUPPORTED_PROVIDERS, 1): - print(f"{i}) {PROVIDER_LABELS[key]}") + print(f" {i}) {PROVIDER_LABELS[key]}") raw = input("Select providers (e.g. 1 or 1,2) [1]: ").strip() or "1" selected = [] @@ -128,9 +79,58 @@ def _select_providers() -> list[str]: return selected or ['linode'] +def _select_countries() -> tuple[list[str], dict[str, list]]: + print(f"\n{COLORS['BLUE']}Country Selection:{COLORS['RESET']}") + print(f"1) Use bundled countries.yaml") + print(f"2) Enter country codes manually") + + choice = input("Select [1]: ").strip() or "1" + exclude_map: dict[str, list] = {} + + if choice == "1": + default_path = str(WEBRUNNER_INPUTS / 'countries.yaml') + raw = input(f"Path [{default_path}]: ").strip() + path = Path(raw) if raw else WEBRUNNER_INPUTS / 'countries.yaml' + + if not path.exists(): + print(f"{COLORS['RED']}File not found: {path}{COLORS['RESET']}") + return [], {} + + import yaml + with open(path) as f: + data = yaml.safe_load(f) + + countries = data.get('countries', []) + codes = [c['code'].upper() for c in countries] + for c in countries: + if c.get('exclude_cidrs'): + exclude_map[c['code'].upper()] = c['exclude_cidrs'] + return codes, exclude_map + + raw = input("Country codes (comma-separated, e.g. RU,IR,CN): ").strip() + codes = [c.strip().upper() for c in raw.split(',') if c.strip()] + return codes, exclude_map + + +def _select_scan_mode() -> str: + print(f"\n{COLORS['BLUE']}Scan Mode:{COLORS['RESET']}") + modes = list(SCAN_MODES.items()) + for i, (key, val) in enumerate(modes, 1): + print(f" {i}) {key:<20} {val['desc']}") + + choice = input("Select [1]: ").strip() or "1" + try: + idx = int(choice) - 1 + if 0 <= idx < len(modes): + return modes[idx][0] + except ValueError: + pass + return 'geo-scout' + + def _get_targets_info(scan_mode: str) -> tuple[str, list[int]]: if scan_mode == 'geo-scout': - default = str(GEO_SCOUT_INPUTS / 'targets.yaml') + default = str(WEBRUNNER_INPUTS / 'targets.yaml') raw = input(f"Path to targets.yaml [{default}]: ").strip() path = raw or default @@ -160,8 +160,8 @@ def _get_targets_info(scan_mode: str) -> tuple[str, list[int]]: def _show_estimate_table(total_ips: int, n_ports: int, providers: list[str], scan_mode: str): rows = build_estimate_table(total_ips, n_ports, providers, scan_mode) - W = COLORS['WHITE'] C = COLORS['CYAN'] + W = COLORS['WHITE'] Y = COLORS['YELLOW'] G = COLORS['GREEN'] R = COLORS['RESET'] @@ -189,54 +189,97 @@ def _show_estimate_table(total_ips: int, n_ports: int, providers: list[str], sca print(f"{Y} * = recommended (Pareto-optimal: speed vs. billing minimum){R}\n") +# ── parameter gathering ─────────────────────────────────────────────────────── + def gather_webrunner_parameters() -> dict | None: clear_screen() print_banner() - print(f"{COLORS['CYAN']}WEBRUNNER — New Scan Deployment{COLORS['RESET']}") - print(f"{COLORS['CYAN']}================================{COLORS['RESET']}\n") + print(f"{COLORS['WHITE']}WEBRUNNER SETUP{COLORS['RESET']}") + print(f"{COLORS['WHITE']}==============={COLORS['RESET']}") - engagement = input("Engagement name: ").strip() - if not engagement: - print(f"{COLORS['RED']}Engagement name required.{COLORS['RESET']}") - wait_for_input() - return None + config: dict = {} - country_codes, exclude_map, _ = _select_countries() - if not country_codes: - print(f"{COLORS['RED']}No countries selected.{COLORS['RESET']}") - wait_for_input() - return None + # Deployment ID + config['deployment_id'] = generate_deployment_id() + print(f"Deployment ID: {COLORS['CYAN']}{config['deployment_id']}{COLORS['RESET']}") - print(f"{COLORS['GREEN']}Countries: {', '.join(country_codes)}{COLORS['RESET']}") + # Engagement name — auto if blank + raw_eng = input(f"Engagement name [{config['deployment_id']}]: ").strip() + config['engagement'] = raw_eng or config['deployment_id'] - scan_mode = _select_scan_mode() + # Provider selection — multi-select, each provider prompts for creds + region providers = _select_providers() + config['providers'] = providers + + for provider in providers: + provider_config = gather_provider_config(provider) + if not provider_config: + return None + config.update(provider_config) + print(f"{COLORS['GREEN']}Providers: {', '.join(PROVIDER_LABELS[p] for p in providers)}{COLORS['RESET']}") - targets_file, ports = _get_targets_info(scan_mode) + # Deployment type + config['deployment_type'] = 'webrunner' + config['webrunner_deployment'] = True - print(f"\n{COLORS['CYAN']}[*] Resolving CIDR data for {len(country_codes)} countries...{COLORS['RESET']}") - cc_cidrs = get_country_cidrs(country_codes, exclude_map) - total_ips = sum( - sum(ip_count(c) for c in cidrs) - for cidrs in cc_cidrs.values() + # Naming + config['webrunner_name'] = get_deployment_name_with_options( + deployment_type='webrunner', + deployment_id=config['deployment_id'], + prefix='wr-', ) + # SSH key + ssh_key_path = generate_ssh_key(config['webrunner_name']) + if not ssh_key_path: + print(f"{COLORS['RED']}Failed to generate SSH key.{COLORS['RESET']}") + return None + config['ssh_key_path'] = f"{ssh_key_path}.pub" + config['ssh_key_name'] = os.path.basename(ssh_key_path) + print(f"{COLORS['GREEN']}SSH key generated: {ssh_key_path}{COLORS['RESET']}") + + # Country / CIDR selection + country_codes, exclude_map = _select_countries() + if not country_codes: + print(f"{COLORS['RED']}No countries selected.{COLORS['RESET']}") + return None + config['country_codes'] = country_codes + print(f"{COLORS['GREEN']}Countries: {', '.join(country_codes)}{COLORS['RESET']}") + + # Scan mode + scan_mode = _select_scan_mode() + config['scan_mode'] = scan_mode + + # Targets / ports + targets_file, ports = _get_targets_info(scan_mode) + config['targets_file'] = targets_file + config['ports'] = ports + config['ports_str'] = ','.join(str(p) for p in ports) + config['masscan_rate'] = SCAN_MODES.get(scan_mode, {}).get('rate', 3000) + + # Resolve CIDR data + print(f"\n{COLORS['CYAN']}[*] Resolving CIDR data for {len(country_codes)} countries...{COLORS['RESET']}") + cc_cidrs = get_country_cidrs(country_codes, exclude_map) + total_ips = sum(sum(ip_count(c) for c in cidrs) for cidrs in cc_cidrs.values()) + if total_ips == 0: - print(f"{COLORS['RED']}No IPs resolved. Ensure geo-scout RIR data is available.{COLORS['RESET']}") - wait_for_input() + print(f"{COLORS['RED']}No IPs resolved. Check your country codes or network connectivity.{COLORS['RESET']}") return None print(f"{COLORS['GREEN']}Total: {fmt_ip_count(total_ips)} IPs across {len(country_codes)} countries{COLORS['RESET']}") + config['total_ips'] = total_ips + # Cost / time estimate _show_estimate_table(total_ips, len(ports), providers, scan_mode) + # Preset selection print(f"{COLORS['BLUE']}Select deployment preset:{COLORS['RESET']}") preset_list = list(PRESETS.items()) for i, (key, val) in enumerate(preset_list, 1): star = " (recommended)" if key == 'balanced' else "" - print(f"{i}) {val['label']}{star} — {val['desc']}") - print(f"4) Custom chunk size") + print(f" {i}) {val['label']}{star} — {val['desc']}") + print(f" 4) Custom chunk size") preset_choice = input("Select [2]: ").strip() or "2" @@ -261,54 +304,10 @@ def gather_webrunner_parameters() -> dict | None: preset_key = 'balanced' chunk_size = PRESETS['balanced']['chunk_size'] - opsec_raw = input(f"\nEnhanced OPSEC mode? (randomize names, auto-teardown) [y/N]: ").strip().lower() - enhanced_opsec = opsec_raw in ['y', 'yes'] + config['preset'] = preset_key + config['chunk_size'] = chunk_size - print(f"\n{COLORS['CYAN']}[*] Generating deployment assets...{COLORS['RESET']}") - deployment_id = generate_deployment_id() - webrunner_name = get_deployment_name_with_options('webrunner', deployment_id, prefix='wr-') - - raw_key_path = generate_ssh_key(webrunner_name) - if not raw_key_path: - print(f"{COLORS['RED']}Failed to generate SSH key.{COLORS['RESET']}") - wait_for_input() - return None - - config: dict = { - 'engagement': engagement, - 'deployment_id': deployment_id, - 'deployment_type': 'webrunner', - 'webrunner_deployment': True, - 'webrunner_name': webrunner_name, - 'ssh_key_path': f"{raw_key_path}.pub", - 'ssh_key_name': os.path.basename(raw_key_path), - 'providers': providers, - 'scan_mode': scan_mode, - 'preset': preset_key, - 'chunk_size': chunk_size, - 'country_codes': country_codes, - 'ports': ports, - 'ports_str': ','.join(str(p) for p in ports), - 'targets_file': targets_file, - 'masscan_rate': SCAN_MODES.get(scan_mode, {}).get('rate', 3000), - 'enhanced_opsec': enhanced_opsec, - 'linode_instance_type': DEFAULT_INSTANCE.get('linode', 'g6-standard-2'), - 'linode_region': 'us-east', - 'aws_instance_type': DEFAULT_INSTANCE.get('aws', 't3.small'), - 'aws_region': 'us-east-1', - } - - operator_ip = get_public_ip() - if operator_ip: - config['operator_ip'] = operator_ip - print(f"{COLORS['GREEN']}Operator IP: {operator_ip}{COLORS['RESET']}") - - for provider in providers: - vars_data = load_vars_file(provider) - if vars_data: - config.update({k: v for k, v in vars_data.items() if k not in config}) - - print(f"{COLORS['CYAN']}[*] Distributing {fmt_ip_count(total_ips)} IPs across nodes...{COLORS['RESET']}") + # Distribute CIDRs into node chunks all_cidrs: list[str] = [] for cc in country_codes: all_cidrs.extend(cc_cidrs.get(cc.upper(), [])) @@ -317,54 +316,109 @@ def gather_webrunner_parameters() -> dict | None: node_chunks = [ { 'idx': i, - 'node_name': f"{webrunner_name}-{i + 1:02d}", + 'node_name': f"{config['webrunner_name']}-{i + 1:02d}", 'provider': providers[i % len(providers)], 'cidrs': chunk['cidrs'], 'ip_count': chunk['ip_count'], } for i, chunk in enumerate(chunks) ] - + config['node_chunks'] = node_chunks print(f"{COLORS['GREEN']}Nodes: {len(node_chunks)} ({preset_key}, {fmt_ip_count(chunk_size)}/node){COLORS['RESET']}") - print(f"\n{COLORS['YELLOW']}{'─' * 50}{COLORS['RESET']}") - print(f" Deployment: {webrunner_name}") - print(f" Nodes: {len(node_chunks)}") - print(f" Providers: {', '.join(PROVIDER_LABELS[p] for p in providers)}") - print(f" Scan mode: {scan_mode}") - print(f" Ports: {', '.join(str(p) for p in ports[:8])}{'...' if len(ports) > 8 else ''}") - print(f" Total IPs: {fmt_ip_count(total_ips)}") - print(f"{COLORS['YELLOW']}{'─' * 50}{COLORS['RESET']}") + # Tor routing + tor_raw = input(f"\nRoute scans through Tor? [y/N]: ").strip().lower() + config['use_tor'] = tor_raw in ['y', 'yes'] + if config['use_tor']: + print(f"{COLORS['YELLOW']} Tor routing enabled — masscan/nmap will use proxychains{COLORS['RESET']}") - confirm = input(f"\nDeploy {len(node_chunks)} nodes? [y/N]: ").strip().lower() - if confirm not in ['y', 'yes']: - print(f"{COLORS['YELLOW']}Deployment cancelled.{COLORS['RESET']}") - return None + # Operator IP + config['operator_ip'] = get_public_ip() + if config['operator_ip']: + print(f"{COLORS['GREEN']}Operator IP: {config['operator_ip']}{COLORS['RESET']}") + + # OPSEC / teardown options + print(f"\n{COLORS['BLUE']}Deployment Options:{COLORS['RESET']}") + + opsec_raw = input(f"Enhanced OPSEC mode? (randomize node names, minimal logging) [y/N]: ").strip().lower() + config['enhanced_opsec'] = opsec_raw in ['y', 'yes'] + + teardown_raw = input(f"Teardown nodes after scan completes? [Y/n]: ").strip().lower() + config['teardown_after_scan'] = teardown_raw not in ['n', 'no'] + + if config['teardown_after_scan']: + print(f"{COLORS['YELLOW']} Nodes will be destroyed automatically when scan completes.{COLORS['RESET']}") + else: + print(f"{COLORS['CYAN']} Nodes will remain running after scan — remember to teardown manually.{COLORS['RESET']}") + + # Merge any provider vars not already set + for provider in providers: + vars_data = load_vars_file(provider) + if vars_data: + config.update({k: v for k, v in vars_data.items() if k not in config}) + + # Apply per-provider instance defaults if not set by gather_provider_config + if 'linode_instance_type' not in config: + config['linode_instance_type'] = DEFAULT_INSTANCE.get('linode', 'g6-standard-2') + if 'aws_instance_type' not in config: + config['aws_instance_type'] = DEFAULT_INSTANCE.get('aws', 't3.small') - config['node_chunks'] = node_chunks - config['total_ips'] = total_ips return config # ── execution ───────────────────────────────────────────────────────────────── def execute_webrunner_deployment(config: dict): - deployment_id = config['deployment_id'] - node_chunks = config.pop('node_chunks') + clear_screen() + print_banner() + print(f"\n{COLORS['GREEN']}Starting WEBRUNNER deployment...{COLORS['RESET']}") + log_file = setup_logging(config['deployment_id'], "webrunner_deployment") + + node_chunks = config.pop('node_chunks') + n_nodes = len(node_chunks) + + # Summary + print(f"\n{COLORS['CYAN']}Deployment Summary:{COLORS['RESET']}") + print(f" Deployment ID: {config['deployment_id']}") + print(f" Name: {config['webrunner_name']}") + + naming_info = show_naming_relationship(config['webrunner_name'], config['deployment_id'], 'webrunner') + if naming_info: + print(f" └─ {naming_info['relationship_text']}") + + print(f" Engagement: {config['engagement']}") + print(f" Providers: {', '.join(PROVIDER_LABELS[p] for p in config['providers'])}") + print(f" Nodes: {n_nodes} ({config['preset']}, {fmt_ip_count(config['chunk_size'])}/node)") + print(f" Scan mode: {config['scan_mode']}") + print(f" Ports: {', '.join(str(p) for p in config['ports'][:8])}{'...' if len(config['ports']) > 8 else ''}") + print(f" Total IPs: {fmt_ip_count(config['total_ips'])}") + print(f" Tor routing: {'Yes' if config['use_tor'] else 'No'}") + print(f" Enhanced OPSEC: {'Yes' if config['enhanced_opsec'] else 'No'}") + print(f" Teardown after: {'Yes' if config['teardown_after_scan'] else 'No'}") + + if config.get('ssh_key_path'): + key_name = os.path.basename(config['ssh_key_path']).replace('.pub', '') + print(f" SSH Key: {key_name}") + + if not confirm_action(f"\n{COLORS['YELLOW']}Proceed with WEBRUNNER deployment?{COLORS['RESET']}", default=True): + print(f"\n{COLORS['YELLOW']}Deployment cancelled.{COLORS['RESET']}") + return + + # Write node chunks file os.makedirs('logs', exist_ok=True) - chunks_file = f"logs/node_chunks_{deployment_id}.json" + chunks_file = f"logs/node_chunks_{config['deployment_id']}.json" with open(chunks_file, 'w') as f: json.dump(node_chunks, f) + config['node_chunks_file'] = chunks_file config['scanner_ip_log'] = f"logs/scanner_ips_{config['webrunner_name']}.txt" - config['results_dir'] = f"logs/webrunner_{deployment_id}" + config['results_dir'] = f"logs/webrunner_{config['deployment_id']}" for provider in config['providers']: set_provider_environment({**config, 'provider': provider}) playbook = 'providers/webrunner.yml' - n_nodes = len(node_chunks) print(f"\n{COLORS['CYAN']}[*] Launching WEBRUNNER — {n_nodes} nodes across " f"{', '.join(PROVIDER_LABELS[p] for p in config['providers'])}{COLORS['RESET']}") @@ -374,8 +428,10 @@ def execute_webrunner_deployment(config: dict): if success: print(f"\n{COLORS['GREEN']}[+] WEBRUNNER complete.{COLORS['RESET']}") - print(f" Results: logs/webrunner_{deployment_id}/") + print(f" Results: logs/webrunner_{config['deployment_id']}/") print(f" Scanner IPs: logs/scanner_ips_{config['webrunner_name']}.txt") - print(f" Log: logs/deployment_{deployment_id}.log") + print(f" Log: logs/deployment_{config['deployment_id']}.log") else: - print(f"\n{COLORS['RED']}[-] WEBRUNNER failed. Check: logs/deployment_{deployment_id}.log{COLORS['RESET']}") + print(f"\n{COLORS['RED']}[-] WEBRUNNER failed. Check: logs/deployment_{config['deployment_id']}.log{COLORS['RESET']}") + + wait_for_input() diff --git a/modules/webrunner/inputs/countries.yaml b/modules/webrunner/inputs/countries.yaml new file mode 100644 index 0000000..efaf6bc --- /dev/null +++ b/modules/webrunner/inputs/countries.yaml @@ -0,0 +1,36 @@ +--- +# WEBRUNNER — default country target list +# Adjust to match your engagement scope +countries: + - code: RU + name: Russia + - code: CN + name: China + - code: IR + name: Iran + - code: KP + name: North Korea + - code: BY + name: Belarus + - code: SY + name: Syria + - code: CU + name: Cuba + - code: VE + name: Venezuela + - code: MM + name: Myanmar + - code: SD + name: Sudan + - code: IQ + name: Iraq + - code: UA + name: Ukraine + - code: AZ + name: Azerbaijan + - code: GE + name: Georgia + - code: AM + name: Armenia + - code: MD + name: Moldova diff --git a/modules/webrunner/inputs/targets.yaml b/modules/webrunner/inputs/targets.yaml new file mode 100644 index 0000000..38593f3 --- /dev/null +++ b/modules/webrunner/inputs/targets.yaml @@ -0,0 +1,55 @@ +--- +# WEBRUNNER — default scan targets +# Define services of interest with ports and match criteria +targets: + - name: SSH + description: OpenSSH / SSH servers + ports: [22] + + - name: HTTP + description: Web servers (plain) + ports: [80, 8080, 8000, 8888] + + - name: HTTPS + description: Web servers (TLS) + ports: [443, 8443, 4443] + + - name: RDP + description: Remote Desktop Protocol + ports: [3389] + + - name: SMB + description: Windows file sharing + ports: [445, 139] + + - name: FTP + description: File Transfer Protocol + ports: [21] + + - name: Telnet + description: Legacy telnet + ports: [23] + + - name: VNC + description: VNC remote desktop + ports: [5900, 5901, 5902] + + - name: Proxy + description: HTTP/SOCKS proxies + ports: [3128, 8888, 1080, 1081] + + - name: Database + description: Common database ports + ports: [3306, 5432, 1433, 27017, 6379] + + - name: Kubernetes + description: K8s API server + ports: [6443, 8001] + + - name: Docker + description: Docker daemon API + ports: [2375, 2376] + + - name: SCADA + description: Industrial control systems + ports: [102, 502, 44818, 47808] diff --git a/modules/webrunner/tasks/configure_node.yml b/modules/webrunner/tasks/configure_node.yml index a454baf..8d5301b 100644 --- a/modules/webrunner/tasks/configure_node.yml +++ b/modules/webrunner/tasks/configure_node.yml @@ -19,6 +19,44 @@ retries: 3 delay: 10 +- name: Install Tor and proxychains + apt: + name: + - tor + - proxychains4 + state: present + retries: 3 + delay: 10 + when: use_tor | default(false) | bool + +- name: Start and enable Tor service + systemd: + name: tor + state: started + enabled: true + when: use_tor | default(false) | bool + +- name: Configure proxychains for Tor SOCKS5 + copy: + content: | + strict_chain + proxy_dns + tcp_read_time_out 15000 + tcp_connect_time_out 8000 + [ProxyList] + socks5 127.0.0.1 9050 + dest: /etc/proxychains4.conf + mode: '0644' + when: use_tor | default(false) | bool + +- name: Wait for Tor to establish circuit + wait_for: + port: 9050 + host: 127.0.0.1 + timeout: 60 + delay: 5 + when: use_tor | default(false) | bool + - name: Create webrunner workspace file: path: /root/webrunner diff --git a/modules/webrunner/tasks/run_scan.yml b/modules/webrunner/tasks/run_scan.yml index d393e2f..4877936 100644 --- a/modules/webrunner/tasks/run_scan.yml +++ b/modules/webrunner/tasks/run_scan.yml @@ -1,7 +1,7 @@ --- # Run the assigned scan on this WEBRUNNER node # Per-node vars: node_name, node_cidrs, node_ip_count, node_idx -# Global vars (from extra-vars): scan_mode, ports_str, masscan_rate, webrunner_name, deployment_id +# Global vars (from extra-vars): scan_mode, ports_str, masscan_rate, webrunner_name, deployment_id, use_tor - name: Write CIDR list for {{ node_name }} copy: @@ -11,6 +11,7 @@ - name: Run scan on {{ node_name }} ({{ node_ip_count }} IPs, mode={{ scan_mode }}) command: > + {{ 'proxychains4 -f /etc/proxychains4.conf' if (use_tor | default(false) | bool) else '' }} python3 /root/webrunner/node_scanner.py --mode {{ scan_mode }} --ports {{ ports_str }} diff --git a/providers/webrunner.yml b/providers/webrunner.yml index 37344d1..e46f68f 100644 --- a/providers/webrunner.yml +++ b/providers/webrunner.yml @@ -90,3 +90,43 @@ - "Results: {{ results_dir }}/merged_results.json" - "Scanner IPs: {{ scanner_ip_log }}" - "Log: logs/deployment_{{ deployment_id }}.log" + + +- name: WEBRUNNER — Teardown scan nodes + hosts: localhost + connection: local + gather_facts: false + when: teardown_after_scan | default(true) | bool + + tasks: + - name: Teardown Linode nodes + uri: + url: "https://api.linode.com/v4/linode/instances/{{ hostvars[item]['linode_instance_id'] }}" + method: DELETE + headers: + Authorization: "Bearer {{ linode_token }}" + status_code: [200, 204] + loop: "{{ groups['webrunner_nodes'] | default([]) }}" + when: + - hostvars[item]['provider'] == 'linode' + - hostvars[item]['linode_instance_id'] is defined + ignore_errors: true + + - name: Teardown AWS nodes + amazon.aws.ec2_instance: + instance_ids: + - "{{ hostvars[item]['ec2_instance_id'] }}" + region: "{{ aws_region | default('us-east-1') }}" + aws_access_key: "{{ aws_access_key | default('') }}" + aws_secret_key: "{{ aws_secret_key | default('') }}" + state: terminated + loop: "{{ groups['webrunner_nodes'] | default([]) }}" + when: + - hostvars[item]['provider'] == 'aws' + - hostvars[item]['ec2_instance_id'] is defined + ignore_errors: true + + - name: Teardown complete + debug: + msg: "All WEBRUNNER scan nodes destroyed." + when: groups['webrunner_nodes'] is defined and groups['webrunner_nodes'] | length > 0 diff --git a/utils/chunk_utils.py b/utils/chunk_utils.py index 3ed1a50..5f82d7d 100644 --- a/utils/chunk_utils.py +++ b/utils/chunk_utils.py @@ -1,20 +1,6 @@ #!/usr/bin/env python3 import ipaddress -import sys -from pathlib import Path - - -def _try_load_geo_scout() -> bool: - candidates = [ - Path(__file__).parents[2] / 'geo-scout', - Path.home() / 'tools' / 'geo-scout', - ] - for p in candidates: - if (p / 'lib' / 'cidr.py').exists(): - if str(p) not in sys.path: - sys.path.insert(0, str(p)) - return True - return False +from utils.cidr_resolver import get_country_cidrs def ip_count(cidr: str) -> int: @@ -24,27 +10,6 @@ def ip_count(cidr: str) -> int: return 0 -def get_country_cidrs(country_codes: list[str], exclude_map: dict[str, list] | None = None) -> dict[str, list[str]]: - if exclude_map is None: - exclude_map = {} - - _try_load_geo_scout() - try: - from lib.cidr import fetch_rir_data, load_index, get_cidrs, merge_cidrs - except ImportError: - return {cc.upper(): [] for cc in country_codes} - - fetch_rir_data() - index = load_index() - result = {} - for cc in country_codes: - cc = cc.upper() - cidrs = get_cidrs(cc, index) - cidrs = merge_cidrs(cidrs, exclude_map.get(cc, [])) - result[cc] = cidrs - return result - - def chunk_cidrs(all_cidrs: list[str], chunk_size: int) -> list[dict]: chunks = [] current_cidrs: list[str] = [] @@ -69,3 +34,6 @@ def chunk_cidrs(all_cidrs: list[str], chunk_size: int) -> list[dict]: chunks.append({'cidrs': current_cidrs, 'ip_count': current_count}) return chunks + + +__all__ = ['get_country_cidrs', 'chunk_cidrs', 'ip_count'] diff --git a/utils/cidr_resolver.py b/utils/cidr_resolver.py new file mode 100644 index 0000000..71b82b4 --- /dev/null +++ b/utils/cidr_resolver.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +import ipaddress +import math +import time +from pathlib import Path +from urllib.request import urlretrieve +from urllib.error import URLError + +CACHE_DIR = Path.home() / '.cache' / 'c2itall' / 'cidr' +CACHE_TTL = 86400 # 24 hours + +RIR_URLS = { + 'arin': 'https://ftp.arin.net/pub/stats/arin/delegated-arin-extended-latest', + 'ripe': 'https://ftp.ripe.net/ripe/stats/delegated-ripencc-extended-latest', + 'apnic': 'https://ftp.apnic.net/stats/apnic/delegated-apnic-extended-latest', + 'lacnic': 'https://ftp.lacnic.net/pub/stats/lacnic/delegated-lacnic-extended-latest', + 'afrinic': 'https://ftp.afrinic.net/stats/delegated-afrinic-extended-latest', +} + + +def _cache_path(rir: str) -> Path: + return CACHE_DIR / f'{rir}.txt' + + +def _is_stale(path: Path) -> bool: + if not path.exists(): + return True + return (time.time() - path.stat().st_mtime) > CACHE_TTL + + +def _fetch_rir(rir: str, url: str) -> Path: + path = _cache_path(rir) + CACHE_DIR.mkdir(parents=True, exist_ok=True) + if _is_stale(path): + try: + urlretrieve(url, path) + except (URLError, OSError): + pass # use stale cache if download fails + return path + + +def _count_to_cidrs(start: str, count: int) -> list[str]: + try: + start_addr = ipaddress.IPv4Address(start) + end_addr = ipaddress.IPv4Address(int(start_addr) + count - 1) + return [str(n) for n in ipaddress.summarize_address_range(start_addr, end_addr)] + except (ipaddress.AddressValueError, ValueError): + return [] + + +def _parse_rir_file(path: Path, target_ccs: set[str]) -> dict[str, list[str]]: + result: dict[str, list[str]] = {cc: [] for cc in target_ccs} + try: + with open(path, encoding='latin-1') as f: + for line in f: + line = line.strip() + if not line or line.startswith('#'): + continue + parts = line.split('|') + if len(parts) < 7: + continue + _, cc, type_, start, count_str, _, status = parts[:7] + if type_ != 'ipv4': + continue + if status not in ('allocated', 'assigned'): + continue + cc = cc.upper() + if cc not in target_ccs: + continue + try: + count = int(count_str) + except ValueError: + continue + result[cc].extend(_count_to_cidrs(start, count)) + except OSError: + pass + return result + + +def get_country_cidrs(country_codes: list[str], exclude_map: dict[str, list] | None = None) -> dict[str, list[str]]: + if exclude_map is None: + exclude_map = {} + target_ccs = {cc.upper() for cc in country_codes} + combined: dict[str, list[str]] = {cc: [] for cc in target_ccs} + + for rir, url in RIR_URLS.items(): + path = _fetch_rir(rir, url) + partial = _parse_rir_file(path, target_ccs) + for cc, cidrs in partial.items(): + combined[cc].extend(cidrs) + + for cc, excludes in exclude_map.items(): + cc = cc.upper() + if cc not in combined or not excludes: + continue + exclude_nets = [] + for ex in excludes: + try: + exclude_nets.append(ipaddress.ip_network(ex, strict=False)) + except ValueError: + pass + if not exclude_nets: + continue + filtered = [] + for cidr in combined[cc]: + try: + net = ipaddress.ip_network(cidr, strict=False) + if not any(net.overlaps(ex) for ex in exclude_nets): + filtered.append(cidr) + except ValueError: + filtered.append(cidr) + combined[cc] = filtered + + return combined