Files
CoM-c2itall/deploy.py
T
n0mad1k 241668a84d init
2025-04-09 15:22:21 -04:00

1246 lines
53 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
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 setup_argparse():
"""Set up and return the argument parser"""
parser = argparse.ArgumentParser(description='Deploy 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')
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
# 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))
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 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
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 a temporary inventory file for Ansible
with open('inventory_aws.yml', 'w') as f:
f.write("---\n")
f.write("all:\n")
f.write(" vars:\n")
f.write(f" ansible_ssh_private_key_file: {config['ssh_key']}\n")
f.write(f" ansible_user: {config['ssh_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")
# Run AWS provisioning playbook
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['aws_region'],
'ami_map': config['ami_map'],
'size': config['size'],
'aws_instance_type': config['size'],
'redirector_name': config['redirector_name'],
'c2_name': config['c2_name'],
'teardown': config['teardown'],
'disable_history': config['disable_history'],
'secure_memory': config['secure_memory'],
'zero_logs': config['zero_logs'],
'redirector_only': config['redirector_only'],
'c2_only': config['c2_only'],
'debug': config['debug'],
'domain': config['domain'],
'letsencrypt_email': config['letsencrypt_email'],
'gophish_admin_port': config['gophish_admin_port'],
'smtp_auth_user': config['smtp_auth_user'],
'smtp_auth_pass': config['smtp_auth_pass'],
}
extra_vars_str = ' '.join([f"{k}={v}" for k, v in extra_vars.items() if not isinstance(v, dict)])
# Handle the ami_map special case
ami_map_str = ""
if config['ami_map']:
ami_map_str = f"ami_map='{json.dumps(config['ami_map'])}'"
# Determine playbook to run based on deployment mode
if config['redirector_only']:
playbook = "AWS/redirector.yml"
cmd = f"ansible-playbook -i inventory_aws.yml {playbook} -e '{extra_vars_str} {ami_map_str}'"
if config['debug']:
print(f"[+] Running: {cmd}")
subprocess.run(cmd, shell=True, check=True)
elif config['c2_only']:
playbook = "AWS/c2.yml"
cmd = f"ansible-playbook -i inventory_aws.yml {playbook} -e '{extra_vars_str} {ami_map_str}'"
if config['debug']:
print(f"[+] Running: {cmd}")
subprocess.run(cmd, shell=True, check=True)
else:
# For full deployment, run redirector first, then C2
# First deploy redirector
redirector_cmd = f"ansible-playbook -i inventory_aws.yml AWS/redirector.yml -e '{extra_vars_str} {ami_map_str}'"
if config['debug']:
print(f"[+] Running: {redirector_cmd}")
subprocess.run(redirector_cmd, shell=True, check=True)
# Then deploy C2 server with redirector IP
c2_cmd = f"ansible-playbook -i inventory_aws.yml AWS/c2.yml -e '{extra_vars_str} {ami_map_str}'"
if config['debug']:
print(f"[+] Running: {c2_cmd}")
subprocess.run(c2_cmd, shell=True, check=True)
print("[+] AWS infrastructure deployed successfully!")
return True
except subprocess.CalledProcessError as e:
print(f"[-] Error: Failed to deploy AWS infrastructure: {e}")
return False
finally:
# Clean up temporary inventory file
if os.path.exists('inventory_aws.yml'):
os.remove('inventory_aws.yml')
def deploy_linode(config):
"""Deploy infrastructure using Linode provider"""
print("[+] Deploying Linode infrastructure...")
# Set Linode environment variables
os.environ['LINODE_TOKEN'] = config['linode_token']
# Create a temporary inventory file for Ansible
with open('inventory_linode.yml', 'w') as f:
f.write("---\n")
f.write("all:\n")
f.write(" vars:\n")
f.write(f" ansible_ssh_private_key_file: {config['ssh_key']}\n")
f.write(f" ansible_user: {config['ssh_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")
# Run Linode provisioning playbook
try:
extra_vars = {
'linode_token': config['linode_token'],
'region': config['linode_region'],
'plan': config['size'],
'redirector_name': config['redirector_name'],
'c2_name': config['c2_name'],
'teardown': config['teardown'],
'disable_history': config['disable_history'],
'secure_memory': config['secure_memory'],
'zero_logs': config['zero_logs'],
'redirector_only': config['redirector_only'],
'c2_only': config['c2_only'],
'debug': config['debug'],
'domain': config['domain'],
'letsencrypt_email': config['letsencrypt_email'],
'gophish_admin_port': config['gophish_admin_port'],
'smtp_auth_user': config['smtp_auth_user'],
'smtp_auth_pass': config['smtp_auth_pass'],
}
extra_vars_str = ' '.join([f"{k}={v}" for k, v in extra_vars.items()])
# Determine playbook to run based on deployment mode
if config['redirector_only']:
playbook = "Linode/redirector.yml"
cmd = f"ansible-playbook -i inventory_linode.yml {playbook} -e '{extra_vars_str}'"
if config['debug']:
print(f"[+] Running: {cmd}")
subprocess.run(cmd, shell=True, check=True)
elif config['c2_only']:
playbook = "Linode/c2.yml"
cmd = f"ansible-playbook -i inventory_linode.yml {playbook} -e '{extra_vars_str}'"
if config['debug']:
print(f"[+] Running: {cmd}")
subprocess.run(cmd, shell=True, check=True)
else:
# For full deployment, run redirector first, then C2
# First deploy redirector
redirector_cmd = f"ansible-playbook -i inventory_linode.yml Linode/redirector.yml -e '{extra_vars_str}'"
if config['debug']:
print(f"[+] Running: {redirector_cmd}")
subprocess.run(redirector_cmd, shell=True, check=True)
# Then deploy C2 server with redirector IP
c2_cmd = f"ansible-playbook -i inventory_linode.yml Linode/c2.yml -e '{extra_vars_str}'"
if config['debug']:
print(f"[+] Running: {c2_cmd}")
subprocess.run(c2_cmd, shell=True, check=True)
print("[+] Linode infrastructure deployed successfully!")
return True
except subprocess.CalledProcessError as e:
print(f"[-] Error: Failed to deploy Linode infrastructure: {e}")
return False
finally:
# Clean up temporary inventory file
if os.path.exists('inventory_linode.yml'):
os.remove('inventory_linode.yml')
def deploy_flokinet(config):
"""Deploy infrastructure using FlokiNET provider"""
print("[+] Deploying FlokiNET infrastructure...")
# 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)
# 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
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")
if config.get('ssh_key'):
f.write(f" ansible_ssh_private_key_file: {config['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
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")
if config.get('ssh_key'):
f.write(f" ansible_ssh_private_key_file: {config['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
# Run FlokiNET provisioning playbooks
try:
# Build extra vars string
extra_vars = {
'redirector_ip': redirector_ip,
'c2_ip': c2_ip,
'disable_history': config['disable_history'],
'secure_memory': config['secure_memory'],
'zero_logs': config['zero_logs'],
'domain': config['domain'],
'letsencrypt_email': config['letsencrypt_email'],
'gophish_admin_port': config['gophish_admin_port'],
'smtp_auth_user': config['smtp_auth_user'],
'smtp_auth_pass': config['smtp_auth_pass'],
'ssh_port': ssh_port
}
extra_vars_str = ' '.join([f"{k}='{v}'" for k, v in extra_vars.items()])
# 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}'"
if config['debug']:
print(f"[+] Running: {cmd}")
subprocess.run(cmd, shell=True, check=True)
# Run redirector-specific playbook
print("[+] Configuring FlokiNET redirector...")
cmd = f"ansible-playbook -i {redirector_inventory} FlokiNET/redirector.yml -e '{extra_vars_str}'"
if config['debug']:
print(f"[+] Running: {cmd}")
subprocess.run(cmd, shell=True, check=True)
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}'"
if config['debug']:
print(f"[+] Running: {cmd}")
subprocess.run(cmd, shell=True, check=True)
# Run C2-specific playbook
print("[+] Configuring FlokiNET C2 server...")
cmd = f"ansible-playbook -i {c2_inventory} FlokiNET/c2.yml -e '{extra_vars_str}'"
if config['debug']:
print(f"[+] Running: {cmd}")
subprocess.run(cmd, shell=True, check=True)
print("[+] FlokiNET infrastructure deployed successfully!")
return True
except subprocess.CalledProcessError as e:
print(f"[-] Error: Failed to deploy FlokiNET infrastructure: {e}")
return False
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)
if config['debug']:
print(f"[+] Removed temporary inventory file: {inv_file}")
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.lower(), []):
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(instance_label=None):
"""Generate a new SSH key with randomized name for better OPSEC"""
# Create a unique key name based on instance label or random string
if not instance_label:
instance_label = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
key_name = f"c2ingred_{instance_label}_{int(time.time())}"
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)
return key_path
except subprocess.CalledProcessError as e:
print(f"[-] Error: Failed to generate SSH key: {e}")
return None
def interactive_config():
"""Interactively collect configuration"""
config = {}
# Initialize deployment mode flags (these were missing)
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 (if not already set)
if not config.get('redirector_only') and not config.get('c2_only'):
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 (if not already set)
if not config.get('redirector_only') and not config.get('c2_only'):
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')
return config
def cleanup_sensitive_files(config):
"""Clean up sensitive files after deployment for better OPSEC"""
# Files to potentially clean up (prompt user first)
sensitive_files = []
# Add config file
if os.path.exists('config.yml'):
sensitive_files.append('config.yml')
# Add log files
for f in os.listdir('.'):
if f.startswith('deployment_') and f.endswith('.log'):
sensitive_files.append(f)
# Add inventory files
for f in os.listdir('.'):
if f.startswith('inventory_') and f.endswith('.yml'):
sensitive_files.append(f)
# 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:
# Secure overwrite then delete
with open(f, 'wb') as file:
# Overwrite with random data 3 times
for _ in range(3):
file.write(os.urandom(os.path.getsize(f)))
file.flush()
os.fsync(file.fileno())
# Finally remove
os.remove(f)
print(f"[+] Securely deleted: {f}")
except Exception as e:
print(f"[!] Error deleting {f}: {e}")
# Remind about SSH keys
if config.get('ssh_key') and os.path.exists(config['ssh_key']):
print(f"\n[!] Remember to secure or remove your SSH key after use: {config['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(config['ssh_key'])
key_base = os.path.basename(config['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")
# Move the key and public key
shutil.move(config['ssh_key'], new_key_path)
if os.path.exists(f"{config['ssh_key']}.pub"):
shutil.move(f"{config['ssh_key']}.pub", f"{new_key_path}.pub")
# 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"""
# 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', 'ssh_key_path',
'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'):
os.remove('config.yml')
# 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"# C2ingRed 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 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['redirector_name']}\n")
f.write(f"C2 Name: {config['c2_name']}\n")
elif config['provider'] == 'linode':
f.write(f"Linode Region: {config.get('linode_region', 'random')}\n")
f.write(f"Plan: {config.get('size', 'default')}\n")
f.write(f"Redirector Name: {config['redirector_name']}\n")
f.write(f"C2 Name: {config['c2_name']}\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 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")
print(f"[+] Deployment log saved to {log_file} (mode 0600)")
def main():
"""Main function"""
print("========================================")
print("C2ingRed - Red Team Infrastructure Setup")
print("========================================")
args = setup_argparse()
# Interactive mode if no arguments provided
if len(sys.argv) == 1:
config = interactive_config()
else:
config = load_config(args)
# 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)
success = False
try:
# Deploy infrastructure based on provider
if config['provider'] == 'aws':
success = deploy_aws(config)
elif config['provider'] == 'linode':
success = deploy_linode(config)
elif config['provider'] == 'flokinet':
success = deploy_flokinet(config)
else:
print(f"[-] Error: Unsupported provider: {config['provider']}")
return
finally:
# OPSEC: Always offer to clean up sensitive files, regardless of success
if not config.get('debug'): # Skip cleanup in debug mode
cleanup_sensitive_files(config)
if success:
print("\n[+] Deployment completed successfully!")
if config['provider'] != 'flokinet':
print("\n[+] Your infrastructure is now ready to use!")
print(f"[+] Redirector: {config['redirector_name']}")
print(f"[+] C2 Server: {config['c2_name']}")
else:
print("\n[+] Your FlokiNET infrastructure is now configured with:")
print(f"[+] Zero-logs configuration")
print(f"[+] Automated shell handler")
print(f"[+] Sliver C2 framework")
print(f"[+] Anti-forensics capabilities")
print("\n[+] To use the automated shell handler, configure your Rubber Ducky with:")
print(f"[+] Redirector IP: {config['flokinet_redirector_ip']}")
if config.get('domain'):
print(f"[+] or Domain: cdn.{config['domain']}")
print(f"[+] Port: {config.get('shell_handler_port', 4444)} (shell handler port)")
# Remind about DNS configuration
if config.get('domain'):
print("\n[!] Don't forget to configure your DNS records:")
if not config.get('c2_only'):
print(f"[!] - Add A record for cdn.{config['domain']} pointing to {config['flokinet_redirector_ip']}")
if not config.get('redirector_only'):
print(f"[!] - Add A record for mail.{config['domain']} pointing to {config['flokinet_c2_ip']}")
else:
print("\n[-] Deployment failed.")
# Display non-standard SSH port warning if applicable
if success and config.get('ssh_port') != 22:
print(f"\n[!] IMPORTANT: Non-standard SSH port {config.get('ssh_port')} is being used")
print(f"[!] Future SSH connections will require: ssh -p {config.get('ssh_port')} user@host")
if __name__ == "__main__":
main()