diff --git a/Linode/cleanup.yml b/Linode/cleanup.yml index 2ea1b00..052ef16 100644 --- a/Linode/cleanup.yml +++ b/Linode/cleanup.yml @@ -9,6 +9,7 @@ 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) }}" confirm_cleanup: "{{ confirm_cleanup | default(true) }}" tasks: @@ -31,6 +32,9 @@ {% 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 %} when: confirm_cleanup | bool - name: Confirm cleanup operation @@ -61,6 +65,15 @@ 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: Report cleanup status debug: msg: | @@ -70,4 +83,7 @@ {% endif %} {% if c2_deletion is defined %} - C2 {{ c2_name }}: {{ c2_deletion.changed | ternary('Deleted', 'Not found/Could not delete') }} + {% endif %} + {% if tracker_deletion is defined %} + - Tracker {{ tracker_name }}: {{ tracker_deletion.changed | ternary('Deleted', 'Not found/Could not delete') }} {% endif %} \ No newline at end of file diff --git a/Linode/tracker.yml b/Linode/tracker.yml index 6b9e3b0..7068f1f 100644 --- a/Linode/tracker.yml +++ b/Linode/tracker.yml @@ -8,10 +8,10 @@ vars_files: - vars.yaml vars: - # Default values + # 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: "linode/debian12" + 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, '')) }}" @@ -67,15 +67,15 @@ vars: tracker_domain: "{{ tracker_domain | default('track.' + domain) }}" tasks: - - name: Update and install required packages + - name: Update and install minimal required packages apt: name: - nginx - certbot - python3-certbot-nginx - git - - curl - python3-pip + - python3-venv update_cache: yes state: present @@ -89,6 +89,8 @@ - name: Install tracker dependencies pip: requirements: /opt/tracker/requirements.txt + virtualenv: /opt/tracker/venv + virtualenv_command: python3 -m venv ignore_errors: yes - name: Configure tracker settings @@ -115,6 +117,14 @@ mode: '0644' ignore_errors: yes + - name: Update tracker service to use virtualenv + blockinfile: + path: /etc/systemd/system/tracker.service + insertafter: "^\\[Service\\]" + block: | + Environment="PATH=/opt/tracker/venv/bin:$PATH" + ExecStart=/opt/tracker/venv/bin/python /opt/tracker/app.py + - name: Start and enable tracker service systemd: name: tracker diff --git a/deploy.py b/deploy.py index fe08cf3..09ad498 100644 --- a/deploy.py +++ b/deploy.py @@ -110,6 +110,7 @@ def parse_arguments(): # Tracker deployment options parser.add_argument('--deploy-tracker', action='store_true', help='Deploy phishing email tracking server') + parser.add_argument('--integrated-tracker', action='store_true', help='Deploy tracker on C2 server instead of separate instance') parser.add_argument('--tracker-domain', help='Domain name for the tracker server') parser.add_argument('--tracker-email', help='Email for Let\'s Encrypt certificate for tracker') parser.add_argument('--tracker-name', help='Name for tracker instance (default: random)') @@ -258,31 +259,43 @@ def interactive_setup(): else: config['ssh_key'] = None # Will generate a new key - # Deployment type + # Deployment type with integrated tracker option print("\nDeployment type:") - print(" 1. Full deployment (Redirector + C2)") - print(" 2. Redirector only") - print(" 3. C2 server only") + print(" 1. Full deployment (Redirector + C2) [default]") + print(" 2. Full deployment with integrated tracker (Redirector + C2 + Tracker)") + print(" 3. Redirector only") + print(" 4. C2 server only") + print(" 5. Standalone tracker only") - while True: - try: - deploy_choice = int(input("\nSelect deployment type (1-3): ")) - if deploy_choice == 1: - config['redirector_only'] = False - config['c2_only'] = False - break - elif deploy_choice == 2: - config['redirector_only'] = True - config['c2_only'] = False - break - elif deploy_choice == 3: - config['redirector_only'] = False - config['c2_only'] = True - break - else: - print("Please enter a number between 1 and 3") - except ValueError: - print("Please enter a valid number") + deploy_choice = input("\nSelect deployment type (1-5) [default: 1]: ") + if not deploy_choice or deploy_choice == "1": + config['redirector_only'] = False + config['c2_only'] = False + config['deploy_tracker'] = False + config['integrated_tracker'] = False + elif deploy_choice == "2": + config['redirector_only'] = False + config['c2_only'] = False + config['deploy_tracker'] = True + config['integrated_tracker'] = True + elif deploy_choice == "3": + config['redirector_only'] = True + config['c2_only'] = False + config['deploy_tracker'] = False + elif deploy_choice == "4": + config['redirector_only'] = False + config['c2_only'] = True + config['deploy_tracker'] = False + elif deploy_choice == "5": + config['redirector_only'] = False + config['c2_only'] = False + config['deploy_tracker'] = True + config['integrated_tracker'] = False + else: + print("Invalid choice, using default (Full deployment)") + config['redirector_only'] = False + config['c2_only'] = False + config['deploy_tracker'] = False # Domain configuration default_domain = vars_data.get('domain', 'example.com') @@ -309,19 +322,10 @@ def interactive_setup(): config['secure_memory'] = True if (secure_memory == 'y' or (default_secure_memory and secure_memory != 'n')) else False config['zero_logs'] = True if (zero_logs == 'y' or (default_zero_logs and zero_logs != 'n')) else False - # Post-deployment options - config['ssh_after_deploy'] = input("\nSSH into instance after deployment? (y/n) [default: n]: ").lower() == 'y' - - # Debug mode - config['debug'] = input("Enable debug mode? (y/n) [default: n]: ").lower() == 'y' - - # Additional options - deploy_tracker = input("\nDeploy email tracking server? (y/n) [default: n]: ").lower() - config['deploy_tracker'] = deploy_tracker == 'y' - + # Tracker configuration if enabled if config['deploy_tracker']: default_tracker_domain = f"track.{config['domain']}" - config['tracker_domain'] = input(f"Tracker domain [default: {default_tracker_domain}]: ") or default_tracker_domain + config['tracker_domain'] = input(f"\nTracker domain [default: {default_tracker_domain}]: ") or default_tracker_domain default_tracker_email = config['letsencrypt_email'] config['tracker_email'] = input(f"Tracker Let's Encrypt email [default: {default_tracker_email}]: ") or default_tracker_email @@ -335,6 +339,12 @@ def interactive_setup(): timestamp = int(time.time()) % 10000 config['tracker_name'] = f"t-{rand_suffix}-{timestamp}" # Changed from track- to t- + # Post-deployment options + config['ssh_after_deploy'] = input("\nSSH into instance after deployment? (y/n) [default: n]: ").lower() == 'y' + + # Debug mode + config['debug'] = input("Enable debug mode? (y/n) [default: n]: ").lower() == 'y' + # SSH key if not already set if 'ssh_key' not in config or not config['ssh_key']: ssh_key = input("\nPath to SSH Private Key [leave blank to generate new key]: ") @@ -356,6 +366,10 @@ def interactive_setup(): config['smtp_auth_pass'] = vars_data.get('smtp_auth_pass', ''.join(random.choices(string.ascii_letters + string.digits, k=20))) config['shell_handler_port'] = vars_data.get('shell_handler_port', str(random.randint(4000, 65000))) + # Set up integrated tracker flag for deployment + if config.get('deploy_tracker') and config.get('integrated_tracker'): + config['setup_integrated_tracker'] = True + # Set SSH key path for proper reference if config['ssh_key'].startswith(os.path.expanduser("~/.ssh/c2deploy_")): config['ssh_key_path'] = f"{config['ssh_key']}.pub" @@ -612,6 +626,11 @@ def deploy_infrastructure(config): elif provider == "aws": config['ssh_user'] = "kali" + # Set tracker deployment flag for C2 configuration + if config.get('deploy_tracker') and config.get('integrated_tracker'): + config['setup_integrated_tracker'] = True + logging.info("Integrated tracker will be deployed on C2 server") + # Select random region if not specified if not config.get('region'): if provider == "linode" and config.get('linode_region'): @@ -933,8 +952,11 @@ def cleanup_resources(config, interactive=True): print("Deployment failed or was interrupted. Resources to clean up:") redirector_name = config.get('redirector_name', 'None') c2_name = config.get('c2_name', 'None') + tracker_name = config.get('tracker_name', 'None') print(f" - Redirector: {redirector_name}") print(f" - C2 Server: {c2_name}") + if config.get('deploy_tracker') and not config.get('integrated_tracker'): + print(f" - Tracker: {tracker_name}") print("============================================================") try: @@ -955,8 +977,10 @@ def cleanup_resources(config, interactive=True): "confirm_cleanup": False, # Skip confirmation prompt "redirector_name": config.get('redirector_name'), "c2_name": config.get('c2_name'), + "tracker_name": config.get('tracker_name'), "cleanup_redirector": True, - "cleanup_c2": True + "cleanup_c2": True, + "cleanup_tracker": config.get('deploy_tracker', False) and not config.get('integrated_tracker', False) } playbook = f"{provider_dir}/cleanup.yml" @@ -978,7 +1002,7 @@ def cleanup_resources(config, interactive=True): logging.error(f"Cleanup playbook failed: {stderr}") # Log what we attempted to clean up - logging.error(f"Failed to clean up resources: redirector={config.get('redirector_name')}, c2={config.get('c2_name')}") + logging.error(f"Failed to clean up resources: redirector={config.get('redirector_name')}, c2={config.get('c2_name')}, tracker={config.get('tracker_name')}") else: logging.info("Cleanup completed successfully") except Exception as e: @@ -1091,7 +1115,7 @@ def teardown_infrastructure(config): return False def deploy_tracker(config): - """Deploy email tracking server""" + """Deploy email tracking server with minimal resources""" provider = config['provider'] provider_dir = PROVIDER_DIRS.get(provider, provider.upper()) @@ -1107,6 +1131,24 @@ def deploy_tracker(config): if config.get('linode_token'): os.environ['LINODE_TOKEN'] = config['linode_token'] + # Override configuration for minimal tracker deployment + tracker_config = config.copy() + + # Use smaller instance sizes for tracker + if provider == "linode": + tracker_config['plan'] = 'g6-nanode-1' # Smallest viable Linode plan + tracker_config['image'] = 'linode/debian12' # Use Debian instead of Kali + elif provider == "aws": + tracker_config['instance_type'] = 't2.micro' # Smallest viable AWS instance + # For AWS, specify a Debian/Ubuntu AMI instead of Kali + if 'ami_map' in tracker_config: + # Try to find a Debian/Ubuntu AMI for the region + region = tracker_config.get('aws_region', tracker_config.get('region')) + for ami_id, ami_info in tracker_config.get('ami_map', {}).items(): + if 'ubuntu' in ami_id.lower() or 'debian' in ami_id.lower(): + tracker_config['ami_id'] = ami_id + break + # Determine playbook path playbook = f"{provider_dir}/tracker.yml" if not os.path.exists(playbook): @@ -1114,12 +1156,12 @@ def deploy_tracker(config): return False # Create inventory file - inventory_path = create_inventory_file(config, "tracker") + inventory_path = create_inventory_file(tracker_config, "local") # Run the playbook try: success, stdout, stderr = run_ansible_playbook( - playbook, inventory_path, config, config.get('debug', False) + playbook, inventory_path, tracker_config, tracker_config.get('debug', False) ) # Clean up inventory file @@ -1253,12 +1295,17 @@ C2itAll - Red Team Infrastructure Setup # Tracker options config['deploy_tracker'] = args.deploy_tracker + config['integrated_tracker'] = args.integrated_tracker if args.deploy_tracker: config['tracker_domain'] = args.tracker_domain or f"track.{config['domain']}" config['tracker_email'] = args.tracker_email or config['letsencrypt_email'] config['tracker_ipinfo_token'] = args.tracker_ipinfo_token config['tracker_setup_ssl'] = args.tracker_setup_ssl config['tracker_create_pixel'] = args.tracker_create_pixel + + # Set the integrated tracker flag for deployment + if args.integrated_tracker: + config['setup_integrated_tracker'] = True # SSH after deploy config['ssh_after_deploy'] = args.ssh_after_deploy @@ -1308,34 +1355,32 @@ C2itAll - Red Team Infrastructure Setup # Run deployment try: - # Deploy tracker if requested - if config.get('deploy_tracker'): + # Deploy the standard infrastructure first (redirector + C2) + if not config.get('deploy_tracker') or config.get('integrated_tracker'): + success = deploy_infrastructure(config) + if not success: + logging.error("Deployment failed!") + cleanup_resources(config, interactive=True) + return + logging.info("Deployment completed successfully!") + + # SSH into instance if requested + if config.get('ssh_after_deploy'): + ssh_to_instance(config) + + # Deploy standalone tracker if requested and not integrated + elif config.get('deploy_tracker') and not config.get('integrated_tracker'): success = deploy_tracker(config) if not success: logging.error("Tracker deployment failed!") cleanup_resources(config, interactive=True) return - logging.info("Tracker deployment completed successfully!") # SSH into tracker if requested if config.get('ssh_after_deploy'): ssh_to_instance(config) - return - - # Deploy main infrastructure - success = deploy_infrastructure(config) - - if success: - logging.info("Deployment completed successfully!") - - # SSH into instance if requested - if config.get('ssh_after_deploy'): - ssh_to_instance(config) - else: - logging.error("Deployment failed!") - cleanup_resources(config, interactive=True) except KeyboardInterrupt: print("\n\nDeployment interrupted by user") logging.info("Deployment interrupted by user") diff --git a/files/simple_email_tracker.py b/files/simple_email_tracker.py new file mode 100644 index 0000000..c372527 --- /dev/null +++ b/files/simple_email_tracker.py @@ -0,0 +1,214 @@ +#!/usr/bin/env python3 +""" +Simple Email Tracking Server + +A minimal Flask application that serves transparent tracking pixels +and logs email opens with metadata. +""" + +import os +import json +import time +import logging +from datetime import datetime +from flask import Flask, request, send_file, render_template_string + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler('/opt/tracker/data/tracker.log'), + logging.StreamHandler() + ] +) + +# Create Flask app +app = Flask(__name__) + +# Directory to store tracking data +DATA_DIR = '/opt/tracker/data' +os.makedirs(DATA_DIR, exist_ok=True) + +# Path to 1x1 transparent pixel +PIXEL_PATH = os.path.join(DATA_DIR, 'pixel.png') + +# Create 1x1 transparent PNG if it doesn't exist +if not os.path.exists(PIXEL_PATH): + from PIL import Image + img = Image.new('RGBA', (1, 1), color=(0, 0, 0, 0)) + img.save(PIXEL_PATH) + +# Path to tracking data +TRACKING_DATA_PATH = os.path.join(DATA_DIR, 'tracking_data.json') + +def load_tracking_data(): + """Load existing tracking data from JSON file""" + if os.path.exists(TRACKING_DATA_PATH): + try: + with open(TRACKING_DATA_PATH, 'r') as f: + return json.load(f) + except json.JSONDecodeError: + logging.error("Error loading tracking data, starting fresh") + return {} + +def save_tracking_data(data): + """Save tracking data to JSON file""" + with open(TRACKING_DATA_PATH, 'w') as f: + json.dump(data, f, indent=2) + +@app.route('/pixel/.png') +def tracking_pixel(tracking_id): + """Serve a tracking pixel and log the request""" + # Get client information + user_agent = request.headers.get('User-Agent', 'Unknown') + ip_address = request.remote_addr + timestamp = datetime.now().isoformat() + referer = request.headers.get('Referer', 'Unknown') + + # Log the tracking event + logging.info(f"Pixel loaded - ID: {tracking_id}, IP: {ip_address}") + + # Add tracking event to data + tracking_data = load_tracking_data() + + if tracking_id not in tracking_data: + tracking_data[tracking_id] = [] + + tracking_data[tracking_id].append({ + 'timestamp': timestamp, + 'ip_address': ip_address, + 'user_agent': user_agent, + 'referer': referer + }) + + save_tracking_data(tracking_data) + + # Return the 1x1 transparent pixel + return send_file(PIXEL_PATH, mimetype='image/png') + +@app.route('/') +def dashboard(): + """Display tracking statistics dashboard""" + tracking_data = load_tracking_data() + + # Prepare data for the dashboard + stats = [] + for tracking_id, events in tracking_data.items(): + stats.append({ + 'id': tracking_id, + 'views': len(events), + 'last_view': events[-1]['timestamp'] if events else 'Never', + 'unique_ips': len(set(e['ip_address'] for e in events)) + }) + + # Sort by most views + stats.sort(key=lambda x: x['views'], reverse=True) + + # Simple HTML dashboard template + template = """ + + + + Email Tracking Dashboard + + + +
+

