From 4e08f52d77d1cbd201ae9b84225be21888a00f1a Mon Sep 17 00:00:00 2001 From: n0mad1k Date: Fri, 11 Apr 2025 09:04:25 -0400 Subject: [PATCH] sigh --- FLOKINET/templates/motd-aws.j2 | 43 -- FlokiNET/c2-deploy.yaml | 634 ++++++++++++++++++ {FLOKINET => FlokiNET}/c2.yml | 0 {FLOKINET => FlokiNET}/cleanup.yml | 0 FlokiNET/flokinet-security.yml | 167 +++++ {FLOKINET => FlokiNET}/provision.yml | 0 {FLOKINET => FlokiNET}/redirector.yml | 0 .../templates/default-site.j2 | 0 .../templates/gophish-config.j2 | 0 .../templates/index.html.j2 | 0 FlokiNET/templates/motd-aws.j2 | 59 ++ .../templates/nginx.conf.j2 | 0 .../templates/proxychains.conf.j2 | 0 .../templates/resolv.conf.j2 | 0 .../templates/secure-ssh.sh.j2 | 0 .../templates/shell-handler.service.j2 | 0 .../templates/sliver-server.service.j2 | 0 {FLOKINET => FlokiNET}/templates/torrc.j2 | 0 {LINODE => Linode}/c2-deploy.yaml | 0 {LINODE => Linode}/c2-vars-template.yaml | 0 {LINODE => Linode}/c2.yml | 0 {LINODE => Linode}/cleanup.yml | 0 {LINODE => Linode}/redirector.yml | 6 +- .../templates/gophish-config.j2 | 0 {LINODE => Linode}/templates/motd-linode.j2 | 0 deploy.py | 510 ++++++++++++-- requirements.txt | 2 +- 27 files changed, 1307 insertions(+), 114 deletions(-) delete mode 100644 FLOKINET/templates/motd-aws.j2 create mode 100644 FlokiNET/c2-deploy.yaml rename {FLOKINET => FlokiNET}/c2.yml (100%) rename {FLOKINET => FlokiNET}/cleanup.yml (100%) create mode 100644 FlokiNET/flokinet-security.yml rename {FLOKINET => FlokiNET}/provision.yml (100%) rename {FLOKINET => FlokiNET}/redirector.yml (100%) rename {FLOKINET => FlokiNET}/templates/default-site.j2 (100%) rename {FLOKINET => FlokiNET}/templates/gophish-config.j2 (100%) rename {FLOKINET => FlokiNET}/templates/index.html.j2 (100%) create mode 100644 FlokiNET/templates/motd-aws.j2 rename {FLOKINET => FlokiNET}/templates/nginx.conf.j2 (100%) rename {FLOKINET => FlokiNET}/templates/proxychains.conf.j2 (100%) rename {FLOKINET => FlokiNET}/templates/resolv.conf.j2 (100%) rename {FLOKINET => FlokiNET}/templates/secure-ssh.sh.j2 (100%) rename {FLOKINET => FlokiNET}/templates/shell-handler.service.j2 (100%) rename {FLOKINET => FlokiNET}/templates/sliver-server.service.j2 (100%) rename {FLOKINET => FlokiNET}/templates/torrc.j2 (100%) rename {LINODE => Linode}/c2-deploy.yaml (100%) rename {LINODE => Linode}/c2-vars-template.yaml (100%) rename {LINODE => Linode}/c2.yml (100%) rename {LINODE => Linode}/cleanup.yml (100%) rename {LINODE => Linode}/redirector.yml (94%) rename {LINODE => Linode}/templates/gophish-config.j2 (100%) rename {LINODE => Linode}/templates/motd-linode.j2 (100%) diff --git a/FLOKINET/templates/motd-aws.j2 b/FLOKINET/templates/motd-aws.j2 deleted file mode 100644 index b8c3efe..0000000 --- a/FLOKINET/templates/motd-aws.j2 +++ /dev/null @@ -1,43 +0,0 @@ -Welcome to your new C2 Server! - -The following tools and utilities have been installed: - -Apt-Installed Tools: --------------------- -- git, wget, curl, unzip -- python3-pip, python3-venv, pipx -- tmux, nmap, tcpdump, hydra, john, hashcat -- sqlmap, gobuster, dirb, enum4linux, dnsenum, seclists, responder -- golang, proxychains, tor, crackmapexec, jq, unzip -- postfix, certbot, opendkim, opendkim-tools - -Pipx-Installed Tools: ---------------------- -- NetExec: git+https://github.com/Pennyw0rth/NetExec -- TREVORspray: git+https://github.com/blacklanternsecurity/TREVORspray -- impacket: (various network protocols and service tools) - -Custom Tools Installed in ~/Tools: ----------------------------------- -- SharpCollection: /home/kali/Tools/SharpCollection -- Kerbrute: /home/kali/Tools/Kerbrute -- PEASS-ng: /home/kali/Tools/PEASS-ng -- MailSniper: /home/kali/Tools/MailSniper -- Inveigh: /home/kali/Tools/Inveigh -- Gophish: /home/kali/Tools/gophish (unzipped here) - -Other Installed C2 Frameworks: ------------------------------- -- Metasploit Framework: system installed (run 'msfconsole') -- Sliver C2: system installed (run 'sliver') - -Also, remember that many reconnaissance and attack tools are now available system-wide due to the apt and pipx installations. - -Once your DNS record points to this server’s public IP, you can obtain a Let’s Encrypt certificate by running: - - sudo certbot certonly --non-interactive --agree-tos --email {{ letsencrypt_email }} --standalone -d {{ mail_hostname }} - sudo certbot certonly --non-interactive --agree-tos --email {{ letsencrypt_email }} --standalone -d {{ domain }} - -**IMPORTANT:** - -Don’t forget to set up a DMARC record for your domain. Update your DNS provider’s dashboard (e.g., GoDaddy) to add a TXT record named `_dmarc` with a suitable DMARC policy (e.g., `v=DMARC1; p=reject; rua=mailto:admin@{{ domain }}; ruf=mailto:admin@{{ domain }}; pct=100`). This ensures better email deliverability and security for your domain. diff --git a/FlokiNET/c2-deploy.yaml b/FlokiNET/c2-deploy.yaml new file mode 100644 index 0000000..0ba976f --- /dev/null +++ b/FlokiNET/c2-deploy.yaml @@ -0,0 +1,634 @@ +--- +# FlokiNET full deployment playbook (C2 + Redirector) + +- name: Prepare FlokiNET infrastructure deployment + 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') }}" + c2_subdomain: "{{ c2_subdomain | default('mail') }}" + + tasks: + - name: Validate required FlokiNET configuration + assert: + that: + - redirector_ip is defined and redirector_ip != "" + - c2_ip is defined and c2_ip != "" + fail_msg: "FlokiNET requires both redirector_ip and c2_ip. Set these values in vars.yaml or via command line arguments." + + - 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: Add C2 server 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 redirector + wait_for: + host: "{{ redirector_ip }}" + port: "{{ ssh_port | default(22) }}" + delay: 10 + timeout: 60 + state: started + ignore_errors: true + + - name: Verify SSH connection to C2 server + wait_for: + host: "{{ c2_ip }}" + port: "{{ ssh_port | default(22) }}" + delay: 10 + timeout: 60 + state: started + ignore_errors: true + +- name: Configure FlokiNET redirector + hosts: redirectors + become: true + gather_facts: true + vars_files: + - vars.yaml + tasks: + - name: Wait for apt to be available + apt: + update_cache: yes + register: apt_result + until: apt_result is success + retries: 5 + delay: 10 + + - name: Set hostname + hostname: + name: "redirector" + + - name: Update apt cache + apt: + update_cache: yes + + - name: Upgrade all packages + apt: + upgrade: dist + + - name: Install base utilities and tools via apt + apt: + name: + - git + - wget + - curl + - unzip + - python3-pip + - python3-virtualenv + - tmux + - pipx + - nmap + - tcpdump + - nginx + - certbot + - python3-certbot-nginx + - socat + - netcat-openbsd + - secure-delete + state: present + + - name: Set a custom MOTD + template: + src: motd-flokinet.j2 + dest: /etc/motd + owner: root + group: root + mode: '0644' + vars: + letsencrypt_email: "{{ letsencrypt_email }}" + domain: "{{ domain }}" + redirector_subdomain: "{{ redirector_subdomain }}" + + - 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 secure-exit.sh script + copy: + src: "../files/secure-exit.sh" + dest: /opt/c2/secure-exit.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 }}"' + + - 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 + + - name: FlokiNET-specific security configurations + include_tasks: flokinet-security.yml + when: enable_hardened_security | default(true) + +- name: Configure FlokiNET C2 server + hosts: c2servers + become: true + gather_facts: true + vars_files: + - vars.yaml + tasks: + - name: Wait for apt to be available + apt: + update_cache: yes + register: apt_result + until: apt_result is success + retries: 5 + delay: 10 + + - name: Set hostname + hostname: + name: "c2" + + - name: Update apt cache + apt: + update_cache: yes + + - name: Upgrade all packages + apt: + upgrade: dist + + - name: Set a custom MOTD + template: + src: motd-flokinet.j2 + dest: /etc/motd + owner: root + group: root + mode: '0644' + vars: + letsencrypt_email: "{{ letsencrypt_email }}" + domain: "{{ domain }}" + c2_subdomain: "{{ c2_subdomain }}" + + - 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 + + - name: Create Tools directory + file: + path: /root/Tools + state: directory + owner: root + group: root + mode: '0755' + + - name: Ensure pipx path is configured + shell: | + pipx ensurepath + 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 + args: + executable: /bin/bash + + - 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: /root/Tools/Kerbrute/kerbrute + + - name: Clone SharpCollection nightly builds + git: + repo: https://github.com/Flangvik/SharpCollection.git + dest: ~/Tools/SharpCollection + version: master + ignore_errors: yes + + - name: Clone PEASS-ng + git: + repo: https://github.com/carlospolop/PEASS-ng.git + dest: ~/Tools/PEASS-ng + ignore_errors: yes + + - name: Clone MailSniper + git: + repo: https://github.com/dafthack/MailSniper.git + dest: ~/Tools/MailSniper + ignore_errors: yes + + - name: Clone Inveigh + git: + repo: https://github.com/Kevin-Robertson/Inveigh.git + dest: ~/Tools/Inveigh + ignore_errors: yes + + - name: Install Sliver C2 server + shell: | + curl https://sliver.sh/install | bash + systemctl enable sliver + systemctl start sliver + + - 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 + 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 ~/Tools/gophish.zip + unzip ~/Tools/gophish.zip -d ~/Tools/gophish + rm -rf ~/Tools/gophish.zip + chmod +x ~/Tools/gophish/gophish + args: + creates: /root/Tools/gophish/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 + + - 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: Create directories for operational scripts + file: + path: "{{ item }}" + state: directory + mode: '0700' + owner: root + group: root + with_items: + - /opt/c2 + - /opt/beacons + + - 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 secure-exit.sh script + copy: + src: "../files/secure-exit.sh" + dest: /opt/c2/secure-exit.sh + mode: '0700' + owner: root + group: root + + - name: Copy serve-beacons.sh script + copy: + src: "../files/serve-beacons.sh" + dest: /opt/c2/serve-beacons.sh + mode: '0700' + owner: root + group: root + + - name: Configure serve-beacons.sh with C2 IP + replace: + path: /opt/c2/serve-beacons.sh + regexp: 'C2_HOST=.*' + replace: 'C2_HOST="{{ c2_ip }}"' + + - 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: Restart Postfix + service: + name: postfix + state: restarted + + - name: Restart Dovecot + service: + name: dovecot + state: restarted + + - name: FlokiNET-specific security configurations + include_tasks: flokinet-security.yml + when: enable_hardened_security | default(true) + + - name: Print deployment summary + debug: + msg: + - "FlokiNET Deployment Complete!" + - "-------------------------------" + - "C2 Server Domain: {{ c2_subdomain }}.{{ domain }} (Update DNS A record)" + - "Redirector Domain: {{ redirector_subdomain }}.{{ domain }} (Update DNS A record)" + - "Shell Handler Port: {{ shell_handler_port }}" + - "GoPhish Admin Port: {{ gophish_admin_port }}" + when: not disable_summary | default(false) \ No newline at end of file diff --git a/FLOKINET/c2.yml b/FlokiNET/c2.yml similarity index 100% rename from FLOKINET/c2.yml rename to FlokiNET/c2.yml diff --git a/FLOKINET/cleanup.yml b/FlokiNET/cleanup.yml similarity index 100% rename from FLOKINET/cleanup.yml rename to FlokiNET/cleanup.yml diff --git a/FlokiNET/flokinet-security.yml b/FlokiNET/flokinet-security.yml new file mode 100644 index 0000000..e29052b --- /dev/null +++ b/FlokiNET/flokinet-security.yml @@ -0,0 +1,167 @@ +--- +# FlokiNET-specific security tasks +# Enhanced security features for FlokiNET servers + +- name: Configure FlokiNET networking for maximum anonymity + lineinfile: + path: /etc/sysctl.conf + regexp: "{{ item.regexp }}" + line: "{{ item.line }}" + state: present + with_items: + - { regexp: '^net.ipv4.tcp_timestamps', line: 'net.ipv4.tcp_timestamps = 0' } + - { regexp: '^net.ipv4.tcp_syncookies', line: 'net.ipv4.tcp_syncookies = 1' } + - { regexp: '^net.ipv4.conf.all.accept_redirects', line: 'net.ipv4.conf.all.accept_redirects = 0' } + - { regexp: '^net.ipv6.conf.all.accept_redirects', line: 'net.ipv6.conf.all.accept_redirects = 0' } + - { regexp: '^net.ipv4.conf.all.send_redirects', line: 'net.ipv4.conf.all.send_redirects = 0' } + - { regexp: '^net.ipv4.conf.all.accept_source_route', line: 'net.ipv4.conf.all.accept_source_route = 0' } + - { regexp: '^net.ipv6.conf.all.accept_source_route', line: 'net.ipv6.conf.all.accept_source_route = 0' } + - { regexp: '^net.ipv4.conf.all.log_martians', line: 'net.ipv4.conf.all.log_martians = 1' } + notify: Apply sysctl settings + +- name: Install Tor for anonymous outbound connections + apt: + name: + - tor + - torsocks + - obfs4proxy + state: present + update_cache: yes + register: tor_installed + when: use_tor_proxy | default(true) + +- name: Create Tor configuration directory + file: + path: /etc/tor + state: directory + mode: '0755' + when: use_tor_proxy | default(true) and tor_installed is succeeded + +- name: Configure Tor for hardened privacy settings + template: + src: torrc.j2 + dest: /etc/tor/torrc + owner: root + group: root + mode: '0644' + notify: Restart Tor service + when: use_tor_proxy | default(true) and tor_installed is succeeded + +- name: Configure ProxyChains for routing through Tor + template: + src: proxychains.conf.j2 + dest: /etc/proxychains.conf + owner: root + group: root + mode: '0644' + when: use_tor_proxy | default(true) and tor_installed is succeeded + +- name: Install iptables-persistent for firewall persistence + apt: + name: iptables-persistent + state: present + update_cache: yes + +- name: Configure hardened iptables rules for FlokiNET + template: + src: iptables-rules.j2 + dest: /etc/iptables/rules.v4 + owner: root + group: root + mode: '0644' + notify: Apply iptables rules + +- name: Create SSH security hardening script + template: + src: secure-ssh.sh.j2 + dest: /opt/c2/secure-ssh.sh + owner: root + group: root + mode: '0700' + when: use_hardened_ssh | default(true) + +- name: Run SSH security hardening script + command: /opt/c2/secure-ssh.sh + args: + creates: /opt/c2/.ssh_hardened + when: use_hardened_ssh | default(true) + +- name: Install timezone data + apt: + name: tzdata + state: present + +- name: Set timezone to UTC + timezone: + name: UTC + +- name: Configure DNS to use secure DNS servers + template: + src: resolv.conf.j2 + dest: /etc/resolv.conf + owner: root + group: root + mode: '0644' + when: enable_dns_encryption | default(true) + +- name: Create directory for DNS cache configuration + file: + path: /etc/systemd/resolved.conf.d + state: directory + mode: '0755' + when: enable_dns_encryption | default(true) + +- name: Configure DNS caching with DNSCrypt + template: + src: dnscrypt.conf.j2 + dest: /etc/systemd/resolved.conf.d/dnscrypt.conf + owner: root + group: root + mode: '0644' + notify: Restart systemd-resolved + when: enable_dns_encryption | default(true) + +# Configure memory security if enabled +- name: Set memory security measures + lineinfile: + path: /etc/sysctl.conf + line: "{{ item }}" + state: present + with_items: + - "vm.swappiness=0" + - "kernel.randomize_va_space=2" + when: secure_memory | default(true) + notify: Apply sysctl settings + +# Configure history settings if disabled +- name: Disable system history + lineinfile: + path: "{{ item.file }}" + line: "{{ item.line }}" + state: present + create: yes + with_nested: + - [{ file: '/etc/profile' }, { file: '/root/.bashrc' }] + - [{ line: 'export HISTFILESIZE=0' }, { line: 'export HISTSIZE=0' }, { line: 'unset HISTFILE' }] + when: disable_history | default(true) + +handlers: + - name: Apply sysctl settings + command: sysctl -p + + - name: Restart Tor service + service: + name: tor + state: restarted + enabled: yes + when: use_tor_proxy | default(true) + + - name: Apply iptables rules + command: iptables-restore < /etc/iptables/rules.v4 + + - name: Restart systemd-resolved + service: + name: systemd-resolved + state: restarted + enabled: yes + when: enable_dns_encryption | default(true) \ No newline at end of file diff --git a/FLOKINET/provision.yml b/FlokiNET/provision.yml similarity index 100% rename from FLOKINET/provision.yml rename to FlokiNET/provision.yml diff --git a/FLOKINET/redirector.yml b/FlokiNET/redirector.yml similarity index 100% rename from FLOKINET/redirector.yml rename to FlokiNET/redirector.yml diff --git a/FLOKINET/templates/default-site.j2 b/FlokiNET/templates/default-site.j2 similarity index 100% rename from FLOKINET/templates/default-site.j2 rename to FlokiNET/templates/default-site.j2 diff --git a/FLOKINET/templates/gophish-config.j2 b/FlokiNET/templates/gophish-config.j2 similarity index 100% rename from FLOKINET/templates/gophish-config.j2 rename to FlokiNET/templates/gophish-config.j2 diff --git a/FLOKINET/templates/index.html.j2 b/FlokiNET/templates/index.html.j2 similarity index 100% rename from FLOKINET/templates/index.html.j2 rename to FlokiNET/templates/index.html.j2 diff --git a/FlokiNET/templates/motd-aws.j2 b/FlokiNET/templates/motd-aws.j2 new file mode 100644 index 0000000..692ac77 --- /dev/null +++ b/FlokiNET/templates/motd-aws.j2 @@ -0,0 +1,59 @@ +Welcome to your secure FlokiNET C2 Server! + +╔═══════════════════════════════════════════════╗ +║ OPERATIONAL SECURITY ║ +║ ║ +║ This server has enhanced security features ║ +║ including hardened SSH, Tor routing, and ║ +║ zero-logs configuration. ║ +╚═══════════════════════════════════════════════╝ + +The following tools and utilities have been installed: + +Apt-Installed Tools: +-------------------- +- git, wget, curl, unzip +- python3-pip, python3-venv, pipx +- tmux, nmap, tcpdump, hydra, john, hashcat +- sqlmap, gobuster, dirb, enum4linux, dnsenum, seclists, responder +- golang, proxychains, tor, crackmapexec, jq, unzip +- postfix, certbot, opendkim, opendkim-tools + +Pipx-Installed Tools: +--------------------- +- NetExec: git+https://github.com/Pennyw0rth/NetExec +- TREVORspray: git+https://github.com/blacklanternsecurity/TREVORspray +- impacket: (various network protocols and service tools) + +Custom Tools Installed in ~/Tools: +---------------------------------- +- SharpCollection: ~/Tools/SharpCollection +- Kerbrute: ~/Tools/Kerbrute +- PEASS-ng: ~/Tools/PEASS-ng +- MailSniper: ~/Tools/MailSniper +- Inveigh: ~/Tools/Inveigh +- Gophish: ~/Tools/gophish (unzipped here) + +Other Installed C2 Frameworks: +------------------------------ +- Metasploit Framework: system installed (run 'msfconsole') +- Sliver C2: system installed (run 'sliver') + +Security Scripts in /opt/c2/: +----------------------------- +- clean-logs.sh: Securely clears all logs on the system +- secure-exit.sh: Perform secure wipe for termination +- serve-beacons.sh: Hosts generated implants for delivery + +Also, remember that many reconnaissance and attack tools are now available system-wide due to the apt and pipx installations. + +Once your DNS record points to this server's public IP, you can obtain a Let's Encrypt certificate by running: + + sudo certbot certonly --non-interactive --agree-tos --email {{ letsencrypt_email }} --standalone -d {{ c2_subdomain }}.{{ domain }} + +**IMPORTANT:** + +To route traffic through Tor for additional anonymity, prefix commands with 'proxychains': + proxychains curl ifconfig.me + +Don't forget to set up a DMARC record for your domain. Update your DNS provider's dashboard to add a TXT record named `_dmarc` with a suitable DMARC policy (e.g., `v=DMARC1; p=reject; rua=mailto:admin@{{ domain }}; ruf=mailto:admin@{{ domain }}; pct=100`). This ensures better email deliverability and security for your domain. \ No newline at end of file diff --git a/FLOKINET/templates/nginx.conf.j2 b/FlokiNET/templates/nginx.conf.j2 similarity index 100% rename from FLOKINET/templates/nginx.conf.j2 rename to FlokiNET/templates/nginx.conf.j2 diff --git a/FLOKINET/templates/proxychains.conf.j2 b/FlokiNET/templates/proxychains.conf.j2 similarity index 100% rename from FLOKINET/templates/proxychains.conf.j2 rename to FlokiNET/templates/proxychains.conf.j2 diff --git a/FLOKINET/templates/resolv.conf.j2 b/FlokiNET/templates/resolv.conf.j2 similarity index 100% rename from FLOKINET/templates/resolv.conf.j2 rename to FlokiNET/templates/resolv.conf.j2 diff --git a/FLOKINET/templates/secure-ssh.sh.j2 b/FlokiNET/templates/secure-ssh.sh.j2 similarity index 100% rename from FLOKINET/templates/secure-ssh.sh.j2 rename to FlokiNET/templates/secure-ssh.sh.j2 diff --git a/FLOKINET/templates/shell-handler.service.j2 b/FlokiNET/templates/shell-handler.service.j2 similarity index 100% rename from FLOKINET/templates/shell-handler.service.j2 rename to FlokiNET/templates/shell-handler.service.j2 diff --git a/FLOKINET/templates/sliver-server.service.j2 b/FlokiNET/templates/sliver-server.service.j2 similarity index 100% rename from FLOKINET/templates/sliver-server.service.j2 rename to FlokiNET/templates/sliver-server.service.j2 diff --git a/FLOKINET/templates/torrc.j2 b/FlokiNET/templates/torrc.j2 similarity index 100% rename from FLOKINET/templates/torrc.j2 rename to FlokiNET/templates/torrc.j2 diff --git a/LINODE/c2-deploy.yaml b/Linode/c2-deploy.yaml similarity index 100% rename from LINODE/c2-deploy.yaml rename to Linode/c2-deploy.yaml diff --git a/LINODE/c2-vars-template.yaml b/Linode/c2-vars-template.yaml similarity index 100% rename from LINODE/c2-vars-template.yaml rename to Linode/c2-vars-template.yaml diff --git a/LINODE/c2.yml b/Linode/c2.yml similarity index 100% rename from LINODE/c2.yml rename to Linode/c2.yml diff --git a/LINODE/cleanup.yml b/Linode/cleanup.yml similarity index 100% rename from LINODE/cleanup.yml rename to Linode/cleanup.yml diff --git a/LINODE/redirector.yml b/Linode/redirector.yml similarity index 94% rename from LINODE/redirector.yml rename to Linode/redirector.yml index feeafc9..6d53019 100644 --- a/LINODE/redirector.yml +++ b/Linode/redirector.yml @@ -55,12 +55,10 @@ - name: Add redirector to inventory add_host: - name: "redirector" - groups: "redirectors" + name: "{{ redirector_name }}" ansible_host: "{{ redirector_ip }}" - ansible_user: "{{ ssh_user }}" + ansible_user: "root" # Or directly use the user you need ansible_ssh_private_key_file: "{{ ssh_key_path | replace('.pub', '') }}" - ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" - name: Configure redirector server hosts: redirectors diff --git a/LINODE/templates/gophish-config.j2 b/Linode/templates/gophish-config.j2 similarity index 100% rename from LINODE/templates/gophish-config.j2 rename to Linode/templates/gophish-config.j2 diff --git a/LINODE/templates/motd-linode.j2 b/Linode/templates/motd-linode.j2 similarity index 100% rename from LINODE/templates/motd-linode.j2 rename to Linode/templates/motd-linode.j2 diff --git a/deploy.py b/deploy.py index f4033a1..5f52d9b 100644 --- a/deploy.py +++ b/deploy.py @@ -17,7 +17,7 @@ from datetime import datetime # Disable Ansible host key checking os.environ["ANSIBLE_HOST_KEY_CHECKING"] = "False" -# Constants +# Constants for providers PROVIDERS = ["aws", "linode", "flokinet"] DEFAULT_SSH_USER = { "aws": "kali", @@ -25,6 +25,13 @@ DEFAULT_SSH_USER = { "flokinet": "root" } +# Directory names - maintain correct case for each provider +PROVIDER_DIRS = { + "aws": "AWS", + "linode": "Linode", + "flokinet": "FlokiNET" +} + def setup_logging(): """Set up logging for the deployment""" log_dir = "logs" @@ -32,9 +39,10 @@ def setup_logging(): timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') log_file = os.path.join(log_dir, f"deployment_{timestamp}.log") + # Configure file handler to log DEBUG and above logging.basicConfig( filename=log_file, - level=logging.INFO, + level=logging.DEBUG, # Changed from INFO to DEBUG format='%(asctime)s - %(levelname)s - %(message)s' ) @@ -50,7 +58,10 @@ def setup_logging(): def parse_arguments(): """Parse command line arguments""" - parser = argparse.ArgumentParser(description='Deploy C2 Red Team infrastructure') + parser = argparse.ArgumentParser( + description='C2itAll - Red Team Infrastructure Setup', + formatter_class=argparse.RawDescriptionHelpFormatter + ) # Provider selection parser.add_argument('-p', '--provider', choices=PROVIDERS, default=None, help='Provider to use for deployment') @@ -72,11 +83,13 @@ def parse_arguments(): # General arguments parser.add_argument('--ssh-key', help='Path to SSH private key') parser.add_argument('--ssh-user', help='SSH username (default: provider-specific)') + parser.add_argument('--size', help='Size of the instance (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)') - # Deployment options + # Common arguments + parser.add_argument('--teardown', action='store_true', help='Tear down existing infrastructure') parser.add_argument('--redirector-only', action='store_true', help='Deploy only the redirector') parser.add_argument('--c2-only', action='store_true', help='Deploy only the C2 server') parser.add_argument('--debug', action='store_true', help='Enable debug mode for verbose output') @@ -91,8 +104,19 @@ def parse_arguments(): # 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('--teardown', action='store_true', help='Tear down existing infrastructure') - + + # Test options + parser.add_argument('--run-tests', action='store_true', help='Run deployment tests') + + # Tracker deployment options + parser.add_argument('--deploy-tracker', action='store_true', help='Deploy phishing email tracking server') + parser.add_argument('--tracker-domain', help='Domain name for the tracker server') + parser.add_argument('--tracker-email', help='Email for Let\'s Encrypt certificate for tracker') + parser.add_argument('--tracker-name', help='Name for tracker instance (default: random)') + parser.add_argument('--tracker-ipinfo-token', help='IPinfo.io API token for geolocation') + parser.add_argument('--tracker-setup-ssl', action='store_true', help='Set up SSL for tracker') + parser.add_argument('--tracker-create-pixel', action='store_true', help='Create tracking pixel') + # Interactive mode parser.add_argument('--interactive', action='store_true', help='Run in interactive wizard mode') @@ -102,7 +126,9 @@ def interactive_setup(): """Interactive setup wizard for deployment""" config = {} - print("\n====== C2ingRed Interactive Setup Wizard ======\n") + print("\n========================================") + print("C2itAll - Interactive Setup Wizard") + print("========================================\n") # Select provider print("Available cloud providers:") @@ -121,8 +147,15 @@ def interactive_setup(): print("Please enter a valid number") # Load vars file for the selected provider - vars_file = f"{config['provider'].upper()}/vars.yaml" + provider_dir = config['provider'].capitalize() + if config['provider'] == "aws": + provider_dir = "AWS" + elif config['provider'] == "flokinet": + provider_dir = "FlokiNET" + + vars_file = f"{provider_dir}/vars.yaml" vars_data = {} + if os.path.exists(vars_file): try: with open(vars_file, 'r') as f: @@ -133,8 +166,12 @@ def interactive_setup(): # Provider-specific configuration if config['provider'] == "aws": - config['aws_access_key'] = input("\nAWS Access Key [leave blank to use AWS CLI profile]: ") or None - config['aws_secret_key'] = input("AWS Secret Key [leave blank to use AWS CLI profile]: ") or None + # Set defaults from vars file + default_aws_key = vars_data.get('aws_access_key', '') + default_aws_secret = vars_data.get('aws_secret_key', '') + + config['aws_access_key'] = input(f"\nAWS Access Key [{'*****' if default_aws_key else 'leave blank to use AWS CLI profile'}]: ") or default_aws_key + config['aws_secret_key'] = input(f"AWS Secret Key [{'*****' if default_aws_secret else 'leave blank to use AWS CLI profile'}]: ") or default_aws_secret # Show available regions aws_regions = vars_data.get('aws_region_choices', []) @@ -158,10 +195,18 @@ def interactive_setup(): except ValueError: print("Please enter a valid number or press Enter") else: - config['aws_region'] = input("\nAWS Region [leave blank for us-east-1]: ") or "us-east-1" + default_region = vars_data.get('aws_region', 'us-east-1') + config['aws_region'] = input(f"\nAWS Region [default: {default_region}]: ") or default_region + + # Instance size + default_size = vars_data.get('aws_instance_type', 't2.medium') + config['aws_instance_type'] = input(f"Instance Size [default: {default_size}]: ") or default_size elif config['provider'] == "linode": - config['linode_token'] = input("\nLinode API Token: ") + # Set defaults from vars file + default_token = vars_data.get('linode_token', '') + + config['linode_token'] = input(f"\nLinode API Token [{'*****' if default_token else 'required'}]: ") or default_token # Show available regions linode_regions = vars_data.get('region_choices', []) @@ -184,12 +229,28 @@ def interactive_setup(): print(f"Please enter a number between 1 and {len(linode_regions)}") except ValueError: print("Please enter a valid number or press Enter") + else: + # Use defaults or empty values if no regions are defined + default_region = vars_data.get('linode_region', '') + config['linode_region'] = input(f"\nLinode Region [default: {default_region or 'random'}]: ") or default_region + + # Instance size/plan + default_plan = vars_data.get('plan', 'g6-standard-2') + config['plan'] = input(f"Instance Plan [default: {default_plan}]: ") or default_plan elif config['provider'] == "flokinet": print("\nFlokiNET requires pre-provisioned servers.") - config['flokinet_redirector_ip'] = input("FlokiNET Redirector IP Address: ") - config['flokinet_c2_ip'] = input("FlokiNET C2 Server IP Address: ") - config['ssh_user'] = input(f"SSH User [default: {DEFAULT_SSH_USER['flokinet']}]: ") or DEFAULT_SSH_USER['flokinet'] + + # Set defaults from vars file + default_redirector_ip = vars_data.get('redirector_ip', '') + default_c2_ip = vars_data.get('c2_ip', '') + default_ssh_user = vars_data.get('ssh_user', DEFAULT_SSH_USER['flokinet']) + default_ssh_port = vars_data.get('ssh_port', 22) + + config['flokinet_redirector_ip'] = input(f"FlokiNET Redirector IP Address [default: {default_redirector_ip}]: ") or default_redirector_ip + config['flokinet_c2_ip'] = input(f"FlokiNET C2 Server IP Address [default: {default_c2_ip}]: ") or default_c2_ip + config['ssh_user'] = input(f"SSH User [default: {default_ssh_user}]: ") or default_ssh_user + config['ssh_port'] = input(f"SSH Port [default: {default_ssh_port}]: ") or default_ssh_port ssh_key = input("Path to SSH Private Key [leave blank to generate new key]: ") if ssh_key: @@ -224,18 +285,25 @@ def interactive_setup(): print("Please enter a valid number") # Domain configuration - config['domain'] = input("\nDomain name [default: example.com]: ") or "example.com" - config['letsencrypt_email'] = input(f"Email for Let's Encrypt [default: admin@{config['domain']}]: ") or f"admin@{config['domain']}" + default_domain = vars_data.get('domain', 'example.com') + config['domain'] = input(f"\nDomain name [default: {default_domain}]: ") or default_domain + + default_email = vars_data.get('letsencrypt_email', f"admin@{config['domain']}") + config['letsencrypt_email'] = input(f"Email for Let's Encrypt [default: {default_email}]: ") or default_email # Security options print("\nSecurity options:") - disable_history = input("Disable command history? (y/n) [default: y]: ").lower() != 'n' - secure_memory = input("Enable secure memory settings? (y/n) [default: y]: ").lower() != 'n' - zero_logs = input("Enable zero-logs configuration? (y/n) [default: y]: ").lower() != 'n' + default_disable_history = vars_data.get('disable_history', True) + default_secure_memory = vars_data.get('secure_memory', True) + default_zero_logs = vars_data.get('zero_logs', True) - config['disable_history'] = disable_history - config['secure_memory'] = secure_memory - config['zero_logs'] = zero_logs + disable_history = input(f"Disable command history? (y/n) [default: {'y' if default_disable_history else 'n'}]: ").lower() + secure_memory = input(f"Enable secure memory settings? (y/n) [default: {'y' if default_secure_memory else 'n'}]: ").lower() + zero_logs = input(f"Enable zero-logs configuration? (y/n) [default: {'y' if default_zero_logs else 'n'}]: ").lower() + + config['disable_history'] = True if (disable_history == 'y' or (default_disable_history and disable_history != 'n')) else False + config['secure_memory'] = True if (secure_memory == 'y' or (default_secure_memory and secure_memory != 'n')) else False + config['zero_logs'] = True if (zero_logs == 'y' or (default_zero_logs and zero_logs != 'n')) else False # Post-deployment options config['ssh_after_deploy'] = input("\nSSH into instance after deployment? (y/n) [default: n]: ").lower() == 'y' @@ -243,9 +311,64 @@ def interactive_setup(): # Debug mode config['debug'] = input("Enable debug mode? (y/n) [default: n]: ").lower() == 'y' - print("\n====== Configuration Summary ======") + # Additional options + deploy_tracker = input("\nDeploy email tracking server? (y/n) [default: n]: ").lower() + config['deploy_tracker'] = deploy_tracker == 'y' + + if config['deploy_tracker']: + default_tracker_domain = f"track.{config['domain']}" + config['tracker_domain'] = input(f"Tracker domain [default: {default_tracker_domain}]: ") or default_tracker_domain + + default_tracker_email = config['letsencrypt_email'] + config['tracker_email'] = input(f"Tracker Let's Encrypt email [default: {default_tracker_email}]: ") or default_tracker_email + + config['tracker_ipinfo_token'] = input("IPinfo.io API token [optional]: ") + config['tracker_setup_ssl'] = input("Set up SSL for tracker? (y/n) [default: y]: ").lower() != 'n' + config['tracker_create_pixel'] = input("Create tracking pixel? (y/n) [default: y]: ").lower() != 'n' + + # Generate random tracker name + rand_suffix = generate_random_string() + timestamp = int(time.time()) % 10000 + config['tracker_name'] = f"track-{rand_suffix}-{timestamp}" + + # SSH key if not already set + if 'ssh_key' not in config or not config['ssh_key']: + ssh_key = input("\nPath to SSH Private Key [leave blank to generate new key]: ") + if ssh_key: + config['ssh_key'] = os.path.expanduser(ssh_key) + else: + # Generate SSH key + config['ssh_key'] = generate_ssh_key() + + # Generate random instance names if not set + rand_suffix = generate_random_string() + timestamp = int(time.time()) % 10000 + config['redirector_name'] = f"srv-{rand_suffix}-{timestamp}" + config['c2_name'] = f"node-{rand_suffix}-{timestamp}" + + # Additional 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))) + config['shell_handler_port'] = vars_data.get('shell_handler_port', str(random.randint(4000, 65000))) + + # Set SSH key path for proper reference + if config['ssh_key'].startswith(os.path.expanduser("~/.ssh/c2deploy_")): + config['ssh_key_path'] = f"{config['ssh_key']}.pub" + else: + config['ssh_key_path'] = f"{config['ssh_key']}.pub" + # Check if the public key exists + if not os.path.exists(config['ssh_key_path']): + # Try alternative extension + alt_path = f"{config['ssh_key']}.pub" + if os.path.exists(alt_path): + config['ssh_key_path'] = alt_path + + print("\n========================================") + print("Configuration Summary") + print("========================================") for key, value in config.items(): - if key not in ['aws_secret_key', 'linode_token']: + if key not in ['aws_secret_key', 'linode_token', 'smtp_auth_pass']: print(f" {key}: {value}") confirm = input("\nProceed with deployment? (y/n): ").lower() @@ -257,14 +380,18 @@ def interactive_setup(): def load_vars_file(provider): """Load vars.yaml for the specified provider""" - # Handle case sensitivity in directory names - provider_dir = provider.upper() + if provider not in PROVIDER_DIRS: + logging.warning(f"Unknown provider: {provider}") + return {} + + # Use correct case for directory + provider_dir = PROVIDER_DIRS[provider] vars_file = f"{provider_dir}/vars.yaml" if os.path.exists(vars_file): try: with open(vars_file, 'r') as f: - vars_data = yaml.safe_load(f) + vars_data = yaml.safe_load(f) or {} logging.info(f"Loaded configuration from {vars_file}") return vars_data except Exception as e: @@ -332,7 +459,12 @@ def create_inventory_file(config, deployment_type): 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") + if config.get('ssh_port'): + inventory_content.append(f"ansible_port={config['ssh_port']}") + + # Use the Python from the virtual environment + venv_python = sys.executable + inventory_content.append(f"ansible_python_interpreter={venv_python}") # Add specific host sections based on deployment type if deployment_type == "local": @@ -344,6 +476,9 @@ def create_inventory_file(config, deployment_type): elif deployment_type == "c2": inventory_content.append("\n[c2servers]") inventory_content.append(f"c2 ansible_host={config.get('c2_ip', '127.0.0.1')}") + elif deployment_type == "tracker": + inventory_content.append("\n[trackers]") + inventory_content.append(f"tracker ansible_host={config.get('tracker_ip', '127.0.0.1')}") # Create temporary file fd, inventory_path = tempfile.mkstemp(prefix=f"inventory_{deployment_type}_", suffix=".ini") @@ -357,9 +492,24 @@ 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 @@ -372,7 +522,15 @@ def run_ansible_playbook(playbook, inventory, config, debug=False): # Add verbosity if debug mode is enabled if debug: - cmd.append("-vvv") + cmd.append("-vvvv") + else: + # Add some verbosity even in non-debug mode to capture errors + 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)}") @@ -384,20 +542,28 @@ def run_ansible_playbook(playbook, inventory, config, debug=False): check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - text=True + text=True, + env=cmd_env ) 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"Error output: {e.stderr}") + 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'] - # Convert provider to uppercase for directory paths - provider_dir = provider.upper() + provider_dir = PROVIDER_DIRS.get(provider, provider.upper()) + logging.info(f"Deploying {provider} infrastructure...") # Set provider-specific environment variables @@ -417,7 +583,10 @@ def deploy_infrastructure(config): config['linode_region'] = select_random_region(config) # Determine playbook path based on deployment mode - if config.get('redirector_only'): + if config.get('deploy_tracker'): + playbook = f"{provider_dir}/tracker.yml" + deployment_type = "tracker" + elif config.get('redirector_only'): playbook = f"{provider_dir}/redirector.yml" deployment_type = "redirector" elif config.get('c2_only'): @@ -477,7 +646,7 @@ def deploy_flokinet_redirector(config): inventory_path = create_inventory_file(config, "redirector") # Run the playbook - playbook = "FLOKINET/redirector.yml" + playbook = f"{PROVIDER_DIRS['flokinet']}/redirector.yml" try: success, stdout, stderr = run_ansible_playbook( playbook, inventory_path, config, config.get('debug', False) @@ -513,7 +682,7 @@ def deploy_flokinet_c2(config): inventory_path = create_inventory_file(config, "c2") # Run the playbook - playbook = "FLOKINET/c2.yml" + playbook = f"{PROVIDER_DIRS['flokinet']}/c2.yml" try: success, stdout, stderr = run_ansible_playbook( playbook, inventory_path, config, config.get('debug', False) @@ -533,12 +702,57 @@ def deploy_flokinet_c2(config): return False +def run_tests(config): + """Run deployment tests""" + provider = config['provider'] + provider_dir = PROVIDER_DIRS.get(provider, provider.upper()) + + logging.info(f"Running {provider} tests...") + + # Set provider-specific environment variables + if provider == "aws": + if config.get('aws_access_key'): + os.environ['AWS_ACCESS_KEY_ID'] = config['aws_access_key'] + if config.get('aws_secret_key'): + os.environ['AWS_SECRET_ACCESS_KEY'] = config['aws_secret_key'] + elif provider == "linode": + if config.get('linode_token'): + os.environ['LINODE_TOKEN'] = config['linode_token'] + + # Run tests playbook + playbook = f"{provider_dir}/tests.yml" + if os.path.exists(playbook): + inventory_path = create_inventory_file(config, "local") + 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"Tests failed: {e}") + + # Clean up inventory file + if os.path.exists(inventory_path): + os.unlink(inventory_path) + + return False + 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...") # Determine which IP to use based on deployment type - if config.get('redirector_only'): + if config.get('deploy_tracker'): + ip_key = 'tracker_ip' + instance_type = 'tracker' + elif config.get('redirector_only'): ip_key = 'redirector_ip' instance_type = 'redirector' else: @@ -601,24 +815,69 @@ def ssh_to_instance(config): def cleanup_resources(config): """Clean up resources if deployment fails""" provider = config.get('provider') - provider_dir = provider.upper() if provider else None + 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 Ansible for cleanup - if provider_dir: - playbook = f"{provider_dir}/cleanup.yml" - if os.path.exists(playbook): - logging.info(f"Running cleanup playbook: {playbook}") - inventory_path = create_inventory_file(config, "local") - try: - run_ansible_playbook(playbook, inventory_path, config) - except Exception as e: - logging.error(f"Cleanup playbook failed: {e}") - finally: - if os.path.exists(inventory_path): - os.unlink(inventory_path) - else: - logging.warning(f"No cleanup playbook found at {playbook}") + 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") + 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}") + + 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') @@ -670,7 +929,7 @@ def check_dependencies(): def teardown_infrastructure(config): """Tear down existing infrastructure""" provider = config['provider'] - provider_dir = provider.upper() + provider_dir = PROVIDER_DIRS.get(provider, provider.upper()) logging.info(f"Tearing down {provider} infrastructure...") # Set provider-specific environment variables @@ -686,8 +945,9 @@ def teardown_infrastructure(config): # Run cleanup playbook playbook = f"{provider_dir}/cleanup.yml" if os.path.exists(playbook): - # Add confirm_cleanup=false to skip confirmation in playbook - config['confirm_cleanup'] = False + # Set confirm_cleanup to false to skip confirmation in playbook + # This needs to be a plain string "false" instead of a boolean + config['confirm_cleanup'] = "false" # Create inventory for local execution inventory_path = create_inventory_file(config, "local") @@ -718,8 +978,61 @@ def teardown_infrastructure(config): logging.error(f"No cleanup playbook found at {playbook}") return False +def deploy_tracker(config): + """Deploy email tracking server""" + provider = config['provider'] + provider_dir = PROVIDER_DIRS.get(provider, provider.upper()) + + logging.info(f"Deploying tracker on {provider}...") + + # 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'] + + # Determine playbook path + playbook = f"{provider_dir}/tracker.yml" + if not os.path.exists(playbook): + logging.error(f"Tracker playbook not found at {playbook}") + return False + + # Create inventory file + inventory_path = create_inventory_file(config, "tracker") + + # 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: + logging.error(f"Tracker deployment failed: {e}") + + # Clean up inventory file + if os.path.exists(inventory_path): + os.unlink(inventory_path) + + return False + def main(): """Main function to run the deployment""" + # Display banner + print(""" +======================================== +C2itAll - Red Team Infrastructure Setup +======================================== + """) + # Set up logging log_file = setup_logging() @@ -729,16 +1042,26 @@ def main(): # Check dependencies check_dependencies() - # Use interactive mode if specified or if no provider is given - if args.interactive or args.provider is None and not args.flokinet: + # Use interactive mode if specified + if args.interactive: config = interactive_setup() else: # 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) + # Load variables from provider-specific vars.yaml if provider is specified + vars_data = {} + if args.provider: + provider_dir = PROVIDER_DIRS.get(args.provider, args.provider.upper()) + vars_file = f"{provider_dir}/vars.yaml" + if os.path.exists(vars_file): + try: + with open(vars_file, 'r') as f: + vars_data = yaml.safe_load(f) or {} + logging.info(f"Loaded configuration from {vars_file}") + except Exception as e: + logging.warning(f"Failed to load {vars_file}: {e}") # Build configuration by combining args and vars_data config = {} @@ -750,16 +1073,17 @@ def main(): 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'] = args.aws_region or args.region or vars_data.get('aws_region') config['aws_region_choices'] = vars_data.get('aws_region_choices', []) config['ami_map'] = vars_data.get('ami_map', {}) + config['size'] = args.size or vars_data.get('aws_instance_type', 't2.medium') # 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['linode_region'] = args.linode_region or args.region or vars_data.get('linode_region') config['region_choices'] = vars_data.get('region_choices', []) - config['plan'] = vars_data.get('plan', 'g6-standard-1') + config['plan'] = args.size or vars_data.get('plan', 'g6-standard-2') config['image'] = vars_data.get('image', 'linode/kali') # FlokiNET settings @@ -770,17 +1094,20 @@ def main(): config['ssh_port'] = vars_data.get('ssh_port', 22) # SSH settings - config['ssh_user'] = args.ssh_user or DEFAULT_SSH_USER.get(args.provider) + config['ssh_user'] = args.ssh_user or vars_data.get('ssh_user') or (DEFAULT_SSH_USER.get(args.provider) if args.provider else None) if args.ssh_key: config['ssh_key'] = os.path.expanduser(args.ssh_key) else: - config['ssh_key'] = generate_ssh_key() + # Generate a key if no key is provided and we're not in teardown mode + if not args.teardown: + 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}" + config['tracker_name'] = args.tracker_name or f"track-{rand_suffix}-{timestamp}" # Deployment options config['redirector_only'] = args.redirector_only @@ -798,9 +1125,38 @@ def main(): 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))) + config['shell_handler_port'] = vars_data.get('shell_handler_port', str(random.randint(4000, 65000))) + + # Tracker options + config['deploy_tracker'] = args.deploy_tracker + if args.deploy_tracker: + config['tracker_domain'] = args.tracker_domain or f"track.{config['domain']}" + config['tracker_email'] = args.tracker_email or config['letsencrypt_email'] + config['tracker_ipinfo_token'] = args.tracker_ipinfo_token + config['tracker_setup_ssl'] = args.tracker_setup_ssl + config['tracker_create_pixel'] = args.tracker_create_pixel # SSH after deploy config['ssh_after_deploy'] = args.ssh_after_deploy + + # Run tests + config['run_tests'] = args.run_tests + + # Make sure we have the public key path if we're generating one + if config.get('ssh_key') and config['ssh_key'].startswith(os.path.expanduser("~/.ssh/c2deploy_")): + config['ssh_key_path'] = f"{config['ssh_key']}.pub" + elif config.get('ssh_key'): + config['ssh_key_path'] = f"{config['ssh_key']}.pub" + # Check if the public key exists + if not os.path.exists(config['ssh_key_path']): + # Try alternative extension + alt_path = f"{config['ssh_key']}.pub" + if os.path.exists(alt_path): + config['ssh_key_path'] = alt_path + elif not args.teardown: # Only generate new key if not in teardown mode + logging.warning(f"Public key not found at {config['ssh_key_path']}. Generating a new key.") + config['ssh_key'] = generate_ssh_key() + config['ssh_key_path'] = f"{config['ssh_key']}.pub" # Validate deployment mode if config.get('redirector_only') and config.get('c2_only'): @@ -810,7 +1166,7 @@ def main(): # 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 key not in ['aws_secret_key', 'linode_token', 'smtp_auth_pass', 'tracker_ipinfo_token']: if isinstance(value, (dict, list)): logging.info(f" {key}: ") else: @@ -821,8 +1177,30 @@ def main(): teardown_infrastructure(config) return + # Check if this is a test operation + if args.run_tests: + run_tests(config) + return + # Run deployment try: + # Deploy tracker if requested + if config.get('deploy_tracker'): + success = deploy_tracker(config) + if not success: + logging.error("Tracker deployment failed!") + cleanup_resources(config) + return + + logging.info("Tracker deployment completed successfully!") + + # SSH into tracker if requested + if config.get('ssh_after_deploy'): + ssh_to_instance(config) + + return + + # Deploy main infrastructure success = deploy_infrastructure(config) if success: diff --git a/requirements.txt b/requirements.txt index ef6a527..d33ed27 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ ansible -linode_api4 +linode-api4 boto3 botocore awscli