From a7490bd655fa415cefefdb6dd077e9635a00f970 Mon Sep 17 00:00:00 2001 From: n0mad1k Date: Fri, 11 Apr 2025 09:56:44 -0400 Subject: [PATCH] working on it --- Linode/c2-deploy.yaml | 22 +- Linode/c2.yml | 34 ++- Linode/cleanup.yml | 109 ++----- Linode/configure-c2.yml | 486 ++++++++++++++++++++++++++++++ Linode/configure-redirector.yml | 218 ++++++++++++++ Linode/initial-infrastructure.yml | 85 ++++++ Linode/redirector.yml | 34 ++- deploy.py | 203 +++++-------- tasks/cleanup_confirmation.yml | 25 ++ 9 files changed, 983 insertions(+), 233 deletions(-) create mode 100644 Linode/configure-c2.yml create mode 100644 Linode/configure-redirector.yml create mode 100644 Linode/initial-infrastructure.yml create mode 100644 tasks/cleanup_confirmation.yml diff --git a/Linode/c2-deploy.yaml b/Linode/c2-deploy.yaml index 1a027e0..e215544 100644 --- a/Linode/c2-deploy.yaml +++ b/Linode/c2-deploy.yaml @@ -60,13 +60,21 @@ redirector_ip: "{{ redirector_instance.instance.ipv4[0] }}" redirector_instance_id: "{{ redirector_instance.instance.id }}" - - name: Wait for redirector SSH to be available - wait_for: - host: "{{ redirector_ip }}" - port: 22 - delay: 30 - timeout: 300 - state: started + - name: Enhanced wait for SSH with retry mechanism + block: + - name: Initial wait for instance to become reachable + wait_for: + host: "{{ linode_instance.instance.ipv4[0] }}" + port: 22 + delay: 60 # Increased initial delay + timeout: 180 + state: started + register: initial_wait + + - name: Additional pause to ensure SSH is fully initialized + pause: + seconds: 120 + when: initial_wait is succeeded - name: Add redirector to inventory add_host: diff --git a/Linode/c2.yml b/Linode/c2.yml index eb4e956..c63e091 100644 --- a/Linode/c2.yml +++ b/Linode/c2.yml @@ -42,13 +42,35 @@ c2_ip: "{{ c2_instance.instance.ipv4[0] }}" c2_instance_id: "{{ c2_instance.instance.id }}" + # Enhanced SSH wait task for Linode/c2.yml - name: Wait for C2 SSH to be available - wait_for: - host: "{{ c2_ip }}" - port: 22 - delay: 30 - timeout: 300 - state: started + block: + - name: Initial wait for port to be open + wait_for: + host: "{{ c2_instance.instance.ipv4[0] }}" + port: 22 + delay: 60 + timeout: 180 + state: started + + - name: Additional pause for SSH initialization + pause: + seconds: 60 + + - name: Test SSH connection + command: > + ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10 + -i {{ ssh_key_path | replace('.pub', '') }} root@{{ c2_instance.instance.ipv4[0] }} echo "SSH Ready" + register: ssh_test + retries: 5 + delay: 20 + until: ssh_test.rc == 0 + ignore_errors: yes + + - name: Fail if SSH test unsuccessful + fail: + msg: "Could not connect to C2 server via SSH after multiple attempts" + when: ssh_test.rc != 0 - name: Add C2 to inventory add_host: diff --git a/Linode/cleanup.yml b/Linode/cleanup.yml index d904384..37460e7 100644 --- a/Linode/cleanup.yml +++ b/Linode/cleanup.yml @@ -1,7 +1,5 @@ +# Linode/cleanup.yml --- -# Linode Cleanup Playbook -# Used to tear down Linode infrastructure - - name: Clean up Linode infrastructure hosts: localhost connection: local @@ -20,94 +18,27 @@ - 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 + - name: Cleanup resources notice + debug: + msg: | + =============================================== + CLEANUP CONFIRMATION REQUIRED + =============================================== + The following resources will be PERMANENTLY DELETED: + {% if cleanup_redirector %} + - Redirector instance: {{ redirector_name }} + {% endif %} + {% if cleanup_c2 %} + - C2 instance: {{ c2_name }} + {% endif %} + when: confirm_cleanup | bool + + - name: Confirm cleanup pause: - prompt: "WARNING: This will permanently delete all infrastructure. Type 'yes' to continue" + prompt: "Type 'yes' to confirm deletion or 'no' to abort" register: confirmation - when: confirm_cleanup + when: confirm_cleanup | bool - 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 + when: confirm_cleanup | bool and confirmation.user_input != 'yes' \ No newline at end of file diff --git a/Linode/configure-c2.yml b/Linode/configure-c2.yml new file mode 100644 index 0000000..1aaa4d2 --- /dev/null +++ b/Linode/configure-c2.yml @@ -0,0 +1,486 @@ +--- +# Linode/configure-c2.yml +# This playbook handles only the configuration of an already-created C2 server + +- name: Configure Linode C2 server + hosts: c2 + gather_facts: false + become: true + vars_files: + - vars.yaml + vars: + # Default values if not provided + domain: "{{ domain | default('example.com') }}" + c2_subdomain: "{{ c2_subdomain | default('mail') }}" + mail_hostname: "{{ mail_hostname | default('mail.' + domain) }}" + gophish_admin_port: "{{ gophish_admin_port | default('8090') }}" + smtp_auth_user: "{{ smtp_auth_user | default('phishuser') }}" + smtp_auth_pass: "{{ smtp_auth_pass | default(lookup('password', '/dev/null length=16 chars=ascii_letters,digits')) }}" + + tasks: + # Retry logic for SSH connection + - name: Wait for SSH connection to stabilize + wait_for_connection: + delay: 10 + timeout: 300 + register: wait_result + ignore_errors: true + retries: 5 + delay: 30 + until: wait_result is success + + - name: Gather facts after ensuring SSH connection + setup: + register: setup_result + ignore_errors: true + retries: 5 + delay: 30 + until: setup_result is success + + - name: Wait for apt lock to be released + shell: | + while lsof /var/lib/dpkg/lock-frontend >/dev/null 2>&1; do + echo "waiting for apt lock to be released..." + sleep 5 + done + changed_when: false + + - name: Disable root password authentication for SSH immediately + lineinfile: + path: /etc/ssh/sshd_config + regexp: '^#?PasswordAuthentication' + line: 'PasswordAuthentication no' + state: present + register: ssh_config_result + ignore_errors: true + retries: 3 + delay: 10 + until: ssh_config_result is success + + - name: Restart SSH service to apply changes + service: + name: ssh + state: restarted + register: ssh_restart_result + ignore_errors: true + retries: 3 + delay: 10 + until: ssh_restart_result is success + + - name: Update apt cache + apt: + update_cache: yes + register: apt_update_result + ignore_errors: true + retries: 5 + delay: 10 + until: apt_update_result is success + + - name: Set a custom MOTD + template: + src: "templates/motd-linode.j2" + dest: /etc/motd + owner: root + group: root + mode: '0644' + vars: + letsencrypt_email: "{{ letsencrypt_email | default('admin@' + domain) }}" + mail_hostname: "{{ mail_hostname }}" + domain: "{{ domain }}" + gophish_admin_port: "{{ gophish_admin_port }}" + + - name: Hush Default Login Message + shell: | + rm -rf '/usr/bin/kali-motd' + changed_when: false + ignore_errors: true + + - name: Install base utilities and tools via apt + apt: + name: + - git + - wget + - curl + - unzip + - python3-pip + - python3-virtualenv + - 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 + register: apt_install_result + ignore_errors: true + retries: 3 + delay: 10 + until: apt_install_result is success + + # Create Tools directory and ensure proper ownership + - name: Create Tools directory + file: + path: ~/Tools + state: directory + mode: '0755' + ignore_errors: true + + - name: Ensure pipx path is configured + shell: | + pipx ensurepath + args: + executable: /bin/bash + ignore_errors: true + + - 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 + args: + executable: /bin/bash + ignore_errors: true + + - name: Download Kerbrute + 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 + creates: ~/Tools/Kerbrute/kerbrute + ignore_errors: true + + - name: Clone SharpCollection nightly builds + git: + repo: https://github.com/Flangvik/SharpCollection.git + dest: ~/Tools/SharpCollection + version: master + ignore_errors: true + + - name: Clone PEASS-ng + git: + repo: https://github.com/carlospolop/PEASS-ng.git + dest: ~/Tools/PEASS-ng + ignore_errors: true + + - name: Clone MailSniper + git: + repo: https://github.com/dafthack/MailSniper.git + dest: ~/Tools/MailSniper + ignore_errors: true + + - name: Clone Inveigh + git: + repo: https://github.com/Kevin-Robertson/Inveigh.git + dest: ~/Tools/Inveigh + ignore_errors: true + + - name: Install Sliver C2 server + shell: | + curl https://sliver.sh/install | bash + systemctl enable sliver + systemctl start sliver + ignore_errors: true + + - name: Install Metasploit Framework (Nightly Build) + 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 + ignore_errors: true + + - 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 ~/Tools/gophish.zip + unzip ~/Tools/gophish.zip -d ~/Tools/gophish + rm -rf ~/Tools/gophish.zip + chmod +x ~/Tools/gophish/gophish + args: + executable: /bin/bash + creates: ~/Tools/gophish/gophish + ignore_errors: true + + - name: Deploy Gophish config.json with custom admin port + template: + src: "templates/gophish-config.j2" + dest: ~/Tools/gophish/config.json + owner: root + group: root + mode: '0644' + vars: + gophish_admin_port: "{{ gophish_admin_port }}" + domain: "{{ domain }}" + ignore_errors: true + + # Mail server configuration + - name: Configure Postfix main.cf + lineinfile: + path: /etc/postfix/main.cf + regexp: "{{ item.regexp }}" + line: "{{ item.line }}" + with_items: + - { regexp: '^myhostname', line: "myhostname = {{ mail_hostname }}" } + - { 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" } + ignore_errors: 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" } + ignore_errors: true + + - name: Create DKIM directory + file: + path: /etc/opendkim/keys/{{ domain }} + state: directory + owner: opendkim + group: opendkim + mode: 0700 + ignore_errors: 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 + ignore_errors: true + + - name: Set permissions for DKIM keys + file: + path: /etc/opendkim/keys/{{ domain }}/mail.private + owner: opendkim + group: opendkim + mode: 0600 + ignore_errors: true + + - name: Configure OpenDKIM TrustedHosts + copy: + content: | + 127.0.0.1 + ::1 + localhost + {{ domain }} + dest: /etc/opendkim/TrustedHosts + owner: opendkim + group: opendkim + mode: 0644 + ignore_errors: 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 + ignore_errors: true + + # Dovecot and authentication configuration + - 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 + } + ignore_errors: true + + - name: Set Dovecot auth_mechanisms + lineinfile: + path: /etc/dovecot/conf.d/10-auth.conf + regexp: '^auth_mechanisms' + line: 'auth_mechanisms = plain login' + ignore_errors: true + + - name: Create Dovecot password file for SASL authentication + file: + path: /etc/dovecot/passwd + state: touch + mode: '0600' + owner: dovecot + group: dovecot + ignore_errors: true + + - name: Add SMTP auth user to Dovecot + lineinfile: + path: /etc/dovecot/passwd + line: "{{ smtp_auth_user }}:{{ smtp_auth_pass | password_hash('sha512_crypt') }}" + ignore_errors: 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' + ignore_errors: 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 + } + ignore_errors: true + + - name: Create vmail user/group + group: + name: vmail + gid: 5000 + state: present + ignore_errors: true + + - name: Create vmail user + user: + name: vmail + uid: 5000 + group: vmail + create_home: no + ignore_errors: true + + - name: Create vmail directory + file: + path: /var/vmail + state: directory + owner: vmail + group: vmail + mode: 0700 + ignore_errors: true + + - name: Install Let's Encrypt certificate if domain specified + shell: | + certbot certonly --standalone -d {{ c2_subdomain }}.{{ domain }} --non-interactive --agree-tos -m {{ letsencrypt_email | default('admin@' + domain) }} + certbot certonly --standalone -d {{ mail_hostname }} --non-interactive --agree-tos -m {{ letsencrypt_email | default('admin@' + domain) }} + args: + creates: /etc/letsencrypt/live/{{ domain }}/fullchain.pem + when: domain != "example.com" + ignore_errors: true + + - name: Restart Postfix + service: + name: postfix + state: restarted + ignore_errors: true + + - name: Restart Dovecot + service: + name: dovecot + state: restarted + ignore_errors: true + + - name: Create operational scripts directory + file: + path: /opt/c2 + state: directory + mode: '0700' + owner: root + group: root + ignore_errors: true + + - name: Copy clean-logs.sh script + copy: + src: "../files/clean-logs.sh" + dest: /opt/c2/clean-logs.sh + mode: '0700' + owner: root + group: root + ignore_errors: true + + - name: Copy secure-exit.sh script + copy: + src: "../files/secure-exit.sh" + dest: /opt/c2/secure-exit.sh + mode: '0700' + owner: root + group: root + ignore_errors: true + + - 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 | default(true) | bool + ignore_errors: true + + - name: Print deployment summary + debug: + msg: + - "C2 Server Configuration Complete!" + - "-----------------------------" + - "C2 Server IP: {{ ansible_host }}" + - "C2 Server Domain: {{ c2_subdomain }}.{{ domain }} (Update DNS A record)" + - "Mail Server Domain: {{ mail_hostname }} (Update DNS A record)" + - "GoPhish Admin Port: {{ gophish_admin_port }}" + - "SMTP Auth User: {{ smtp_auth_user }}" + - "SMTP Auth Password: {{ smtp_auth_pass }}" \ No newline at end of file diff --git a/Linode/configure-redirector.yml b/Linode/configure-redirector.yml new file mode 100644 index 0000000..4d7357c --- /dev/null +++ b/Linode/configure-redirector.yml @@ -0,0 +1,218 @@ +--- +# Linode/configure-redirector.yml +# This playbook handles only the configuration of an already-created redirector + +- name: Configure Linode redirector + hosts: redirector + gather_facts: false + become: true + vars_files: + - vars.yaml + vars: + # Default values if not provided + shell_handler_port: "{{ shell_handler_port | default(4000 + 60000 | random) }}" + domain: "{{ domain | default('example.com') }}" + redirector_subdomain: "{{ redirector_subdomain | default('cdn') }}" + + tasks: + # Retry logic for SSH connection + - name: Wait for SSH connection to stabilize + wait_for_connection: + delay: 10 + timeout: 300 + register: wait_result + ignore_errors: true + retries: 5 + delay: 30 + until: wait_result is success + + - name: Gather facts after ensuring SSH connection + setup: + register: setup_result + ignore_errors: true + retries: 5 + delay: 30 + until: setup_result is success + + - name: Wait for apt lock to be released + shell: | + while lsof /var/lib/dpkg/lock-frontend >/dev/null 2>&1; do + echo "waiting for apt lock to be released..." + sleep 5 + done + changed_when: false + + - name: Disable root password authentication for SSH immediately + lineinfile: + path: /etc/ssh/sshd_config + regexp: '^#?PasswordAuthentication' + line: 'PasswordAuthentication no' + state: present + register: ssh_config_result + ignore_errors: true + retries: 3 + delay: 10 + until: ssh_config_result is success + + - name: Restart SSH service to apply changes + service: + name: ssh + state: restarted + register: ssh_restart_result + ignore_errors: true + retries: 3 + delay: 10 + until: ssh_restart_result is success + + - name: Update apt cache + apt: + update_cache: yes + register: apt_update_result + ignore_errors: true + retries: 5 + delay: 10 + until: apt_update_result is success + + - name: Install nginx and other required packages + apt: + name: + - nginx + - certbot + - python3-certbot-nginx + - socat + - netcat-openbsd + - secure-delete + state: present + register: apt_install_result + ignore_errors: true + retries: 3 + delay: 10 + until: apt_install_result is success + + - 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: "../files/clean-logs.sh" + dest: /opt/c2/clean-logs.sh + mode: '0700' + owner: root + group: root + + - name: Copy shell handler script + copy: + src: "../files/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 | default("127.0.0.1") }}"' + + - 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: "../FlokiNET/templates/shell-handler.service.j2" + dest: /etc/systemd/system/shell-handler.service + mode: '0644' + owner: root + group: root + vars: + c2_ip: "{{ c2_ip | default('127.0.0.1') }}" + shell_handler_port: "{{ shell_handler_port }}" + + - name: Configure NGINX for zero-logging + template: + src: "../FlokiNET/templates/nginx.conf.j2" + dest: /etc/nginx/nginx.conf + mode: '0644' + owner: root + group: root + when: zero_logs | default(true) | bool + + - name: Configure NGINX for C2 redirection + template: + src: "../FlokiNET/templates/default-site.j2" + dest: /etc/nginx/sites-available/default + mode: '0644' + owner: root + group: root + vars: + domain: "{{ domain }}" + c2_host: "{{ c2_ip | default('127.0.0.1') }}" + + - name: Create legitimate-looking index.html + template: + src: "../FlokiNET/templates/index.html.j2" + dest: /var/www/html/index.html + mode: '0644' + owner: www-data + group: www-data + vars: + domain: "{{ domain }}" + redirector_subdomain: "{{ redirector_subdomain }}" + + - name: Start and enable shell handler service + systemd: + name: shell-handler + state: started + enabled: yes + daemon_reload: yes + register: shell_handler_result + ignore_errors: true + retries: 3 + delay: 10 + until: shell_handler_result is success + + - 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 | default(true) | bool + + - name: Install Let's Encrypt certificate if domain specified + shell: | + certbot --nginx -d {{ redirector_subdomain }}.{{ domain }} --non-interactive --agree-tos -m {{ letsencrypt_email | default('admin@' + domain) }} + args: + creates: /etc/letsencrypt/live/{{ redirector_subdomain }}.{{ domain }}/fullchain.pem + when: domain != "example.com" + ignore_errors: true + + - name: Restart NGINX + systemd: + name: nginx + state: restarted + register: nginx_restart_result + ignore_errors: true + retries: 3 + delay: 10 + until: nginx_restart_result is success + + - name: Print deployment summary + debug: + msg: + - "Redirector Configuration Complete!" + - "-------------------------------" + - "Redirector IP: {{ ansible_host }}" + - "Redirector Domain: {{ redirector_subdomain }}.{{ domain }} (Update DNS A record)" + - "Shell Handler Port: {{ shell_handler_port }}" \ No newline at end of file diff --git a/Linode/initial-infrastructure.yml b/Linode/initial-infrastructure.yml new file mode 100644 index 0000000..1290793 --- /dev/null +++ b/Linode/initial-infrastructure.yml @@ -0,0 +1,85 @@ +--- +# Linode/initial-infrastructure.yml +# This playbook only creates the Linode instances without trying to configure them +# This separation makes the deployment more reliable + +- name: Create Linode infrastructure + hosts: localhost + gather_facts: false + connection: local + vars_files: + - vars.yaml + vars: + # Default values if not provided + ssh_user: "{{ ssh_user | default('root') }}" + linode_region: "{{ linode_region | default(region_choices | random) }}" + plan: "{{ plan | default('g6-standard-2') }}" + image: "{{ image | default('linode/kali') }}" + + # Determine what to deploy based on configuration + deploy_redirector: "{{ not (c2_only | default(false)) }}" + deploy_c2: "{{ not (redirector_only | default(false)) }}" + + # Generate random names if not provided + redirector_name: "{{ redirector_name | default('srv-' + 100000000 | random | to_uuid | hash('md5') | truncate(8, True, '')) }}" + c2_name: "{{ c2_name | default('node-' + 100000000 | random | to_uuid | hash('md5') | truncate(8, 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: Create redirector Linode instance + community.general.linode_v4: + access_token: "{{ linode_token }}" + label: "{{ redirector_name }}" + type: "{{ plan }}" + region: "{{ linode_region }}" + image: "{{ image }}" + root_pass: "{{ lookup('password', '/dev/null length=24 chars=ascii_letters,digits') }}" + authorized_keys: + - "{{ lookup('file', ssh_key_path) }}" + state: present + register: redirector_instance + when: deploy_redirector + + - name: Set redirector_ip for later use + set_fact: + redirector_instance_id: "{{ redirector_instance.instance.id }}" + redirector_ip: "{{ redirector_instance.instance.ipv4[0] }}" + when: deploy_redirector and redirector_instance is defined + + - name: Create C2 Linode instance + community.general.linode_v4: + access_token: "{{ linode_token }}" + label: "{{ c2_name }}" + type: "{{ plan }}" + region: "{{ linode_region }}" + image: "{{ image }}" + root_pass: "{{ lookup('password', '/dev/null length=24 chars=ascii_letters,digits') }}" + authorized_keys: + - "{{ lookup('file', ssh_key_path) }}" + state: present + register: c2_instance + when: deploy_c2 + + - name: Set c2_ip for later use + set_fact: + c2_instance_id: "{{ c2_instance.instance.id }}" + c2_ip: "{{ c2_instance.instance.ipv4[0] }}" + when: deploy_c2 and c2_instance is defined + + - name: Display instance information + debug: + msg: + - "Linode instances created successfully!" + - "Waiting for instances to initialize..." + - "{{ 'Redirector IP: ' + redirector_ip if redirector_ip is defined else 'No redirector deployed' }}" + - "{{ 'C2 Server IP: ' + c2_ip if c2_ip is defined else 'No C2 server deployed' }}" + + - name: Wait for instances to initialize (30 seconds) + pause: + seconds: 30 + when: (deploy_redirector and redirector_instance is defined) or (deploy_c2 and c2_instance is defined) \ No newline at end of file diff --git a/Linode/redirector.yml b/Linode/redirector.yml index 6d53019..5f2f7f8 100644 --- a/Linode/redirector.yml +++ b/Linode/redirector.yml @@ -45,13 +45,35 @@ redirector_ip: "{{ redirector_instance.instance.ipv4[0] }}" redirector_instance_id: "{{ redirector_instance.instance.id }}" + # Enhanced SSH wait task for Linode/redirector.yml - name: Wait for redirector SSH to be available - wait_for: - host: "{{ redirector_ip }}" - port: 22 - delay: 30 - timeout: 300 - state: started + block: + - name: Initial wait for port to be open + wait_for: + host: "{{ redirector_instance.instance.ipv4[0] }}" + port: 22 + delay: 60 + timeout: 180 + state: started + + - name: Additional pause for SSH initialization + pause: + seconds: 60 + + - name: Test SSH connection + command: > + ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10 + -i {{ ssh_key_path | replace('.pub', '') }} root@{{ redirector_instance.instance.ipv4[0] }} echo "SSH Ready" + register: ssh_test + retries: 5 + delay: 20 + until: ssh_test.rc == 0 + ignore_errors: yes + + - name: Fail if SSH test unsuccessful + fail: + msg: "Could not connect to redirector via SSH after multiple attempts" + when: ssh_test.rc != 0 - name: Add redirector to inventory add_host: diff --git a/deploy.py b/deploy.py index 5f52d9b..ff31c5a 100644 --- a/deploy.py +++ b/deploy.py @@ -436,12 +436,22 @@ def select_random_region(config): if provider == "aws": region_choices = config.get("aws_region_choices", []) elif provider == "linode": + # Look for multiple possible key names in the Linode config region_choices = config.get("region_choices", []) + if not region_choices: + region_choices = config.get("linode_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}") + + # Fallback regions by provider if none found in config + if provider == "linode": + region_choices = ["us-east", "us-central", "eu-west", "ap-south"] + logging.info(f"Using fallback regions for Linode: {region_choices}") + + if not region_choices: return None # Select random region @@ -492,24 +502,9 @@ def create_inventory_file(config, deployment_type): def run_ansible_playbook(playbook, inventory, config, debug=False): """Run an Ansible playbook with the given inventory and config""" - # Check if playbook exists - if not os.path.exists(playbook): - logging.error(f"Playbook {playbook} not found") - return False, "", f"ERROR! the playbook: {playbook} could not be found" - # 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))} - - # Ensure ssh_user is defined to avoid template recursion - if 'ssh_user' not in extra_vars: - provider = config.get('provider') - if provider and provider in DEFAULT_SSH_USER: - extra_vars['ssh_user'] = DEFAULT_SSH_USER[provider] - else: - extra_vars['ssh_user'] = "root" - - # Convert to JSON extra_vars_json = json.dumps(extra_vars) # Build command @@ -522,18 +517,16 @@ def run_ansible_playbook(playbook, inventory, config, debug=False): # Add verbosity if debug mode is enabled if debug: - cmd.append("-vvvv") + cmd.append("-vvv") else: - # Add some verbosity even in non-debug mode to capture errors + # Always add some verbosity for basic output cmd.append("-v") - # Add environment variables to avoid hangs on confirmations - cmd_env = os.environ.copy() - cmd_env["ANSIBLE_HOST_KEY_CHECKING"] = "False" - cmd_env["ANSIBLE_NOCOLOR"] = "True" - # Log the command - logging.debug(f"Running Ansible command: {' '.join(cmd)}") + if debug: + logging.debug(f"Running Ansible command: {' '.join(cmd)}") + else: + logging.info(f"Running Ansible playbook: {playbook}") # Run the command try: @@ -542,28 +535,20 @@ def run_ansible_playbook(playbook, inventory, config, debug=False): check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - text=True, - env=cmd_env + text=True ) logging.info(f"Playbook {playbook} executed successfully") - # Log the stdout/stderr, but truncate if too long - if len(result.stdout) > 0: - logging.debug(f"Command stdout: {result.stdout}") - if len(result.stderr) > 0: - logging.debug(f"Command stderr: {result.stderr}") return True, result.stdout, result.stderr except subprocess.CalledProcessError as e: - # More detailed error logging logging.error(f"Playbook {playbook} failed with exit code {e.returncode}") - logging.error(f"Command stdout: {e.stdout}") - logging.error(f"Command stderr: {e.stderr}") + if debug: + logging.error(f"Command stdout: {e.stdout}") + logging.error(f"Command stderr: {e.stderr}") return False, e.stdout, e.stderr def deploy_infrastructure(config): """Deploy infrastructure based on provider and configuration""" provider = config['provider'] - provider_dir = PROVIDER_DIRS.get(provider, provider.upper()) - logging.info(f"Deploying {provider} infrastructure...") # Set provider-specific environment variables @@ -577,58 +562,68 @@ def deploy_infrastructure(config): 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) + if not config.get('region'): + config['region'] = select_random_region(config) - # Determine playbook path based on deployment mode - if config.get('deploy_tracker'): - playbook = f"{provider_dir}/tracker.yml" - deployment_type = "tracker" - elif config.get('redirector_only'): + # Determine which components to deploy + success = True + + # Use the correct case for provider directory + provider_dir = provider.capitalize() if provider != "aws" else provider.upper() + + # Redirector deployment + if not config.get('c2_only'): playbook = f"{provider_dir}/redirector.yml" - deployment_type = "redirector" - elif config.get('c2_only'): - playbook = f"{provider_dir}/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_dir}/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( + inventory_path = create_inventory_file(config, "local") + + logging.info(f"Deploying redirector using {playbook}") + redirector_success, stdout, stderr = run_ansible_playbook( playbook, inventory_path, config, config.get('debug', False) ) - # Clean up inventory file + # Log Ansible output + if config.get('debug', False): + logging.debug(f"Ansible stdout: {stdout}") + if stderr: + logging.debug(f"Ansible stderr: {stderr}") + else: + # Always log at least a summary even without debug mode + logging.info(f"Redirector deployment {'succeeded' if redirector_success else 'failed'}") + if os.path.exists(inventory_path): os.unlink(inventory_path) + + if not redirector_success: + logging.error("Redirector deployment failed") + return False + + # C2 deployment + if not config.get('redirector_only'): + playbook = f"{provider_dir}/c2.yml" + inventory_path = create_inventory_file(config, "local") - return success - except Exception as e: - logging.error(f"Deployment failed: {e}") + logging.info(f"Deploying C2 server using {playbook}") + c2_success, stdout, stderr = run_ansible_playbook( + playbook, inventory_path, config, config.get('debug', False) + ) + + # Log Ansible output + if config.get('debug', False): + logging.debug(f"Ansible stdout: {stdout}") + if stderr: + logging.debug(f"Ansible stderr: {stderr}") + else: + # Always log at least a summary even without debug mode + logging.info(f"C2 server deployment {'succeeded' if c2_success else 'failed'}") - # Clean up inventory file if os.path.exists(inventory_path): os.unlink(inventory_path) - - return False + + if not c2_success: + logging.error("C2 server deployment failed") + return False + + return True def deploy_flokinet_redirector(config): """Deploy FlokiNET redirector separately""" @@ -744,6 +739,8 @@ def run_tests(config): else: logging.warning(f"No tests playbook found at {playbook}") + + def ssh_to_instance(config): """SSH into the deployed instance""" logging.info("Connecting to instance via SSH...") @@ -815,62 +812,18 @@ def ssh_to_instance(config): def cleanup_resources(config): """Clean up resources if deployment fails""" provider = config.get('provider') - if not provider: - logging.warning("No provider specified for cleanup") - return - - provider_dir = PROVIDER_DIRS.get(provider, provider.upper()) logging.info(f"Cleaning up {provider} resources...") + # Use the correct case for provider directory + provider_dir = provider.capitalize() if provider != "aws" else provider.upper() + # Use Ansible for cleanup playbook = f"{provider_dir}/cleanup.yml" if os.path.exists(playbook): logging.info(f"Running cleanup playbook: {playbook}") - - # Create a new config with string values to avoid template recursion issues - safe_config = config.copy() - # Set confirm_cleanup to "false" as a string value - safe_config['confirm_cleanup'] = "false" - # Set ssh_user explicitly if it's missing to avoid template recursion - if 'ssh_user' not in safe_config: - if provider in DEFAULT_SSH_USER: - safe_config['ssh_user'] = DEFAULT_SSH_USER[provider] - else: - safe_config['ssh_user'] = "root" - - inventory_path = create_inventory_file(safe_config, "local") + inventory_path = create_inventory_file(config, "local") try: - # Run with non-interactive mode to bypass confirmation prompts - cmd_env = os.environ.copy() - cmd_env["ANSIBLE_HOST_KEY_CHECKING"] = "False" - cmd_env["ANSIBLE_NOCOLOR"] = "True" - - # Build custom command with additional environment variables - playbook_cmd = [ - "ansible-playbook", - "-i", inventory_path, - playbook, - "-e", json.dumps(safe_config), - "--extra-vars", "confirm_cleanup=false", - "-v" - ] - - # Run command with environment variables - result = subprocess.run( - playbook_cmd, - env=cmd_env, - check=False, # Don't check return code to allow for partial cleanup - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True - ) - - # Log output regardless of success/failure - if result.stdout: - logging.info(f"Cleanup playbook output: {result.stdout}") - if result.stderr: - logging.warning(f"Cleanup playbook errors: {result.stderr}") - + run_ansible_playbook(playbook, inventory_path, config) except Exception as e: logging.error(f"Cleanup playbook failed: {e}") finally: diff --git a/tasks/cleanup_confirmation.yml b/tasks/cleanup_confirmation.yml new file mode 100644 index 0000000..22763f7 --- /dev/null +++ b/tasks/cleanup_confirmation.yml @@ -0,0 +1,25 @@ +# tasks/cleanup_confirmation.yml - Common task file for cleanup confirmation +- name: Show cleanup information + debug: + msg: | + ************************************************ + * CLEANUP OPERATION * + ************************************************ + The following resources will be DELETED PERMANENTLY: + {% if cleanup_redirector and redirector_name is defined %} + - Redirector: {{ redirector_name }} ({{ redirector_ip | default('IP unknown') }}) + {% endif %} + {% if cleanup_c2 and c2_name is defined %} + - C2 Server: {{ c2_name }} ({{ c2_ip | default('IP unknown') }}) + {% endif %} + when: confirm_cleanup | bool + +- name: Confirm cleanup operation + pause: + prompt: "\n>>> Type 'yes' to confirm deletion or press Ctrl+C to abort <<<" + register: confirmation + when: confirm_cleanup | bool + +- name: Skip cleanup if not confirmed + meta: end_play + when: confirm_cleanup | bool and confirmation.user_input != 'yes' \ No newline at end of file