Mehmet Ergene

Detecting Cobalt Strike HTTP(S) Beacons with a Simple Method

Cobalt Strike remains one of the most widely used tools in adversary toolkits, from red teams to ransomware affiliates and nation-state groups. In this post, I'll share a simple method for detecting Cobalt Strike HTTP(S) beacon traffic.

One of the most effective components of Cobalt Strike is its malleable C2 profiles, which allow operators to customize beacon behavior and network indicators to evade detection. These profiles enable control over HTTP headers, user agents, URIs, and more to mimick legitimate traffic and avoid traditional network defenses.

Although malleable C2 profiles allow operators to define multiple URIs for both the http-get and http-post blocks, the beacon communicates with the C2 server using only one GET URI and one POST URI. Although public profiles use different URIs for GET and POST, it seems like there is no strict requirement for them to differ. Therefore both methods can technically use the same URI.

This static URI selection becomes a chokepoint that can be leveraged to detect Cobalt Strike beacons across environments.

Hunting Cobalt Strike via URI Analysis

By focusing on web proxy logs and looking for internal hosts that communicate with low-prevalence domains or IPs using only one or two unique URIs, defenders can detect potential Cobalt Strike traffic. 

The KQL query below demonstrates this approach. It first identifies hosts communicating with rare domains using a maximum of two unique URIs, then extracts additional telemetry for enrichment and triage triage (You need to modify the table and field names according to your logs).
let whitelisted_hosts = dynamic(["add-hosts-after-initial-analysis"]);
let lookback = 1d;
let susp_hosts = 
OPNsense_CL
| where TimeGenerated > ago(lookback)
| where destination_hostname_s !in (whitelisted_hosts)
| extend RequestURI = tostring(parse_url(url_original_s).Path)
| where isnotempty(RequestURI)
| summarize dcount(RequestURI), prevalence = dcount(source_ip_s) by destination_hostname_s
| where dcount_RequestURI <= 2 and prevalence <= 4
;
susp_hosts
| join hint.strategy=shuffle kind=inner (
    OPNsense_CL
    | where TimeGenerated > ago(lookback)
    | extend RequestURI = tostring(parse_url(url_original_s).Path)
    | where isnotempty(RequestURI)
    | project TimeGenerated, source_ip_s, RequestURI, destination_hostname_s, http_request_method_s, http_request_bytes_d, http_response_bytes_d
) on destination_hostname_s
| summarize hint.shufflekey=source_ip_s count(), dcount(http_request_bytes_d), dcount(http_response_bytes_d), make_set(destination_hostname_s) by RequestURI, http_request_method_s, source_ip_s, bin(TimeGenerated, 1d)
// filter traffic where at least 300 connections were seen
| where count_ >= 300
| sort by TimeGenerated, count_ desc

Caveats

With Cobalt Strike version >= 4.10, it is possible to update the host and URI configuration. Also, external(user defined) c2 is not covered by this detection method. Considering the fact that cracked Cobalt Strike versions are mainly 4.5 and 4.9, the detection still provides great coverage. 

Conclusion

Cobalt Strike's reliance on a single URI for communication provides a reliable detection opportunity. By applying simple logic and URI-based analysis, defenders can effectively uncover beacon traffic.
Share