Skip to content

09. Redirector

BaddKharma edited this page Jul 19, 2026 · 52 revisions

🛡️ Redirector

The public face of the lab. Apache (with mod_proxy, mod_rewrite, and mod_ssl) serves a CloudEdge CDN decoy page when traffic fails ingress criteria. It proxies legitimate C2 traffic to Mythic, Sliver, or Adaptix based on URI prefix and a custom HTTP header.

At a glance
Hostname redirector
VPC Redirector VPC (10.60.0.0/16 default), peered with Teamserver VPC
Public surface Elastic IP, ports 80 + 443 only (port 22 closed publicly)
SSH access Internal only via Guacamole portal (Redirector (SSH) connection)
Apache modules proxy, proxy_http, rewrite, ssl, headers, deflate
redirect.rules Auto-downloaded at boot from redRules
Cert Let's Encrypt (Direct Access, manual via Certbot) or self-signed (Tunneled Access, auto)
GatewayPorts clientspecified (enables ssh -R *:port for Kali callback workflows)

Note

The redirector lives in its own VPC to simulate a real engagement architecture: an external VPS provider hosts the public-facing redirector, while corporate / private infrastructure hosts the C2 backends. redStack mirrors that with two AWS VPCs peered together. The redirector knows nothing about the C2 backends except their private IPs and the URI prefixes used to route to each.


Considerations

Why a redirector at all?

Three reasons C2 traffic should never hit the C2 server directly:

  1. Burn isolation: When the redirector's IP/domain is flagged or burned during an engagement, you destroy and rebuild only the redirector. The C2 server retains its callbacks and tasking history. With redStack, this is a terraform apply -replace="aws_instance.redirector" away (advanced; not the default flow). Without a redirector, a burn requires rebuilding everything.
  2. OPSEC: A redirector lets you blend C2 traffic into "normal" looking patterns: legitimate-looking URI prefixes (CDN/cloud paths), scanner blocking via redirect.rules, decoy responses for unauthorized requests. The C2 server itself doesn't have to look HTTP-fluent; the redirector handles the appearance.
  3. Multiple C2 backends behind one front door: Mythic, Sliver, and Adaptix all sit behind the same redirector with different URI prefixes. From the target's perspective, all callbacks go to a single domain. Operators can run multiple C2s in parallel for redundancy or compartmentalization.

Direct Access vs. Tunneled Access

The redirector behaves differently based on redirector_domain and enable_vpn_tunnel tfvars:

Direct Access (internet-accessible targets) Tunneled Access (OpenVPN tunnel targets)
redirector_domain Set to your domain Empty
enable_vpn_tunnel false true
enable_redirector_htaccess_filtering true false (irrelevant in isolated lab nets)
TLS Let's Encrypt (manual one-time Certbot) Self-signed with public IP SAN (auto)
C2 ingress Public Elastic IP, real internet targets Public Elastic IP for callbacks (tun0 only for internet-isolated targets)
redirect.rules Loaded and active Loaded but bypassed by OpenVPN tunnel traffic
Operator workflow DNS A record > Certbot > done .ovpn file > systemctl start vpn-tunnel

See Deployment Modes for the full decision matrix and OpenVPN Tunnel Environments for the Tunneled Access walkthrough.

Sizing tradeoffs

Default t3.small (2 vCPU, 2 GB RAM, 20 GB disk):

  • More than enough for Apache + the redirector workload. Apache is light: the per-request work is a header check, a URI rewrite, and a proxy forward.
  • Becomes load-sensitive only if you handle thousands of callbacks per minute, which is not realistic for a training lab.
  • If you enable OpenVPN routing (Tunneled Access), the OpenVPN client and WireGuard server add roughly 10% CPU; still fine on t3.small.

Disk: 20 GB. Apache logs grow steadily; rotate them or truncate -s 0 if disk fills during a long-running engagement.

Why three layers?

Each layer addresses a different threat:

  1. redirect.rules (Layer 1): blocks known scanners, AV vendor IPs, sandbox infrastructure, TOR exits. Returns 403 Forbidden. The implant never reaches the redirector's authentication / routing logic. This is for defenders' research probes, not for attackers.

  2. Header validation (Layer 2): the X-Request-ID header with the auto-generated token. Anyone without the token receives the CloudEdge decoy page. This is for casual scanners and curious passersby: anyone reaching the redirector via direct IP or wrong domain who doesn't have the token.

  3. URI prefix routing (Layer 3): only requests to /cdn/media/stream/, /cloud/storage/objects/, or /edge/cache/assets/ are proxied. Everything else gets the decoy. This is for routing, but it also functions as a third gate: a passing-through-headers attacker who doesn't know the URI prefixes still hits the decoy.

