1246 lines
53 KiB
Python
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() |