Email Tracking Dashboard

+

To track email opens, add this HTML to your emails:

+
<img src="https://YOUR_DOMAIN/px/YOUR_TRACKING_ID.png" height="1" width="1" />
+ +

Tracking Statistics

+ + + + + + + + + {% for stat in stats %} + + + + + + + + {% endfor %} +
Tracking IDViewsUnique IPsLast ViewDetails
{{ stat.id }}{{ stat.views }}{{ stat.unique_ips }}{{ stat.last_view }}View Details
+
+ + + """ + + return render_template_string(template, stats=stats) + +@app.route('/details/') +def tracking_details(tracking_id): + """Display detailed tracking information for a specific ID""" + tracking_data = load_tracking_data() + + if tracking_id not in tracking_data: + return f"No data found for tracking ID: {tracking_id}", 404 + + events = tracking_data[tracking_id] + + # Simple HTML template for details + template = """ + + + + Tracking Details: {{ tracking_id }} + + + +
+ +

Tracking Details: {{ tracking_id }}

+

Total views: {{ events|length }}

+ +

Events

+ + + + + + + + {% for event in events %} + + + + + + + {% endfor %} +
TimeIP AddressUser AgentReferer
{{ event.timestamp }}{{ event.ip_address }}{{ event.user_agent }}{{ event.referer }}
+
+ + + """ + + return render_template_string(template, tracking_id=tracking_id, events=events) + +if __name__ == '__main__': + app.run(host='127.0.0.1', port=5000, debug=False) \ No newline at end of file diff --git a/files/tracker-nginx.conf b/files/tracker-nginx.conf new file mode 100644 index 0000000..f1645f7 --- /dev/null +++ b/files/tracker-nginx.conf @@ -0,0 +1,47 @@ +server { + listen 80; + listen [::]:80; + server_name {{ tracker_domain }}; + + # Redirect to HTTPS if SSL is enabled + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name {{ tracker_domain }}; + + # SSL Configuration + ssl_certificate /etc/letsencrypt/live/{{ tracker_domain }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{{ tracker_domain }}/privkey.pem; + + # Restrict dashboard to localhost only + location / { + allow 127.0.0.1; + deny all; + proxy_pass http://127.0.0.1:5000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + # Allow public access only to pixel endpoints + location ~ ^/pixel/(.+)\.png$ { + # Public access allowed + proxy_pass http://127.0.0.1:5000/pixel/$1.png; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + # Cache control - don't cache tracking pixels + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0"; + expires off; + } + + # Security headers + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "no-referrer" always; +} \ No newline at end of file diff --git a/files/tracker-stats.sh b/files/tracker-stats.sh new file mode 100644 index 0000000..eb57a35 --- /dev/null +++ b/files/tracker-stats.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# CLI tool to view email tracking stats + +DATA_FILE="/opt/tracker/data/tracking_data.json" + +function show_summary() { + if [ ! -f "$DATA_FILE" ]; then + echo "No tracking data found." + exit 1 + fi + + echo "Email Tracking Summary:" + echo "=======================" + jq -r 'to_entries | sort_by(.value | length) | reverse | .[] | "\(.key): \(.value | length) views"' $DATA_FILE +} + +function show_details() { + ID=$1 + if [ ! -f "$DATA_FILE" ]; then + echo "No tracking data found." + exit 1 + fi + + echo "Details for tracking ID: $ID" + echo "==========================" + jq -r --arg id "$ID" '.[$id] | if . then .[] | "\(.timestamp) | \(.ip_address) | \(.user_agent)" else "No data found for this ID" end' $DATA_FILE +} + +case "$1" in + "list") + show_summary + ;; + "details") + if [ -z "$2" ]; then + echo "Usage: $0 details TRACKING_ID" + exit 1 + fi + show_details "$2" + ;; + *) + echo "Usage: $0 [list|details TRACKING_ID]" + echo " list - Show summary of all tracking IDs" + echo " details ID - Show details for specific tracking ID" + ;; +esac \ No newline at end of file diff --git a/files/tracker.service b/files/tracker.service new file mode 100644 index 0000000..b70c22f --- /dev/null +++ b/files/tracker.service @@ -0,0 +1,19 @@ +[Unit] +Description=Email Tracking Server +After=network.target + +[Service] +User=tracker +Group=tracker +WorkingDirectory=/opt/tracker +ExecStart=/opt/tracker/venv/bin/python /opt/tracker/simple_email_tracker.py +Restart=always +RestartSec=10 + +# Security settings +PrivateTmp=true +ProtectSystem=full +NoNewPrivileges=true + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/tasks/configure-redirector.yml b/old_playbooks/configure-redirector.yml similarity index 100% rename from tasks/configure-redirector.yml rename to old_playbooks/configure-redirector.yml diff --git a/tasks/configure_c2.yml b/tasks/configure_c2.yml index 9bd1e18..410f6cf 100644 --- a/tasks/configure_c2.yml +++ b/tasks/configure_c2.yml @@ -109,4 +109,9 @@ # Check if beacon server is already running if ! pgrep -f "/opt/c2/serve-beacons.sh" > /dev/null; then nohup /opt/c2/serve-beacons.sh > /dev/null 2>&1 & - fi \ No newline at end of file + fi + +# Include integrated tracker tasks if requested +- name: Include integrated tracker setup + include_tasks: "configure_integrated_tracker.yml" + when: setup_integrated_tracker | default(false) | bool \ No newline at end of file diff --git a/tasks/configure_integrated_tracker.yml b/tasks/configure_integrated_tracker.yml new file mode 100644 index 0000000..650f723 --- /dev/null +++ b/tasks/configure_integrated_tracker.yml @@ -0,0 +1,156 @@ +--- +# Common task for configuring integrated email tracker on C2 server + +- name: Check if integrated tracker setup is requested + debug: + msg: "Setting up integrated email tracker on C2 server" + when: setup_integrated_tracker | default(false) | bool + +- name: Install required packages for tracker + apt: + name: + - python3-pip + - python3-venv + - python3-pillow + - nginx + - certbot + - python3-certbot-nginx + - jq + state: present + update_cache: yes + when: setup_integrated_tracker | default(false) | bool + +- name: Create tracker directory + file: + path: /opt/tracker + state: directory + mode: '0755' + owner: root + group: root + when: setup_integrated_tracker | default(false) | bool + +- name: Create tracker data directory + file: + path: /opt/tracker/data + state: directory + mode: '0755' + owner: root + group: root + when: setup_integrated_tracker | default(false) | bool + +- name: Create tracker system user + user: + name: tracker + system: yes + shell: /usr/sbin/nologin + home: /opt/tracker + create_home: no + when: setup_integrated_tracker | default(false) | bool + +- name: Create Python virtual environment for tracker + pip: + virtualenv: /opt/tracker/venv + name: + - flask + - pillow + - gunicorn + virtualenv_command: /usr/bin/python3 -m venv + environment: + PATH: "/usr/local/bin:/usr/bin:/bin" + vars: + ansible_python_interpreter: /usr/bin/python3 + when: setup_integrated_tracker | default(false) | bool + +- name: Copy tracker application code + copy: + src: "../files/simple_email_tracker.py" + dest: /opt/tracker/simple_email_tracker.py + mode: '0755' + owner: root + group: root + when: setup_integrated_tracker | default(false) | bool + +- name: Copy tracker statistics CLI tool + copy: + src: "../files/tracker-stats.sh" + dest: /opt/tracker/tracker-stats.sh + mode: '0755' + owner: root + group: root + when: setup_integrated_tracker | default(false) | bool + +- name: Copy systemd service file for tracker + copy: + src: "../files/tracker.service" + dest: /etc/systemd/system/tracker.service + mode: '0644' + owner: root + group: root + when: setup_integrated_tracker | default(false) | bool + +- name: Set correct permissions for tracker directories + file: + path: "{{ item }}" + state: directory + owner: tracker + group: tracker + recurse: yes + loop: + - /opt/tracker + - /opt/tracker/data + when: setup_integrated_tracker | default(false) | bool + +- name: Create NGINX site config for tracker + template: + src: "../files/tracker-nginx.conf" + dest: /etc/nginx/sites-available/tracker + mode: '0644' + owner: root + group: root + vars: + tracker_domain: "{{ tracker_domain | default('track.' + domain) }}" + when: setup_integrated_tracker | default(false) | bool + +- name: Enable tracker NGINX site + file: + src: /etc/nginx/sites-available/tracker + dest: /etc/nginx/sites-enabled/tracker + state: link + when: setup_integrated_tracker | default(false) | bool + +- name: Configure redirector to proxy tracking requests + lineinfile: + path: /etc/nginx/sites-available/default + insertafter: "^\\s*location / {" + line: " # Email tracker proxy path\n location /px/(.*)\\.png$ {\n proxy_pass http://{{ c2_ip }}:443/pixel/$1.png;\n proxy_set_header Host {{ tracker_domain }};\n proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_set_header X-Forwarded-Proto https;\n }" + delegate_to: "{{ groups['redirectors'][0] }}" + when: setup_integrated_tracker | default(false) | bool and groups['redirectors'] is defined + +- name: Set up SSL if requested + shell: | + certbot --nginx -d {{ tracker_domain }} --non-interactive --agree-tos -m {{ tracker_email }} + args: + creates: /etc/letsencrypt/live/{{ tracker_domain }}/fullchain.pem + when: setup_integrated_tracker | default(false) | bool and tracker_setup_ssl | default(true) | bool + ignore_errors: yes + +- name: Start and enable tracker service + systemd: + name: tracker + state: started + enabled: yes + daemon_reload: yes + when: setup_integrated_tracker | default(false) | bool + +- name: Restart NGINX to apply tracker configuration + systemd: + name: nginx + state: restarted + when: setup_integrated_tracker | default(false) | bool + +- name: Add tracker alias to bashrc for easy access + lineinfile: + path: /root/.bashrc + line: 'alias tracker="/opt/tracker/tracker-stats.sh"' + state: present + when: setup_integrated_tracker | default(false) | bool \ No newline at end of file diff --git a/tasks/configure_redirector.yml b/tasks/configure_redirector.yml index 999d3ab..2b753d1 100644 --- a/tasks/configure_redirector.yml +++ b/tasks/configure_redirector.yml @@ -27,7 +27,7 @@ - name: Copy clean-logs.sh script copy: - src: "clean-logs.sh" + src: "../files/clean-logs.sh" dest: /opt/c2/clean-logs.sh mode: '0700' owner: root @@ -35,7 +35,7 @@ - name: Copy shell handler script copy: - src: "persistent-listener.sh" + src: "../files/persistent-listener.sh" dest: /opt/shell-handler/persistent-listener.sh mode: '0700' owner: root @@ -72,15 +72,25 @@ - name: Set subdomain value explicitly set_fact: - redirector_subdomain: "cdn" + redirector_subdomain: "{{ redirector_subdomain | default('cdn') }}" -- name: Configure NGINX for C2 redirection +- name: Configure NGINX for C2 redirection (standard) template: src: "../templates/redirector-site.conf.j2" dest: /etc/nginx/sites-available/default mode: '0644' owner: root group: root + when: not (setup_integrated_tracker | default(false) | bool) + +- name: Configure NGINX for C2 redirection with tracker support + template: + src: "../templates/redirector-site-with-tracker.conf.j2" + dest: /etc/nginx/sites-available/default + mode: '0644' + owner: root + group: root + when: setup_integrated_tracker | default(false) | bool - name: Create legitimate-looking index.html template: @@ -122,57 +132,6 @@ var: nginx_config_test.stderr_lines when: nginx_config_test.rc != 0 -- name: Ensure NGINX configurations are valid - block: - - name: Fix NGINX configuration if test failed - copy: - content: | - server { - listen 80; - listen [::]:80; - server_name cdn.{{ domain }}; - - # Root directory - root /var/www/html; - index index.html; - - # Primary location for legitimate traffic - location / { - try_files $uri $uri/ =404; - } - - # Special URI patterns for C2 traffic - location /ajax/ { - proxy_pass http://{{ c2_ip }}:8888; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - } - - # Static resources - location ~ ^/static/(css|js|images)/.*\.(css|js|png|jpg|jpeg|gif|ico)$ { - proxy_pass http://{{ c2_ip }}:8888; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - } - } - - # Catch-all server block - server { - listen 80 default_server; - listen [::]:80 default_server; - - # Redirect unknown traffic - return 301 https://www.google.com; - } - dest: /etc/nginx/sites-available/default - mode: '0644' - owner: root - group: root - when: nginx_config_test.rc != 0 - when: nginx_config_test.rc != 0 - - name: Restart NGINX systemd: name: nginx diff --git a/tasks/clean-logs.sh b/tasks/files/clean-logs.sh similarity index 100% rename from tasks/clean-logs.sh rename to tasks/files/clean-logs.sh diff --git a/tasks/persistent-listener.sh b/tasks/files/persistent-listener.sh similarity index 100% rename from tasks/persistent-listener.sh rename to tasks/files/persistent-listener.sh diff --git a/tasks/rubber-ducky.txt b/tasks/files/rubber-ducky.txt similarity index 100% rename from tasks/rubber-ducky.txt rename to tasks/files/rubber-ducky.txt diff --git a/tasks/secure-exit.sh b/tasks/files/secure-exit.sh similarity index 100% rename from tasks/secure-exit.sh rename to tasks/files/secure-exit.sh diff --git a/tasks/serve-beacons.sh b/tasks/files/serve-beacons.sh similarity index 100% rename from tasks/serve-beacons.sh rename to tasks/files/serve-beacons.sh diff --git a/tasks/files/simple_email_tracker.py b/tasks/files/simple_email_tracker.py new file mode 100644 index 0000000..c372527 --- /dev/null +++ b/tasks/files/simple_email_tracker.py @@ -0,0 +1,214 @@ +#!/usr/bin/env python3 +""" +Simple Email Tracking Server + +A minimal Flask application that serves transparent tracking pixels +and logs email opens with metadata. +""" + +import os +import json +import time +import logging +from datetime import datetime +from flask import Flask, request, send_file, render_template_string + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler('/opt/tracker/data/tracker.log'), + logging.StreamHandler() + ] +) + +# Create Flask app +app = Flask(__name__) + +# Directory to store tracking data +DATA_DIR = '/opt/tracker/data' +os.makedirs(DATA_DIR, exist_ok=True) + +# Path to 1x1 transparent pixel +PIXEL_PATH = os.path.join(DATA_DIR, 'pixel.png') + +# Create 1x1 transparent PNG if it doesn't exist +if not os.path.exists(PIXEL_PATH): + from PIL import Image + img = Image.new('RGBA', (1, 1), color=(0, 0, 0, 0)) + img.save(PIXEL_PATH) + +# Path to tracking data +TRACKING_DATA_PATH = os.path.join(DATA_DIR, 'tracking_data.json') + +def load_tracking_data(): + """Load existing tracking data from JSON file""" + if os.path.exists(TRACKING_DATA_PATH): + try: + with open(TRACKING_DATA_PATH, 'r') as f: + return json.load(f) + except json.JSONDecodeError: + logging.error("Error loading tracking data, starting fresh") + return {} + +def save_tracking_data(data): + """Save tracking data to JSON file""" + with open(TRACKING_DATA_PATH, 'w') as f: + json.dump(data, f, indent=2) + +@app.route('/pixel/.png') +def tracking_pixel(tracking_id): + """Serve a tracking pixel and log the request""" + # Get client information + user_agent = request.headers.get('User-Agent', 'Unknown') + ip_address = request.remote_addr + timestamp = datetime.now().isoformat() + referer = request.headers.get('Referer', 'Unknown') + + # Log the tracking event + logging.info(f"Pixel loaded - ID: {tracking_id}, IP: {ip_address}") + + # Add tracking event to data + tracking_data = load_tracking_data() + + if tracking_id not in tracking_data: + tracking_data[tracking_id] = [] + + tracking_data[tracking_id].append({ + 'timestamp': timestamp, + 'ip_address': ip_address, + 'user_agent': user_agent, + 'referer': referer + }) + + save_tracking_data(tracking_data) + + # Return the 1x1 transparent pixel + return send_file(PIXEL_PATH, mimetype='image/png') + +@app.route('/') +def dashboard(): + """Display tracking statistics dashboard""" + tracking_data = load_tracking_data() + + # Prepare data for the dashboard + stats = [] + for tracking_id, events in tracking_data.items(): + stats.append({ + 'id': tracking_id, + 'views': len(events), + 'last_view': events[-1]['timestamp'] if events else 'Never', + 'unique_ips': len(set(e['ip_address'] for e in events)) + }) + + # Sort by most views + stats.sort(key=lambda x: x['views'], reverse=True) + + # Simple HTML dashboard template + template = """ + + + + Email Tracking Dashboard + + + +
+

