From 43a6ddc5790c9c1622b59e0ae485bfa2995d1c2e Mon Sep 17 00:00:00 2001 From: n0mad1k Date: Thu, 10 Apr 2025 06:59:59 -0400 Subject: [PATCH] still working out bugs --- .gitignore | 3 +- Linode/redirector.yml | 31 +- deploy.py | 1998 +++++++++++++++++++++++++++++++++++------ requirements.txt | 3 +- 4 files changed, 1721 insertions(+), 314 deletions(-) diff --git a/.gitignore b/.gitignore index 6cdf905..c26cca1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ vars.yaml venv deployment* -config.yml \ No newline at end of file +config.yml +logs/ \ No newline at end of file diff --git a/Linode/redirector.yml b/Linode/redirector.yml index bfe3b3e..a94e7eb 100644 --- a/Linode/redirector.yml +++ b/Linode/redirector.yml @@ -3,33 +3,28 @@ hosts: localhost gather_facts: false connection: local - vars: - region: "{{ linode_region | default('us-east') }}" - instance_type: "{{ size | default('g6-standard-1') }}" - instance_name: "{{ redirector_name | default('redirector') }}" - domain: "{{ domain | default('example.com') }}" - redirector_subdomain: "cdn" - linode_image: "linode/debian11" - + vars_files: + - vars.yaml tasks: - name: Create Linode redirector instance community.general.linode_v4: - label: "{{ instance_name }}" + access_token: "{{ linode_token }}" + label: "{{ redirector_name }}" type: "{{ plan }}" region: "{{ region }}" - image: "{{ linode_image }}" - root_pass: "{{ lookup('password', '/dev/null length=24 chars=ascii_letters,digits') }}" - authorized_keys: - - "{{ ssh_key_content }}" + image: "linode/debian11" + root_pass: "{{ lookup('password', '/dev/null length=16') }}" + authorized_keys: + - "{{ lookup('file', ssh_key_path + '.pub') }}" state: present register: redirector_instance - name: Save redirector IP - set_fact: + ansible.builtin.set_fact: redirector_ip: "{{ redirector_instance.instance.ipv4[0] }}" - name: Wait for SSH to become available - wait_for: + ansible.builtin.wait_for: host: "{{ redirector_ip }}" port: 22 delay: 10 @@ -37,11 +32,11 @@ state: started - name: Add redirector to inventory - add_host: - name: redirector + ansible.builtin.add_host: + name: "redirector" ansible_host: "{{ redirector_ip }}" ansible_user: "{{ ssh_user | default('root') }}" - ansible_ssh_private_key_file: "{{ ssh_key | replace('.pub', '') }}" + ansible_ssh_private_key_file: "{{ ssh_key_path }}" groups: redirectors - name: Configure redirector diff --git a/deploy.py b/deploy.py index f538797..c396983 100644 --- a/deploy.py +++ b/deploy.py @@ -10,6 +10,11 @@ import json import random import string import shutil +import re +import socket +import paramiko +import logging +import glob from datetime import datetime # Constants @@ -25,9 +30,26 @@ DEFAULT_SIZE = { "flokinet": "standard" } +def normalize_provider_name(provider_name): + """Normalize provider name to expected capitalization""" + if not provider_name: + return None + + provider_map = { + 'aws': 'AWS', + 'linode': 'Linode', + 'flokinet': 'FlokiNET' + } + + normalized = provider_map.get(provider_name.lower()) + if normalized: + return normalized + + return provider_name + def setup_argparse(): """Set up and return the argument parser""" - parser = argparse.ArgumentParser(description='Deploy Red Team infrastructure') + parser = argparse.ArgumentParser(description='Deploy C2itAll Red Team infrastructure') # Provider selection parser.add_argument('-p', '--provider', choices=PROVIDERS, help='Provider to use for deployment') @@ -67,6 +89,20 @@ def setup_argparse(): parser.add_argument('--secure-memory', action='store_true', help='Enable secure memory settings') parser.add_argument('--zero-logs', action='store_true', help='Enable zero-logs configuration') + # Post-deployment options + parser.add_argument('--ssh-after-deploy', action='store_true', help='SSH into the instance after deployment') + parser.add_argument('--copy-ssh-key', action='store_true', help='Copy SSH key to the server for passwordless login') + parser.add_argument('--run-tests', action='store_true', help='Run tests after deployment') + + # Tracker module integration + parser.add_argument('--deploy-tracker', action='store_true', help='Deploy tracker module') + parser.add_argument('--tracker-domain', help='Domain name for the tracker server') + parser.add_argument('--tracker-email', help='Email for the tracker server Let\'s Encrypt certificate') + parser.add_argument('--tracker-name', help='Name for the tracker engagement') + parser.add_argument('--tracker-ipinfo-token', help='IPinfo API token for tracker geolocation data') + parser.add_argument('--tracker-setup-ssl', action='store_true', help='Set up SSL certificates for tracker') + parser.add_argument('--tracker-create-pixel', action='store_true', help='Create email tracking pixel') + return parser.parse_args() def load_vars_file(provider): @@ -181,6 +217,9 @@ def load_config(args): config['redirector_only'] = args.redirector_only config['c2_only'] = args.c2_only config['debug'] = args.debug + config['ssh_after_deploy'] = args.ssh_after_deploy + config['copy_ssh_key'] = args.copy_ssh_key + config['run_tests'] = args.run_tests # Domain and email config['domain'] = args.domain or vars_data.get('domain', 'example.com') @@ -210,6 +249,15 @@ def load_config(args): # Set random SSH port for better OPSEC config['ssh_port'] = vars_data.get('ssh_port', random.randint(20000, 65000)) + # Tracker module settings + config['deploy_tracker'] = args.deploy_tracker + config['tracker_domain'] = args.tracker_domain + config['tracker_email'] = args.tracker_email + config['tracker_name'] = args.tracker_name + config['tracker_ipinfo_token'] = args.tracker_ipinfo_token + config['tracker_setup_ssl'] = args.tracker_setup_ssl + config['tracker_create_pixel'] = args.tracker_create_pixel + return config def select_region(config): @@ -267,6 +315,81 @@ def select_region(config): return DEFAULT_REGION.get(provider, 'us-east') +def verify_key_file_permissions(key_path): + """Verify and fix SSH key file permissions""" + try: + # Check if key file exists + if not os.path.exists(key_path): + print(f"[-] SSH key file not found: {key_path}") + return False + + # Fix permissions + current_mode = os.stat(key_path).st_mode & 0o777 + if current_mode != 0o600: + print(f"[!] SSH key has incorrect permissions: {oct(current_mode)[2:]}. Fixing to 0600...") + os.chmod(key_path, 0o600) + print(f"[+] SSH key permissions fixed") + + return True + except Exception as e: + print(f"[-] Error verifying key file permissions: {e}") + return False + +def parse_ansible_error(stderr): + """Parse ansible error output to extract useful information""" + error_info = {} + + # Look for common ansible error patterns + if "FAILED! => " in stderr: + # Extract the JSON error part + error_start = stderr.find("FAILED! => ") + error_text = stderr[error_start:].split("\n")[0] + # Remove the "FAILED! => " prefix + error_text = error_text.replace("FAILED! => ", "") + + try: + # Try to parse as JSON + error_info = json.loads(error_text) + except: + # If not valid JSON, use the text as is + error_info = {"msg": error_text} + + # Look for common error messages + if "SSH Error:" in stderr: + error_info["ssh_error"] = True + + if "No such file or directory" in stderr: + error_info["file_not_found"] = True + + if "Permission denied" in stderr: + error_info["permission_denied"] = True + + if "The API Token provided is not valid" in stderr: + error_info["invalid_api_token"] = True + + return error_info + +def setup_error_logging(): + """Setup error logging and ensure log directory exists""" + log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs") + os.makedirs(log_dir, exist_ok=True) + + # Set up global log file + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + log_file = os.path.join(log_dir, f"deployment_{timestamp}.log") + + # Configure logging + logging.basicConfig( + filename=log_file, + level=logging.DEBUG if '--debug' in sys.argv else logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' + ) + + # Log start of script + logging.info("C2itAll deployment script started") + + return log_file + def validate_config(config): """Validate the configuration""" provider = config['provider'] @@ -301,6 +424,11 @@ def validate_config(config): print("[-] Error: Cannot specify both --redirector-only and --c2-only") return False + # Tracker module validation + if config['deploy_tracker']: + if not config.get('tracker_domain') and not config.get('tracker_ipinfo_token'): + print("[-] Warning: Tracker deployment without domain or IPInfo token will have limited functionality") + return True def deploy_aws(config): @@ -311,19 +439,34 @@ def deploy_aws(config): os.environ['AWS_ACCESS_KEY_ID'] = config['aws_key'] os.environ['AWS_SECRET_ACCESS_KEY'] = config['aws_secret'] + # Create logs directory + log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs") + os.makedirs(log_dir, exist_ok=True) + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + + # Only create one log file in the logs directory + log_file = os.path.join(log_dir, f"deployment_{timestamp}_aws.log") + + print(f"[+] Creating deployment log at: {log_file}") + # Prepare SSH key - for AWS we'll use AWS key pairs instead of local files ssh_key_name = config.get('ssh_key_name') if not ssh_key_name: # Generate a random key name for AWS rand_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8)) timestamp = int(time.time()) % 10000 - ssh_key_name = f"c2ingred-{rand_suffix}-{timestamp}" + ssh_key_name = f"c2itall-{rand_suffix}-{timestamp}" config['ssh_key_name'] = ssh_key_name print(f"[+] Using AWS key pair name: {ssh_key_name}") # Create a temporary inventory file for Ansible - with open('inventory_aws.yml', 'w') as f: + inventory_file = f"inventory_aws_{int(time.time())}.yml" + + with open(log_file, 'a') as f: + f.write(f"==== CREATING INVENTORY FILE: {inventory_file} ====\n") + + with open(inventory_file, 'w') as f: f.write("---\n") f.write("all:\n") f.write(" vars:\n") @@ -340,6 +483,13 @@ def deploy_aws(config): f.write(" c2:\n") f.write(" ansible_host: '{{ c2_ip }}'\n") + # Log inventory file content + with open(log_file, 'a') as f: + f.write(f"INVENTORY CONTENT:\n") + with open(inventory_file, 'r') as inv: + f.write(inv.read()) + f.write("\n==== END INVENTORY FILE ====\n\n") + # Generate default names if not provided rand_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8)) timestamp = int(time.time()) % 10000 @@ -351,26 +501,29 @@ def deploy_aws(config): config['c2_name'] = c2_name # Log important information - print(f"[+] Deployment information:") - print(f" - AWS Key Pair Name: {ssh_key_name}") - print(f" - Redirector Name: {redirector_name}") - print(f" - C2 Name: {c2_name}") - print(f" - AWS Region: {config.get('aws_region', 'default region')}") - print(f" - Domain: {config.get('domain', 'example.com')}") + with open(log_file, 'a') as f: + f.write(f"==== DEPLOYMENT INFORMATION ====\n") + f.write(f"AWS Key Pair Name: {ssh_key_name}\n") + f.write(f"Redirector Name: {redirector_name}\n") + f.write(f"C2 Name: {c2_name}\n") + f.write(f"AWS Region: {config.get('aws_region', 'default region')}\n") + f.write(f"Domain: {config.get('domain', 'example.com')}\n") + f.write(f"==== END DEPLOYMENT INFORMATION ====\n\n") - # Run AWS provisioning playbook - success = False + # Track deployed instances and resources for cleanup if needed deployed_instances = [] + deployed_resources = [] + success = False try: # Create extra_vars dictionary with all config values extra_vars = { 'aws_access_key': config['aws_key'], 'aws_secret_key': config['aws_secret'], - 'aws_region': config.get('aws_region', DEFAULT_REGION.get('AWS')), + 'aws_region': config.get('aws_region', DEFAULT_REGION['aws']), 'ami_map': config.get('ami_map', {}), - 'size': config.get('size', DEFAULT_SIZE.get('AWS')), - 'aws_instance_type': config.get('size', DEFAULT_SIZE.get('AWS')), + 'size': config.get('size', DEFAULT_SIZE['aws']), + 'aws_instance_type': config.get('size', DEFAULT_SIZE['aws']), 'redirector_name': redirector_name, 'c2_name': c2_name, 'teardown': config.get('teardown', False), @@ -386,7 +539,9 @@ def deploy_aws(config): 'smtp_auth_user': config.get('smtp_auth_user', f"user{random.randint(1000, 9999)}"), 'smtp_auth_pass': config.get('smtp_auth_pass', ''.join(random.choices(string.ascii_letters + string.digits + "!@#$%^&*", k=20))), 'key_name': ssh_key_name, - 'private_key_path': f"~/.ssh/{ssh_key_name}.pem", + 'ssh_user': config.get('ssh_user', 'ec2-user'), + 'instance_label': c2_name if config.get('c2_only', False) else redirector_name, + 'private_key_path': f"~/.ssh/{ssh_key_name}", } # Format extra vars for command line, handling special characters @@ -410,74 +565,269 @@ def deploy_aws(config): if config.get('ami_map'): ami_map_str = f"ami_map='{json.dumps(config['ami_map'])}'" - # Determine playbook to run based on deployment mode + # Execute deployment based on deployment mode if extra_vars['redirector_only']: playbook = "AWS/redirector.yml" - cmd = f"ansible-playbook -i inventory_aws.yml {playbook} -e '{extra_vars_str} {ami_map_str}'" + cmd = f"ansible-playbook -i {inventory_file} {playbook} -e '{extra_vars_str} {ami_map_str}'" - if extra_vars['debug']: - print(f"[+] Running: {cmd}") + with open(log_file, 'a') as f: + f.write(f"==== RUNNING REDIRECTOR PLAYBOOK ====\n") + f.write(f"Command: {cmd}\n\n") + + print(f"[+] Running: {cmd}") + success, stdout, stderr = run_command_with_logging(cmd, config, log_file) + if not success: + print(f"[-] Redirector deployment failed. See {log_file} for details.") + # Track created resources for cleanup + try: + # Get instance ID for cleanup + instance_id = get_aws_instance_id(redirector_name, config) + if instance_id: + deployed_instances.append((redirector_name, instance_id)) + # Also add key pair to resources for cleanup + deployed_resources.append(("key_pair", ssh_key_name)) + except Exception as e: + print(f"[!] Failed to get instance ID: {e}") + return False, [log_file], deployed_instances + deployed_resources + + # Track deployed instances and resources + instance_id = get_aws_instance_id(redirector_name, config) + if instance_id: + deployed_instances.append((redirector_name, instance_id)) + deployed_resources.append(("key_pair", ssh_key_name)) + + # Store redirector IP and key path for SSH access later + config['redirector_ip'] = get_aws_instance_ip(redirector_name, config) + config['redirector_key_path'] = f"~/.ssh/{ssh_key_name}.pem" - subprocess.run(cmd, shell=True, check=True) - deployed_instances.append(redirector_name) elif extra_vars['c2_only']: playbook = "AWS/c2.yml" - cmd = f"ansible-playbook -i inventory_aws.yml {playbook} -e '{extra_vars_str} {ami_map_str}'" + cmd = f"ansible-playbook -i {inventory_file} {playbook} -e '{extra_vars_str} {ami_map_str}'" - if extra_vars['debug']: - print(f"[+] Running: {cmd}") + with open(log_file, 'a') as f: + f.write(f"==== RUNNING C2 PLAYBOOK ====\n") + f.write(f"Command: {cmd}\n\n") + + print(f"[+] Running: {cmd}") + success, stdout, stderr = run_command_with_logging(cmd, config, log_file) + if not success: + print(f"[-] C2 server deployment failed. See {log_file} for details.") + # Track created resources for cleanup + try: + # Get instance ID for cleanup + instance_id = get_aws_instance_id(c2_name, config) + if instance_id: + deployed_instances.append((c2_name, instance_id)) + # Also add key pair to resources for cleanup + deployed_resources.append(("key_pair", ssh_key_name)) + except Exception as e: + print(f"[!] Failed to get instance ID: {e}") + return False, [log_file], deployed_instances + deployed_resources + + # Track deployed instances and resources + instance_id = get_aws_instance_id(c2_name, config) + if instance_id: + deployed_instances.append((c2_name, instance_id)) + deployed_resources.append(("key_pair", ssh_key_name)) + + # Store C2 IP and key path for SSH access later + config['c2_ip'] = get_aws_instance_ip(c2_name, config) + config['c2_key_path'] = f"~/.ssh/{ssh_key_name}.pem" - subprocess.run(cmd, shell=True, check=True) - deployed_instances.append(c2_name) 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 extra_vars['debug']: - print(f"[+] Running: {redirector_cmd}") - subprocess.run(redirector_cmd, shell=True, check=True) - deployed_instances.append(redirector_name) + redirector_cmd = f"ansible-playbook -i {inventory_file} AWS/redirector.yml -e '{extra_vars_str} {ami_map_str}'" + + with open(log_file, 'a') as f: + f.write(f"==== RUNNING REDIRECTOR PLAYBOOK (FULL DEPLOYMENT) ====\n") + f.write(f"Command: {redirector_cmd}\n\n") + + print(f"[+] Running: {redirector_cmd}") + success, stdout, stderr = run_command_with_logging(redirector_cmd, config, log_file) + if not success: + print(f"[-] Redirector deployment failed. See {log_file} for details.") + # Track created resources for cleanup + try: + # Get instance ID for cleanup + instance_id = get_aws_instance_id(redirector_name, config) + if instance_id: + deployed_instances.append((redirector_name, instance_id)) + # Also add key pair to resources for cleanup + deployed_resources.append(("key_pair", ssh_key_name)) + except Exception as e: + print(f"[!] Failed to get instance ID: {e}") + return False, [log_file], deployed_instances + deployed_resources + + # Track deployed redirector + instance_id = get_aws_instance_id(redirector_name, config) + if instance_id: + deployed_instances.append((redirector_name, instance_id)) + + # Get and store redirector IP for C2 configuration + redirector_ip = get_aws_instance_ip(redirector_name, config) + config['redirector_ip'] = redirector_ip + config['redirector_key_path'] = f"~/.ssh/{ssh_key_name}.pem" + + # Update extra vars with redirector IP + extra_vars['redirector_ip'] = redirector_ip + extra_vars_list.append(f"redirector_ip='{redirector_ip}'") + extra_vars_str = " ".join(extra_vars_list) + + # Log updated extra_vars + with open(log_file, 'a') as f: + f.write(f"==== UPDATED EXTRA VARS WITH REDIRECTOR IP ====\n") + f.write(f"redirector_ip: {redirector_ip}\n") + f.write(f"==== END UPDATED EXTRA VARS ====\n\n") # Then deploy C2 server with redirector IP - c2_cmd = f"ansible-playbook -i inventory_aws.yml AWS/c2.yml -e '{extra_vars_str} {ami_map_str}'" - if extra_vars['debug']: - print(f"[+] Running: {c2_cmd}") - subprocess.run(c2_cmd, shell=True, check=True) - deployed_instances.append(c2_name) + c2_cmd = f"ansible-playbook -i {inventory_file} AWS/c2.yml -e '{extra_vars_str} {ami_map_str}'" + + with open(log_file, 'a') as f: + f.write(f"==== RUNNING C2 PLAYBOOK (FULL DEPLOYMENT) ====\n") + f.write(f"Command: {c2_cmd}\n\n") + + print(f"[+] Running: {c2_cmd}") + success, stdout, stderr = run_command_with_logging(c2_cmd, config, log_file) + if not success: + print(f"[-] C2 server deployment failed. See {log_file} for details.") + # Track created resources for cleanup + try: + # Get instance ID for cleanup + instance_id = get_aws_instance_id(c2_name, config) + if instance_id: + deployed_instances.append((c2_name, instance_id)) + except Exception as e: + print(f"[!] Failed to get instance ID: {e}") + return False, [log_file], deployed_instances + deployed_resources + + # Track deployed C2 + instance_id = get_aws_instance_id(c2_name, config) + if instance_id: + deployed_instances.append((c2_name, instance_id)) + + # Add key pair to resources + deployed_resources.append(("key_pair", ssh_key_name)) + + # Store C2 IP and key path for SSH access later + config['c2_ip'] = get_aws_instance_ip(c2_name, config) + config['c2_key_path'] = f"~/.ssh/{ssh_key_name}.pem" + + # Deploy tracker if requested + if config.get('deploy_tracker'): + with open(log_file, 'a') as f: + f.write(f"==== DEPLOYING TRACKER MODULE ====\n") + + tracker_success = deploy_tracker_module(config) + + with open(log_file, 'a') as f: + f.write(f"Tracker deployment {'succeeded' if tracker_success else 'failed'}\n") + f.write(f"==== END TRACKER DEPLOYMENT ====\n\n") + + if not tracker_success: + print("[!] Warning: Tracker deployment failed, but continuing with main deployment") print("[+] AWS infrastructure deployed successfully!") success = True - return True - except subprocess.CalledProcessError as e: + + # Log success + with open(log_file, 'a') as f: + f.write(f"==== DEPLOYMENT SUCCESSFUL ====\n") + f.write(f"Deployed instances: {', '.join([n for n, _ in deployed_instances])}\n") + f.write(f"Deployed resources: {', '.join([f'{t}: {n}' for t, n in deployed_resources])}\n") + if 'redirector_ip' in config: + f.write(f"Redirector IP: {config['redirector_ip']}\n") + if 'c2_ip' in config: + f.write(f"C2 IP: {config['c2_ip']}\n") + f.write(f"==== END DEPLOYMENT SUCCESS ====\n\n") + + return success, [log_file], deployed_instances + deployed_resources + + except Exception as e: + # Log exception details + with open(log_file, 'a') as f: + f.write(f"==== DEPLOYMENT ERROR ====\n") + f.write(f"Error: {str(e)}\n") + import traceback + f.write(f"Traceback: {traceback.format_exc()}\n") + f.write(f"==== END DEPLOYMENT ERROR ====\n\n") + print(f"[-] Error: Failed to deploy AWS infrastructure: {e}") - # Clean up deployed instances if any - if deployed_instances: - print(f"[!] Cleaning up deployed instances due to failure...") - for instance in deployed_instances: - cleanup_cmd = f"aws ec2 terminate-instances --instance-ids {instance}" - try: - subprocess.run(cleanup_cmd, shell=True, check=False) - print(f"[+] Terminated instance: {instance}") - except Exception as cleanup_err: - print(f"[!] Failed to terminate instance {instance}: {cleanup_err}") + return False, [log_file], deployed_instances + deployed_resources - # Delete the key pair - delete_key_cmd = f"aws ec2 delete-key-pair --key-name {ssh_key_name}" - try: - subprocess.run(delete_key_cmd, shell=True, check=False) - print(f"[+] Deleted key pair: {ssh_key_name}") - except Exception as key_err: - print(f"[!] Failed to delete key pair {ssh_key_name}: {key_err}") - - return False finally: # Clean up temporary inventory file - if os.path.exists('inventory_aws.yml'): - os.remove('inventory_aws.yml') + if os.path.exists(inventory_file): + with open(log_file, 'a') as f: + f.write(f"==== REMOVING INVENTORY FILE ====\n") + f.write(f"Removing file: {inventory_file}\n") + + os.remove(inventory_file) + + with open(log_file, 'a') as f: + f.write(f"Inventory file removed successfully\n") + f.write(f"==== END REMOVAL ====\n\n") + + # If failure, clean up deployed instances + if not success and (deployed_instances or deployed_resources): + with open(log_file, 'a') as f: + f.write(f"==== CLEANING UP DEPLOYED RESOURCES ====\n") + + print(f"[!] Cleaning up deployed resources due to failure...") + + # Clean up EC2 instances + for instance_name, instance_id in deployed_instances: + with open(log_file, 'a') as f: + f.write(f"Cleaning up instance: {instance_name} (ID: {instance_id})\n") + + try: + cleanup_cmd = f"aws ec2 terminate-instances --instance-ids {instance_id} --region {config.get('aws_region')}" + print(f"[+] Running cleanup command: {cleanup_cmd}") + subprocess.run(cleanup_cmd, shell=True, check=False) + print(f"[+] Terminated instance: {instance_name}") + + with open(log_file, 'a') as f: + f.write(f"Instance {instance_name} terminated successfully\n") + except Exception as cleanup_err: + print(f"[!] Failed to terminate instance {instance_name}: {cleanup_err}") + + with open(log_file, 'a') as f: + f.write(f"Failed to terminate instance {instance_name}: {cleanup_err}\n") + + # Clean up other AWS resources (key pairs, etc.) + for resource_type, resource_name in deployed_resources: + with open(log_file, 'a') as f: + f.write(f"Cleaning up {resource_type}: {resource_name}\n") + + try: + if resource_type == "key_pair": + cleanup_cmd = f"aws ec2 delete-key-pair --key-name {resource_name} --region {config.get('aws_region')}" + print(f"[+] Running cleanup command: {cleanup_cmd}") + subprocess.run(cleanup_cmd, shell=True, check=False) + print(f"[+] Deleted key pair: {resource_name}") + + # Also remove local key file + local_key_path = f"~/.ssh/{resource_name}.pem" + expanded_path = os.path.expanduser(local_key_path) + if os.path.exists(expanded_path): + os.remove(expanded_path) + print(f"[+] Removed local key file: {local_key_path}") + + with open(log_file, 'a') as f: + f.write(f"{resource_type} {resource_name} deleted successfully\n") + # Add more resource types here if needed + except Exception as cleanup_err: + print(f"[!] Failed to clean up {resource_type} {resource_name}: {cleanup_err}") + + with open(log_file, 'a') as f: + f.write(f"Failed to clean up {resource_type} {resource_name}: {cleanup_err}\n") + + with open(log_file, 'a') as f: + f.write(f"==== END CLEANUP ====\n\n") def deploy_linode(config): - """Deploy infrastructure using Linode provider""" + """Deploy infrastructure using Linode provider with improved logging""" print("[+] Deploying Linode infrastructure...") # Set Linode environment variables @@ -487,52 +837,76 @@ def deploy_linode(config): ssh_key, ssh_key_content = prepare_ssh_key(config) if not ssh_key or not ssh_key_content: print("[-] Failed to prepare SSH key, aborting deployment") - return False + return False, [], [] - # 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: {ssh_key}\n") - f.write(f" ansible_user: {config.get('ssh_user', 'root')}\n") - f.write(f" ansible_python_interpreter: /usr/bin/python3\n") - f.write(" children:\n") - f.write(" redirectors:\n") - f.write(" hosts:\n") - f.write(" redirector:\n") - f.write(" ansible_host: '{{ redirector_ip }}'\n") - f.write(" c2servers:\n") - f.write(" hosts:\n") - f.write(" c2:\n") - f.write(" ansible_host: '{{ c2_ip }}'\n") + # Create logs directory + log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs") + os.makedirs(log_dir, exist_ok=True) + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') - # Generate default names if not provided - rand_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8)) - timestamp = int(time.time()) % 10000 # Last 4 digits of timestamp - redirector_name = config.get('redirector_name', f"srv-{rand_suffix}-{timestamp}") - c2_name = config.get('c2_name', f"node-{rand_suffix}-{timestamp}") + # Only create one log file in the logs directory + log_file = os.path.join(log_dir, f"deployment_{timestamp}_linode.log") - # Update config with generated names for logging - config['redirector_name'] = redirector_name - config['c2_name'] = c2_name + print(f"[+] Creating deployment log at: {log_file}") - # Log important information - print(f"[+] Deployment information:") - print(f" - SSH Key: {ssh_key}") - print(f" - Redirector Name: {redirector_name}") - print(f" - C2 Name: {c2_name}") - print(f" - Domain: {config.get('domain', 'example.com')}") + # Create a temporary inventory file for Ansible with detailed logging + inventory_file = f"inventory_linode_{int(time.time())}.yml" + with open(log_file, 'a') as f: + f.write(f"==== CREATING INVENTORY FILE: {inventory_file} ====\n") - # Run Linode provisioning playbook - success = False deployed_instances = [] + success = False try: + # Write inventory file + with open(inventory_file, 'w') as f: + f.write("---\n") + f.write("all:\n") + f.write(" vars:\n") + f.write(f" ansible_ssh_private_key_file: {ssh_key}\n") + f.write(f" ansible_user: {config.get('ssh_user', 'root')}\n") + f.write(f" ansible_python_interpreter: /usr/bin/python3\n") + f.write(" children:\n") + f.write(" redirectors:\n") + f.write(" hosts:\n") + f.write(" redirector:\n") + f.write(" ansible_host: '{{ redirector_ip }}'\n") + f.write(" c2servers:\n") + f.write(" hosts:\n") + f.write(" c2:\n") + f.write(" ansible_host: '{{ c2_ip }}'\n") + + # Log inventory file content + with open(log_file, 'a') as f: + f.write(f"INVENTORY CONTENT:\n") + with open(inventory_file, 'r') as inv: + f.write(inv.read()) + f.write("\n==== END INVENTORY FILE ====\n\n") + + # Generate default names if not provided + rand_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8)) + timestamp = int(time.time()) % 10000 # Last 4 digits of timestamp + redirector_name = config.get('redirector_name', f"srv-{rand_suffix}-{timestamp}") + c2_name = config.get('c2_name', f"node-{rand_suffix}-{timestamp}") + + # Update config with generated names for logging + config['redirector_name'] = redirector_name + config['c2_name'] = c2_name + + # Log important information + with open(log_file, 'a') as f: + f.write(f"==== DEPLOYMENT INFORMATION ====\n") + f.write(f"SSH Key: {ssh_key}\n") + f.write(f"Redirector Name: {redirector_name}\n") + f.write(f"C2 Name: {c2_name}\n") + f.write(f"Domain: {config.get('domain', 'example.com')}\n") + f.write(f"==== END DEPLOYMENT INFORMATION ====\n\n") + + # Build variables for command with ALL required parameters extra_vars = { 'linode_token': config['linode_token'], - 'region': config.get('linode_region', DEFAULT_REGION.get('Linode', 'us-east')), - 'plan': config.get('plan', DEFAULT_SIZE.get('Linode', 'g6-standard-1')), + 'region': config.get('linode_region', 'us-east'), + 'plan': config.get('size', 'g6-standard-1'), 'redirector_name': redirector_name, 'c2_name': c2_name, 'teardown': config.get('teardown', False), @@ -548,10 +922,12 @@ def deploy_linode(config): 'smtp_auth_user': config.get('smtp_auth_user', f"user{random.randint(1000, 9999)}"), 'smtp_auth_pass': config.get('smtp_auth_pass', ''.join(random.choices(string.ascii_letters + string.digits + "!@#$%^&*", k=20))), 'ssh_key_path': ssh_key, # Path to the private key - 'ssh_key': ssh_key_content, # Content of public key + 'ssh_user': config.get('ssh_user', 'root'), + 'instance_label': c2_name if config.get('c2_only', False) else redirector_name, + 'linode_image': 'linode/debian11', } - # Format extra vars for command line, handling special characters + # Format extra vars for command line extra_vars_list = [] for k, v in extra_vars.items(): if isinstance(v, bool): @@ -565,73 +941,249 @@ def deploy_linode(config): extra_vars_str = " ".join(extra_vars_list) - # Determine playbook to run based on deployment mode + # Log extra vars for debugging (masking sensitive info) + with open(log_file, 'a') as f: + f.write("==== EXTRA VARS ====\n") + for k, v in extra_vars.items(): + if k in ['linode_token', 'smtp_auth_pass']: + v_masked = str(v)[:4] + '****' if v else 'None' + f.write(f"{k}: {v_masked}\n") + else: + f.write(f"{k}: {v}\n") + f.write("==== END EXTRA VARS ====\n\n") + + # Execute deployment based on deployment mode if extra_vars['redirector_only']: playbook = "Linode/redirector.yml" - cmd = f"ansible-playbook -i inventory_linode.yml {playbook} -e '{extra_vars_str}'" + cmd = f"ansible-playbook -i {inventory_file} {playbook} -e '{extra_vars_str}' -vvv" - if extra_vars['debug']: - print(f"[+] Running: {cmd}") + with open(log_file, 'a') as f: + f.write(f"==== RUNNING REDIRECTOR PLAYBOOK ====\n") + f.write(f"Command: {cmd}\n\n") + + print(f"[+] Running: {cmd}") + success, stdout, stderr = run_command_with_logging(cmd, config, log_file) + if not success: + print(f"[-] Redirector deployment failed. See {log_file} for details.") + # Get the deployed instance ID for cleanup + try: + linode_id = get_linode_id(redirector_name, config) + if linode_id: + deployed_instances.append((redirector_name, linode_id)) + except Exception as e: + print(f"[!] Failed to get instance ID: {e}") + return False, [log_file], deployed_instances + + # Store redirector IP and key path for SSH access later + linode_id = get_linode_id(redirector_name, config) + if linode_id: + deployed_instances.append((redirector_name, linode_id)) + config['redirector_ip'] = get_linode_ip(linode_id, config) + config['redirector_key_path'] = ssh_key - subprocess.run(cmd, shell=True, check=True) - deployed_instances.append(redirector_name) elif extra_vars['c2_only']: playbook = "Linode/c2.yml" - cmd = f"ansible-playbook -i inventory_linode.yml {playbook} -e '{extra_vars_str}'" + cmd = f"ansible-playbook -i {inventory_file} {playbook} -e '{extra_vars_str}' -vvv" - if extra_vars['debug']: - print(f"[+] Running: {cmd}") + with open(log_file, 'a') as f: + f.write(f"==== RUNNING C2 PLAYBOOK ====\n") + f.write(f"Command: {cmd}\n\n") + + print(f"[+] Running: {cmd}") + success, stdout, stderr = run_command_with_logging(cmd, config, log_file) + if not success: + print(f"[-] C2 server deployment failed. See {log_file} for details.") + # Get the deployed instance ID for cleanup + try: + linode_id = get_linode_id(c2_name, config) + if linode_id: + deployed_instances.append((c2_name, linode_id)) + except Exception as e: + print(f"[!] Failed to get instance ID: {e}") + return False, [log_file], deployed_instances + + # Store C2 IP and key path for SSH access later + linode_id = get_linode_id(c2_name, config) + if linode_id: + deployed_instances.append((c2_name, linode_id)) + config['c2_ip'] = get_linode_ip(linode_id, config) + config['c2_key_path'] = ssh_key - subprocess.run(cmd, shell=True, check=True) - deployed_instances.append(c2_name) 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 extra_vars['debug']: - print(f"[+] Running: {redirector_cmd}") - subprocess.run(redirector_cmd, shell=True, check=True) - deployed_instances.append(redirector_name) + redirector_cmd = f"ansible-playbook -i {inventory_file} Linode/redirector.yml -e '{extra_vars_str}' -vvv" + + with open(log_file, 'a') as f: + f.write(f"==== RUNNING REDIRECTOR PLAYBOOK (FULL DEPLOYMENT) ====\n") + f.write(f"Command: {redirector_cmd}\n\n") + + print(f"[+] Running: {redirector_cmd}") + success, stdout, stderr = run_command_with_logging(redirector_cmd, config, log_file) + if not success: + print(f"[-] Redirector deployment failed. See {log_file} for details.") + # Get the deployed instance ID for cleanup + try: + linode_id = get_linode_id(redirector_name, config) + if linode_id: + deployed_instances.append((redirector_name, linode_id)) + except Exception as e: + print(f"[!] Failed to get instance ID: {e}") + return False, [log_file], deployed_instances + + # Get and store redirector IP for C2 configuration + linode_id = get_linode_id(redirector_name, config) + if linode_id: + deployed_instances.append((redirector_name, linode_id)) + redirector_ip = get_linode_ip(linode_id, config) + config['redirector_ip'] = redirector_ip + config['redirector_key_path'] = ssh_key + + # Update extra vars with redirector IP + extra_vars['redirector_ip'] = redirector_ip + extra_vars_list.append(f"redirector_ip='{redirector_ip}'") + extra_vars_str = " ".join(extra_vars_list) + + # Log updated extra_vars + with open(log_file, 'a') as f: + f.write(f"==== UPDATED EXTRA VARS WITH REDIRECTOR IP ====\n") + f.write(f"redirector_ip: {redirector_ip}\n") + f.write(f"==== END UPDATED EXTRA VARS ====\n\n") # Then deploy C2 server with redirector IP - c2_cmd = f"ansible-playbook -i inventory_linode.yml Linode/c2.yml -e '{extra_vars_str}'" - if extra_vars['debug']: - print(f"[+] Running: {c2_cmd}") - subprocess.run(c2_cmd, shell=True, check=True) - deployed_instances.append(c2_name) + c2_cmd = f"ansible-playbook -i {inventory_file} Linode/c2.yml -e '{extra_vars_str}' -vvv" + + with open(log_file, 'a') as f: + f.write(f"==== RUNNING C2 PLAYBOOK (FULL DEPLOYMENT) ====\n") + f.write(f"Command: {c2_cmd}\n\n") + + print(f"[+] Running: {c2_cmd}") + success, stdout, stderr = run_command_with_logging(c2_cmd, config, log_file) + if not success: + print(f"[-] C2 server deployment failed. See {log_file} for details.") + # Get the deployed instance ID for cleanup + try: + linode_id = get_linode_id(c2_name, config) + if linode_id: + deployed_instances.append((c2_name, linode_id)) + except Exception as e: + print(f"[!] Failed to get instance ID: {e}") + return False, [log_file], deployed_instances + + # Store C2 IP and key path for SSH access later + linode_id = get_linode_id(c2_name, config) + if linode_id: + deployed_instances.append((c2_name, linode_id)) + config['c2_ip'] = get_linode_ip(linode_id, config) + config['c2_key_path'] = ssh_key + + # Deploy tracker if requested + if config.get('deploy_tracker'): + with open(log_file, 'a') as f: + f.write(f"==== DEPLOYING TRACKER MODULE ====\n") + + tracker_success = deploy_tracker_module(config) + + with open(log_file, 'a') as f: + f.write(f"Tracker deployment {'succeeded' if tracker_success else 'failed'}\n") + f.write(f"==== END TRACKER DEPLOYMENT ====\n\n") + + if not tracker_success: + print("[!] Warning: Tracker deployment failed, but continuing with main deployment") print("[+] Linode infrastructure deployed successfully!") success = True - return True - except subprocess.CalledProcessError as e: + + # Log success + with open(log_file, 'a') as f: + f.write(f"==== DEPLOYMENT SUCCESSFUL ====\n") + f.write(f"Deployed instances: {', '.join([n for n, _ in deployed_instances])}\n") + if 'redirector_ip' in config: + f.write(f"Redirector IP: {config['redirector_ip']}\n") + if 'c2_ip' in config: + f.write(f"C2 IP: {config['c2_ip']}\n") + f.write(f"==== END DEPLOYMENT SUCCESS ====\n\n") + + return success, [log_file], deployed_instances + + except Exception as e: + # Log exception details + with open(log_file, 'a') as f: + f.write(f"==== DEPLOYMENT ERROR ====\n") + f.write(f"Error: {str(e)}\n") + import traceback + f.write(f"Traceback: {traceback.format_exc()}\n") + f.write(f"==== END DEPLOYMENT ERROR ====\n\n") + print(f"[-] Error: Failed to deploy Linode infrastructure: {e}") - # Clean up deployed instances if any - if deployed_instances: - print(f"[!] Cleaning up deployed instances due to failure...") - for instance in deployed_instances: - cleanup_cmd = f"linode-cli linodes delete {instance} --yes" - try: - subprocess.run(cleanup_cmd, shell=True, check=False) - print(f"[+] Deleted instance: {instance}") - except Exception as cleanup_err: - print(f"[!] Failed to delete instance {instance}: {cleanup_err}") + # Additional error information + with open(log_file, 'a') as f: + f.write(f"Failed deployment, deployed instances that need cleanup: {deployed_instances}\n") + + return False, [log_file], deployed_instances - return False finally: # Clean up temporary inventory file - if os.path.exists('inventory_linode.yml'): - os.remove('inventory_linode.yml') + if os.path.exists(inventory_file): + with open(log_file, 'a') as f: + f.write(f"==== REMOVING INVENTORY FILE ====\n") + f.write(f"Removing file: {inventory_file}\n") + + os.remove(inventory_file) + + with open(log_file, 'a') as f: + f.write(f"Inventory file removed successfully\n") + f.write(f"==== END REMOVAL ====\n\n") + + # If failure, clean up deployed instances + if not success and deployed_instances: + with open(log_file, 'a') as f: + f.write(f"==== CLEANING UP DEPLOYED INSTANCES ====\n") + + print(f"[!] Cleaning up deployed instances due to failure...") + + for instance_name, instance_id in deployed_instances: + with open(log_file, 'a') as f: + f.write(f"Cleaning up instance: {instance_name} (ID: {instance_id})\n") + + try: + cleanup_cmd = f"linode-cli linodes delete {instance_id} --yes" + print(f"[+] Running cleanup command: {cleanup_cmd}") + subprocess.run(cleanup_cmd, shell=True, check=False) + print(f"[+] Deleted instance: {instance_name}") + + with open(log_file, 'a') as f: + f.write(f"Instance {instance_name} deleted successfully\n") + except Exception as cleanup_err: + print(f"[!] Failed to delete instance {instance_name}: {cleanup_err}") + + with open(log_file, 'a') as f: + f.write(f"Failed to delete instance {instance_name}: {cleanup_err}\n") + + with open(log_file, 'a') as f: + f.write(f"==== END CLEANUP ====\n\n") # If this is a new key and deployment failed, remove it - if not success and ssh_key and ssh_key.startswith(os.path.expanduser("~/.ssh/c2ingred_")): + if not success and ssh_key and ssh_key.startswith(os.path.expanduser("~/.ssh/c2itall_")): try: + with open(log_file, 'a') as f: + f.write(f"==== REMOVING GENERATED SSH KEY ====\n") + f.write(f"Removing SSH key: {ssh_key}\n") + os.remove(ssh_key) if os.path.exists(f"{ssh_key}.pub"): os.remove(f"{ssh_key}.pub") print(f"[+] Removed generated SSH key due to deployment failure") + + with open(log_file, 'a') as f: + f.write(f"SSH key removed successfully\n") + f.write(f"==== END SSH KEY REMOVAL ====\n\n") except Exception as key_err: print(f"[!] Failed to remove SSH key {ssh_key}: {key_err}") + + with open(log_file, 'a') as f: + f.write(f"Failed to remove SSH key: {key_err}\n") + f.write(f"==== END SSH KEY REMOVAL ====\n\n") def deploy_flokinet(config): """Deploy infrastructure using FlokiNET provider""" @@ -641,7 +1193,17 @@ def deploy_flokinet(config): ssh_key, ssh_key_content = prepare_ssh_key(config) if not ssh_key or not ssh_key_content: print("[-] Failed to prepare SSH key, aborting deployment") - return False + return False, [], [] + + # Create logs directory + log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs") + os.makedirs(log_dir, exist_ok=True) + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + + # Only create one log file in the logs directory + log_file = os.path.join(log_dir, f"deployment_{timestamp}_flokinet.log") + + print(f"[+] Creating deployment log at: {log_file}") # OPSEC: Use the random SSH port from config ssh_port = config.get('ssh_port', random.randint(20000, 65000)) @@ -656,7 +1218,7 @@ def deploy_flokinet(config): redirector_ip = input("[?] Enter FlokiNET redirector IP address: ") if not redirector_ip: print("[-] Error: Redirector IP is required for deployment") - return False + return False, [], [] # Use a placeholder for C2 if we're only deploying redirector c2_ip = "127.0.0.1" @@ -664,7 +1226,7 @@ def deploy_flokinet(config): c2_ip = input("[?] Enter FlokiNET C2 server IP address: ") if not c2_ip: print("[-] Error: C2 server IP is required for deployment") - return False + return False, [], [] # If we're only deploying C2, but need redirector IP for configuration if not redirector_ip: @@ -684,7 +1246,7 @@ def deploy_flokinet(config): 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 + return False, [], [] # Full deployment - need both IPs elif not config['redirector_only'] and not config['c2_only']: @@ -695,7 +1257,7 @@ def deploy_flokinet(config): if not redirector_ip or not c2_ip: print("[-] Error: Both redirector and C2 IPs are required for full deployment") - return False + return False, [], [] # Update config with obtained IPs config['flokinet_redirector_ip'] = redirector_ip @@ -710,21 +1272,30 @@ def deploy_flokinet(config): prepare_flokinet_vars(config) # Log important information - print(f"[+] Deployment information:") - print(f" - SSH Key: {ssh_key}") - print(f" - SSH Port: {ssh_port}") - print(f" - Redirector IP: {redirector_ip}") - print(f" - C2 IP: {c2_ip}") - print(f" - Domain: {config.get('domain', 'example.com')}") + with open(log_file, 'a') as f: + f.write(f"==== DEPLOYMENT INFORMATION ====\n") + f.write(f"SSH Key: {ssh_key}\n") + f.write(f"SSH Port: {ssh_port}\n") + f.write(f"Redirector IP: {redirector_ip}\n") + f.write(f"C2 IP: {c2_ip}\n") + f.write(f"Domain: {config.get('domain', 'example.com')}\n") + f.write(f"==== END DEPLOYMENT INFORMATION ====\n\n") # Create temporary inventory files for Ansible with randomized names for OPSEC inventory_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6)) redirector_inventory = None c2_inventory = None + deployed_resources = [] success = False try: + # Track resources for potential cleanup + deployed_resources = [ + ("redirector_ip", redirector_ip), + ("c2_ip", c2_ip) + ] + if not config['c2_only']: redirector_inventory = f'inventory_r_{inventory_suffix}.yml' with open(redirector_inventory, 'w') as f: @@ -738,6 +1309,13 @@ def deploy_flokinet(config): f.write(f" ansible_ssh_private_key_file: {ssh_key}\n") f.write(f" ansible_python_interpreter: /usr/bin/python3\n") f.write(f" ansible_port: {ssh_port}\n") # Use custom SSH port + + # Log inventory file content + with open(log_file, 'a') as f: + f.write(f"==== REDIRECTOR INVENTORY: {redirector_inventory} ====\n") + with open(redirector_inventory, 'r') as inv: + f.write(inv.read()) + f.write("\n==== END REDIRECTOR INVENTORY ====\n\n") if not config['redirector_only']: c2_inventory = f'inventory_c_{inventory_suffix}.yml' @@ -752,6 +1330,13 @@ def deploy_flokinet(config): f.write(f" ansible_ssh_private_key_file: {ssh_key}\n") f.write(f" ansible_python_interpreter: /usr/bin/python3\n") f.write(f" ansible_port: {ssh_port}\n") # Use custom SSH port + + # Log inventory file content + with open(log_file, 'a') as f: + f.write(f"==== C2 INVENTORY: {c2_inventory} ====\n") + with open(c2_inventory, 'r') as inv: + f.write(inv.read()) + f.write("\n==== END C2 INVENTORY ====\n\n") # Build extra vars string extra_vars = { @@ -767,7 +1352,7 @@ def deploy_flokinet(config): 'smtp_auth_pass': config.get('smtp_auth_pass', ''.join(random.choices(string.ascii_letters + string.digits + "!@#$%^&*", k=20))), 'ssh_port': ssh_port, 'ssh_key_path': ssh_key, - 'ssh_key_content': ssh_key_content, + 'ssh_user': config.get('ssh_user', 'root'), } # Format extra vars for command line, handling special characters @@ -784,41 +1369,131 @@ def deploy_flokinet(config): extra_vars_str = " ".join(extra_vars_list) + # Log extra vars for debugging (masking sensitive info) + with open(log_file, 'a') as f: + f.write("==== EXTRA VARS ====\n") + for k, v in extra_vars.items(): + if k in ['smtp_auth_pass']: + v_masked = str(v)[:4] + '****' if v else 'None' + f.write(f"{k}: {v_masked}\n") + else: + f.write(f"{k}: {v}\n") + f.write("==== END EXTRA VARS ====\n\n") + # Common provisioning for both servers if not config['c2_only'] and redirector_inventory: print("[+] Provisioning FlokiNET redirector...") cmd = f"ansible-playbook -i {redirector_inventory} FlokiNET/provision.yml -e '{extra_vars_str}'" + + with open(log_file, 'a') as f: + f.write(f"==== RUNNING REDIRECTOR PROVISION PLAYBOOK ====\n") + f.write(f"Command: {cmd}\n\n") + if config['debug']: print(f"[+] Running: {cmd}") - subprocess.run(cmd, shell=True, check=True) + + success, stdout, stderr = run_command_with_logging(cmd, config, log_file) + if not success: + print(f"[-] Redirector provisioning failed. See {log_file} for details.") + return False, [log_file], deployed_resources # Run redirector-specific playbook print("[+] Configuring FlokiNET redirector...") cmd = f"ansible-playbook -i {redirector_inventory} FlokiNET/redirector.yml -e '{extra_vars_str}'" + + with open(log_file, 'a') as f: + f.write(f"==== RUNNING REDIRECTOR CONFIGURATION PLAYBOOK ====\n") + f.write(f"Command: {cmd}\n\n") + if config['debug']: print(f"[+] Running: {cmd}") - subprocess.run(cmd, shell=True, check=True) + + success, stdout, stderr = run_command_with_logging(cmd, config, log_file) + if not success: + print(f"[-] Redirector configuration failed. See {log_file} for details.") + return False, [log_file], deployed_resources + + # Store SSH key path for later + config['redirector_ip'] = redirector_ip + config['redirector_key_path'] = ssh_key if not config['redirector_only'] and c2_inventory: print("[+] Provisioning FlokiNET C2 server...") cmd = f"ansible-playbook -i {c2_inventory} FlokiNET/provision.yml -e '{extra_vars_str}'" + + with open(log_file, 'a') as f: + f.write(f"==== RUNNING C2 PROVISION PLAYBOOK ====\n") + f.write(f"Command: {cmd}\n\n") + if config['debug']: print(f"[+] Running: {cmd}") - subprocess.run(cmd, shell=True, check=True) + + success, stdout, stderr = run_command_with_logging(cmd, config, log_file) + if not success: + print(f"[-] C2 server provisioning failed. See {log_file} for details.") + return False, [log_file], deployed_resources # Run C2-specific playbook print("[+] Configuring FlokiNET C2 server...") cmd = f"ansible-playbook -i {c2_inventory} FlokiNET/c2.yml -e '{extra_vars_str}'" + + with open(log_file, 'a') as f: + f.write(f"==== RUNNING C2 CONFIGURATION PLAYBOOK ====\n") + f.write(f"Command: {cmd}\n\n") + if config['debug']: print(f"[+] Running: {cmd}") - subprocess.run(cmd, shell=True, check=True) + + success, stdout, stderr = run_command_with_logging(cmd, config, log_file) + if not success: + print(f"[-] C2 server configuration failed. See {log_file} for details.") + return False, [log_file], deployed_resources + + # Store SSH key path for later + config['c2_ip'] = c2_ip + config['c2_key_path'] = ssh_key + + # Deploy tracker if requested + if config.get('deploy_tracker'): + with open(log_file, 'a') as f: + f.write(f"==== DEPLOYING TRACKER MODULE ====\n") + + tracker_success = deploy_tracker_module(config) + + with open(log_file, 'a') as f: + f.write(f"Tracker deployment {'succeeded' if tracker_success else 'failed'}\n") + f.write(f"==== END TRACKER DEPLOYMENT ====\n\n") + + if not tracker_success: + print("[!] Warning: Tracker deployment failed, but continuing with main deployment") print("[+] FlokiNET infrastructure deployed successfully!") success = True - return True - except subprocess.CalledProcessError as e: + + # Log success + with open(log_file, 'a') as f: + f.write(f"==== DEPLOYMENT SUCCESSFUL ====\n") + if not config['c2_only']: + f.write(f"Redirector IP: {redirector_ip}\n") + if not config['redirector_only']: + f.write(f"C2 IP: {c2_ip}\n") + f.write(f"==== END DEPLOYMENT SUCCESS ====\n\n") + + return success, [log_file], deployed_resources + + except Exception as e: + # Log exception details + with open(log_file, 'a') as f: + f.write(f"==== DEPLOYMENT ERROR ====\n") + f.write(f"Error: {str(e)}\n") + import traceback + f.write(f"Traceback: {traceback.format_exc()}\n") + f.write(f"==== END DEPLOYMENT ERROR ====\n\n") + print(f"[-] Error: Failed to deploy FlokiNET infrastructure: {e}") - return False + + return False, [log_file], deployed_resources + finally: # Clean up temporary inventory files for inv_file in [redirector_inventory, c2_inventory]: @@ -827,18 +1502,34 @@ def deploy_flokinet(config): with open(inv_file, 'w') as f: f.write('\0' * 1024) # Overwrite with null bytes os.remove(inv_file) + + with open(log_file, 'a') as f: + f.write(f"Inventory file {inv_file} removed successfully\n") + if config.get('debug'): print(f"[+] Removed temporary inventory file: {inv_file}") # If this is a new key and deployment failed, remove it - if not success and ssh_key and ssh_key.startswith(os.path.expanduser("~/.ssh/c2ingred_")): + if not success and ssh_key and ssh_key.startswith(os.path.expanduser("~/.ssh/c2itall_")): try: + with open(log_file, 'a') as f: + f.write(f"==== REMOVING GENERATED SSH KEY ====\n") + f.write(f"Removing SSH key: {ssh_key}\n") + os.remove(ssh_key) if os.path.exists(f"{ssh_key}.pub"): os.remove(f"{ssh_key}.pub") print(f"[+] Removed generated SSH key due to deployment failure") + + with open(log_file, 'a') as f: + f.write(f"SSH key removed successfully\n") + f.write(f"==== END SSH KEY REMOVAL ====\n\n") except Exception as key_err: print(f"[!] Failed to remove SSH key {ssh_key}: {key_err}") + + with open(log_file, 'a') as f: + f.write(f"Failed to remove SSH key: {key_err}\n") + f.write(f"==== END SSH KEY REMOVAL ====\n\n") def prepare_flokinet_vars(config): """Prepare FlokiNET vars.yaml with dynamic values""" @@ -962,7 +1653,7 @@ def generate_ssh_key(): rand_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8)) timestamp = int(time.time()) % 10000 # Last 4 digits of timestamp - key_name = f"c2ingred_{rand_suffix}_{timestamp}" + key_name = f"c2itall_{rand_suffix}_{timestamp}" key_path = os.path.expanduser(f"~/.ssh/{key_name}") print(f"[+] Generating new SSH key: {key_path}") @@ -987,8 +1678,8 @@ def prepare_ssh_key(config): ssh_key = config.get('ssh_key') # If key not provided or doesn't exist, generate a new one - if not ssh_key or not os.path.exists(ssh_key): - if ssh_key and not os.path.exists(ssh_key): + if not ssh_key or not os.path.exists(os.path.expanduser(ssh_key)): + if ssh_key and not os.path.exists(os.path.expanduser(ssh_key)): print(f"[-] Warning: Specified SSH key {ssh_key} not found") ssh_key = generate_ssh_key() @@ -997,6 +1688,9 @@ def prepare_ssh_key(config): config['ssh_key'] = ssh_key + # Make sure we're using the expanded path + ssh_key = os.path.expanduser(ssh_key) + # Ensure the public key exists ssh_public_key = f"{ssh_key}.pub" if not os.path.exists(ssh_public_key): @@ -1012,16 +1706,508 @@ def prepare_ssh_key(config): try: with open(ssh_public_key, 'r') as f: ssh_key_content = f.read().strip() + + # Verify and fix permissions + current_mode = os.stat(ssh_key).st_mode & 0o777 + if current_mode != 0o600: + print(f"[!] SSH key has incorrect permissions: {oct(current_mode)[2:]}. Fixing to 0600...") + os.chmod(ssh_key, 0o600) + print(f"[+] SSH key permissions fixed") + return ssh_key, ssh_key_content except Exception as e: print(f"[-] Error reading SSH public key: {e}") return None, None +def get_instance_ip(instance_name, config): + """Get the IP address of an EC2 instance by name""" + try: + cmd = [ + "aws", "ec2", "describe-instances", + "--filters", f"Name=tag:Name,Values={instance_name}", + "--query", "Reservations[0].Instances[0].PublicIpAddress", + "--output", "text", + "--region", config.get('aws_region', 'us-east-1') + ] + + result = subprocess.check_output(cmd).decode().strip() + + if result == "None" or not result: + # Try again with a different command format that works better in some AWS CLI versions + cmd = [ + "aws", "ec2", "describe-instances", + "--filters", f"Name=tag:Name,Values={instance_name}", + "Name=instance-state-name,Values=running", + "--query", "Reservations[*].Instances[*].PublicIpAddress", + "--output", "text", + "--region", config.get('aws_region', 'us-east-1') + ] + result = subprocess.check_output(cmd).decode().strip() + + if result and result != "None": + print(f"[+] Found IP address for {instance_name}: {result}") + return result + else: + print(f"[-] Could not find IP address for instance {instance_name}") + return None + except subprocess.CalledProcessError as e: + print(f"[-] Error getting instance IP: {e}") + return None + +def get_linode_id(instance_name, config): + """Get the ID of a Linode instance by name""" + try: + cmd = [ + "linode-cli", "--json", "linodes", "list", + "--label", instance_name + ] + + result = subprocess.check_output(cmd, stderr=subprocess.PIPE) + + # Parse the JSON response + instances = json.loads(result.decode()) + + if instances and len(instances) > 0: + linode_id = instances[0]['id'] + print(f"[+] Found Linode ID for {instance_name}: {linode_id}") + return linode_id + else: + print(f"[-] Could not find Linode ID for instance {instance_name}") + return None + except subprocess.CalledProcessError as e: + print(f"[-] Error getting Linode ID: {e}") + print(f" Command output: {e.stderr.decode() if e.stderr else 'No error output'}") + return None + except Exception as e: + print(f"[-] Unexpected error getting Linode ID: {e}") + return None + +def get_linode_ip(linode_id, config): + """Get the IP address of a Linode instance by ID""" + try: + cmd = [ + "linode-cli", "linodes", "view", + str(linode_id), + "--json" + ] + + result = subprocess.check_output(cmd).decode().strip() + + # Parse the JSON response + instance = json.loads(result) + + if instance and 'ipv4' in instance and instance['ipv4']: + ip = instance['ipv4'][0] + print(f"[+] Found IP address for Linode {linode_id}: {ip}") + return ip + else: + print(f"[-] Could not find IP address for Linode {linode_id}") + return None + except subprocess.CalledProcessError as e: + print(f"[-] Error getting Linode IP: {e}") + return None + +def ssh_to_c2(config): + """SSH into the C2 server after successful deployment""" + print("[+] Attempting to SSH into C2 server...") + + # Get C2 server details + c2_ip = config.get('c2_ip') + key_path = config.get('c2_key_path') + + # Check if we have the necessary information + if not c2_ip: + print(f"[-] Error: No C2 IP address found") + return False + + if not key_path: + print(f"[-] Error: No SSH key path found for C2 server") + return False + + # Determine user and port + user = config.get('ssh_user', 'root') + port = config.get('ssh_port', 22) + + # Wait a few seconds to ensure everything is fully set up + print(f"[+] Waiting 10 seconds for SSH to be fully ready on {c2_ip}...") + time.sleep(10) + + # Build the SSH command + ssh_cmd = [ + "ssh", + "-i", key_path, + "-o", "StrictHostKeyChecking=no", + "-o", "UserKnownHostsFile=/dev/null", # For OPSEC, don't store the host key + ] + + # Add port if not standard + if port != 22: + ssh_cmd.extend(["-p", str(port)]) + + # Add user and host + ssh_cmd.append(f"{user}@{c2_ip}") + + print(f"[+] Connecting to C2 server at {c2_ip}...") + print(f"[+] SSH command: {' '.join(ssh_cmd)}") + + try: + # Execute SSH command + subprocess.call(ssh_cmd) + return True + except Exception as e: + print(f"[-] Error SSHing to C2 server: {e}") + return False + + +def cleanup_interrupted_deployment(provider, deployed_instances, generated_ssh_keys): + """Clean up resources after an interrupted deployment""" + print("[+] Cleaning up deployed resources...") + + # Clean up based on provider + if provider.lower() == 'aws': + for instance in deployed_instances: + if isinstance(instance, tuple) and len(instance) >= 2: + instance_name, instance_id = instance + try: + print(f"[+] Terminating AWS instance: {instance_name} (ID: {instance_id})") + cmd = f"aws ec2 terminate-instances --instance-ids {instance_id}" + subprocess.run(cmd, shell=True, check=False) + except Exception as e: + print(f"[!] Error terminating AWS instance {instance_name}: {e}") + + elif provider.lower() == 'linode': + for instance in deployed_instances: + if isinstance(instance, tuple) and len(instance) >= 2: + instance_name, instance_id = instance + try: + print(f"[+] Deleting Linode instance: {instance_name} (ID: {instance_id})") + cmd = f"linode-cli linodes delete {instance_id} --yes" + subprocess.run(cmd, shell=True, check=False) + except Exception as e: + print(f"[!] Error deleting Linode instance {instance_name}: {e}") + + elif provider.lower() == 'flokinet': + print("[!] FlokiNET instances must be cleaned up manually through their web interface.") + print("[!] Please log in to your FlokiNET account and remove the deployed instances.") + + # Clean up generated SSH keys + for ssh_key in generated_ssh_keys: + try: + print(f"[+] Removing generated SSH key: {ssh_key}") + if os.path.exists(ssh_key): + os.remove(ssh_key) + if os.path.exists(f"{ssh_key}.pub"): + os.remove(f"{ssh_key}.pub") + except Exception as e: + print(f"[!] Error removing SSH key {ssh_key}: {e}") + + print("[+] Cleanup completed.") + +def run_command_with_logging(cmd, config, log_file=None): + """Run a command and log both stdout and stderr to the deployment log""" + try: + print(f"[+] Running: {cmd}") + + # If no log file specified, create or use the deployment log + if not log_file: + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs") + os.makedirs(log_dir, exist_ok=True) + log_file = os.path.join(log_dir, f"deployment_{timestamp}.log") + + # Execute the command and capture output + process = subprocess.Popen( + cmd, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True + ) + + # Read output in real-time + stdout_data = "" + stderr_data = "" + + # Append command to log file + with open(log_file, 'a') as f: + f.write(f"\n\n==== COMMAND EXECUTION: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} ====\n") + f.write(f"COMMAND: {cmd}\n\n") + f.write("OUTPUT:\n") + + # Read and process output + while True: + stdout_line = process.stdout.readline() + stderr_line = process.stderr.readline() + + if stdout_line == '' and stderr_line == '' and process.poll() is not None: + break + + if stdout_line: + print(stdout_line.strip()) + stdout_data += stdout_line + # Write to log file in real-time + with open(log_file, 'a') as f: + f.write(f"STDOUT: {stdout_line}") + + if stderr_line: + print(stderr_line.strip(), file=sys.stderr) + stderr_data += stderr_line + # Write to log file in real-time + with open(log_file, 'a') as f: + f.write(f"STDERR: {stderr_line}") + + # Wait for command to complete + return_code = process.wait() + + # Log completion status + with open(log_file, 'a') as f: + f.write(f"\nRETURN CODE: {return_code}\n") + f.write(f"==== END COMMAND EXECUTION ====\n\n") + + # Check if command succeeded + if return_code != 0: + raise subprocess.CalledProcessError(return_code, cmd, stdout_data, stderr_data) + + return True, stdout_data, stderr_data + + except subprocess.CalledProcessError as e: + # Log error details + with open(log_file, 'a') as f: + f.write(f"\nERROR: Command failed with return code {e.returncode}\n") + f.write("==== END COMMAND EXECUTION WITH ERROR ====\n\n") + + print(f"[-] Command failed with return code {e.returncode}") + return False, e.stdout, e.stderr + + except Exception as e: + # Log unexpected error + with open(log_file, 'a') as f: + f.write(f"\nUNEXPECTED ERROR: {str(e)}\n") + f.write("==== END COMMAND EXECUTION WITH UNEXPECTED ERROR ====\n\n") + + print(f"[-] Unexpected error running command: {e}") + return False, "", str(e) + +def run_tests(config): + """Run basic connectivity and functionality tests""" + print("[+] Running connectivity tests...") + + # Test both redirector and C2 if both were deployed + if not config.get('redirector_only') and not config.get('c2_only'): + # Test redirector first + redirector_ip = config.get('redirector_ip') + if redirector_ip: + print(f"[+] Testing redirector at {redirector_ip}...") + try: + # Try to ping the redirector + ping_cmd = ["ping", "-c", "3", redirector_ip] + subprocess.run(ping_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + print(f"[+] Redirector ping test successful") + + # Test SSH connection + test_ssh(redirector_ip, config.get('redirector_key_path'), config.get('ssh_user', 'root'), config.get('ssh_port', 22)) + + # Test HTTP(S) connectivity if deployed with a domain + if config.get('domain') != 'example.com': + test_http(f"https://cdn.{config['domain']}") + except subprocess.CalledProcessError as e: + print(f"[-] Redirector connectivity test failed: {e}") + + # Then test C2 + c2_ip = config.get('c2_ip') + if c2_ip: + print(f"[+] Testing C2 server at {c2_ip}...") + try: + # Try to ping the C2 server + ping_cmd = ["ping", "-c", "3", c2_ip] + subprocess.run(ping_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + print(f"[+] C2 server ping test successful") + + # Test SSH connection + test_ssh(c2_ip, config.get('c2_key_path'), config.get('ssh_user', 'root'), config.get('ssh_port', 22)) + + # Test HTTP(S) connectivity if deployed with a domain + if config.get('domain') != 'example.com': + test_http(f"https://mail.{config['domain']}") + except subprocess.CalledProcessError as e: + print(f"[-] C2 server connectivity test failed: {e}") + + # Test only redirector if that's all that was deployed + elif config.get('redirector_only'): + redirector_ip = config.get('redirector_ip') + if redirector_ip: + print(f"[+] Testing redirector at {redirector_ip}...") + try: + # Try to ping the redirector + ping_cmd = ["ping", "-c", "3", redirector_ip] + subprocess.run(ping_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + print(f"[+] Redirector ping test successful") + + # Test SSH connection + test_ssh(redirector_ip, config.get('redirector_key_path'), config.get('ssh_user', 'root'), config.get('ssh_port', 22)) + + # Test HTTP(S) connectivity if deployed with a domain + if config.get('domain') != 'example.com': + test_http(f"https://cdn.{config['domain']}") + except subprocess.CalledProcessError as e: + print(f"[-] Redirector connectivity test failed: {e}") + + # Test only C2 if that's all that was deployed + elif config.get('c2_only'): + c2_ip = config.get('c2_ip') + if c2_ip: + print(f"[+] Testing C2 server at {c2_ip}...") + try: + # Try to ping the C2 server + ping_cmd = ["ping", "-c", "3", c2_ip] + subprocess.run(ping_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + print(f"[+] C2 server ping test successful") + + # Test SSH connection + test_ssh(c2_ip, config.get('c2_key_path'), config.get('ssh_user', 'root'), config.get('ssh_port', 22)) + + # Test HTTP(S) connectivity if deployed with a domain + if config.get('domain') != 'example.com': + test_http(f"https://mail.{config['domain']}") + except subprocess.CalledProcessError as e: + print(f"[-] C2 server connectivity test failed: {e}") + + print("[+] Tests completed") + return True + +def test_ssh(ip, key_path, user, port): + """Test SSH connectivity to a server""" + print(f"[+] Testing SSH connectivity to {ip}...") + try: + # Use the paramiko library for a programmatic SSH connection test + client = paramiko.SSHClient() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + # Connect with timeout + client.connect(ip, port=port, username=user, key_filename=key_path, timeout=10) + + # Run a simple command to check if the connection works + stdin, stdout, stderr = client.exec_command("echo SSH test successful") + response = stdout.read().decode().strip() + + # Close the connection + client.close() + + print(f"[+] SSH test result: {response}") + return True + except Exception as e: + print(f"[-] SSH connectivity test failed: {e}") + return False + +def test_http(url): + """Test HTTP(S) connectivity to a URL""" + print(f"[+] Testing HTTP(S) connectivity to {url}...") + try: + # Use requests library with a timeout + response = subprocess.run(["curl", "-s", "-k", "-L", "--connect-timeout", "10", url], check=True, stdout=subprocess.PIPE) + + # Check if we got a response + if response.stdout: + print(f"[+] HTTP(S) test successful") + return True + else: + print(f"[-] HTTP(S) test returned empty response") + return False + except subprocess.CalledProcessError as e: + print(f"[-] HTTP(S) connectivity test failed: {e}") + return False + +def deploy_with_error_handling(provider_func, config): + """Deploy with better error handling and logs""" + logs_created = [] + deployed_resources = [] + + try: + result, logs, resources = provider_func(config) + logs_created.extend(logs) + deployed_resources.extend(resources) + return result, logs_created, deployed_resources + except subprocess.CalledProcessError as e: + # Log detailed error information + error_log = log_debug_info(e.cmd, e) + if error_log: + logs_created.append(error_log) + print(f"[-] Deployment failed: {e}. See {error_log} for detailed debug information.") + return False, logs_created, deployed_resources + except Exception as e: + error_log = log_debug_info("Unknown command", e) + if error_log: + logs_created.append(error_log) + print(f"[-] Unexpected error during deployment: {e}. See {error_log} for details.") + return False, logs_created, deployed_resources + +def deploy_tracker_module(config): + """Deploy tracker module alongside the C2 infrastructure""" + if not config.get('deploy_tracker'): + return True + + print("[+] Deploying tracker module...") + + # Make sure the module exists + tracker_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "modules/tracker") + if not os.path.exists(tracker_dir): + print(f"[-] Error: Tracker module directory not found at {tracker_dir}") + print("[-] Make sure the tracker module is installed in the modules/tracker directory") + return False + + # Prepare tracker deploy command + tracker_script = os.path.join(tracker_dir, "Tools/deploy_tracker.py") + if not os.path.exists(tracker_script): + print(f"[-] Error: Tracker deployment script not found at {tracker_script}") + return False + + # Build command arguments + tracker_cmd = [ + "python3", tracker_script, + "--quick-deploy" # Use quick deployment mode + ] + + # Add domain if specified + if config.get('tracker_domain'): + tracker_cmd.extend(["--domain", config['tracker_domain']]) + else: + tracker_cmd.append("--ip-only") + + # Add email if specified + if config.get('tracker_email'): + tracker_cmd.extend(["--email", config['tracker_email']]) + + # Add tracker name if specified + if config.get('tracker_name'): + tracker_cmd.extend(["--engagement-name", config['tracker_name']]) + + # Add IPinfo token if specified + if config.get('tracker_ipinfo_token'): + tracker_cmd.extend(["--ipinfo-token", config['tracker_ipinfo_token']]) + + # Add SSL setup if requested + if config.get('tracker_setup_ssl'): + tracker_cmd.append("--setup-ssl") + + # Add tracking pixel creation if requested + if config.get('tracker_create_pixel'): + tracker_cmd.append("--create-pixel") + + # Run the tracker deployment command + try: + print(f"[+] Running tracker deployment: {' '.join(tracker_cmd)}") + subprocess.run(tracker_cmd, check=True) + print("[+] Tracker module deployed successfully") + return True + except subprocess.CalledProcessError as e: + print(f"[-] Error deploying tracker module: {e}") + return False + def interactive_config(): """Interactively collect configuration""" config = {} - # Initialize deployment mode flags (these were missing) + # Initialize deployment mode flags config['redirector_only'] = False config['c2_only'] = False @@ -1074,27 +2260,26 @@ def interactive_config(): 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": + # Ask for architecture type + print("\nDeployment architecture:") + print(" 1. Combined (single server for both redirector and C2)") + print(" 2. Split (separate servers for redirector and C2)") + arch_choice = input("\nSelect architecture (1-2, default: 2): ") or "2" + if arch_choice == "1": + config['redirector_only'] = False + config['c2_only'] = False + else: + # For split architecture, ask which components to deploy + comp_choice = input("\nDeploy (1: Both components, 2: Redirector only, 3: C2 only, default: 1): ") or "1" + if comp_choice == "2": + config['redirector_only'] = True + config['c2_only'] = False + elif comp_choice == "3": + config['redirector_only'] = False + config['c2_only'] = True + else: config['redirector_only'] = False config['c2_only'] = False - 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 ===") @@ -1125,27 +2310,26 @@ def interactive_config(): 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": + # Ask for architecture type + print("\nDeployment architecture:") + print(" 1. Combined (single server for both redirector and C2)") + print(" 2. Split (separate servers for redirector and C2)") + arch_choice = input("\nSelect architecture (1-2, default: 2): ") or "2" + if arch_choice == "1": + config['redirector_only'] = False + config['c2_only'] = False + else: + # For split architecture, ask which components to deploy + comp_choice = input("\nDeploy (1: Both components, 2: Redirector only, 3: C2 only, default: 1): ") or "1" + if comp_choice == "2": + config['redirector_only'] = True + config['c2_only'] = False + elif comp_choice == "3": + config['redirector_only'] = False + config['c2_only'] = True + else: config['redirector_only'] = False config['c2_only'] = False - 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 ===") @@ -1226,26 +2410,80 @@ def interactive_config(): zero_logs = input("Enable zero-logs configuration? (Y/n): ").lower() or "y" config['zero_logs'] = zero_logs.startswith('y') + # Post-deployment options + print("\n=== Post-Deployment Options ===") + ssh_after_deploy = input("SSH into instance after deployment? (y/N): ").lower() + config['ssh_after_deploy'] = ssh_after_deploy.startswith('y') + + run_tests = input("Run connectivity tests after deployment? (y/N): ").lower() + config['run_tests'] = run_tests.startswith('y') + + # Tracker module deployment + print("\n=== Tracker Module ===") + deploy_tracker = input("Deploy tracker module alongside C2 infrastructure? (y/N): ").lower() + config['deploy_tracker'] = deploy_tracker.startswith('y') + + if config['deploy_tracker']: + # Additional tracker configuration + print("\n=== Tracker Configuration ===") + config['tracker_domain'] = input("Domain for tracker (leave empty to use server IP): ") + + if config['tracker_domain']: + config['tracker_email'] = input(f"Email for tracker Let's Encrypt certificate: ") or config['letsencrypt_email'] + + setup_ssl = input("Set up SSL for tracker? (Y/n): ").lower() or "y" + config['tracker_setup_ssl'] = setup_ssl.startswith('y') + + config['tracker_name'] = input("Tracker engagement name (leave empty for random): ") + config['tracker_ipinfo_token'] = input("IPinfo API token for geolocation (optional): ") + + create_pixel = input("Create email tracking pixel? (Y/n): ").lower() or "y" + config['tracker_create_pixel'] = create_pixel.startswith('y') + return config def cleanup_sensitive_files(config): """Clean up sensitive files after deployment for better OPSEC""" + print("\n[+] Starting cleanup of sensitive files...") + # Files to potentially clean up (prompt user first) sensitive_files = [] # Add config file if os.path.exists('config.yml'): sensitive_files.append('config.yml') + print(f"[+] Found sensitive file: config.yml") - # Add log files - for f in os.listdir('.'): - if f.startswith('deployment_') and f.endswith('.log'): + # Add log files - be more specific about deployment logs + log_patterns = ['deployment_*.log', 'deployment_*_linode.log', 'deployment_*_aws.log'] + log_count = 0 + + # Search in current directory + for pattern in log_patterns: + for f in glob.glob(pattern): sensitive_files.append(f) + print(f"[+] Found sensitive log file: {f}") + log_count += 1 + + # Also check the logs directory if it exists + log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs") + if os.path.exists(log_dir): + for pattern in log_patterns: + for f in glob.glob(os.path.join(log_dir, pattern)): + sensitive_files.append(f) + print(f"[+] Found sensitive log file in logs directory: {f}") + log_count += 1 + + print(f"[+] Found {log_count} deployment log files") # Add inventory files - for f in os.listdir('.'): - if f.startswith('inventory_') and f.endswith('.yml'): - sensitive_files.append(f) + inventory_count = 0 + for f in glob.glob('inventory_*.yml'): + sensitive_files.append(f) + print(f"[+] Found sensitive inventory file: {f}") + inventory_count += 1 + + print(f"[+] Found {inventory_count} inventory files") # If there are sensitive files and user wants to clean up if sensitive_files: @@ -1257,36 +2495,45 @@ def cleanup_sensitive_files(config): if cleanup.startswith('y'): for f in sensitive_files: try: + print(f"[+] Securely deleting: {f}") # Secure overwrite then delete with open(f, 'wb') as file: # Overwrite with random data 3 times - for _ in range(3): - file.write(os.urandom(os.path.getsize(f))) + for i in range(3): + file_size = os.path.getsize(f) + file.write(os.urandom(file_size)) file.flush() os.fsync(file.fileno()) + print(f"[+] Completed overwrite pass {i+1}/3") # Finally remove os.remove(f) - print(f"[+] Securely deleted: {f}") + print(f"[+] Successfully deleted: {f}") except Exception as e: print(f"[!] Error deleting {f}: {e}") + else: + print("[+] No sensitive files found to clean up") # Remind about SSH keys - if config.get('ssh_key') and os.path.exists(config['ssh_key']): - print(f"\n[!] Remember to secure or remove your SSH key after use: {config['ssh_key']}") + if config.get('ssh_key') and os.path.exists(os.path.expanduser(config['ssh_key'])): + ssh_key = os.path.expanduser(config['ssh_key']) + print(f"\n[!] Remember to secure or remove your SSH key after use: {ssh_key}") secure_key = input("[?] Would you like to secure this SSH key now? (y/N): ").lower() if secure_key.startswith('y'): try: # Rename the SSH key with random suffix - key_dir = os.path.dirname(config['ssh_key']) - key_base = os.path.basename(config['ssh_key']) + key_dir = os.path.dirname(ssh_key) + key_base = os.path.basename(ssh_key) timestamp = datetime.now().strftime('%Y%m%d%H%M%S') new_key_path = os.path.join(key_dir, f"{key_base}.{timestamp}.bak") + print(f"[+] Renaming SSH key to: {new_key_path}") + # Move the key and public key - shutil.move(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") + shutil.move(ssh_key, new_key_path) + if os.path.exists(f"{ssh_key}.pub"): + shutil.move(f"{ssh_key}.pub", f"{new_key_path}.pub") + print(f"[+] Also renamed public key") # Set permissions os.chmod(new_key_path, 0o400) # Read-only by owner @@ -1342,7 +2589,7 @@ def save_config(config): 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"# C2itAll Deployment Log\n") f.write(f"# Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n") # Log general info @@ -1358,17 +2605,17 @@ def save_config(config): f.write(f"SSH Port: {config['ssh_port']}\n") # Log provider-specific info without exposing sensitive details - if config['provider'] == 'AWS': + if config['provider'] == 'aws': f.write(f"AWS Region: {config.get('aws_region', 'random')}\n") f.write(f"Instance Type: {config.get('size', 'default')}\n") f.write(f"Redirector Name: {config.get('redirector_name', 'Not specified')}\n") f.write(f"C2 Name: {config.get('c2_name', 'Not specified')}\n") - elif config['provider'] == 'Linode': + elif config['provider'] == 'linode': f.write(f"Linode Region: {config.get('linode_region', 'random')}\n") f.write(f"Plan: {config.get('plan', 'default')}\n") f.write(f"Redirector Name: {config.get('redirector_name', 'Not specified')}\n") f.write(f"C2 Name: {config.get('c2_name', 'Not specified')}\n") - elif config['provider'] == 'FlokiNET': + 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 @@ -1395,100 +2642,263 @@ def save_config(config): f.write(f"- Secure memory: {'Enabled' if config.get('secure_memory') else 'Disabled'}\n") f.write(f"- History disabled: {'Yes' if config.get('disable_history') else 'No'}\n") f.write(f"- Non-standard SSH port: {config.get('ssh_port', 'Default')}\n") + + # Log tracker module info if deployed + if config.get('deploy_tracker'): + f.write(f"\nTracker Module:\n") + f.write(f"- Tracker Domain: {config.get('tracker_domain', 'IP-based')}\n") + f.write(f"- Engagement Name: {config.get('tracker_name', 'Auto-generated')}\n") + f.write(f"- Email Pixel: {'Enabled' if config.get('tracker_create_pixel') else 'Disabled'}\n") print(f"[+] Deployment log saved to {log_file} (mode 0600)") +def show_deployment_summary(config): + """Display a summary of the deployed infrastructure""" + print("\n" + "="*60) + print(" C2itAll Deployment Summary") + print("="*60) + + # Show provider + print(f"Provider: {config['provider']}") + + # Show infrastructure components + if config.get('c2_only'): + print("\nC2 Server:") + print(f" Name: {config.get('c2_name', 'c2')}") + print(f" IP: {config.get('c2_ip', 'Not available')}") + if config.get('domain') != 'example.com': + print(f" Domain: mail.{config['domain']}") + + elif config.get('redirector_only'): + print("\nRedirector:") + print(f" Name: {config.get('redirector_name', 'redirector')}") + print(f" IP: {config.get('redirector_ip', 'Not available')}") + if config.get('domain') != 'example.com': + print(f" Domain: cdn.{config['domain']}") + + else: + print("\nFull Infrastructure:") + print("\n- Redirector:") + print(f" Name: {config.get('redirector_name', 'redirector')}") + print(f" IP: {config.get('redirector_ip', 'Not available')}") + if config.get('domain') != 'example.com': + print(f" Domain: cdn.{config['domain']}") + + print("\n- C2 Server:") + print(f" Name: {config.get('c2_name', 'c2')}") + print(f" IP: {config.get('c2_ip', 'Not available')}") + if config.get('domain') != 'example.com': + print(f" Domain: mail.{config['domain']}") + + # Show SSH connection details + print("\nSSH Access:") + user = config.get('ssh_user', 'root') + + if config.get('redirector_ip') and not config.get('c2_only'): + redirector_key = config.get('redirector_key_path', config.get('ssh_key', 'No key available')) + port = config.get('ssh_port', 22) + print(f" Redirector: ssh -i {redirector_key}{' -p ' + str(port) if port != 22 else ''} {user}@{config.get('redirector_ip')}") + + if config.get('c2_ip') and not config.get('redirector_only'): + c2_key = config.get('c2_key_path', config.get('ssh_key', 'No key available')) + port = config.get('ssh_port', 22) + print(f" C2 Server: ssh -i {c2_key}{' -p ' + str(port) if port != 22 else ''} {user}@{config.get('c2_ip')}") + + # Show domain configuration if used + if config.get('domain') != 'example.com': + print("\nDomain Configuration:") + print(f" Domain: {config['domain']}") + print(" Required DNS Records:") + if not config.get('c2_only'): + print(f" cdn.{config['domain']} -> {config.get('redirector_ip', 'IP_NOT_AVAILABLE')}") + if not config.get('redirector_only'): + print(f" mail.{config['domain']} -> {config.get('c2_ip', 'IP_NOT_AVAILABLE')}") + + # Show tracker details if deployed + if config.get('deploy_tracker'): + print("\nTracker Module:") + print(f" Engagement: {config.get('tracker_name', 'Auto-generated')}") + if config.get('tracker_domain'): + print(f" Domain: {config['tracker_domain']}") + print(f" DNS Record: {config['tracker_domain']} -> {config.get('redirector_ip', config.get('c2_ip', 'IP_NOT_AVAILABLE'))}") + else: + print(f" Access: http://{config.get('redirector_ip', config.get('c2_ip', 'IP_NOT_AVAILABLE'))}") + + if config.get('tracker_create_pixel'): + print(" Email Tracking: Enabled") + hostname = config.get('tracker_domain') or config.get('redirector_ip', config.get('c2_ip', 'IP_NOT_AVAILABLE')) + protocol = "https" if config.get('tracker_setup_ssl') else "http" + print(f" Tracking Pixel: ") + + print("\nFor more details, see deployment log file.") + print("="*60) + +def log_debug_info(command, error, log_file=None): + """Log detailed debug information when a deployment fails""" + timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + + # If no log file is specified, create one + if not log_file: + log_file = f"deployment_error_{int(time.time())}.log" + + with open(log_file, 'a') as f: + f.write(f"==== ERROR LOG: {timestamp} ====\n") + f.write(f"COMMAND: {command}\n") + f.write(f"ERROR: {str(error)}\n\n") + + # Try to get additional error information from ansible logs + try: + # Look for ansible log files + ansible_logs = [file for file in os.listdir('/tmp') if 'ansible-' in file and '.log' in file] + if ansible_logs: + f.write("ANSIBLE LOGS:\n") + # Get the most recent ansible log + latest_log = sorted(ansible_logs, key=lambda x: os.path.getmtime(os.path.join('/tmp', x)))[-1] + with open(os.path.join('/tmp', latest_log), 'r') as log: + f.write(log.read()) + except Exception as e: + f.write(f"Failed to retrieve ansible logs: {e}\n") + + f.write("==== END ERROR LOG ====\n\n") + + print(f"[+] Debug information logged to {log_file}") + return log_file + def main(): """Main function""" print("========================================") - print("C2ingRed - Red Team Infrastructure Setup") + print("C2itAll - Red Team Infrastructure Setup") print("========================================") - args = setup_argparse() + # Set up error logging + error_log_file = setup_error_logging() - # Interactive mode if no arguments provided - if len(sys.argv) == 1: - config = interactive_config() - else: - config = load_config(args) + # Track deployed resources for cleanup on interruption + deployed_instances = [] + generated_ssh_keys = [] - # Ensure provider has proper case - if config['provider'] and config['provider'].lower() in ['aws', 'linode', 'flokinet']: - # Convert to proper case if necessary - provider_map = {'aws': 'AWS', 'linode': 'Linode', 'flokinet': 'FlokiNET'} - config['provider'] = provider_map.get(config['provider'].lower(), config['provider']) - - # Validate configuration - if not validate_config(config): - return - - # Select region - select_region(config) - - # Ensure provider files exist - if not ensure_provider_files_exist(config): - proceed = input("\n[?] Continue despite missing files? (y/N): ").lower() - if not proceed.startswith('y'): - print("[-] Deployment aborted.") - return - - # Save configuration - save_config(config) - - # Deploy infrastructure based on provider - success = False try: - if config['provider'] == 'AWS': - success = deploy_aws(config) - elif config['provider'] == 'Linode': - success = deploy_linode(config) - elif config['provider'] == 'FlokiNET': - success = deploy_flokinet(config) + args = setup_argparse() + + # Interactive mode if no arguments provided + if len(sys.argv) == 1: + config = interactive_config() else: - print(f"[-] Error: Unsupported provider: {config['provider']}") + config = load_config(args) + + # Normalize provider name to proper capitalization + config['provider'] = normalize_provider_name(config['provider']) + + # Validate configuration + if not validate_config(config): return - except Exception as e: - print(f"[-] Unexpected error during deployment: {e}") - return - - # Only offer cleanup if deployment was successful - if success: - print("\n[+] Deployment completed successfully!") - if config['provider'] != 'FlokiNET': - print("\n[+] Your infrastructure is now ready to use!") - print(f"[+] Redirector: {config.get('redirector_name', 'redirector')}") - print(f"[+] C2 Server: {config.get('c2_name', 'c2')}") - 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.get('flokinet_redirector_ip', 'IP_NOT_PROVIDED')}") - if config.get('domain'): - print(f"[+] or Domain: cdn.{config['domain']}") - print(f"[+] Port: {config.get('shell_handler_port', 4444)} (shell handler port)") + # Select region + select_region(config) + + # Ensure provider files exist + if not ensure_provider_files_exist(config): + proceed = input("\n[?] Continue despite missing files? (y/N): ").lower() + if not proceed.startswith('y'): + print("[-] Deployment aborted.") + return + + # Save configuration + save_config(config) + + # Keep track of any generated SSH keys + if config.get('ssh_key') and config['ssh_key'].startswith(os.path.expanduser("~/.ssh/c2itall_")): + generated_ssh_keys.append(config['ssh_key']) + + # Track deployment logs + deployment_logs = [] + + # Deploy infrastructure based on provider + success = False + + try: + if config['provider'].lower() == 'aws': + # Verify SSH key permissions before deployment + if config.get('ssh_key'): + verify_key_file_permissions(os.path.expanduser(config['ssh_key'])) + + success, logs, aws_resources = deploy_with_error_handling(deploy_aws, config) + deployment_logs.extend(logs) + deployed_instances.extend(aws_resources) + + elif config['provider'].lower() == 'linode': + # Verify SSH key permissions before deployment + if config.get('ssh_key'): + verify_key_file_permissions(os.path.expanduser(config['ssh_key'])) + + success, logs, linode_resources = deploy_with_error_handling(deploy_linode, config) + deployment_logs.extend(logs) + deployed_instances.extend(linode_resources) + + elif config['provider'].lower() == 'flokinet': + success, logs, flokinet_resources = deploy_with_error_handling(deploy_flokinet, config) + deployment_logs.extend(logs) + deployed_instances.extend(flokinet_resources) + + else: + print(f"[-] Error: Unsupported provider: {config['provider']}") + return + + except Exception as e: + error_log = log_debug_info("Unexpected error", e, error_log_file) + if error_log: + deployment_logs.append(error_log) - # 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.get('flokinet_redirector_ip', 'IP_NOT_PROVIDED')}") - if not config.get('redirector_only'): - print(f"[!] - Add A record for mail.{config['domain']} pointing to {config.get('flokinet_c2_ip', 'IP_NOT_PROVIDED')}") + print(f"[-] Unexpected error during deployment: {e}") + print(f"[+] See {error_log_file} for detailed error information") + success = False - # Display non-standard SSH port warning if applicable - if 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") + # Show result and handle cleanup + if success: + print("\n[+] Deployment completed successfully!") + show_deployment_summary(config) + + # Only SSH if deployment was successful and user requested it + if config.get('ssh_after_deploy'): + print("\n[+] Preparing to SSH into C2 server...") + ssh_to_c2(config) + else: + print("\n[-] Deployment failed.") + if error_log_file: + print(f"[+] Check {error_log_file} for detailed error information") - # OPSEC: Clean up sensitive files after successful deployment - if not config.get('debug'): # Skip cleanup in debug mode + # Add all discovered logs to the config + config['deployment_logs'] = deployment_logs + + # Always ask if user wants to clean up sensitive files + cleanup_option = "y/N" if not success else "Y/n" + cleanup_default = "n" if not success else "y" + + cleanup = input(f"\n[?] Would you like to clean up sensitive files{' despite deployment failure' if not success else ''}? ({cleanup_option}): ").lower() or cleanup_default + + if cleanup.startswith('y'): cleanup_sensitive_files(config) - else: - print("\n[-] Deployment failed.") + + except KeyboardInterrupt: + print("\n\n[!] Deployment interrupted by user (Ctrl+C).") + + if deployed_instances: + print(f"[!] The following resources were deployed:") + for instance in deployed_instances: + if isinstance(instance, tuple) and len(instance) >= 2: + print(f" - {instance[0]} (ID: {instance[1]})") + else: + print(f" - {instance}") + + cleanup = input("\n[?] Would you like to clean up deployed resources before exiting? (Y/n): ").lower() or "y" + + if cleanup.startswith('y'): + cleanup_interrupted_deployment(config['provider'], deployed_instances, generated_ssh_keys) + else: + print("[!] Deployed resources will remain active. You will need to clean them up manually.") + + print("[+] Exiting...") + sys.exit(1) if __name__ == "__main__": main() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index eaaff9e..ef6a527 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,5 @@ linode_api4 boto3 botocore awscli -passlib \ No newline at end of file +passlib +paramiko \ No newline at end of file