Initial public portfolio release

Sanitized version of red team infrastructure automation platform.
Operational content (implant pipelines, lures, credential capture)
replaced with documented stubs. Architecture and infrastructure
automation code intact.
This commit is contained in:
Operator
2026-06-23 16:12:14 -04:00
commit 98103466d8
239 changed files with 40012 additions and 0 deletions
+143
View File
@@ -0,0 +1,143 @@
#!/bin/bash
# Default configurations
DEFAULT_IMAGE_FILTER="*kali-last-snapshot*"
DEFAULT_OWNER_ID="679593333241"
DEFAULT_REGION="us-east-1"
# Debugging flag
DEBUG=false
# Usage function
function usage() {
echo "Usage: $0 [--image-filter <filter>] [--owner-id <owner-id>] [--debug] [--help]"
echo ""
echo "Options:"
echo " --image-filter <filter> Filter for AMI names (default: '${DEFAULT_IMAGE_FILTER}')."
echo " --owner-id <owner-id> Owner ID for filtering AMIs (default: '${DEFAULT_OWNER_ID}')."
echo " --debug Enable verbose debugging."
echo " --help Display this help message."
exit 1
}
# Parse arguments
IMAGE_FILTER="$DEFAULT_IMAGE_FILTER"
OWNER_ID="$DEFAULT_OWNER_ID"
while [[ $# -gt 0 ]]; do
case $1 in
--image-filter)
IMAGE_FILTER="$2"
shift 2
;;
--owner-id)
OWNER_ID="$2"
shift 2
;;
--debug)
DEBUG=true
shift
;;
--help)
usage
;;
*)
echo "Unknown option: $1"
usage
;;
esac
done
if $DEBUG; then
echo "DEBUG: Using image filter: $IMAGE_FILTER"
echo "DEBUG: Using owner ID: $OWNER_ID"
fi
# Step 1: Fetch all AMIs in the default region to identify the latest version
LATEST_AMI=""
LATEST_YEAR=0
LATEST_VERSION=0
AMI_LIST=$(aws ec2 describe-images \
--region "$DEFAULT_REGION" \
--filters "Name=name,Values=$IMAGE_FILTER" "Name=owner-id,Values=$OWNER_ID" \
--query "Images[].[Name]" \
--output text)
if $DEBUG; then
echo "DEBUG: AMI list in $DEFAULT_REGION: $AMI_LIST"
fi
for AMI_NAME in $AMI_LIST; do
if [[ $AMI_NAME != *"-prod-"* ]]; then
# Extract year and version using regex
if [[ $AMI_NAME =~ ([0-9]{4})\.([0-9]+)\.([0-9]+) ]]; then
YEAR=${BASH_REMATCH[1]}
VERSION=${BASH_REMATCH[2]}
if $DEBUG; then
echo "DEBUG: Checking AMI: $AMI_NAME (Year: $YEAR, Version: $VERSION)"
fi
if (( YEAR > LATEST_YEAR )) || (( YEAR == LATEST_YEAR && VERSION > LATEST_VERSION )); then
LATEST_AMI="$AMI_NAME"
LATEST_YEAR=$YEAR
LATEST_VERSION=$VERSION
fi
fi
fi
done
if $DEBUG; then
echo "DEBUG: Latest AMI determined: $LATEST_AMI"
fi
# Step 2: Use the latest AMI name to filter across all regions
if [[ -z "$LATEST_AMI" ]]; then
echo "No valid AMIs found matching the criteria."
exit 1
fi
IMAGE_FILTER_LATEST="${LATEST_AMI%-*}*" # Strip the region-specific suffix and add a wildcard
if $DEBUG; then
echo "DEBUG: Using refined image filter: $IMAGE_FILTER_LATEST"
fi
# Step 3: Fetch AMIs across all regions
REGIONS=$(aws ec2 describe-regions --query "Regions[].RegionName" --output text)
echo "Fetching AMIs with filter '$IMAGE_FILTER_LATEST' and owner ID '$OWNER_ID'..."
AMI_MAP=""
REGION_LIST=()
for REGION in $REGIONS; do
if $DEBUG; then
echo "DEBUG: Querying region: $REGION"
fi
AMI_INFO=$(aws ec2 describe-images \
--region "$REGION" \
--filters "Name=name,Values=$IMAGE_FILTER_LATEST" "Name=owner-id,Values=$OWNER_ID" \
--query "Images[].[Name,ImageId]" \
--output text)
if [[ -n "$AMI_INFO" ]]; then
while read -r NAME AMI_ID; do
echo "$NAME"
echo " $REGION: $AMI_ID"
REGION_LIST+=("$REGION")
AMI_MAP+="$REGION: $AMI_ID"$'\n'
done <<< "$AMI_INFO"
fi
done
# Step 4: Generate YAML
YAML_OUTPUT="aws_region_choices:\n"
for REGION in "${REGION_LIST[@]}"; do
YAML_OUTPUT+=" - $REGION\n"
done
YAML_OUTPUT+="ami_map:\n$AMI_MAP"
echo -e "\nGenerated YAML:\n$YAML_OUTPUT"
+171
View File
@@ -0,0 +1,171 @@
---
# AWS Attack Box Deployment Playbook
# Based on attk-box-setup but optimized for AWS deployment
- name: Deploy Attack Box on AWS
hosts: localhost
connection: local
gather_facts: false
vars_files:
- vars.yaml
vars:
ansible_python_interpreter: "{{ ansible_playbook_python }}"
deployment_type: "attack_box"
attack_box_name: "{{ attack_box_name | default('a-' + deployment_id) }}"
attack_box_type: "{{ attack_box_type | default('kali') }}"
aws_region: "{{ aws_region | default(aws_region_choices | random) }}"
instance_type: "{{ aws_instance_type | default('t2.medium') }}"
ssh_key_name: "attack-box-{{ deployment_id }}"
# Attack box AMI mapping (use Kali Linux AMIs)
attack_box_ami_map:
us-east-1: "{{ kali_ami_map['us-east-1'] | default('ami-061b17d332829ab1c') }}"
us-east-2: "{{ kali_ami_map['us-east-2'] | default('ami-061b17d332829ab1c') }}"
us-west-1: "{{ kali_ami_map['us-west-1'] | default('ami-061b17d332829ab1c') }}"
us-west-2: "{{ kali_ami_map['us-west-2'] | default('ami-061b17d332829ab1c') }}"
tasks:
- name: Validate 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"
- name: Debug attack box deployment
debug:
msg: "Deploying {{ attack_box_type }} attack box on AWS in {{ aws_region }}"
- name: Set attack box AMI
set_fact:
attack_box_ami: "{{ attack_box_ami_map[aws_region] | default(attack_box_ami_map['us-east-1']) }}"
- name: Generate SSH key pair for attack box
ec2_key:
name: "{{ ssh_key_name }}"
region: "{{ aws_region }}"
aws_access_key: "{{ aws_access_key }}"
aws_secret_key: "{{ aws_secret_key }}"
state: present
register: ec2_key_result
- name: Save private key locally
copy:
content: "{{ ec2_key_result.key.private_key }}"
dest: "~/.ssh/{{ ssh_key_name }}"
mode: '0600'
when: ec2_key_result.key.private_key is defined
- name: Create security group for attack box
ec2_group:
name: "attack-box-sg-{{ deployment_id }}"
description: "Security group for attack box {{ deployment_id }}"
region: "{{ aws_region }}"
aws_access_key: "{{ aws_access_key }}"
aws_secret_key: "{{ aws_secret_key }}"
rules:
- proto: tcp
ports:
- 22
cidr_ip: "{{ operator_ip | default('0.0.0.0/0') }}/32"
rule_desc: "SSH access from operator IP"
- proto: tcp
ports:
- 80
- 443
cidr_ip: 0.0.0.0/0
rule_desc: "HTTP/HTTPS for tools"
rules_egress:
- proto: all
cidr_ip: 0.0.0.0/0
tags:
Name: "attack-box-sg-{{ deployment_id }}"
DeploymentID: "{{ deployment_id }}"
Type: "attack-box"
register: security_group
- name: Launch attack box EC2 instance
ec2:
key_name: "{{ ssh_key_name }}"
group: "{{ security_group.group_name }}"
instance_type: "{{ instance_type }}"
image: "{{ attack_box_ami }}"
region: "{{ aws_region }}"
aws_access_key: "{{ aws_access_key }}"
aws_secret_key: "{{ aws_secret_key }}"
wait: true
count: 1
instance_tags:
Name: "{{ attack_box_name }}"
DeploymentID: "{{ deployment_id }}"
Type: "attack-box"
Environment: "{{ attack_box_type }}"
user_data: |
#!/bin/bash
# Update system
apt-get update
apt-get upgrade -y
# Install additional tools if needed
{% if attack_box_type == 'kali' %}
# Kali already has most tools
apt-get install -y nmap curl wget git
{% else %}
# Install basic pentest tools for other distros
apt-get install -y nmap curl wget git python3-pip
{% endif %}
# Set hostname
echo "{{ attack_box_name }}" > /etc/hostname
hostname "{{ attack_box_name }}"
# Create deployment info
mkdir -p /root/deployment-info
cat > /root/deployment-info/info.txt << EOF
Deployment ID: {{ deployment_id }}
Attack Box Name: {{ attack_box_name }}
Attack Box Type: {{ attack_box_type }}
Region: {{ aws_region }}
Instance Type: {{ instance_type }}
SSH Key: {{ ssh_key_name }}
EOF
register: ec2_result
- name: Wait for SSH to be available
wait_for:
host: "{{ ec2_result.instances[0].public_ip }}"
port: 22
delay: 60
timeout: 300
- name: Display attack box information
debug:
msg:
- "Attack box deployed successfully!"
- "Instance ID: {{ ec2_result.instances[0].id }}"
- "Public IP: {{ ec2_result.instances[0].public_ip }}"
- "Private IP: {{ ec2_result.instances[0].private_ip }}"
- "SSH Command: ssh -i ~/.ssh/{{ ssh_key_name }} root@{{ ec2_result.instances[0].public_ip }}"
- name: Save deployment information
copy:
content: |
# Attack Box Deployment Information
Instance ID: {{ ec2_result.instances[0].id }}
Public IP: {{ ec2_result.instances[0].public_ip }}
Private IP: {{ ec2_result.instances[0].private_ip }}
SSH Key: ~/.ssh/{{ ssh_key_name }}
SSH Command: ssh -i ~/.ssh/{{ ssh_key_name }} root@{{ ec2_result.instances[0].public_ip }}
Region: {{ aws_region }}
Instance Type: {{ instance_type }}
AMI: {{ attack_box_ami }}
Security Group: {{ security_group.group_name }}
dest: "logs/attack_box_{{ deployment_id }}_info.txt"
- name: Set attack box facts for other playbooks
set_fact:
attack_box_instance_id: "{{ ec2_result.instances[0].id }}"
attack_box_public_ip: "{{ ec2_result.instances[0].public_ip }}"
attack_box_private_ip: "{{ ec2_result.instances[0].private_ip }}"
attack_box_security_group: "{{ security_group.group_name }}"
+63
View File
@@ -0,0 +1,63 @@
---
# AWS-specific phishing infrastructure tasks
- name: Set AWS-specific variables
set_fact:
region: "{{ aws_region | default('us-east-1') }}"
instance_type_map:
gophish: "{{ gophish_instance_type | default('t3.large') }}"
mta_front: "{{ mta_instance_type | default('t3.medium') }}"
redirector: "{{ redirector_instance_type | default('t3.small') }}"
webserver: "{{ webserver_instance_type | default('t3.medium') }}"
- name: Create security group for phishing infrastructure
debug:
msg: "Would create security group: phishing-{{ deployment_id }}"
- name: Deploy Gophish server
debug:
msg: |
Would deploy Gophish server:
- Instance type: {{ instance_type_map.gophish }}
- Region: {{ region }}
- Name: gophish-{{ deployment_id }}
- Framework: gophish
when: "'gophish' in deployment_components"
- name: Deploy MTA Front server
debug:
msg: |
Would deploy MTA Front server:
- Instance type: {{ instance_type_map.mta_front }}
- Region: {{ region }}
- Name: mta-{{ deployment_id }}
- Hostname: {{ mta_hostname | default('mail.' + (phishing_domain | default(domain))) }}
when: "'mta_front' in deployment_components"
- name: Deploy Phishing Redirector
debug:
msg: |
Would deploy Phishing Redirector:
- Instance type: {{ instance_type_map.redirector }}
- Region: {{ region }}
- Name: redirector-{{ deployment_id }}
when: "'redirector' in deployment_components"
- name: Deploy Phishing Webserver
debug:
msg: |
Would deploy Phishing Webserver:
- Instance type: {{ instance_type_map.webserver }}
- Region: {{ region }}
- Name: web-{{ deployment_id }}
when: "'webserver' in deployment_components"
- name: Set deployment results
set_fact:
phishing_deployment_results:
gophish_ip: "{{ '192.168.1.10' if 'gophish' in deployment_components else '' }}"
mta_ip: "{{ '192.168.1.11' if 'mta_front' in deployment_components else '' }}"
redirector_ip: "{{ '192.168.1.12' if 'redirector' in deployment_components else '' }}"
webserver_ip: "{{ '192.168.1.13' if 'webserver' in deployment_components else '' }}"
deployment_id: "{{ deployment_id }}"
domain: "{{ phishing_domain | default(domain) }}"
+45
View File
@@ -0,0 +1,45 @@
aws_access_key: "YOUR_AWS_ACCESS_KEY" # Your AWS access key
aws_secret_key: "YOUR_AWS_SECRET_KEY" # Your AWS secret key
aws_region_choices:
- ap-south-1
- eu-north-1
- eu-west-3
- eu-west-2
- eu-west-1
- ap-northeast-3
- ap-northeast-2
- ap-northeast-1
- ca-central-1
- sa-east-1
- ap-southeast-1
- ap-southeast-2
- eu-central-1
- us-east-1
- us-east-2
- us-west-1
- us-west-2
ami_map:
ap-south-1: ami-0eeeb93aa51c48595
eu-north-1: ami-05bb943edc7d12d2f
eu-west-3: ami-01c1cbe631d766dcd
eu-west-2: ami-0a9aba19a0b8e81da
eu-west-1: ami-05b908c468c3a5373
ap-northeast-3: ami-03809b00a4487dc46
ap-northeast-2: ami-048f3574b7d304c04
ap-northeast-1: ami-0b74305a62f8299e1
ca-central-1: ami-0415ef7b9c3019285
sa-east-1: ami-0ab7401488d50bf51
ap-southeast-1: ami-0d2d12d390e9c0a34
ap-southeast-2: ami-0bd344ea1f492feab
eu-central-1: ami-093d1ceb3279619b0
us-east-1: ami-061b17d332829ab1c
us-east-2: ami-0327cf1c5e479e093
us-west-1: ami-0fbe3a8e1dcd86f23
us-west-2: ami-030d7e8d6fbca8332
aws_instance_type: "t2.medium" # EC2 instance type
domain: "example.com"
mail_hostname: "mail.example.com"
letsencrypt_email: "admin@example.com"
smtp_auth_user: "phishuser"
smtp_auth_pass: "SuperSecretPass123!"
gophish_admin_port: "2222"
+518
View File
@@ -0,0 +1,518 @@
---
# AWS C2 Server Deployment Playbook
- name: Deploy AWS C2 server
hosts: localhost
gather_facts: false
connection: local
vars_files:
- vars.yaml
vars:
# Default values
ssh_user: "{{ ssh_user | default('kali') }}"
aws_region: "{{ aws_region | default(aws_region_choices | random) }}"
instance_type: "{{ aws_instance_type | default('t2.medium') }}"
deployment_id: "{{ deployment_id | default('') }}"
c2_name: "{{ c2_name | default('s-' + deployment_id) }}"
# Define split_regions - only true when regions are explicitly different
split_regions: "{{ c2_region is defined and redirector_region is defined and c2_region != redirector_region }}"
# Only use shared infra when NOT doing split-region deployment
use_shared_infra: "{{ not split_regions and not c2_only | default(false) | bool and not redirector_only | default(false) | bool }}"
# Set correct region variable
aws_c2_region: "{{ c2_region | default(aws_region) }}"
# AMI map comes from vars.yaml - add fallback for safety
kali_ami_map_fallback:
us-east-1: "ami-061b17d332829ab1c"
us-east-2: "ami-061b17d332829ab1c" # Fallback to us-east-1 AMI
tasks:
- name: Validate 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"
# Load shared infrastructure state if available
- name: Check for shared infrastructure state
stat:
path: "infrastructure_state_{{ deployment_id }}.json"
register: infra_state_file
when: use_shared_infra | bool
- name: Load shared infrastructure state
include_vars:
file: "infrastructure_state_{{ deployment_id }}.json"
name: shared_infra
when: use_shared_infra | bool and infra_state_file.stat.exists | default(false)
- name: Set region for C2
set_fact:
aws_c2_region: "{{ shared_infra.region | default(aws_region) }}"
when: use_shared_infra | bool and infra_state_file.stat.exists | default(false)
- name: Set default region for C2
set_fact:
aws_c2_region: "{{ c2_region | default(aws_region) }}"
when: not (use_shared_infra | bool and infra_state_file.stat.exists | default(false))
- name: Check if ami_map is provided in vars.yaml
debug:
msg: "ami_map is {{ 'defined' if ami_map is defined else 'NOT defined' }} in vars.yaml"
- name: Set AMI ID for selected region (from vars.yaml)
set_fact:
ami_id: "{{ ami_map[aws_c2_region] | default(ami_map.us-east-1) }}"
when: ami_map is defined and ami_map
- name: Set AMI ID for selected region (fallback)
set_fact:
ami_id: "{{ kali_ami_map_fallback[aws_c2_region] | default(kali_ami_map_fallback['us-east-1']) }}"
when: ami_id is not defined or ami_id == ""
- name: Ensure we have a valid AMI ID
assert:
that:
- ami_id is defined and ami_id != ""
fail_msg: "Could not determine a valid AMI ID for region {{ aws_c2_region }}. Please add it to ami_map in vars.yaml."
# Add AMI username mapping - improved with better detection
- name: Determine correct SSH user for the AMI
set_fact:
ami_ssh_user: "{{ 'kali' if (ami_id is defined and ami_id is search('-kali-')) or (ami_id is defined and ami_id == 'ami-061b17d332829ab1c') else 'ubuntu' }}"
- name: Display AMI and user information for debugging
debug:
msg:
- "Using AMI ID: {{ ami_id | default('AMI not defined') }}"
- "Detected SSH user: {{ ami_ssh_user }}"
# Create new infrastructure only if not using shared
- name: Create VPC
amazon.aws.ec2_vpc_net:
name: "{{ c2_name }}-vpc"
cidr_block: "10.0.0.0/16"
region: "{{ aws_c2_region }}"
tags:
Name: "{{ c2_name }}-vpc"
deployment_id: "{{ deployment_id }}"
state: present
register: vpc_result
when: not (use_shared_infra | bool and infra_state_file.stat.exists | default(false))
- name: Create internet gateway for VPC
amazon.aws.ec2_vpc_igw:
vpc_id: "{{ vpc_result.vpc.id }}"
region: "{{ aws_c2_region }}"
state: present
tags:
Name: "{{ c2_name }}-igw"
deployment_id: "{{ deployment_id }}"
register: igw_result
when: not (use_shared_infra | bool and infra_state_file.stat.exists | default(false))
- name: Create subnet in VPC
amazon.aws.ec2_vpc_subnet:
vpc_id: "{{ vpc_result.vpc.id }}"
cidr: "10.0.1.0/24"
region: "{{ aws_c2_region }}"
az: "{{ aws_c2_region }}a"
map_public: yes
tags:
Name: "{{ c2_name }}-subnet"
deployment_id: "{{ deployment_id }}"
register: subnet_result
when: not (use_shared_infra | bool and infra_state_file.stat.exists | default(false))
- name: Create routing table for internet access
amazon.aws.ec2_vpc_route_table:
vpc_id: "{{ vpc_result.vpc.id }}"
region: "{{ aws_c2_region }}"
tags:
Name: "{{ c2_name }}-rtb"
deployment_id: "{{ deployment_id }}"
routes:
- dest: "0.0.0.0/0"
gateway_id: "{{ igw_result.gateway_id }}"
subnets:
- "{{ subnet_result.subnet.id }}"
register: route_table_result
when: not (use_shared_infra | bool and infra_state_file.stat.exists | default(false))
# Set VPC ID based on shared or created
- name: Set VPC ID from shared infrastructure
set_fact:
vpc_id: "{{ shared_infra.vpc_id }}"
subnet_id: "{{ shared_infra.subnet_id }}"
c2_vpc_id: "{{ shared_infra.vpc_id }}" # Store for cleanup reference
when: use_shared_infra | bool and infra_state_file.stat.exists | default(false)
- name: Set VPC ID from created infrastructure
set_fact:
vpc_id: "{{ vpc_result.vpc.id }}"
subnet_id: "{{ subnet_result.subnet.id }}"
c2_vpc_id: "{{ vpc_result.vpc.id }}" # Store for cleanup reference
when: not (use_shared_infra | bool and infra_state_file.stat.exists | default(false))
- name: Set default redirector_ip for C2-only deployments
set_fact:
redirector_ip: "{{ operator_ip }}"
when: c2_only | default(false) | bool and redirector_ip is not defined
- name: Load redirector_ip from infrastructure state
block:
- name: Check if infrastructure state file exists
stat:
path: "infrastructure_state_{{ deployment_id }}.json"
register: infra_state_check
- name: Load infrastructure state
include_vars:
file: "infrastructure_state_{{ deployment_id }}.json"
name: infra_state
when: infra_state_check.stat.exists
- name: Set redirector IP from infrastructure state
set_fact:
redirector_ip: "{{ infra_state.redirector_ip | default(operator_ip) }}"
when: infra_state_check.stat.exists and infra_state.redirector_ip is defined
- name: Debug redirector IP
debug:
msg: "Using redirector IP: {{ redirector_ip | default('undefined') }}"
when: redirector_ip is undefined
- name: Default to operator IP if redirector IP is still undefined
set_fact:
redirector_ip: "{{ operator_ip }}"
when: redirector_ip is undefined
- name: Create security group for C2 server
amazon.aws.ec2_security_group:
name: "{{ c2_name }}-sg"
description: "Secured C2 server {{ c2_name }}"
vpc_id: "{{ vpc_id }}"
region: "{{ aws_c2_region }}"
rules:
# Management access only from operator IP
- proto: tcp
ports: 22
cidr_ip: "{{ operator_ip }}/32"
- proto: tcp
ports: "{{ havoc_teamserver_port | default(40056) }}"
cidr_ip: "{{ operator_ip }}/32"
# Allow traffic only from redirector
- proto: tcp
ports:
- 80
- 443
- "{{ havoc_http_port | default(8080) }}"
- "{{ havoc_https_port | default(9443) }}" # Updated from 443
- "{{ havoc_payload_port | default(8443) }}"
- "{{ gophish_admin_port | default(2222) }}"
- "{{ gophish_phish_port | default(8081) }}"
- "{{ tracker_port | default(5000) }}"
cidr_ip: "{{ redirector_ip }}/32"
rules_egress:
- proto: -1
cidr_ip: 0.0.0.0/0
state: present
register: security_group
# Generate or import SSH key for the deployment - FIXED KEY HANDLING
- name: Check if deployment SSH key already exists locally
stat:
path: "~/.ssh/c2deploy_{{ deployment_id }}.pem"
register: ssh_key_file
- name: Generate key pair if it doesn't exist
block:
- name: Create SSH key pair
command: ssh-keygen -t rsa -b 2048 -f ~/.ssh/c2deploy_{{ deployment_id }} -N ""
args:
creates: "~/.ssh/c2deploy_{{ deployment_id }}"
- name: Rename private key to .pem format
command: mv ~/.ssh/c2deploy_{{ deployment_id }} ~/.ssh/c2deploy_{{ deployment_id }}.pem
args:
creates: "~/.ssh/c2deploy_{{ deployment_id }}.pem"
removes: "~/.ssh/c2deploy_{{ deployment_id }}"
when: not ssh_key_file.stat.exists
- name: Ensure proper permissions on SSH key
file:
path: "~/.ssh/c2deploy_{{ deployment_id }}.pem"
mode: '0600'
state: file
- name: Check if key pair exists in AWS
amazon.aws.ec2_key_info:
region: "{{ aws_c2_region }}"
filters:
key-name: "c2deploy_{{ deployment_id }}"
register: existing_key_pair
- name: Import SSH key to AWS
amazon.aws.ec2_key:
name: "c2deploy_{{ deployment_id }}"
key_material: "{{ lookup('file', '~/.ssh/c2deploy_{{ deployment_id }}.pub') }}"
region: "{{ aws_c2_region }}"
state: present
when: existing_key_pair.keypairs | length == 0
# Launch the C2 server - use the consistent key pair name
- name: Launch C2 instance
amazon.aws.ec2_instance:
name: "{{ c2_name }}"
key_name: "c2deploy_{{ deployment_id }}" # Use the imported key
instance_type: "{{ instance_type | default('t2.medium') }}"
vpc_subnet_id: "{{ subnet_id }}"
security_groups:
- "{{ security_group.group_id }}"
image_id: "{{ ami_id }}"
region: "{{ aws_c2_region }}"
state: present
wait: yes
volumes:
- device_name: "/dev/xvda"
ebs:
volume_size: 100
delete_on_termination: true
tags:
Name: "{{ c2_name }}"
deployment_id: "{{ deployment_id }}"
register: c2_instance
- 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: Display C2 instance details for debugging
debug:
msg:
- "C2 IP: {{ c2_ip }}"
- "C2 Instance ID: {{ c2_instance_id }}"
- "SSH User to use: {{ ami_ssh_user }}"
- "SSH Key path: ~/.ssh/c2deploy_{{ deployment_id }}.pem"
- name: Wait for C2 instance initialization
pause:
seconds: 180
when: c2_instance.changed
- name: Set correct permissions on SSH key
file:
path: "~/.ssh/c2deploy_{{ deployment_id }}.pem"
mode: "0600"
- name: Wait for C2 SSH to be available
wait_for:
host: "{{ c2_ip }}"
port: 22
delay: 30
timeout: 300
state: started
- name: Test SSH and prepare remote environment
block:
- name: Ensure .ansible directory exists with proper permissions
shell: |
ssh -i ~/.ssh/c2deploy_{{ deployment_id }}.pem -o StrictHostKeyChecking=no {{ ami_ssh_user }}@{{ c2_ip }} "sudo mkdir -p /root/.ansible/tmp && sudo chmod 0700 /root/.ansible/tmp && sudo chown {{ ami_ssh_user }}:{{ ami_ssh_user }} /root/.ansible/tmp"
register: ssh_prep
until: ssh_prep is success
retries: 5
delay: 15
ignore_errors: yes
delegate_to: localhost
- name: Display SSH preparation results
debug:
msg: "SSH preparation completed: {{ ssh_prep.stdout | default('No output') }}"
# Test SSH connection directly to verify key is working
- name: Test SSH connection to verify key
shell: "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes -i ~/.ssh/c2deploy_{{ deployment_id }}.pem {{ ami_ssh_user }}@{{ c2_ip }} 'echo SSH CONNECTION SUCCESSFUL'"
register: ssh_test
ignore_errors: yes
- name: Display SSH test results
debug:
msg: "{{ ssh_test.stdout | default('SSH Connection failed!') }}"
- name: Add C2 to inventory with updated SSH key path
add_host:
name: "c2"
groups: "c2servers"
ansible_host: "{{ c2_ip }}"
ansible_user: "{{ ami_ssh_user }}"
ansible_ssh_private_key_file: "~/.ssh/c2deploy_{{ deployment_id }}.pem"
ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes"
ansible_python_interpreter: "/usr/bin/python3"
# Add these lines to pass all required variables:
smtp_auth_user: "{{ smtp_auth_user }}"
smtp_auth_pass: "{{ smtp_auth_pass }}"
gophish_admin_port: "{{ gophish_admin_port }}"
domain: "{{ domain }}"
redirector_subdomain: "{{ redirector_subdomain }}"
letsencrypt_email: "{{ letsencrypt_email }}"
havoc_teamserver_port: "{{ havoc_teamserver_port | default(40056) }}"
havoc_http_port: "{{ havoc_http_port | default(8080) }}"
havoc_https_port: "{{ havoc_https_port | default(9443) }}"
zero_logs: "{{ zero_logs | default(true) }}"
secure_memory: "{{ secure_memory | default(true) }}"
disable_history: "{{ disable_history | default(true) }}"
setup_integrated_tracker: "{{ setup_integrated_tracker | default(false) }}"
tracker_domain: "{{ tracker_domain | default('track.' + domain) | default('') }}"
# Configure C2 server with a proper structure
- name: Configure C2 server
hosts: c2servers
become: yes
become_user: root
gather_facts: true
vars_files:
- vars.yaml # Add this line to load the variables
vars:
redirector_ip: "{{ hostvars['localhost']['redirector_ip'] | default('127.0.0.1') }}"
c2_subdomain: "{{ c2_subdomain | default('mail') }}"
tasks:
- name: Install python3 if it doesn't exist on target
raw: test -e /usr/bin/python3 || (apt-get update && apt-get install -y python3)
args:
executable: /bin/bash
register: python_install
ignore_errors: yes
- name: Debug connection information
debug:
msg:
- "Connected to C2 server successfully"
- "Host: {{ ansible_host }}"
- "User: {{ ansible_user }}"
- "Python version: {{ ansible_python_version | default('unknown') }}"
- name: Download Kali archive keyring to temporary location
get_url:
url: https://archive.kali.org/archive-keyring.gpg
dest: /tmp/kali-archive-keyring.gpg
mode: "0644"
force: yes
register: keyring_download
- name: Install Kali archive keyring
command: install -m 0644 /tmp/kali-archive-keyring.gpg /usr/share/keyrings/kali-archive-keyring.gpg
when: keyring_download is changed
- name: Wait for apt to be available
apt:
update_cache: yes
register: apt_result
until: apt_result is success
retries: 10
delay: 10
- name: Install Rust compiler
apt:
name:
- cargo
- rustc
- libssl-dev
- pkg-config
state: present
register: rust_install
until: rust_install is success
retries: 3
delay: 5
- name: Clean up any failed pipx installations
file:
path: "{{ item }}"
state: absent
with_items:
- "/root/.local/state/pipx/venvs/netexec"
- "/root/.local/state/pipx/venvs/trevorspray"
ignore_errors: yes
- name: Include common tool installation tasks
include_tasks: "../../common/tasks/install_tools.yml"
- name: Include common C2 configuration tasks
include_tasks: "../../modules/c2/tasks/configure_c2.yml"
- name: Include common security hardening tasks
include_tasks: "../../common/tasks/security_hardening.yml"
- name: Include common mail server configuration tasks
include_tasks: "../../common/tasks/configure_mail.yml"
- name: Set up SSH access to redirector
block:
- name: Ensure /root/.ssh directory exists on C2 server
file:
path: /root/.ssh
state: directory
mode: '0700'
owner: root
group: root
- name: Copy private SSH key to C2 server for redirector access
copy:
src: "{{ playbook_dir }}/../.ssh/c2deploy_{{ hostvars['localhost']['deployment_id'] }}.pem"
dest: "/root/.ssh/c2deploy_{{ hostvars['localhost']['deployment_id'] }}.pem"
mode: '0600'
owner: root
group: root
register: key_copy
ignore_errors: yes
- name: If direct path fails, try home directory location
copy:
src: "~/.ssh/c2deploy_{{ hostvars['localhost']['deployment_id'] }}.pem"
dest: "/root/.ssh/c2deploy_{{ hostvars['localhost']['deployment_id'] }}.pem"
mode: '0600'
owner: root
group: root
when: key_copy is failed
- name: Create SSH config file to use key automatically
copy:
dest: "/root/.ssh/config"
content: |
Host redirector
HostName {{ hostvars['localhost']['redirector_ip'] }}
User ubuntu
IdentityFile /root/.ssh/c2deploy_{{ hostvars['localhost']['deployment_id'] }}.pem
StrictHostKeyChecking no
UserKnownHostsFile /dev/null
mode: '0600'
owner: root
group: root
- name: Create alias for easy redirector access
lineinfile:
path: /root/.bashrc
line: 'alias redirector="ssh -i /root/.ssh/c2deploy_{{ hostvars["localhost"]["deployment_id"] }}.pem ubuntu@{{ hostvars["localhost"]["redirector_ip"] }}"'
state: present
- name: Test SSH from C2 to redirector
shell: |
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/c2deploy_{{ hostvars['localhost']['deployment_id'] }}.pem ubuntu@{{ hostvars['localhost']['redirector_ip'] }} "echo 'SSH CONNECTION SUCCESSFUL FROM C2'"
register: ssh_test_result
changed_when: false
ignore_errors: yes
- name: Display SSH test result
debug:
msg: "{{ ssh_test_result.stdout if ssh_test_result.rc == 0 else 'SSH connection failed: ' + ssh_test_result.stderr }}"
- 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/c2deploy_{{ hostvars['localhost']['deployment_id'] }}.pem"
when: not disable_summary | default(false)
+465
View File
@@ -0,0 +1,465 @@
---
# AWS Cleanup Playbook - Comprehensive version with robust VPC removal
- name: Clean up AWS resources
hosts: localhost
gather_facts: false
connection: local
vars_files:
- vars.yaml
vars:
aws_region: "{{ aws_region | default(aws_region_choices | random) }}"
confirm_cleanup: false # Skip confirmation in automated teardown
deployment_id: "{{ deployment_id | default('') }}"
redirector_name: "{{ redirector_name | default('r-' + deployment_id) }}"
c2_name: "{{ c2_name | default('s-' + deployment_id) }}"
tracker_name: "{{ tracker_name | default('t-' + deployment_id) }}"
cleanup_summary: {}
tasks:
# Confirmation step (if enabled)
- name: Confirm cleanup
pause:
prompt: "Are you sure you want to delete all AWS resources for deployment ID {{ deployment_id }}? This action cannot be undone. Type 'yes' to confirm"
register: cleanup_confirmation
when: confirm_cleanup | bool
- name: Check confirmation
assert:
that:
- cleanup_confirmation.user_input | default('yes') == 'yes'
fail_msg: "Cleanup cancelled by user"
when: confirm_cleanup | bool
# STEP 1: Find all instances by deployment ID
- name: Find all EC2 instances for this deployment
amazon.aws.ec2_instance_info:
region: "{{ aws_region }}"
filters:
"tag:deployment_id": "{{ deployment_id }}"
register: deployment_instances
- name: Set fact for instances found
set_fact:
cleanup_summary: "{{ cleanup_summary | combine({'instances_found': deployment_instances.instances | length}) }}"
# STEP 2: Terminate all instances with proper tagging
- name: Terminate all instances for this deployment
amazon.aws.ec2_instance:
instance_ids: "{{ item.instance_id }}"
region: "{{ aws_region }}"
state: absent
loop: "{{ deployment_instances.instances }}"
register: terminated_instances
when: deployment_instances.instances | length > 0
- name: Wait for instances to be terminated
pause:
seconds: 30
when: deployment_instances.instances | length > 0
# STEP 3: Find all security groups by deployment ID
- name: Find all security groups for this deployment
amazon.aws.ec2_security_group_info:
region: "{{ aws_region }}"
filters:
"tag:deployment_id": "{{ deployment_id }}"
register: deployment_sgs
# Also find SGs by name pattern
- name: Find security groups by name pattern
amazon.aws.ec2_security_group_info:
region: "{{ aws_region }}"
register: all_sgs
- name: Filter SGs by name pattern
set_fact:
named_sgs: "{{ all_sgs.security_groups | selectattr('group_name', 'search', redirector_name + '-sg|' + c2_name + '-sg') | list }}"
- name: Combine all security groups to delete
set_fact:
all_sgs_to_delete: "{{ deployment_sgs.security_groups + named_sgs }}"
cleanup_summary: "{{ cleanup_summary | combine({'security_groups_found': (deployment_sgs.security_groups + named_sgs) | length}) }}"
# STEP 4: Delete all security groups
- name: Delete security groups
amazon.aws.ec2_security_group:
group_id: "{{ item.group_id }}"
region: "{{ aws_region }}"
state: absent
loop: "{{ all_sgs_to_delete }}"
when: all_sgs_to_delete | length > 0
ignore_errors: yes
register: deleted_sgs
# STEP 5: Find and delete all ENIs
- name: Find network interfaces by tag
amazon.aws.ec2_eni_info:
region: "{{ aws_region }}"
filters:
"tag:deployment_id": "{{ deployment_id }}"
register: deployment_enis
- name: Delete ENIs
amazon.aws.ec2_eni:
region: "{{ aws_region }}"
eni_id: "{{ item.id }}"
state: absent
force_detach: true
loop: "{{ deployment_enis.network_interfaces }}"
ignore_errors: yes
register: deleted_enis
when: deployment_enis.network_interfaces | length > 0
- name: Set ENIs count in summary
set_fact:
cleanup_summary: "{{ cleanup_summary | combine({'enis_found': deployment_enis.network_interfaces | length}) }}"
# STEP 6: Find all VPCs by deployment ID
- name: Find all VPCs for this deployment
amazon.aws.ec2_vpc_net_info:
region: "{{ aws_region }}"
filters:
"tag:deployment_id": "{{ deployment_id }}"
register: deployment_vpcs
# STEP 7: Find VPCs by name pattern as fallback
- name: Find all VPCs by name pattern
amazon.aws.ec2_vpc_net_info:
region: "{{ aws_region }}"
register: all_vpcs
- name: Filter VPCs by name pattern
set_fact:
named_vpcs: "{{ all_vpcs.vpcs | selectattr('tags', 'defined') | selectattr('tags.Name', 'defined') | selectattr('tags.Name', 'search', redirector_name + '-vpc|' + c2_name + '-vpc') | list }}"
- name: Combine all VPCs to delete
set_fact:
all_vpcs_to_delete: "{{ deployment_vpcs.vpcs + named_vpcs | unique(attribute='vpc_id') }}"
cleanup_summary: "{{ cleanup_summary | combine({'vpcs_found': (deployment_vpcs.vpcs + named_vpcs | unique(attribute='vpc_id')) | length}) }}"
# STEP 8: Find and delete NAT Gateways for each VPC separately
- name: Find NAT gateways in each VPC
amazon.aws.ec2_vpc_nat_gateway_info:
region: "{{ aws_region }}"
filters:
vpc-id: "{{ item.vpc_id }}"
register: natgw_results
loop: "{{ all_vpcs_to_delete }}"
when: all_vpcs_to_delete | length > 0
- name: Delete NAT gateways
amazon.aws.ec2_vpc_nat_gateway:
region: "{{ aws_region }}"
nat_gateway_id: "{{ item.1.nat_gateway_id }}"
state: absent
release_eip: true
loop: "{{ natgw_results.results | default([]) | selectattr('skipped', 'undefined') | selectattr('nat_gateways', 'defined') | subelements('nat_gateways') }}"
ignore_errors: yes
register: deleted_natgws
when: natgw_results.results is defined
- name: Wait after NAT deletion
pause:
seconds: 15
when: deleted_natgws.results is defined and deleted_natgws.results | length > 0
# STEP 9: Find and delete Internet Gateways
- name: Find internet gateways for each VPC
amazon.aws.ec2_vpc_igw_info:
region: "{{ aws_region }}"
filters:
attachment.vpc-id: "{{ item.vpc_id }}"
register: igw_results
loop: "{{ all_vpcs_to_delete }}"
when: all_vpcs_to_delete | length > 0
- name: Detach and delete internet gateways
amazon.aws.ec2_vpc_igw:
internet_gateway_id: "{{ item.1.internet_gateway_id }}"
state: absent
region: "{{ aws_region }}"
loop: "{{ igw_results.results | default([]) | subelements('internet_gateways') }}"
ignore_errors: yes
register: deleted_igws
- name: Wait after IGW deletion
pause:
seconds: 15
when: deleted_igws.results is defined and deleted_igws.results | length > 0
# STEP 10: Find and delete Route Tables
- name: Find route tables for each VPC
amazon.aws.ec2_vpc_route_table_info:
region: "{{ aws_region }}"
filters:
vpc-id: "{{ item.vpc_id }}"
register: rtb_results
loop: "{{ all_vpcs_to_delete }}"
when: all_vpcs_to_delete | length > 0
- name: Delete non-main route tables
amazon.aws.ec2_vpc_route_table:
region: "{{ aws_region }}"
route_table_id: "{{ item.1.id }}"
lookup: id
state: absent
loop: "{{ rtb_results.results | default([]) | selectattr('skipped', 'undefined') | selectattr('route_tables', 'defined') | subelements('route_tables') }}"
when: not item.1.associations[0].main | default(false)
ignore_errors: yes
register: deleted_rtbs
# Add this after your existing route table deletion
- name: Delete main route tables with AWS CLI
shell: |
for rtb in $(aws ec2 describe-route-tables --region {{ aws_region }} --filters "Name=vpc-id,Values={{ item.vpc_id }}" --query 'RouteTables[?Associations[?Main==`true`]].RouteTableId' --output text); do
aws ec2 delete-route --route-table-id $rtb --destination-cidr-block 0.0.0.0/0 --region {{ aws_region }} || true
done
environment:
AWS_ACCESS_KEY_ID: "{{ aws_access_key }}"
AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}"
loop: "{{ all_vpcs_to_delete }}"
ignore_errors: yes
when: all_vpcs_to_delete | length > 0
# STEP 11: Find and delete Subnets
- name: Find subnets for each VPC
amazon.aws.ec2_vpc_subnet_info:
region: "{{ aws_region }}"
filters:
vpc-id: "{{ item.vpc_id }}"
loop: "{{ all_vpcs_to_delete }}"
register: subnet_results
- name: Delete subnets
amazon.aws.ec2_vpc_subnet:
region: "{{ aws_region }}"
vpc_id: "{{ item.1.vpc_id }}"
cidr: "{{ item.1.cidr_block }}"
state: absent
loop: "{{ subnet_results.results | default([]) | subelements('subnets') }}"
ignore_errors: yes
register: deleted_subnets
when: subnet_results.results is defined
# STEP 12: Find and delete VPC Endpoints
- name: Find VPC endpoints for each VPC
amazon.aws.ec2_vpc_endpoint_info:
region: "{{ aws_region }}"
filters:
vpc-id: "{{ item.vpc_id }}"
register: endpoint_results
loop: "{{ all_vpcs_to_delete }}"
when: all_vpcs_to_delete | length > 0
- name: Delete VPC endpoints
amazon.aws.ec2_vpc_endpoint:
region: "{{ aws_region }}"
vpc_endpoint_id: "{{ item.1.vpc_endpoint_id }}"
state: absent
loop: "{{ endpoint_results.results | default([]) | selectattr('skipped', 'undefined') | selectattr('vpc_endpoints', 'defined') | subelements('vpc_endpoints') }}"
ignore_errors: yes
register: deleted_endpoints
when: endpoint_results.results is defined
# Add before STEP 13
- name: Check for remaining VPC dependencies
shell: |
aws ec2 describe-network-interfaces --region {{ aws_region }} --filters "Name=vpc-id,Values={{ item.vpc_id }}" --output json
environment:
AWS_ACCESS_KEY_ID: "{{ aws_access_key }}"
AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}"
register: remaining_deps
loop: "{{ all_vpcs_to_delete }}"
when: all_vpcs_to_delete | length > 0
- name: Display any remaining dependencies
debug:
msg: "VPC {{ item.item.vpc_id }} still has dependencies that need to be removed"
loop: "{{ remaining_deps.results }}"
when: item.stdout | from_json | json_query('NetworkInterfaces') | length > 0
# Add this before the force delete of network interfaces
- name: Detach remaining network interfaces
shell: |
aws ec2 detach-network-interface --attachment-id $(aws ec2 describe-network-interfaces --network-interface-ids {{ item.1 }} --query 'NetworkInterfaces[0].Attachment.AttachmentId' --output text) --region {{ aws_region }} --force
environment:
AWS_ACCESS_KEY_ID: "{{ aws_access_key }}"
AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}"
loop: "{{ remaining_deps.results | selectattr('stdout', 'defined') |
map('attr', 'stdout') | map('from_json') |
map('json_query', 'NetworkInterfaces[?Status==`in-use`].NetworkInterfaceId') |
zip(remaining_deps.results | map('attr', 'item')) | list }}"
ignore_errors: yes
when: item.0 | length > 0
- name: Force delete any remaining network interfaces
shell: |
aws ec2 delete-network-interface --network-interface-id {{ item.1 }} --region {{ aws_region }}
environment:
AWS_ACCESS_KEY_ID: "{{ aws_access_key }}"
AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}"
loop: "{{ remaining_deps.results | selectattr('stdout', 'defined') |
map('attr', 'stdout') | map('from_json') |
map('json_query', 'NetworkInterfaces[].NetworkInterfaceId') |
zip(remaining_deps.results | map('attr', 'item')) | list }}"
ignore_errors: yes
when: item.0 | length > 0
# STEP 13: Final VPC deletion with multiple retries
- name: Wait for all dependencies to clear
pause:
seconds: 20
when: all_vpcs_to_delete | length > 0
# First attempt with normal module - with error display
- name: Delete all VPCs (first attempt)
amazon.aws.ec2_vpc_net:
vpc_id: "{{ item.vpc_id }}"
region: "{{ aws_region }}"
state: absent
loop: "{{ all_vpcs_to_delete }}"
register: vpc_deletion
when: all_vpcs_to_delete | length > 0
ignore_errors: yes
- name: Display VPC deletion errors
debug:
msg: "Failed to delete VPC {{ item.item.vpc_id }}: {{ item.msg }}"
loop: "{{ vpc_deletion.results | default([]) }}"
when: item.failed is defined and item.failed
# Direct API call for any VPCs that failed
- name: Find which VPCs still exist
amazon.aws.ec2_vpc_net_info:
region: "{{ aws_region }}"
vpc_ids: "{{ all_vpcs_to_delete | map(attribute='vpc_id') | list }}"
register: remaining_vpcs
when: all_vpcs_to_delete | length > 0
# Forcibly delete with direct AWS CLI command
- name: Force delete remaining VPCs with CLI
shell: |
aws ec2 delete-vpc --vpc-id {{ item.vpc_id }} --region {{ aws_region }}
environment:
AWS_ACCESS_KEY_ID: "{{ aws_access_key }}"
AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}"
loop: "{{ remaining_vpcs.vpcs }}"
ignore_errors: yes
when: remaining_vpcs is defined and remaining_vpcs.vpcs | length > 0
register: force_vpc_delete
# Add after the VPC deletion attempts - more aggressive approach
- name: Force delete remaining VPCs with AWS CLI and debug output
shell: |
aws ec2 delete-vpc --vpc-id {{ item.vpc_id }} --region {{ aws_region }} 2>&1 || echo "Failed with: $?"
environment:
AWS_ACCESS_KEY_ID: "{{ aws_access_key }}"
AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}"
loop: "{{ remaining_vpcs.vpcs }}"
register: force_vpc_delete_debug
when: remaining_vpcs is defined and remaining_vpcs.vpcs | length > 0
- name: Display debug output from force delete
debug:
msg: "{{ item.stdout }}"
loop: "{{ force_vpc_delete_debug.results | default([]) }}"
when: item.stdout is defined and item.stdout | trim != ""
# Track deleted VPCs in summary
- name: Set VPC deletion results in summary
set_fact:
cleanup_summary: "{{ cleanup_summary | combine({
'vpcs_deleted': ((vpc_deletion.results | default([]) | selectattr('failed', 'undefined') | list | length) + (force_vpc_delete.results | default([]) | selectattr('failed', 'undefined') | list | length))}) }}"
when: all_vpcs_to_delete | length > 0
# Add these tasks to confirm VPC deletion
- name: Verify VPC deletion
amazon.aws.ec2_vpc_net_info:
region: "{{ aws_region }}"
filters:
"tag:deployment_id": "{{ deployment_id }}"
register: vpc_check
- name: Display cleanup summary
debug:
msg:
- "Cleanup Summary:"
- "Redirector instance deleted: {{ redirector_deleted | default('N/A') }}"
- "C2 instance deleted: {{ c2_deleted | default('N/A') }}"
- "VPC resources deleted: {{ vpc_check.vpcs | length == 0 }}"
when: not disable_summary | default(false)
# STEP 14: Delete key pairs - completely revised implementation
- name: Check for deployment key in AWS
shell: |
aws ec2 describe-key-pairs --region {{ aws_region }} --filters "Name=key-name,Values=c2deploy_{{ deployment_id }}" --query "KeyPairs[*].KeyName" --output text
environment:
AWS_ACCESS_KEY_ID: "{{ aws_access_key }}"
AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}"
register: keypair_check
ignore_errors: yes
- name: Delete deployment key pair if it exists
amazon.aws.ec2_key:
name: "c2deploy_{{ deployment_id }}"
region: "{{ aws_region }}"
state: absent
when: keypair_check.stdout | trim != ""
register: deleted_keypair
- name: Delete SSH key files
file:
path: "{{ item }}"
state: absent
with_items:
- "~/.ssh/c2deploy_{{ deployment_id }}.pem"
- "~/.ssh/c2deploy_{{ deployment_id }}.pub"
- "~/.ssh/{{ redirector_name }}.pem"
- "~/.ssh/{{ c2_name }}.pem"
- "~/.ssh/{{ tracker_name }}.pem"
ignore_errors: yes
register: deleted_ssh_files
- name: Count deleted SSH files
set_fact:
cleanup_summary: "{{ cleanup_summary | combine({
'keypairs_deleted': (deleted_keypair.changed | default(false)) | ternary(1, 0),
'ssh_files_deleted': (deleted_ssh_files.results | selectattr('changed', 'defined') | selectattr('changed') | list | length)}) }}"
# Remove infrastructure state file - fix path to include deployment_id
- name: Remove infrastructure state file
file:
path: "infrastructure_state_{{ deployment_id }}.json"
state: absent
ignore_errors: yes
register: infra_file
- name: Clean up deployment state file
file:
path: "{{ playbook_dir }}/../deployment_state_{{ deployment_id }}.json"
state: absent
ignore_errors: yes
register: state_file_deletion
- name: Report state file cleanup
debug:
msg: "Deployment state file {{ 'deleted' if state_file_deletion.changed else 'not found' }}"
when: state_file_deletion is defined
# STEP 16: Enhanced and Accurate Cleanup Summary
- name: Enhanced cleanup summary
debug:
msg:
- "=========================================================="
- " AWS CLEANUP SUMMARY: {{ deployment_id }} "
- "=========================================================="
- "EC2 Instances: {{ cleanup_summary.instances_found | default(0) }} found, {{ terminated_instances.results | default([]) | length }} terminated"
- "Security Groups: {{ cleanup_summary.security_groups_found | default(0) }} found, {{ deleted_sgs.results | default([]) | length }} deleted"
- "Network Interfaces: {{ cleanup_summary.enis_found | default(0) }} found, {{ deleted_enis.results | default([]) | length }} deleted"
- "VPCs: {{ cleanup_summary.vpcs_found | default(0) }} found, {{ cleanup_summary.vpcs_deleted | default(0) }} deleted"
- "Key Pairs: {{ deleted_key_pairs.results | default([]) | length }} deleted"
- "SSH Key Files: {{ cleanup_summary.ssh_files_deleted | default(0) }} deleted"
- "Infrastructure file: {{ 'Removed' if infra_file.changed else 'Not found' }}"
- "=========================================================="
- "CLEANUP {{ 'COMPLETED' if (cleanup_summary.vpcs_deleted | default(0) == cleanup_summary.vpcs_found | default(0)) else 'PARTIAL - SOME RESOURCES MAY REMAIN' }}"
- "========================================================="
+115
View File
@@ -0,0 +1,115 @@
---
# AWS Shared Infrastructure Deployment Playbook
- name: Deploy shared AWS infrastructure
hosts: localhost
gather_facts: false
connection: local
vars_files:
- vars.yaml
vars:
# Deployment identifiers
deployment_id: "{{ deployment_id | default('') }}"
infra_name: "infra-{{ deployment_id }}"
# Region settings
deployment_region: "{{ aws_region | default(aws_region_choices | random) }}"
# Check if using split regions (C2 and redirector in different regions)
split_regions: "{{ c2_region is defined and redirector_region is defined and c2_region != redirector_region }}"
# Check if only deploying one component (C2 only or redirector only)
single_component: "{{ c2_only | default(false) | bool or redirector_only | default(false) | bool }}"
tasks:
- name: Validate 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"
- name: Skip shared infrastructure if using split regions
meta: end_play
when: split_regions | bool
- name: Print infrastructure deployment info
debug:
msg: "Deploying shared infrastructure in {{ deployment_region }}"
when: not split_regions | bool and not single_component | bool
- name: Check if deploying just C2 or just redirector
debug:
msg: "Skipping shared infrastructure for single component deployment ({{ 'C2 only' if c2_only | default(false) else 'Redirector only' }})"
when: single_component | bool
- name: Skip shared infrastructure for single component deployment
meta: end_play
when: single_component | bool
- name: Create shared VPC
amazon.aws.ec2_vpc_net:
name: "{{ infra_name }}-vpc"
cidr_block: "10.0.0.0/16"
region: "{{ deployment_region }}"
tags:
Name: "{{ infra_name }}-vpc"
deployment_id: "{{ deployment_id }}"
state: present
register: vpc_result
when: not split_regions | bool and not single_component | bool
- name: Store shared VPC ID
set_fact:
shared_vpc_id: "{{ vpc_result.vpc.id }}"
when: not split_regions | bool and not single_component | bool and vpc_result is defined
- name: Create internet gateway
amazon.aws.ec2_vpc_igw:
vpc_id: "{{ shared_vpc_id }}"
region: "{{ deployment_region }}"
state: present
tags:
Name: "{{ infra_name }}-igw"
deployment_id: "{{ deployment_id }}"
register: igw_result
when: not split_regions | bool and not single_component | bool and shared_vpc_id is defined
- name: Create subnet
amazon.aws.ec2_vpc_subnet:
vpc_id: "{{ shared_vpc_id }}"
cidr: "10.0.1.0/24"
region: "{{ deployment_region }}"
az: "{{ deployment_region }}a"
map_public: yes
tags:
Name: "{{ infra_name }}-subnet"
deployment_id: "{{ deployment_id }}"
register: subnet_result
when: not split_regions | bool and not single_component | bool and shared_vpc_id is defined
- name: Create routing table
amazon.aws.ec2_vpc_route_table:
vpc_id: "{{ shared_vpc_id }}"
region: "{{ deployment_region }}"
tags:
Name: "{{ infra_name }}-rtb"
deployment_id: "{{ deployment_id }}"
routes:
- dest: "0.0.0.0/0"
gateway_id: "{{ igw_result.gateway_id }}"
subnets:
- "{{ subnet_result.subnet.id }}"
register: route_table_result
when: not split_regions | bool and not single_component | bool and shared_vpc_id is defined and igw_result is defined and subnet_result is defined
- name: Write infrastructure info to state file
copy:
content: |
{
"vpc_id": "{{ shared_vpc_id }}",
"subnet_id": "{{ subnet_result.subnet.id }}",
"igw_id": "{{ igw_result.gateway_id }}",
"region": "{{ deployment_region }}",
"deployment_id": "{{ deployment_id }}"
}
dest: "infrastructure_state.json"
mode: "0600"
when: not split_regions | bool and not single_component | bool and shared_vpc_id is defined and subnet_result is defined and igw_result is defined
+65
View File
@@ -0,0 +1,65 @@
---
# Phishing infrastructure deployment playbook
# This is a comprehensive playbook that handles all phishing components
- name: Deploy Phishing Infrastructure
hosts: localhost
gather_facts: true
connection: local
vars_files:
- vars.yaml
tasks:
- name: Display deployment information
debug:
msg: |
Deploying phishing infrastructure
Deployment ID: {{ deployment_id }}
Provider: {{ provider }}
Domain: {{ domain | default(phishing_domain) }}
Components: {{ deployment_type }}
- name: Set deployment facts
set_fact:
deployment_timestamp: "{{ ansible_date_time.epoch }}"
phishing_deployment_results: {}
deployment_components: >-
{% set components = [] %}
{% if deployment_type in ['gophish_only', 'basic_phishing', 'advanced_phishing', 'full_phishing', 'fedramp_phishing'] %}
{% set _ = components.append('gophish') %}
{% endif %}
{% if deployment_type in ['mta_front_only', 'basic_phishing', 'advanced_phishing', 'full_phishing', 'ephemeral_mta'] %}
{% set _ = components.append('mta_front') %}
{% endif %}
{% if deployment_type in ['phishing_redirector_only', 'advanced_phishing', 'full_phishing'] %}
{% set _ = components.append('redirector') %}
{% endif %}
{% if deployment_type in ['phishing_webserver_only', 'full_phishing', 'fedramp_phishing'] %}
{% set _ = components.append('webserver') %}
{% endif %}
{{ components }}
- name: Include provider-specific tasks
include_tasks: "{{ provider }}_phishing.yml"
when: deployment_components | length > 0
- name: Ensure logs directory exists
file:
path: "../../logs"
state: directory
mode: '0755'
delegate_to: localhost
- name: Save deployment information
template:
src: phishing_deployment_info.j2
dest: "{{ playbook_dir }}/logs/phishing_deployment_{{ deployment_id }}.json"
delegate_to: localhost
- name: Display success message
debug:
msg: |
✅ Phishing infrastructure deployment completed
Deployment ID: {{ deployment_id }}
Components deployed: {{ deployment_components | join(', ') }}
Check logs/phishing_deployment_{{ deployment_id }}.json for details
+14
View File
@@ -0,0 +1,14 @@
---
# VPC Cleanup Process with enhanced dependency handling
# Removed detailed cleanup tasks for ENIs, RTs, IGWs, NATs, subnets, SGs to simplify deletion
# Step X: Delete VPC directly
- name: Delete VPC {{ vpc_id }}
amazon.aws.ec2_vpc_net:
region: "{{ aws_region }}"
vpc_id: "{{ vpc_id }}"
state: absent
retries: 5
delay: 15
register: vpc_delete_result
until: vpc_delete_result is success
+379
View File
@@ -0,0 +1,379 @@
---
# AWS Redirector Deployment Playbook
- name: Deploy AWS redirector
hosts: localhost
gather_facts: false
connection: local
vars_files:
- vars.yaml
vars:
# Default values
ssh_user: "{{ ssh_user | default('ubuntu') }}"
aws_region: "{{ aws_region | default(aws_region_choices | random) }}"
instance_type: "{{ aws_instance_type | default('t2.micro') }}"
deployment_id: "{{ deployment_id | default('') }}"
redirector_name: "{{ redirector_name | default('r-' + deployment_id) }}"
effective_listen_port: "{{ shell_handler_port | default(4488) }}"
# Check for shared infrastructure
# Only use shared when C2 and redirector are in the same region
use_shared_infra: "{{ not (c2_region is defined and redirector_region is defined and c2_region != redirector_region) and not c2_only | default(false) | bool and not redirector_only | default(false) | bool }}"
# Ubuntu AMI IDs for different regions (Ubuntu 22.04 LTS)
ubuntu_ami_map:
us-east-1: "ami-0aa2b7722dc1b5612"
us-east-2: "ami-06c4532923d4ba1ec"
us-west-1: "ami-0573b70afecda915d"
us-west-2: "ami-0c79c59ac2c572b87"
eu-west-1: "ami-0694d931cee176e7d"
eu-west-2: "ami-0505148b3591e4c07"
eu-central-1: "ami-06dd92ecc74fdfb36"
ap-southeast-1: "ami-0df7a207adb9748c7"
ap-southeast-2: "ami-0df4b2961410d4cff"
ap-northeast-1: "ami-0014b5f031a76c1b1"
sa-east-1: "ami-0af6e9042ea5a4e3e"
tasks:
- name: Validate 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 region for redirector - fix for noop task error
- name: Set region for redirector
set_fact:
aws_redirector_region: "{{ redirector_region | default(aws_region) }}"
# Load shared infrastructure state if available - fixed implementation
- name: Check for shared infrastructure state
block:
- name: Check if state file exists
stat:
path: "infrastructure_state_{{ deployment_id }}.json"
register: infra_state_file
- name: Include vars if file exists
include_vars:
file: "infrastructure_state_{{ deployment_id }}.json"
name: shared_infra
when: infra_state_file.stat.exists | default(false)
when: use_shared_infra | bool
# After loading shared infrastructure state
- name: Validate shared VPC exists
amazon.aws.ec2_vpc_net_info:
region: "{{ shared_infra.region }}"
vpc_ids:
- "{{ shared_infra.vpc_id }}"
register: vpc_check
when: use_shared_infra | bool and infra_state_file.stat.exists | default(false)
ignore_errors: yes
- name: Delete stale infrastructure state file
file:
path: "infrastructure_state.json"
state: absent
when: use_shared_infra | bool and vpc_check.vpcs is defined and vpc_check.vpcs | length == 0
- name: Disable shared infrastructure when VPC doesn't exist
set_fact:
use_shared_infra: false
when: use_shared_infra | bool and vpc_check.vpcs is defined and vpc_check.vpcs | length == 0
- name: Set region variables from shared infra
set_fact:
aws_redirector_region: "{{ shared_infra.region | default(aws_region) }}"
when: use_shared_infra | bool and infra_state_file.stat.exists | default(false)
- name: Set AMI ID for selected region
set_fact:
ami_id: "{{ ubuntu_ami_map[aws_redirector_region] | default(ubuntu_ami_map['us-east-1']) }}"
# Create new infrastructure only if not using shared
- name: Create VPC
amazon.aws.ec2_vpc_net:
name: "{{ redirector_name }}-vpc"
cidr_block: "10.0.0.0/16"
region: "{{ aws_redirector_region }}"
tags:
Name: "{{ redirector_name }}-vpc"
deployment_id: "{{ deployment_id }}"
state: present
register: vpc_result
when: not (use_shared_infra | bool and infra_state_file.stat.exists | default(false))
- name: Create internet gateway for VPC
amazon.aws.ec2_vpc_igw:
vpc_id: "{{ vpc_result.vpc.id }}"
region: "{{ aws_redirector_region }}"
state: present
tags:
Name: "{{ redirector_name }}-igw"
deployment_id: "{{ deployment_id }}"
register: igw_result
when: not (use_shared_infra | bool and infra_state_file.stat.exists | default(false))
- name: Create subnet in VPC
amazon.aws.ec2_vpc_subnet:
vpc_id: "{{ vpc_result.vpc.id }}"
cidr: "10.0.1.0/24"
region: "{{ aws_redirector_region }}"
az: "{{ aws_redirector_region }}a"
map_public: yes
tags:
Name: "{{ redirector_name }}-subnet"
deployment_id: "{{ deployment_id }}"
register: subnet_result
when: not (use_shared_infra | bool and infra_state_file.stat.exists | default(false))
- name: Create routing table for internet access
amazon.aws.ec2_vpc_route_table:
vpc_id: "{{ vpc_result.vpc.id }}"
region: "{{ aws_redirector_region }}"
tags:
Name: "{{ redirector_name }}-rtb"
deployment_id: "{{ deployment_id }}"
routes:
- dest: "0.0.0.0/0"
gateway_id: "{{ igw_result.gateway_id }}"
subnets:
- "{{ subnet_result.subnet.id }}"
register: route_table_result
when: not (use_shared_infra | bool and infra_state_file.stat.exists | default(false))
# Set VPC ID based on shared or created
- name: Set VPC ID from shared infrastructure
set_fact:
vpc_id: "{{ shared_infra.vpc_id }}"
subnet_id: "{{ shared_infra.subnet_id }}"
redirector_vpc_id: "{{ shared_infra.vpc_id }}" # Store for cleanup reference
when: use_shared_infra | bool and infra_state_file.stat.exists | default(false)
- name: Set VPC ID from created infrastructure
set_fact:
vpc_id: "{{ vpc_result.vpc.id }}"
subnet_id: "{{ subnet_result.subnet.id }}"
redirector_vpc_id: "{{ vpc_result.vpc.id }}" # Store for cleanup reference
when: not (use_shared_infra | bool and infra_state_file.stat.exists | default(false))
- name: Set default c2_ip for redirector-only deployments
set_fact:
c2_ip: "{{ operator_ip }}"
when: redirector_only | default(false) | bool and c2_ip is not defined
- name: Load c2_ip from infrastructure state
block:
- name: Check if infrastructure state file exists
stat:
path: "infrastructure_state_{{ deployment_id }}.json"
register: infra_state_check
- name: Load infrastructure state
include_vars:
file: "infrastructure_state_{{ deployment_id }}.json"
name: infra_state
when: infra_state_check.stat.exists
- name: Set c2 IP from infrastructure state
set_fact:
c2_ip: "{{ infra_state.c2_ip | default(operator_ip) }}"
when: infra_state_check.stat.exists and infra_state.c2_ip is defined
- name: Debug c2 IP
debug:
msg: "Using c2 IP: {{ c2_ip | default('undefined') }}"
when: c2_ip is undefined
- name: Default to operator IP if c2 IP is still undefined
set_fact:
c2_ip: "{{ operator_ip }}"
when: c2_ip is undefined
- name: Create security group for redirector
amazon.aws.ec2_security_group:
name: "{{ redirector_name }}-sg"
description: "Security group for redirector {{ redirector_name }}"
vpc_id: "{{ vpc_id }}"
region: "{{ aws_redirector_region }}"
rules:
# Management access only from operator IP
- proto: tcp
ports: 22
cidr_ip: "{{ operator_ip }}/32"
# Public-facing services are open to internet
- proto: tcp
ports:
- 80
- 443
- "{{ effective_listen_port }}"
cidr_ip: 0.0.0.0/0
rules_egress:
- proto: -1
cidr_ip: 0.0.0.0/0
state: present
register: security_group
# Save infrastructure state for reuse
- name: Save infrastructure state for reuse
copy:
content: |
{
"vpc_id": "{{ vpc_id }}",
"subnet_id": "{{ subnet_id }}",
"security_group_id": "{{ security_group.group_id }}",
"region": "{{ aws_redirector_region }}",
"deployment_id": "{{ deployment_id }}"
}
dest: "infrastructure_state_{{ deployment_id }}.json"
when: not (use_shared_infra | bool and infra_state_file.stat.exists | default(false))
# Generate or import SSH key for the deployment - FIXED KEY HANDLING
- name: Check if deployment SSH key already exists locally
stat:
path: "~/.ssh/c2deploy_{{ deployment_id }}.pem"
register: ssh_key_file
- name: Generate SSH key pair if it doesn't exist
block:
- name: Create SSH key pair
command: ssh-keygen -t rsa -b 2048 -f ~/.ssh/c2deploy_{{ deployment_id }} -N ""
args:
creates: "~/.ssh/c2deploy_{{ deployment_id }}"
- name: Rename private key to .pem format
command: mv ~/.ssh/c2deploy_{{ deployment_id }} ~/.ssh/c2deploy_{{ deployment_id }}.pem
args:
creates: "~/.ssh/c2deploy_{{ deployment_id }}.pem"
removes: "~/.ssh/c2deploy_{{ deployment_id }}"
when: not ssh_key_file.stat.exists
- name: Ensure proper permissions on SSH key
file:
path: "~/.ssh/c2deploy_{{ deployment_id }}.pem"
mode: '0600'
state: file
# Check if key pair exists in AWS
- name: Check if key pair exists in AWS
amazon.aws.ec2_key_info:
region: "{{ aws_redirector_region }}"
filters:
key-name: "c2deploy_{{ deployment_id }}"
register: existing_key_pair
# Import SSH key to AWS - FIXED CONDITION
- name: Import SSH key to AWS
amazon.aws.ec2_key:
name: "c2deploy_{{ deployment_id }}"
key_material: "{{ lookup('file', '~/.ssh/c2deploy_{{ deployment_id }}.pub') }}"
region: "{{ aws_redirector_region }}"
state: present
when: existing_key_pair.keypairs | length == 0
# Launch the redirector instance
- name: Launch redirector instance
amazon.aws.ec2_instance:
name: "{{ redirector_name }}"
key_name: "c2deploy_{{ deployment_id }}"
instance_type: "{{ instance_type | default('t2.micro') }}"
vpc_subnet_id: "{{ subnet_id }}"
security_groups:
- "{{ security_group.group_id }}"
image_id: "{{ ami_id }}"
region: "{{ aws_redirector_region }}"
state: present
wait: yes
volumes:
- device_name: "/dev/xvda"
ebs:
volume_size: 20
delete_on_termination: true
tags:
Name: "{{ redirector_name }}"
deployment_id: "{{ deployment_id }}"
register: redirector_instance
- 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: Display redirector instance details for debugging
debug:
msg:
- "Redirector IP: {{ redirector_ip }}"
- "Redirector Instance ID: {{ redirector_instance_id }}"
- "SSH User to use: {{ ssh_user }}"
- "SSH Key path: ~/.ssh/c2deploy_{{ deployment_id }}.pem"
- name: Wait for instance initialization
pause:
seconds: 120
when: redirector_instance.changed
- name: Set correct permissions on SSH key
file:
path: "~/.ssh/c2deploy_{{ deployment_id }}.pem"
mode: "0600"
- name: Wait for redirector SSH to be available
wait_for:
host: "{{ redirector_ip }}"
port: 22
delay: 30
timeout: 300
state: started
- name: Add redirector to inventory
add_host:
name: "redirector"
groups: "redirectors"
ansible_host: "{{ redirector_ip }}"
ansible_user: "ubuntu"
ansible_ssh_private_key_file: "~/.ssh/c2deploy_{{ deployment_id }}.pem"
ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes"
ansible_python_interpreter: "/usr/bin/python3"
shell_handler_port: "{{ effective_listen_port }}"
# Add necessary variables for redirector configuration
c2_ip: "{{ c2_ip }}"
domain: "{{ domain }}"
redirector_subdomain: "{{ redirector_subdomain }}"
havoc_teamserver_port: "{{ havoc_teamserver_port | default(40056) }}"
havoc_http_port: "{{ havoc_http_port | default(8080) }}"
havoc_https_port: "{{ havoc_https_port | default(443) }}"
zero_logs: "{{ zero_logs | default(true) }}"
# Rest of the playbook for configuring the redirector
- name: Configure redirector
hosts: redirectors
become: true
gather_facts: true
vars_files:
- vars.yaml
vars:
c2_ip: "{{ hostvars['localhost']['c2_ip'] | default('127.0.0.1') }}"
shell_handler_port: "{{ hostvars['localhost']['effective_listen_port'] }}"
# Include the rest of your redirector configuration tasks here
tasks:
- name: Include common redirector configuration tasks
include_tasks: "../../modules/redirectors/tasks/configure_redirector.yml"
- name: Configure shell handler script with listening port
template:
src: "../../modules/c2/files/havoc_shell_handler.sh"
dest: "/root/Tools/shell_handler.sh"
mode: 0755
vars:
listen_port: "{{ effective_listen_port }}"
- name: Print deployment summary
debug:
msg:
- "Redirector Deployment Complete!"
- "-----------------------------"
- "Redirector IP: {{ ansible_host }}"
- "Redirector Domain: {{ redirector_subdomain | default('cdn') }}.{{ domain }} (Update DNS A record)"
- "SSH Key: ~/.ssh/c2deploy_{{ hostvars['localhost']['deployment_id'] }}.pem"
when: not disable_summary | default(false)
@@ -0,0 +1,10 @@
{
"deployment_id": "{{ deployment_id }}",
"deployment_type": "{{ deployment_type }}",
"provider": "{{ provider }}",
"domain": "{{ phishing_domain | default(domain) }}",
"timestamp": "{{ ansible_date_time.iso8601 }}",
"components": {{ deployment_components | to_json }},
"results": {{ phishing_deployment_results | default({}) | to_json }},
"status": "completed"
}
@@ -0,0 +1,98 @@
---
# AWS provision tasks for one WEBRUNNER node
# Called in a loop — loop_var: node_chunk
# Requires: aws_access_key, aws_secret_key, aws_region, aws_instance_type,
# ssh_key_name, webrunner_name, deployment_id, operator_ip,
# scanner_ip_log, results_dir
- name: Read public key for {{ node_chunk.node_name }}
slurp:
src: "~/.ssh/{{ ssh_key_name }}.pub"
register: wr_pubkey_aws
- name: Import SSH key to AWS ({{ node_chunk.node_name }})
amazon.aws.ec2_key:
name: "{{ webrunner_name }}"
key_material: "{{ wr_pubkey_aws.content | b64decode | trim }}"
region: "{{ aws_region | default('us-east-1') }}"
aws_access_key: "{{ aws_access_key }}"
aws_secret_key: "{{ aws_secret_key }}"
state: present
ignore_errors: true
- name: Create security group for {{ node_chunk.node_name }}
amazon.aws.ec2_security_group:
name: "wr-{{ deployment_id }}-sg"
description: "WEBRUNNER {{ deployment_id }} scanner nodes"
region: "{{ aws_region | default('us-east-1') }}"
aws_access_key: "{{ aws_access_key }}"
aws_secret_key: "{{ aws_secret_key }}"
rules:
- proto: tcp
ports: [22]
cidr_ip: "{{ operator_ip | default('0.0.0.0/0') }}/32"
rules_egress:
- proto: all
cidr_ip: "0.0.0.0/0"
tags:
Name: "wr-{{ deployment_id }}-sg"
DeploymentID: "{{ deployment_id }}"
state: present
register: wr_sg
ignore_errors: true
- name: Launch EC2 instance {{ node_chunk.node_name }}
amazon.aws.ec2_instance:
name: "{{ node_chunk.node_name }}"
key_name: "{{ webrunner_name }}"
instance_type: "{{ aws_instance_type | default('t3.small') }}"
image_id: "{{ aws_ami | default('ami-0c55b159cbfafe1f0') }}"
region: "{{ aws_region | default('us-east-1') }}"
aws_access_key: "{{ aws_access_key }}"
aws_secret_key: "{{ aws_secret_key }}"
security_group: "wr-{{ deployment_id }}-sg"
network:
assign_public_ip: true
tags:
Name: "{{ node_chunk.node_name }}"
DeploymentID: "{{ deployment_id }}"
webrunner: "{{ webrunner_name }}"
wait: true
state: running
register: wr_ec2
- name: Extract EC2 public IP
set_fact:
wr_node_ip: "{{ wr_ec2.instances[0].public_ip_address }}"
- name: Log scanner IP
lineinfile:
path: "{{ scanner_ip_log }}"
line: "{{ node_chunk.node_name }}: {{ wr_node_ip }}"
create: true
- name: Wait for SSH on {{ node_chunk.node_name }} ({{ wr_node_ip }})
wait_for:
host: "{{ wr_node_ip }}"
port: 22
delay: 30
timeout: 300
- name: Add {{ node_chunk.node_name }} to inventory
add_host:
name: "{{ wr_node_ip }}"
groups: webrunner_nodes
ansible_host: "{{ wr_node_ip }}"
ansible_user: admin
ansible_ssh_private_key_file: "~/.ssh/{{ ssh_key_name }}"
ansible_ssh_common_args: "-o StrictHostKeyChecking=no"
node_name: "{{ node_chunk.node_name }}"
node_cidrs: "{{ node_chunk.cidrs }}"
node_ip_count: "{{ node_chunk.ip_count }}"
node_idx: "{{ node_chunk.idx }}"
ec2_instance_id: "{{ wr_ec2.instances[0].instance_id }}"
provider: aws
- name: Show {{ node_chunk.node_name }} ready
debug:
msg: "AWS node ready: {{ node_chunk.node_name }} @ {{ wr_node_ip }} ({{ node_chunk.ip_count | int | string }} IPs)"
+634
View File
@@ -0,0 +1,634 @@
---
# FlokiNET full deployment playbook (C2 + Redirector)
- name: Prepare FlokiNET infrastructure deployment
hosts: localhost
gather_facts: false
connection: local
vars_files:
- vars.yaml
vars:
# Generate random shell handler port if not provided
shell_handler_port: "{{ shell_handler_port | default(4000 + 60000 | random) }}"
redirector_subdomain: "{{ redirector_subdomain | default('cdn') }}"
c2_subdomain: "{{ c2_subdomain | default('mail') }}"
tasks:
- name: Validate required FlokiNET configuration
assert:
that:
- redirector_ip is defined and redirector_ip != ""
- c2_ip is defined and c2_ip != ""
fail_msg: "FlokiNET requires both redirector_ip and c2_ip. Set these values in vars.yaml or via command line arguments."
- name: Add redirector to inventory
add_host:
name: "redirector"
groups: "redirectors"
ansible_host: "{{ redirector_ip }}"
ansible_user: "{{ ssh_user | default('root') }}"
ansible_ssh_private_key_file: "{{ ssh_key_path | replace('.pub', '') }}"
ansible_ssh_port: "{{ ssh_port | default(22) }}"
ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
- name: Add C2 server to inventory
add_host:
name: "c2"
groups: "c2servers"
ansible_host: "{{ c2_ip }}"
ansible_user: "{{ ssh_user | default('root') }}"
ansible_ssh_private_key_file: "{{ ssh_key_path | replace('.pub', '') }}"
ansible_ssh_port: "{{ ssh_port | default(22) }}"
ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
- name: Verify SSH connection to redirector
wait_for:
host: "{{ redirector_ip }}"
port: "{{ ssh_port | default(22) }}"
delay: 10
timeout: 60
state: started
ignore_errors: true
- name: Verify SSH connection to C2 server
wait_for:
host: "{{ c2_ip }}"
port: "{{ ssh_port | default(22) }}"
delay: 10
timeout: 60
state: started
ignore_errors: true
- name: Configure FlokiNET redirector
hosts: redirectors
become: true
gather_facts: true
vars_files:
- vars.yaml
tasks:
- name: Wait for apt to be available
apt:
update_cache: yes
register: apt_result
until: apt_result is success
retries: 5
delay: 10
- name: Set hostname
hostname:
name: "redirector"
- name: Update apt cache
apt:
update_cache: yes
- name: Upgrade all packages
apt:
upgrade: dist
- name: Install base utilities and tools via apt
apt:
name:
- git
- wget
- curl
- unzip
- python3-pip
- python3-virtualenv
- tmux
- pipx
- nmap
- tcpdump
- nginx
- certbot
- python3-certbot-nginx
- socat
- netcat-openbsd
- secure-delete
state: present
- name: Set a custom MOTD
template:
src: motd-flokinet.j2
dest: /etc/motd
owner: root
group: root
mode: '0644'
vars:
letsencrypt_email: "{{ letsencrypt_email }}"
domain: "{{ domain }}"
redirector_subdomain: "{{ redirector_subdomain }}"
- name: Create directories for operational scripts
file:
path: "{{ item }}"
state: directory
mode: '0700'
owner: root
group: root
with_items:
- /opt/c2
- /opt/shell-handler
- name: Copy clean-logs.sh script
copy:
src: "../../common/files/clean-logs.sh"
dest: /opt/c2/clean-logs.sh
mode: '0700'
owner: root
group: root
- name: Copy secure-exit.sh script
copy:
src: "../../common/files/secure-exit.sh"
dest: /opt/c2/secure-exit.sh
mode: '0700'
owner: root
group: root
- name: Copy shell handler script
copy:
src: "../../common/files/persistent-listener.sh"
dest: /opt/shell-handler/persistent-listener.sh
mode: '0700'
owner: root
group: root
- name: Configure shell handler script with C2 IP
replace:
path: /opt/shell-handler/persistent-listener.sh
regexp: 'C2_HOST="127.0.0.1"'
replace: 'C2_HOST="{{ c2_ip }}"'
- name: Configure shell handler script with listening port
replace:
path: /opt/shell-handler/persistent-listener.sh
regexp: 'LISTEN_PORT=4444'
replace: 'LISTEN_PORT={{ shell_handler_port }}'
- name: Create shell handler service
template:
src: shell-handler.service.j2
dest: /etc/systemd/system/shell-handler.service
mode: '0644'
owner: root
group: root
- name: Configure NGINX for zero-logging if enabled
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
mode: '0644'
owner: root
group: root
when: zero_logs | bool
- name: Configure NGINX for C2 redirection
template:
src: redirector-site.conf.j2
dest: /etc/nginx/sites-available/default
mode: '0644'
owner: root
group: root
- name: Create legitimate-looking index.html
template:
src: redirector-index.html.j2
dest: /var/www/html/index.html
mode: '0644'
owner: www-data
group: www-data
- name: Start and enable shell handler service
systemd:
name: shell-handler
state: started
enabled: yes
daemon_reload: yes
- name: Set up cron job for log cleaning if zero-logs enabled
cron:
name: "Clean logs"
minute: "0"
hour: "*/6"
job: "/opt/c2/clean-logs.sh > /dev/null 2>&1"
when: zero_logs | bool
- name: Install Let's Encrypt certificate if domain specified
shell: |
certbot --nginx -d {{ redirector_subdomain }}.{{ domain }} --non-interactive --agree-tos -m {{ letsencrypt_email }}
args:
creates: /etc/letsencrypt/live/{{ redirector_subdomain }}.{{ domain }}/fullchain.pem
when: domain != "example.com"
- name: Restart NGINX
systemd:
name: nginx
state: restarted
- name: FlokiNET-specific security configurations
include_tasks: flokinet-security.yml
when: enable_hardened_security | default(true)
- name: Configure FlokiNET C2 server
hosts: c2servers
become: true
gather_facts: true
vars_files:
- vars.yaml
tasks:
- name: Wait for apt to be available
apt:
update_cache: yes
register: apt_result
until: apt_result is success
retries: 5
delay: 10
- name: Set hostname
hostname:
name: "c2"
- name: Update apt cache
apt:
update_cache: yes
- name: Upgrade all packages
apt:
upgrade: dist
- name: Set a custom MOTD
template:
src: motd-flokinet.j2
dest: /etc/motd
owner: root
group: root
mode: '0644'
vars:
letsencrypt_email: "{{ letsencrypt_email }}"
domain: "{{ domain }}"
c2_subdomain: "{{ c2_subdomain }}"
- name: Install base utilities and tools via apt
apt:
name:
- git
- wget
- curl
- unzip
- python3-pip
- python3-virtualenv
- tmux
- pipx
- nmap
- tcpdump
- hydra
- john
- hashcat
- sqlmap
- gobuster
- dirb
- enum4linux
- dnsenum
- seclists
- responder
- golang
- proxychains
- tor
- crackmapexec
- jq
- unzip
- postfix
- certbot
- opendkim
- opendkim-tools
- dovecot-core
- dovecot-imapd
- dovecot-pop3d
- dovecot-sieve
- dovecot-managesieved
- yq
state: present
- name: Create Tools directory
file:
path: /root/Tools
state: directory
owner: root
group: root
mode: '0755'
- name: Ensure pipx path is configured
shell: |
pipx ensurepath
args:
executable: /bin/bash
- name: Install tools via pipx
shell: |
export PATH=$PATH:/root/.local/bin
pipx ensurepath
pipx install git+https://github.com/Pennyw0rth/NetExec
pipx install git+https://github.com/blacklanternsecurity/TREVORspray
pipx install impacket
args:
executable: /bin/bash
- name: Download Kerbrute
shell: |
mkdir -p ~/Tools/Kerbrute
wget https://github.com/ropnop/kerbrute/releases/latest/download/kerbrute_linux_amd64 -O ~/Tools/Kerbrute/kerbrute
chmod +x ~/Tools/Kerbrute/kerbrute
args:
executable: /bin/bash
creates: /root/Tools/Kerbrute/kerbrute
- name: Clone SharpCollection nightly builds
git:
repo: https://github.com/Flangvik/SharpCollection.git
dest: ~/Tools/SharpCollection
version: master
ignore_errors: yes
- name: Clone PEASS-ng
git:
repo: https://github.com/carlospolop/PEASS-ng.git
dest: ~/Tools/PEASS-ng
ignore_errors: yes
- name: Clone MailSniper
git:
repo: https://github.com/dafthack/MailSniper.git
dest: ~/Tools/MailSniper
ignore_errors: yes
- name: Clone Inveigh
git:
repo: https://github.com/Kevin-Robertson/Inveigh.git
dest: ~/Tools/Inveigh
ignore_errors: yes
- name: Install Sliver C2 server
shell: |
curl https://sliver.sh/install | bash
systemctl enable sliver
systemctl start sliver
- name: Install Metasploit Framework (Nightly Build)
shell: |
curl https://raw.githubusercontent.com/rapid7/metasploit-omnibus/master/config/templates/metasploit-framework-wrappers/msfupdate.erb > ~/Tools/msfinstall
chmod 755 ~/Tools/msfinstall
~/Tools/msfinstall
args:
executable: /bin/bash
creates: /usr/bin/msfconsole
- name: Grab GoPhish
shell: |
curl -L "$(curl -s https://api.github.com/repos/gophish/gophish/releases/latest | jq -r '.assets[] | select(.browser_download_url | contains("linux-64bit.zip")) | .browser_download_url')" -o ~/Tools/gophish.zip
unzip ~/Tools/gophish.zip -d ~/Tools/gophish
rm -rf ~/Tools/gophish.zip
chmod +x ~/Tools/gophish/gophish
args:
creates: /root/Tools/gophish/gophish
- name: Deploy Gophish config.json with custom admin port
template:
src: gophish-config.j2
dest: ~/Tools/gophish/config.json
owner: root
group: root
mode: '0644'
vars:
gophish_admin_port: "{{ gophish_admin_port }}"
domain: "{{ domain }}"
- name: Configure Postfix main.cf
lineinfile:
path: /etc/postfix/main.cf
regexp: "{{ item.regexp }}"
line: "{{ item.line }}"
with_items:
- { regexp: '^myhostname', line: "myhostname = mail.{{ domain }}" }
- { regexp: '^mydomain', line: "mydomain = {{ domain }}" }
- { regexp: '^myorigin', line: "myorigin = $mydomain" }
- { regexp: '^inet_interfaces', line: "inet_interfaces = all" }
- { regexp: '^inet_protocols', line: "inet_protocols = ipv4" }
- { regexp: '^smtpd_banner', line: "smtpd_banner = $myhostname ESMTP $mail_name" }
- { regexp: '^mynetworks', line: "mynetworks = 127.0.0.0/8 [::1]/128" }
- { regexp: '^relay_domains', line: "relay_domains = $mydestination" }
- { regexp: '^smtpd_tls_cert_file', line: "smtpd_tls_cert_file = /etc/letsencrypt/live/{{ domain }}/fullchain.pem" }
- { regexp: '^smtpd_tls_key_file', line: "smtpd_tls_key_file = /etc/letsencrypt/live/{{ domain }}/privkey.pem" }
- { regexp: '^smtpd_tls_security_level', line: "smtpd_tls_security_level = encrypt" }
- { regexp: '^smtpd_tls_session_cache_database', line: "smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache" }
- { regexp: '^smtp_tls_session_cache_database', line: "smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache" }
- { regexp: '^smtpd_use_tls', line: "smtpd_use_tls = yes" }
- { regexp: '^smtpd_tls_auth_only', line: "smtpd_tls_auth_only = yes" }
- { regexp: '^milter_default_action', line: "milter_default_action = accept" }
- { regexp: '^milter_protocol', line: "milter_protocol = 6" }
- { regexp: '^smtpd_milters', line: "smtpd_milters = unix:/var/spool/postfix/opendkim/opendkim.sock" }
- { regexp: '^non_smtpd_milters', line: "non_smtpd_milters = unix:/var/spool/postfix/opendkim/opendkim.sock" }
- name: Configure OpenDKIM
lineinfile:
path: /etc/opendkim.conf
regexp: "{{ item.regexp }}"
line: "{{ item.line }}"
with_items:
- { regexp: '^Domain', line: "Domain {{ domain }}" }
- { regexp: '^KeyFile', line: "KeyFile /etc/opendkim/keys/{{ domain }}/mail.private" }
- { regexp: '^Selector', line: "Selector mail" }
- { regexp: '^Socket', line: "Socket local:/var/spool/postfix/opendkim/opendkim.sock" }
- { regexp: '^Syslog', line: "Syslog yes" }
- { regexp: '^UMask', line: "UMask 002" }
- { regexp: '^Mode', line: "Mode sv" }
- name: Create DKIM directory
file:
path: /etc/opendkim/keys/{{ domain }}
state: directory
owner: opendkim
group: opendkim
mode: 0700
- name: Generate DKIM keys
command: >
opendkim-genkey -D /etc/opendkim/keys/{{ domain }} -d {{ domain }} -s mail
args:
creates: /etc/opendkim/keys/{{ domain }}/mail.private
- name: Set permissions for DKIM keys
file:
path: /etc/opendkim/keys/{{ domain }}/mail.private
owner: opendkim
group: opendkim
mode: 0600
- name: Configure OpenDKIM TrustedHosts
copy:
content: |
127.0.0.1
::1
localhost
{{ domain }}
dest: /etc/opendkim/TrustedHosts
owner: opendkim
group: opendkim
mode: 0644
- name: Enable submission port (587) in master.cf
blockinfile:
path: /etc/postfix/master.cf
insertafter: '^#submission'
block: |
submission inet n - y - - smtpd
-o syslog_name=postfix/submission
-o smtpd_tls_security_level=encrypt
-o smtpd_sasl_auth_enable=yes
-o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
-o smtpd_relay_restrictions=permit_sasl_authenticated,reject
- name: Configure Dovecot for Postfix SASL
blockinfile:
path: /etc/dovecot/conf.d/10-master.conf
insertafter: '^service auth {'
block: |
# Postfix smtp-auth
unix_listener /var/spool/postfix/private/auth {
mode = 0660
user = postfix
group = postfix
}
- name: Set Dovecot auth_mechanisms
lineinfile:
path: /etc/dovecot/conf.d/10-auth.conf
regexp: '^auth_mechanisms'
line: 'auth_mechanisms = plain login'
- name: Create Dovecot password file for SASL authentication
file:
path: /etc/dovecot/passwd
state: touch
mode: '0600'
owner: dovecot
group: dovecot
- name: Add SMTP auth user to Dovecot
lineinfile:
path: /etc/dovecot/passwd
line: "{{ smtp_auth_user }}:{{ smtp_auth_pass | password_hash('sha512_crypt') }}"
- name: Disable system auth and use passwd-file
lineinfile:
path: /etc/dovecot/conf.d/10-auth.conf
regexp: '^!include auth-system.conf.ext'
line: '#!include auth-system.conf.ext'
- name: Add auth-passwdfile configuration
blockinfile:
path: /etc/dovecot/conf.d/10-auth.conf
insertafter: '^auth_mechanisms ='
block: |
passdb {
driver = passwd-file
args = scheme=sha512_crypt /etc/dovecot/passwd
}
userdb {
driver = static
args = uid=vmail gid=vmail home=/var/vmail/%u
}
- name: Create vmail user/group
group:
name: vmail
gid: 5000
state: present
- name: Create vmail user
user:
name: vmail
uid: 5000
group: vmail
create_home: no
- name: Create directories for operational scripts
file:
path: "{{ item }}"
state: directory
mode: '0700'
owner: root
group: root
with_items:
- /opt/c2
- /opt/beacons
- name: Copy clean-logs.sh script
copy:
src: "../../common/files/clean-logs.sh"
dest: /opt/c2/clean-logs.sh
mode: '0700'
owner: root
group: root
- name: Copy secure-exit.sh script
copy:
src: "../../common/files/secure-exit.sh"
dest: /opt/c2/secure-exit.sh
mode: '0700'
owner: root
group: root
- name: Copy serve-beacons.sh script
copy:
src: "../../common/files/serve-beacons.sh"
dest: /opt/c2/serve-beacons.sh
mode: '0700'
owner: root
group: root
- name: Configure serve-beacons.sh with C2 IP
replace:
path: /opt/c2/serve-beacons.sh
regexp: 'C2_HOST=.*'
replace: 'C2_HOST="{{ c2_ip }}"'
- name: Set up cron job for log cleaning if zero-logs enabled
cron:
name: "Clean logs"
minute: "0"
hour: "*/6"
job: "/opt/c2/clean-logs.sh > /dev/null 2>&1"
when: zero_logs | bool
- name: Install Let's Encrypt certificate if domain specified
shell: |
certbot certonly --standalone -d {{ c2_subdomain }}.{{ domain }} --non-interactive --agree-tos -m {{ letsencrypt_email }}
args:
creates: /etc/letsencrypt/live/{{ c2_subdomain }}.{{ domain }}/fullchain.pem
when: domain != "example.com"
- name: Restart Postfix
service:
name: postfix
state: restarted
- name: Restart Dovecot
service:
name: dovecot
state: restarted
- name: FlokiNET-specific security configurations
include_tasks: flokinet-security.yml
when: enable_hardened_security | default(true)
- name: Print deployment summary
debug:
msg:
- "FlokiNET Deployment Complete!"
- "-------------------------------"
- "C2 Server Domain: {{ c2_subdomain }}.{{ domain }} (Update DNS A record)"
- "Redirector Domain: {{ redirector_subdomain }}.{{ domain }} (Update DNS A record)"
- "Shell Handler Port: {{ shell_handler_port }}"
- "GoPhish Admin Port: {{ gophish_admin_port }}"
when: not disable_summary | default(false)
+101
View File
@@ -0,0 +1,101 @@
---
# 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: 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
- gnupg
- lsb-release
- unattended-upgrades
- ufw
- fail2ban
- secure-delete
state: present
- name: Include common security hardening tasks
include_tasks: "../../common/tasks/security_hardening.yml"
- name: Include common tool installation tasks
include_tasks: "../../common/tasks/install_tools.yml"
- name: Include common C2 configuration tasks
include_tasks: "../../modules/c2/tasks/configure_c2.yml"
- name: Include common mail server configuration tasks
include_tasks: "../../common/tasks/configure_mail.yml"
- name: Print deployment summary
debug:
msg:
- "C2 Server Configuration Complete!"
- "-------------------------------"
- "C2 Server IP: {{ ansible_host }}"
- "C2 Server Domain: {{ c2_subdomain }}.{{ domain }} (Update DNS A record)"
- "GoPhish Admin Port: {{ gophish_admin_port }}"
when: not disable_summary | default(false)
+75
View File
@@ -0,0 +1,75 @@
---
# FlokiNET Cleanup Playbook
# Used for documentation since FlokiNET requires manual cleanup through their interface
- name: Document FlokiNET cleanup procedure
hosts: localhost
connection: local
gather_facts: false
vars_files:
- vars.yaml
vars:
cleanup_redirector: "{{ (redirector_ip is defined and redirector_ip != '') | ternary(true, false) }}"
cleanup_c2: "{{ (c2_ip is defined and c2_ip != '') | ternary(true, false) }}"
confirm_cleanup: false # Skip confirmation in automated teardown
tasks:
- name: Confirm cleanup if required
pause:
prompt: "WARNING: This script provides guidance for manual FlokiNET cleanup. Type 'yes' to continue"
register: confirmation
when: confirm_cleanup
- name: Exit if not confirmed
meta: end_play
when: confirm_cleanup and confirmation.user_input != 'yes'
- name: Display cleanup instructions
debug:
msg:
- "FlokiNET Cleanup Instructions"
- "========================================"
- "Since FlokiNET resources must be manually terminated through their control panel,"
- "this playbook provides guidance on the steps needed for cleanup."
- ""
- "Resources to clean up:"
- "{{ cleanup_redirector | ternary('- Redirector server: ' + redirector_ip, '') }}"
- "{{ cleanup_c2 | ternary('- C2 server: ' + c2_ip, '') }}"
- ""
- "Steps to terminate FlokiNET servers:"
- "1. Log in to your FlokiNET control panel"
- "2. Navigate to the Virtual Servers section"
- "3. Select each server from the list"
- "4. Click 'Terminate' and confirm termination"
- ""
- "For security, consider also:"
- "- Manually executing the secure-exit.sh script on each server before termination"
- "- Removing DNS records associated with these servers"
- "- Ensuring any SSH keys used for these servers are removed or rotated"
- name: Remove SSH key file if it's not a shared key
file:
path: "{{ ssh_key_path | replace('.pub', '') }}"
state: absent
when:
- ssh_key_path is defined
- ssh_key_path is search('c2deploy_')
ignore_errors: true
- name: Remove SSH public key file if it's not a shared key
file:
path: "{{ ssh_key_path }}"
state: absent
when:
- ssh_key_path is defined
- ssh_key_path is search('c2deploy_')
ignore_errors: true
- name: Pre-termination security recommendations
debug:
msg:
- "Security Recommendations before manual termination:"
- "------------------------------------------------"
- " SSH Command: ssh -i {{ ssh_key_path | replace('.pub', '') }} {{ ssh_user | default('root') }}@SERVER_IP -p {{ ssh_port | default('22') }}"
- " Then run: /opt/c2/secure-exit.sh"
when: (cleanup_redirector or cleanup_c2) and ssh_key_path is defined
+167
View File
@@ -0,0 +1,167 @@
---
# FlokiNET-specific security tasks
# Enhanced security features for FlokiNET servers
- name: Configure FlokiNET networking for maximum anonymity
lineinfile:
path: /etc/sysctl.conf
regexp: "{{ item.regexp }}"
line: "{{ item.line }}"
state: present
with_items:
- { regexp: '^net.ipv4.tcp_timestamps', line: 'net.ipv4.tcp_timestamps = 0' }
- { regexp: '^net.ipv4.tcp_syncookies', line: 'net.ipv4.tcp_syncookies = 1' }
- { regexp: '^net.ipv4.conf.all.accept_redirects', line: 'net.ipv4.conf.all.accept_redirects = 0' }
- { regexp: '^net.ipv6.conf.all.accept_redirects', line: 'net.ipv6.conf.all.accept_redirects = 0' }
- { regexp: '^net.ipv4.conf.all.send_redirects', line: 'net.ipv4.conf.all.send_redirects = 0' }
- { regexp: '^net.ipv4.conf.all.accept_source_route', line: 'net.ipv4.conf.all.accept_source_route = 0' }
- { regexp: '^net.ipv6.conf.all.accept_source_route', line: 'net.ipv6.conf.all.accept_source_route = 0' }
- { regexp: '^net.ipv4.conf.all.log_martians', line: 'net.ipv4.conf.all.log_martians = 1' }
notify: Apply sysctl settings
- name: Install Tor for anonymous outbound connections
apt:
name:
- tor
- torsocks
- obfs4proxy
state: present
update_cache: yes
register: tor_installed
when: use_tor_proxy | default(true)
- name: Create Tor configuration directory
file:
path: /etc/tor
state: directory
mode: '0755'
when: use_tor_proxy | default(true) and tor_installed is succeeded
- name: Configure Tor for hardened privacy settings
template:
src: torrc.j2
dest: /etc/tor/torrc
owner: root
group: root
mode: '0644'
notify: Restart Tor service
when: use_tor_proxy | default(true) and tor_installed is succeeded
- name: Configure ProxyChains for routing through Tor
template:
src: proxychains.conf.j2
dest: /etc/proxychains.conf
owner: root
group: root
mode: '0644'
when: use_tor_proxy | default(true) and tor_installed is succeeded
- name: Install iptables-persistent for firewall persistence
apt:
name: iptables-persistent
state: present
update_cache: yes
- name: Configure hardened iptables rules for FlokiNET
template:
src: iptables-rules.j2
dest: /etc/iptables/rules.v4
owner: root
group: root
mode: '0644'
notify: Apply iptables rules
- name: Create SSH security hardening script
template:
src: secure-ssh.sh.j2
dest: /opt/c2/secure-ssh.sh
owner: root
group: root
mode: '0700'
when: use_hardened_ssh | default(true)
- name: Run SSH security hardening script
command: /opt/c2/secure-ssh.sh
args:
creates: /opt/c2/.ssh_hardened
when: use_hardened_ssh | default(true)
- name: Install timezone data
apt:
name: tzdata
state: present
- name: Set timezone to UTC
timezone:
name: UTC
- name: Configure DNS to use secure DNS servers
template:
src: resolv.conf.j2
dest: /etc/resolv.conf
owner: root
group: root
mode: '0644'
when: enable_dns_encryption | default(true)
- name: Create directory for DNS cache configuration
file:
path: /etc/systemd/resolved.conf.d
state: directory
mode: '0755'
when: enable_dns_encryption | default(true)
- name: Configure DNS caching with DNSCrypt
template:
src: dnscrypt.conf.j2
dest: /etc/systemd/resolved.conf.d/dnscrypt.conf
owner: root
group: root
mode: '0644'
notify: Restart systemd-resolved
when: enable_dns_encryption | default(true)
# Configure memory security if enabled
- name: Set memory security measures
lineinfile:
path: /etc/sysctl.conf
line: "{{ item }}"
state: present
with_items:
- "vm.swappiness=0"
- "kernel.randomize_va_space=2"
when: secure_memory | default(true)
notify: Apply sysctl settings
# Configure history settings if disabled
- name: Disable system history
lineinfile:
path: "{{ item.file }}"
line: "{{ item.line }}"
state: present
create: yes
with_nested:
- [{ file: '/etc/profile' }, { file: '/root/.bashrc' }]
- [{ line: 'export HISTFILESIZE=0' }, { line: 'export HISTSIZE=0' }, { line: 'unset HISTFILE' }]
when: disable_history | default(true)
handlers:
- name: Apply sysctl settings
command: sysctl -p
- name: Restart Tor service
service:
name: tor
state: restarted
enabled: yes
when: use_tor_proxy | default(true)
- name: Apply iptables rules
command: iptables-restore < /etc/iptables/rules.v4
- name: Restart systemd-resolved
service:
name: systemd-resolved
state: restarted
enabled: yes
when: enable_dns_encryption | default(true)
+119
View File
@@ -0,0 +1,119 @@
---
# FlokiNET-specific phishing infrastructure tasks
- name: Create FlokiNET instances for phishing
uri:
url: "{{ flokinet_api_endpoint }}/instances"
method: POST
headers:
Authorization: "Bearer {{ flokinet_api_token }}"
Content-Type: "application/json"
body_format: json
body:
plan: "{{ flokinet_plan | default('basic') }}"
region: "{{ flokinet_region | default('romania') }}"
os: "{{ flokinet_os | default('ubuntu-22.04') }}"
hostname: "{{ deployment_id }}-{{ item }}"
ssh_keys:
- "{{ ssh_public_key }}"
tags:
- "c2itall"
- "phishing"
- "{{ deployment_id }}"
status_code: [200, 201]
register: flokinet_instances
loop: "{{ deployment_components }}"
when: item in deployment_components
- name: Wait for instances to be active
uri:
url: "{{ flokinet_api_endpoint }}/instances/{{ item.json.id }}"
method: GET
headers:
Authorization: "Bearer {{ flokinet_api_token }}"
register: instance_status
until: instance_status.json.status == "active"
retries: 30
delay: 10
loop: "{{ flokinet_instances.results }}"
when: flokinet_instances.results is defined
- name: Get instance details
uri:
url: "{{ flokinet_api_endpoint }}/instances/{{ item.json.id }}"
method: GET
headers:
Authorization: "Bearer {{ flokinet_api_token }}"
register: instance_details
loop: "{{ flokinet_instances.results }}"
when: flokinet_instances.results is defined
- name: Set instance facts
set_fact:
phishing_instances: >-
{% set instances = [] %}
{% for result in instance_details.results %}
{% set instance = {
'id': result.json.id,
'hostname': result.json.hostname,
'ip': result.json.main_ip,
'region': result.json.region,
'plan': result.json.plan,
'status': result.json.status
} %}
{% set _ = instances.append(instance) %}
{% endfor %}
{{ instances }}
when: instance_details.results is defined
- name: Wait for SSH connectivity
wait_for:
host: "{{ item.ip }}"
port: 22
delay: 30
timeout: 300
loop: "{{ phishing_instances }}"
when: phishing_instances is defined
- name: Update instance inventory
add_host:
name: "{{ item.ip }}"
groups: "phishing_{{ item.hostname.split('-')[-1] }}"
ansible_host: "{{ item.ip }}"
ansible_user: root
ansible_ssh_private_key_file: "{{ ssh_private_key_path }}"
ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
instance_id: "{{ item.id }}"
instance_hostname: "{{ item.hostname }}"
provider: "flokinet"
loop: "{{ phishing_instances }}"
when: phishing_instances is defined
- name: Configure Gophish servers
include_tasks: ../common/configure_gophish.yml
when: "'gophish' in deployment_components"
- name: Configure MTA fronts
include_tasks: ../common/configure_mta.yml
when: "'mta_front' in deployment_components"
- name: Configure redirectors
include_tasks: ../common/configure_redirector.yml
when: "'redirector' in deployment_components"
- name: Configure web servers
include_tasks: ../common/configure_webserver.yml
when: "'webserver' in deployment_components"
- name: Display deployment summary
debug:
msg: |
🎯 FlokiNET Phishing Infrastructure Deployed
{{ phishing_instances | length }} instances created
Provider: FlokiNET
Deployment ID: {{ deployment_id }}
Instances:
{% for instance in phishing_instances %}
- {{ instance.hostname }}: {{ instance.ip }} ({{ instance.plan }})
{% endfor %}
when: phishing_instances is defined
+65
View File
@@ -0,0 +1,65 @@
---
# Phishing infrastructure deployment playbook
# This is a comprehensive playbook that handles all phishing components
- name: Deploy Phishing Infrastructure
hosts: localhost
gather_facts: true
connection: local
vars_files:
- vars.yaml
tasks:
- name: Display deployment information
debug:
msg: |
Deploying phishing infrastructure
Deployment ID: {{ deployment_id }}
Provider: {{ provider }}
Domain: {{ domain | default(phishing_domain) }}
Components: {{ deployment_type }}
- name: Set deployment facts
set_fact:
deployment_timestamp: "{{ ansible_date_time.epoch }}"
phishing_deployment_results: {}
deployment_components: >-
{% set components = [] %}
{% if deployment_type in ['gophish_only', 'basic_phishing', 'advanced_phishing', 'full_phishing', 'fedramp_phishing'] %}
{% set _ = components.append('gophish') %}
{% endif %}
{% if deployment_type in ['mta_front_only', 'basic_phishing', 'advanced_phishing', 'full_phishing', 'ephemeral_mta'] %}
{% set _ = components.append('mta_front') %}
{% endif %}
{% if deployment_type in ['phishing_redirector_only', 'advanced_phishing', 'full_phishing'] %}
{% set _ = components.append('redirector') %}
{% endif %}
{% if deployment_type in ['phishing_webserver_only', 'full_phishing', 'fedramp_phishing'] %}
{% set _ = components.append('webserver') %}
{% endif %}
{{ components }}
- name: Include provider-specific tasks
include_tasks: "{{ provider }}_phishing.yml"
when: deployment_components | length > 0
- name: Ensure logs directory exists
file:
path: "../../logs"
state: directory
mode: '0755'
delegate_to: localhost
- name: Save deployment information
template:
src: phishing_deployment_info.j2
dest: "{{ playbook_dir }}/logs/phishing_deployment_{{ deployment_id }}.json"
delegate_to: localhost
- name: Display success message
debug:
msg: |
✅ Phishing infrastructure deployment completed
Deployment ID: {{ deployment_id }}
Components deployed: {{ deployment_components | join(', ') }}
Check logs/phishing_deployment_{{ deployment_id }}.json for details
+134
View File
@@ -0,0 +1,134 @@
---
- name: Common provisioning for FlokiNET servers
hosts: all
gather_facts: true
vars_files:
- vars.yaml
tasks:
- name: Set hostname
ansible.builtin.hostname:
name: "{{ inventory_hostname }}"
become: true
- name: Update apt cache
ansible.builtin.apt:
update_cache: yes
become: true
- name: Upgrade all packages
ansible.builtin.apt:
upgrade: dist
become: true
- name: Install base security packages
ansible.builtin.apt:
name:
- apt-transport-https
- ca-certificates
- curl
- gnupg
- lsb-release
- unattended-upgrades
- ufw
- fail2ban
- secure-delete
state: present
become: true
- name: Configure UFW
ansible.builtin.ufw:
state: enabled
policy: deny
logging: 'on'
become: true
- name: Add SSH rule to UFW
ansible.builtin.ufw:
rule: allow
port: "{{ ssh_port }}"
proto: tcp
become: true
- name: Configure SSH for better security
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: "{{ item.regexp }}"
line: "{{ item.line }}"
state: present
with_items:
- { regexp: '^#?PermitRootLogin', line: 'PermitRootLogin prohibit-password' }
- { regexp: '^#?PasswordAuthentication', line: 'PasswordAuthentication no' }
- { regexp: '^#?X11Forwarding', line: 'X11Forwarding no' }
- { regexp: '^#?AllowTcpForwarding', line: 'AllowTcpForwarding no' }
- { regexp: '^#?Port', line: 'Port {{ ssh_port }}' }
- { regexp: '^#?LogLevel', line: 'LogLevel ERROR' }
- { regexp: '^#?MaxAuthTries', line: 'MaxAuthTries 3' }
- { regexp: '^#?ClientAliveInterval', line: 'ClientAliveInterval 300' }
become: true
- name: Restart SSH service
ansible.builtin.service:
name: ssh
state: restarted
become: true
- name: Setup directory for operational scripts
ansible.builtin.file:
path: /opt/c2
state: directory
mode: '0700'
owner: root
group: root
become: true
- name: Copy operational scripts
ansible.builtin.copy:
src: "../../common/files/{{ item }}"
dest: "/opt/c2/{{ item }}"
mode: '0700'
owner: root
group: root
with_items:
- clean-logs.sh
- secure-exit.sh
become: true
- name: Setup cron to clean logs
ansible.builtin.cron:
name: "Log cleanup"
minute: "*/{{ log_rotation_hours * 60 }}"
job: "/opt/c2/clean-logs.sh >/dev/null 2>&1"
become: true
when: disable_history | bool
- name: Disable system history
ansible.builtin.lineinfile:
path: "{{ item }}"
line: "{{ line_item }}"
state: present
create: yes
with_items:
- /etc/profile
- /root/.bashrc
with_nested:
- [ 'export HISTFILESIZE=0', 'export HISTSIZE=0', 'unset HISTFILE' ]
loop_control:
loop_var: line_item
become: true
when: disable_history | bool
- name: Set memory security measures
ansible.builtin.lineinfile:
path: /etc/sysctl.conf
line: "{{ item }}"
state: present
with_items:
- "vm.swappiness=0"
- "kernel.randomize_va_space=2"
become: true
when: secure_memory | bool
- name: Apply sysctl settings
ansible.builtin.command: sysctl -p
become: true
when: secure_memory | bool
+97
View File
@@ -0,0 +1,97 @@
---
# 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: 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:
- apt-transport-https
- ca-certificates
- curl
- gnupg
- lsb-release
- unattended-upgrades
- ufw
- fail2ban
- secure-delete
state: present
- name: Include common security hardening tasks
include_tasks: "../../common/tasks/security_hardening.yml"
- name: Include common redirector configuration tasks
include_tasks: "../../modules/redirectors/tasks/configure_redirector.yml"
- name: Print deployment summary
debug:
msg:
- "Redirector Configuration Complete!"
- "---------------------------------"
- "Redirector IP: {{ ansible_host }}"
- "Redirector Domain: {{ redirector_subdomain }}.{{ domain }} (Update DNS A record)"
- "Shell Handler Port: {{ shell_handler_port }}"
when: not disable_summary | default(false)
@@ -0,0 +1,10 @@
{
"deployment_id": "{{ deployment_id }}",
"deployment_type": "{{ deployment_type }}",
"provider": "{{ provider }}",
"domain": "{{ phishing_domain | default(domain) }}",
"timestamp": "{{ ansible_date_time.iso8601 }}",
"components": {{ deployment_components | to_json }},
"results": {{ phishing_deployment_results | default({}) | to_json }},
"status": "completed"
}
@@ -0,0 +1,12 @@
---
# FlokiNET provision tasks for WEBRUNNER nodes
# FlokiNET does not expose a public API — manual provisioning required.
- name: FlokiNET not supported for automated WEBRUNNER deployment
fail:
msg: >
FlokiNET does not provide a public provisioning API.
To use FlokiNET nodes with WEBRUNNER, provision VPS instances manually,
add their IPs to the [webrunner_nodes] inventory group, and re-run
the scan plays directly. Node chunk {{ node_chunk.node_name }} assigned
{{ node_chunk.ip_count }} IPs.
+251
View File
@@ -0,0 +1,251 @@
---
# Linode Attack Box Deployment Playbook
# Based on attk-box-setup but optimized for headless server deployment
- name: Deploy Attack Box on Linode
hosts: localhost
connection: local
gather_facts: false
vars:
ansible_python_interpreter: "{{ ansible_playbook_python }}"
deployment_type: "attack_box"
attack_box_name: "{{ attack_box_name | default('a-' + deployment_id) }}"
attack_box_type: "{{ attack_box_type | default('kali') }}"
linode_instance_type: "{{ linode_instance_type | default('g6-standard-4') }}"
linode_region: "{{ linode_region | default('us-east') }}"
ssh_key_name: "{{ ssh_key_path | default('c2deploy_' + attack_box_name) | basename | regex_replace('\\.pub$', '') }}"
# Attack box image mapping (headless versions)
attack_box_images:
kali: "linode/kali"
custom: "linode/kali" # Use Kali as base for custom builds
quick_recon: "linode/kali" # Kali Linux for easy tool expansion
tasks:
- name: Debug attack box deployment
debug:
msg: "Deploying {{ attack_box_type }} attack box on Linode in {{ linode_region }}"
- name: Set attack box image
set_fact:
attack_box_image: "{{ attack_box_images[attack_box_type] | default('linode/kali') }}"
- name: Check if SSH key exists (should be pre-generated)
stat:
path: "~/.ssh/{{ ssh_key_name }}"
register: ssh_key_check
- name: Generate SSH key pair for attack box (if not exists)
openssh_keypair:
path: "~/.ssh/{{ ssh_key_name }}"
type: rsa
size: 2048
force: no
register: ssh_key_result
when: not ssh_key_check.stat.exists
- name: Read public key content
slurp:
src: "~/.ssh/{{ ssh_key_name }}.pub"
register: public_key_content
- name: Add SSH key to Linode
uri:
url: "https://api.linode.com/v4/profile/sshkeys"
method: POST
headers:
Authorization: "Bearer {{ linode_token }}"
Content-Type: "application/json"
body_format: json
body:
label: "{{ ssh_key_name }}"
ssh_key: "{{ public_key_content.content | b64decode | trim }}"
status_code: [200, 201]
register: linode_ssh_key
ignore_errors: yes
- name: Generate strong root password
set_fact:
root_password: "{{ lookup('password', '/dev/null chars=ascii_letters,digits,!@#$%^&*-_=+ length=32') }}"
when: ansible_password is not defined
- name: Set ansible_password if not defined
set_fact:
ansible_password: "{{ root_password }}"
when: ansible_password is not defined and root_password is defined
- name: Create Linode instance for attack box
uri:
url: "https://api.linode.com/v4/linode/instances"
method: POST
headers:
Authorization: "Bearer {{ linode_token }}"
Content-Type: "application/json"
body_format: json
body:
label: "{{ attack_box_name }}"
type: "{{ linode_instance_type }}"
region: "{{ linode_region }}"
image: "{{ attack_box_image }}"
root_pass: "{{ ansible_password | default(root_password) }}"
authorized_keys:
- "{{ public_key_content.content | b64decode | trim }}"
booted: true
backups_enabled: false
private_ip: false
tags:
- "attack-box"
- "c2itall"
- "{{ deployment_id }}"
status_code: [200, 201]
register: linode_instance
- name: Get current timestamp
setup:
gather_subset: min
delegate_to: localhost
- name: Ensure logs directory exists
file:
path: "{{ playbook_dir }}/logs"
state: directory
mode: '0755'
delegate_to: localhost
- name: Save attack box credentials to local file
copy:
content: |
Attack Box: {{ attack_box_name }}
Deployment ID: {{ deployment_id }}
Instance IP: {{ linode_instance.json.ipv4[0] if linode_instance.json.ipv4 else 'Pending' }}
SSH Key: ~/.ssh/{{ ssh_key_name }}
Root Password: {{ ansible_password | default(root_password) }}
SSH Command: ssh -i ~/.ssh/{{ ssh_key_name }} root@{{ linode_instance.json.ipv4[0] if linode_instance.json.ipv4 else 'IP_ADDRESS' }}
Generated at: {{ ansible_date_time.iso8601 }}
dest: "{{ playbook_dir }}/logs/deployment_info_{{ deployment_id }}.txt"
mode: '0600'
when: linode_instance is succeeded
delegate_to: localhost
- name: Save instance information
set_fact:
attack_box_ip: "{{ linode_instance.json.ipv4[0] }}"
attack_box_id: "{{ linode_instance.json.id }}"
- name: Display attack box information
debug:
msg:
- "Attack Box Created Successfully!"
- "Name: {{ attack_box_name }}"
- "IP Address: {{ attack_box_ip }}"
- "Instance ID: {{ attack_box_id }}"
- "Type: {{ attack_box_type }}"
- "SSH Key: ~/.ssh/{{ ssh_key_name }}"
- name: Wait for instance to be fully booted
wait_for:
host: "{{ attack_box_ip }}"
port: 22
delay: 30
timeout: 300
ignore_errors: yes
- name: Test SSH connectivity
command: ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no -i ~/.ssh/{{ ssh_key_name }} root@{{ attack_box_ip }} echo "SSH connection successful"
register: ssh_test
retries: 5
delay: 30
until: ssh_test.rc == 0
ignore_errors: yes
- name: Create dynamic inventory for attack box configuration
add_host:
name: "{{ attack_box_ip }}"
groups: attack_boxes
ansible_host: "{{ attack_box_ip }}"
ansible_user: root
ansible_ssh_private_key_file: "~/.ssh/{{ ssh_key_name }}"
ansible_ssh_common_args: "-o StrictHostKeyChecking=no"
attack_box_type: "{{ attack_box_type }}"
deployment_id: "{{ deployment_id }}"
- name: Save deployment information
copy:
content: |
# Attack Box Deployment Information
DEPLOYMENT_ID={{ deployment_id }}
ATTACK_BOX_NAME={{ attack_box_name }}
ATTACK_BOX_IP={{ attack_box_ip }}
ATTACK_BOX_ID={{ attack_box_id }}
ATTACK_BOX_TYPE={{ attack_box_type }}
SSH_KEY_PATH=~/.ssh/{{ ssh_key_name }}
PROVIDER=linode
REGION={{ linode_region }}
INSTANCE_TYPE={{ linode_instance_type }}
DEPLOYED_DATE={{ ansible_date_time.iso8601 }}
dest: "{{ playbook_dir }}/logs/attack_box_{{ deployment_id }}.env"
- name: Configure Attack Box
hosts: attack_boxes
gather_facts: yes
become: yes
vars:
setup_workspace: "{{ setup_workspace | default(true) }}"
setup_vpn: "{{ setup_vpn | default(false) }}"
setup_tor: "{{ setup_tor | default(false) }}"
tasks:
- name: Wait for system to be ready
wait_for_connection:
delay: 30
timeout: 300
- name: Update package cache only (avoid grub-pc issues)
apt:
update_cache: yes
cache_valid_time: 3600
when: ansible_os_family == "Debian"
retries: 3
delay: 10
- name: Include attack box configuration tasks
include_tasks: "{{ playbook_dir }}/../../modules/attack-box/tasks/configure_attack_box.yml"
when: deployment_type != "quick_recon_box"
- name: Include quick recon configuration tasks
include_tasks: "{{ playbook_dir }}/../../modules/attack-box/tasks/configure_quick_recon.yml"
when: deployment_type == "quick_recon_box"
- name: Display final setup information (Full Attack Box)
debug:
msg:
- "🎯 Attack Box Configuration Complete!"
- "📦 Instance: {{ attack_box_name }}"
- "🔑 SSH Command: ssh -i ~/.ssh/a-{{ deployment_id }} root@{{ ansible_host }}"
- "🔒 Root Password: {{ ansible_password | default('Check deployment_info_' + deployment_id + '.txt') }}"
- "📁 Credentials saved to: logs/deployment_info_{{ deployment_id }}.txt"
- ""
- "🛠️ Available Commands:"
- " recon <target> - Run reconnaissance automation"
- " portscan <target> - Run port scan automation"
- " webenum <target> - Run web enumeration automation"
when: deployment_type != "quick_recon_box"
- name: Display final setup information (Quick Recon Box)
debug:
msg:
- "🎯 Quick Recon Box Configuration Complete!"
- "📦 Instance: {{ attack_box_name }}"
- "🔑 SSH Command: ssh -i ~/.ssh/qr-{{ deployment_id }} root@{{ ansible_host }}"
- "🔒 Root Password: {{ ansible_password | default('Check deployment_info_' + deployment_id + '.txt') }}"
- "📁 Credentials saved to: logs/deployment_info_{{ deployment_id }}.txt"
- ""
- "🎯 Quick Recon Commands:"
- " qr - Go to working directory"
- " toolkit - Show available tools"
- " portscan <target> - Port scan"
- " subfind <domain> - Subdomain enumeration"
- " webscan <url> - Web application scan"
- " tor-recon <target> - Anonymous reconnaissance"
when: deployment_type == "quick_recon_box"
+205
View File
@@ -0,0 +1,205 @@
---
# Linode C2 Deployment Playbook
- name: Deploy Linode C2 server
hosts: localhost
gather_facts: false
connection: local
vars_files:
- vars.yaml
vars:
# Default values for required variables
linode_region: "{{ linode_region | default(region_choices | random) }}"
plan: "{{ plan | default('g6-standard-2') }}"
# Use static value to avoid recursive templating
c2_image_static: "linode/kali"
# Generate random instance name if not provided
c2_name: "{{ c2_name | default('node-' + 9999999999 | random | to_uuid | hash('md5') | truncate(8, True, '')) }}"
tasks:
- name: Validate required Linode token
assert:
that:
- linode_token is defined and linode_token != ""
fail_msg: "Linode API token is required. Set linode_token in vars.yaml or via --linode-token."
# Replace these tasks at the beginning of the playbook
- name: Set region for C2 server
set_fact:
c2_region_value: "{{ selected_region | default(linode_region, true) | default('us-east', true) }}"
- name: Display region selection information for debugging
debug:
msg:
- "Selected Region: {{ selected_region | default('Not set') }}"
- "Linode Region: {{ linode_region | default('Not set') }}"
- "Using Region: {{ c2_region_value }}"
when: debug | default(false) | bool
- name: Set random region only if absolutely no region specified
set_fact:
c2_region_value: "{{ region_choices | random }}"
when: region_choices is defined and region_choices|length > 0 and not selected_region is defined and not linode_region is defined and not c2_region_value is defined
- name: Create C2 Linode instance
block:
- name: Try creating instance in specified region
community.general.linode_v4:
access_token: "{{ linode_token }}"
label: "{{ c2_name }}"
type: "{{ plan }}"
region: "{{ c2_region_value }}"
image: "linode/kali"
root_pass: "{{ lookup('password', '/dev/null length=16') }}"
authorized_keys:
- "{{ lookup('file', ssh_key_path) }}"
state: present
register: c2_instance
rescue:
- name: Log region restriction error
debug:
msg: "Region {{ c2_region_value }} is restricted. Trying fallback regions..."
- name: Set fallback regions (most reliable ones first)
set_fact:
fallback_regions:
- "us-east"
- "us-central"
- "eu-west"
- "us-west"
- name: Try fallback regions
community.general.linode_v4:
access_token: "{{ linode_token }}"
label: "{{ c2_name }}"
type: "{{ plan }}"
region: "{{ item }}"
image: "linode/kali"
root_pass: "{{ lookup('password', '/dev/null length=16') }}"
authorized_keys:
- "{{ lookup('file', ssh_key_path) }}"
state: present
register: c2_instance
loop: "{{ fallback_regions }}"
when: c2_instance is not defined or c2_instance.failed
ignore_errors: yes
- name: Update deployment region with successful fallback
set_fact:
c2_region_value: "{{ item }}"
loop: "{{ fallback_regions }}"
when: c2_instance.results is defined and c2_instance.results[ansible_loop.index0] is defined and not c2_instance.results[ansible_loop.index0].failed
- name: Fail if all regions are restricted
fail:
msg: "All attempted regions are restricted. Please try again later or contact Linode support."
when: c2_instance.failed | default(true)
- name: Set c2_ip for later use
block:
- name: Extract instance info from direct creation
set_fact:
c2_ip: "{{ c2_instance.instance.ipv4[0] }}"
c2_instance_id: "{{ c2_instance.instance.id }}"
when: c2_instance.instance is defined
- name: Extract instance info from fallback creation
set_fact:
c2_ip: "{{ item.instance.ipv4[0] }}"
c2_instance_id: "{{ item.instance.id }}"
loop: "{{ c2_instance.results | default([]) }}"
when: c2_instance.results is defined and item.instance is defined and not item.failed
- name: Display final C2 deployment region and IP
debug:
msg: "C2 server deployed successfully in region {{ c2_region_value }} with IP {{ c2_ip }}"
# Enhanced SSH wait task for Linode/c2.yml
- name: Wait for C2 SSH to be available
block:
- name: Initial wait for port to be open
wait_for:
host: "{{ c2_instance.instance.ipv4[0] }}"
port: 22
delay: 60
timeout: 180
state: started
- name: Additional pause for SSH initialization
pause:
seconds: 60
- name: Test SSH connection
command: >
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10
-i {{ ssh_key_path | replace('.pub', '') }} root@{{ c2_instance.instance.ipv4[0] }} echo "SSH Ready"
register: ssh_test
retries: 5
delay: 20
until: ssh_test.rc == 0
ignore_errors: yes
- name: Fail if SSH test unsuccessful
fail:
msg: "Could not connect to C2 server via SSH after multiple attempts"
when: ssh_test.rc != 0
- name: Add C2 to inventory
add_host:
name: "c2"
groups: "c2servers"
ansible_host: "{{ c2_ip }}"
ansible_user: "root"
ansible_ssh_private_key_file: "{{ ssh_key_path | replace('.pub', '') }}"
ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
ansible_python_interpreter: "/usr/bin/python3" # Use remote system's Python
- name: Configure C2 server
hosts: c2servers
become: true
gather_facts: true
vars_files:
- vars.yaml
vars:
redirector_ip: "{{ hostvars['localhost']['redirector_ip'] | default('127.0.0.1') }}"
c2_subdomain: "{{ c2_subdomain | default('mail') }}"
tasks:
- name: Wait for apt to be available
apt:
update_cache: yes
register: apt_result
until: apt_result is success
retries: 5
delay: 10
- name: Disable root password authentication for SSH immediately
lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#?PasswordAuthentication'
line: 'PasswordAuthentication no'
state: present
- name: Restart SSH service to apply changes
service:
name: ssh
state: restarted
- name: Include common tool installation tasks
include_tasks: "../../common/tasks/install_tools.yml"
- name: Include common C2 configuration tasks
include_tasks: "../../modules/c2/tasks/configure_c2.yml"
- name: Include common mail server configuration tasks
include_tasks: "../../common/tasks/configure_mail.yml"
- name: Print deployment summary
debug:
msg:
- "C2 Server Deployment Complete!"
- "-----------------------------"
- "C2 Server IP: {{ ansible_host }}"
- "C2 Server Domain: {{ domain }}"
- "GoPhish Admin Port: {{ gophish_admin_port }}"
when: not disable_summary | default(false)
+117
View File
@@ -0,0 +1,117 @@
---
# Linode/cleanup.yml
- 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) }}"
cleanup_tracker: "{{ (tracker_name is defined and tracker_name != '') | ternary(true, false) }}"
cleanup_attack_box: "{{ (attack_box_name is defined and attack_box_name != '') | ternary(true, false) }}"
confirm_cleanup: false # Skip confirmation in automated teardown
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: Cleanup resources notice
debug:
msg: |
===============================================
CLEANUP OPERATION
===============================================
The following resources will be DELETED PERMANENTLY:
{% if cleanup_redirector and redirector_name is defined %}
- Redirector instance: {{ redirector_name }}
{% endif %}
{% if cleanup_c2 and c2_name is defined %}
- C2 instance: {{ c2_name }}
{% endif %}
{% if cleanup_tracker and tracker_name is defined %}
- Tracker instance: {{ tracker_name }}
{% endif %}
{% if cleanup_attack_box and attack_box_name is defined %}
- Attack Box instance: {{ attack_box_name }}
{% endif %}
when: confirm_cleanup | bool
- name: Confirm cleanup operation
pause:
prompt: "\n>>> Type 'yes' to confirm deletion or press Ctrl+C to abort <<<"
register: confirmation
when: confirm_cleanup | bool
- name: Skip cleanup if not confirmed
meta: end_play
when: confirm_cleanup | bool and (confirmation.user_input | default('')) != 'yes'
- name: Delete redirector instance
community.general.linode_v4:
access_token: "{{ linode_token }}"
label: "{{ redirector_name }}"
state: absent
when: cleanup_redirector and redirector_name is defined and redirector_name != ""
register: redirector_deletion
ignore_errors: yes
- name: Delete C2 instance
community.general.linode_v4:
access_token: "{{ linode_token }}"
label: "{{ c2_name }}"
state: absent
when: cleanup_c2 and c2_name is defined and c2_name != ""
register: c2_deletion
ignore_errors: yes
- name: Delete tracker instance
community.general.linode_v4:
access_token: "{{ linode_token }}"
label: "{{ tracker_name }}"
state: absent
when: cleanup_tracker and tracker_name is defined and tracker_name != ""
register: tracker_deletion
ignore_errors: yes
- name: Delete attack box instance
community.general.linode_v4:
access_token: "{{ linode_token }}"
label: "{{ attack_box_name }}"
state: absent
when: cleanup_attack_box and attack_box_name is defined and attack_box_name != ""
register: attack_box_deletion
ignore_errors: yes
- name: Clean up deployment state file
file:
path: "{{ playbook_dir }}/../deployment_state_{{ deployment_id }}.json"
state: absent
ignore_errors: yes
register: state_file_deletion
- name: Report state file cleanup
debug:
msg: "Deployment state file {{ 'deleted' if state_file_deletion.changed else 'not found' }}"
when: state_file_deletion is defined
- name: Report cleanup status
debug:
msg: |
Cleanup results:
{% if redirector_deletion is defined and redirector_name is defined %}
- Redirector {{ redirector_name }}: {{ redirector_deletion.changed | ternary('Deleted', 'Not found/Could not delete') }}
{% endif %}
{% if c2_deletion is defined and c2_name is defined %}
- C2 {{ c2_name }}: {{ c2_deletion.changed | ternary('Deleted', 'Not found/Could not delete') }}
{% endif %}
{% if tracker_deletion is defined and tracker_name is defined %}
- Tracker {{ tracker_name }}: {{ tracker_deletion.changed | ternary('Deleted', 'Not found/Could not delete') }}
{% endif %}
{% if attack_box_deletion is defined and attack_box_name is defined %}
- Attack Box {{ attack_box_name }}: {{ attack_box_deletion.changed | ternary('Deleted', 'Not found/Could not delete') }}
{% endif %}
+120
View File
@@ -0,0 +1,120 @@
---
# Linode-specific phishing infrastructure tasks
- name: Create Linode instances for phishing
uri:
url: "https://api.linode.com/v4/linode/instances"
method: POST
headers:
Authorization: "Bearer {{ linode_api_token }}"
Content-Type: "application/json"
body_format: json
body:
type: "{{ linode_instance_type | default('g6-nanode-1') }}"
region: "{{ linode_region | default('us-east') }}"
image: "{{ linode_image | default('linode/ubuntu22.04') }}"
label: "{{ deployment_id }}-{{ item }}"
root_pass: "{{ instance_password }}"
authorized_keys:
- "{{ ssh_public_key }}"
tags:
- "c2itall"
- "phishing"
- "{{ deployment_id }}"
status_code: 200
register: linode_instances
loop: "{{ deployment_components }}"
when: item in deployment_components
- name: Wait for instances to be running
uri:
url: "https://api.linode.com/v4/linode/instances/{{ item.json.id }}"
method: GET
headers:
Authorization: "Bearer {{ linode_api_token }}"
register: instance_status
until: instance_status.json.status == "running"
retries: 30
delay: 10
loop: "{{ linode_instances.results }}"
when: linode_instances.results is defined
- name: Get instance details
uri:
url: "https://api.linode.com/v4/linode/instances/{{ item.json.id }}"
method: GET
headers:
Authorization: "Bearer {{ linode_api_token }}"
register: instance_details
loop: "{{ linode_instances.results }}"
when: linode_instances.results is defined
- name: Set instance facts
set_fact:
phishing_instances: >-
{% set instances = [] %}
{% for result in instance_details.results %}
{% set instance = {
'id': result.json.id,
'label': result.json.label,
'ipv4': result.json.ipv4[0],
'region': result.json.region,
'type': result.json.type,
'status': result.json.status
} %}
{% set _ = instances.append(instance) %}
{% endfor %}
{{ instances }}
when: instance_details.results is defined
- name: Wait for SSH connectivity
wait_for:
host: "{{ item.ipv4 }}"
port: 22
delay: 30
timeout: 300
loop: "{{ phishing_instances }}"
when: phishing_instances is defined
- name: Update instance inventory
add_host:
name: "{{ item.ipv4 }}"
groups: "phishing_{{ item.label.split('-')[-1] }}"
ansible_host: "{{ item.ipv4 }}"
ansible_user: root
ansible_ssh_private_key_file: "{{ ssh_private_key_path }}"
ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
instance_id: "{{ item.id }}"
instance_label: "{{ item.label }}"
provider: "linode"
loop: "{{ phishing_instances }}"
when: phishing_instances is defined
- name: Configure Gophish servers
include_tasks: ../common/configure_gophish.yml
when: "'gophish' in deployment_components"
- name: Configure MTA fronts
include_tasks: ../common/configure_mta.yml
when: "'mta_front' in deployment_components"
- name: Configure redirectors
include_tasks: ../common/configure_redirector.yml
when: "'redirector' in deployment_components"
- name: Configure web servers
include_tasks: ../common/configure_webserver.yml
when: "'webserver' in deployment_components"
- name: Display deployment summary
debug:
msg: |
🎯 Linode Phishing Infrastructure Deployed
{{ phishing_instances | length }} instances created
Provider: Linode
Deployment ID: {{ deployment_id }}
Instances:
{% for instance in phishing_instances %}
- {{ instance.label }}: {{ instance.ipv4 }} ({{ instance.type }})
{% endfor %}
when: phishing_instances is defined
+65
View File
@@ -0,0 +1,65 @@
---
# Phishing infrastructure deployment playbook
# This is a comprehensive playbook that handles all phishing components
- name: Deploy Phishing Infrastructure
hosts: localhost
gather_facts: true
connection: local
vars_files:
- vars.yaml
tasks:
- name: Display deployment information
debug:
msg: |
Deploying phishing infrastructure
Deployment ID: {{ deployment_id }}
Provider: {{ provider }}
Domain: {{ domain | default(phishing_domain) }}
Components: {{ deployment_type }}
- name: Set deployment facts
set_fact:
deployment_timestamp: "{{ ansible_date_time.epoch }}"
phishing_deployment_results: {}
deployment_components: >-
{% set components = [] %}
{% if deployment_type in ['gophish_only', 'basic_phishing', 'advanced_phishing', 'full_phishing', 'fedramp_phishing'] %}
{% set _ = components.append('gophish') %}
{% endif %}
{% if deployment_type in ['mta_front_only', 'basic_phishing', 'advanced_phishing', 'full_phishing', 'ephemeral_mta'] %}
{% set _ = components.append('mta_front') %}
{% endif %}
{% if deployment_type in ['phishing_redirector_only', 'advanced_phishing', 'full_phishing'] %}
{% set _ = components.append('redirector') %}
{% endif %}
{% if deployment_type in ['phishing_webserver_only', 'full_phishing', 'fedramp_phishing'] %}
{% set _ = components.append('webserver') %}
{% endif %}
{{ components }}
- name: Include provider-specific tasks
include_tasks: "{{ provider }}_phishing.yml"
when: deployment_components | length > 0
- name: Ensure logs directory exists
file:
path: "../../logs"
state: directory
mode: '0755'
delegate_to: localhost
- name: Save deployment information
template:
src: phishing_deployment_info.j2
dest: "{{ playbook_dir }}/logs/phishing_deployment_{{ deployment_id }}.json"
delegate_to: localhost
- name: Display success message
debug:
msg: |
✅ Phishing infrastructure deployment completed
Deployment ID: {{ deployment_id }}
Components deployed: {{ deployment_components | join(', ') }}
Check logs/phishing_deployment_{{ deployment_id }}.json for details
+190
View File
@@ -0,0 +1,190 @@
---
# Linode Redirector-only Deployment Playbook
- name: Deploy Linode redirector
hosts: localhost
gather_facts: false
connection: local
vars_files:
- vars.yaml
vars:
# Use constant values directly to avoid recursive template resolution
redirector_image_static: "linode/debian12"
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."
# Update the region selection logic in Linode/redirector.yml
- name: Set region for redirector
set_fact:
deployment_region: "{{ selected_region | default(linode_region, true) | default('us-east', true) }}"
- name: Display region selection for redirector
debug:
msg: "Using region for redirector: {{ deployment_region }}"
when: debug | default(false) | bool
# Only use random region as a last resort
- name: Set random region only if no region specified
set_fact:
deployment_region: "{{ region_choices | random }}"
when: region_choices is defined and region_choices|length > 0 and not selected_region is defined and not linode_region is defined and not deployment_region is defined
- name: Create redirector Linode instance
block:
- name: Try creating instance in specified region
community.general.linode_v4:
access_token: "{{ linode_token }}"
label: "{{ redirector_name }}"
type: "{{ redirector_plan | default('g6-nanode-1') }}"
region: "{{ deployment_region }}"
image: "linode/debian12"
root_pass: "{{ lookup('password', '/dev/null length=16') }}"
authorized_keys:
- "{{ lookup('file', ssh_key_path) }}"
state: present
register: redirector_instance
rescue:
- name: Log region restriction error
debug:
msg: "Region {{ deployment_region }} is restricted. Trying fallback regions..."
- name: Set fallback regions (most reliable ones first)
set_fact:
fallback_regions:
- "us-east"
- "us-central"
- "eu-west"
- "us-west"
- name: Try fallback regions
community.general.linode_v4:
access_token: "{{ linode_token }}"
label: "{{ redirector_name }}"
type: "{{ redirector_plan | default('g6-nanode-1') }}"
region: "{{ item }}"
image: "linode/debian12"
root_pass: "{{ lookup('password', '/dev/null length=16') }}"
authorized_keys:
- "{{ lookup('file', ssh_key_path) }}"
state: present
register: redirector_instance
loop: "{{ fallback_regions }}"
when: redirector_instance is not defined or redirector_instance.failed
ignore_errors: yes
- name: Update deployment region with successful fallback
set_fact:
deployment_region: "{{ item }}"
loop: "{{ fallback_regions }}"
when: redirector_instance.results is defined and redirector_instance.results[ansible_loop.index0] is defined and not redirector_instance.results[ansible_loop.index0].failed
- name: Fail if all regions are restricted
fail:
msg: "All attempted regions are restricted. Please try again later or contact Linode support."
when: redirector_instance.failed | default(true)
- name: Set redirector_ip for later use
block:
- name: Extract instance info from direct creation
set_fact:
redirector_ip: "{{ redirector_instance.instance.ipv4[0] }}"
redirector_instance_id: "{{ redirector_instance.instance.id }}"
when: redirector_instance.instance is defined
- name: Extract instance info from fallback creation
set_fact:
redirector_ip: "{{ item.instance.ipv4[0] }}"
redirector_instance_id: "{{ item.instance.id }}"
loop: "{{ redirector_instance.results | default([]) }}"
when: redirector_instance.results is defined and item.instance is defined and not item.failed
- name: Display final deployment region and IP
debug:
msg: "Redirector deployed successfully in region {{ deployment_region }} with IP {{ redirector_ip }}"
# Enhanced SSH wait task with better retry mechanism
- name: Wait for redirector SSH to be available
block:
- name: Initial wait for port to be open
wait_for:
host: "{{ redirector_instance.instance.ipv4[0] }}"
port: 22
delay: 60
timeout: 180
state: started
- name: Additional pause for SSH initialization
pause:
seconds: 60
- name: Test SSH connection
command: >
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10
-i {{ ssh_key_path | replace('.pub', '') }} root@{{ redirector_instance.instance.ipv4[0] }} echo "SSH Ready"
register: ssh_test
retries: 5
delay: 20
until: ssh_test.rc == 0
ignore_errors: yes
- name: Fail if SSH test unsuccessful
fail:
msg: "Could not connect to redirector via SSH after multiple attempts"
when: ssh_test.rc != 0
- name: Add redirector to inventory
add_host:
name: "redirector"
groups: "redirectors"
ansible_host: "{{ redirector_ip }}"
ansible_user: "root"
ansible_ssh_private_key_file: "{{ ssh_key_path | replace('.pub', '') }}"
ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
- name: Configure redirector server
hosts: redirectors
become: true
gather_facts: true
vars_files:
- vars.yaml
vars:
c2_ip: "{{ hostvars['localhost']['c2_ip'] | default('127.0.0.1') }}"
redirector_subdomain: "{{ redirector_subdomain | default('cdn') }}"
ansible_python_interpreter: auto # Explicitly add this line
tasks:
- name: Wait for apt to be available
apt:
update_cache: yes
register: apt_result
until: apt_result is success
retries: 5
delay: 10
- name: Disable root password authentication for SSH immediately
lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#?PasswordAuthentication'
line: 'PasswordAuthentication no'
state: present
- name: Restart SSH service to apply changes
service:
name: ssh
state: restarted
- name: Include common redirector configuration tasks
include_tasks: "../../modules/redirectors/tasks/configure_redirector.yml"
- name: Print deployment summary
debug:
msg:
- "Redirector Deployment Complete!"
- "-------------------------------"
- "Redirector IP: {{ ansible_host }}"
- "Redirector Domain: {{ redirector_subdomain }}.{{ domain }} (Update DNS A record)"
when: not disable_summary | default(false)
@@ -0,0 +1,10 @@
{
"deployment_id": "{{ deployment_id }}",
"deployment_type": "{{ deployment_type }}",
"provider": "{{ provider }}",
"domain": "{{ phishing_domain | default(domain) }}",
"timestamp": "{{ ansible_date_time.iso8601 }}",
"components": {{ deployment_components | to_json }},
"results": {{ phishing_deployment_results | default({}) | to_json }},
"status": "completed"
}
+152
View File
@@ -0,0 +1,152 @@
---
# Linode Tracker Deployment Playbook
- name: Deploy Linode email tracking server
hosts: localhost
gather_facts: false
connection: local
vars_files:
- vars.yaml
vars:
# Default values - using minimalist settings
linode_region: "{{ linode_region | default(region_choices | random) }}"
plan: "{{ plan | default('g6-nanode-1') }}" # Use smallest plan for tracker
tracker_image: "{{ image | default('linode/debian12') }}" # Use Debian instead of Kali
# Generate random instance name if not provided
tracker_name: "{{ tracker_name | default('t-' + 9999999999 | random | to_uuid | hash('md5') | truncate(8, True, '')) }}"
tasks:
- name: Validate required Linode token
assert:
that:
- linode_token is defined and linode_token != ""
fail_msg: "Linode API token is required. Set linode_token in vars.yaml."
- name: Set region for tracker
set_fact:
tracker_region_value: "{{ selected_region | default(region, true) | default(linode_region, true) | default('us-east', true) }}"
when: region_choices is not defined or region_choices|length == 0
- name: Set random region for tracker
set_fact:
tracker_region_value: "{{ region_choices | random }}"
when: region_choices is defined and region_choices|length > 0 and tracker_region_value is not defined
- name: Create tracker Linode instance
community.general.linode_v4:
access_token: "{{ linode_token }}"
label: "{{ tracker_name }}"
type: "{{ plan }}"
region: "{{ tracker_region_value }}"
image: "{{ image | default('linode/debian12') }}"
root_pass: "{{ lookup('password', '/dev/null length=16') }}"
authorized_keys:
- "{{ lookup('file', ssh_key_path) }}"
state: present
register: tracker_instance
- name: Set tracker_ip for later use
set_fact:
tracker_ip: "{{ tracker_instance.instance.ipv4[0] }}"
tracker_instance_id: "{{ tracker_instance.instance.id }}"
- name: Wait for tracker SSH to be available
wait_for:
host: "{{ tracker_ip }}"
port: 22
delay: 60
timeout: 300
state: started
- name: Add tracker to inventory
add_host:
name: "tracker"
groups: "trackers"
ansible_host: "{{ tracker_ip }}"
ansible_user: "root"
ansible_ssh_private_key_file: "{{ ssh_key_path | replace('.pub', '') }}"
ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
- name: Configure tracker server
hosts: trackers
become: true
gather_facts: true
vars_files:
- vars.yaml
vars:
tracker_domain: "{{ tracker_domain | default('track.' + domain) }}"
tasks:
- name: Update and install minimal required packages
apt:
name:
- nginx
- certbot
- python3-certbot-nginx
- git
- python3-pip
- python3-venv
update_cache: yes
state: present
- name: Clone email tracker repository
git:
repo: https://github.com/Datalux/Osint-Tracker.git
dest: /root/Tools/tracker
version: master
ignore_errors: yes
- name: Install tracker dependencies
pip:
requirements: /root/Tools/tracker/requirements.txt
virtualenv: /root/Tools/tracker/venv
virtualenv_command: python3 -m venv
ignore_errors: yes
- name: Configure tracker settings
template:
src: "../../modules/tracker/templates/tracker-config.j2"
dest: /root/Tools/tracker/config.py
mode: '0644'
ignore_errors: yes
- name: Set up SSL if requested
block:
- name: Install Let's Encrypt certificate
shell: |
certbot --nginx -d {{ tracker_domain }} --non-interactive --agree-tos -m {{ tracker_email }}
args:
creates: /etc/letsencrypt/live/{{ tracker_domain }}/fullchain.pem
when: tracker_setup_ssl | default(true) | bool
ignore_errors: yes
- name: Create systemd service for tracker
template:
src: "../../modules/tracker/templates/tracker.service.j2"
dest: /etc/systemd/system/tracker.service
mode: '0644'
ignore_errors: yes
- name: Update tracker service to use virtualenv
blockinfile:
path: /etc/systemd/system/tracker.service
insertafter: "^\\[Service\\]"
block: |
Environment="PATH=/root/Tools/tracker/venv/bin:$PATH"
ExecStart=/root/Tools/tracker/venv/bin/python /root/Tools/tracker/app.py
- name: Start and enable tracker service
systemd:
name: tracker
state: started
enabled: yes
daemon_reload: yes
ignore_errors: yes
- name: Print deployment summary
debug:
msg:
- "Email Tracker Deployment Complete!"
- "------------------------------"
- "Tracker IP: {{ ansible_host }}"
- "Tracker Domain: {{ tracker_domain }} (Update DNS A record)"
@@ -0,0 +1,90 @@
---
# Linode provision tasks for one WEBRUNNER node
# Called in a loop — loop_var: node_chunk
# Requires: linode_token, ssh_key_name, webrunner_name, deployment_id,
# linode_instance_type, linode_region, scanner_ip_log, results_dir
- name: Generate root password for {{ node_chunk.node_name }}
set_fact:
wr_root_pass: "{{ lookup('password', '/dev/null chars=ascii_letters,digits length=32') }}"
- name: Read public key
slurp:
src: "~/.ssh/{{ ssh_key_name }}.pub"
register: wr_pubkey
- name: Register SSH key in Linode ({{ node_chunk.node_name }})
uri:
url: "https://api.linode.com/v4/profile/sshkeys"
method: POST
headers:
Authorization: "Bearer {{ linode_token }}"
Content-Type: "application/json"
body_format: json
body:
label: "{{ webrunner_name }}"
ssh_key: "{{ wr_pubkey.content | b64decode | trim }}"
status_code: [200, 201]
ignore_errors: true
- name: Create Linode instance {{ node_chunk.node_name }}
uri:
url: "https://api.linode.com/v4/linode/instances"
method: POST
headers:
Authorization: "Bearer {{ linode_token }}"
Content-Type: "application/json"
body_format: json
body:
label: "{{ node_chunk.node_name }}"
type: "{{ linode_instance_type | default('g6-standard-2') }}"
region: "{{ linode_region | default('us-east') }}"
image: "linode/debian12"
root_pass: "{{ wr_root_pass }}"
authorized_keys:
- "{{ wr_pubkey.content | b64decode | trim }}"
booted: true
backups_enabled: false
private_ip: false
tags:
- "webrunner"
- "{{ webrunner_name }}"
- "{{ deployment_id }}"
status_code: [200, 201]
register: wr_linode
- name: Extract node IP
set_fact:
wr_node_ip: "{{ wr_linode.json.ipv4[0] }}"
- name: Log scanner IP
lineinfile:
path: "{{ scanner_ip_log }}"
line: "{{ node_chunk.node_name }}: {{ wr_node_ip }}"
create: true
- name: Wait for SSH on {{ node_chunk.node_name }} ({{ wr_node_ip }})
wait_for:
host: "{{ wr_node_ip }}"
port: 22
delay: 30
timeout: 300
- name: Add {{ node_chunk.node_name }} to inventory
add_host:
name: "{{ wr_node_ip }}"
groups: webrunner_nodes
ansible_host: "{{ wr_node_ip }}"
ansible_user: root
ansible_ssh_private_key_file: "~/.ssh/{{ ssh_key_name }}"
ansible_ssh_common_args: "-o StrictHostKeyChecking=no"
node_name: "{{ node_chunk.node_name }}"
node_cidrs: "{{ node_chunk.cidrs }}"
node_ip_count: "{{ node_chunk.ip_count }}"
node_idx: "{{ node_chunk.idx }}"
linode_instance_id: "{{ wr_linode.json.id }}"
provider: linode
- name: Show {{ node_chunk.node_name }} ready
debug:
msg: "Linode node ready: {{ node_chunk.node_name }} @ {{ wr_node_ip }} ({{ node_chunk.ip_count | int | string }} IPs)"
@@ -0,0 +1,90 @@
---
# Linode provision tasks for one WEBRUNNER node
# Called in a loop — loop_var: node_chunk
# Requires: linode_token, ssh_key_name, webrunner_name, deployment_id,
# linode_instance_type, linode_region, scanner_ip_log, results_dir
- name: Generate root password for {{ node_chunk.node_name }}
set_fact:
wr_root_pass: "{{ lookup('password', '/dev/null chars=ascii_letters,digits length=32') }}"
- name: Read public key
slurp:
src: "~/.ssh/{{ ssh_key_name }}.pub"
register: wr_pubkey
- name: Register SSH key in Linode ({{ node_chunk.node_name }})
uri:
url: "https://api.linode.com/v4/profile/sshkeys"
method: POST
headers:
Authorization: "Bearer {{ linode_token }}"
Content-Type: "application/json"
body_format: json
body:
label: "{{ webrunner_name }}"
ssh_key: "{{ wr_pubkey.content | b64decode | trim }}"
status_code: [200, 201]
ignore_errors: true
- name: Create Linode instance {{ node_chunk.node_name }}
uri:
url: "https://api.linode.com/v4/linode/instances"
method: POST
headers:
Authorization: "Bearer {{ linode_token }}"
Content-Type: "application/json"
body_format: json
body:
label: "{{ node_chunk.node_name }}"
type: "{{ linode_instance_type | default('g6-standard-2') }}"
region: "{{ linode_region | default('us-east') }}"
image: "linode/debian12"
root_pass: "{{ wr_root_pass }}"
authorized_keys:
- "{{ wr_pubkey.content | b64decode | trim }}"
booted: true
backups_enabled: false
private_ip: false
tags:
- "webrunner"
- "{{ webrunner_name }}"
- "{{ deployment_id }}"
status_code: [200, 201]
register: wr_linode
- name: Extract node IP
set_fact:
wr_node_ip: "{{ wr_linode.json.ipv4[0] }}"
- name: Log scanner IP
lineinfile:
path: "{{ scanner_ip_log }}"
line: "{{ node_chunk.node_name }}: {{ wr_node_ip }}"
create: true
- name: Wait for SSH on {{ node_chunk.node_name }} ({{ wr_node_ip }})
wait_for:
host: "{{ wr_node_ip }}"
port: 22
delay: 30
timeout: 300
- name: Add {{ node_chunk.node_name }} to inventory
add_host:
name: "{{ wr_node_ip }}"
groups: webrunner_nodes
ansible_host: "{{ wr_node_ip }}"
ansible_user: root
ansible_ssh_private_key_file: "~/.ssh/{{ ssh_key_name }}"
ansible_ssh_common_args: "-o StrictHostKeyChecking=no"
node_name: "{{ node_chunk.node_name }}"
node_cidrs: "{{ node_chunk.cidrs }}"
node_ip_count: "{{ node_chunk.ip_count }}"
node_idx: "{{ node_chunk.idx }}"
linode_instance_id: "{{ wr_linode.json.id }}"
provider: linode
- name: Show {{ node_chunk.node_name }} ready
debug:
msg: "Linode node ready: {{ node_chunk.node_name }} @ {{ wr_node_ip }} ({{ node_chunk.ip_count | int | string }} IPs)"
+1
View File
@@ -0,0 +1 @@
# Provider utils package
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env python3
"""
AWS provider utilities for C2ingRed deployment system
"""
import random
import logging
from ..utils.common import COLORS, load_vars_file, confirm_action
def get_aws_credentials(provider_vars=None):
"""Get AWS credentials from user or vars file"""
if not provider_vars:
provider_vars = load_vars_file('aws')
default_aws_key = provider_vars.get('aws_access_key', '')
default_aws_secret = provider_vars.get('aws_secret_key', '')
print(f"\n{COLORS['BLUE']}AWS Configuration{COLORS['RESET']}")
aws_key = input(f"AWS Access Key [{'*****' if default_aws_key else 'leave blank to use AWS CLI profile'}]: ") or default_aws_key
aws_secret = input(f"AWS Secret Key [{'*****' if default_aws_secret else 'leave blank to use AWS CLI profile'}]: ") or default_aws_secret
return {
'aws_access_key': aws_key,
'aws_secret_key': aws_secret
}
def select_aws_region(provider_vars=None, component=None):
"""Let the user select an AWS region"""
if not provider_vars:
provider_vars = load_vars_file('aws')
regions = provider_vars.get('aws_region_choices', [])
component_str = f" for {component}" if component else ""
if not regions:
print(f"{COLORS['YELLOW']}No regions found for AWS, using us-east-1{COLORS['RESET']}")
return "us-east-1"
print(f"\nAvailable AWS regions{component_str}:")
for i, region in enumerate(regions, 1):
print(f" {i}. {region}")
region_input = input(f"\nSelect region{component_str} (number or leave blank for random): ")
if not region_input:
return random.choice(regions)
try:
region_choice = int(region_input)
if 1 <= region_choice <= len(regions):
return regions[region_choice - 1]
else:
print(f"{COLORS['RED']}Invalid choice, using random region{COLORS['RESET']}")
return random.choice(regions)
except ValueError:
print(f"{COLORS['RED']}Invalid input, using random region{COLORS['RESET']}")
return random.choice(regions)
def gather_aws_config():
"""Gather all AWS-specific configuration"""
provider_vars = load_vars_file('aws')
config = {}
# Get credentials
aws_creds = get_aws_credentials(provider_vars)
config.update(aws_creds)
# Get region
config['aws_region'] = select_aws_region(provider_vars)
# Additional AWS-specific settings
config['aws_instance_type'] = provider_vars.get('aws_instance_type', 't3.micro')
config['aws_volume_size'] = provider_vars.get('aws_volume_size', 20)
return config
+24
View File
@@ -0,0 +1,24 @@
---
# Common Gophish server configuration tasks
- name: Configure Gophish servers
debug:
msg: |
Configuring Gophish servers for deployment {{ deployment_id }}
Admin port: {{ gophish_admin_port | default('8090') }}
Domain: {{ phishing_domain | default(domain) }}
- name: Install Gophish
debug:
msg: "Would install and configure Gophish on target hosts"
delegate_to: "{{ item }}"
loop: "{{ groups['phishing_gophish'] | default([]) }}"
when: groups['phishing_gophish'] is defined
- name: Configure Gophish templates
debug:
msg: |
Would configure Gophish with:
- Email template: {{ email_template | default('office365_login') }}
- Campaign: {{ campaign_name | default('test-campaign') }}
- Sender: {{ sender_name | default('IT Support') }}
+23
View File
@@ -0,0 +1,23 @@
---
# Common MTA front server configuration tasks
- name: Configure MTA front servers
debug:
msg: |
Configuring MTA front servers for deployment {{ deployment_id }}
MTA hostname: {{ mta_hostname | default('mail.' + (phishing_domain | default(domain))) }}
SMTP auth user: {{ smtp_auth_user | default('admin') }}
- name: Install MTA software
debug:
msg: "Would install and configure MTA (Postfix/Exim) on target hosts"
delegate_to: "{{ item }}"
loop: "{{ groups['phishing_mta_front'] | default([]) }}"
when: groups['phishing_mta_front'] is defined
- name: Configure DKIM/SPF
debug:
msg: |
Would configure DKIM/SPF records for:
- Domain: {{ phishing_domain | default(domain) }}
- MTA hostname: {{ mta_hostname | default('mail.' + (phishing_domain | default(domain))) }}
+23
View File
@@ -0,0 +1,23 @@
---
# Common redirector configuration tasks
- name: Configure redirector servers
debug:
msg: |
Configuring redirector servers for deployment {{ deployment_id }}
Backend host: {{ backend_host | default('N/A') }}
Redirector type: {{ redirector_type | default('http') }}
- name: Install redirector software
debug:
msg: "Would install and configure Apache/Nginx redirectors on target hosts"
delegate_to: "{{ item }}"
loop: "{{ groups['phishing_redirector'] | default([]) }}"
when: groups['phishing_redirector'] is defined
- name: Configure SSL certificates
debug:
msg: |
Would configure SSL certificates for:
- Domain: {{ phishing_domain | default(domain) }}
- Let's Encrypt email: {{ letsencrypt_email | default('admin@' + (phishing_domain | default(domain))) }}
+22
View File
@@ -0,0 +1,22 @@
---
# Common web server configuration tasks
- name: Configure web servers
debug:
msg: |
Configuring web servers for deployment {{ deployment_id }}
Phishing hostname: {{ phishing_hostname | default('portal.' + (phishing_domain | default(domain))) }}
- name: Install web server software
debug:
msg: "Would install and configure Apache/Nginx web servers on target hosts"
delegate_to: "{{ item }}"
loop: "{{ groups['phishing_webserver'] | default([]) }}"
when: groups['phishing_webserver'] is defined
- name: Deploy phishing pages
debug:
msg: |
Would deploy phishing pages:
- Template: {{ email_template | default('office365_login') }}
- Domain: {{ phishing_domain | default(domain) }}
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""
FlokiNET provider utilities for C2ingRed deployment system
"""
import logging
from ..utils.common import COLORS, load_vars_file, validate_ip_address
def get_flokinet_credentials(provider_vars=None):
"""Get FlokiNET server IPs from user or vars file"""
if not provider_vars:
provider_vars = load_vars_file('flokinet')
default_redirector_ip = provider_vars.get('redirector_ip', '')
default_c2_ip = provider_vars.get('c2_ip', '')
print(f"\n{COLORS['BLUE']}FlokiNET Configuration{COLORS['RESET']}")
print(f"{COLORS['YELLOW']}Note: FlokiNET requires pre-provisioned servers{COLORS['RESET']}")
redirector_ip = input(f"FlokiNET Redirector IP Address [default: {default_redirector_ip}]: ") or default_redirector_ip
c2_ip = input(f"FlokiNET C2 Server IP Address [default: {default_c2_ip}]: ") or default_c2_ip
# Validate IP addresses
if redirector_ip and not validate_ip_address(redirector_ip):
print(f"{COLORS['RED']}Invalid redirector IP address{COLORS['RESET']}")
return None
if c2_ip and not validate_ip_address(c2_ip):
print(f"{COLORS['RED']}Invalid C2 server IP address{COLORS['RESET']}")
return None
return {
'flokinet_redirector_ip': redirector_ip,
'flokinet_c2_ip': c2_ip
}
def gather_flokinet_config():
"""Gather all FlokiNET-specific configuration"""
provider_vars = load_vars_file('flokinet')
config = {}
# Get server IPs
flokinet_ips = get_flokinet_credentials(provider_vars)
if not flokinet_ips:
return None
config.update(flokinet_ips)
# FlokiNET-specific settings
config['ssh_user'] = 'root' # FlokiNET typically uses root
return config
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env python3
"""
Linode provider utilities for C2ingRed deployment system
"""
import random
import logging
from ..utils.common import COLORS, load_vars_file
def get_linode_credentials(provider_vars=None):
"""Get Linode API token from user or vars file"""
if not provider_vars:
provider_vars = load_vars_file('linode')
default_token = provider_vars.get('linode_token', '')
print(f"\n{COLORS['BLUE']}Linode Configuration{COLORS['RESET']}")
token = input(f"Linode API Token [{'*****' if default_token else 'required'}]: ") or default_token
if not token:
print(f"{COLORS['RED']}Linode API token is required{COLORS['RESET']}")
return None
return {'linode_token': token}
def select_linode_region(provider_vars=None, component=None):
"""Let the user select a Linode region"""
if not provider_vars:
provider_vars = load_vars_file('linode')
regions = provider_vars.get('region_choices', [])
component_str = f" for {component}" if component else ""
if not regions:
print(f"{COLORS['YELLOW']}No regions found for Linode, using us-east{COLORS['RESET']}")
return "us-east"
print(f"\nAvailable Linode regions{component_str}:")
for i, region in enumerate(regions, 1):
print(f" {i}. {region}")
region_input = input(f"\nSelect region{component_str} (number or leave blank for random): ")
if not region_input:
return random.choice(regions)
try:
region_choice = int(region_input)
if 1 <= region_choice <= len(regions):
return regions[region_choice - 1]
else:
print(f"{COLORS['RED']}Invalid choice, using random region{COLORS['RESET']}")
return random.choice(regions)
except ValueError:
print(f"{COLORS['RED']}Invalid input, using random region{COLORS['RESET']}")
return random.choice(regions)
def gather_linode_config():
"""Gather all Linode-specific configuration"""
provider_vars = load_vars_file('linode')
config = {}
# Get credentials
linode_creds = get_linode_credentials(provider_vars)
if not linode_creds:
return None
config.update(linode_creds)
# Get region
config['linode_region'] = select_linode_region(provider_vars)
# Additional Linode-specific settings
config['linode_instance_type'] = provider_vars.get('linode_instance_type', 'g6-nanode-1')
config['linode_image'] = provider_vars.get('linode_image', 'linode/kali')
return config
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env python3
"""
Provider selection and configuration utilities
"""
from ..utils.common import COLORS, PROVIDERS
from .aws_utils import gather_aws_config
from .linode_utils import gather_linode_config
from .flokinet_utils import gather_flokinet_config
def select_provider():
"""Let the user select a cloud provider"""
print(f"\n{COLORS['BLUE']}Available cloud providers:{COLORS['RESET']}")
for i, provider in enumerate(PROVIDERS, 1):
print(f" {i}. {provider.capitalize()}")
while True:
try:
provider_choice = input(f"\nSelect a provider (1-{len(PROVIDERS)} or 99 to cancel): ")
if provider_choice == "99":
return None
provider_choice = int(provider_choice)
if 1 <= provider_choice <= len(PROVIDERS):
return PROVIDERS[provider_choice - 1]
else:
print(f"{COLORS['RED']}Please enter a number between 1 and {len(PROVIDERS)}{COLORS['RESET']}")
except ValueError:
print(f"{COLORS['RED']}Please enter a valid number{COLORS['RESET']}")
def gather_provider_config(provider):
"""Gather configuration for the specified provider"""
if provider == "aws":
return gather_aws_config()
elif provider == "linode":
return gather_linode_config()
elif provider == "flokinet":
return gather_flokinet_config()
else:
print(f"{COLORS['RED']}Unknown provider: {provider}{COLORS['RESET']}")
return None
+344
View File
@@ -0,0 +1,344 @@
---
# WEBRUNNER — Distributed geo-targeted recon
# Multi-provider: Linode, AWS, FlokiNET
# Controller: ~/tools/c2itall/ (CWD when ansible-playbook runs)
- name: WEBRUNNER — Provision scan nodes
hosts: localhost
connection: local
gather_facts: false
vars:
ansible_python_interpreter: "{{ ansible_playbook_python }}"
ssh_key_name: "{{ ssh_key_path | basename | regex_replace('\\.pub$', '') }}"
all_node_chunks: "{{ lookup('file', node_chunks_file) | from_json }}"
linode_chunks: "{{ all_node_chunks | selectattr('provider', 'equalto', 'linode') | list }}"
aws_chunks: "{{ all_node_chunks | selectattr('provider', 'equalto', 'aws') | list }}"
tasks:
- name: Ensure results directory
file:
path: "{{ results_dir }}"
state: directory
mode: '0755'
- name: Initialize scanner IP log
copy:
content: "# WEBRUNNER scanner IPs — {{ webrunner_name }}\n"
dest: "{{ scanner_ip_log }}"
mode: '0644'
force: false
# ── Linode — all nodes created in parallel ────────────────────────────────
- block:
- name: Generate shared root password
set_fact:
wr_root_pass: "{{ lookup('password', '/dev/null chars=ascii_letters,digits length=32') }}"
- name: Read SSH public key
slurp:
src: "~/.ssh/{{ ssh_key_name }}.pub"
register: wr_pubkey
- name: Register SSH key with Linode
uri:
url: "https://api.linode.com/v4/profile/sshkeys"
method: POST
headers:
Authorization: "Bearer {{ lookup('env', 'LINODE_TOKEN') }}"
Content-Type: "application/json"
body_format: json
body:
label: "{{ webrunner_name }}"
ssh_key: "{{ wr_pubkey.content | b64decode | trim }}"
status_code: [200, 201, 422]
ignore_errors: true
- name: Fire all Linode instance creates
uri:
url: "https://api.linode.com/v4/linode/instances"
method: POST
headers:
Authorization: "Bearer {{ lookup('env', 'LINODE_TOKEN') }}"
Content-Type: "application/json"
body_format: json
body:
label: "{{ item.node_name }}"
type: "{{ linode_instance_type | default('g6-nanode-1') }}"
region: "{{ item.region | default('us-east') }}"
image: "linode/debian12"
root_pass: "{{ wr_root_pass }}"
authorized_keys:
- "{{ wr_pubkey.content | b64decode | trim }}"
booted: true
backups_enabled: false
private_ip: false
tags:
- "webrunner"
- "{{ webrunner_name }}"
- "{{ deployment_id }}"
status_code: [200, 201]
loop: "{{ linode_chunks }}"
async: 300
poll: 0
register: wr_create_jobs
- name: Wait for all creates to confirm
async_status:
jid: "{{ item.ansible_job_id }}"
loop: "{{ wr_create_jobs.results }}"
register: wr_create_results
until: wr_create_results.finished
retries: 30
delay: 10
- name: Add all Linode nodes to inventory
add_host:
name: "{{ item.json.ipv4[0] }}"
groups: webrunner_nodes
ansible_host: "{{ item.json.ipv4[0] }}"
ansible_user: root
ansible_ssh_private_key_file: "~/.ssh/{{ ssh_key_name }}"
ansible_ssh_common_args: "-o StrictHostKeyChecking=no"
node_name: "{{ linode_chunks[idx].node_name }}"
node_cidrs: "{{ linode_chunks[idx].cidrs }}"
node_ip_count: "{{ linode_chunks[idx].ip_count }}"
node_idx: "{{ linode_chunks[idx].idx }}"
linode_instance_id: "{{ item.json.id }}"
provider: linode
loop: "{{ wr_create_results.results }}"
loop_control:
index_var: idx
- name: Log scanner IPs
lineinfile:
path: "{{ scanner_ip_log }}"
line: "{{ linode_chunks[idx].node_name }}: {{ item.json.ipv4[0] }}"
create: true
loop: "{{ wr_create_results.results }}"
loop_control:
index_var: idx
- name: Show provisioned nodes
debug:
msg: "Node ready: {{ linode_chunks[idx].node_name }} @ {{ item.json.ipv4[0] }} ({{ linode_chunks[idx].region }})"
loop: "{{ wr_create_results.results }}"
loop_control:
index_var: idx
when: linode_chunks | length > 0
# ── AWS — per-unique-region setup then parallel creates ───────────────────
- block:
- name: Read SSH public key for AWS
slurp:
src: "~/.ssh/{{ ssh_key_name }}.pub"
register: aws_pubkey
- name: Get unique AWS regions
set_fact:
aws_unique_regions: "{{ aws_chunks | map(attribute='region') | unique | list }}"
- name: Import SSH key pair per region
amazon.aws.ec2_key:
access_key: "{{ lookup('env', 'AWS_ACCESS_KEY_ID') }}"
secret_key: "{{ lookup('env', 'AWS_SECRET_ACCESS_KEY') }}"
name: "{{ webrunner_name }}"
key_material: "{{ aws_pubkey.content | b64decode | trim }}"
region: "{{ item }}"
state: present
loop: "{{ aws_unique_regions }}"
ignore_errors: true
- name: Create security group per region
amazon.aws.ec2_security_group:
access_key: "{{ lookup('env', 'AWS_ACCESS_KEY_ID') }}"
secret_key: "{{ lookup('env', 'AWS_SECRET_ACCESS_KEY') }}"
name: "webrunner-{{ webrunner_name }}"
description: "WEBRUNNER scan node SG"
region: "{{ item }}"
rules:
- proto: tcp
ports: [22]
cidr_ip: "{{ (operator_ip ~ '/32') if operator_ip else '0.0.0.0/0' }}"
rules_egress:
- proto: all
cidr_ip: "0.0.0.0/0"
state: present
loop: "{{ aws_unique_regions }}"
ignore_errors: true
- name: Fire all AWS instance creates
amazon.aws.ec2_instance:
access_key: "{{ lookup('env', 'AWS_ACCESS_KEY_ID') }}"
secret_key: "{{ lookup('env', 'AWS_SECRET_ACCESS_KEY') }}"
name: "{{ item.node_name }}"
instance_type: "{{ aws_instance_type | default('t3.micro') }}"
region: "{{ item.region }}"
image_id: "{{ ami_map[item.region] }}"
key_name: "{{ webrunner_name }}"
security_groups:
- "webrunner-{{ webrunner_name }}"
network:
assign_public_ip: true
tags:
Name: "{{ item.node_name }}"
webrunner: "{{ webrunner_name }}"
deployment_id: "{{ deployment_id }}"
state: running
wait: true
wait_timeout: 300
loop: "{{ aws_chunks }}"
async: 600
poll: 0
register: aws_create_jobs
- name: Wait for all AWS creates to confirm
async_status:
jid: "{{ item.ansible_job_id }}"
loop: "{{ aws_create_jobs.results }}"
register: aws_create_results
until: aws_create_results.finished
retries: 60
delay: 10
- name: Add all AWS nodes to inventory
add_host:
name: "{{ item.instances[0].public_ip_address }}"
groups: webrunner_nodes
ansible_host: "{{ item.instances[0].public_ip_address }}"
ansible_user: admin
ansible_ssh_private_key_file: "~/.ssh/{{ ssh_key_name }}"
ansible_ssh_common_args: "-o StrictHostKeyChecking=no"
node_name: "{{ aws_chunks[idx].node_name }}"
node_cidrs: "{{ aws_chunks[idx].cidrs }}"
node_ip_count: "{{ aws_chunks[idx].ip_count }}"
node_idx: "{{ aws_chunks[idx].idx }}"
ec2_instance_id: "{{ item.instances[0].instance_id }}"
ec2_region: "{{ aws_chunks[idx].region }}"
provider: aws
loop: "{{ aws_create_results.results }}"
loop_control:
index_var: idx
- name: Log AWS scanner IPs
lineinfile:
path: "{{ scanner_ip_log }}"
line: "{{ aws_chunks[idx].node_name }}: {{ item.instances[0].public_ip_address }}"
create: true
loop: "{{ aws_create_results.results }}"
loop_control:
index_var: idx
- name: Show provisioned AWS nodes
debug:
msg: "Node ready: {{ aws_chunks[idx].node_name }} @ {{ item.instances[0].public_ip_address }} ({{ aws_chunks[idx].region }})"
loop: "{{ aws_create_results.results }}"
loop_control:
index_var: idx
when: aws_chunks | length > 0
# ── FlokiNET ──────────────────────────────────────────────────────────────
- name: Provision FlokiNET nodes
include_tasks: "FlokiNET/webrunner_provision_tasks.yml"
loop: "{{ all_node_chunks | selectattr('provider', 'equalto', 'flokinet') | list }}"
loop_control:
loop_var: node_chunk
- name: WEBRUNNER — Configure and scan
hosts: webrunner_nodes
gather_facts: false
become: true
strategy: free
tasks:
- name: Wait for SSH
wait_for_connection:
delay: 20
timeout: 300
- name: Configure node
include_tasks: "{{ playbook_dir }}/../modules/webrunner/tasks/configure_node.yml"
- name: Run scan
include_tasks: "{{ playbook_dir }}/../modules/webrunner/tasks/run_scan.yml"
- name: Collect results
include_tasks: "{{ playbook_dir }}/../modules/webrunner/tasks/collect_results.yml"
- name: WEBRUNNER — Merge and report
hosts: localhost
connection: local
gather_facts: false
tasks:
- name: Merge per-node results
command: >
python3 {{ playbook_dir }}/../modules/webrunner/tasks/merge_results.py
--results-dir {{ results_dir }}
--output {{ results_dir }}/merged_results.json
--deployment-id {{ deployment_id }}
register: merge_out
ignore_errors: true
- name: Show merge output
debug:
var: merge_out.stdout_lines
when: merge_out.stdout_lines is defined and merge_out.stdout_lines | length > 0
- name: Summary
debug:
msg:
- "WEBRUNNER complete — {{ webrunner_name }}"
- "Results: {{ results_dir }}/merged_results.json"
- "Scanner IPs: {{ scanner_ip_log }}"
- "Log: logs/deployment_{{ deployment_id }}.log"
- name: WEBRUNNER — Teardown scan nodes
hosts: localhost
connection: local
gather_facts: false
tasks:
- name: Teardown Linode nodes
uri:
url: "https://api.linode.com/v4/linode/instances/{{ hostvars[item]['linode_instance_id'] }}"
method: DELETE
headers:
Authorization: "Bearer {{ lookup('env', 'LINODE_TOKEN') }}"
status_code: [200, 204]
loop: "{{ groups['webrunner_nodes'] | default([]) }}"
when:
- teardown_after_scan | default(true) | bool
- hostvars[item]['provider'] == 'linode'
- hostvars[item]['linode_instance_id'] is defined
ignore_errors: true
- name: Teardown AWS nodes
amazon.aws.ec2_instance:
access_key: "{{ lookup('env', 'AWS_ACCESS_KEY_ID') }}"
secret_key: "{{ lookup('env', 'AWS_SECRET_ACCESS_KEY') }}"
instance_ids:
- "{{ hostvars[item]['ec2_instance_id'] }}"
region: "{{ hostvars[item]['ec2_region'] }}"
state: terminated
loop: "{{ groups['webrunner_nodes'] | default([]) }}"
when:
- teardown_after_scan | default(true) | bool
- hostvars[item]['provider'] == 'aws'
- hostvars[item]['ec2_instance_id'] is defined
ignore_errors: true
- name: Teardown complete
debug:
msg: "All WEBRUNNER scan nodes destroyed."
when:
- teardown_after_scan | default(true) | bool
- groups['webrunner_nodes'] is defined
- groups['webrunner_nodes'] | length > 0