Files
CoM-c2itall/deploy.py
T
2025-04-10 06:59:59 -04:00

2904 lines
125 KiB
Python

#!/usr/bin/env python3
import os
import sys
import subprocess
import argparse
import time
import yaml
import json
import random
import string
import shutil
import re
import socket
import paramiko
import logging
import glob
from datetime import datetime
# Constants
PROVIDERS = ["aws", "linode", "flokinet"]
DEFAULT_REGION = {
"aws": "us-east-1",
"linode": "us-east",
"flokinet": "anonymous"
}
DEFAULT_SIZE = {
"aws": "t2.micro",
"linode": "g6-standard-1",
"flokinet": "standard"
}
def normalize_provider_name(provider_name):
"""Normalize provider name to expected capitalization"""
if not provider_name:
return None
provider_map = {
'aws': 'AWS',
'linode': 'Linode',
'flokinet': 'FlokiNET'
}
normalized = provider_map.get(provider_name.lower())
if normalized:
return normalized
return provider_name
def setup_argparse():
"""Set up and return the argument parser"""
parser = argparse.ArgumentParser(description='Deploy C2itAll Red Team infrastructure')
# Provider selection
parser.add_argument('-p', '--provider', choices=PROVIDERS, help='Provider to use for deployment')
# AWS-specific arguments
parser.add_argument('--aws-key', help='AWS access key')
parser.add_argument('--aws-secret', help='AWS secret key')
parser.add_argument('--aws-region', help=f'AWS region (default: random from vars.yaml)')
# Linode-specific arguments
parser.add_argument('--linode-token', help='Linode API token')
parser.add_argument('--linode-region', help=f'Linode region (default: random from vars.yaml)')
# FlokiNET-specific arguments
parser.add_argument('--flokinet', action='store_true', help='Use FlokiNET as the provider (manual setup required)')
parser.add_argument('--flokinet-redirector-ip', help='FlokiNET redirector IP address')
parser.add_argument('--flokinet-c2-ip', help='FlokiNET C2 server IP address')
# General arguments
parser.add_argument('--ssh-key', help='Path to SSH private key')
parser.add_argument('--ssh-user', help='SSH username (default: from vars.yaml or "root")')
parser.add_argument('--size', help='Size/type of the instances')
parser.add_argument('--region', help='Region for the instances')
parser.add_argument('--redirector-name', help='Name for the redirector instance (default: random)')
parser.add_argument('--c2-name', help='Name for the C2 instance (default: random)')
parser.add_argument('--teardown', action='store_true', help='Tear down existing infrastructure')
# Deployment options
parser.add_argument('--redirector-only', action='store_true', help='Deploy only the redirector')
parser.add_argument('--c2-only', action='store_true', help='Deploy only the C2 server')
parser.add_argument('--debug', action='store_true', help='Enable debug mode for verbose output')
parser.add_argument('--domain', help='Domain name for the C2 infrastructure')
parser.add_argument('--letsencrypt-email', help='Email for Let\'s Encrypt certificate')
# OPSEC settings
parser.add_argument('--disable-history', action='store_true', help='Disable command history on the servers')
parser.add_argument('--secure-memory', action='store_true', help='Enable secure memory settings')
parser.add_argument('--zero-logs', action='store_true', help='Enable zero-logs configuration')
# Post-deployment options
parser.add_argument('--ssh-after-deploy', action='store_true', help='SSH into the instance after deployment')
parser.add_argument('--copy-ssh-key', action='store_true', help='Copy SSH key to the server for passwordless login')
parser.add_argument('--run-tests', action='store_true', help='Run tests after deployment')
# Tracker module integration
parser.add_argument('--deploy-tracker', action='store_true', help='Deploy tracker module')
parser.add_argument('--tracker-domain', help='Domain name for the tracker server')
parser.add_argument('--tracker-email', help='Email for the tracker server Let\'s Encrypt certificate')
parser.add_argument('--tracker-name', help='Name for the tracker engagement')
parser.add_argument('--tracker-ipinfo-token', help='IPinfo API token for tracker geolocation data')
parser.add_argument('--tracker-setup-ssl', action='store_true', help='Set up SSL certificates for tracker')
parser.add_argument('--tracker-create-pixel', action='store_true', help='Create email tracking pixel')
return parser.parse_args()
def load_vars_file(provider):
"""Load vars.yaml for the specified provider"""
vars_file = f"{provider}/vars.yaml"
if os.path.exists(vars_file):
try:
with open(vars_file, 'r') as f:
vars_data = yaml.safe_load(f)
print(f"[+] Loaded configuration from {vars_file}")
return vars_data
except Exception as e:
print(f"[!] Warning: Failed to load {vars_file}: {e}")
else:
print(f"[!] Warning: {vars_file} not found")
return {}
def load_config(args):
"""Load configuration from vars.yaml, environment variables, and arguments"""
config = {}
# Determine provider
provider = args.provider
if args.flokinet:
provider = 'flokinet'
if not provider:
# Check config.yml for a saved provider
if os.path.exists('config.yml'):
try:
with open('config.yml', 'r') as f:
saved_config = yaml.safe_load(f)
provider = saved_config.get('provider')
print(f"[+] Using saved provider from config.yml: {provider}")
except Exception as e:
print(f"[!] Warning: Failed to load config.yml: {e}")
# If still no provider, try each vars.yaml to see which ones exist
if not provider:
for p in PROVIDERS:
if os.path.exists(f"{p}/vars.yaml"):
provider = p
print(f"[+] Auto-detected provider from {p}/vars.yaml")
break
# If still no provider, default to 'aws'
if not provider:
provider = 'aws'
print(f"[!] No provider specified, defaulting to {provider}")
config['provider'] = provider
# Load vars.yaml for the selected provider
vars_data = load_vars_file(provider)
# Override with environment variables and command line arguments
if provider == 'aws':
config['aws_key'] = args.aws_key or os.environ.get('AWS_ACCESS_KEY_ID') or vars_data.get('aws_access_key')
config['aws_secret'] = args.aws_secret or os.environ.get('AWS_SECRET_ACCESS_KEY') or vars_data.get('aws_secret_key')
# Get region choices from vars.yaml
config['aws_region_choices'] = vars_data.get('aws_region_choices', [DEFAULT_REGION['aws']])
# Set region (explicitly provided, or keep it None for random selection later)
config['aws_region'] = args.aws_region
# Load AMI map
config['ami_map'] = vars_data.get('ami_map', {})
elif provider == 'linode':
config['linode_token'] = args.linode_token or os.environ.get('LINODE_TOKEN') or vars_data.get('linode_token')
# Get region choices from vars.yaml
config['linode_region_choices'] = vars_data.get('region_choices', [DEFAULT_REGION['linode']])
# Set region (explicitly provided, or keep it None for random selection later)
config['linode_region'] = args.linode_region
elif provider == 'flokinet':
config['flokinet_redirector_ip'] = args.flokinet_redirector_ip or vars_data.get('redirector_ip')
config['flokinet_c2_ip'] = args.flokinet_c2_ip or vars_data.get('c2_ip')
# Get region choices from vars.yaml
config['flokinet_region_choices'] = vars_data.get('flokinet_region_choices', [DEFAULT_REGION['flokinet']])
# General configuration from vars.yaml
config['ssh_key'] = args.ssh_key or vars_data.get('ssh_key_path')
config['ssh_user'] = args.ssh_user or vars_data.get('ssh_user', 'root')
# Instance size from vars.yaml or default
if provider == 'aws':
config['size'] = args.size or vars_data.get('aws_instance_type', DEFAULT_SIZE['aws'])
elif provider == 'linode':
config['size'] = args.size or vars_data.get('plan', DEFAULT_SIZE['linode'])
else:
config['size'] = args.size or DEFAULT_SIZE.get(provider, 'small')
# Set region to None to trigger randomization later if not explicitly provided
config['region'] = args.region
# Generate random instance names if not provided - OPSEC improvement
rand_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
timestamp = int(time.time()) % 10000 # Last 4 digits of timestamp for brevity
# OPSEC: Use generic names that don't indicate purpose
config['redirector_name'] = args.redirector_name or f"srv-{rand_suffix}-{timestamp}"
config['c2_name'] = args.c2_name or f"node-{rand_suffix}-{timestamp}"
# Other flags
config['teardown'] = args.teardown
config['redirector_only'] = args.redirector_only
config['c2_only'] = args.c2_only
config['debug'] = args.debug
config['ssh_after_deploy'] = args.ssh_after_deploy
config['copy_ssh_key'] = args.copy_ssh_key
config['run_tests'] = args.run_tests
# Domain and email
config['domain'] = args.domain or vars_data.get('domain', 'example.com')
config['letsencrypt_email'] = args.letsencrypt_email or vars_data.get('letsencrypt_email', 'admin@example.com')
# OPSEC settings - default to enabled for better security
config['disable_history'] = args.disable_history if args.disable_history is not None else vars_data.get('disable_history', True)
config['secure_memory'] = args.secure_memory if args.secure_memory is not None else vars_data.get('secure_memory', True)
config['zero_logs'] = args.zero_logs if args.zero_logs is not None else vars_data.get('zero_logs', True)
# Generate random SSH key for this deployment if not specified
if not config.get('ssh_key') and provider != 'flokinet':
config['ssh_key'] = generate_ssh_key(rand_suffix)
# GoPhish settings
config['gophish_admin_port'] = vars_data.get('gophish_admin_port', str(random.randint(2000, 9000)))
config['smtp_auth_user'] = vars_data.get('smtp_auth_user', f"user{random.randint(1000, 9999)}")
# Generate strong random password if not specified
if not vars_data.get('smtp_auth_pass'):
charset = string.ascii_letters + string.digits + "!@#$%^&*()-_=+[]{};:,.<>?"
random_pass = ''.join(random.choices(charset, k=20))
config['smtp_auth_pass'] = random_pass
else:
config['smtp_auth_pass'] = vars_data.get('smtp_auth_pass')
# Set random SSH port for better OPSEC
config['ssh_port'] = vars_data.get('ssh_port', random.randint(20000, 65000))
# Tracker module settings
config['deploy_tracker'] = args.deploy_tracker
config['tracker_domain'] = args.tracker_domain
config['tracker_email'] = args.tracker_email
config['tracker_name'] = args.tracker_name
config['tracker_ipinfo_token'] = args.tracker_ipinfo_token
config['tracker_setup_ssl'] = args.tracker_setup_ssl
config['tracker_create_pixel'] = args.tracker_create_pixel
return config
def select_region(config):
"""Select region randomly from choices or use explicitly provided region"""
provider = config['provider']
if provider == 'aws':
if config.get('aws_region'):
selected_region = config['aws_region']
print(f"[+] Using specified AWS region: {selected_region}")
else:
# Randomize for OPSEC
region_choices = config.get('aws_region_choices', [DEFAULT_REGION['aws']])
# If multiple regions available, select randomly
if len(region_choices) > 1:
# Weighted selection favoring less common regions for better OPSEC
weights = [1.5 if 'gov' not in r and 'us-east' not in r else 1.0 for r in region_choices]
selected_region = random.choices(region_choices, weights=weights, k=1)[0]
else:
selected_region = region_choices[0]
print(f"[+] Randomly selected AWS region: {selected_region}")
config['aws_region'] = selected_region
return selected_region
elif provider == 'linode':
if config.get('linode_region'):
selected_region = config['linode_region']
print(f"[+] Using specified Linode region: {selected_region}")
else:
# Randomize for OPSEC
region_choices = config.get('linode_region_choices', [DEFAULT_REGION['linode']])
# Favor less common regions for better OPSEC
weights = [1.5 if 'us' not in r else 1.0 for r in region_choices]
selected_region = random.choices(region_choices, weights=weights, k=1)[0]
print(f"[+] Randomly selected Linode region: {selected_region}")
config['linode_region'] = selected_region
return selected_region
elif provider == 'flokinet':
if config.get('region'):
selected_region = config['region']
print(f"[+] Using specified FlokiNET region: {selected_region}")
else:
region_choices = config.get('flokinet_region_choices', [DEFAULT_REGION['flokinet']])
selected_region = random.choice(region_choices)
print(f"[+] Randomly selected FlokiNET region: {selected_region}")
config['region'] = selected_region
return selected_region
# Fallback
if config.get('region'):
return config['region']
return DEFAULT_REGION.get(provider, 'us-east')
def verify_key_file_permissions(key_path):
"""Verify and fix SSH key file permissions"""
try:
# Check if key file exists
if not os.path.exists(key_path):
print(f"[-] SSH key file not found: {key_path}")
return False
# Fix permissions
current_mode = os.stat(key_path).st_mode & 0o777
if current_mode != 0o600:
print(f"[!] SSH key has incorrect permissions: {oct(current_mode)[2:]}. Fixing to 0600...")
os.chmod(key_path, 0o600)
print(f"[+] SSH key permissions fixed")
return True
except Exception as e:
print(f"[-] Error verifying key file permissions: {e}")
return False
def parse_ansible_error(stderr):
"""Parse ansible error output to extract useful information"""
error_info = {}
# Look for common ansible error patterns
if "FAILED! => " in stderr:
# Extract the JSON error part
error_start = stderr.find("FAILED! => ")
error_text = stderr[error_start:].split("\n")[0]
# Remove the "FAILED! => " prefix
error_text = error_text.replace("FAILED! => ", "")
try:
# Try to parse as JSON
error_info = json.loads(error_text)
except:
# If not valid JSON, use the text as is
error_info = {"msg": error_text}
# Look for common error messages
if "SSH Error:" in stderr:
error_info["ssh_error"] = True
if "No such file or directory" in stderr:
error_info["file_not_found"] = True
if "Permission denied" in stderr:
error_info["permission_denied"] = True
if "The API Token provided is not valid" in stderr:
error_info["invalid_api_token"] = True
return error_info
def setup_error_logging():
"""Setup error logging and ensure log directory exists"""
log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs")
os.makedirs(log_dir, exist_ok=True)
# Set up global log file
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
log_file = os.path.join(log_dir, f"deployment_{timestamp}.log")
# Configure logging
logging.basicConfig(
filename=log_file,
level=logging.DEBUG if '--debug' in sys.argv else logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
# Log start of script
logging.info("C2itAll deployment script started")
return log_file
def validate_config(config):
"""Validate the configuration"""
provider = config['provider']
if provider == 'aws':
if not config.get('aws_key') or not config.get('aws_secret'):
print("[-] Error: AWS credentials must be provided")
print(" Use --aws-key and --aws-secret, or set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables")
return False
if provider == 'linode':
if not config.get('linode_token'):
print("[-] Error: Linode token must be provided")
print(" Use --linode-token or set LINODE_TOKEN environment variable")
return False
if provider == 'flokinet':
# For FlokiNET, we'll prompt for IPs if not provided
if (not config.get('flokinet_redirector_ip') and not config['c2_only']) or \
(not config.get('flokinet_c2_ip') and not config['redirector_only']):
print("[*] FlokiNET IPs not fully specified - will prompt during deployment")
# This is not a failure case anymore, just a warning
if not config.get('ssh_key') and provider != 'flokinet':
print("[*] SSH key not provided, will generate one")
config['ssh_key'] = generate_ssh_key()
if not config['ssh_key']:
return False
# Check for deployment logic issues
if config['redirector_only'] and config['c2_only']:
print("[-] Error: Cannot specify both --redirector-only and --c2-only")
return False
# Tracker module validation
if config['deploy_tracker']:
if not config.get('tracker_domain') and not config.get('tracker_ipinfo_token'):
print("[-] Warning: Tracker deployment without domain or IPInfo token will have limited functionality")
return True
def deploy_aws(config):
"""Deploy infrastructure using AWS provider"""
print("[+] Deploying AWS infrastructure...")
# Set AWS environment variables
os.environ['AWS_ACCESS_KEY_ID'] = config['aws_key']
os.environ['AWS_SECRET_ACCESS_KEY'] = config['aws_secret']
# Create logs directory
log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs")
os.makedirs(log_dir, exist_ok=True)
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
# Only create one log file in the logs directory
log_file = os.path.join(log_dir, f"deployment_{timestamp}_aws.log")
print(f"[+] Creating deployment log at: {log_file}")
# Prepare SSH key - for AWS we'll use AWS key pairs instead of local files
ssh_key_name = config.get('ssh_key_name')
if not ssh_key_name:
# Generate a random key name for AWS
rand_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
timestamp = int(time.time()) % 10000
ssh_key_name = f"c2itall-{rand_suffix}-{timestamp}"
config['ssh_key_name'] = ssh_key_name
print(f"[+] Using AWS key pair name: {ssh_key_name}")
# Create a temporary inventory file for Ansible
inventory_file = f"inventory_aws_{int(time.time())}.yml"
with open(log_file, 'a') as f:
f.write(f"==== CREATING INVENTORY FILE: {inventory_file} ====\n")
with open(inventory_file, 'w') as f:
f.write("---\n")
f.write("all:\n")
f.write(" vars:\n")
f.write(f" ansible_ssh_private_key_file: ~/.ssh/{ssh_key_name}.pem\n")
f.write(f" ansible_user: {config.get('ssh_user', 'ec2-user')}\n")
f.write(f" ansible_python_interpreter: /usr/bin/python3\n")
f.write(" children:\n")
f.write(" redirectors:\n")
f.write(" hosts:\n")
f.write(" redirector:\n")
f.write(" ansible_host: '{{ redirector_ip }}'\n")
f.write(" c2servers:\n")
f.write(" hosts:\n")
f.write(" c2:\n")
f.write(" ansible_host: '{{ c2_ip }}'\n")
# Log inventory file content
with open(log_file, 'a') as f:
f.write(f"INVENTORY CONTENT:\n")
with open(inventory_file, 'r') as inv:
f.write(inv.read())
f.write("\n==== END INVENTORY FILE ====\n\n")
# Generate default names if not provided
rand_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
timestamp = int(time.time()) % 10000
redirector_name = config.get('redirector_name', f"srv-{rand_suffix}-{timestamp}")
c2_name = config.get('c2_name', f"node-{rand_suffix}-{timestamp}")
# Update config with generated names for logging
config['redirector_name'] = redirector_name
config['c2_name'] = c2_name
# Log important information
with open(log_file, 'a') as f:
f.write(f"==== DEPLOYMENT INFORMATION ====\n")
f.write(f"AWS Key Pair Name: {ssh_key_name}\n")
f.write(f"Redirector Name: {redirector_name}\n")
f.write(f"C2 Name: {c2_name}\n")
f.write(f"AWS Region: {config.get('aws_region', 'default region')}\n")
f.write(f"Domain: {config.get('domain', 'example.com')}\n")
f.write(f"==== END DEPLOYMENT INFORMATION ====\n\n")
# Track deployed instances and resources for cleanup if needed
deployed_instances = []
deployed_resources = []
success = False
try:
# Create extra_vars dictionary with all config values
extra_vars = {
'aws_access_key': config['aws_key'],
'aws_secret_key': config['aws_secret'],
'aws_region': config.get('aws_region', DEFAULT_REGION['aws']),
'ami_map': config.get('ami_map', {}),
'size': config.get('size', DEFAULT_SIZE['aws']),
'aws_instance_type': config.get('size', DEFAULT_SIZE['aws']),
'redirector_name': redirector_name,
'c2_name': c2_name,
'teardown': config.get('teardown', False),
'disable_history': config.get('disable_history', True),
'secure_memory': config.get('secure_memory', True),
'zero_logs': config.get('zero_logs', True),
'redirector_only': config.get('redirector_only', False),
'c2_only': config.get('c2_only', False),
'debug': config.get('debug', False),
'domain': config.get('domain', 'example.com'),
'letsencrypt_email': config.get('letsencrypt_email', 'admin@example.com'),
'gophish_admin_port': config.get('gophish_admin_port', str(random.randint(2000, 9000))),
'smtp_auth_user': config.get('smtp_auth_user', f"user{random.randint(1000, 9999)}"),
'smtp_auth_pass': config.get('smtp_auth_pass', ''.join(random.choices(string.ascii_letters + string.digits + "!@#$%^&*", k=20))),
'key_name': ssh_key_name,
'ssh_user': config.get('ssh_user', 'ec2-user'),
'instance_label': c2_name if config.get('c2_only', False) else redirector_name,
'private_key_path': f"~/.ssh/{ssh_key_name}",
}
# Format extra vars for command line, handling special characters
extra_vars_list = []
for k, v in extra_vars.items():
if isinstance(v, bool):
extra_vars_list.append(f"{k}={str(v).lower()}")
elif isinstance(v, (int, float)):
extra_vars_list.append(f"{k}={v}")
elif isinstance(v, str):
# Escape quotes in string values
escaped_v = v.replace("'", "'\\''")
extra_vars_list.append(f"{k}='{escaped_v}'")
elif not isinstance(v, dict): # Skip dict values
extra_vars_list.append(f"{k}={v}")
extra_vars_str = " ".join(extra_vars_list)
# Handle the ami_map special case
ami_map_str = ""
if config.get('ami_map'):
ami_map_str = f"ami_map='{json.dumps(config['ami_map'])}'"
# Execute deployment based on deployment mode
if extra_vars['redirector_only']:
playbook = "AWS/redirector.yml"
cmd = f"ansible-playbook -i {inventory_file} {playbook} -e '{extra_vars_str} {ami_map_str}'"
with open(log_file, 'a') as f:
f.write(f"==== RUNNING REDIRECTOR PLAYBOOK ====\n")
f.write(f"Command: {cmd}\n\n")
print(f"[+] Running: {cmd}")
success, stdout, stderr = run_command_with_logging(cmd, config, log_file)
if not success:
print(f"[-] Redirector deployment failed. See {log_file} for details.")
# Track created resources for cleanup
try:
# Get instance ID for cleanup
instance_id = get_aws_instance_id(redirector_name, config)
if instance_id:
deployed_instances.append((redirector_name, instance_id))
# Also add key pair to resources for cleanup
deployed_resources.append(("key_pair", ssh_key_name))
except Exception as e:
print(f"[!] Failed to get instance ID: {e}")
return False, [log_file], deployed_instances + deployed_resources
# Track deployed instances and resources
instance_id = get_aws_instance_id(redirector_name, config)
if instance_id:
deployed_instances.append((redirector_name, instance_id))
deployed_resources.append(("key_pair", ssh_key_name))
# Store redirector IP and key path for SSH access later
config['redirector_ip'] = get_aws_instance_ip(redirector_name, config)
config['redirector_key_path'] = f"~/.ssh/{ssh_key_name}.pem"
elif extra_vars['c2_only']:
playbook = "AWS/c2.yml"
cmd = f"ansible-playbook -i {inventory_file} {playbook} -e '{extra_vars_str} {ami_map_str}'"
with open(log_file, 'a') as f:
f.write(f"==== RUNNING C2 PLAYBOOK ====\n")
f.write(f"Command: {cmd}\n\n")
print(f"[+] Running: {cmd}")
success, stdout, stderr = run_command_with_logging(cmd, config, log_file)
if not success:
print(f"[-] C2 server deployment failed. See {log_file} for details.")
# Track created resources for cleanup
try:
# Get instance ID for cleanup
instance_id = get_aws_instance_id(c2_name, config)
if instance_id:
deployed_instances.append((c2_name, instance_id))
# Also add key pair to resources for cleanup
deployed_resources.append(("key_pair", ssh_key_name))
except Exception as e:
print(f"[!] Failed to get instance ID: {e}")
return False, [log_file], deployed_instances + deployed_resources
# Track deployed instances and resources
instance_id = get_aws_instance_id(c2_name, config)
if instance_id:
deployed_instances.append((c2_name, instance_id))
deployed_resources.append(("key_pair", ssh_key_name))
# Store C2 IP and key path for SSH access later
config['c2_ip'] = get_aws_instance_ip(c2_name, config)
config['c2_key_path'] = f"~/.ssh/{ssh_key_name}.pem"
else:
# For full deployment, run redirector first, then C2
# First deploy redirector
redirector_cmd = f"ansible-playbook -i {inventory_file} AWS/redirector.yml -e '{extra_vars_str} {ami_map_str}'"
with open(log_file, 'a') as f:
f.write(f"==== RUNNING REDIRECTOR PLAYBOOK (FULL DEPLOYMENT) ====\n")
f.write(f"Command: {redirector_cmd}\n\n")
print(f"[+] Running: {redirector_cmd}")
success, stdout, stderr = run_command_with_logging(redirector_cmd, config, log_file)
if not success:
print(f"[-] Redirector deployment failed. See {log_file} for details.")
# Track created resources for cleanup
try:
# Get instance ID for cleanup
instance_id = get_aws_instance_id(redirector_name, config)
if instance_id:
deployed_instances.append((redirector_name, instance_id))
# Also add key pair to resources for cleanup
deployed_resources.append(("key_pair", ssh_key_name))
except Exception as e:
print(f"[!] Failed to get instance ID: {e}")
return False, [log_file], deployed_instances + deployed_resources
# Track deployed redirector
instance_id = get_aws_instance_id(redirector_name, config)
if instance_id:
deployed_instances.append((redirector_name, instance_id))
# Get and store redirector IP for C2 configuration
redirector_ip = get_aws_instance_ip(redirector_name, config)
config['redirector_ip'] = redirector_ip
config['redirector_key_path'] = f"~/.ssh/{ssh_key_name}.pem"
# Update extra vars with redirector IP
extra_vars['redirector_ip'] = redirector_ip
extra_vars_list.append(f"redirector_ip='{redirector_ip}'")
extra_vars_str = " ".join(extra_vars_list)
# Log updated extra_vars
with open(log_file, 'a') as f:
f.write(f"==== UPDATED EXTRA VARS WITH REDIRECTOR IP ====\n")
f.write(f"redirector_ip: {redirector_ip}\n")
f.write(f"==== END UPDATED EXTRA VARS ====\n\n")
# Then deploy C2 server with redirector IP
c2_cmd = f"ansible-playbook -i {inventory_file} AWS/c2.yml -e '{extra_vars_str} {ami_map_str}'"
with open(log_file, 'a') as f:
f.write(f"==== RUNNING C2 PLAYBOOK (FULL DEPLOYMENT) ====\n")
f.write(f"Command: {c2_cmd}\n\n")
print(f"[+] Running: {c2_cmd}")
success, stdout, stderr = run_command_with_logging(c2_cmd, config, log_file)
if not success:
print(f"[-] C2 server deployment failed. See {log_file} for details.")
# Track created resources for cleanup
try:
# Get instance ID for cleanup
instance_id = get_aws_instance_id(c2_name, config)
if instance_id:
deployed_instances.append((c2_name, instance_id))
except Exception as e:
print(f"[!] Failed to get instance ID: {e}")
return False, [log_file], deployed_instances + deployed_resources
# Track deployed C2
instance_id = get_aws_instance_id(c2_name, config)
if instance_id:
deployed_instances.append((c2_name, instance_id))
# Add key pair to resources
deployed_resources.append(("key_pair", ssh_key_name))
# Store C2 IP and key path for SSH access later
config['c2_ip'] = get_aws_instance_ip(c2_name, config)
config['c2_key_path'] = f"~/.ssh/{ssh_key_name}.pem"
# Deploy tracker if requested
if config.get('deploy_tracker'):
with open(log_file, 'a') as f:
f.write(f"==== DEPLOYING TRACKER MODULE ====\n")
tracker_success = deploy_tracker_module(config)
with open(log_file, 'a') as f:
f.write(f"Tracker deployment {'succeeded' if tracker_success else 'failed'}\n")
f.write(f"==== END TRACKER DEPLOYMENT ====\n\n")
if not tracker_success:
print("[!] Warning: Tracker deployment failed, but continuing with main deployment")
print("[+] AWS infrastructure deployed successfully!")
success = True
# Log success
with open(log_file, 'a') as f:
f.write(f"==== DEPLOYMENT SUCCESSFUL ====\n")
f.write(f"Deployed instances: {', '.join([n for n, _ in deployed_instances])}\n")
f.write(f"Deployed resources: {', '.join([f'{t}: {n}' for t, n in deployed_resources])}\n")
if 'redirector_ip' in config:
f.write(f"Redirector IP: {config['redirector_ip']}\n")
if 'c2_ip' in config:
f.write(f"C2 IP: {config['c2_ip']}\n")
f.write(f"==== END DEPLOYMENT SUCCESS ====\n\n")
return success, [log_file], deployed_instances + deployed_resources
except Exception as e:
# Log exception details
with open(log_file, 'a') as f:
f.write(f"==== DEPLOYMENT ERROR ====\n")
f.write(f"Error: {str(e)}\n")
import traceback
f.write(f"Traceback: {traceback.format_exc()}\n")
f.write(f"==== END DEPLOYMENT ERROR ====\n\n")
print(f"[-] Error: Failed to deploy AWS infrastructure: {e}")
return False, [log_file], deployed_instances + deployed_resources
finally:
# Clean up temporary inventory file
if os.path.exists(inventory_file):
with open(log_file, 'a') as f:
f.write(f"==== REMOVING INVENTORY FILE ====\n")
f.write(f"Removing file: {inventory_file}\n")
os.remove(inventory_file)
with open(log_file, 'a') as f:
f.write(f"Inventory file removed successfully\n")
f.write(f"==== END REMOVAL ====\n\n")
# If failure, clean up deployed instances
if not success and (deployed_instances or deployed_resources):
with open(log_file, 'a') as f:
f.write(f"==== CLEANING UP DEPLOYED RESOURCES ====\n")
print(f"[!] Cleaning up deployed resources due to failure...")
# Clean up EC2 instances
for instance_name, instance_id in deployed_instances:
with open(log_file, 'a') as f:
f.write(f"Cleaning up instance: {instance_name} (ID: {instance_id})\n")
try:
cleanup_cmd = f"aws ec2 terminate-instances --instance-ids {instance_id} --region {config.get('aws_region')}"
print(f"[+] Running cleanup command: {cleanup_cmd}")
subprocess.run(cleanup_cmd, shell=True, check=False)
print(f"[+] Terminated instance: {instance_name}")
with open(log_file, 'a') as f:
f.write(f"Instance {instance_name} terminated successfully\n")
except Exception as cleanup_err:
print(f"[!] Failed to terminate instance {instance_name}: {cleanup_err}")
with open(log_file, 'a') as f:
f.write(f"Failed to terminate instance {instance_name}: {cleanup_err}\n")
# Clean up other AWS resources (key pairs, etc.)
for resource_type, resource_name in deployed_resources:
with open(log_file, 'a') as f:
f.write(f"Cleaning up {resource_type}: {resource_name}\n")
try:
if resource_type == "key_pair":
cleanup_cmd = f"aws ec2 delete-key-pair --key-name {resource_name} --region {config.get('aws_region')}"
print(f"[+] Running cleanup command: {cleanup_cmd}")
subprocess.run(cleanup_cmd, shell=True, check=False)
print(f"[+] Deleted key pair: {resource_name}")
# Also remove local key file
local_key_path = f"~/.ssh/{resource_name}.pem"
expanded_path = os.path.expanduser(local_key_path)
if os.path.exists(expanded_path):
os.remove(expanded_path)
print(f"[+] Removed local key file: {local_key_path}")
with open(log_file, 'a') as f:
f.write(f"{resource_type} {resource_name} deleted successfully\n")
# Add more resource types here if needed
except Exception as cleanup_err:
print(f"[!] Failed to clean up {resource_type} {resource_name}: {cleanup_err}")
with open(log_file, 'a') as f:
f.write(f"Failed to clean up {resource_type} {resource_name}: {cleanup_err}\n")
with open(log_file, 'a') as f:
f.write(f"==== END CLEANUP ====\n\n")
def deploy_linode(config):
"""Deploy infrastructure using Linode provider with improved logging"""
print("[+] Deploying Linode infrastructure...")
# Set Linode environment variables
os.environ['LINODE_TOKEN'] = config['linode_token']
# Prepare SSH key
ssh_key, ssh_key_content = prepare_ssh_key(config)
if not ssh_key or not ssh_key_content:
print("[-] Failed to prepare SSH key, aborting deployment")
return False, [], []
# Create logs directory
log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs")
os.makedirs(log_dir, exist_ok=True)
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
# Only create one log file in the logs directory
log_file = os.path.join(log_dir, f"deployment_{timestamp}_linode.log")
print(f"[+] Creating deployment log at: {log_file}")
# Create a temporary inventory file for Ansible with detailed logging
inventory_file = f"inventory_linode_{int(time.time())}.yml"
with open(log_file, 'a') as f:
f.write(f"==== CREATING INVENTORY FILE: {inventory_file} ====\n")
deployed_instances = []
success = False
try:
# Write inventory file
with open(inventory_file, 'w') as f:
f.write("---\n")
f.write("all:\n")
f.write(" vars:\n")
f.write(f" ansible_ssh_private_key_file: {ssh_key}\n")
f.write(f" ansible_user: {config.get('ssh_user', 'root')}\n")
f.write(f" ansible_python_interpreter: /usr/bin/python3\n")
f.write(" children:\n")
f.write(" redirectors:\n")
f.write(" hosts:\n")
f.write(" redirector:\n")
f.write(" ansible_host: '{{ redirector_ip }}'\n")
f.write(" c2servers:\n")
f.write(" hosts:\n")
f.write(" c2:\n")
f.write(" ansible_host: '{{ c2_ip }}'\n")
# Log inventory file content
with open(log_file, 'a') as f:
f.write(f"INVENTORY CONTENT:\n")
with open(inventory_file, 'r') as inv:
f.write(inv.read())
f.write("\n==== END INVENTORY FILE ====\n\n")
# Generate default names if not provided
rand_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
timestamp = int(time.time()) % 10000 # Last 4 digits of timestamp
redirector_name = config.get('redirector_name', f"srv-{rand_suffix}-{timestamp}")
c2_name = config.get('c2_name', f"node-{rand_suffix}-{timestamp}")
# Update config with generated names for logging
config['redirector_name'] = redirector_name
config['c2_name'] = c2_name
# Log important information
with open(log_file, 'a') as f:
f.write(f"==== DEPLOYMENT INFORMATION ====\n")
f.write(f"SSH Key: {ssh_key}\n")
f.write(f"Redirector Name: {redirector_name}\n")
f.write(f"C2 Name: {c2_name}\n")
f.write(f"Domain: {config.get('domain', 'example.com')}\n")
f.write(f"==== END DEPLOYMENT INFORMATION ====\n\n")
# Build variables for command with ALL required parameters
extra_vars = {
'linode_token': config['linode_token'],
'region': config.get('linode_region', 'us-east'),
'plan': config.get('size', 'g6-standard-1'),
'redirector_name': redirector_name,
'c2_name': c2_name,
'teardown': config.get('teardown', False),
'disable_history': config.get('disable_history', True),
'secure_memory': config.get('secure_memory', True),
'zero_logs': config.get('zero_logs', True),
'redirector_only': config.get('redirector_only', False),
'c2_only': config.get('c2_only', False),
'debug': config.get('debug', False),
'domain': config.get('domain', 'example.com'),
'letsencrypt_email': config.get('letsencrypt_email', 'admin@example.com'),
'gophish_admin_port': config.get('gophish_admin_port', str(random.randint(2000, 9000))),
'smtp_auth_user': config.get('smtp_auth_user', f"user{random.randint(1000, 9999)}"),
'smtp_auth_pass': config.get('smtp_auth_pass', ''.join(random.choices(string.ascii_letters + string.digits + "!@#$%^&*", k=20))),
'ssh_key_path': ssh_key, # Path to the private key
'ssh_user': config.get('ssh_user', 'root'),
'instance_label': c2_name if config.get('c2_only', False) else redirector_name,
'linode_image': 'linode/debian11',
}
# Format extra vars for command line
extra_vars_list = []
for k, v in extra_vars.items():
if isinstance(v, bool):
extra_vars_list.append(f"{k}={str(v).lower()}")
elif isinstance(v, (int, float)):
extra_vars_list.append(f"{k}={v}")
elif isinstance(v, str):
# Escape quotes in string values
escaped_v = v.replace("'", "'\\''")
extra_vars_list.append(f"{k}='{escaped_v}'")
extra_vars_str = " ".join(extra_vars_list)
# Log extra vars for debugging (masking sensitive info)
with open(log_file, 'a') as f:
f.write("==== EXTRA VARS ====\n")
for k, v in extra_vars.items():
if k in ['linode_token', 'smtp_auth_pass']:
v_masked = str(v)[:4] + '****' if v else 'None'
f.write(f"{k}: {v_masked}\n")
else:
f.write(f"{k}: {v}\n")
f.write("==== END EXTRA VARS ====\n\n")
# Execute deployment based on deployment mode
if extra_vars['redirector_only']:
playbook = "Linode/redirector.yml"
cmd = f"ansible-playbook -i {inventory_file} {playbook} -e '{extra_vars_str}' -vvv"
with open(log_file, 'a') as f:
f.write(f"==== RUNNING REDIRECTOR PLAYBOOK ====\n")
f.write(f"Command: {cmd}\n\n")
print(f"[+] Running: {cmd}")
success, stdout, stderr = run_command_with_logging(cmd, config, log_file)
if not success:
print(f"[-] Redirector deployment failed. See {log_file} for details.")
# Get the deployed instance ID for cleanup
try:
linode_id = get_linode_id(redirector_name, config)
if linode_id:
deployed_instances.append((redirector_name, linode_id))
except Exception as e:
print(f"[!] Failed to get instance ID: {e}")
return False, [log_file], deployed_instances
# Store redirector IP and key path for SSH access later
linode_id = get_linode_id(redirector_name, config)
if linode_id:
deployed_instances.append((redirector_name, linode_id))
config['redirector_ip'] = get_linode_ip(linode_id, config)
config['redirector_key_path'] = ssh_key
elif extra_vars['c2_only']:
playbook = "Linode/c2.yml"
cmd = f"ansible-playbook -i {inventory_file} {playbook} -e '{extra_vars_str}' -vvv"
with open(log_file, 'a') as f:
f.write(f"==== RUNNING C2 PLAYBOOK ====\n")
f.write(f"Command: {cmd}\n\n")
print(f"[+] Running: {cmd}")
success, stdout, stderr = run_command_with_logging(cmd, config, log_file)
if not success:
print(f"[-] C2 server deployment failed. See {log_file} for details.")
# Get the deployed instance ID for cleanup
try:
linode_id = get_linode_id(c2_name, config)
if linode_id:
deployed_instances.append((c2_name, linode_id))
except Exception as e:
print(f"[!] Failed to get instance ID: {e}")
return False, [log_file], deployed_instances
# Store C2 IP and key path for SSH access later
linode_id = get_linode_id(c2_name, config)
if linode_id:
deployed_instances.append((c2_name, linode_id))
config['c2_ip'] = get_linode_ip(linode_id, config)
config['c2_key_path'] = ssh_key
else:
# For full deployment, run redirector first, then C2
redirector_cmd = f"ansible-playbook -i {inventory_file} Linode/redirector.yml -e '{extra_vars_str}' -vvv"
with open(log_file, 'a') as f:
f.write(f"==== RUNNING REDIRECTOR PLAYBOOK (FULL DEPLOYMENT) ====\n")
f.write(f"Command: {redirector_cmd}\n\n")
print(f"[+] Running: {redirector_cmd}")
success, stdout, stderr = run_command_with_logging(redirector_cmd, config, log_file)
if not success:
print(f"[-] Redirector deployment failed. See {log_file} for details.")
# Get the deployed instance ID for cleanup
try:
linode_id = get_linode_id(redirector_name, config)
if linode_id:
deployed_instances.append((redirector_name, linode_id))
except Exception as e:
print(f"[!] Failed to get instance ID: {e}")
return False, [log_file], deployed_instances
# Get and store redirector IP for C2 configuration
linode_id = get_linode_id(redirector_name, config)
if linode_id:
deployed_instances.append((redirector_name, linode_id))
redirector_ip = get_linode_ip(linode_id, config)
config['redirector_ip'] = redirector_ip
config['redirector_key_path'] = ssh_key
# Update extra vars with redirector IP
extra_vars['redirector_ip'] = redirector_ip
extra_vars_list.append(f"redirector_ip='{redirector_ip}'")
extra_vars_str = " ".join(extra_vars_list)
# Log updated extra_vars
with open(log_file, 'a') as f:
f.write(f"==== UPDATED EXTRA VARS WITH REDIRECTOR IP ====\n")
f.write(f"redirector_ip: {redirector_ip}\n")
f.write(f"==== END UPDATED EXTRA VARS ====\n\n")
# Then deploy C2 server with redirector IP
c2_cmd = f"ansible-playbook -i {inventory_file} Linode/c2.yml -e '{extra_vars_str}' -vvv"
with open(log_file, 'a') as f:
f.write(f"==== RUNNING C2 PLAYBOOK (FULL DEPLOYMENT) ====\n")
f.write(f"Command: {c2_cmd}\n\n")
print(f"[+] Running: {c2_cmd}")
success, stdout, stderr = run_command_with_logging(c2_cmd, config, log_file)
if not success:
print(f"[-] C2 server deployment failed. See {log_file} for details.")
# Get the deployed instance ID for cleanup
try:
linode_id = get_linode_id(c2_name, config)
if linode_id:
deployed_instances.append((c2_name, linode_id))
except Exception as e:
print(f"[!] Failed to get instance ID: {e}")
return False, [log_file], deployed_instances
# Store C2 IP and key path for SSH access later
linode_id = get_linode_id(c2_name, config)
if linode_id:
deployed_instances.append((c2_name, linode_id))
config['c2_ip'] = get_linode_ip(linode_id, config)
config['c2_key_path'] = ssh_key
# Deploy tracker if requested
if config.get('deploy_tracker'):
with open(log_file, 'a') as f:
f.write(f"==== DEPLOYING TRACKER MODULE ====\n")
tracker_success = deploy_tracker_module(config)
with open(log_file, 'a') as f:
f.write(f"Tracker deployment {'succeeded' if tracker_success else 'failed'}\n")
f.write(f"==== END TRACKER DEPLOYMENT ====\n\n")
if not tracker_success:
print("[!] Warning: Tracker deployment failed, but continuing with main deployment")
print("[+] Linode infrastructure deployed successfully!")
success = True
# Log success
with open(log_file, 'a') as f:
f.write(f"==== DEPLOYMENT SUCCESSFUL ====\n")
f.write(f"Deployed instances: {', '.join([n for n, _ in deployed_instances])}\n")
if 'redirector_ip' in config:
f.write(f"Redirector IP: {config['redirector_ip']}\n")
if 'c2_ip' in config:
f.write(f"C2 IP: {config['c2_ip']}\n")
f.write(f"==== END DEPLOYMENT SUCCESS ====\n\n")
return success, [log_file], deployed_instances
except Exception as e:
# Log exception details
with open(log_file, 'a') as f:
f.write(f"==== DEPLOYMENT ERROR ====\n")
f.write(f"Error: {str(e)}\n")
import traceback
f.write(f"Traceback: {traceback.format_exc()}\n")
f.write(f"==== END DEPLOYMENT ERROR ====\n\n")
print(f"[-] Error: Failed to deploy Linode infrastructure: {e}")
# Additional error information
with open(log_file, 'a') as f:
f.write(f"Failed deployment, deployed instances that need cleanup: {deployed_instances}\n")
return False, [log_file], deployed_instances
finally:
# Clean up temporary inventory file
if os.path.exists(inventory_file):
with open(log_file, 'a') as f:
f.write(f"==== REMOVING INVENTORY FILE ====\n")
f.write(f"Removing file: {inventory_file}\n")
os.remove(inventory_file)
with open(log_file, 'a') as f:
f.write(f"Inventory file removed successfully\n")
f.write(f"==== END REMOVAL ====\n\n")
# If failure, clean up deployed instances
if not success and deployed_instances:
with open(log_file, 'a') as f:
f.write(f"==== CLEANING UP DEPLOYED INSTANCES ====\n")
print(f"[!] Cleaning up deployed instances due to failure...")
for instance_name, instance_id in deployed_instances:
with open(log_file, 'a') as f:
f.write(f"Cleaning up instance: {instance_name} (ID: {instance_id})\n")
try:
cleanup_cmd = f"linode-cli linodes delete {instance_id} --yes"
print(f"[+] Running cleanup command: {cleanup_cmd}")
subprocess.run(cleanup_cmd, shell=True, check=False)
print(f"[+] Deleted instance: {instance_name}")
with open(log_file, 'a') as f:
f.write(f"Instance {instance_name} deleted successfully\n")
except Exception as cleanup_err:
print(f"[!] Failed to delete instance {instance_name}: {cleanup_err}")
with open(log_file, 'a') as f:
f.write(f"Failed to delete instance {instance_name}: {cleanup_err}\n")
with open(log_file, 'a') as f:
f.write(f"==== END CLEANUP ====\n\n")
# If this is a new key and deployment failed, remove it
if not success and ssh_key and ssh_key.startswith(os.path.expanduser("~/.ssh/c2itall_")):
try:
with open(log_file, 'a') as f:
f.write(f"==== REMOVING GENERATED SSH KEY ====\n")
f.write(f"Removing SSH key: {ssh_key}\n")
os.remove(ssh_key)
if os.path.exists(f"{ssh_key}.pub"):
os.remove(f"{ssh_key}.pub")
print(f"[+] Removed generated SSH key due to deployment failure")
with open(log_file, 'a') as f:
f.write(f"SSH key removed successfully\n")
f.write(f"==== END SSH KEY REMOVAL ====\n\n")
except Exception as key_err:
print(f"[!] Failed to remove SSH key {ssh_key}: {key_err}")
with open(log_file, 'a') as f:
f.write(f"Failed to remove SSH key: {key_err}\n")
f.write(f"==== END SSH KEY REMOVAL ====\n\n")
def deploy_flokinet(config):
"""Deploy infrastructure using FlokiNET provider"""
print("[+] Deploying FlokiNET infrastructure...")
# Prepare SSH key
ssh_key, ssh_key_content = prepare_ssh_key(config)
if not ssh_key or not ssh_key_content:
print("[-] Failed to prepare SSH key, aborting deployment")
return False, [], []
# Create logs directory
log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs")
os.makedirs(log_dir, exist_ok=True)
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
# Only create one log file in the logs directory
log_file = os.path.join(log_dir, f"deployment_{timestamp}_flokinet.log")
print(f"[+] Creating deployment log at: {log_file}")
# OPSEC: Use the random SSH port from config
ssh_port = config.get('ssh_port', random.randint(20000, 65000))
print(f"[+] Using SSH port: {ssh_port}")
# Prompt for IP addresses if not provided
redirector_ip = config.get('flokinet_redirector_ip')
c2_ip = config.get('flokinet_c2_ip')
# Handle partial deployment modes
if config['redirector_only'] and not redirector_ip:
redirector_ip = input("[?] Enter FlokiNET redirector IP address: ")
if not redirector_ip:
print("[-] Error: Redirector IP is required for deployment")
return False, [], []
# Use a placeholder for C2 if we're only deploying redirector
c2_ip = "127.0.0.1"
elif config['c2_only'] and not c2_ip:
c2_ip = input("[?] Enter FlokiNET C2 server IP address: ")
if not c2_ip:
print("[-] Error: C2 server IP is required for deployment")
return False, [], []
# If we're only deploying C2, but need redirector IP for configuration
if not redirector_ip:
# Try to read existing redirector IP from saved config
try:
with open('FlokiNET/vars.yaml', 'r') as f:
existing_config = yaml.safe_load(f)
if existing_config and 'redirector_ip' in existing_config:
redirector_ip = existing_config['redirector_ip']
if redirector_ip == "__REDIRECTOR_IP__": # Placeholder
redirector_ip = None
except (FileNotFoundError, yaml.YAMLError):
pass
# If still no redirector IP, prompt for it
if not redirector_ip:
redirector_ip = input("[?] Enter existing redirector IP address (needed for C2 configuration): ")
if not redirector_ip:
print("[-] Error: Existing redirector IP is required to configure C2 server")
return False, [], []
# Full deployment - need both IPs
elif not config['redirector_only'] and not config['c2_only']:
if not redirector_ip:
redirector_ip = input("[?] Enter FlokiNET redirector IP address: ")
if not c2_ip:
c2_ip = input("[?] Enter FlokiNET C2 server IP address: ")
if not redirector_ip or not c2_ip:
print("[-] Error: Both redirector and C2 IPs are required for full deployment")
return False, [], []
# Update config with obtained IPs
config['flokinet_redirector_ip'] = redirector_ip
config['flokinet_c2_ip'] = c2_ip
# Create FlokiNET directory if it doesn't exist
if not os.path.exists('FlokiNET'):
print("[+] Creating FlokiNET directory structure...")
os.makedirs('FlokiNET', exist_ok=True)
# Prepare vars.yaml with dynamic IPs
prepare_flokinet_vars(config)
# Log important information
with open(log_file, 'a') as f:
f.write(f"==== DEPLOYMENT INFORMATION ====\n")
f.write(f"SSH Key: {ssh_key}\n")
f.write(f"SSH Port: {ssh_port}\n")
f.write(f"Redirector IP: {redirector_ip}\n")
f.write(f"C2 IP: {c2_ip}\n")
f.write(f"Domain: {config.get('domain', 'example.com')}\n")
f.write(f"==== END DEPLOYMENT INFORMATION ====\n\n")
# Create temporary inventory files for Ansible with randomized names for OPSEC
inventory_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
redirector_inventory = None
c2_inventory = None
deployed_resources = []
success = False
try:
# Track resources for potential cleanup
deployed_resources = [
("redirector_ip", redirector_ip),
("c2_ip", c2_ip)
]
if not config['c2_only']:
redirector_inventory = f'inventory_r_{inventory_suffix}.yml'
with open(redirector_inventory, 'w') as f:
os.chmod(redirector_inventory, 0o600) # Secure permissions
f.write("---\n")
f.write("redirector:\n")
f.write(f" hosts:\n")
f.write(f" redirector:\n")
f.write(f" ansible_host: {redirector_ip}\n")
f.write(f" ansible_user: {config['ssh_user']}\n")
f.write(f" ansible_ssh_private_key_file: {ssh_key}\n")
f.write(f" ansible_python_interpreter: /usr/bin/python3\n")
f.write(f" ansible_port: {ssh_port}\n") # Use custom SSH port
# Log inventory file content
with open(log_file, 'a') as f:
f.write(f"==== REDIRECTOR INVENTORY: {redirector_inventory} ====\n")
with open(redirector_inventory, 'r') as inv:
f.write(inv.read())
f.write("\n==== END REDIRECTOR INVENTORY ====\n\n")
if not config['redirector_only']:
c2_inventory = f'inventory_c_{inventory_suffix}.yml'
with open(c2_inventory, 'w') as f:
os.chmod(c2_inventory, 0o600) # Secure permissions
f.write("---\n")
f.write("c2:\n")
f.write(f" hosts:\n")
f.write(f" c2:\n")
f.write(f" ansible_host: {c2_ip}\n")
f.write(f" ansible_user: {config['ssh_user']}\n")
f.write(f" ansible_ssh_private_key_file: {ssh_key}\n")
f.write(f" ansible_python_interpreter: /usr/bin/python3\n")
f.write(f" ansible_port: {ssh_port}\n") # Use custom SSH port
# Log inventory file content
with open(log_file, 'a') as f:
f.write(f"==== C2 INVENTORY: {c2_inventory} ====\n")
with open(c2_inventory, 'r') as inv:
f.write(inv.read())
f.write("\n==== END C2 INVENTORY ====\n\n")
# Build extra vars string
extra_vars = {
'redirector_ip': redirector_ip,
'c2_ip': c2_ip,
'disable_history': config.get('disable_history', True),
'secure_memory': config.get('secure_memory', True),
'zero_logs': config.get('zero_logs', True),
'domain': config.get('domain', 'example.com'),
'letsencrypt_email': config.get('letsencrypt_email', 'admin@example.com'),
'gophish_admin_port': config.get('gophish_admin_port', str(random.randint(2000, 9000))),
'smtp_auth_user': config.get('smtp_auth_user', f"user{random.randint(1000, 9999)}"),
'smtp_auth_pass': config.get('smtp_auth_pass', ''.join(random.choices(string.ascii_letters + string.digits + "!@#$%^&*", k=20))),
'ssh_port': ssh_port,
'ssh_key_path': ssh_key,
'ssh_user': config.get('ssh_user', 'root'),
}
# Format extra vars for command line, handling special characters
extra_vars_list = []
for k, v in extra_vars.items():
if isinstance(v, bool):
extra_vars_list.append(f"{k}={str(v).lower()}")
elif isinstance(v, (int, float)):
extra_vars_list.append(f"{k}={v}")
elif isinstance(v, str):
# Escape quotes in string values
escaped_v = v.replace("'", "'\\''")
extra_vars_list.append(f"{k}='{escaped_v}'")
extra_vars_str = " ".join(extra_vars_list)
# Log extra vars for debugging (masking sensitive info)
with open(log_file, 'a') as f:
f.write("==== EXTRA VARS ====\n")
for k, v in extra_vars.items():
if k in ['smtp_auth_pass']:
v_masked = str(v)[:4] + '****' if v else 'None'
f.write(f"{k}: {v_masked}\n")
else:
f.write(f"{k}: {v}\n")
f.write("==== END EXTRA VARS ====\n\n")
# Common provisioning for both servers
if not config['c2_only'] and redirector_inventory:
print("[+] Provisioning FlokiNET redirector...")
cmd = f"ansible-playbook -i {redirector_inventory} FlokiNET/provision.yml -e '{extra_vars_str}'"
with open(log_file, 'a') as f:
f.write(f"==== RUNNING REDIRECTOR PROVISION PLAYBOOK ====\n")
f.write(f"Command: {cmd}\n\n")
if config['debug']:
print(f"[+] Running: {cmd}")
success, stdout, stderr = run_command_with_logging(cmd, config, log_file)
if not success:
print(f"[-] Redirector provisioning failed. See {log_file} for details.")
return False, [log_file], deployed_resources
# Run redirector-specific playbook
print("[+] Configuring FlokiNET redirector...")
cmd = f"ansible-playbook -i {redirector_inventory} FlokiNET/redirector.yml -e '{extra_vars_str}'"
with open(log_file, 'a') as f:
f.write(f"==== RUNNING REDIRECTOR CONFIGURATION PLAYBOOK ====\n")
f.write(f"Command: {cmd}\n\n")
if config['debug']:
print(f"[+] Running: {cmd}")
success, stdout, stderr = run_command_with_logging(cmd, config, log_file)
if not success:
print(f"[-] Redirector configuration failed. See {log_file} for details.")
return False, [log_file], deployed_resources
# Store SSH key path for later
config['redirector_ip'] = redirector_ip
config['redirector_key_path'] = ssh_key
if not config['redirector_only'] and c2_inventory:
print("[+] Provisioning FlokiNET C2 server...")
cmd = f"ansible-playbook -i {c2_inventory} FlokiNET/provision.yml -e '{extra_vars_str}'"
with open(log_file, 'a') as f:
f.write(f"==== RUNNING C2 PROVISION PLAYBOOK ====\n")
f.write(f"Command: {cmd}\n\n")
if config['debug']:
print(f"[+] Running: {cmd}")
success, stdout, stderr = run_command_with_logging(cmd, config, log_file)
if not success:
print(f"[-] C2 server provisioning failed. See {log_file} for details.")
return False, [log_file], deployed_resources
# Run C2-specific playbook
print("[+] Configuring FlokiNET C2 server...")
cmd = f"ansible-playbook -i {c2_inventory} FlokiNET/c2.yml -e '{extra_vars_str}'"
with open(log_file, 'a') as f:
f.write(f"==== RUNNING C2 CONFIGURATION PLAYBOOK ====\n")
f.write(f"Command: {cmd}\n\n")
if config['debug']:
print(f"[+] Running: {cmd}")
success, stdout, stderr = run_command_with_logging(cmd, config, log_file)
if not success:
print(f"[-] C2 server configuration failed. See {log_file} for details.")
return False, [log_file], deployed_resources
# Store SSH key path for later
config['c2_ip'] = c2_ip
config['c2_key_path'] = ssh_key
# Deploy tracker if requested
if config.get('deploy_tracker'):
with open(log_file, 'a') as f:
f.write(f"==== DEPLOYING TRACKER MODULE ====\n")
tracker_success = deploy_tracker_module(config)
with open(log_file, 'a') as f:
f.write(f"Tracker deployment {'succeeded' if tracker_success else 'failed'}\n")
f.write(f"==== END TRACKER DEPLOYMENT ====\n\n")
if not tracker_success:
print("[!] Warning: Tracker deployment failed, but continuing with main deployment")
print("[+] FlokiNET infrastructure deployed successfully!")
success = True
# Log success
with open(log_file, 'a') as f:
f.write(f"==== DEPLOYMENT SUCCESSFUL ====\n")
if not config['c2_only']:
f.write(f"Redirector IP: {redirector_ip}\n")
if not config['redirector_only']:
f.write(f"C2 IP: {c2_ip}\n")
f.write(f"==== END DEPLOYMENT SUCCESS ====\n\n")
return success, [log_file], deployed_resources
except Exception as e:
# Log exception details
with open(log_file, 'a') as f:
f.write(f"==== DEPLOYMENT ERROR ====\n")
f.write(f"Error: {str(e)}\n")
import traceback
f.write(f"Traceback: {traceback.format_exc()}\n")
f.write(f"==== END DEPLOYMENT ERROR ====\n\n")
print(f"[-] Error: Failed to deploy FlokiNET infrastructure: {e}")
return False, [log_file], deployed_resources
finally:
# Clean up temporary inventory files
for inv_file in [redirector_inventory, c2_inventory]:
if inv_file and os.path.exists(inv_file):
# Securely overwrite before removal for better OPSEC
with open(inv_file, 'w') as f:
f.write('\0' * 1024) # Overwrite with null bytes
os.remove(inv_file)
with open(log_file, 'a') as f:
f.write(f"Inventory file {inv_file} removed successfully\n")
if config.get('debug'):
print(f"[+] Removed temporary inventory file: {inv_file}")
# If this is a new key and deployment failed, remove it
if not success and ssh_key and ssh_key.startswith(os.path.expanduser("~/.ssh/c2itall_")):
try:
with open(log_file, 'a') as f:
f.write(f"==== REMOVING GENERATED SSH KEY ====\n")
f.write(f"Removing SSH key: {ssh_key}\n")
os.remove(ssh_key)
if os.path.exists(f"{ssh_key}.pub"):
os.remove(f"{ssh_key}.pub")
print(f"[+] Removed generated SSH key due to deployment failure")
with open(log_file, 'a') as f:
f.write(f"SSH key removed successfully\n")
f.write(f"==== END SSH KEY REMOVAL ====\n\n")
except Exception as key_err:
print(f"[!] Failed to remove SSH key {ssh_key}: {key_err}")
with open(log_file, 'a') as f:
f.write(f"Failed to remove SSH key: {key_err}\n")
f.write(f"==== END SSH KEY REMOVAL ====\n\n")
def prepare_flokinet_vars(config):
"""Prepare FlokiNET vars.yaml with dynamic values"""
vars_file = 'FlokiNET/vars.yaml'
# Check if template exists, create it if not
if not os.path.exists(vars_file):
# Default template content with enhanced OPSEC
template = {
'flokinet_region_choices': ['iceland', 'romania', 'finland', 'netherlands'],
'redirector_ip': config['flokinet_redirector_ip'],
'c2_ip': config['flokinet_c2_ip'],
'domain': config['domain'],
'redirector_subdomain': 'cdn',
'c2_subdomain': 'mail',
'letsencrypt_email': config['letsencrypt_email'],
'ssh_port': config.get('ssh_port', random.randint(20000, 65000)),
'ssh_user': config['ssh_user'],
'ssh_key_path': config.get('ssh_key', '~/.ssh/id_ed25519.pub'),
'c2_framework': 'sliver',
'shell_handler_port': random.randint(10000, 65000), # Random port for better OPSEC
'shell_handler_protocol': 'http',
'disable_history': config['disable_history'],
'secure_memory': config['secure_memory'],
'zero_logs': config['zero_logs'],
'log_rotation_hours': 6,
'secure_delete_tools': True,
'memory_clear_interval': 60,
'infrastructure_lifespan': 72,
'auto_rotate_certs': True,
'cert_rotation_days': 30,
'gophish_admin_port': config.get('gophish_admin_port', str(random.randint(2000, 9000))),
'gophish_admin_domain': f"admin.{config['domain']}",
'gophish_site_domain': f"portal.{config['domain']}",
'smtp_auth_user': config.get('smtp_auth_user', f"user{random.randint(1000, 9999)}"),
'smtp_auth_pass': config.get('smtp_auth_pass', ''.join(random.choices(string.ascii_letters + string.digits + "!@#$%^&*", k=20)))
}
# Write template to file with secure permissions
with open(vars_file, 'w') as f:
os.chmod(vars_file, 0o600) # Readable only by owner
yaml.dump(template, f, default_flow_style=False)
print(f"[+] Created new FlokiNET configuration in {vars_file} (mode 0600)")
else:
# Read existing vars
with open(vars_file, 'r') as f:
vars_data = yaml.safe_load(f)
# Update with dynamic values
vars_data['redirector_ip'] = config['flokinet_redirector_ip']
vars_data['c2_ip'] = config['flokinet_c2_ip']
# Update domain and email if provided
vars_data['domain'] = config['domain']
vars_data['letsencrypt_email'] = config['letsencrypt_email']
# Update OPSEC settings
vars_data['disable_history'] = config['disable_history']
vars_data['secure_memory'] = config['secure_memory']
vars_data['zero_logs'] = config['zero_logs']
vars_data['ssh_port'] = config.get('ssh_port', vars_data.get('ssh_port', 22222))
# Write updated vars back to file
with open(vars_file, 'w') as f:
yaml.dump(vars_data, f, default_flow_style=False)
print(f"[+] Updated existing FlokiNET configuration in {vars_file}")
def ensure_provider_files_exist(config):
"""Ensure necessary files exist for the provider"""
provider = config['provider']
# Check for required directories
if not os.path.exists(provider):
print(f"[+] Creating {provider} directory structure...")
os.makedirs(provider, exist_ok=True)
# Basic file existence checks
required_files = {
'AWS': ['redirector.yml', 'c2.yml'],
'Linode': ['redirector.yml', 'c2.yml'],
'FlokiNET': ['provision.yml', 'redirector.yml', 'c2.yml']
}
# Check for shared files directory
if not os.path.exists('files'):
print("[+] Creating shared files directory...")
os.makedirs('files', exist_ok=True)
# Check for essential OPSEC scripts
essential_scripts = [
'clean-logs.sh',
'persistent-listener.sh',
'secure-exit.sh',
'serve-beacons.sh'
]
missing_files = []
# Check provider-specific files
for file in required_files.get(provider, []):
full_path = f"{provider}/{file}"
if not os.path.exists(full_path):
missing_files.append(full_path)
# Check shared OPSEC scripts
for script in essential_scripts:
if not os.path.exists(f"files/{script}"):
missing_files.append(f"files/{script}")
if missing_files:
print(f"[!] Warning: The following files are missing:")
for file in missing_files:
print(f" - {file}")
print(f"[!] You may need to create these files before deployment will work.")
return False
return True
def generate_ssh_key():
"""Generate a new SSH key with randomized name for better OPSEC"""
# Create a unique key name with random string
rand_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
timestamp = int(time.time()) % 10000 # Last 4 digits of timestamp
key_name = f"c2itall_{rand_suffix}_{timestamp}"
key_path = os.path.expanduser(f"~/.ssh/{key_name}")
print(f"[+] Generating new SSH key: {key_path}")
try:
subprocess.run(["ssh-keygen", "-t", "ed25519", "-f", key_path, "-N", ""], check=True)
# Set proper permissions
os.chmod(key_path, 0o600)
os.chmod(f"{key_path}.pub", 0o644)
# Log the key info for reference
print(f"[+] SSH private key: {key_path}")
print(f"[+] SSH public key: {key_path}.pub")
return key_path
except subprocess.CalledProcessError as e:
print(f"[-] Error: Failed to generate SSH key: {e}")
return None
def prepare_ssh_key(config):
"""Prepare and validate SSH key, generating a new one if needed"""
# Check if SSH key is provided
ssh_key = config.get('ssh_key')
# If key not provided or doesn't exist, generate a new one
if not ssh_key or not os.path.exists(os.path.expanduser(ssh_key)):
if ssh_key and not os.path.exists(os.path.expanduser(ssh_key)):
print(f"[-] Warning: Specified SSH key {ssh_key} not found")
ssh_key = generate_ssh_key()
if not ssh_key:
return None, None
config['ssh_key'] = ssh_key
# Make sure we're using the expanded path
ssh_key = os.path.expanduser(ssh_key)
# Ensure the public key exists
ssh_public_key = f"{ssh_key}.pub"
if not os.path.exists(ssh_public_key):
print(f"[-] Warning: SSH public key not found at {ssh_public_key}, generating new key pair")
ssh_key = generate_ssh_key()
if not ssh_key:
return None, None
config['ssh_key'] = ssh_key
ssh_public_key = f"{ssh_key}.pub"
# Read the public key content
try:
with open(ssh_public_key, 'r') as f:
ssh_key_content = f.read().strip()
# Verify and fix permissions
current_mode = os.stat(ssh_key).st_mode & 0o777
if current_mode != 0o600:
print(f"[!] SSH key has incorrect permissions: {oct(current_mode)[2:]}. Fixing to 0600...")
os.chmod(ssh_key, 0o600)
print(f"[+] SSH key permissions fixed")
return ssh_key, ssh_key_content
except Exception as e:
print(f"[-] Error reading SSH public key: {e}")
return None, None
def get_instance_ip(instance_name, config):
"""Get the IP address of an EC2 instance by name"""
try:
cmd = [
"aws", "ec2", "describe-instances",
"--filters", f"Name=tag:Name,Values={instance_name}",
"--query", "Reservations[0].Instances[0].PublicIpAddress",
"--output", "text",
"--region", config.get('aws_region', 'us-east-1')
]
result = subprocess.check_output(cmd).decode().strip()
if result == "None" or not result:
# Try again with a different command format that works better in some AWS CLI versions
cmd = [
"aws", "ec2", "describe-instances",
"--filters", f"Name=tag:Name,Values={instance_name}",
"Name=instance-state-name,Values=running",
"--query", "Reservations[*].Instances[*].PublicIpAddress",
"--output", "text",
"--region", config.get('aws_region', 'us-east-1')
]
result = subprocess.check_output(cmd).decode().strip()
if result and result != "None":
print(f"[+] Found IP address for {instance_name}: {result}")
return result
else:
print(f"[-] Could not find IP address for instance {instance_name}")
return None
except subprocess.CalledProcessError as e:
print(f"[-] Error getting instance IP: {e}")
return None
def get_linode_id(instance_name, config):
"""Get the ID of a Linode instance by name"""
try:
cmd = [
"linode-cli", "--json", "linodes", "list",
"--label", instance_name
]
result = subprocess.check_output(cmd, stderr=subprocess.PIPE)
# Parse the JSON response
instances = json.loads(result.decode())
if instances and len(instances) > 0:
linode_id = instances[0]['id']
print(f"[+] Found Linode ID for {instance_name}: {linode_id}")
return linode_id
else:
print(f"[-] Could not find Linode ID for instance {instance_name}")
return None
except subprocess.CalledProcessError as e:
print(f"[-] Error getting Linode ID: {e}")
print(f" Command output: {e.stderr.decode() if e.stderr else 'No error output'}")
return None
except Exception as e:
print(f"[-] Unexpected error getting Linode ID: {e}")
return None
def get_linode_ip(linode_id, config):
"""Get the IP address of a Linode instance by ID"""
try:
cmd = [
"linode-cli", "linodes", "view",
str(linode_id),
"--json"
]
result = subprocess.check_output(cmd).decode().strip()
# Parse the JSON response
instance = json.loads(result)
if instance and 'ipv4' in instance and instance['ipv4']:
ip = instance['ipv4'][0]
print(f"[+] Found IP address for Linode {linode_id}: {ip}")
return ip
else:
print(f"[-] Could not find IP address for Linode {linode_id}")
return None
except subprocess.CalledProcessError as e:
print(f"[-] Error getting Linode IP: {e}")
return None
def ssh_to_c2(config):
"""SSH into the C2 server after successful deployment"""
print("[+] Attempting to SSH into C2 server...")
# Get C2 server details
c2_ip = config.get('c2_ip')
key_path = config.get('c2_key_path')
# Check if we have the necessary information
if not c2_ip:
print(f"[-] Error: No C2 IP address found")
return False
if not key_path:
print(f"[-] Error: No SSH key path found for C2 server")
return False
# Determine user and port
user = config.get('ssh_user', 'root')
port = config.get('ssh_port', 22)
# Wait a few seconds to ensure everything is fully set up
print(f"[+] Waiting 10 seconds for SSH to be fully ready on {c2_ip}...")
time.sleep(10)
# Build the SSH command
ssh_cmd = [
"ssh",
"-i", key_path,
"-o", "StrictHostKeyChecking=no",
"-o", "UserKnownHostsFile=/dev/null", # For OPSEC, don't store the host key
]
# Add port if not standard
if port != 22:
ssh_cmd.extend(["-p", str(port)])
# Add user and host
ssh_cmd.append(f"{user}@{c2_ip}")
print(f"[+] Connecting to C2 server at {c2_ip}...")
print(f"[+] SSH command: {' '.join(ssh_cmd)}")
try:
# Execute SSH command
subprocess.call(ssh_cmd)
return True
except Exception as e:
print(f"[-] Error SSHing to C2 server: {e}")
return False
def cleanup_interrupted_deployment(provider, deployed_instances, generated_ssh_keys):
"""Clean up resources after an interrupted deployment"""
print("[+] Cleaning up deployed resources...")
# Clean up based on provider
if provider.lower() == 'aws':
for instance in deployed_instances:
if isinstance(instance, tuple) and len(instance) >= 2:
instance_name, instance_id = instance
try:
print(f"[+] Terminating AWS instance: {instance_name} (ID: {instance_id})")
cmd = f"aws ec2 terminate-instances --instance-ids {instance_id}"
subprocess.run(cmd, shell=True, check=False)
except Exception as e:
print(f"[!] Error terminating AWS instance {instance_name}: {e}")
elif provider.lower() == 'linode':
for instance in deployed_instances:
if isinstance(instance, tuple) and len(instance) >= 2:
instance_name, instance_id = instance
try:
print(f"[+] Deleting Linode instance: {instance_name} (ID: {instance_id})")
cmd = f"linode-cli linodes delete {instance_id} --yes"
subprocess.run(cmd, shell=True, check=False)
except Exception as e:
print(f"[!] Error deleting Linode instance {instance_name}: {e}")
elif provider.lower() == 'flokinet':
print("[!] FlokiNET instances must be cleaned up manually through their web interface.")
print("[!] Please log in to your FlokiNET account and remove the deployed instances.")
# Clean up generated SSH keys
for ssh_key in generated_ssh_keys:
try:
print(f"[+] Removing generated SSH key: {ssh_key}")
if os.path.exists(ssh_key):
os.remove(ssh_key)
if os.path.exists(f"{ssh_key}.pub"):
os.remove(f"{ssh_key}.pub")
except Exception as e:
print(f"[!] Error removing SSH key {ssh_key}: {e}")
print("[+] Cleanup completed.")
def run_command_with_logging(cmd, config, log_file=None):
"""Run a command and log both stdout and stderr to the deployment log"""
try:
print(f"[+] Running: {cmd}")
# If no log file specified, create or use the deployment log
if not log_file:
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs")
os.makedirs(log_dir, exist_ok=True)
log_file = os.path.join(log_dir, f"deployment_{timestamp}.log")
# Execute the command and capture output
process = subprocess.Popen(
cmd,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True
)
# Read output in real-time
stdout_data = ""
stderr_data = ""
# Append command to log file
with open(log_file, 'a') as f:
f.write(f"\n\n==== COMMAND EXECUTION: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} ====\n")
f.write(f"COMMAND: {cmd}\n\n")
f.write("OUTPUT:\n")
# Read and process output
while True:
stdout_line = process.stdout.readline()
stderr_line = process.stderr.readline()
if stdout_line == '' and stderr_line == '' and process.poll() is not None:
break
if stdout_line:
print(stdout_line.strip())
stdout_data += stdout_line
# Write to log file in real-time
with open(log_file, 'a') as f:
f.write(f"STDOUT: {stdout_line}")
if stderr_line:
print(stderr_line.strip(), file=sys.stderr)
stderr_data += stderr_line
# Write to log file in real-time
with open(log_file, 'a') as f:
f.write(f"STDERR: {stderr_line}")
# Wait for command to complete
return_code = process.wait()
# Log completion status
with open(log_file, 'a') as f:
f.write(f"\nRETURN CODE: {return_code}\n")
f.write(f"==== END COMMAND EXECUTION ====\n\n")
# Check if command succeeded
if return_code != 0:
raise subprocess.CalledProcessError(return_code, cmd, stdout_data, stderr_data)
return True, stdout_data, stderr_data
except subprocess.CalledProcessError as e:
# Log error details
with open(log_file, 'a') as f:
f.write(f"\nERROR: Command failed with return code {e.returncode}\n")
f.write("==== END COMMAND EXECUTION WITH ERROR ====\n\n")
print(f"[-] Command failed with return code {e.returncode}")
return False, e.stdout, e.stderr
except Exception as e:
# Log unexpected error
with open(log_file, 'a') as f:
f.write(f"\nUNEXPECTED ERROR: {str(e)}\n")
f.write("==== END COMMAND EXECUTION WITH UNEXPECTED ERROR ====\n\n")
print(f"[-] Unexpected error running command: {e}")
return False, "", str(e)
def run_tests(config):
"""Run basic connectivity and functionality tests"""
print("[+] Running connectivity tests...")
# Test both redirector and C2 if both were deployed
if not config.get('redirector_only') and not config.get('c2_only'):
# Test redirector first
redirector_ip = config.get('redirector_ip')
if redirector_ip:
print(f"[+] Testing redirector at {redirector_ip}...")
try:
# Try to ping the redirector
ping_cmd = ["ping", "-c", "3", redirector_ip]
subprocess.run(ping_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
print(f"[+] Redirector ping test successful")
# Test SSH connection
test_ssh(redirector_ip, config.get('redirector_key_path'), config.get('ssh_user', 'root'), config.get('ssh_port', 22))
# Test HTTP(S) connectivity if deployed with a domain
if config.get('domain') != 'example.com':
test_http(f"https://cdn.{config['domain']}")
except subprocess.CalledProcessError as e:
print(f"[-] Redirector connectivity test failed: {e}")
# Then test C2
c2_ip = config.get('c2_ip')
if c2_ip:
print(f"[+] Testing C2 server at {c2_ip}...")
try:
# Try to ping the C2 server
ping_cmd = ["ping", "-c", "3", c2_ip]
subprocess.run(ping_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
print(f"[+] C2 server ping test successful")
# Test SSH connection
test_ssh(c2_ip, config.get('c2_key_path'), config.get('ssh_user', 'root'), config.get('ssh_port', 22))
# Test HTTP(S) connectivity if deployed with a domain
if config.get('domain') != 'example.com':
test_http(f"https://mail.{config['domain']}")
except subprocess.CalledProcessError as e:
print(f"[-] C2 server connectivity test failed: {e}")
# Test only redirector if that's all that was deployed
elif config.get('redirector_only'):
redirector_ip = config.get('redirector_ip')
if redirector_ip:
print(f"[+] Testing redirector at {redirector_ip}...")
try:
# Try to ping the redirector
ping_cmd = ["ping", "-c", "3", redirector_ip]
subprocess.run(ping_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
print(f"[+] Redirector ping test successful")
# Test SSH connection
test_ssh(redirector_ip, config.get('redirector_key_path'), config.get('ssh_user', 'root'), config.get('ssh_port', 22))
# Test HTTP(S) connectivity if deployed with a domain
if config.get('domain') != 'example.com':
test_http(f"https://cdn.{config['domain']}")
except subprocess.CalledProcessError as e:
print(f"[-] Redirector connectivity test failed: {e}")
# Test only C2 if that's all that was deployed
elif config.get('c2_only'):
c2_ip = config.get('c2_ip')
if c2_ip:
print(f"[+] Testing C2 server at {c2_ip}...")
try:
# Try to ping the C2 server
ping_cmd = ["ping", "-c", "3", c2_ip]
subprocess.run(ping_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
print(f"[+] C2 server ping test successful")
# Test SSH connection
test_ssh(c2_ip, config.get('c2_key_path'), config.get('ssh_user', 'root'), config.get('ssh_port', 22))
# Test HTTP(S) connectivity if deployed with a domain
if config.get('domain') != 'example.com':
test_http(f"https://mail.{config['domain']}")
except subprocess.CalledProcessError as e:
print(f"[-] C2 server connectivity test failed: {e}")
print("[+] Tests completed")
return True
def test_ssh(ip, key_path, user, port):
"""Test SSH connectivity to a server"""
print(f"[+] Testing SSH connectivity to {ip}...")
try:
# Use the paramiko library for a programmatic SSH connection test
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# Connect with timeout
client.connect(ip, port=port, username=user, key_filename=key_path, timeout=10)
# Run a simple command to check if the connection works
stdin, stdout, stderr = client.exec_command("echo SSH test successful")
response = stdout.read().decode().strip()
# Close the connection
client.close()
print(f"[+] SSH test result: {response}")
return True
except Exception as e:
print(f"[-] SSH connectivity test failed: {e}")
return False
def test_http(url):
"""Test HTTP(S) connectivity to a URL"""
print(f"[+] Testing HTTP(S) connectivity to {url}...")
try:
# Use requests library with a timeout
response = subprocess.run(["curl", "-s", "-k", "-L", "--connect-timeout", "10", url], check=True, stdout=subprocess.PIPE)
# Check if we got a response
if response.stdout:
print(f"[+] HTTP(S) test successful")
return True
else:
print(f"[-] HTTP(S) test returned empty response")
return False
except subprocess.CalledProcessError as e:
print(f"[-] HTTP(S) connectivity test failed: {e}")
return False
def deploy_with_error_handling(provider_func, config):
"""Deploy with better error handling and logs"""
logs_created = []
deployed_resources = []
try:
result, logs, resources = provider_func(config)
logs_created.extend(logs)
deployed_resources.extend(resources)
return result, logs_created, deployed_resources
except subprocess.CalledProcessError as e:
# Log detailed error information
error_log = log_debug_info(e.cmd, e)
if error_log:
logs_created.append(error_log)
print(f"[-] Deployment failed: {e}. See {error_log} for detailed debug information.")
return False, logs_created, deployed_resources
except Exception as e:
error_log = log_debug_info("Unknown command", e)
if error_log:
logs_created.append(error_log)
print(f"[-] Unexpected error during deployment: {e}. See {error_log} for details.")
return False, logs_created, deployed_resources
def deploy_tracker_module(config):
"""Deploy tracker module alongside the C2 infrastructure"""
if not config.get('deploy_tracker'):
return True
print("[+] Deploying tracker module...")
# Make sure the module exists
tracker_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "modules/tracker")
if not os.path.exists(tracker_dir):
print(f"[-] Error: Tracker module directory not found at {tracker_dir}")
print("[-] Make sure the tracker module is installed in the modules/tracker directory")
return False
# Prepare tracker deploy command
tracker_script = os.path.join(tracker_dir, "Tools/deploy_tracker.py")
if not os.path.exists(tracker_script):
print(f"[-] Error: Tracker deployment script not found at {tracker_script}")
return False
# Build command arguments
tracker_cmd = [
"python3", tracker_script,
"--quick-deploy" # Use quick deployment mode
]
# Add domain if specified
if config.get('tracker_domain'):
tracker_cmd.extend(["--domain", config['tracker_domain']])
else:
tracker_cmd.append("--ip-only")
# Add email if specified
if config.get('tracker_email'):
tracker_cmd.extend(["--email", config['tracker_email']])
# Add tracker name if specified
if config.get('tracker_name'):
tracker_cmd.extend(["--engagement-name", config['tracker_name']])
# Add IPinfo token if specified
if config.get('tracker_ipinfo_token'):
tracker_cmd.extend(["--ipinfo-token", config['tracker_ipinfo_token']])
# Add SSL setup if requested
if config.get('tracker_setup_ssl'):
tracker_cmd.append("--setup-ssl")
# Add tracking pixel creation if requested
if config.get('tracker_create_pixel'):
tracker_cmd.append("--create-pixel")
# Run the tracker deployment command
try:
print(f"[+] Running tracker deployment: {' '.join(tracker_cmd)}")
subprocess.run(tracker_cmd, check=True)
print("[+] Tracker module deployed successfully")
return True
except subprocess.CalledProcessError as e:
print(f"[-] Error deploying tracker module: {e}")
return False
def interactive_config():
"""Interactively collect configuration"""
config = {}
# Initialize deployment mode flags
config['redirector_only'] = False
config['c2_only'] = False
# Select provider
print("\n=== Provider Selection ===")
print("Available providers:")
for i, provider in enumerate(PROVIDERS):
print(f" {i+1}. {provider}")
while True:
try:
choice = int(input("\nSelect provider (1-3): ")) - 1
if 0 <= choice < len(PROVIDERS):
config['provider'] = PROVIDERS[choice]
break
else:
print("Invalid choice. Please enter a number between 1 and 3.")
except ValueError:
print("Invalid input. Please enter a number.")
# Provider-specific configuration
if config['provider'] == 'aws':
print("\n=== AWS Configuration ===")
# Try to load defaults from vars.yaml
aws_vars = load_vars_file('AWS')
default_key = aws_vars.get('aws_access_key', '')
default_secret = aws_vars.get('aws_secret_key', '')
config['aws_key'] = input(f"AWS Access Key [{default_key[:4]}{'*'*(len(default_key)-4) if default_key else ''}]: ") or default_key
config['aws_secret'] = input(f"AWS Secret Key [{default_secret[:4]}{'*'*(len(default_secret)-4) if default_secret else ''}]: ") or default_secret
# Region choices
region_choices = aws_vars.get('aws_region_choices', [DEFAULT_REGION['aws']])
print("\nAvailable AWS regions:")
for i, region in enumerate(region_choices):
print(f" {i+1}. {region}")
use_random = input("\nUse random region? (Y/n): ").lower() != 'n'
if use_random:
config['aws_region'] = None # Will be randomized later
else:
while True:
try:
region_choice = int(input(f"Select region (1-{len(region_choices)}): ")) - 1
if 0 <= region_choice < len(region_choices):
config['aws_region'] = region_choices[region_choice]
break
except ValueError:
pass
print(f"Invalid choice. Please enter a number between 1 and {len(region_choices)}.")
# Ask for architecture type
print("\nDeployment architecture:")
print(" 1. Combined (single server for both redirector and C2)")
print(" 2. Split (separate servers for redirector and C2)")
arch_choice = input("\nSelect architecture (1-2, default: 2): ") or "2"
if arch_choice == "1":
config['redirector_only'] = False
config['c2_only'] = False
else:
# For split architecture, ask which components to deploy
comp_choice = input("\nDeploy (1: Both components, 2: Redirector only, 3: C2 only, default: 1): ") or "1"
if comp_choice == "2":
config['redirector_only'] = True
config['c2_only'] = False
elif comp_choice == "3":
config['redirector_only'] = False
config['c2_only'] = True
else:
config['redirector_only'] = False
config['c2_only'] = False
elif config['provider'] == 'linode':
print("\n=== Linode Configuration ===")
# Try to load defaults from vars.yaml
linode_vars = load_vars_file('Linode')
default_token = linode_vars.get('linode_token', '')
config['linode_token'] = input(f"Linode API Token [{default_token[:4]}{'*'*(len(default_token)-4) if default_token else ''}]: ") or default_token
# Region choices
region_choices = linode_vars.get('region_choices', [DEFAULT_REGION['linode']])
print("\nAvailable Linode regions:")
for i, region in enumerate(region_choices):
print(f" {i+1}. {region}")
use_random = input("\nUse random region? (Y/n): ").lower() != 'n'
if use_random:
config['linode_region'] = None # Will be randomized later
else:
while True:
try:
region_choice = int(input(f"Select region (1-{len(region_choices)}): ")) - 1
if 0 <= region_choice < len(region_choices):
config['linode_region'] = region_choices[region_choice]
break
except ValueError:
pass
print(f"Invalid choice. Please enter a number between 1 and {len(region_choices)}.")
# Ask for architecture type
print("\nDeployment architecture:")
print(" 1. Combined (single server for both redirector and C2)")
print(" 2. Split (separate servers for redirector and C2)")
arch_choice = input("\nSelect architecture (1-2, default: 2): ") or "2"
if arch_choice == "1":
config['redirector_only'] = False
config['c2_only'] = False
else:
# For split architecture, ask which components to deploy
comp_choice = input("\nDeploy (1: Both components, 2: Redirector only, 3: C2 only, default: 1): ") or "1"
if comp_choice == "2":
config['redirector_only'] = True
config['c2_only'] = False
elif comp_choice == "3":
config['redirector_only'] = False
config['c2_only'] = True
else:
config['redirector_only'] = False
config['c2_only'] = False
elif config['provider'] == 'flokinet':
print("\n=== FlokiNET Configuration ===")
print("Note: FlokiNET requires manual server setup due to their privacy focus")
# Try to load defaults from vars.yaml
flokinet_vars = load_vars_file('FlokiNET')
# Deployment mode
print("\nDeployment mode:")
print(" 1. Full deployment (redirector and C2)")
print(" 2. Redirector only")
print(" 3. C2 server only")
while True:
try:
mode = int(input("\nSelect deployment mode (1-3): "))
if 1 <= mode <= 3:
break
else:
print("Invalid choice. Please enter a number between 1 and 3.")
except ValueError:
print("Invalid input. Please enter a number.")
# Set deployment flags
if mode == 2:
config['redirector_only'] = True
config['c2_only'] = False
config['flokinet_redirector_ip'] = input("FlokiNET Redirector IP: ")
elif mode == 3:
config['redirector_only'] = False
config['c2_only'] = True
config['flokinet_c2_ip'] = input("FlokiNET C2 Server IP: ")
# Need redirector IP for configuration
config['flokinet_redirector_ip'] = input("Existing Redirector IP (for configuration): ")
else:
config['redirector_only'] = False
config['c2_only'] = False
config['flokinet_redirector_ip'] = input("FlokiNET Redirector IP: ")
config['flokinet_c2_ip'] = input("FlokiNET C2 Server IP: ")
# General configuration
print("\n=== General Configuration ===")
# SSH key and user
default_ssh_user = config.get('ssh_user', 'root')
config['ssh_user'] = input(f"SSH Username (default: {default_ssh_user}): ") or default_ssh_user
default_ssh_key = '~/.ssh/id_rsa'
if config['provider'] == 'aws':
ssh_key = input(f"Path to SSH private key (leave empty to generate): ")
else:
ssh_key = input(f"Path to SSH private key (default: {default_ssh_key}): ") or default_ssh_key
if not ssh_key and config['provider'] == 'aws':
ssh_key = generate_ssh_key()
config['ssh_key'] = ssh_key
# Domain configuration
default_domain = 'example.com'
config['domain'] = input(f"Domain name for infrastructure (default: {default_domain}): ") or default_domain
if config['domain'] != default_domain:
default_email = f"admin@{config['domain']}"
config['letsencrypt_email'] = input(f"Email for Let's Encrypt certificate (default: {default_email}): ") or default_email
else:
config['letsencrypt_email'] = 'admin@example.com'
# OPSEC settings
print("\n=== OPSEC Configuration ===")
disable_history = input("Disable command history? (Y/n): ").lower() or "y"
config['disable_history'] = disable_history.startswith('y')
secure_memory = input("Enable secure memory settings? (Y/n): ").lower() or "y"
config['secure_memory'] = secure_memory.startswith('y')
zero_logs = input("Enable zero-logs configuration? (Y/n): ").lower() or "y"
config['zero_logs'] = zero_logs.startswith('y')
# Post-deployment options
print("\n=== Post-Deployment Options ===")
ssh_after_deploy = input("SSH into instance after deployment? (y/N): ").lower()
config['ssh_after_deploy'] = ssh_after_deploy.startswith('y')
run_tests = input("Run connectivity tests after deployment? (y/N): ").lower()
config['run_tests'] = run_tests.startswith('y')
# Tracker module deployment
print("\n=== Tracker Module ===")
deploy_tracker = input("Deploy tracker module alongside C2 infrastructure? (y/N): ").lower()
config['deploy_tracker'] = deploy_tracker.startswith('y')
if config['deploy_tracker']:
# Additional tracker configuration
print("\n=== Tracker Configuration ===")
config['tracker_domain'] = input("Domain for tracker (leave empty to use server IP): ")
if config['tracker_domain']:
config['tracker_email'] = input(f"Email for tracker Let's Encrypt certificate: ") or config['letsencrypt_email']
setup_ssl = input("Set up SSL for tracker? (Y/n): ").lower() or "y"
config['tracker_setup_ssl'] = setup_ssl.startswith('y')
config['tracker_name'] = input("Tracker engagement name (leave empty for random): ")
config['tracker_ipinfo_token'] = input("IPinfo API token for geolocation (optional): ")
create_pixel = input("Create email tracking pixel? (Y/n): ").lower() or "y"
config['tracker_create_pixel'] = create_pixel.startswith('y')
return config
def cleanup_sensitive_files(config):
"""Clean up sensitive files after deployment for better OPSEC"""
print("\n[+] Starting cleanup of sensitive files...")
# Files to potentially clean up (prompt user first)
sensitive_files = []
# Add config file
if os.path.exists('config.yml'):
sensitive_files.append('config.yml')
print(f"[+] Found sensitive file: config.yml")
# Add log files - be more specific about deployment logs
log_patterns = ['deployment_*.log', 'deployment_*_linode.log', 'deployment_*_aws.log']
log_count = 0
# Search in current directory
for pattern in log_patterns:
for f in glob.glob(pattern):
sensitive_files.append(f)
print(f"[+] Found sensitive log file: {f}")
log_count += 1
# Also check the logs directory if it exists
log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs")
if os.path.exists(log_dir):
for pattern in log_patterns:
for f in glob.glob(os.path.join(log_dir, pattern)):
sensitive_files.append(f)
print(f"[+] Found sensitive log file in logs directory: {f}")
log_count += 1
print(f"[+] Found {log_count} deployment log files")
# Add inventory files
inventory_count = 0
for f in glob.glob('inventory_*.yml'):
sensitive_files.append(f)
print(f"[+] Found sensitive inventory file: {f}")
inventory_count += 1
print(f"[+] Found {inventory_count} inventory files")
# If there are sensitive files and user wants to clean up
if sensitive_files:
print("\n[!] The following sensitive files remain from deployment:")
for f in sensitive_files:
print(f" - {f}")
cleanup = input("\n[?] Would you like to securely delete these files? (y/N): ").lower()
if cleanup.startswith('y'):
for f in sensitive_files:
try:
print(f"[+] Securely deleting: {f}")
# Secure overwrite then delete
with open(f, 'wb') as file:
# Overwrite with random data 3 times
for i in range(3):
file_size = os.path.getsize(f)
file.write(os.urandom(file_size))
file.flush()
os.fsync(file.fileno())
print(f"[+] Completed overwrite pass {i+1}/3")
# Finally remove
os.remove(f)
print(f"[+] Successfully deleted: {f}")
except Exception as e:
print(f"[!] Error deleting {f}: {e}")
else:
print("[+] No sensitive files found to clean up")
# Remind about SSH keys
if config.get('ssh_key') and os.path.exists(os.path.expanduser(config['ssh_key'])):
ssh_key = os.path.expanduser(config['ssh_key'])
print(f"\n[!] Remember to secure or remove your SSH key after use: {ssh_key}")
secure_key = input("[?] Would you like to secure this SSH key now? (y/N): ").lower()
if secure_key.startswith('y'):
try:
# Rename the SSH key with random suffix
key_dir = os.path.dirname(ssh_key)
key_base = os.path.basename(ssh_key)
timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
new_key_path = os.path.join(key_dir, f"{key_base}.{timestamp}.bak")
print(f"[+] Renaming SSH key to: {new_key_path}")
# Move the key and public key
shutil.move(ssh_key, new_key_path)
if os.path.exists(f"{ssh_key}.pub"):
shutil.move(f"{ssh_key}.pub", f"{new_key_path}.pub")
print(f"[+] Also renamed public key")
# Set permissions
os.chmod(new_key_path, 0o400) # Read-only by owner
if os.path.exists(f"{new_key_path}.pub"):
os.chmod(f"{new_key_path}.pub", 0o400)
print(f"[+] SSH key secured and renamed to: {new_key_path}")
print(f"[+] This key is now read-only (mode 0400)")
except Exception as e:
print(f"[!] Error securing SSH key: {e}")
return True
def save_config(config):
"""Save configuration to config.yml file and create detailed deployment log"""
# Make a copy to avoid modifying the original
config_to_save = config.copy()
# Remove ALL sensitive data before saving
sensitive_keys = [
'aws_key', 'aws_secret', 'linode_token',
'smtp_auth_pass', 'ssh_key_content',
'flokinet_redirector_ip', 'flokinet_c2_ip',
'c2_ip', 'redirector_ip'
]
for key in sensitive_keys:
if key in config_to_save:
# Replace with asterisks, preserving first 2 chars for reference
value = config_to_save[key]
if isinstance(value, str) and len(value) > 4:
config_to_save[key] = f"{value[:2]}{'*' * (len(value) - 2)}"
else:
config_to_save[key] = '***'
# Use a more secure way to write sensitive files
# Ensure the file doesn't exist first to prevent race conditions
if os.path.exists('config.yml'):
# Instead of removing, just open in write mode to overwrite
pass
# Create with restricted permissions
with open('config.yml', 'w') as f:
os.chmod('config.yml', 0o600) # Readable only by owner
yaml.dump(config_to_save, f, default_flow_style=False)
print(f"[+] Configuration saved to config.yml (mode 0600)")
# Create deployment log with timestamp but with minimal sensitive info
log_file = f"deployment_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
# Create with restricted permissions
with open(log_file, 'w') as f:
os.chmod(log_file, 0o600) # Readable only by owner
f.write(f"# C2itAll Deployment Log\n")
f.write(f"# Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
# Log general info
f.write(f"Provider: {config['provider']}\n")
# Log SSH key information
f.write(f"SSH Key: {config.get('ssh_key', 'Not specified')}\n")
if 'ssh_key_name' in config:
f.write(f"SSH Key Name: {config['ssh_key_name']}\n")
# Log custom SSH port if used
if config.get('ssh_port') and config.get('ssh_port') != 22:
f.write(f"SSH Port: {config['ssh_port']}\n")
# Log provider-specific info without exposing sensitive details
if config['provider'] == 'aws':
f.write(f"AWS Region: {config.get('aws_region', 'random')}\n")
f.write(f"Instance Type: {config.get('size', 'default')}\n")
f.write(f"Redirector Name: {config.get('redirector_name', 'Not specified')}\n")
f.write(f"C2 Name: {config.get('c2_name', 'Not specified')}\n")
elif config['provider'] == 'linode':
f.write(f"Linode Region: {config.get('linode_region', 'random')}\n")
f.write(f"Plan: {config.get('plan', 'default')}\n")
f.write(f"Redirector Name: {config.get('redirector_name', 'Not specified')}\n")
f.write(f"C2 Name: {config.get('c2_name', 'Not specified')}\n")
elif config['provider'] == 'flokinet':
# IPs are sensitive for FlokiNET, only log region
f.write(f"FlokiNET Region: {config.get('region', 'anonymous')}\n")
# Note that we're not logging IPs directly
# Log domain info if provided
if config.get('domain'):
f.write(f"Domain: {config['domain']}\n")
if not config.get('c2_only'):
f.write(f"Redirector Domain: cdn.{config['domain']}\n")
if not config.get('redirector_only'):
f.write(f"C2 Domain: mail.{config['domain']}\n")
# Log gophish admin port
if config.get('gophish_admin_port'):
f.write(f"GoPhish Admin Port: {config['gophish_admin_port']}\n")
# Log SMTP auth user
if config.get('smtp_auth_user'):
f.write(f"SMTP Auth User: {config['smtp_auth_user']}\n")
# Log OPSEC settings
f.write(f"\nOPSEC Settings:\n")
f.write(f"- Zero-logs: {'Enabled' if config.get('zero_logs') else 'Disabled'}\n")
f.write(f"- Secure memory: {'Enabled' if config.get('secure_memory') else 'Disabled'}\n")
f.write(f"- History disabled: {'Yes' if config.get('disable_history') else 'No'}\n")
f.write(f"- Non-standard SSH port: {config.get('ssh_port', 'Default')}\n")
# Log tracker module info if deployed
if config.get('deploy_tracker'):
f.write(f"\nTracker Module:\n")
f.write(f"- Tracker Domain: {config.get('tracker_domain', 'IP-based')}\n")
f.write(f"- Engagement Name: {config.get('tracker_name', 'Auto-generated')}\n")
f.write(f"- Email Pixel: {'Enabled' if config.get('tracker_create_pixel') else 'Disabled'}\n")
print(f"[+] Deployment log saved to {log_file} (mode 0600)")
def show_deployment_summary(config):
"""Display a summary of the deployed infrastructure"""
print("\n" + "="*60)
print(" C2itAll Deployment Summary")
print("="*60)
# Show provider
print(f"Provider: {config['provider']}")
# Show infrastructure components
if config.get('c2_only'):
print("\nC2 Server:")
print(f" Name: {config.get('c2_name', 'c2')}")
print(f" IP: {config.get('c2_ip', 'Not available')}")
if config.get('domain') != 'example.com':
print(f" Domain: mail.{config['domain']}")
elif config.get('redirector_only'):
print("\nRedirector:")
print(f" Name: {config.get('redirector_name', 'redirector')}")
print(f" IP: {config.get('redirector_ip', 'Not available')}")
if config.get('domain') != 'example.com':
print(f" Domain: cdn.{config['domain']}")
else:
print("\nFull Infrastructure:")
print("\n- Redirector:")
print(f" Name: {config.get('redirector_name', 'redirector')}")
print(f" IP: {config.get('redirector_ip', 'Not available')}")
if config.get('domain') != 'example.com':
print(f" Domain: cdn.{config['domain']}")
print("\n- C2 Server:")
print(f" Name: {config.get('c2_name', 'c2')}")
print(f" IP: {config.get('c2_ip', 'Not available')}")
if config.get('domain') != 'example.com':
print(f" Domain: mail.{config['domain']}")
# Show SSH connection details
print("\nSSH Access:")
user = config.get('ssh_user', 'root')
if config.get('redirector_ip') and not config.get('c2_only'):
redirector_key = config.get('redirector_key_path', config.get('ssh_key', 'No key available'))
port = config.get('ssh_port', 22)
print(f" Redirector: ssh -i {redirector_key}{' -p ' + str(port) if port != 22 else ''} {user}@{config.get('redirector_ip')}")
if config.get('c2_ip') and not config.get('redirector_only'):
c2_key = config.get('c2_key_path', config.get('ssh_key', 'No key available'))
port = config.get('ssh_port', 22)
print(f" C2 Server: ssh -i {c2_key}{' -p ' + str(port) if port != 22 else ''} {user}@{config.get('c2_ip')}")
# Show domain configuration if used
if config.get('domain') != 'example.com':
print("\nDomain Configuration:")
print(f" Domain: {config['domain']}")
print(" Required DNS Records:")
if not config.get('c2_only'):
print(f" cdn.{config['domain']} -> {config.get('redirector_ip', 'IP_NOT_AVAILABLE')}")
if not config.get('redirector_only'):
print(f" mail.{config['domain']} -> {config.get('c2_ip', 'IP_NOT_AVAILABLE')}")
# Show tracker details if deployed
if config.get('deploy_tracker'):
print("\nTracker Module:")
print(f" Engagement: {config.get('tracker_name', 'Auto-generated')}")
if config.get('tracker_domain'):
print(f" Domain: {config['tracker_domain']}")
print(f" DNS Record: {config['tracker_domain']} -> {config.get('redirector_ip', config.get('c2_ip', 'IP_NOT_AVAILABLE'))}")
else:
print(f" Access: http://{config.get('redirector_ip', config.get('c2_ip', 'IP_NOT_AVAILABLE'))}")
if config.get('tracker_create_pixel'):
print(" Email Tracking: Enabled")
hostname = config.get('tracker_domain') or config.get('redirector_ip', config.get('c2_ip', 'IP_NOT_AVAILABLE'))
protocol = "https" if config.get('tracker_setup_ssl') else "http"
print(f" Tracking Pixel: <img src=\"{protocol}://{hostname}/track-open?email=TARGET_EMAIL\" width=\"1\" height=\"1\" style=\"display:none;\">")
print("\nFor more details, see deployment log file.")
print("="*60)
def log_debug_info(command, error, log_file=None):
"""Log detailed debug information when a deployment fails"""
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
# If no log file is specified, create one
if not log_file:
log_file = f"deployment_error_{int(time.time())}.log"
with open(log_file, 'a') as f:
f.write(f"==== ERROR LOG: {timestamp} ====\n")
f.write(f"COMMAND: {command}\n")
f.write(f"ERROR: {str(error)}\n\n")
# Try to get additional error information from ansible logs
try:
# Look for ansible log files
ansible_logs = [file for file in os.listdir('/tmp') if 'ansible-' in file and '.log' in file]
if ansible_logs:
f.write("ANSIBLE LOGS:\n")
# Get the most recent ansible log
latest_log = sorted(ansible_logs, key=lambda x: os.path.getmtime(os.path.join('/tmp', x)))[-1]
with open(os.path.join('/tmp', latest_log), 'r') as log:
f.write(log.read())
except Exception as e:
f.write(f"Failed to retrieve ansible logs: {e}\n")
f.write("==== END ERROR LOG ====\n\n")
print(f"[+] Debug information logged to {log_file}")
return log_file
def main():
"""Main function"""
print("========================================")
print("C2itAll - Red Team Infrastructure Setup")
print("========================================")
# Set up error logging
error_log_file = setup_error_logging()
# Track deployed resources for cleanup on interruption
deployed_instances = []
generated_ssh_keys = []
try:
args = setup_argparse()
# Interactive mode if no arguments provided
if len(sys.argv) == 1:
config = interactive_config()
else:
config = load_config(args)
# Normalize provider name to proper capitalization
config['provider'] = normalize_provider_name(config['provider'])
# Validate configuration
if not validate_config(config):
return
# Select region
select_region(config)
# Ensure provider files exist
if not ensure_provider_files_exist(config):
proceed = input("\n[?] Continue despite missing files? (y/N): ").lower()
if not proceed.startswith('y'):
print("[-] Deployment aborted.")
return
# Save configuration
save_config(config)
# Keep track of any generated SSH keys
if config.get('ssh_key') and config['ssh_key'].startswith(os.path.expanduser("~/.ssh/c2itall_")):
generated_ssh_keys.append(config['ssh_key'])
# Track deployment logs
deployment_logs = []
# Deploy infrastructure based on provider
success = False
try:
if config['provider'].lower() == 'aws':
# Verify SSH key permissions before deployment
if config.get('ssh_key'):
verify_key_file_permissions(os.path.expanduser(config['ssh_key']))
success, logs, aws_resources = deploy_with_error_handling(deploy_aws, config)
deployment_logs.extend(logs)
deployed_instances.extend(aws_resources)
elif config['provider'].lower() == 'linode':
# Verify SSH key permissions before deployment
if config.get('ssh_key'):
verify_key_file_permissions(os.path.expanduser(config['ssh_key']))
success, logs, linode_resources = deploy_with_error_handling(deploy_linode, config)
deployment_logs.extend(logs)
deployed_instances.extend(linode_resources)
elif config['provider'].lower() == 'flokinet':
success, logs, flokinet_resources = deploy_with_error_handling(deploy_flokinet, config)
deployment_logs.extend(logs)
deployed_instances.extend(flokinet_resources)
else:
print(f"[-] Error: Unsupported provider: {config['provider']}")
return
except Exception as e:
error_log = log_debug_info("Unexpected error", e, error_log_file)
if error_log:
deployment_logs.append(error_log)
print(f"[-] Unexpected error during deployment: {e}")
print(f"[+] See {error_log_file} for detailed error information")
success = False
# Show result and handle cleanup
if success:
print("\n[+] Deployment completed successfully!")
show_deployment_summary(config)
# Only SSH if deployment was successful and user requested it
if config.get('ssh_after_deploy'):
print("\n[+] Preparing to SSH into C2 server...")
ssh_to_c2(config)
else:
print("\n[-] Deployment failed.")
if error_log_file:
print(f"[+] Check {error_log_file} for detailed error information")
# Add all discovered logs to the config
config['deployment_logs'] = deployment_logs
# Always ask if user wants to clean up sensitive files
cleanup_option = "y/N" if not success else "Y/n"
cleanup_default = "n" if not success else "y"
cleanup = input(f"\n[?] Would you like to clean up sensitive files{' despite deployment failure' if not success else ''}? ({cleanup_option}): ").lower() or cleanup_default
if cleanup.startswith('y'):
cleanup_sensitive_files(config)
except KeyboardInterrupt:
print("\n\n[!] Deployment interrupted by user (Ctrl+C).")
if deployed_instances:
print(f"[!] The following resources were deployed:")
for instance in deployed_instances:
if isinstance(instance, tuple) and len(instance) >= 2:
print(f" - {instance[0]} (ID: {instance[1]})")
else:
print(f" - {instance}")
cleanup = input("\n[?] Would you like to clean up deployed resources before exiting? (Y/n): ").lower() or "y"
if cleanup.startswith('y'):
cleanup_interrupted_deployment(config['provider'], deployed_instances, generated_ssh_keys)
else:
print("[!] Deployed resources will remain active. You will need to clean them up manually.")
print("[+] Exiting...")
sys.exit(1)
if __name__ == "__main__":
main()