Email Tracking Dashboard

+

To track email opens, add this HTML to your emails:

+
<img src="https://YOUR_DOMAIN/px/YOUR_TRACKING_ID.png" height="1" width="1" />
+ +

Tracking Statistics

+ + + + + + + + + {% for stat in stats %} + + + + + + + + {% endfor %} +
Tracking IDViewsUnique IPsLast ViewDetails
{{ stat.id }}{{ stat.views }}{{ stat.unique_ips }}{{ stat.last_view }}View Details
+
+ + + """ + + return render_template_string(template, stats=stats) + +@app.route('/details/') +def tracking_details(tracking_id): + """Display detailed tracking information for a specific ID""" + tracking_data = load_tracking_data() + + if tracking_id not in tracking_data: + return f"No data found for tracking ID: {tracking_id}", 404 + + events = tracking_data[tracking_id] + + # Simple HTML template for details + template = """ + + + + Tracking Details: {{ tracking_id }} + + + +
+ +

Tracking Details: {{ tracking_id }}

+

Total views: {{ events|length }}

+ +

Events

+ + + + + + + + {% for event in events %} + + + + + + + {% endfor %} +
TimeIP AddressUser AgentReferer
{{ event.timestamp }}{{ event.ip_address }}{{ event.user_agent }}{{ event.referer }}
+
+ + + """ + + return render_template_string(template, tracking_id=tracking_id, events=events) + +if __name__ == '__main__': + app.run(host='127.0.0.1', port=5000, debug=False) \ No newline at end of file diff --git a/tasks/files/tracker-nginx.conf b/tasks/files/tracker-nginx.conf new file mode 100644 index 0000000..f1645f7 --- /dev/null +++ b/tasks/files/tracker-nginx.conf @@ -0,0 +1,47 @@ +server { + listen 80; + listen [::]:80; + server_name {{ tracker_domain }}; + + # Redirect to HTTPS if SSL is enabled + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name {{ tracker_domain }}; + + # SSL Configuration + ssl_certificate /etc/letsencrypt/live/{{ tracker_domain }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{{ tracker_domain }}/privkey.pem; + + # Restrict dashboard to localhost only + location / { + allow 127.0.0.1; + deny all; + proxy_pass http://127.0.0.1:5000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + # Allow public access only to pixel endpoints + location ~ ^/pixel/(.+)\.png$ { + # Public access allowed + proxy_pass http://127.0.0.1:5000/pixel/$1.png; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + # Cache control - don't cache tracking pixels + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0"; + expires off; + } + + # Security headers + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "no-referrer" always; +} \ No newline at end of file diff --git a/tasks/files/tracker-stats.sh b/tasks/files/tracker-stats.sh new file mode 100644 index 0000000..eb57a35 --- /dev/null +++ b/tasks/files/tracker-stats.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# CLI tool to view email tracking stats + +DATA_FILE="/opt/tracker/data/tracking_data.json" + +function show_summary() { + if [ ! -f "$DATA_FILE" ]; then + echo "No tracking data found." + exit 1 + fi + + echo "Email Tracking Summary:" + echo "=======================" + jq -r 'to_entries | sort_by(.value | length) | reverse | .[] | "\(.key): \(.value | length) views"' $DATA_FILE +} + +function show_details() { + ID=$1 + if [ ! -f "$DATA_FILE" ]; then + echo "No tracking data found." + exit 1 + fi + + echo "Details for tracking ID: $ID" + echo "==========================" + jq -r --arg id "$ID" '.[$id] | if . then .[] | "\(.timestamp) | \(.ip_address) | \(.user_agent)" else "No data found for this ID" end' $DATA_FILE +} + +case "$1" in + "list") + show_summary + ;; + "details") + if [ -z "$2" ]; then + echo "Usage: $0 details TRACKING_ID" + exit 1 + fi + show_details "$2" + ;; + *) + echo "Usage: $0 [list|details TRACKING_ID]" + echo " list - Show summary of all tracking IDs" + echo " details ID - Show details for specific tracking ID" + ;; +esac \ No newline at end of file diff --git a/tasks/files/tracker.service b/tasks/files/tracker.service new file mode 100644 index 0000000..b70c22f --- /dev/null +++ b/tasks/files/tracker.service @@ -0,0 +1,19 @@ +[Unit] +Description=Email Tracking Server +After=network.target + +[Service] +User=tracker +Group=tracker +WorkingDirectory=/opt/tracker +ExecStart=/opt/tracker/venv/bin/python /opt/tracker/simple_email_tracker.py +Restart=always +RestartSec=10 + +# Security settings +PrivateTmp=true +ProtectSystem=full +NoNewPrivileges=true + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/templates/redirector-site-with-tracker.conf.j2 b/templates/redirector-site-with-tracker.conf.j2 new file mode 100644 index 0000000..66125bb --- /dev/null +++ b/templates/redirector-site-with-tracker.conf.j2 @@ -0,0 +1,104 @@ +server { + listen 80; + listen [::]:80; + server_name {{ redirector_subdomain }}.{{ domain }}; + + # Redirect to HTTPS + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name {{ redirector_subdomain }}.{{ domain }}; + + # SSL Configuration + ssl_certificate /etc/letsencrypt/live/{{ redirector_subdomain }}.{{ domain }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{{ redirector_subdomain }}.{{ domain }}/privkey.pem; + + # Root directory + root /var/www/html; + index index.html; + + # Primary location for legitimate website traffic + location / { + try_files $uri $uri/ =404; + } + + # Special URI patterns for C2 traffic + # These will redirect to the actual C2 server + + # Sliver HTTP C2 channel + location /ajax/ { + proxy_pass http://{{ c2_ip }}:8888; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + # Static resources that actually redirect to C2 + location ~ ^/static/(css|js|images)/.*\.(css|js|png|jpg|jpeg|gif|ico)$ { + proxy_pass http://{{ c2_ip }}:8888; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; + } + + {% if setup_integrated_tracker | default(false) | bool %} + # Email tracker proxy settings + location /track/ { + proxy_pass http://{{ c2_ip }}:5000/; + proxy_http_version 1.1; + proxy_set_header Host {{ tracker_domain }}; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Proto https; + } + + # Tracking pixel specific location + location /px.png { + proxy_pass http://{{ c2_ip }}:5000/pixel.png; + proxy_http_version 1.1; + proxy_set_header Host {{ tracker_domain }}; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Proto https; + } + {% endif %} + + # Additional security headers + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "no-referrer" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;" always; + + # Disable logging for this server block if zero-logs is enabled + {% if zero_logs | default(true) | bool %} + access_log off; + error_log /dev/null crit; + {% endif %} +} + +# Catch-all server block to respond to unknown hosts +server { + listen 80 default_server; + listen [::]:80 default_server; + listen 443 ssl default_server; + listen [::]:443 ssl default_server; + + # Self-signed cert for catch-all + ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem; + ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key; + + # Redirect all unknown traffic to a legitimate-looking site + return 301 https://www.google.com; + + # Disable logs + access_log off; + error_log /dev/null crit; +} \ No newline at end of file diff --git a/templates/simple_email_tracker.py.j2 b/templates/simple_email_tracker.py.j2 new file mode 100644 index 0000000..de11cb5 --- /dev/null +++ b/templates/simple_email_tracker.py.j2 @@ -0,0 +1,221 @@ +#!/usr/bin/env python3 +""" +Simple Email Tracking Server + +A minimal Flask application that serves transparent tracking pixels +and logs email opens with metadata. +""" + +import os +import json +import time +import logging +from datetime import datetime +from flask import Flask, request, send_file, render_template_string + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler('/opt/tracker/data/tracker.log'), + logging.StreamHandler() + ] +) + +# Create Flask app +app = Flask(__name__) + +# Directory to store tracking data +DATA_DIR = '/opt/tracker/data' +os.makedirs(DATA_DIR, exist_ok=True) + +# Path to 1x1 transparent pixel +PIXEL_PATH = os.path.join(DATA_DIR, 'pixel.png') + +# Create 1x1 transparent PNG if it doesn't exist +if not os.path.exists(PIXEL_PATH): + from PIL import Image + img = Image.new('RGBA', (1, 1), color=(0, 0, 0, 0)) + img.save(PIXEL_PATH) + +# Path to tracking data +TRACKING_DATA_PATH = os.path.join(DATA_DIR, 'tracking_data.json') + +def load_tracking_data(): + """Load existing tracking data from JSON file""" + if os.path.exists(TRACKING_DATA_PATH): + try: + with open(TRACKING_DATA_PATH, 'r') as f: + return json.load(f) + except json.JSONDecodeError: + logging.error("Error loading tracking data, starting fresh") + return {} + +def save_tracking_data(data): + """Save tracking data to JSON file""" + with open(TRACKING_DATA_PATH, 'w') as f: + json.dump(data, f, indent=2) + +@app.route('/pixel/.png') +def tracking_pixel(tracking_id): + """Serve a tracking pixel and log the request""" + # Get client information + user_agent = request.headers.get('User-Agent', 'Unknown') + ip_address = request.remote_addr + timestamp = datetime.now().isoformat() + referer = request.headers.get('Referer', 'Unknown') + + # Log the tracking event + logging.info(f"Pixel loaded - ID: {tracking_id}, IP: {ip_address}") + + # Add tracking event to data + tracking_data = load_tracking_data() + + if tracking_id not in tracking_data: + tracking_data[tracking_id] = [] + + tracking_data[tracking_id].append({ + 'timestamp': timestamp, + 'ip_address': ip_address, + 'user_agent': user_agent, + 'referer': referer + }) + + save_tracking_data(tracking_data) + + # Return the 1x1 transparent pixel + return send_file(PIXEL_PATH, mimetype='image/png') + +@app.route('/') +def dashboard(): + """Display tracking statistics dashboard""" + tracking_data = load_tracking_data() + + # Prepare data for the dashboard + stats = [] + for tracking_id, events in tracking_data.items(): + stats.append({ + 'id': tracking_id, + 'views': len(events), + 'last_view': events[-1]['timestamp'] if events else 'Never', + 'unique_ips': len(set(e['ip_address'] for e in events)) + }) + + # Sort by most views + stats.sort(key=lambda x: x['views'], reverse=True) + + # Simple HTML dashboard template + template = """ + + + + Email Tracking Dashboard - {{ tracker_domain }} + + + +
+

