Got attack box and c2-redirector working
This commit is contained in:
@@ -0,0 +1 @@
|
||||
# Utils package for C2ingRed
|
||||
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
AWS provider utilities for C2ingRed deployment system
|
||||
"""
|
||||
|
||||
import random
|
||||
import logging
|
||||
from .common import COLORS, load_vars_file, confirm_action
|
||||
|
||||
def get_aws_credentials(provider_vars=None):
|
||||
"""Get AWS credentials from user or vars file"""
|
||||
if not provider_vars:
|
||||
provider_vars = load_vars_file('aws')
|
||||
|
||||
default_aws_key = provider_vars.get('aws_access_key', '')
|
||||
default_aws_secret = provider_vars.get('aws_secret_key', '')
|
||||
|
||||
print(f"\n{COLORS['BLUE']}AWS Configuration{COLORS['RESET']}")
|
||||
aws_key = input(f"AWS Access Key [{'*****' if default_aws_key else 'leave blank to use AWS CLI profile'}]: ") or default_aws_key
|
||||
aws_secret = input(f"AWS Secret Key [{'*****' if default_aws_secret else 'leave blank to use AWS CLI profile'}]: ") or default_aws_secret
|
||||
|
||||
return {
|
||||
'aws_access_key': aws_key,
|
||||
'aws_secret_key': aws_secret
|
||||
}
|
||||
|
||||
def select_aws_region(provider_vars=None, component=None):
|
||||
"""Let the user select an AWS region"""
|
||||
if not provider_vars:
|
||||
provider_vars = load_vars_file('aws')
|
||||
|
||||
regions = provider_vars.get('aws_region_choices', [])
|
||||
component_str = f" for {component}" if component else ""
|
||||
|
||||
if not regions:
|
||||
print(f"{COLORS['YELLOW']}No regions found for AWS, using us-east-1{COLORS['RESET']}")
|
||||
return "us-east-1"
|
||||
|
||||
print(f"\nAvailable AWS regions{component_str}:")
|
||||
for i, region in enumerate(regions, 1):
|
||||
print(f" {i}. {region}")
|
||||
|
||||
region_input = input(f"\nSelect region{component_str} (number or leave blank for random): ")
|
||||
|
||||
if not region_input:
|
||||
return random.choice(regions)
|
||||
|
||||
try:
|
||||
region_choice = int(region_input)
|
||||
if 1 <= region_choice <= len(regions):
|
||||
return regions[region_choice - 1]
|
||||
else:
|
||||
print(f"{COLORS['RED']}Invalid choice, using random region{COLORS['RESET']}")
|
||||
return random.choice(regions)
|
||||
except ValueError:
|
||||
print(f"{COLORS['RED']}Invalid input, using random region{COLORS['RESET']}")
|
||||
return random.choice(regions)
|
||||
|
||||
def gather_aws_config():
|
||||
"""Gather all AWS-specific configuration"""
|
||||
provider_vars = load_vars_file('aws')
|
||||
config = {}
|
||||
|
||||
# Get credentials
|
||||
aws_creds = get_aws_credentials(provider_vars)
|
||||
config.update(aws_creds)
|
||||
|
||||
# Get region
|
||||
config['aws_region'] = select_aws_region(provider_vars)
|
||||
|
||||
# Additional AWS-specific settings
|
||||
config['aws_instance_type'] = provider_vars.get('aws_instance_type', 't3.micro')
|
||||
config['aws_volume_size'] = provider_vars.get('aws_volume_size', 20)
|
||||
|
||||
return config
|
||||
@@ -0,0 +1,320 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Cleanup and teardown engine for C2ingRed deployments
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import logging
|
||||
import json
|
||||
import glob
|
||||
|
||||
# Add project root to path
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..'))
|
||||
|
||||
from utils.common import COLORS, PROVIDER_DIRS, confirm_action
|
||||
|
||||
def teardown_by_deployment_id(deployment_id):
|
||||
"""Teardown infrastructure by deployment ID"""
|
||||
print(f"\n{COLORS['BLUE']}Teardown Infrastructure by ID: {deployment_id}{COLORS['RESET']}")
|
||||
|
||||
# Look for deployment logs to determine provider and configuration
|
||||
log_files = glob.glob(f"logs/deployment_{deployment_id}*.log")
|
||||
|
||||
if not log_files:
|
||||
print(f"{COLORS['RED']}No deployment logs found for ID: {deployment_id}{COLORS['RESET']}")
|
||||
return False
|
||||
|
||||
# Try to find deployment info files
|
||||
info_files = glob.glob(f"logs/deployment_info_{deployment_id}*.txt")
|
||||
|
||||
if info_files:
|
||||
config = parse_deployment_info(info_files[0])
|
||||
if config:
|
||||
return execute_teardown(config)
|
||||
|
||||
print(f"{COLORS['YELLOW']}Could not determine deployment configuration from logs{COLORS['RESET']}")
|
||||
print(f"{COLORS['YELLOW']}Manual cleanup may be required{COLORS['RESET']}")
|
||||
return False
|
||||
|
||||
def teardown_all_infrastructure():
|
||||
"""Teardown all deployed infrastructure"""
|
||||
print(f"\n{COLORS['RED']}⚠️ WARNING: This will attempt to destroy ALL infrastructure!{COLORS['RESET']}")
|
||||
|
||||
if not confirm_action("Are you absolutely sure?", default=False):
|
||||
return False
|
||||
|
||||
# Find all deployment info files
|
||||
info_files = glob.glob("logs/deployment_info_*.txt")
|
||||
|
||||
if not info_files:
|
||||
print(f"{COLORS['GREEN']}No deployment info files found - nothing to teardown{COLORS['RESET']}")
|
||||
return True
|
||||
|
||||
print(f"Found {len(info_files)} deployments to teardown:")
|
||||
|
||||
success_count = 0
|
||||
for info_file in info_files:
|
||||
config = parse_deployment_info(info_file)
|
||||
if config:
|
||||
deployment_id = config.get('deployment_id', 'unknown')
|
||||
print(f"\nTearing down deployment: {deployment_id}")
|
||||
if execute_teardown(config):
|
||||
success_count += 1
|
||||
|
||||
print(f"\n{COLORS['GREEN']}Successfully tore down {success_count}/{len(info_files)} deployments{COLORS['RESET']}")
|
||||
return success_count == len(info_files)
|
||||
|
||||
def parse_deployment_info(info_file):
|
||||
"""Parse deployment info from file to extract configuration"""
|
||||
try:
|
||||
config = {}
|
||||
with open(info_file, 'r') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
in_config_section = False
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
|
||||
if line == "Configuration:":
|
||||
in_config_section = True
|
||||
continue
|
||||
elif line.startswith("-" * 30):
|
||||
continue
|
||||
elif line.startswith("Access Information:"):
|
||||
break
|
||||
|
||||
if in_config_section and ": " in line:
|
||||
key, value = line.split(": ", 1)
|
||||
|
||||
# Convert string values back to appropriate types
|
||||
if value.lower() == 'true':
|
||||
value = True
|
||||
elif value.lower() == 'false':
|
||||
value = False
|
||||
elif value.lower() == 'none':
|
||||
value = None
|
||||
|
||||
config[key] = value
|
||||
|
||||
return config
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to parse deployment info from {info_file}: {e}")
|
||||
return None
|
||||
|
||||
def execute_teardown(config):
|
||||
"""Execute teardown based on configuration"""
|
||||
provider = config.get('provider')
|
||||
deployment_id = config.get('deployment_id')
|
||||
|
||||
if not provider or not deployment_id:
|
||||
print(f"{COLORS['RED']}Missing provider or deployment ID{COLORS['RESET']}")
|
||||
return False
|
||||
|
||||
print(f"{COLORS['BLUE']}Executing teardown for {provider} deployment {deployment_id}...{COLORS['RESET']}")
|
||||
|
||||
# Set up logging for teardown
|
||||
from utils.common import setup_logging
|
||||
setup_logging(deployment_id, "teardown")
|
||||
|
||||
try:
|
||||
# Set provider-specific environment variables
|
||||
set_provider_environment_for_teardown(config)
|
||||
|
||||
# Get correct provider directory
|
||||
provider_dir = PROVIDER_DIRS.get(provider, provider.capitalize())
|
||||
|
||||
# Execute teardown playbook
|
||||
playbook = f"providers/{provider_dir}/cleanup.yml"
|
||||
|
||||
if not os.path.exists(playbook):
|
||||
print(f"{COLORS['YELLOW']}Teardown playbook not found: {playbook}{COLORS['RESET']}")
|
||||
print(f"{COLORS['YELLOW']}Manual cleanup required{COLORS['RESET']}")
|
||||
return manual_cleanup_guidance(config)
|
||||
|
||||
success = run_teardown_playbook(playbook, config)
|
||||
|
||||
if success:
|
||||
# Clean up local SSH keys
|
||||
cleanup_ssh_keys_for_deployment(deployment_id)
|
||||
|
||||
# Move deployment logs to cleanup folder
|
||||
archive_deployment_logs(deployment_id)
|
||||
|
||||
print(f"{COLORS['GREEN']}Teardown completed successfully{COLORS['RESET']}")
|
||||
else:
|
||||
print(f"{COLORS['RED']}Teardown failed - manual cleanup may be required{COLORS['RESET']}")
|
||||
|
||||
return success
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Teardown execution failed: {e}")
|
||||
print(f"{COLORS['RED']}Teardown execution failed: {e}{COLORS['RESET']}")
|
||||
return False
|
||||
|
||||
def set_provider_environment_for_teardown(config):
|
||||
"""Set provider-specific environment variables for teardown"""
|
||||
provider = config.get('provider')
|
||||
|
||||
if provider == "aws":
|
||||
if config.get('aws_access_key'):
|
||||
os.environ['AWS_ACCESS_KEY_ID'] = config['aws_access_key']
|
||||
if config.get('aws_secret_key'):
|
||||
os.environ['AWS_SECRET_ACCESS_KEY'] = config['aws_secret_key']
|
||||
elif provider == "linode":
|
||||
# Try to get token from config first, then from vars file
|
||||
if config.get('linode_token'):
|
||||
os.environ['LINODE_TOKEN'] = config['linode_token']
|
||||
else:
|
||||
# Load token from provider vars file
|
||||
try:
|
||||
from utils.common import load_vars_file, PROVIDER_DIRS
|
||||
provider_dir = PROVIDER_DIRS.get(provider, provider.capitalize())
|
||||
vars_file = f"providers/{provider_dir}/vars.yaml"
|
||||
vars_data = load_vars_file(vars_file)
|
||||
if vars_data and vars_data.get('linode_token'):
|
||||
os.environ['LINODE_TOKEN'] = vars_data['linode_token']
|
||||
config['linode_token'] = vars_data['linode_token'] # Add to config for later use
|
||||
else:
|
||||
logging.warning("Linode token not found in vars file - cleanup may fail")
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to load Linode token from vars file: {e}")
|
||||
|
||||
def run_teardown_playbook(playbook, config):
|
||||
"""Run the teardown Ansible playbook"""
|
||||
try:
|
||||
cmd = [
|
||||
'ansible-playbook',
|
||||
playbook,
|
||||
'--extra-vars', create_teardown_vars(config)
|
||||
]
|
||||
|
||||
if config.get('debug'):
|
||||
cmd.append('-vvv')
|
||||
|
||||
logging.info(f"Executing teardown: {' '.join(cmd)}")
|
||||
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
logging.info("Teardown playbook executed successfully")
|
||||
return True
|
||||
else:
|
||||
logging.error(f"Teardown playbook failed with return code {result.returncode}")
|
||||
logging.error(f"STDOUT: {result.stdout}")
|
||||
logging.error(f"STDERR: {result.stderr}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error executing teardown playbook: {e}")
|
||||
return False
|
||||
|
||||
def create_teardown_vars(config):
|
||||
"""Create Ansible extra vars for teardown"""
|
||||
teardown_vars = {
|
||||
'deployment_id': config.get('deployment_id'),
|
||||
'provider': config.get('provider'),
|
||||
'operation': 'teardown'
|
||||
}
|
||||
|
||||
# Add instance names for proper cleanup
|
||||
if config.get('redirector_name'):
|
||||
teardown_vars['redirector_name'] = config['redirector_name']
|
||||
if config.get('c2_name'):
|
||||
teardown_vars['c2_name'] = config['c2_name']
|
||||
if config.get('tracker_name'):
|
||||
teardown_vars['tracker_name'] = config['tracker_name']
|
||||
if config.get('attack_box_name'):
|
||||
teardown_vars['attack_box_name'] = config['attack_box_name']
|
||||
|
||||
# Add provider-specific vars if present
|
||||
if config.get('aws_access_key'):
|
||||
teardown_vars['aws_access_key'] = config['aws_access_key']
|
||||
if config.get('aws_secret_key'):
|
||||
teardown_vars['aws_secret_key'] = config['aws_secret_key']
|
||||
if config.get('aws_region'):
|
||||
teardown_vars['aws_region'] = config['aws_region']
|
||||
if config.get('linode_token'):
|
||||
teardown_vars['linode_token'] = config['linode_token']
|
||||
if config.get('linode_region'):
|
||||
teardown_vars['linode_region'] = config['linode_region']
|
||||
|
||||
return json.dumps(teardown_vars)
|
||||
|
||||
def cleanup_ssh_keys_for_deployment(deployment_id):
|
||||
"""Clean up SSH keys for a specific deployment"""
|
||||
ssh_dir = os.path.expanduser("~/.ssh")
|
||||
key_pattern = f"c2deploy_{deployment_id}*"
|
||||
|
||||
for key_file in glob.glob(os.path.join(ssh_dir, key_pattern)):
|
||||
try:
|
||||
os.remove(key_file)
|
||||
logging.info(f"Removed SSH key: {key_file}")
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to remove SSH key {key_file}: {e}")
|
||||
|
||||
def archive_deployment_logs(deployment_id):
|
||||
"""Archive deployment logs after successful teardown"""
|
||||
try:
|
||||
# Create archive directory
|
||||
archive_dir = "logs/archive"
|
||||
os.makedirs(archive_dir, exist_ok=True)
|
||||
|
||||
# Move all files related to this deployment
|
||||
log_patterns = [
|
||||
f"logs/deployment_{deployment_id}*",
|
||||
f"logs/deployment_info_{deployment_id}*",
|
||||
f"logs/teardown_{deployment_id}*"
|
||||
]
|
||||
|
||||
for pattern in log_patterns:
|
||||
for file_path in glob.glob(pattern):
|
||||
archive_path = os.path.join(archive_dir, os.path.basename(file_path))
|
||||
os.rename(file_path, archive_path)
|
||||
logging.info(f"Archived: {file_path} -> {archive_path}")
|
||||
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to archive logs for {deployment_id}: {e}")
|
||||
|
||||
def manual_cleanup_guidance(config):
|
||||
"""Provide manual cleanup guidance when automated teardown isn't available"""
|
||||
provider = config.get('provider')
|
||||
deployment_id = config.get('deployment_id')
|
||||
|
||||
print(f"\n{COLORS['YELLOW']}Manual Cleanup Required{COLORS['RESET']}")
|
||||
print(f"{COLORS['YELLOW']}========================{COLORS['RESET']}")
|
||||
print(f"Deployment ID: {deployment_id}")
|
||||
print(f"Provider: {provider}")
|
||||
|
||||
if provider == "aws":
|
||||
print(f"\nAWS Resources to check:")
|
||||
print(f"- EC2 instances with tags containing: {deployment_id}")
|
||||
print(f"- Security groups with names containing: {deployment_id}")
|
||||
print(f"- Key pairs with names containing: {deployment_id}")
|
||||
print(f"- EIPs associated with the deployment")
|
||||
|
||||
elif provider == "linode":
|
||||
print(f"\nLinode Resources to check:")
|
||||
print(f"- Linodes with labels containing: {deployment_id}")
|
||||
print(f"- NodeBalancers with labels containing: {deployment_id}")
|
||||
print(f"- Firewalls with labels containing: {deployment_id}")
|
||||
|
||||
elif provider == "flokinet":
|
||||
print(f"\nFlokiNET Resources to check:")
|
||||
print(f"- Check your FlokiNET control panel for resources created")
|
||||
print(f"- Look for servers with the deployment ID: {deployment_id}")
|
||||
|
||||
print(f"\nLocal cleanup:")
|
||||
print(f"- SSH keys: ~/.ssh/c2deploy_{deployment_id}*")
|
||||
print(f"- Log files: logs/*{deployment_id}*")
|
||||
|
||||
return False # Manual cleanup required
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Cleanup engine loaded")
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Common utilities and constants for C2ingRed deployment system
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import random
|
||||
import string
|
||||
import logging
|
||||
import subprocess
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
# Constants for providers
|
||||
PROVIDERS = ["aws", "linode", "flokinet"]
|
||||
DEFAULT_SSH_USER = {
|
||||
"aws": "kali",
|
||||
"linode": "root",
|
||||
"flokinet": "root"
|
||||
}
|
||||
|
||||
# Directory names - maintain correct case for each provider
|
||||
PROVIDER_DIRS = {
|
||||
"aws": "AWS",
|
||||
"linode": "Linode",
|
||||
"flokinet": "FlokiNET"
|
||||
}
|
||||
|
||||
# Color codes for terminal output
|
||||
COLORS = {
|
||||
"RESET": "\033[0m",
|
||||
"RED": "\033[91m",
|
||||
"GREEN": "\033[92m",
|
||||
"YELLOW": "\033[93m",
|
||||
"BLUE": "\033[94m",
|
||||
"PURPLE": "\033[95m",
|
||||
"CYAN": "\033[96m",
|
||||
"WHITE": "\033[97m",
|
||||
"GRAY": "\033[90m"
|
||||
}
|
||||
|
||||
def clear_screen():
|
||||
"""Clear the terminal screen"""
|
||||
os.system('cls' if os.name == 'nt' else 'clear')
|
||||
|
||||
def print_banner():
|
||||
"""Print the C2ingRed banner"""
|
||||
banner = f"""
|
||||
{COLORS['BLUE']}========================================================{COLORS['RESET']}
|
||||
{COLORS['BLUE']} ██████╗██████╗ ██╗███╗ ██╗ ██████╗ ██████╗ ███████╗██████╗{COLORS['RESET']}
|
||||
{COLORS['BLUE']} ██╔════╝╚════██╗██║████╗ ██║██╔════╝ ██╔══██╗██╔════╝██╔══██╗{COLORS['RESET']}
|
||||
{COLORS['BLUE']} ██║ █████╔╝██║██╔██╗ ██║██║ ███╗██████╔╝█████╗ ██║ ██║{COLORS['RESET']}
|
||||
{COLORS['BLUE']} ██║ ██╔═══╝ ██║██║╚██╗██║██║ ██║██╔══██╗██╔══╝ ██║ ██║{COLORS['RESET']}
|
||||
{COLORS['BLUE']} ╚██████╗███████╗██║██║ ╚████║╚██████╔╝██║ ██║███████╗██████╔╝{COLORS['RESET']}
|
||||
{COLORS['BLUE']} ╚═════╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝╚═════╝{COLORS['RESET']}
|
||||
{COLORS['BLUE']} {COLORS['RESET']}
|
||||
{COLORS['BLUE']} Red Team Infrastructure Deployment Tool {COLORS['RESET']}
|
||||
{COLORS['BLUE']}========================================================{COLORS['RESET']}
|
||||
"""
|
||||
print(banner)
|
||||
|
||||
def generate_random_string(length=8):
|
||||
"""Generate a random string of letters and digits."""
|
||||
return ''.join(random.choices(string.ascii_lowercase + string.digits, k=length))
|
||||
|
||||
def generate_deployment_id():
|
||||
"""Generate a consistent deployment ID for all resources in this deployment"""
|
||||
from utils.name_generator import generate_deployment_id as generate_verb_animal_id
|
||||
return generate_verb_animal_id()
|
||||
|
||||
def archive_old_logs(max_logs_to_keep=2):
|
||||
"""Archive old log files to keep logs directory clean - more aggressive archiving"""
|
||||
import glob
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
log_dir = "logs"
|
||||
archive_dir = os.path.join(log_dir, "archive")
|
||||
|
||||
if not os.path.exists(log_dir):
|
||||
return
|
||||
|
||||
# Create archive directory if it doesn't exist
|
||||
os.makedirs(archive_dir, exist_ok=True)
|
||||
|
||||
# Get all log files (excluding archive directory) - ANY existing logs should be archived
|
||||
log_files = []
|
||||
for pattern in ["deployment_*.log", "teardown_*.log", "20*.log"]:
|
||||
log_files.extend(glob.glob(os.path.join(log_dir, pattern)))
|
||||
|
||||
# Remove duplicates and filter out archive directory
|
||||
log_files = [f for f in set(log_files) if "archive" not in f]
|
||||
|
||||
# Sort by modification time (newest first)
|
||||
log_files.sort(key=lambda x: os.path.getmtime(x), reverse=True)
|
||||
|
||||
# Archive ALL deployment logs to keep directory clean for new deployments
|
||||
if len(log_files) > 0:
|
||||
print(f"Found {len(log_files)} log files to archive")
|
||||
|
||||
archived_count = 0
|
||||
for log_file in log_files:
|
||||
try:
|
||||
filename = os.path.basename(log_file)
|
||||
# Add timestamp to archived filename to prevent conflicts
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
archived_filename = f"{timestamp}_{filename}"
|
||||
archive_path = os.path.join(archive_dir, archived_filename)
|
||||
|
||||
shutil.move(log_file, archive_path)
|
||||
archived_count += 1
|
||||
print(f"Archived: {filename} -> archive/{archived_filename}")
|
||||
except Exception as e:
|
||||
print(f"Failed to archive {log_file}: {e}")
|
||||
|
||||
if archived_count > 0:
|
||||
print(f"Archived {archived_count} log files to {archive_dir}")
|
||||
|
||||
# Also archive old deployment info files
|
||||
info_files = glob.glob(os.path.join(log_dir, "deployment_info_*.txt"))
|
||||
|
||||
if len(info_files) > 0:
|
||||
print(f"Found {len(info_files)} info files to archive")
|
||||
for info_file in info_files:
|
||||
try:
|
||||
filename = os.path.basename(info_file)
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
archived_filename = f"{timestamp}_{filename}"
|
||||
archive_path = os.path.join(archive_dir, archived_filename)
|
||||
|
||||
shutil.move(info_file, archive_path)
|
||||
print(f"Archived: {filename} -> archive/{archived_filename}")
|
||||
except Exception as e:
|
||||
print(f"Failed to archive {info_file}: {e}")
|
||||
|
||||
def setup_logging(deployment_id=None, operation_type="deployment"):
|
||||
"""Set up logging for the deployment or teardown"""
|
||||
log_dir = "logs"
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
|
||||
# Archive old logs before starting new deployment
|
||||
if operation_type == "deployment":
|
||||
archive_old_logs()
|
||||
|
||||
# Create distinct log files for deployment vs teardown operations
|
||||
if operation_type == "teardown":
|
||||
log_file = os.path.join(log_dir, f"teardown_{deployment_id}.log")
|
||||
else:
|
||||
log_file = os.path.join(log_dir, f"deployment_{deployment_id}.log")
|
||||
|
||||
# Clear any existing handlers
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.handlers.clear()
|
||||
|
||||
# Configure file handler to log DEBUG and above for full verbosity
|
||||
file_handler = logging.FileHandler(log_file)
|
||||
file_handler.setLevel(logging.DEBUG)
|
||||
file_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
||||
file_handler.setFormatter(file_formatter)
|
||||
|
||||
# Add console handler for INFO level and above
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setLevel(logging.INFO)
|
||||
console_formatter = logging.Formatter('[%(levelname)s] %(message)s')
|
||||
console_handler.setFormatter(console_formatter)
|
||||
|
||||
# Configure root logger
|
||||
root_logger.setLevel(logging.DEBUG) # Capture everything at root level
|
||||
root_logger.addHandler(file_handler)
|
||||
root_logger.addHandler(console_handler)
|
||||
|
||||
logging.info(f"{operation_type.capitalize()} operation started")
|
||||
logging.info(f"Deployment ID: {deployment_id}")
|
||||
logging.info(f"Full verbose output will be captured in: {log_file}")
|
||||
return log_file
|
||||
|
||||
def load_vars_file(provider):
|
||||
"""Load vars.yaml for the specified provider"""
|
||||
if provider not in PROVIDER_DIRS:
|
||||
return {}
|
||||
|
||||
# Use correct case for directory
|
||||
provider_dir = PROVIDER_DIRS[provider]
|
||||
vars_file = f"providers/{provider_dir}/vars.yaml"
|
||||
|
||||
if os.path.exists(vars_file):
|
||||
try:
|
||||
import yaml
|
||||
with open(vars_file, 'r') as f:
|
||||
return yaml.safe_load(f) or {}
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to load {vars_file}: {e}")
|
||||
return {}
|
||||
else:
|
||||
logging.warning(f"Vars file not found: {vars_file}")
|
||||
return {}
|
||||
|
||||
def validate_ip_address(ip):
|
||||
"""Validate IP address format"""
|
||||
pattern = r'^(\d{1,3}\.){3}\d{1,3}$'
|
||||
return re.match(pattern, ip) is not None
|
||||
|
||||
def get_public_ip():
|
||||
"""Get the user's public IP address"""
|
||||
try:
|
||||
import requests
|
||||
return requests.get('https://api.ipify.org', timeout=5).text.strip()
|
||||
except:
|
||||
return None
|
||||
|
||||
def confirm_action(message, default=False):
|
||||
"""Ask for user confirmation with a yes/no prompt"""
|
||||
prompt = f"{message} ({'Y/n' if default else 'y/N'}): "
|
||||
response = input(prompt).lower().strip()
|
||||
|
||||
if not response:
|
||||
return default
|
||||
|
||||
return response in ['y', 'yes']
|
||||
|
||||
def wait_for_input(message="Press Enter to continue..."):
|
||||
"""Wait for user input before continuing"""
|
||||
input(f"\n{message}")
|
||||
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
FlokiNET provider utilities for C2ingRed deployment system
|
||||
"""
|
||||
|
||||
import logging
|
||||
from .common import COLORS, load_vars_file, validate_ip_address
|
||||
|
||||
def get_flokinet_credentials(provider_vars=None):
|
||||
"""Get FlokiNET server IPs from user or vars file"""
|
||||
if not provider_vars:
|
||||
provider_vars = load_vars_file('flokinet')
|
||||
|
||||
default_redirector_ip = provider_vars.get('redirector_ip', '')
|
||||
default_c2_ip = provider_vars.get('c2_ip', '')
|
||||
|
||||
print(f"\n{COLORS['BLUE']}FlokiNET Configuration{COLORS['RESET']}")
|
||||
print(f"{COLORS['YELLOW']}Note: FlokiNET requires pre-provisioned servers{COLORS['RESET']}")
|
||||
|
||||
redirector_ip = input(f"FlokiNET Redirector IP Address [default: {default_redirector_ip}]: ") or default_redirector_ip
|
||||
c2_ip = input(f"FlokiNET C2 Server IP Address [default: {default_c2_ip}]: ") or default_c2_ip
|
||||
|
||||
# Validate IP addresses
|
||||
if redirector_ip and not validate_ip_address(redirector_ip):
|
||||
print(f"{COLORS['RED']}Invalid redirector IP address{COLORS['RESET']}")
|
||||
return None
|
||||
|
||||
if c2_ip and not validate_ip_address(c2_ip):
|
||||
print(f"{COLORS['RED']}Invalid C2 server IP address{COLORS['RESET']}")
|
||||
return None
|
||||
|
||||
return {
|
||||
'flokinet_redirector_ip': redirector_ip,
|
||||
'flokinet_c2_ip': c2_ip
|
||||
}
|
||||
|
||||
def gather_flokinet_config():
|
||||
"""Gather all FlokiNET-specific configuration"""
|
||||
provider_vars = load_vars_file('flokinet')
|
||||
config = {}
|
||||
|
||||
# Get server IPs
|
||||
flokinet_ips = get_flokinet_credentials(provider_vars)
|
||||
if not flokinet_ips:
|
||||
return None
|
||||
config.update(flokinet_ips)
|
||||
|
||||
# FlokiNET-specific settings
|
||||
config['ssh_user'] = 'root' # FlokiNET typically uses root
|
||||
|
||||
return config
|
||||
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Linode provider utilities for C2ingRed deployment system
|
||||
"""
|
||||
|
||||
import random
|
||||
import logging
|
||||
from .common import COLORS, load_vars_file
|
||||
|
||||
def get_linode_credentials(provider_vars=None):
|
||||
"""Get Linode API token from user or vars file"""
|
||||
if not provider_vars:
|
||||
provider_vars = load_vars_file('linode')
|
||||
|
||||
default_token = provider_vars.get('linode_token', '')
|
||||
|
||||
print(f"\n{COLORS['BLUE']}Linode Configuration{COLORS['RESET']}")
|
||||
token = input(f"Linode API Token [{'*****' if default_token else 'required'}]: ") or default_token
|
||||
|
||||
if not token:
|
||||
print(f"{COLORS['RED']}Linode API token is required{COLORS['RESET']}")
|
||||
return None
|
||||
|
||||
return {'linode_token': token}
|
||||
|
||||
def select_linode_region(provider_vars=None, component=None):
|
||||
"""Let the user select a Linode region"""
|
||||
if not provider_vars:
|
||||
provider_vars = load_vars_file('linode')
|
||||
|
||||
regions = provider_vars.get('region_choices', [])
|
||||
component_str = f" for {component}" if component else ""
|
||||
|
||||
if not regions:
|
||||
print(f"{COLORS['YELLOW']}No regions found for Linode, using us-east{COLORS['RESET']}")
|
||||
return "us-east"
|
||||
|
||||
print(f"\nAvailable Linode regions{component_str}:")
|
||||
for i, region in enumerate(regions, 1):
|
||||
print(f" {i}. {region}")
|
||||
|
||||
region_input = input(f"\nSelect region{component_str} (number or leave blank for random): ")
|
||||
|
||||
if not region_input:
|
||||
# Use random from reliable regions only (first 8 are most reliable)
|
||||
reliable_regions = regions[:8] if len(regions) >= 8 else regions
|
||||
return random.choice(reliable_regions)
|
||||
|
||||
try:
|
||||
region_choice = int(region_input)
|
||||
if 1 <= region_choice <= len(regions):
|
||||
return regions[region_choice - 1]
|
||||
else:
|
||||
print(f"{COLORS['RED']}Invalid choice, using random region{COLORS['RESET']}")
|
||||
return random.choice(regions)
|
||||
except ValueError:
|
||||
print(f"{COLORS['RED']}Invalid input, using random region{COLORS['RESET']}")
|
||||
return random.choice(regions)
|
||||
|
||||
def gather_linode_config():
|
||||
"""Gather all Linode-specific configuration"""
|
||||
provider_vars = load_vars_file('linode')
|
||||
config = {}
|
||||
|
||||
# Get credentials
|
||||
linode_creds = get_linode_credentials(provider_vars)
|
||||
if not linode_creds:
|
||||
return None
|
||||
config.update(linode_creds)
|
||||
|
||||
# Get region
|
||||
config['linode_region'] = select_linode_region(provider_vars)
|
||||
|
||||
# Additional Linode-specific settings
|
||||
config['linode_instance_type'] = provider_vars.get('linode_instance_type', 'g6-nanode-1')
|
||||
config['linode_image'] = provider_vars.get('linode_image', 'linode/kali')
|
||||
|
||||
return config
|
||||
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Name generation utility for c2itall deployments
|
||||
Generates verb-animal names similar to FourEyes with shared deployment IDs
|
||||
"""
|
||||
|
||||
import random
|
||||
import os
|
||||
|
||||
def load_word_list(filename):
|
||||
"""Load words from a text file, one word per line"""
|
||||
try:
|
||||
# Check if we have FourEyes word lists
|
||||
foureyes_path = "/home/n0mad1k/Tools/FourEyes"
|
||||
if os.path.exists(foureyes_path):
|
||||
file_path = os.path.join(foureyes_path, filename)
|
||||
if os.path.exists(file_path):
|
||||
with open(file_path, 'r') as f:
|
||||
words = [line.strip().lower() for line in f if line.strip()]
|
||||
return [word for word in words if word] # Remove empty strings
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Fallback word lists if FourEyes not available
|
||||
if 'verbs' in filename:
|
||||
return [
|
||||
"blazing", "soaring", "charging", "prowling", "hunting", "stalking",
|
||||
"striking", "rushing", "dashing", "racing", "flying", "diving",
|
||||
"leaping", "climbing", "sliding", "spinning", "rolling", "sneaking",
|
||||
"roaming", "wandering", "running", "jumping", "swimming", "crawling",
|
||||
"fighting", "defending", "attacking", "scanning", "searching", "finding"
|
||||
]
|
||||
else: # animals
|
||||
return [
|
||||
"wolf", "eagle", "tiger", "falcon", "bear", "lion", "shark", "hawk",
|
||||
"panther", "cobra", "viper", "rhino", "bull", "fox", "raven", "crow",
|
||||
"spider", "scorpion", "mantis", "dragon", "phoenix", "griffin",
|
||||
"badger", "wolverine", "lynx", "jaguar", "cheetah", "leopard"
|
||||
]
|
||||
|
||||
def _generate_verb_animal():
|
||||
"""Generate a verb-animal combination"""
|
||||
verbs = load_word_list('verbs.txt')
|
||||
animals = load_word_list('animals.txt')
|
||||
|
||||
verb = random.choice(verbs)
|
||||
animal = random.choice(animals)
|
||||
return f"{verb}{animal}"
|
||||
|
||||
def generate_deployment_id():
|
||||
"""Generate a deployment ID using verb-animal combination"""
|
||||
return _generate_verb_animal()
|
||||
|
||||
def generate_attack_box_name(deployment_id):
|
||||
"""Generate attack box name with a- prefix using shared deployment ID"""
|
||||
return f"a-{deployment_id}"
|
||||
|
||||
def generate_redirector_name(deployment_id):
|
||||
"""Generate redirector name with r- prefix using shared deployment ID"""
|
||||
return f"r-{deployment_id}"
|
||||
|
||||
def generate_c2_name(deployment_id):
|
||||
"""Generate C2 server name with s- prefix using shared deployment ID"""
|
||||
return f"s-{deployment_id}"
|
||||
|
||||
def generate_phishing_name(deployment_id):
|
||||
"""Generate phishing server name with p- prefix using shared deployment ID"""
|
||||
return f"p-{deployment_id}"
|
||||
|
||||
def generate_tracker_name(deployment_id):
|
||||
"""Generate tracker name with t- prefix using shared deployment ID"""
|
||||
return f"t-{deployment_id}"
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Test the name generation
|
||||
deployment_id = generate_deployment_id()
|
||||
print(f"Testing shared deployment ID: {deployment_id}")
|
||||
print(f"Attack Box: {generate_attack_box_name(deployment_id)}")
|
||||
print(f"Redirector: {generate_redirector_name(deployment_id)}")
|
||||
print(f"C2 Server: {generate_c2_name(deployment_id)}")
|
||||
print(f"Phishing: {generate_phishing_name(deployment_id)}")
|
||||
print(f"Tracker: {generate_tracker_name(deployment_id)}")
|
||||
@@ -0,0 +1,244 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Common naming utilities for C2itall deployments
|
||||
Provides consistent naming options across all deployment types
|
||||
"""
|
||||
|
||||
import os
|
||||
import glob
|
||||
from utils.common import COLORS
|
||||
|
||||
def get_existing_deployments():
|
||||
"""Get list of existing deployments from deployment info files"""
|
||||
try:
|
||||
info_files = glob.glob("logs/deployment_info_*.txt")
|
||||
deployments = []
|
||||
|
||||
for info_file in info_files:
|
||||
try:
|
||||
deployment_info = {}
|
||||
with open(info_file, 'r') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if ": " in line and not line.startswith("-"):
|
||||
parts = line.split(": ", 1)
|
||||
if len(parts) == 2:
|
||||
key, value = parts
|
||||
deployment_info[key] = value
|
||||
|
||||
if deployment_info.get('deployment_id'):
|
||||
deployments.append(deployment_info)
|
||||
except Exception as e:
|
||||
continue # Skip problematic files
|
||||
|
||||
return deployments
|
||||
except Exception as e:
|
||||
return []
|
||||
|
||||
def select_deployment_for_naming(exclude_types=None, current_deployment_id=None):
|
||||
"""
|
||||
Allow user to select an existing deployment to base naming on
|
||||
|
||||
Args:
|
||||
exclude_types: List of deployment types to exclude (e.g., ['attack_box'])
|
||||
current_deployment_id: Current deployment ID to exclude from list
|
||||
"""
|
||||
deployments = get_existing_deployments()
|
||||
|
||||
if not deployments:
|
||||
print(f"{COLORS['YELLOW']}No existing deployments found{COLORS['RESET']}")
|
||||
return None
|
||||
|
||||
print(f"\n{COLORS['CYAN']}Existing Deployments:{COLORS['RESET']}")
|
||||
print(f"{COLORS['CYAN']}==================={COLORS['RESET']}")
|
||||
|
||||
# Filter deployments based on criteria
|
||||
filtered_deployments = []
|
||||
for deployment in deployments:
|
||||
deployment_id = deployment.get('deployment_id', 'unknown')
|
||||
|
||||
# Skip current deployment
|
||||
if current_deployment_id and deployment_id == current_deployment_id:
|
||||
continue
|
||||
|
||||
# Skip excluded types
|
||||
deployment_type = deployment.get('deployment_type', 'unknown')
|
||||
if exclude_types and deployment_type in exclude_types:
|
||||
continue
|
||||
|
||||
# Check if this is an attack box deployment (legacy check)
|
||||
is_attack_box = (
|
||||
deployment.get('deployment_type') == 'attack_box' or
|
||||
deployment.get('attack_box_deployment') == 'True' or
|
||||
deployment.get('attack_box_name')
|
||||
)
|
||||
|
||||
if exclude_types and 'attack_box' in exclude_types and is_attack_box:
|
||||
continue
|
||||
|
||||
filtered_deployments.append(deployment)
|
||||
|
||||
if not filtered_deployments:
|
||||
print(f"{COLORS['YELLOW']}No suitable deployments found for naming{COLORS['RESET']}")
|
||||
return None
|
||||
|
||||
# Display available deployments
|
||||
for i, deployment in enumerate(filtered_deployments, 1):
|
||||
deployment_id = deployment.get('deployment_id', 'unknown')
|
||||
provider = deployment.get('provider', 'unknown')
|
||||
domain = deployment.get('domain', 'N/A')
|
||||
deployment_type = deployment.get('deployment_type', 'unknown')
|
||||
|
||||
print(f"{i}. {deployment_id}")
|
||||
print(f" Type: {deployment_type}")
|
||||
print(f" Provider: {provider}")
|
||||
print(f" Domain: {domain}")
|
||||
|
||||
# Show instance names if available
|
||||
instances = []
|
||||
if deployment.get('redirector_name'):
|
||||
instances.append(f"Redirector: {deployment.get('redirector_name')}")
|
||||
if deployment.get('c2_name'):
|
||||
instances.append(f"C2: {deployment.get('c2_name')}")
|
||||
if deployment.get('tracker_name'):
|
||||
instances.append(f"Tracker: {deployment.get('tracker_name')}")
|
||||
if deployment.get('attack_box_name'):
|
||||
instances.append(f"Attack Box: {deployment.get('attack_box_name')}")
|
||||
|
||||
if instances:
|
||||
print(f" Instances: {', '.join(instances)}")
|
||||
print()
|
||||
|
||||
# Get user selection
|
||||
while True:
|
||||
try:
|
||||
choice = input(f"Select deployment (1-{len(filtered_deployments)}) or 'c' to cancel: ").strip()
|
||||
|
||||
if choice.lower() == 'c':
|
||||
return None
|
||||
|
||||
choice_num = int(choice)
|
||||
if 1 <= choice_num <= len(filtered_deployments):
|
||||
selected = filtered_deployments[choice_num - 1]
|
||||
return selected.get('deployment_id')
|
||||
else:
|
||||
print(f"{COLORS['RED']}Invalid choice. Please try again.{COLORS['RESET']}")
|
||||
except ValueError:
|
||||
print(f"{COLORS['RED']}Invalid input. Please enter a number or 'c'.{COLORS['RESET']}")
|
||||
|
||||
def get_deployment_name_with_options(deployment_type, deployment_id, prefix="", existing_name=None):
|
||||
"""
|
||||
Get deployment name with multiple naming options
|
||||
|
||||
Args:
|
||||
deployment_type: Type of deployment (c2, redirector, tracker, attack_box, etc.)
|
||||
deployment_id: Current deployment ID
|
||||
prefix: Prefix for the name (e.g., 'r-', 'c-', 'a-', etc.)
|
||||
existing_name: Existing name if updating
|
||||
|
||||
Returns:
|
||||
Chosen name for the deployment
|
||||
"""
|
||||
|
||||
print(f"\n{COLORS['BLUE']}{deployment_type.title()} Naming Options:{COLORS['RESET']}")
|
||||
print(f"1) Auto-generate name ({prefix}{deployment_id})")
|
||||
print(f"2) Name after existing deployment")
|
||||
print(f"3) Custom name")
|
||||
|
||||
if existing_name:
|
||||
print(f"4) Keep current name ({existing_name})")
|
||||
default_choice = "4"
|
||||
else:
|
||||
default_choice = "1"
|
||||
|
||||
naming_choice = input(f"Select naming option [{default_choice}]: ").strip() or default_choice
|
||||
|
||||
if naming_choice == "1":
|
||||
# Auto-generate using deployment ID
|
||||
chosen_name = f"{prefix}{deployment_id}"
|
||||
print(f"Using auto-generated name: {COLORS['CYAN']}{chosen_name}{COLORS['RESET']}")
|
||||
|
||||
elif naming_choice == "2":
|
||||
# Name after existing deployment
|
||||
# Exclude attack boxes when naming other types, but allow other types when naming attack boxes
|
||||
exclude_types = ['attack_box'] if deployment_type != 'attack_box' else []
|
||||
selected_deployment_id = select_deployment_for_naming(
|
||||
exclude_types=exclude_types,
|
||||
current_deployment_id=deployment_id
|
||||
)
|
||||
|
||||
if selected_deployment_id:
|
||||
chosen_name = f"{prefix}{selected_deployment_id}"
|
||||
print(f"{deployment_type.title()} will be named: {COLORS['CYAN']}{chosen_name}{COLORS['RESET']}")
|
||||
print(f"This associates it with deployment: {COLORS['YELLOW']}{selected_deployment_id}{COLORS['RESET']}")
|
||||
else:
|
||||
print(f"{COLORS['YELLOW']}No deployment selected, using auto-generated name{COLORS['RESET']}")
|
||||
chosen_name = f"{prefix}{deployment_id}"
|
||||
|
||||
elif naming_choice == "3":
|
||||
# Custom name
|
||||
while True:
|
||||
custom_name = input(f"Enter custom {deployment_type} name: ").strip()
|
||||
if custom_name:
|
||||
# Ensure it starts with the correct prefix for consistency
|
||||
if prefix and not custom_name.startswith(prefix):
|
||||
chosen_name = f"{prefix}{custom_name}"
|
||||
print(f"Prefixed with '{prefix}': {COLORS['CYAN']}{chosen_name}{COLORS['RESET']}")
|
||||
else:
|
||||
chosen_name = custom_name
|
||||
break
|
||||
else:
|
||||
print(f"{COLORS['RED']}Name cannot be empty. Please try again.{COLORS['RESET']}")
|
||||
|
||||
elif naming_choice == "4" and existing_name:
|
||||
# Keep existing name
|
||||
chosen_name = existing_name
|
||||
print(f"Keeping current name: {COLORS['CYAN']}{chosen_name}{COLORS['RESET']}")
|
||||
|
||||
else:
|
||||
# Default fallback
|
||||
print(f"{COLORS['YELLOW']}Invalid choice, using auto-generated name{COLORS['RESET']}")
|
||||
chosen_name = f"{prefix}{deployment_id}"
|
||||
|
||||
return chosen_name
|
||||
|
||||
def show_naming_relationship(name, deployment_id, deployment_type):
|
||||
"""Show the relationship between the chosen name and deployment"""
|
||||
if not name:
|
||||
return
|
||||
|
||||
# Determine prefix based on deployment type
|
||||
prefix_map = {
|
||||
'redirector': 'r-',
|
||||
'c2': 's-', # s for server
|
||||
'tracker': 't-',
|
||||
'attack_box': 'a-',
|
||||
'payload': 'p-'
|
||||
}
|
||||
|
||||
expected_prefix = prefix_map.get(deployment_type, '')
|
||||
|
||||
if expected_prefix and name.startswith(expected_prefix):
|
||||
target_deployment = name[len(expected_prefix):] # Remove prefix
|
||||
if target_deployment != deployment_id:
|
||||
return {
|
||||
'target_deployment': target_deployment,
|
||||
'relationship_text': f"Named after deployment: {target_deployment}",
|
||||
'purpose_text': f"This {deployment_type} supports the {target_deployment} engagement"
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
def get_deployment_type_prefix(deployment_type):
|
||||
"""Get the standard prefix for a deployment type"""
|
||||
prefix_map = {
|
||||
'redirector': 'r-',
|
||||
'c2': 's-', # s for server
|
||||
'tracker': 't-',
|
||||
'attack_box': 'a-',
|
||||
'payload': 'p-',
|
||||
'phishing': 'p-'
|
||||
}
|
||||
return prefix_map.get(deployment_type, '')
|
||||
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Provider selection and configuration utilities
|
||||
"""
|
||||
|
||||
from .common import COLORS, PROVIDERS
|
||||
from .aws_utils import gather_aws_config
|
||||
from .linode_utils import gather_linode_config
|
||||
from .flokinet_utils import gather_flokinet_config
|
||||
|
||||
def select_provider():
|
||||
"""Let the user select a cloud provider"""
|
||||
print(f"\n{COLORS['BLUE']}Available cloud providers:{COLORS['RESET']}")
|
||||
for i, provider in enumerate(PROVIDERS, 1):
|
||||
print(f" {i}. {provider.capitalize()}")
|
||||
|
||||
while True:
|
||||
try:
|
||||
provider_choice = input(f"\nSelect a provider (1-{len(PROVIDERS)} or 99 to cancel): ")
|
||||
if provider_choice == "99":
|
||||
return None
|
||||
|
||||
provider_choice = int(provider_choice)
|
||||
if 1 <= provider_choice <= len(PROVIDERS):
|
||||
return PROVIDERS[provider_choice - 1]
|
||||
else:
|
||||
print(f"{COLORS['RED']}Please enter a number between 1 and {len(PROVIDERS)}{COLORS['RESET']}")
|
||||
except ValueError:
|
||||
print(f"{COLORS['RED']}Please enter a valid number{COLORS['RESET']}")
|
||||
|
||||
def gather_provider_config(provider):
|
||||
"""Gather configuration for the specified provider"""
|
||||
if provider == "aws":
|
||||
return gather_aws_config()
|
||||
elif provider == "linode":
|
||||
return gather_linode_config()
|
||||
elif provider == "flokinet":
|
||||
return gather_flokinet_config()
|
||||
else:
|
||||
print(f"{COLORS['RED']}Unknown provider: {provider}{COLORS['RESET']}")
|
||||
return None
|
||||
@@ -0,0 +1,208 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
SSH utilities for C2ingRed deployment system
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import logging
|
||||
import re
|
||||
import glob
|
||||
from .common import COLORS, generate_random_string
|
||||
|
||||
def generate_ssh_key(deployment_id=None):
|
||||
"""Generate an SSH key for deployment with proper tracking for cleanup"""
|
||||
# Use deployment_id if provided, otherwise generate random suffix
|
||||
if not deployment_id:
|
||||
deployment_id = generate_random_string(6)
|
||||
|
||||
ssh_key_path = os.path.expanduser(f"~/.ssh/c2deploy_{deployment_id}")
|
||||
ssh_key_pub_path = f"{ssh_key_path}.pub"
|
||||
|
||||
# Check if key already exists
|
||||
if os.path.exists(ssh_key_path):
|
||||
logging.info(f"SSH key already exists at {ssh_key_path}")
|
||||
return ssh_key_path
|
||||
|
||||
try:
|
||||
# Generate the SSH key
|
||||
subprocess.run([
|
||||
"ssh-keygen", "-t", "rsa", "-b", "4096",
|
||||
"-f", ssh_key_path, "-q", "-N", ""
|
||||
], check=True)
|
||||
|
||||
# Set proper permissions
|
||||
os.chmod(ssh_key_path, 0o600)
|
||||
|
||||
# Track generated keys for cleanup
|
||||
if not hasattr(generate_ssh_key, 'generated_keys'):
|
||||
generate_ssh_key.generated_keys = []
|
||||
generate_ssh_key.generated_keys.append(ssh_key_path)
|
||||
|
||||
logging.info(f"Generated SSH key: {ssh_key_path}")
|
||||
return ssh_key_path
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
logging.error(f"Failed to generate SSH key: {e}")
|
||||
return None
|
||||
|
||||
def get_ssh_public_key(private_key_path):
|
||||
"""Get the public key content from a private key file"""
|
||||
public_key_path = f"{private_key_path}.pub"
|
||||
|
||||
if not os.path.exists(public_key_path):
|
||||
logging.error(f"Public key file not found: {public_key_path}")
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(public_key_path, 'r') as f:
|
||||
return f.read().strip()
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to read public key: {e}")
|
||||
return None
|
||||
|
||||
def extract_attack_box_ip_from_logs(config):
|
||||
"""Extract attack box IP from deployment logs or Ansible output"""
|
||||
deployment_id = config.get('deployment_id', 'unknown')
|
||||
|
||||
# Check deployment log file first
|
||||
log_files_to_check = [
|
||||
f"logs/deployment_{deployment_id}.log",
|
||||
# Also check archived logs
|
||||
f"logs/archive/deployment_{deployment_id}.log"
|
||||
]
|
||||
|
||||
# Check for timestamped archived logs
|
||||
archive_pattern = f"logs/archive/*_deployment_{deployment_id}.log"
|
||||
archived_logs = glob.glob(archive_pattern)
|
||||
if archived_logs:
|
||||
# Get the most recent archived log
|
||||
log_files_to_check.append(max(archived_logs, key=os.path.getmtime))
|
||||
|
||||
for log_file in log_files_to_check:
|
||||
if os.path.exists(log_file):
|
||||
try:
|
||||
with open(log_file, 'r') as f:
|
||||
content = f.read()
|
||||
# Look for various IP patterns in the log
|
||||
ip_patterns = [
|
||||
r'"attack_box_ip":\s*"([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})"',
|
||||
r'attack_box_ip.*?([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})',
|
||||
r'"ipv4":\s*\["([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})"\]',
|
||||
r'ansible_host["\s]*=[\s]*([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})',
|
||||
r'instance_ip["\s]*:[\s]*([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})',
|
||||
r'Target: ([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})',
|
||||
r'IP["\s]*:[\s]*([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})',
|
||||
# Look for IP in Ansible task output patterns
|
||||
r'([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})\s+:\s+ok=',
|
||||
r'PLAY RECAP.*?([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})',
|
||||
r'changed: \[([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})\]',
|
||||
r'ok: \[([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})\]'
|
||||
]
|
||||
|
||||
for pattern in ip_patterns:
|
||||
match = re.search(pattern, content)
|
||||
if match:
|
||||
ip = match.group(1)
|
||||
logging.info(f"Extracted attack box IP from logs: {ip}")
|
||||
return ip
|
||||
except Exception as e:
|
||||
logging.warning(f"Could not read deployment log {log_file}: {e}")
|
||||
|
||||
# Check deployment info file
|
||||
info_file = f"logs/deployment_info_{deployment_id}.txt"
|
||||
if os.path.exists(info_file):
|
||||
try:
|
||||
with open(info_file, 'r') as f:
|
||||
content = f.read()
|
||||
# Look for IP in SSH command or other contexts
|
||||
ip_patterns = [
|
||||
r'root@([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})',
|
||||
r'Instance IP:\s*([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})',
|
||||
r'IP["\s]*:[\s]*([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})'
|
||||
]
|
||||
|
||||
for pattern in ip_patterns:
|
||||
match = re.search(pattern, content)
|
||||
if match:
|
||||
ip = match.group(1)
|
||||
logging.info(f"Extracted attack box IP from deployment info: {ip}")
|
||||
return ip
|
||||
except Exception as e:
|
||||
logging.warning(f"Could not read deployment info: {e}")
|
||||
|
||||
logging.warning("Could not extract attack box IP from logs")
|
||||
return None
|
||||
|
||||
def ssh_to_instance(config):
|
||||
"""SSH into an instance after deployment"""
|
||||
ssh_key_path = config.get('ssh_key_path', '').replace('.pub', '')
|
||||
|
||||
# Handle attack box deployments
|
||||
if config.get('attack_box_deployment'):
|
||||
instance_ip = config.get('attack_box_ip')
|
||||
instance_name = config.get('attack_box_name', 'attack box')
|
||||
|
||||
# If no IP stored in config, try to extract from deployment logs
|
||||
if not instance_ip:
|
||||
instance_ip = extract_attack_box_ip_from_logs(config)
|
||||
|
||||
# Determine which instance to connect to for C2 deployments
|
||||
elif config.get('c2_only') or (not config.get('redirector_only') and not config.get('deploy_tracker')):
|
||||
# Connect to C2 server
|
||||
instance_ip = config.get('c2_ip')
|
||||
instance_name = config.get('c2_name', 'C2 server')
|
||||
elif config.get('redirector_only'):
|
||||
# Connect to redirector
|
||||
instance_ip = config.get('redirector_ip')
|
||||
instance_name = config.get('redirector_name', 'redirector')
|
||||
elif config.get('deploy_tracker') and not config.get('integrated_tracker'):
|
||||
# Connect to tracker
|
||||
instance_ip = config.get('tracker_ip')
|
||||
instance_name = config.get('tracker_name', 'tracker')
|
||||
else:
|
||||
# Default to C2 server
|
||||
instance_ip = config.get('c2_ip')
|
||||
instance_name = config.get('c2_name', 'C2 server')
|
||||
|
||||
if not instance_ip:
|
||||
print(f"{COLORS['RED']}No instance IP found for SSH connection{COLORS['RESET']}")
|
||||
return
|
||||
|
||||
ssh_user = config.get('ssh_user', 'root')
|
||||
|
||||
print(f"{COLORS['GREEN']}Connecting to {instance_name} ({instance_ip})...{COLORS['RESET']}")
|
||||
|
||||
ssh_command = [
|
||||
"ssh",
|
||||
"-i", ssh_key_path,
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "UserKnownHostsFile=/dev/null",
|
||||
"-o", "IdentitiesOnly=yes",
|
||||
f"{ssh_user}@{instance_ip}"
|
||||
]
|
||||
|
||||
try:
|
||||
subprocess.run(ssh_command)
|
||||
except KeyboardInterrupt:
|
||||
print(f"\n{COLORS['YELLOW']}SSH session ended{COLORS['RESET']}")
|
||||
except Exception as e:
|
||||
print(f"{COLORS['RED']}SSH connection failed: {e}{COLORS['RESET']}")
|
||||
|
||||
def cleanup_ssh_keys(deployment_id=None, keep_keys=False):
|
||||
"""Clean up generated SSH keys"""
|
||||
if keep_keys:
|
||||
return
|
||||
|
||||
if hasattr(generate_ssh_key, 'generated_keys'):
|
||||
for key_path in generate_ssh_key.generated_keys:
|
||||
# Only remove keys we generated for this deployment
|
||||
if deployment_id and f"_{deployment_id}" in key_path:
|
||||
try:
|
||||
if os.path.exists(key_path):
|
||||
os.remove(key_path)
|
||||
if os.path.exists(f"{key_path}.pub"):
|
||||
os.remove(f"{key_path}.pub")
|
||||
logging.info(f"Removed SSH key: {key_path}")
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to remove SSH key {key_path}: {e}")
|
||||
Reference in New Issue
Block a user