Repository navigation
09. 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.
Three reasons C2 traffic should never hit the C2 server directly:
-
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. -
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. - 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.
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.
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.
Each layer addresses a different threat:
-
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.
-
Header validation (Layer 2): the
X-Request-IDheader 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. -
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.
| 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.
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
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'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
302redirects replaced with403 Forbiddenresponses (no redirect target needed; all matches are dropped) - Setup directives stripped (
Define REDIR_TARGET,RewriteEngine On,RewriteOptions Inherit) so the rules can beInclude'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 apache2To 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 apache2Configured 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:
- Edit
terraform.tfvars: setc2_header_value = "<new-token>"(or leave empty to auto-generate on next apply). -
terraform apply. Cloud-init re-renders the redirector config with the new token. - Re-generate any agents that bake the old token.
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.
/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.).
Direct Access (Let's Encrypt):
sudo certbot --apache -d yourdomain.tldCertbot 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-runTunneled 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# 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.logWhat 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 |
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.
- apt update + install (apache2, certbot, mod modules)
- Enable mod_proxy / mod_rewrite / mod_ssl / mod_headers / mod_deflate
- Render
redirector-http.confandredirector-https.conffrom templatefile() with backend private IPs and URI prefixes - Download
redirect.rulesfrom redRules - If Tunneled Access: generate self-signed cert with public IP SAN
- If
enable_vpn_tunnel = true: install OpenVPN client + WireGuard server, generate WG keypair, push WG client config to Guacamole via SSH - Start Apache, run
apache2ctl configtest - Drop
test_redirector.shinto/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.
The walkthrough to confirm the redirector is wired correctly. Assumes Verify is complete.
DNS A record must point at the redirector EIP first. Verify with:
dig +short yourdomain.tld
# Should match the Redirector EIP in deployment_info.txtSSH to the redirector via Guacamole > Redirector (SSH), then:
sudo certbot --apache -d yourdomain.tldCertbot 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.
sudo /home/admin/test_redirector.shExpected 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)

[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.
Skim the actual configs to know what you're working with:
sudo cat /etc/apache2/sites-available/redirector-https.confLook for:
- The three
RewriteRuleblocks (one per backend) - The
RewriteCond %{HTTP:X-Request-ID}line (header check) - The
Include /etc/apache2/redirect.rulesline (Layer 1)
Optional: count active redirect.rules entries:
grep -c 'RewriteCond' /etc/apache2/redirect.rulesShould return 200+ (a healthy ruleset).
✅ Checkpoint: You understand what's in the config and where to look when debugging.
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.
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.logYou'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.
sudo systemctl status apache2
sudo journalctl -u apache2 -n 50
sudo apache2ctl configtestCommon 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.rulesIf 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 apache2Cert 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).
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)
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.logIf you see RewriteCond pattern matches, identify which rule and either:
- Comment out that specific rule in
/etc/apache2/redirect.rules - Or set
enable_redirector_htaccess_filtering = falsein tfvars (disables ALL redirect.rules, including legitimate scanner blocking; use sparingly)
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 RewriteCondThe 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.
sudo certbot renew --dry-runCommon 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.
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_configShould 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 sshdSee Kali for the full callback workflow.
Long-running deploys without log rotation can fill disk. Force rotation:
sudo logrotate -f /etc/logrotate.d/apache2Or truncate (loses history):
sudo truncate -s 0 /var/log/apache2/redirector-*.logOr configure a smaller rotation cadence in /etc/logrotate.d/apache2 (default is daily, keep 14 days).
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 adaptixIf 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)