Email Tracking Dashboard

+

To track email opens, add this HTML to your emails:

+
<img src="https://{{ redirector_domain }}/px/YOUR_TRACKING_ID.png" height="1" width="1" />
+

Alternatively, use this URL with your C2 server directly:

+
<img src="https://{{ tracker_domain }}/pixel/YOUR_TRACKING_ID.png" height="1" width="1" />
+ +

Tracking Statistics

+ + + + + + + + + {% for stat in stats %} + + + + + + + + {% endfor %} +
Tracking IDViewsUnique IPsLast ViewDetails
{{ stat.id }}{{ stat.views }}{{ stat.unique_ips }}{{ stat.last_view }}View Details
+
+ + + """ + + return render_template_string( + template, + stats=stats, + tracker_domain="{{ tracker_domain }}", + redirector_domain="{{ redirector_subdomain }}.{{ domain }}" + ) + +@app.route('/details/') +def tracking_details(tracking_id): + """Display detailed tracking information for a specific ID""" + tracking_data = load_tracking_data() + + if tracking_id not in tracking_data: + return f"No data found for tracking ID: {tracking_id}", 404 + + events = tracking_data[tracking_id] + + # Simple HTML template for details + template = """ + + + + Tracking Details: {{ tracking_id }} + + + +
+ +

