From 6538b09b7db5c0c9dc69ecb54706b478b2d67c1a Mon Sep 17 00:00:00 2001 From: n0mad1k Date: Thu, 30 Apr 2026 19:04:42 -0400 Subject: [PATCH] Fix P1 WEBRUNNER deployment failures and secret leakage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - providers/webrunner.yml: wrap loop_var under loop_control in all three provider provision tasks — Ansible was rejecting loop_var as a conflicting action statement alongside include_tasks - deployment_engine.py: strip _SECRET_KEYS (tokens, passwords, api keys) from create_extra_vars() — credentials are already set as env vars by set_provider_environment(), no need to pass them through --extra-vars where they end up in process listings and logs - deployment_engine.py: remove debug log line that dumped the full ansible-playbook command including all extra-vars JSON with live creds - deployment_engine.py: create deployment log files with 0o600 permissions instead of world-readable 0o644 - deploy_webrunner.py: remove load_vars_file() call that was pulling phishing module creds (SMTP password, gophish port, domain) into the webrunner config dict — all provider creds already collected by gather_provider_config() - deploy_webrunner.py: remove unused select_provider import - .gitignore: narrow 'deployment*' glob to specific artifact patterns so deployment_engine.py is no longer excluded from tracking --- .gitignore | 5 +- modules/webrunner/deploy_webrunner.py | 9 +- providers/webrunner.yml | 9 +- utils/deployment_engine.py | 337 ++++++++++++++++++++++++++ 4 files changed, 348 insertions(+), 12 deletions(-) create mode 100644 utils/deployment_engine.py diff --git a/.gitignore b/.gitignore index e748bb4..2f88c98 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ vars.yaml venv -deployment* +deployment_*.log +deployment_info_*.txt +node_chunks_*.json +scanner_ips_*.txt config.yml logs/ domainhunter/ diff --git a/modules/webrunner/deploy_webrunner.py b/modules/webrunner/deploy_webrunner.py index 20ff699..15dddaa 100644 --- a/modules/webrunner/deploy_webrunner.py +++ b/modules/webrunner/deploy_webrunner.py @@ -17,9 +17,8 @@ if _base not in sys.path: from utils.common import ( 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.provider_utils import gather_provider_config from utils.ssh_utils import generate_ssh_key from utils.naming_utils import show_naming_relationship from utils.deployment_engine import execute_playbook, set_provider_environment @@ -353,12 +352,6 @@ def gather_webrunner_parameters() -> dict | None: 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') diff --git a/providers/webrunner.yml b/providers/webrunner.yml index e46f68f..2ec8521 100644 --- a/providers/webrunner.yml +++ b/providers/webrunner.yml @@ -29,17 +29,20 @@ - name: Provision Linode nodes include_tasks: "Linode/webrunner_provision_tasks.yml" loop: "{{ all_node_chunks | selectattr('provider', 'equalto', 'linode') | list }}" - loop_var: node_chunk + loop_control: + loop_var: node_chunk - name: Provision AWS nodes include_tasks: "AWS/webrunner_provision_tasks.yml" loop: "{{ all_node_chunks | selectattr('provider', 'equalto', 'aws') | list }}" - loop_var: node_chunk + loop_control: + loop_var: node_chunk - name: Provision FlokiNET nodes include_tasks: "FlokiNET/webrunner_provision_tasks.yml" loop: "{{ all_node_chunks | selectattr('provider', 'equalto', 'flokinet') | list }}" - loop_var: node_chunk + loop_control: + loop_var: node_chunk - name: WEBRUNNER — Configure and scan diff --git a/utils/deployment_engine.py b/utils/deployment_engine.py new file mode 100644 index 0000000..25bd8d3 --- /dev/null +++ b/utils/deployment_engine.py @@ -0,0 +1,337 @@ +#!/usr/bin/env python3 +""" +Deployment engine for C2ingRed infrastructure deployments +""" + +import os +import sys +import subprocess +import logging +import json + +# Add project root to path +sys.path.append(os.path.join(os.path.dirname(__file__), '..')) + +from utils.common import COLORS, PROVIDER_DIRS + +_SECRET_KEYS = frozenset({ + 'linode_token', 'aws_access_key', 'aws_secret_key', 'flokinet_api_key', + 'smtp_auth_pass', 'smtp_password', 'ftp_password', 'api_key', + 'password', 'secret', 'token', +}) + + +def deploy_infrastructure(config): + """ + Deploy infrastructure based on the provided configuration. + + Args: + config: Dictionary containing deployment configuration + + Returns: + bool: True if deployment succeeded, False otherwise + """ + provider = config.get('provider') + deployment_id = config.get('deployment_id') + + if not provider or not deployment_id: + logging.error("Missing provider or deployment ID in configuration") + return False + + # Determine which playbook to use + playbook = determine_playbook(config) + + if not playbook: + logging.error("Could not determine appropriate playbook for deployment") + return False + + if not os.path.exists(playbook): + logging.error(f"Playbook not found: {playbook}") + print(f"{COLORS['RED']}Playbook not found: {playbook}{COLORS['RESET']}") + return False + + logging.info(f"Using playbook: {playbook}") + + # Set provider-specific environment variables + set_provider_environment(config) + + # Execute the deployment + return execute_playbook(playbook, config) + + +def determine_playbook(config): + """ + Determine the correct playbook based on deployment configuration. + + Args: + config: Deployment configuration dictionary + + Returns: + str: Path to the playbook file + """ + provider = config.get('provider') + provider_dir = PROVIDER_DIRS.get(provider, provider.capitalize()) + base_path = f"providers/{provider_dir}" + + # Attack box deployment + if config.get('attack_box_deployment'): + return f"{base_path}/attack_box.yml" + + # Redirector-only deployment + if config.get('redirector_only'): + return f"{base_path}/redirector.yml" + + # Tracker deployment + if config.get('tracker_deployment'): + return f"{base_path}/tracker.yml" + + # Phishing infrastructure deployment + if config.get('phishing_deployment'): + # Check for provider-specific phishing playbook first + provider_specific = f"{base_path}/{provider}_phishing.yml" + if os.path.exists(provider_specific): + return provider_specific + return f"{base_path}/phishing.yml" + + # Chat server deployment + if config.get('chat_deployment'): + return f"{base_path}/chat_server.yml" + + # C2 deployment (default for C2 infrastructure) + if config.get('c2_only') or config.get('deploy_c2') or config.get('c2_framework'): + return f"{base_path}/c2.yml" + + # Default to C2 playbook + return f"{base_path}/c2.yml" + + +def set_provider_environment(config): + """ + Set provider-specific environment variables for deployment. + + Args: + config: Deployment configuration dictionary + """ + provider = config.get('provider') + + if provider == "aws": + if config.get('aws_access_key'): + os.environ['AWS_ACCESS_KEY_ID'] = config['aws_access_key'] + if config.get('aws_secret_key'): + os.environ['AWS_SECRET_ACCESS_KEY'] = config['aws_secret_key'] + if config.get('aws_region'): + os.environ['AWS_DEFAULT_REGION'] = config['aws_region'] + + elif provider == "linode": + if config.get('linode_token'): + os.environ['LINODE_TOKEN'] = config['linode_token'] + else: + # Try to load from vars file + try: + from utils.common import load_vars_file + vars_data = load_vars_file(provider) + if vars_data and vars_data.get('linode_token'): + os.environ['LINODE_TOKEN'] = vars_data['linode_token'] + config['linode_token'] = vars_data['linode_token'] + except Exception as e: + logging.warning(f"Failed to load Linode token from vars file: {e}") + + elif provider == "flokinet": + if config.get('flokinet_api_key'): + os.environ['FLOKINET_API_KEY'] = config['flokinet_api_key'] + + +def execute_playbook(playbook, config): + """ + Execute an Ansible playbook with the provided configuration. + + Args: + playbook: Path to the Ansible playbook + config: Deployment configuration dictionary + + Returns: + bool: True if playbook executed successfully, False otherwise + """ + try: + # Build the ansible-playbook command + cmd = [ + 'ansible-playbook', + playbook, + '--extra-vars', create_extra_vars(config) + ] + + # Add verbosity if debug mode is enabled + if config.get('debug'): + cmd.append('-vvv') + + logging.info(f"Executing: {' '.join(cmd[:3])} ...") + + # Get log file path for output + log_file = f"logs/deployment_{config['deployment_id']}.log" + + # Execute the playbook — log file restricted to owner-only (contains Ansible output) + log_fd = os.open(log_file, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600) + with open(log_fd, 'a') as log_output: + # Write command to log + log_output.write(f"\n{'='*60}\n") + log_output.write(f"Executing playbook: {playbook}\n") + log_output.write(f"{'='*60}\n\n") + log_output.flush() + + process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + cwd=os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + ) + + # Stream output to both console and log file + for line in process.stdout: + # Write to log file + log_output.write(line) + log_output.flush() + + # Print select lines to console for user feedback + if should_print_line(line): + print(line.rstrip()) + + process.wait() + + if process.returncode == 0: + logging.info("Playbook executed successfully") + + # Save deployment info + save_deployment_info(config) + + return True + else: + logging.error(f"Playbook failed with return code {process.returncode}") + print(f"\n{COLORS['RED']}Deployment failed. Check logs for details: {log_file}{COLORS['RESET']}") + return False + + except FileNotFoundError: + logging.error("ansible-playbook command not found. Is Ansible installed?") + print(f"{COLORS['RED']}Error: ansible-playbook not found. Please install Ansible.{COLORS['RESET']}") + return False + except Exception as e: + logging.error(f"Error executing playbook: {e}") + print(f"{COLORS['RED']}Error executing playbook: {e}{COLORS['RESET']}") + return False + + +def should_print_line(line): + """ + Determine if a line should be printed to console. + Filters out verbose Ansible output to show only key information. + + Args: + line: Output line from Ansible + + Returns: + bool: True if line should be printed + """ + # Always show task names and important messages + important_patterns = [ + 'TASK [', + 'PLAY [', + 'ok:', + 'changed:', + 'failed:', + 'fatal:', + 'skipping:', + 'PLAY RECAP', + '=> {', + 'msg:', + 'IP Address:', + 'Instance ID:', + ] + + for pattern in important_patterns: + if pattern in line: + return True + + return False + + +def create_extra_vars(config): + """ + Create JSON string of extra variables for Ansible. + + Args: + config: Deployment configuration dictionary + + Returns: + str: JSON string of variables + """ + # Credentials are passed via environment variables (set_provider_environment). + # Strip them here so they never appear in --extra-vars or process listings. + extra_vars = {} + + for key, value in config.items(): + if value is None: + continue + if any(secret in key.lower() for secret in _SECRET_KEYS): + continue + if isinstance(value, bool): + extra_vars[key] = value + elif isinstance(value, (str, int, float, list, dict)): + extra_vars[key] = value + + return json.dumps(extra_vars) + + +def save_deployment_info(config): + """ + Save deployment information to a file for later reference/cleanup. + + Args: + config: Deployment configuration dictionary + """ + try: + deployment_id = config.get('deployment_id') + info_file = f"logs/deployment_info_{deployment_id}.txt" + + with open(info_file, 'w') as f: + f.write(f"Deployment Information\n") + f.write(f"{'='*50}\n") + f.write(f"Deployment ID: {deployment_id}\n") + f.write(f"Provider: {config.get('provider')}\n") + f.write(f"Deployment Type: {config.get('deployment_type', 'unknown')}\n") + f.write(f"\n") + f.write(f"Configuration:\n") + f.write(f"{'-'*30}\n") + + # Write key configuration items + keys_to_save = [ + 'provider', 'deployment_id', 'deployment_type', + 'attack_box_name', 'c2_name', 'redirector_name', 'tracker_name', + 'chat_server_name', + 'domain', 'c2_subdomain', 'redirector_subdomain', + 'c2_framework', 'redirector_type', + 'linode_region', 'aws_region', + 'setup_vpn', 'setup_tor', + 'ssh_key_path', + 'matrix_admin_user', 'enable_bot_support' + ] + + for key in keys_to_save: + if key in config and config[key] is not None: + # Don't save sensitive data + if 'password' in key.lower() or 'token' in key.lower() or 'secret' in key.lower(): + continue + f.write(f"{key}: {config[key]}\n") + + f.write(f"\n") + f.write(f"Access Information:\n") + f.write(f"{'-'*30}\n") + f.write(f"(Instance IPs will be displayed in Ansible output above)\n") + + logging.info(f"Deployment info saved to: {info_file}") + + except Exception as e: + logging.warning(f"Failed to save deployment info: {e}") + + +if __name__ == "__main__": + print("Deployment engine loaded")