0799bfbae8
Sanitized version of red team infrastructure automation platform. Operational content (implant pipelines, lures, credential capture) replaced with documented stubs. Architecture and infrastructure automation code intact.
534 lines
20 KiB
Python
Executable File
534 lines
20 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Standalone teardown script for C2itall deployments
|
|
This script can be used to teardown infrastructure when the main menu fails
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import subprocess
|
|
import logging
|
|
import json
|
|
import glob
|
|
import argparse
|
|
from datetime import datetime
|
|
|
|
# Add current directory to path for imports
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
try:
|
|
from utils.common import COLORS, PROVIDER_DIRS, setup_logging
|
|
except ImportError as e:
|
|
print(f"Error importing utils: {e}")
|
|
print("Make sure you're running this from the c2itall directory")
|
|
sys.exit(1)
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description='C2itall Standalone Teardown Tool')
|
|
parser.add_argument('--deployment-id', help='Specific deployment ID to teardown')
|
|
parser.add_argument('--list', action='store_true', help='List all deployments')
|
|
parser.add_argument('--all', action='store_true', help='Teardown all deployments')
|
|
parser.add_argument('--select', action='store_true', help='Interactively select deployment to teardown')
|
|
parser.add_argument('--debug', action='store_true', help='Enable debug output')
|
|
parser.add_argument('--force', action='store_true', help='Skip confirmation prompts')
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.debug:
|
|
logging.basicConfig(level=logging.DEBUG)
|
|
|
|
print(f"{COLORS['BLUE']}C2itall Standalone Teardown Tool{COLORS['RESET']}")
|
|
print(f"{COLORS['BLUE']}================================{COLORS['RESET']}")
|
|
|
|
if args.list:
|
|
list_deployments()
|
|
elif args.all:
|
|
teardown_all_deployments(force=args.force)
|
|
elif args.deployment_id:
|
|
teardown_deployment(args.deployment_id, force=args.force)
|
|
elif args.select:
|
|
select_and_teardown_deployment()
|
|
else:
|
|
interactive_mode()
|
|
|
|
def list_deployments():
|
|
"""List all available deployments"""
|
|
print(f"\n{COLORS['CYAN']}Available Deployments:{COLORS['RESET']}")
|
|
|
|
# Find deployment info files
|
|
info_files = glob.glob("logs/deployment_info_*.txt")
|
|
|
|
if not info_files:
|
|
print(f"{COLORS['GREEN']}No deployments found{COLORS['RESET']}")
|
|
return
|
|
|
|
deployments = []
|
|
for info_file in info_files:
|
|
config = parse_deployment_info(info_file)
|
|
if config:
|
|
deployments.append(config)
|
|
|
|
if not deployments:
|
|
print(f"{COLORS['YELLOW']}Found info files but could not parse deployment data{COLORS['RESET']}")
|
|
return
|
|
|
|
print(f"Found {len(deployments)} deployments:\n")
|
|
|
|
for i, config in enumerate(deployments, 1):
|
|
deployment_id = config.get('deployment_id', 'unknown')
|
|
provider = config.get('provider', 'unknown')
|
|
domain = config.get('domain', 'N/A')
|
|
status = config.get('status', 'unknown')
|
|
|
|
print(f"{i}. Deployment ID: {deployment_id}")
|
|
print(f" Provider: {provider}")
|
|
print(f" Domain: {domain}")
|
|
print(f" Status: {status}")
|
|
|
|
# Show instance names if available
|
|
instances = []
|
|
if config.get('redirector_name'):
|
|
instances.append(f"Redirector: {config['redirector_name']}")
|
|
if config.get('c2_name'):
|
|
instances.append(f"C2: {config['c2_name']}")
|
|
if config.get('tracker_name'):
|
|
instances.append(f"Tracker: {config['tracker_name']}")
|
|
if config.get('attack_box_name'):
|
|
instances.append(f"Attack Box: {config['attack_box_name']}")
|
|
|
|
if instances:
|
|
print(f" Instances: {', '.join(instances)}")
|
|
|
|
print()
|
|
|
|
def interactive_mode():
|
|
"""Interactive teardown mode"""
|
|
while True:
|
|
print(f"\n{COLORS['WHITE']}Teardown Options:{COLORS['RESET']}")
|
|
print("1) List all deployments")
|
|
print("2) Select deployment to teardown")
|
|
print("3) Teardown all deployments")
|
|
print("4) Manual cleanup guidance")
|
|
print("5) Exit")
|
|
|
|
choice = input(f"\nSelect option (1-5): ").strip()
|
|
|
|
if choice == "1":
|
|
list_deployments()
|
|
elif choice == "2":
|
|
select_and_teardown_deployment()
|
|
elif choice == "3":
|
|
teardown_all_deployments()
|
|
elif choice == "4":
|
|
show_manual_cleanup_guidance()
|
|
elif choice == "5":
|
|
print(f"{COLORS['GREEN']}Goodbye!{COLORS['RESET']}")
|
|
break
|
|
else:
|
|
print(f"{COLORS['RED']}Invalid option{COLORS['RESET']}")
|
|
|
|
def teardown_deployment(deployment_id, force=False):
|
|
"""Teardown a specific deployment"""
|
|
print(f"\n{COLORS['BLUE']}Tearing down deployment: {deployment_id}{COLORS['RESET']}")
|
|
|
|
# Find deployment info
|
|
info_files = glob.glob(f"logs/deployment_info_{deployment_id}*.txt")
|
|
|
|
if not info_files:
|
|
print(f"{COLORS['RED']}No deployment info found for ID: {deployment_id}{COLORS['RESET']}")
|
|
print(f"{COLORS['YELLOW']}Available deployments:{COLORS['RESET']}")
|
|
list_deployments()
|
|
return False
|
|
|
|
config = parse_deployment_info(info_files[0])
|
|
if not config:
|
|
print(f"{COLORS['RED']}Could not parse deployment configuration{COLORS['RESET']}")
|
|
return False
|
|
|
|
# Show what will be torn down
|
|
provider = config.get('provider', 'unknown')
|
|
print(f"Provider: {provider}")
|
|
|
|
instances_to_delete = []
|
|
if config.get('redirector_name'):
|
|
instances_to_delete.append(f"Redirector: {config['redirector_name']}")
|
|
if config.get('c2_name'):
|
|
instances_to_delete.append(f"C2: {config['c2_name']}")
|
|
if config.get('tracker_name'):
|
|
instances_to_delete.append(f"Tracker: {config['tracker_name']}")
|
|
if config.get('attack_box_name'):
|
|
instances_to_delete.append(f"Attack Box: {config['attack_box_name']}")
|
|
|
|
if instances_to_delete:
|
|
print(f"Instances to delete:")
|
|
for instance in instances_to_delete:
|
|
print(f" - {instance}")
|
|
else:
|
|
print(f"{COLORS['YELLOW']}No instances found in configuration{COLORS['RESET']}")
|
|
|
|
# Confirm deletion
|
|
if not force:
|
|
confirm = input(f"\n{COLORS['YELLOW']}Proceed with teardown? (yes/no): {COLORS['RESET']}").strip().lower()
|
|
if confirm != 'yes':
|
|
print(f"{COLORS['GREEN']}Teardown cancelled{COLORS['RESET']}")
|
|
return False
|
|
|
|
# Set up logging
|
|
log_file = setup_logging(deployment_id, "teardown")
|
|
print(f"Teardown logs will be written to: {log_file}")
|
|
|
|
# Execute teardown
|
|
success = execute_teardown(config)
|
|
|
|
if success:
|
|
print(f"\n{COLORS['GREEN']}Teardown completed successfully!{COLORS['RESET']}")
|
|
# Clean up SSH keys
|
|
cleanup_ssh_keys(deployment_id)
|
|
# Archive logs
|
|
archive_logs(deployment_id)
|
|
else:
|
|
print(f"\n{COLORS['RED']}Teardown failed or incomplete{COLORS['RESET']}")
|
|
print(f"Check logs for details: {log_file}")
|
|
|
|
return success
|
|
|
|
def teardown_all_deployments(force=False):
|
|
"""Teardown all deployments"""
|
|
print(f"\n{COLORS['RED']}⚠️ WARNING: This will teardown ALL deployments!{COLORS['RESET']}")
|
|
|
|
if not force:
|
|
confirm = input(f"{COLORS['YELLOW']}Type 'DESTROY' to confirm: {COLORS['RESET']}")
|
|
if confirm != "DESTROY":
|
|
print(f"{COLORS['GREEN']}Operation cancelled{COLORS['RESET']}")
|
|
return
|
|
|
|
info_files = glob.glob("logs/deployment_info_*.txt")
|
|
|
|
if not info_files:
|
|
print(f"{COLORS['GREEN']}No deployments found{COLORS['RESET']}")
|
|
return
|
|
|
|
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"\n{COLORS['CYAN']}Tearing down: {deployment_id}{COLORS['RESET']}")
|
|
|
|
if execute_teardown(config):
|
|
success_count += 1
|
|
cleanup_ssh_keys(deployment_id)
|
|
archive_logs(deployment_id)
|
|
print(f"{COLORS['GREEN']}✓ {deployment_id} torn down{COLORS['RESET']}")
|
|
else:
|
|
print(f"{COLORS['RED']}✗ {deployment_id} teardown failed{COLORS['RESET']}")
|
|
|
|
print(f"\n{COLORS['BLUE']}Summary: {success_count}/{len(info_files)} deployments torn down{COLORS['RESET']}")
|
|
|
|
def parse_deployment_info(info_file):
|
|
"""Parse deployment info file"""
|
|
try:
|
|
config = {}
|
|
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
|
|
|
|
# Convert string values
|
|
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:
|
|
print(f"{COLORS['RED']}Error parsing {info_file}: {e}{COLORS['RESET']}")
|
|
return None
|
|
|
|
def execute_teardown(config):
|
|
"""Execute the actual teardown"""
|
|
provider = config.get('provider')
|
|
deployment_id = config.get('deployment_id')
|
|
|
|
if not provider:
|
|
print(f"{COLORS['RED']}No provider specified in configuration{COLORS['RESET']}")
|
|
return False
|
|
|
|
# Set provider environment
|
|
set_provider_environment(config)
|
|
|
|
# Get provider directory
|
|
provider_dir = PROVIDER_DIRS.get(provider, provider.capitalize())
|
|
cleanup_playbook = f"providers/{provider_dir}/cleanup.yml"
|
|
|
|
if not os.path.exists(cleanup_playbook):
|
|
print(f"{COLORS['RED']}Cleanup playbook not found: {cleanup_playbook}{COLORS['RESET']}")
|
|
show_manual_cleanup_guidance(config)
|
|
return False
|
|
|
|
print(f"Using cleanup playbook: {cleanup_playbook}")
|
|
|
|
# Create extra vars
|
|
extra_vars = create_extra_vars(config)
|
|
|
|
# Build ansible command - use venv ansible if available
|
|
venv_ansible = os.path.join(os.path.dirname(__file__), 'venv', 'bin', 'ansible-playbook')
|
|
if os.path.exists(venv_ansible):
|
|
ansible_cmd = venv_ansible
|
|
# Set Python interpreter for the venv
|
|
venv_python = os.path.join(os.path.dirname(__file__), 'venv', 'bin', 'python')
|
|
os.environ['ANSIBLE_PYTHON_INTERPRETER'] = venv_python
|
|
else:
|
|
ansible_cmd = 'ansible-playbook'
|
|
|
|
cmd = [
|
|
ansible_cmd,
|
|
cleanup_playbook,
|
|
'--extra-vars', extra_vars,
|
|
'-v'
|
|
]
|
|
|
|
print(f"Executing: {' '.join(cmd)}")
|
|
|
|
try:
|
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
|
|
|
print(f"Return code: {result.returncode}")
|
|
if result.stdout:
|
|
print(f"STDOUT:\n{result.stdout}")
|
|
if result.stderr:
|
|
print(f"STDERR:\n{result.stderr}")
|
|
|
|
return result.returncode == 0
|
|
|
|
except subprocess.TimeoutExpired:
|
|
print(f"{COLORS['RED']}Teardown timed out after 5 minutes{COLORS['RESET']}")
|
|
return False
|
|
except Exception as e:
|
|
print(f"{COLORS['RED']}Error executing teardown: {e}{COLORS['RESET']}")
|
|
return False
|
|
|
|
def set_provider_environment(config):
|
|
"""Set provider-specific environment variables"""
|
|
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":
|
|
if config.get('linode_token'):
|
|
os.environ['LINODE_TOKEN'] = config['linode_token']
|
|
else:
|
|
# Try to load from vars file
|
|
try:
|
|
import yaml
|
|
provider_dir = PROVIDER_DIRS.get(provider, provider.capitalize())
|
|
vars_file = f"providers/{provider_dir}/vars.yaml"
|
|
if os.path.exists(vars_file):
|
|
with open(vars_file, 'r') as f:
|
|
vars_data = yaml.safe_load(f)
|
|
if vars_data and vars_data.get('linode_token'):
|
|
os.environ['LINODE_TOKEN'] = vars_data['linode_token']
|
|
print(f"Loaded Linode token from {vars_file}")
|
|
except Exception as e:
|
|
print(f"{COLORS['YELLOW']}Warning: Could not load Linode token: {e}{COLORS['RESET']}")
|
|
|
|
def create_extra_vars(config):
|
|
"""Create Ansible extra variables"""
|
|
extra_vars = {
|
|
'deployment_id': config.get('deployment_id'),
|
|
'provider': config.get('provider'),
|
|
'skip_confirmation': True
|
|
}
|
|
|
|
# Add instance names
|
|
if config.get('redirector_name'):
|
|
extra_vars['redirector_name'] = config['redirector_name']
|
|
if config.get('c2_name'):
|
|
extra_vars['c2_name'] = config['c2_name']
|
|
if config.get('tracker_name'):
|
|
extra_vars['tracker_name'] = config['tracker_name']
|
|
if config.get('attack_box_name'):
|
|
extra_vars['attack_box_name'] = config['attack_box_name']
|
|
|
|
return json.dumps(extra_vars)
|
|
|
|
def cleanup_ssh_keys(deployment_id):
|
|
"""Clean up SSH keys for deployment"""
|
|
ssh_dir = os.path.expanduser("~/.ssh")
|
|
patterns = [
|
|
f"c2deploy_{deployment_id}*",
|
|
f"attack-box-{deployment_id}*",
|
|
f"a-{deployment_id}*"
|
|
]
|
|
|
|
removed_keys = []
|
|
for pattern in patterns:
|
|
for key_file in glob.glob(os.path.join(ssh_dir, pattern)):
|
|
try:
|
|
os.remove(key_file)
|
|
removed_keys.append(key_file)
|
|
except Exception as e:
|
|
print(f"{COLORS['YELLOW']}Warning: Could not remove {key_file}: {e}{COLORS['RESET']}")
|
|
|
|
if removed_keys:
|
|
print(f"Removed SSH keys: {', '.join([os.path.basename(k) for k in removed_keys])}")
|
|
else:
|
|
print("No SSH keys found to remove")
|
|
|
|
def archive_logs(deployment_id):
|
|
"""Archive logs for the deployment"""
|
|
try:
|
|
archive_dir = "logs/archive"
|
|
os.makedirs(archive_dir, exist_ok=True)
|
|
|
|
patterns = [
|
|
f"logs/deployment_{deployment_id}*",
|
|
f"logs/deployment_info_{deployment_id}*",
|
|
f"logs/teardown_{deployment_id}*"
|
|
]
|
|
|
|
archived_files = []
|
|
for pattern in patterns:
|
|
for file_path in glob.glob(pattern):
|
|
filename = os.path.basename(file_path)
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
archive_filename = f"{timestamp}_{filename}"
|
|
archive_path = os.path.join(archive_dir, archive_filename)
|
|
|
|
os.rename(file_path, archive_path)
|
|
archived_files.append(archive_filename)
|
|
|
|
if archived_files:
|
|
print(f"Archived logs: {', '.join(archived_files)}")
|
|
else:
|
|
print("No logs found to archive")
|
|
|
|
except Exception as e:
|
|
print(f"{COLORS['YELLOW']}Warning: Could not archive logs: {e}{COLORS['RESET']}")
|
|
|
|
def show_manual_cleanup_guidance(config=None):
|
|
"""Show manual cleanup guidance"""
|
|
print(f"\n{COLORS['CYAN']}Manual Cleanup Guidance{COLORS['RESET']}")
|
|
print(f"{COLORS['CYAN']}========================{COLORS['RESET']}")
|
|
|
|
if config:
|
|
provider = config.get('provider', 'unknown')
|
|
deployment_id = config.get('deployment_id', 'unknown')
|
|
|
|
print(f"Deployment ID: {deployment_id}")
|
|
print(f"Provider: {provider}")
|
|
|
|
if provider == "linode":
|
|
print(f"\nLinode cleanup steps:")
|
|
print(f"1. Go to https://cloud.linode.com/")
|
|
print(f"2. Check 'Linodes' section for instances with labels containing: {deployment_id}")
|
|
print(f"3. Delete any instances found")
|
|
print(f"4. Check 'Firewalls' section for firewalls with labels containing: {deployment_id}")
|
|
print(f"5. Delete any firewalls found")
|
|
|
|
elif provider == "aws":
|
|
print(f"\nAWS cleanup steps:")
|
|
print(f"1. Go to AWS EC2 Console")
|
|
print(f"2. Check instances with tags containing: {deployment_id}")
|
|
print(f"3. Delete instances and associated resources")
|
|
print(f"4. Check security groups, key pairs, and elastic IPs")
|
|
|
|
print(f"\nLocal cleanup:")
|
|
print(f"1. SSH keys in ~/.ssh/ starting with c2deploy_, attack-box-, or a-")
|
|
print(f"2. Log files in logs/ directory")
|
|
print(f"3. Any temporary files or state files")
|
|
|
|
def select_and_teardown_deployment():
|
|
"""Interactive deployment selection for teardown"""
|
|
print(f"\n{COLORS['CYAN']}Select Deployment to Teardown:{COLORS['RESET']}")
|
|
|
|
# Find deployment info files
|
|
info_files = glob.glob("logs/deployment_info_*.txt")
|
|
|
|
if not info_files:
|
|
print(f"{COLORS['GREEN']}No deployments found{COLORS['RESET']}")
|
|
return
|
|
|
|
deployments = []
|
|
for info_file in info_files:
|
|
config = parse_deployment_info(info_file)
|
|
if config:
|
|
deployments.append(config)
|
|
|
|
if not deployments:
|
|
print(f"{COLORS['YELLOW']}Found info files but could not parse deployment data{COLORS['RESET']}")
|
|
return
|
|
|
|
print(f"\nFound {len(deployments)} deployments:")
|
|
print("=" * 40)
|
|
|
|
# Display deployments with selection numbers
|
|
for i, config in enumerate(deployments, 1):
|
|
deployment_id = config.get('deployment_id', 'unknown')
|
|
provider = config.get('provider', 'unknown')
|
|
domain = config.get('domain', 'N/A')
|
|
|
|
print(f"{i}. {COLORS['WHITE']}{deployment_id}{COLORS['RESET']}")
|
|
print(f" Provider: {provider}")
|
|
print(f" Domain: {domain}")
|
|
|
|
# Show instance names if available
|
|
instances = []
|
|
if config.get('redirector_name'):
|
|
instances.append(f"Redirector: {config['redirector_name']}")
|
|
if config.get('c2_name'):
|
|
instances.append(f"C2: {config['c2_name']}")
|
|
if config.get('tracker_name'):
|
|
instances.append(f"Tracker: {config['tracker_name']}")
|
|
if config.get('attack_box_name'):
|
|
instances.append(f"Attack Box: {config['attack_box_name']}")
|
|
|
|
if instances:
|
|
print(f" Instances: {', '.join(instances)}")
|
|
print()
|
|
|
|
# Get user selection
|
|
while True:
|
|
try:
|
|
selection = input(f"\nSelect deployment to teardown (1-{len(deployments)}), or 'q' to quit: ").strip()
|
|
|
|
if selection.lower() == 'q':
|
|
print(f"{COLORS['GREEN']}Selection cancelled{COLORS['RESET']}")
|
|
return
|
|
|
|
selection_num = int(selection)
|
|
if 1 <= selection_num <= len(deployments):
|
|
selected_deployment = deployments[selection_num - 1]
|
|
deployment_id = selected_deployment.get('deployment_id', 'unknown')
|
|
|
|
print(f"\n{COLORS['YELLOW']}Selected deployment: {deployment_id}{COLORS['RESET']}")
|
|
|
|
# Confirm selection and proceed with teardown
|
|
confirm = input(f"Proceed with teardown of {deployment_id}? (yes/no): ").strip().lower()
|
|
if confirm == 'yes':
|
|
teardown_deployment(deployment_id)
|
|
else:
|
|
print(f"{COLORS['GREEN']}Teardown cancelled{COLORS['RESET']}")
|
|
return
|
|
else:
|
|
print(f"{COLORS['RED']}Please enter a number between 1 and {len(deployments)}{COLORS['RESET']}")
|
|
except ValueError:
|
|
print(f"{COLORS['RED']}Please enter a valid number or 'q' to quit{COLORS['RESET']}")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|