Tracking Details: {{ tracking_id }}

+

Total views: {{ events|length }}

+ +

Events

+ + + + + + + + {% for event in events %} + + + + + + + {% endfor %} +
TimeIP AddressUser AgentReferer
{{ event.timestamp }}{{ event.ip_address }}{{ event.user_agent }}{{ event.referer }}
+
+ + + """ + + return render_template_string(template, tracking_id=tracking_id, events=events) + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=5000, debug=False) \ No newline at end of file diff --git a/templates/tracker-nginx.conf.j2 b/templates/tracker-nginx.conf.j2 new file mode 100644 index 0000000..589161f --- /dev/null +++ b/templates/tracker-nginx.conf.j2 @@ -0,0 +1,52 @@ +server { + listen 80; + listen [::]:80; + server_name {{ tracker_domain }}; + + # Redirect to HTTPS if SSL is enabled + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name {{ tracker_domain }}; + + # SSL Configuration + ssl_certificate /etc/letsencrypt/live/{{ tracker_domain }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{{ tracker_domain }}/privkey.pem; + + # Proxy to tracker app + location / { + proxy_pass http://127.0.0.1:5000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Specific location for tracking pixel + location ~ ^/pixel/(.+)\.png$ { + proxy_pass http://127.0.0.1:5000/pixel/$1.png; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Cache control - don't cache tracking pixels + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0"; + expires off; + } + + # Security headers + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "no-referrer" always; + + {% if zero_logs | default(true) %} + # Disable logging for privacy + access_log off; + error_log /dev/null crit; + {% endif %} +} \ No newline at end of file diff --git a/templates/tracker.service.j2 b/templates/tracker.service.j2 index 8053667..b257a00 100644 --- a/templates/tracker.service.j2 +++ b/templates/tracker.service.j2 @@ -1,20 +1,23 @@ -# templates/tracker.service.j2 [Unit] Description=Email Tracking Server After=network.target [Service] -User=root -Group=root +User=tracker +Group=tracker WorkingDirectory=/opt/tracker -ExecStart=/usr/bin/python3 /opt/tracker/app.py +ExecStart=/opt/tracker/venv/bin/python /opt/tracker/simple_email_tracker.py Restart=always RestartSec=10 -# Security measures +# Security settings PrivateTmp=true ProtectSystem=full NoNewPrivileges=true +# Hide process information +StandardOutput=null +StandardError=journal + [Install] WantedBy=multi-user.target \ No newline at end of file