Skip to content

Latest commit

 

History

6 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Malleable Redirector

Apache, Nginx, and Caddy redirector config generator for Cobalt Strike Malleable C2 profiles.

Malleable Redirector parses a Cobalt Strike Malleable C2 profile and emits a hardened redirector configuration for the web server of your choice (--format apache|nginx|caddy). It proxies only profile-matching beacon traffic to your team server. Everything else, including scanners, bots, and blue-team probes, is redirected to a convincing decoy.

It understands the profile DSL well enough to enforce exact header matching on beacon comms endpoints, not just User-Agent (UA) filtering.

Intended for authorised red team engagements only.

What's new: Malleable Redirector used to generate Apache .htaccess files only. It now targets Apache, Nginx, and Caddy from the same profile via --format, adds a unified --domain flag that sets the actual site hostname (not just report text), and introduces --phantomblogger-decoy — a Caddy-only mode that serves a real static site for unmatched traffic instead of redirecting to an external decoy.

Features

  • Three output formats. Apache (.htaccess), Nginx, and Caddy from the same profile, same policy engine, same routing logic. Pick your web server with --format.

  • Precise header matching on beacon routes. Extracts every client {} header from the profile (Accept, Accept-Language, Content-Type, etc.) and enforces them as match conditions on the exact URIs where the beacon sends them. A scanner with the right UA still gets turned away if it doesn't send the right headers.

  • URI-specific routing policy. Generated rules do not treat every path the same. Profile-derived beacon endpoints are locked to the profile's user agents and client {} headers; operator-added extra paths can accept extra --allow-ua values without weakening beacon routes; and explicitly lax paths, such as health checks, can be proxied without required UA or header checks when you ask for that behavior.

  • Correct rule ordering. Profile proxy rules run before the probe path filter, so .php or .aspx URIs in your profile are proxied rather than blocked by the probe filter that comes after.

  • UA scope separation. Operator-supplied --allow-ua values (e.g. a PowerShell stager UA) only apply to extra operator URIs, not to beacon comms endpoints. Your C2 traffic gate is not widened by your staging setup.

  • Multi-URI support. set uri "/path1 /path2"; is correctly split into separate routes.

  • Staging awareness. If your profile doesn't set host_stage "false", the tool warns loudly before generation. Automatic Cobalt Strike staging rules are not generated, by design.

  • PhantomBlogger catch-all (Caddy only). Instead of redirecting unmatched-but-legitimate traffic to an external decoy URL, --phantomblogger-decoy serves a real static site directly from the redirector, so a curious visitor lands on convincing content rather than bouncing to another domain.

How it works

Three-track URI model

Track Source UA matching Header matching
Track 1 — profile http-get / http-post URIs parsed from the profile Beacon UAs from profile only All client {} headers (strict policy)
Track 2 — extra --extra-uri operator flags for manually added staging or payload routes Beacon UAs + operator --allow-ua (strict policy) None
Track 3 — lax --lax-uri operator flags None None

Different traffic on your redirector carries different levels of certainty: you know exactly what your beacon will send, but you cannot always predict the shape of a staging or operator-added request. The three tracks translate that operational reality into distinct rule sets.

Policy modes

Policy Bad-UA blocklist Direct-IP guard Method guard UA check Profile header check
strict (default)
lax
none

strict is the intended operational mode. lax and none exist for testing or environments where access is already controlled at the network layer.

Rule order

Rules are emitted in this fixed order so each gate is evaluated before the next:

0  Forbid plain HTTP          (--forbid-http)
1  Global bad-UA blocklist    (strict + lax)
2a Direct-IP guard            (strict)
2b Method guard GET|POST      (strict)
3  Proxy routes               (all tracks, BEFORE probe filter)
4  Probe path filter          (strict, after proxy routes)
5  Catch-all → decoy

The probe path filter (section 4) intentionally comes after proxy routes (section 3), so a .php or .aspx URI in your malleable profile is proxied before the filter ever sees it.

Setup and usage

A sample Malleable C2 profile, chches_APT10.profile, is included for testing.

Requirements

  • Python 3.7+ — pure stdlib, no third-party dependencies
  • One of:
    • Apache 2.4 with modules: mod_rewrite, mod_proxy, mod_proxy_http, mod_ssl, mod_headers
    • Nginx with the map module (built in on virtually all distro packages)
    • Caddy v2

Installation

git clone https://github.com/InTheCyber/malleable-redirector
cd malleable-redirector

Generate the config

Run the tool against your profile, pointing it at your team server and a decoy, and choose your web server with --format:

python3 profile_to_htaccess.py current.profile \
    --backend  https://teamserver.internal:443 \
    --decoy    https://www.legit-looking-site.com/ \
    --policy   strict \
    --forbid-http \
    --format   apache \
    -o .htaccess
python3 profile_to_htaccess.py current.profile \
    --backend  https://teamserver.internal:443 \
    --decoy    https://www.legit-looking-site.com/ \
    --domain   redirector.contoso-infra.com \
    --format   nginx \
    -o redirector.conf
python3 profile_to_htaccess.py current.profile \
    --backend  https://teamserver.internal:443 \
    --decoy    https://www.legit-looking-site.com/ \
    --domain   redirector.contoso-infra.com \
    --format   caddy \
    -o Caddyfile

