From 6d307f82d8013b0536df8e1e0c867b5c3551b90a Mon Sep 17 00:00:00 2001 From: n0mad1k Date: Thu, 10 Apr 2025 13:03:32 -0400 Subject: [PATCH] Sadge --- {FlokiNET => FLOKINET}/c2.yml | 0 FLOKINET/cleanup.yml | 75 +++ {FlokiNET => FLOKINET}/provision.yml | 0 {FlokiNET => FLOKINET}/redirector.yml | 0 .../templates/default-site.j2 | 0 .../templates/gophish-config.j2 | 0 .../templates/index.html.j2 | 0 {FlokiNET => FLOKINET}/templates/motd-aws.j2 | 0 .../templates/nginx.conf.j2 | 0 FLOKINET/templates/proxychains.conf.j2 | 67 +++ FLOKINET/templates/resolv.conf.j2 | 18 + .../templates/secure-ssh.sh.j2 | 0 .../templates/shell-handler.service.j2 | 0 .../templates/sliver-server.service.j2 | 0 FLOKINET/templates/torrc.j2 | 48 ++ LINODE/c2-deploy.yaml | 205 ++++++++ {Linode => LINODE}/c2-vars-template.yaml | 0 {Linode => LINODE}/c2.yml | 0 {Linode => LINODE}/cleanup.yml | 0 {Linode => LINODE}/redirector.yml | 0 .../templates/gophish-config.j2 | 0 {Linode => LINODE}/templates/motd-linode.j2 | 0 deploy.py | 439 ++++++++++++++---- 23 files changed, 767 insertions(+), 85 deletions(-) rename {FlokiNET => FLOKINET}/c2.yml (100%) create mode 100644 FLOKINET/cleanup.yml rename {FlokiNET => FLOKINET}/provision.yml (100%) rename {FlokiNET => FLOKINET}/redirector.yml (100%) rename {FlokiNET => FLOKINET}/templates/default-site.j2 (100%) rename {FlokiNET => FLOKINET}/templates/gophish-config.j2 (100%) rename {FlokiNET => FLOKINET}/templates/index.html.j2 (100%) rename {FlokiNET => FLOKINET}/templates/motd-aws.j2 (100%) rename {FlokiNET => FLOKINET}/templates/nginx.conf.j2 (100%) create mode 100644 FLOKINET/templates/proxychains.conf.j2 create mode 100644 FLOKINET/templates/resolv.conf.j2 rename FlokiNET/cleanup.yml => FLOKINET/templates/secure-ssh.sh.j2 (100%) rename {FlokiNET => FLOKINET}/templates/shell-handler.service.j2 (100%) rename {FlokiNET => FLOKINET}/templates/sliver-server.service.j2 (100%) create mode 100644 FLOKINET/templates/torrc.j2 create mode 100644 LINODE/c2-deploy.yaml rename {Linode => LINODE}/c2-vars-template.yaml (100%) rename {Linode => LINODE}/c2.yml (100%) rename {Linode => LINODE}/cleanup.yml (100%) rename {Linode => LINODE}/redirector.yml (100%) rename {Linode => LINODE}/templates/gophish-config.j2 (100%) rename {Linode => LINODE}/templates/motd-linode.j2 (100%) diff --git a/FlokiNET/c2.yml b/FLOKINET/c2.yml similarity index 100% rename from FlokiNET/c2.yml rename to FLOKINET/c2.yml diff --git a/FLOKINET/cleanup.yml b/FLOKINET/cleanup.yml new file mode 100644 index 0000000..c8c0954 --- /dev/null +++ b/FLOKINET/cleanup.yml @@ -0,0 +1,75 @@ +--- +# FlokiNET Cleanup Playbook +# Used for documentation since FlokiNET requires manual cleanup through their interface + +- name: Document FlokiNET cleanup procedure + hosts: localhost + connection: local + gather_facts: false + vars_files: + - vars.yaml + vars: + cleanup_redirector: "{{ (redirector_ip is defined and redirector_ip != '') | ternary(true, false) }}" + cleanup_c2: "{{ (c2_ip is defined and c2_ip != '') | ternary(true, false) }}" + confirm_cleanup: "{{ confirm_cleanup | default(true) }}" + + tasks: + - name: Confirm cleanup if required + pause: + prompt: "WARNING: This script provides guidance for manual FlokiNET cleanup. Type 'yes' to continue" + register: confirmation + when: confirm_cleanup + + - name: Exit if not confirmed + meta: end_play + when: confirm_cleanup and confirmation.user_input != 'yes' + + - name: Display cleanup instructions + debug: + msg: + - "FlokiNET Cleanup Instructions" + - "========================================" + - "Since FlokiNET resources must be manually terminated through their control panel," + - "this playbook provides guidance on the steps needed for cleanup." + - "" + - "Resources to clean up:" + - "{{ cleanup_redirector | ternary('- Redirector server: ' + redirector_ip, '') }}" + - "{{ cleanup_c2 | ternary('- C2 server: ' + c2_ip, '') }}" + - "" + - "Steps to terminate FlokiNET servers:" + - "1. Log in to your FlokiNET control panel" + - "2. Navigate to the Virtual Servers section" + - "3. Select each server from the list" + - "4. Click 'Terminate' and confirm termination" + - "" + - "For security, consider also:" + - "- Manually executing the secure-exit.sh script on each server before termination" + - "- Removing DNS records associated with these servers" + - "- Ensuring any SSH keys used for these servers are removed or rotated" + + - name: Remove SSH key file if it's not a shared key + file: + path: "{{ ssh_key_path | replace('.pub', '') }}" + state: absent + when: + - ssh_key_path is defined + - ssh_key_path is search('c2deploy_') + ignore_errors: true + + - name: Remove SSH public key file if it's not a shared key + file: + path: "{{ ssh_key_path }}" + state: absent + when: + - ssh_key_path is defined + - ssh_key_path is search('c2deploy_') + ignore_errors: true + + - name: Pre-termination security recommendations + debug: + msg: + - "Security Recommendations before manual termination:" + - "------------------------------------------------" + - " SSH Command: ssh -i {{ ssh_key_path | replace('.pub', '') }} {{ ssh_user | default('root') }}@SERVER_IP -p {{ ssh_port | default('22') }}" + - " Then run: /opt/c2/secure-exit.sh" + when: (cleanup_redirector or cleanup_c2) and ssh_key_path is defined \ No newline at end of file diff --git a/FlokiNET/provision.yml b/FLOKINET/provision.yml similarity index 100% rename from FlokiNET/provision.yml rename to FLOKINET/provision.yml diff --git a/FlokiNET/redirector.yml b/FLOKINET/redirector.yml similarity index 100% rename from FlokiNET/redirector.yml rename to FLOKINET/redirector.yml diff --git a/FlokiNET/templates/default-site.j2 b/FLOKINET/templates/default-site.j2 similarity index 100% rename from FlokiNET/templates/default-site.j2 rename to FLOKINET/templates/default-site.j2 diff --git a/FlokiNET/templates/gophish-config.j2 b/FLOKINET/templates/gophish-config.j2 similarity index 100% rename from FlokiNET/templates/gophish-config.j2 rename to FLOKINET/templates/gophish-config.j2 diff --git a/FlokiNET/templates/index.html.j2 b/FLOKINET/templates/index.html.j2 similarity index 100% rename from FlokiNET/templates/index.html.j2 rename to FLOKINET/templates/index.html.j2 diff --git a/FlokiNET/templates/motd-aws.j2 b/FLOKINET/templates/motd-aws.j2 similarity index 100% rename from FlokiNET/templates/motd-aws.j2 rename to FLOKINET/templates/motd-aws.j2 diff --git a/FlokiNET/templates/nginx.conf.j2 b/FLOKINET/templates/nginx.conf.j2 similarity index 100% rename from FlokiNET/templates/nginx.conf.j2 rename to FLOKINET/templates/nginx.conf.j2 diff --git a/FLOKINET/templates/proxychains.conf.j2 b/FLOKINET/templates/proxychains.conf.j2 new file mode 100644 index 0000000..b346c22 --- /dev/null +++ b/FLOKINET/templates/proxychains.conf.j2 @@ -0,0 +1,67 @@ +# FlokiNET/templates/proxychains.conf.j2 +# ProxyChains configuration for C2 server +# Routes traffic through Tor for anonymity + +# Dynamic chain - Each connection through the proxy list +# Uses chained proxies in the order they appear in the list +dynamic_chain + +# Proxy DNS requests - no leak for DNS data +proxy_dns + +# Randomize the order of the proxies on each start +# random_chain + +# Set the type of chain (dynamic, strict, random) +# strict_chain +# random_chain + +# Quiet mode (less console output) +quiet_mode + +# ProxyList format: +# type host port [user pass] +# (values separated by 'tab' or 'blank') +[ProxyList] +# add proxy here ... +# socks5 127.0.0.1 1080 +socks5 127.0.0.1 9050 + +# FlokiNET/templates/iptables-rules.j2 +# Hardened iptables rules for FlokiNET C2 server +# Applied at system startup + +*filter +:INPUT DROP [0:0] +:FORWARD DROP [0:0] +:OUTPUT ACCEPT [0:0] + +# Allow established and related connections +-A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT + +# Allow loopback +-A INPUT -i lo -j ACCEPT + +# Allow SSH +-A INPUT -p tcp -m state --state NEW -m tcp --dport {{ ssh_port | default(22) }} -j ACCEPT + +# Allow HTTP/HTTPS +-A INPUT -p tcp -m state --state NEW -m tcp --dport 80 -j ACCEPT +-A INPUT -p tcp -m state --state NEW -m tcp --dport 443 -j ACCEPT + +# Allow Sliver C2 ports +-A INPUT -p tcp -m state --state NEW -m tcp --dport 8888 -j ACCEPT +-A INPUT -p tcp -m state --state NEW -m tcp --dport 31337 -j ACCEPT + +# Allow shell handler port +{% if shell_handler_port is defined %} +-A INPUT -p tcp -m state --state NEW -m tcp --dport {{ shell_handler_port }} -j ACCEPT +{% endif %} + +# Block all other incoming traffic +-A INPUT -j DROP + +# Allow all outbound traffic by default +-A OUTPUT -j ACCEPT + +COMMIT \ No newline at end of file diff --git a/FLOKINET/templates/resolv.conf.j2 b/FLOKINET/templates/resolv.conf.j2 new file mode 100644 index 0000000..e6444ec --- /dev/null +++ b/FLOKINET/templates/resolv.conf.j2 @@ -0,0 +1,18 @@ +# FlokiNET/templates/resolv.conf.j2 +# Secure DNS configuration +# Uses privacy-respecting DNS servers + +nameserver 9.9.9.9 +nameserver 1.1.1.1 +options edns0 single-request-reopen +options timeout:1 +options attempts:2 + +# FlokiNET/templates/dnscrypt.conf.j2 +[Resolve] +DNS=9.9.9.9 1.1.1.1 +FallbackDNS=8.8.8.8 8.8.4.4 +DNSSEC=yes +DNSOverTLS=yes +Cache=yes +DNSStubListener=yes diff --git a/FlokiNET/cleanup.yml b/FLOKINET/templates/secure-ssh.sh.j2 similarity index 100% rename from FlokiNET/cleanup.yml rename to FLOKINET/templates/secure-ssh.sh.j2 diff --git a/FlokiNET/templates/shell-handler.service.j2 b/FLOKINET/templates/shell-handler.service.j2 similarity index 100% rename from FlokiNET/templates/shell-handler.service.j2 rename to FLOKINET/templates/shell-handler.service.j2 diff --git a/FlokiNET/templates/sliver-server.service.j2 b/FLOKINET/templates/sliver-server.service.j2 similarity index 100% rename from FlokiNET/templates/sliver-server.service.j2 rename to FLOKINET/templates/sliver-server.service.j2 diff --git a/FLOKINET/templates/torrc.j2 b/FLOKINET/templates/torrc.j2 new file mode 100644 index 0000000..f6797c3 --- /dev/null +++ b/FLOKINET/templates/torrc.j2 @@ -0,0 +1,48 @@ +# FlokiNET/templates/torrc.j2 +# +# Tor configuration for C2 server +# Hardened configuration for operational security + +# General settings +DataDirectory /var/lib/tor +RunAsDaemon 1 +ControlPort 9051 +CookieAuthentication 1 +CookieAuthFileGroupReadable 0 +DisableDebuggerAttachment 1 + +# Network settings +SOCKSPort 127.0.0.1:9050 +SOCKSPolicy accept 127.0.0.1/8 +SOCKSPolicy reject * +Log notice file /var/log/tor/notices.log +SafeSocks 1 +TestSocks 0 + +# Circuit settings +NumEntryGuards 4 +EnforceDistinctSubnets 1 +CircuitBuildTimeout 60 +PathsNeededToBuildCircuits 0.95 +NewCircuitPeriod 900 +MaxCircuitDirtiness 1800 + +# Security settings +StrictNodes 1 +WarnPlaintextPorts 23,109,110,143,80,21 +ReachableAddresses *:80,*:443 +ReachableAddresses reject *:* +ReachableAddresses accept *:80 +ReachableAddresses accept *:443 + +# Obfuscation settings +Bridge obfs4 {{ bridge_address | default('placeholderbridge.example.org:443') }} {{ bridge_fingerprint | default('PLACEHOLDERFINGERPRINT') }} cert=PLACEHOLDER +UseBridges 1 +ClientTransportPlugin obfs4 exec /usr/bin/obfs4proxy +ClientTransportPlugin meek exec /usr/bin/obfs4proxy + +# Exit policy (no exits allowed) +ExitPolicy reject *:* + +# DNS resolution +AutomapHostsOnResolve 1 \ No newline at end of file diff --git a/LINODE/c2-deploy.yaml b/LINODE/c2-deploy.yaml new file mode 100644 index 0000000..1a027e0 --- /dev/null +++ b/LINODE/c2-deploy.yaml @@ -0,0 +1,205 @@ +--- +# Linode C2 Deployment Playbook +# This playbook handles both redirector and C2 deployment for Linode + +- name: Create and configure Linode infrastructure + hosts: localhost + gather_facts: false + connection: local + vars_files: + - vars.yaml + vars: + # Default values for required variables + ssh_user: "{{ ssh_user | default('root') }}" + linode_region: "{{ linode_region | default(region_choices | random) }}" + plan: "{{ plan | default('g6-standard-1') }}" + image: "{{ image | default('linode/debian11') }}" + + # Deployment mode flags + redirector_only: "{{ redirector_only | default(false) }}" + c2_only: "{{ c2_only | default(false) }}" + + # Generate random instance names if not provided + redirector_name: "{{ redirector_name | default('srv-' + 9999999999 | random | to_uuid | hash('md5') | truncate(8, True, '')) }}" + c2_name: "{{ c2_name | default('node-' + 9999999999 | random | to_uuid | hash('md5') | truncate(8, True, '')) }}" + + # Generate random shell handler port if not provided + shell_handler_port: "{{ shell_handler_port | default(4000 + 60000 | random) }}" + + tasks: + - name: Validate required Linode token + assert: + that: + - linode_token is defined and linode_token != "" + fail_msg: "Linode API token is required. Set linode_token in vars.yaml or via --linode-token." + + - name: Set the target instances to deploy based on configuration + set_fact: + deploy_redirector: "{{ not c2_only }}" + deploy_c2: "{{ not redirector_only }}" + + # REDIRECTOR DEPLOYMENT + - name: Deploy redirector Linode instance + when: deploy_redirector + block: + - name: Create redirector Linode instance + community.general.linode_v4: + access_token: "{{ linode_token }}" + label: "{{ redirector_name }}" + type: "{{ plan }}" + region: "{{ linode_region }}" + image: "{{ image }}" + root_pass: "{{ lookup('password', '/dev/null length=24 chars=ascii_letters,digits') }}" + authorized_keys: + - "{{ lookup('file', ssh_key_path) }}" + state: present + register: redirector_instance + + - name: Set redirector_ip for later use + set_fact: + redirector_ip: "{{ redirector_instance.instance.ipv4[0] }}" + redirector_instance_id: "{{ redirector_instance.instance.id }}" + + - name: Wait for redirector SSH to be available + wait_for: + host: "{{ redirector_ip }}" + port: 22 + delay: 30 + timeout: 300 + state: started + + - name: Add redirector to inventory + add_host: + name: "redirector" + groups: "redirectors" + ansible_host: "{{ redirector_ip }}" + ansible_user: "{{ ssh_user }}" + ansible_ssh_private_key_file: "{{ ssh_key_path | replace('.pub', '') }}" + ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" + + # C2 SERVER DEPLOYMENT + - name: Deploy C2 Linode instance + when: deploy_c2 + block: + - name: Create C2 Linode instance + community.general.linode_v4: + access_token: "{{ linode_token }}" + label: "{{ c2_name }}" + type: "{{ plan }}" + region: "{{ linode_region }}" + image: "{{ image }}" + root_pass: "{{ lookup('password', '/dev/null length=24 chars=ascii_letters,digits') }}" + authorized_keys: + - "{{ lookup('file', ssh_key_path) }}" + state: present + register: c2_instance + + - name: Set c2_ip for later use + set_fact: + c2_ip: "{{ c2_instance.instance.ipv4[0] }}" + c2_instance_id: "{{ c2_instance.instance.id }}" + + - name: Wait for C2 SSH to be available + wait_for: + host: "{{ c2_ip }}" + port: 22 + delay: 30 + timeout: 300 + state: started + + - name: Add C2 to inventory + add_host: + name: "c2" + groups: "c2servers" + ansible_host: "{{ c2_ip }}" + ansible_user: "{{ ssh_user }}" + ansible_ssh_private_key_file: "{{ ssh_key_path | replace('.pub', '') }}" + ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" + +- name: Configure redirector server + hosts: redirectors + become: true + gather_facts: true + vars_files: + - vars.yaml + vars: + c2_ip: "{{ hostvars['localhost']['c2_ip'] | default('127.0.0.1') }}" + redirector_subdomain: "{{ redirector_subdomain | default('cdn') }}" + tasks: + - name: Wait for apt to be available + apt: + update_cache: yes + register: apt_result + until: apt_result is success + retries: 5 + delay: 10 + + - name: Disable root password authentication for SSH immediately + lineinfile: + path: /etc/ssh/sshd_config + regexp: '^#?PasswordAuthentication' + line: 'PasswordAuthentication no' + state: present + + - name: Restart SSH service to apply changes + service: + name: ssh + state: restarted + + - name: Include common redirector configuration tasks + include_tasks: "../tasks/configure_redirector.yml" + +- name: Configure C2 server + hosts: c2servers + become: true + gather_facts: true + vars_files: + - vars.yaml + vars: + redirector_ip: "{{ hostvars['localhost']['redirector_ip'] | default('127.0.0.1') }}" + c2_subdomain: "{{ c2_subdomain | default('mail') }}" + tasks: + - name: Wait for apt to be available + apt: + update_cache: yes + register: apt_result + until: apt_result is success + retries: 5 + delay: 10 + + - name: Disable root password authentication for SSH immediately + lineinfile: + path: /etc/ssh/sshd_config + regexp: '^#?PasswordAuthentication' + line: 'PasswordAuthentication no' + state: present + + - name: Restart SSH service to apply changes + service: + name: ssh + state: restarted + + - name: Include common tool installation tasks + include_tasks: "../tasks/install_tools.yml" + + - name: Include common C2 configuration tasks + include_tasks: "../tasks/configure_c2.yml" + + - name: Include common security hardening tasks + include_tasks: "../tasks/security_hardening.yml" + + - name: Include common mail server configuration tasks + include_tasks: "../tasks/configure_mail.yml" + + - name: Print deployment summary + debug: + msg: + - "C2 Infrastructure Deployment Complete!" + - "--------------------------------------" + - "Redirector IP: {{ hostvars['localhost']['redirector_ip'] | default('Not deployed') }}" + - "Redirector Domain: {{ redirector_subdomain }}.{{ domain }}" + - "C2 Server IP: {{ ansible_host }}" + - "C2 Server Domain: {{ c2_subdomain }}.{{ domain }}" + - "GoPhish Admin Port: {{ gophish_admin_port }}" + - "Shell Handler Port: {{ shell_handler_port }}" + when: not disable_summary | default(false) \ No newline at end of file diff --git a/Linode/c2-vars-template.yaml b/LINODE/c2-vars-template.yaml similarity index 100% rename from Linode/c2-vars-template.yaml rename to LINODE/c2-vars-template.yaml diff --git a/Linode/c2.yml b/LINODE/c2.yml similarity index 100% rename from Linode/c2.yml rename to LINODE/c2.yml diff --git a/Linode/cleanup.yml b/LINODE/cleanup.yml similarity index 100% rename from Linode/cleanup.yml rename to LINODE/cleanup.yml diff --git a/Linode/redirector.yml b/LINODE/redirector.yml similarity index 100% rename from Linode/redirector.yml rename to LINODE/redirector.yml diff --git a/Linode/templates/gophish-config.j2 b/LINODE/templates/gophish-config.j2 similarity index 100% rename from Linode/templates/gophish-config.j2 rename to LINODE/templates/gophish-config.j2 diff --git a/Linode/templates/motd-linode.j2 b/LINODE/templates/motd-linode.j2 similarity index 100% rename from Linode/templates/motd-linode.j2 rename to LINODE/templates/motd-linode.j2 diff --git a/deploy.py b/deploy.py index afe0cf4..f4033a1 100644 --- a/deploy.py +++ b/deploy.py @@ -53,7 +53,7 @@ def parse_arguments(): parser = argparse.ArgumentParser(description='Deploy C2 Red Team infrastructure') # Provider selection - parser.add_argument('-p', '--provider', choices=PROVIDERS, default="aws", help='Provider to use for deployment') + parser.add_argument('-p', '--provider', choices=PROVIDERS, default=None, help='Provider to use for deployment') # AWS-specific arguments parser.add_argument('--aws-key', help='AWS access key') @@ -92,12 +92,175 @@ def parse_arguments(): 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('--teardown', action='store_true', help='Tear down existing infrastructure') + + # Interactive mode + parser.add_argument('--interactive', action='store_true', help='Run in interactive wizard mode') return parser.parse_args() +def interactive_setup(): + """Interactive setup wizard for deployment""" + config = {} + + print("\n====== C2ingRed Interactive Setup Wizard ======\n") + + # Select provider + print("Available cloud providers:") + for i, provider in enumerate(PROVIDERS, 1): + print(f" {i}. {provider.capitalize()}") + + while True: + try: + provider_choice = int(input("\nSelect a provider (1-3): ")) + if 1 <= provider_choice <= len(PROVIDERS): + config['provider'] = PROVIDERS[provider_choice - 1] + break + else: + print(f"Please enter a number between 1 and {len(PROVIDERS)}") + except ValueError: + print("Please enter a valid number") + + # Load vars file for the selected provider + vars_file = f"{config['provider'].upper()}/vars.yaml" + vars_data = {} + if os.path.exists(vars_file): + try: + with open(vars_file, 'r') as f: + vars_data = yaml.safe_load(f) or {} + print(f"Loaded configuration from {vars_file}") + except Exception as e: + print(f"Warning: Failed to load {vars_file}: {e}") + + # Provider-specific configuration + if config['provider'] == "aws": + config['aws_access_key'] = input("\nAWS Access Key [leave blank to use AWS CLI profile]: ") or None + config['aws_secret_key'] = input("AWS Secret Key [leave blank to use AWS CLI profile]: ") or None + + # Show available regions + aws_regions = vars_data.get('aws_region_choices', []) + if aws_regions: + print("\nAvailable AWS regions:") + for i, region in enumerate(aws_regions, 1): + print(f" {i}. {region}") + + while True: + region_input = input("\nSelect a region (number) or press Enter for random: ") + if not region_input: + config['aws_region'] = None # Random + break + try: + region_choice = int(region_input) + if 1 <= region_choice <= len(aws_regions): + config['aws_region'] = aws_regions[region_choice - 1] + break + else: + print(f"Please enter a number between 1 and {len(aws_regions)}") + except ValueError: + print("Please enter a valid number or press Enter") + else: + config['aws_region'] = input("\nAWS Region [leave blank for us-east-1]: ") or "us-east-1" + + elif config['provider'] == "linode": + config['linode_token'] = input("\nLinode API Token: ") + + # Show available regions + linode_regions = vars_data.get('region_choices', []) + if linode_regions: + print("\nAvailable Linode regions:") + for i, region in enumerate(linode_regions, 1): + print(f" {i}. {region}") + + while True: + region_input = input("\nSelect a region (number) or press Enter for random: ") + if not region_input: + config['linode_region'] = None # Random + break + try: + region_choice = int(region_input) + if 1 <= region_choice <= len(linode_regions): + config['linode_region'] = linode_regions[region_choice - 1] + break + else: + print(f"Please enter a number between 1 and {len(linode_regions)}") + except ValueError: + print("Please enter a valid number or press Enter") + + elif config['provider'] == "flokinet": + print("\nFlokiNET requires pre-provisioned servers.") + config['flokinet_redirector_ip'] = input("FlokiNET Redirector IP Address: ") + config['flokinet_c2_ip'] = input("FlokiNET C2 Server IP Address: ") + config['ssh_user'] = input(f"SSH User [default: {DEFAULT_SSH_USER['flokinet']}]: ") or DEFAULT_SSH_USER['flokinet'] + + ssh_key = input("Path to SSH Private Key [leave blank to generate new key]: ") + if ssh_key: + config['ssh_key'] = os.path.expanduser(ssh_key) + else: + config['ssh_key'] = None # Will generate a new key + + # Deployment type + print("\nDeployment type:") + print(" 1. Full deployment (Redirector + C2)") + print(" 2. Redirector only") + print(" 3. C2 server only") + + while True: + try: + deploy_choice = int(input("\nSelect deployment type (1-3): ")) + if deploy_choice == 1: + config['redirector_only'] = False + config['c2_only'] = False + break + elif deploy_choice == 2: + config['redirector_only'] = True + config['c2_only'] = False + break + elif deploy_choice == 3: + config['redirector_only'] = False + config['c2_only'] = True + break + else: + print("Please enter a number between 1 and 3") + except ValueError: + print("Please enter a valid number") + + # Domain configuration + config['domain'] = input("\nDomain name [default: example.com]: ") or "example.com" + config['letsencrypt_email'] = input(f"Email for Let's Encrypt [default: admin@{config['domain']}]: ") or f"admin@{config['domain']}" + + # Security options + print("\nSecurity options:") + disable_history = input("Disable command history? (y/n) [default: y]: ").lower() != 'n' + secure_memory = input("Enable secure memory settings? (y/n) [default: y]: ").lower() != 'n' + zero_logs = input("Enable zero-logs configuration? (y/n) [default: y]: ").lower() != 'n' + + config['disable_history'] = disable_history + config['secure_memory'] = secure_memory + config['zero_logs'] = zero_logs + + # Post-deployment options + config['ssh_after_deploy'] = input("\nSSH into instance after deployment? (y/n) [default: n]: ").lower() == 'y' + + # Debug mode + config['debug'] = input("Enable debug mode? (y/n) [default: n]: ").lower() == 'y' + + print("\n====== Configuration Summary ======") + for key, value in config.items(): + if key not in ['aws_secret_key', 'linode_token']: + print(f" {key}: {value}") + + confirm = input("\nProceed with deployment? (y/n): ").lower() + if confirm != 'y': + print("Deployment cancelled.") + sys.exit(0) + + return config + def load_vars_file(provider): """Load vars.yaml for the specified provider""" - vars_file = f"{provider}/vars.yaml" + # Handle case sensitivity in directory names + provider_dir = provider.upper() + vars_file = f"{provider_dir}/vars.yaml" + if os.path.exists(vars_file): try: with open(vars_file, 'r') as f: @@ -233,6 +396,8 @@ def run_ansible_playbook(playbook, inventory, config, debug=False): def deploy_infrastructure(config): """Deploy infrastructure based on provider and configuration""" provider = config['provider'] + # Convert provider to uppercase for directory paths + provider_dir = provider.upper() logging.info(f"Deploying {provider} infrastructure...") # Set provider-specific environment variables @@ -253,10 +418,10 @@ def deploy_infrastructure(config): # Determine playbook path based on deployment mode if config.get('redirector_only'): - playbook = f"{provider}/redirector.yml" + playbook = f"{provider_dir}/redirector.yml" deployment_type = "redirector" elif config.get('c2_only'): - playbook = f"{provider}/c2.yml" + playbook = f"{provider_dir}/c2.yml" deployment_type = "c2" else: # Full deployment @@ -270,7 +435,7 @@ def deploy_infrastructure(config): return deploy_flokinet_c2(config) else: # AWS and Linode use a single playbook for full deployment - playbook = f"{provider}/c2-deploy.yaml" + playbook = f"{provider_dir}/c2-deploy.yaml" deployment_type = "local" # Create inventory file @@ -312,7 +477,7 @@ def deploy_flokinet_redirector(config): inventory_path = create_inventory_file(config, "redirector") # Run the playbook - playbook = "FlokiNET/redirector.yml" + playbook = "FLOKINET/redirector.yml" try: success, stdout, stderr = run_ansible_playbook( playbook, inventory_path, config, config.get('debug', False) @@ -348,7 +513,7 @@ def deploy_flokinet_c2(config): inventory_path = create_inventory_file(config, "c2") # Run the playbook - playbook = "FlokiNET/c2.yml" + playbook = "FLOKINET/c2.yml" try: success, stdout, stderr = run_ansible_playbook( playbook, inventory_path, config, config.get('debug', False) @@ -436,22 +601,24 @@ def ssh_to_instance(config): def cleanup_resources(config): """Clean up resources if deployment fails""" provider = config.get('provider') + provider_dir = provider.upper() if provider else None logging.info(f"Cleaning up {provider} resources...") # Use Ansible for cleanup - playbook = f"{provider}/cleanup.yml" - if os.path.exists(playbook): - logging.info(f"Running cleanup playbook: {playbook}") - inventory_path = create_inventory_file(config, "local") - try: - run_ansible_playbook(playbook, inventory_path, config) - except Exception as e: - logging.error(f"Cleanup playbook failed: {e}") - finally: - if os.path.exists(inventory_path): - os.unlink(inventory_path) - else: - logging.warning(f"No cleanup playbook found at {playbook}") + if provider_dir: + playbook = f"{provider_dir}/cleanup.yml" + if os.path.exists(playbook): + logging.info(f"Running cleanup playbook: {playbook}") + inventory_path = create_inventory_file(config, "local") + try: + run_ansible_playbook(playbook, inventory_path, config) + except Exception as e: + logging.error(f"Cleanup playbook failed: {e}") + finally: + if os.path.exists(inventory_path): + os.unlink(inventory_path) + else: + logging.warning(f"No cleanup playbook found at {playbook}") # Clean up SSH key if we generated one ssh_key = config.get('ssh_key') @@ -464,6 +631,93 @@ def cleanup_resources(config): except Exception as e: logging.error(f"Failed to remove SSH key: {e}") +def check_dependencies(): + """Check if required dependencies are installed""" + dependencies = { + "ansible": "ansible-playbook --version", + "aws": "aws --version", + "linode-cli": "linode-cli --version", + } + + missing = [] + for dep, cmd in dependencies.items(): + try: + subprocess.run(cmd.split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True) + except (subprocess.CalledProcessError, FileNotFoundError): + missing.append(dep) + + if missing: + logging.warning(f"Missing dependencies: {', '.join(missing)}") + print("\nWarning: The following dependencies are missing or not in PATH:") + for dep in missing: + print(f" - {dep}") + print("\nPlease install them to ensure proper functionality.") + print("You can install Python dependencies with: pip install -r requirements.txt") + + if "ansible" in missing: + print("\nTo install Ansible: pip install ansible") + + if "aws" in missing: + print("\nTo install AWS CLI: pip install awscli") + + if "linode-cli" in missing: + print("\nTo install Linode CLI: pip install linode-cli") + + confirm = input("\nContinue anyway? (y/n): ").lower() + if confirm != 'y': + sys.exit(1) + +def teardown_infrastructure(config): + """Tear down existing infrastructure""" + provider = config['provider'] + provider_dir = provider.upper() + logging.info(f"Tearing down {provider} infrastructure...") + + # Set provider-specific environment variables + if provider == "aws": + if config.get('aws_access_key'): + os.environ['AWS_ACCESS_KEY_ID'] = config['aws_access_key'] + if config.get('aws_secret_key'): + os.environ['AWS_SECRET_ACCESS_KEY'] = config['aws_secret_key'] + elif provider == "linode": + if config.get('linode_token'): + os.environ['LINODE_TOKEN'] = config['linode_token'] + + # Run cleanup playbook + playbook = f"{provider_dir}/cleanup.yml" + if os.path.exists(playbook): + # Add confirm_cleanup=false to skip confirmation in playbook + config['confirm_cleanup'] = False + + # Create inventory for local execution + inventory_path = create_inventory_file(config, "local") + + try: + success, stdout, stderr = run_ansible_playbook( + playbook, inventory_path, config, config.get('debug', False) + ) + + if success: + logging.info(f"Successfully tore down {provider} infrastructure") + else: + logging.error(f"Failed to tear down {provider} infrastructure") + logging.error(stderr) + + # Clean up inventory + if os.path.exists(inventory_path): + os.unlink(inventory_path) + + return success + except Exception as e: + logging.error(f"Teardown failed: {e}") + # Clean up inventory + if os.path.exists(inventory_path): + os.unlink(inventory_path) + return False + else: + logging.error(f"No cleanup playbook found at {playbook}") + return False + def main(): """Main function to run the deployment""" # Set up logging @@ -472,74 +726,84 @@ def main(): # Parse command line arguments args = parse_arguments() - # Override provider if --flokinet is specified - if args.flokinet: - args.provider = "flokinet" + # Check dependencies + check_dependencies() - # Load variables from provider-specific vars.yaml - vars_data = load_vars_file(args.provider) - - # Build configuration by combining args and vars_data - config = {} - - # Provider settings - config['provider'] = args.provider - - # AWS settings - if args.provider == "aws": - config['aws_access_key'] = args.aws_key or vars_data.get('aws_access_key') - config['aws_secret_key'] = args.aws_secret or vars_data.get('aws_secret_key') - config['aws_region'] = args.aws_region or args.region - config['aws_region_choices'] = vars_data.get('aws_region_choices', []) - config['ami_map'] = vars_data.get('ami_map', {}) - - # Linode settings - elif args.provider == "linode": - config['linode_token'] = args.linode_token or vars_data.get('linode_token') - config['linode_region'] = args.linode_region or args.region - config['region_choices'] = vars_data.get('region_choices', []) - config['plan'] = vars_data.get('plan', 'g6-standard-1') - config['image'] = vars_data.get('image', 'linode/debian11') - - # FlokiNET settings - elif args.provider == "flokinet": - config['flokinet_redirector_ip'] = args.flokinet_redirector_ip or vars_data.get('redirector_ip') - config['flokinet_c2_ip'] = args.flokinet_c2_ip or vars_data.get('c2_ip') - config['flokinet_region_choices'] = vars_data.get('flokinet_region_choices', []) - config['ssh_port'] = vars_data.get('ssh_port', 22) - - # SSH settings - config['ssh_user'] = args.ssh_user or DEFAULT_SSH_USER.get(args.provider) - if args.ssh_key: - config['ssh_key'] = os.path.expanduser(args.ssh_key) + # Use interactive mode if specified or if no provider is given + if args.interactive or args.provider is None and not args.flokinet: + config = interactive_setup() else: - config['ssh_key'] = generate_ssh_key() - - # Generate random instance names - rand_suffix = generate_random_string() - timestamp = int(time.time()) % 10000 - config['redirector_name'] = args.redirector_name or f"srv-{rand_suffix}-{timestamp}" - config['c2_name'] = args.c2_name or f"node-{rand_suffix}-{timestamp}" - - # Deployment options - config['redirector_only'] = args.redirector_only - config['c2_only'] = args.c2_only - config['debug'] = args.debug - config['domain'] = args.domain or vars_data.get('domain', 'example.com') - config['letsencrypt_email'] = args.letsencrypt_email or vars_data.get('letsencrypt_email', f"admin@{config['domain']}") - - # OPSEC settings - config['disable_history'] = args.disable_history if args.disable_history is not None else vars_data.get('disable_history', True) - config['secure_memory'] = args.secure_memory if args.secure_memory is not None else vars_data.get('secure_memory', True) - config['zero_logs'] = args.zero_logs if args.zero_logs is not None else vars_data.get('zero_logs', True) - - # Other settings from vars_data - config['gophish_admin_port'] = vars_data.get('gophish_admin_port', str(random.randint(2000, 9000))) - config['smtp_auth_user'] = vars_data.get('smtp_auth_user', f"user{random.randint(1000, 9999)}") - config['smtp_auth_pass'] = vars_data.get('smtp_auth_pass', ''.join(random.choices(string.ascii_letters + string.digits, k=20))) + # Override provider if --flokinet is specified + if args.flokinet: + args.provider = "flokinet" + + # Load variables from provider-specific vars.yaml + vars_data = load_vars_file(args.provider) + + # Build configuration by combining args and vars_data + config = {} + + # Provider settings + config['provider'] = args.provider + + # AWS settings + if args.provider == "aws": + config['aws_access_key'] = args.aws_key or vars_data.get('aws_access_key') + config['aws_secret_key'] = args.aws_secret or vars_data.get('aws_secret_key') + config['aws_region'] = args.aws_region or args.region + config['aws_region_choices'] = vars_data.get('aws_region_choices', []) + config['ami_map'] = vars_data.get('ami_map', {}) + + # Linode settings + elif args.provider == "linode": + config['linode_token'] = args.linode_token or vars_data.get('linode_token') + config['linode_region'] = args.linode_region or args.region + config['region_choices'] = vars_data.get('region_choices', []) + config['plan'] = vars_data.get('plan', 'g6-standard-1') + config['image'] = vars_data.get('image', 'linode/kali') + + # FlokiNET settings + elif args.provider == "flokinet": + config['flokinet_redirector_ip'] = args.flokinet_redirector_ip or vars_data.get('redirector_ip') + config['flokinet_c2_ip'] = args.flokinet_c2_ip or vars_data.get('c2_ip') + config['flokinet_region_choices'] = vars_data.get('flokinet_region_choices', []) + config['ssh_port'] = vars_data.get('ssh_port', 22) + + # SSH settings + config['ssh_user'] = args.ssh_user or DEFAULT_SSH_USER.get(args.provider) + if args.ssh_key: + config['ssh_key'] = os.path.expanduser(args.ssh_key) + else: + config['ssh_key'] = generate_ssh_key() + + # Generate random instance names + rand_suffix = generate_random_string() + timestamp = int(time.time()) % 10000 + config['redirector_name'] = args.redirector_name or f"srv-{rand_suffix}-{timestamp}" + config['c2_name'] = args.c2_name or f"node-{rand_suffix}-{timestamp}" + + # Deployment options + config['redirector_only'] = args.redirector_only + config['c2_only'] = args.c2_only + config['debug'] = args.debug + config['domain'] = args.domain or vars_data.get('domain', 'example.com') + config['letsencrypt_email'] = args.letsencrypt_email or vars_data.get('letsencrypt_email', f"admin@{config['domain']}") + + # OPSEC settings + config['disable_history'] = args.disable_history if args.disable_history is not None else vars_data.get('disable_history', True) + config['secure_memory'] = args.secure_memory if args.secure_memory is not None else vars_data.get('secure_memory', True) + config['zero_logs'] = args.zero_logs if args.zero_logs is not None else vars_data.get('zero_logs', True) + + # Other settings from vars_data + config['gophish_admin_port'] = vars_data.get('gophish_admin_port', str(random.randint(2000, 9000))) + config['smtp_auth_user'] = vars_data.get('smtp_auth_user', f"user{random.randint(1000, 9999)}") + config['smtp_auth_pass'] = vars_data.get('smtp_auth_pass', ''.join(random.choices(string.ascii_letters + string.digits, k=20))) + + # SSH after deploy + config['ssh_after_deploy'] = args.ssh_after_deploy # Validate deployment mode - if config['redirector_only'] and config['c2_only']: + if config.get('redirector_only') and config.get('c2_only'): logging.error("Cannot specify both --redirector-only and --c2-only") return @@ -552,6 +816,11 @@ def main(): else: logging.info(f" {key}: {value}") + # Check if this is a teardown operation + if args.teardown: + teardown_infrastructure(config) + return + # Run deployment try: success = deploy_infrastructure(config) @@ -560,7 +829,7 @@ def main(): logging.info("Deployment completed successfully!") # SSH into instance if requested - if args.ssh_after_deploy: + if config.get('ssh_after_deploy'): ssh_to_instance(config) else: logging.error("Deployment failed!")