The three layers compound. To reach a C2 backend, an attacker would need a clean source IP (Layer 1), the header token (Layer 2), and the right URI prefix (Layer 3). Each layer is individually bypassable; together they raise the bar significantly.

URI strip vs. preserve

C2 Behavior Why
Mythic Prefix stripped before forward (/cdn/media/stream/update > /update) Mythic doesn't validate URIs against a configured list. Accepts any path.
Sliver Prefix stripped before forward (/cloud/storage/objects/index.php > /index.php) Sliver's HTTP listener uses pattern matching, not exact-match URIs. Stripped works.
Adaptix Full path preserved (/edge/cache/assets/update > /edge/cache/assets/update) Adaptix's BeaconHTTP listener validates URIs against its configured list. Strip would 404.

The Apache rewrite rules are different per backend to handle this. See redirector_setup.sh for the actual RewriteRule strings.


Configuration

What runs where

redirector (Debian 12) [t3.small, EBS gp3 20 GB]
├── /etc/apache2/                                   Apache main install
│   ├── apache2.conf                                Main config
│   ├── sites-available/redirector-http.conf        VirtualHost :80 (HTTP, redirects to HTTPS)
│   ├── sites-available/redirector-https.conf      VirtualHost :443 (the actual C2 routing)
│   ├── redirect.rules                              Scanner blocklist (downloaded at boot)
│   └── ssl/                                        Cert files (Let's Encrypt or self-signed)
├── /var/log/apache2/                               Per-VirtualHost access + error logs
│   ├── redirector-access.log                       :80 access log
│   ├── redirector-error.log                        :80 error log
│   ├── redirector-ssl-access.log                   :443 access log
│   └── redirector-ssl-error.log                    :443 error log
├── /home/admin/test_redirector.sh                  Pre-installed end-to-end test script
└── /etc/ssh/sshd_config                            GatewayPorts clientspecified for ssh -R workflows

Apache module loadout

The redStack setup script enables only what's needed:

Module Why
proxy + proxy_http Forward C2 traffic to backend C2 servers
rewrite URI prefix matching, redirect.rules pattern engine
ssl HTTPS termination on :443
headers Inspect/modify the X-Request-ID header
deflate gzip compression of decoy page (smaller scanner response, less bandwidth)
apache2ctl -M | grep -E 'proxy|rewrite|ssl|headers|deflate'

redirect.rules

The file /etc/apache2/redirect.rules is downloaded at boot from github.com/BaddKharma/redRules, an adapted version of curi0usJack's redirect rules.

Adaptations:

  • All 302 redirects replaced with 403 Forbidden responses (no redirect target needed; all matches are dropped)
  • Setup directives stripped (Define REDIR_TARGET, RewriteEngine On, RewriteOptions Inherit) so the rules can be Include'd cleanly
  • AWS / Azure / GCP cloud IP blocks commented out by default (would block our own AWS-hosted callbacks)
  • Included in both HTTP and HTTPS VirtualHosts

To update manually after deploy:

curl -sL "https://raw.githubusercontent.com/BaddKharma/redRules/main/redirect.rules" \
  -o /etc/apache2/redirect.rules
sudo apache2ctl configtest && sudo systemctl reload apache2

To re-enable cloud IP blocks (for non-cloud-hosted deployments):

sudo nano /etc/apache2/redirect.rules
# Uncomment the "Class A Exclusions", "AWS Fine Grained", "Azure", "Other VT hosts" sections
sudo systemctl reload apache2

Header validation

Configured in redirector-https.conf (and -http.conf). The relevant snippet:

RewriteCond %{HTTP:X-Request-ID} !=<token>
RewriteRule ^/(cdn/media/stream|cloud/storage/objects|edge/cache/assets)/.*$ /index.html [L]

If the header is missing or wrong, the request is rewritten to /index.html (the CloudEdge decoy page) and short-circuits before any proxy.

To rotate the token:

  1. Edit terraform.tfvars: set c2_header_value = "<new-token>" (or leave empty to auto-generate on next apply).
  2. terraform apply. Cloud-init re-renders the redirector config with the new token.
  3. Re-generate any agents that bake the old token.

URI prefix routing

The three configured prefixes:

# Mythic - strip prefix before forward
RewriteRule ^/cdn/media/stream/(.*)$ https://<MYTHIC_PRIVATE_IP>/$1 [P,L]

# Sliver - strip prefix before forward
RewriteRule ^/cloud/storage/objects/(.*)$ https://<SLIVER_PRIVATE_IP>/$1 [P,L]

# Adaptix - preserve full path
RewriteRule ^/edge/cache/assets/(.*)$ https://<ADAPTIX_PRIVATE_IP>/edge/cache/assets/$1 [P,L]

To customize prefixes: edit mythic_uri_prefix, sliver_uri_prefix, adaptix_uri_prefix in tfvars. Re-apply Terraform. Note: prefixes are also baked into agent payloads at generation time, so existing agents stop working after a prefix change. Re-generate agents.

Decoy page

/var/www/html/index.html is the CloudEdge CDN maintenance page served when:

  • Header is missing/wrong
  • URI doesn't match any configured prefix

To customize, edit /var/www/html/index.html directly. It's a static HTML file. Suggested content: something innocuous and plausible for your domain (CDN maintenance, parking page, "this site doesn't exist", etc.).

TLS certificates

Direct Access (Let's Encrypt):

sudo certbot --apache -d yourdomain.tld

Certbot updates redirector-https.conf with the cert paths and configures auto-renewal via systemd timers. To verify renewal config:

sudo systemctl list-timers | grep certbot
sudo certbot renew --dry-run

Tunneled Access (self-signed):

Generated automatically at deploy time. Cert is at /etc/apache2/ssl/redirector.crt with the redirector's public IP as the Subject Alternative Name. Agents can connect via https://<REDIR_PUBLIC_IP>/... after disabling cert verification (or installing the cert as trusted on the agent host).

To regenerate (e.g. if the EIP changed after a redeploy):

sudo openssl req -x509 -newkey rsa:4096 \
  -keyout /etc/apache2/ssl/redirector.key \
  -out /etc/apache2/ssl/redirector.crt \
  -days 365 -nodes \
  -subj "/CN=redirector" \
  -addext "subjectAltName=IP:$(curl -s ifconfig.me)"
sudo systemctl reload apache2

Logs

# HTTP (port 80, mostly redirects to HTTPS)
sudo tail -f /var/log/apache2/redirector-access.log
sudo tail -f /var/log/apache2/redirector-error.log

# HTTPS (port 443, the actual C2 traffic)
sudo tail -f /var/log/apache2/redirector-ssl-access.log
sudo tail -f /var/log/apache2/redirector-ssl-error.log

What different log lines mean:

HTTP code What happened
200 + small body Decoy page served (header check or URI prefix failed)
200 + Mythic/Sliver/Adaptix-shaped response Successful proxy to backend; backend returned 200
404 Proxied to backend; backend returned 404 (e.g., listener not running yet, or wrong path)
403 redirect.rules matched (scanner / AV / TOR / commented-out cloud IP)
502 Proxy attempted but backend unreachable (VPC peering broken, backend down)
503 Apache itself overloaded

SSH GatewayPorts (Kali callback workflows)

Configured in /etc/ssh/sshd_config:

GatewayPorts clientspecified

This enables ssh -R '*:port:host:port' admin@redirector workflows where Kali publishes a listener on the redirector's tun0 interface for receiving Meterpreter / msf callbacks from external CTF targets. See Kali > Port Forwarding for Callbacks for the full pattern.

clientspecified means non-localhost binds are opt-in per command (must use *:port or <bind>:port in the -R flag). Without it, all ssh -R listeners would silently bind to 127.0.0.1 only.

What happens during cloud-init

  1. apt update + install (apache2, certbot, mod modules)
  2. Enable mod_proxy / mod_rewrite / mod_ssl / mod_headers / mod_deflate
  3. Render redirector-http.conf and redirector-https.conf from templatefile() with backend private IPs and URI prefixes
  4. Download redirect.rules from redRules
  5. If Tunneled Access: generate self-signed cert with public IP SAN
  6. If enable_vpn_tunnel = true: install OpenVPN client + WireGuard server, generate WG keypair, push WG client config to Guacamole via SSH
  7. Start Apache, run apache2ctl configtest
  8. Drop test_redirector.sh into /home/admin/

Total: ~2-3 minutes after instance boot.


Note

Tunneled Access: skip Step 1. Self-signed cert was auto-generated at deploy time. Proceed directly to Step 2.

Initial Use

The walkthrough to confirm the redirector is wired correctly. Assumes Verify is complete.

Step 1: Obtain SSL Certificate (Direct Access Only)

DNS A record must point at the redirector EIP first. Verify with:

dig +short yourdomain.tld
# Should match the Redirector EIP in deployment_info.txt

SSH to the redirector via Guacamole > Redirector (SSH), then:

sudo certbot --apache -d yourdomain.tld

Certbot prompts for email, ToS, EFF mailing list. After acceptance, the cert is issued, redirector-https.conf is updated, and auto-renewal is configured. ~30 seconds total.

✅ Checkpoint: SSL cert issued, HTTPS active.

Step 2: Run the Connectivity Test

sudo /home/admin/test_redirector.sh

Expected output sections:

[*] Apache status:
● apache2.service - active (running) ...

[*] Active VirtualHosts:
*:80    yourdomain.tld
*:443   yourdomain.tld

[*] Testing direct backend connectivity:
  Mythic: OK
  Sliver: FAILED        <- expected (no listener yet, see Sliver page)
  Adaptix: FAILED        <- expected (no listener yet, see Adaptix page)

[*] Testing decoy page (no header):
<!DOCTYPE html>
<html lang="en">...

[*] Testing C2 routing WITH correct header:
< HTTP/1.1 404 Not Found  <- expected (no listener; header check passed)

[*] Testing C2 routing WITHOUT header:
< HTTP/1.1 200 OK         <- expected (decoy served)

CloudEdge CDN decoy page served to unauthorized visitors
[Figure 9.3.1: CloudEdge CDN decoy page served to any visitor without a valid X-Request-ID header]

✅ Checkpoint: Apache running, modules loaded, VirtualHosts active, decoy + header validation working.

Step 3: Review the Three Security Layers

Skim the actual configs to know what you're working with:

sudo cat /etc/apache2/sites-available/redirector-https.conf

Look for:

  • The three RewriteRule blocks (one per backend)
  • The RewriteCond %{HTTP:X-Request-ID} line (header check)
  • The Include /etc/apache2/redirect.rules line (Layer 1)

Optional: count active redirect.rules entries:

grep -c 'RewriteCond' /etc/apache2/redirect.rules

Should return 200+ (a healthy ruleset).

✅ Checkpoint: You understand what's in the config and where to look when debugging.

Step 4: Test Each Security Layer Manually

From your laptop (or any internet-reachable host), verify each layer:

# Helper: store these for the rest of the session
UA="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
TOKEN="<from deployment_info.txt>"
HOST="yourdomain.tld"   # or <REDIR_PUBLIC_IP> in Tunneled Access

# Layer 3 test: no header, no prefix - should return decoy
curl -sk -A "$UA" https://$HOST/ | head -3
# Expected: <!DOCTYPE html>... (CloudEdge decoy)

# Layer 2 test: wrong header, valid prefix - should return decoy
curl -sk -A "$UA" -H "X-Request-ID: wrong" https://$HOST/cdn/media/stream/test | head -3
# Expected: decoy

# Layer 2 + 3 success: correct header, valid prefix - should reach Mythic backend
curl -sk -A "$UA" -H "X-Request-ID: $TOKEN" https://$HOST/cdn/media/stream/test
# Expected: Mythic response (404 if no listener, 200 if listener has a callback registered for /test)

# Layer 1 test: scanner User-Agent - should get 403
curl -sk -A "Nmap NSE" https://$HOST/
# Expected: 403 Forbidden (redirect.rules matched on UA)

✅ Checkpoint: Each layer behaves as documented when probed independently.

Step 5: Tail the Logs During a Real Callback

Once you complete a C2 walkthrough (Mythic / Sliver / Adaptix) and an agent is calling back, watch the redirector ssl-access log to see live C2 traffic:

sudo tail -f /var/log/apache2/redirector-ssl-access.log

You'll see regular GET/POST requests at the agent's callback_interval. The URI prefix tells you which C2 the traffic is destined for. The 200 response codes tell you the backend accepted and responded.

✅ Checkpoint: Live C2 traffic visible in logs, correctly prefixed and authenticated.

Important

Sliver and Adaptix show FAILED initially because their listeners aren't started yet. Mythic shows OK because its HTTP port is reachable (the Docker stack is up; Mythic's HTTP profile is running). Re-run this script after completing Sliver and Adaptix walkthroughs to see all three show OK.


Troubleshooting

Apache fails to start after boot

sudo systemctl status apache2
sudo journalctl -u apache2 -n 50
sudo apache2ctl configtest

Common causes:

Invalid command '404:' or similar in apache2ctl configtest:

redirect.rules failed to download or downloaded an error response (HTML 404 instead of the rules file). Verify:

head -3 /etc/apache2/redirect.rules

If it looks like HTML, re-download:

curl -sL "https://raw.githubusercontent.com/BaddKharma/redRules/main/redirect.rules" \
  -o /etc/apache2/redirect.rules
sudo apache2ctl configtest && sudo systemctl restart apache2

Cert files missing:

SSLCertificateFile: file '/etc/letsencrypt/live/...' does not exist

Cert wasn't issued (Direct Access) or wasn't generated (Tunneled Access). For Direct Access, run Certbot. For Tunneled Access, regenerate the self-signed cert (see Configuration section).

Connectivity test reports OK for backends but agents won't call back

The connectivity test does a TCP-level check on backend ports. It doesn't validate the URI prefix or header logic. Agents failing despite an OK connectivity test usually means:

  • Header value mismatch: agent baked an old token, redirector expects a new one
  • URI prefix mismatch: agent uses /cdn/media/stream/ but redirector configured for /cdn/v2/... (after a tfvar change)
  • Backend listener isn't running (Sliver jobs, Adaptix Listeners view, Mythic Installed Services > C2)

redirect.rules blocking legitimate callbacks

If your test target's source IP matches a rule (commonly: source is in a flagged cloud range), the request gets 403 before reaching the backend. Check the error log:

sudo grep '<target-source-ip>' /var/log/apache2/redirector-ssl-error.log

If you see RewriteCond pattern matches, identify which rule and either:

  1. Comment out that specific rule in /etc/apache2/redirect.rules
  2. Or set enable_redirector_htaccess_filtering = false in tfvars (disables ALL redirect.rules, including legitimate scanner blocking; use sparingly)

Decoy page not served when header is wrong

Means the rewrite rule isn't matching. Check the order of rules in redirector-https.conf:

sudo cat /etc/apache2/sites-available/redirector-https.conf | grep -A2 RewriteCond

The header check RewriteCond should come BEFORE the URI prefix RewriteRules. If the order is wrong, requests with the right URI but wrong header bypass the decoy.

Cert renewal failing

sudo certbot renew --dry-run

Common cause: domain no longer points at the redirector EIP (you redeployed and the EIP changed). Update DNS and retry. Let's Encrypt rate-limits to five duplicate certs per week per domain; if you redeploy more than that, switch to staging certs during testing or use Tunneled Access self-signed.

ssh -R from Kali fails to bind

GatewayPorts clientspecified requires the explicit bind syntax: ssh -R '*:4444:localhost:4444' or ssh -R 'tun0:4444:localhost:4444'. Plain ssh -R 4444:localhost:4444 silently binds localhost only.

Verify the sshd config:

sudo grep -i gatewayports /etc/ssh/sshd_config

Should show GatewayPorts clientspecified. If missing or wrong, the redirector's cloud-init didn't apply the setting. Set it explicitly:

sudo sed -i 's/^#*\s*GatewayPorts.*/GatewayPorts clientspecified/' /etc/ssh/sshd_config
sudo systemctl reload sshd

See Kali for the full callback workflow.

Apache logs are growing too fast

Long-running deploys without log rotation can fill disk. Force rotation:

sudo logrotate -f /etc/logrotate.d/apache2

Or truncate (loses history):

sudo truncate -s 0 /var/log/apache2/redirector-*.log

Or configure a smaller rotation cadence in /etc/logrotate.d/apache2 (default is daily, keep 14 days).

Backend reachability fails after VPC peering changes

If you tweaked VPC CIDRs or peering and the connectivity test now shows FAILED for previously-working backends:

# From the redirector
ping mythic
ping sliver
ping adaptix

If pings fail, VPC peering is broken or route tables aren't configured. Verify with ip route (should show the teamserver VPC CIDR routed via the peering connection). Re-applying Terraform usually fixes this; if not, manually verify in AWS Console > VPC > Peering Connections > Routes.


← Previous: Guacamole | Next: Mythic →


"OPSEC is everybody's responsibility."

Raphael Mudge, Cobalt Strike (~2014)

Clone this wiki locally