For a more complete operator setup with extra operator URIs and an allow-listed stager UA:

python3 profile_to_htaccess.py ops.profile \
    --backend   https://10.10.10.5:443 \
    --decoy     https://www.microsoft.com/ \
    --forbid-http \
    --policy    strict \
    --format    caddy \
    --extra-uri /cdn/update.cab \
    --extra-uri /telemetry/v2/push \
    --lax-uri   /health \
    --allow-ua  "PowerShell/5.1 (Windows NT 10.0; Win64; x64)" \
    --domain    redirector.contoso-infra.com \
    -o Caddyfile

The tool prints a full operator report and a ready-to-use setup snippet to stderr (an Apache VirtualHost, or reload/validate commands for Nginx/Caddy), leaving stdout free for config output when -o is not used.

PhantomBlogger catch-all (Caddy only)

By default, unmatched-but-legitimate traffic (a real visitor who isn't your beacon) is redirected to --decoy, an external site. If you'd rather this traffic land on a real page served by the redirector itself, pass --phantomblogger-decoy:

python3 profile_to_htaccess.py ops.profile \
    --backend   https://10.10.10.5:443 \
    --decoy     https://www.microsoft.com/ \
    --domain    redirector.contoso-infra.com \
    --phantomblogger-decoy \
    -o Caddyfile

--decoy still receives redirects from the security filters (bad-UA, bad-path, direct-IP, bad-method) — only the final catch-all is replaced with a static file server block. --phantomblogger-decoy forces --format caddy and expects the static site content at /opt/Blog-Template/dist on the redirector; adjust the path in the generated Caddyfile if yours differs.

Deploy on the redirector

Enable the required Apache modules:

sudo a2enmod rewrite proxy proxy_http ssl headers

Harden the server identity in /etc/apache2/conf-available/security.conf:

ServerTokens Prod
ServerSignature Off

Drop the generated .htaccess into your DocumentRoot and configure the VirtualHost (the tool prints a tailored snippet to stderr, but the general structure is):

<VirtualHost *:80>
    ServerName redirector.example.com
    <Location />
        Require all denied
    </Location>
</VirtualHost>

<VirtualHost *:443>
    ServerName redirector.example.com

    SSLEngine on
    SSLCertificateFile    /etc/letsencrypt/live/redirector.example.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/redirector.example.com/privkey.pem

    DocumentRoot /var/www/html
    <Directory /var/www/html>
        Options -Indexes +FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>

    # Required if team server uses a self-signed certificate
    SSLProxyEngine On
    SSLProxyVerify none
    SSLProxyCheckPeerName off
    SSLProxyCheckPeerCN off
    SSLProxyCheckPeerExpire off

    Header always unset X-Powered-By
    Header always unset Server
</VirtualHost>

Enable and reload:

sudo a2ensite redirector.conf
sudo apachectl configtest && sudo systemctl reload apache2

Nginx

The generated file has two sections: map blocks that must live in the http {} context, and a server {} block. Split them accordingly, e.g.:

# map blocks → /etc/nginx/conf.d/malleable-maps.conf
# server block → /etc/nginx/sites-available/redirector.conf
sudo ln -s /etc/nginx/sites-available/redirector.conf /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

Caddy

Drop the generated file in as your Caddyfile (or a site block in /etc/caddy/conf.d/) and reload:

sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy

CLI reference

usage: profile_to_htaccess.py profile [options]
Flag Default Description
profile Path to the .profile file
--format apache apache / nginx / caddy
--backend URL https://teamserver.internal:443 Team server URL. No trailing slash.
--decoy URL https://www.example.com/ Non-matching traffic redirects here (302).
-o / --output PATH stdout Write the generated config to file.
--forbid-http off Reject plain HTTP (403 on Apache; commented-out block to adapt yourself on Nginx/Caddy).
--policy strict strict / lax / none
--extra-uri PATH Repeatable. Track 2 URI — required UA check, no profile header matching.
--lax-uri PATH Repeatable. Track 3 URI — no required UA or header checks.
--allow-ua STRING Repeatable. Exact UA for Track 2 only. Does not affect beacon comms routes.
--block-ua STRING Repeatable. Extra substring added to the bad-UA blocklist.
--domain NAME profile Host header Site hostname — Caddyfile site block, Nginx server_name, Apache ServerName in the report. (--server-name is a backward-compat alias.)
--phantomblogger-decoy off Caddy only. Serve a local static site as the catch-all instead of redirecting to --decoy. Forces --format caddy. See PhantomBlogger catch-all.
--document-root PATH /var/www/redirector Apache only. For the setup report.
--site-name NAME redirector Apache only. For the setup report.

Limitations

  • Beacon staging rules are not generated. If your profile has host_stage enabled (or unset), the tool warns before generating. Add set host_stage "false"; to suppress. Automatic staging from a redirector adds operational complexity with limited benefit.

  • PhantomBlogger's static site path is hardcoded to /opt/Blog-Template/dist. Edit the generated Caddyfile if your content lives elsewhere.

Credits

Thanks to the team behind cs2modrewrite for their inspiration.

About

Generate smart web server redirector configs from a Cobalt Strike malleable C2 profile. Precise beacon matching, clean separation between C2 comms and staging, and aggressive traffic filtering, all from a single command.

Resources

Stars

8 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages