Detecting SharkLoader’s Cobalt Strike Beacons with Node.js and Network Telemetry

Detecting SharkLoader’s Cobalt Strike Beacons with Node.js and Network Telemetry

pr0h0
cybersecuritymalwarecobalt-strikenodejsnetwork-telemetry
AI Usage (76%)

The public reporting I could find is thin, but the signal still matters: a new SharkLoader campaign is being described as deploying Cobalt Strike in StrikeShark cyberattacks. That is not just “another payload” in a log line. It is usually the point where an intrusion turns into an operator-controlled session, and once that happens, network telemetry often tells you more than a lone hash or domain ever will.

My take is straightforward: if you are hunting this class of activity, start with cadence and destination behavior across DNS, proxy, and TLS logs. IOC chasing is too brittle on its own. Loader infrastructure changes fast, but beacon behavior tends to leak into logs in ways that are much harder to hide.

What the SharkLoader report actually says

What I can confirm from the source summary is limited:

  • The report was published on 2026-06-26.
  • It describes a new SharkLoader malware wave.
  • That wave is said to deploy Cobalt Strike.
  • The activity is framed as StrikeShark cyberattacks.

What I cannot confirm from the summary alone:

  • the initial delivery vector
  • the victim sector
  • the exact loader chain
  • the beacon configuration
  • whether the report includes host artifacts or only network-side indicators

That distinction matters. Security writing often stretches a short report into a full intrusion story. I would not do that here. The safer claim is narrower: if SharkLoader is being used to stage Cobalt Strike, defenders should expect short-lived infrastructure, repeated callbacks, and a network footprint that looks more like a scheduled control channel than normal application traffic.

Why Cobalt Strike beacons are a telemetry problem, not just a malware problem

Cobalt Strike beacons are annoying because they often look normal in isolation. A single HTTPS request can resemble any software that checks in, fetches config, or polls an API. The real clue is usually the shape of the traffic over time.

What you can confirm from network logs alone

From DNS, proxy, and TLS logs, you can usually confirm:

  • one host repeatedly reaches the same destination
  • the interval between connections is unusually regular
  • the destination is uncommon for that host group
  • the user-agent, SNI, or DNS pattern is odd for the environment
  • the traffic volume is low compared with interactive browsing

That is enough to raise a strong suspicion of beaconing. It is not enough to name the malware with certainty.

A useful way to think about it is this:

SignalWhat it suggestsConfidence
Repeated callback to one destinationScheduled control trafficMedium
Tight interval clusteringBeacon-like behaviorMedium to high
Rare UA or SNINon-browser client or custom configMedium
DNS-only resolution followed by quiet HTTPSStaged telemetry or short sessionMedium
Single IOC match onlyPossible artifact hit, weak by itselfLow

What still needs host or EDR validation

Network telemetry cannot tell you everything. You still need host validation for:

  • process lineage
  • parent/child process chains
  • in-memory injection
  • persistence
  • whether the traffic belongs to a browser extension, updater, or actual payload

If a host is beaconing to a suspicious destination every three minutes, that is a very good triage lead. It is not a final attribution. I would treat it as “likely malicious until disproven,” then validate on endpoint evidence before I wrote a high-confidence incident report.

Build a Node.js pipeline for beacon hunting

I like Node.js for this kind of pass because it is quick to stitch together and good enough for a first detector. You do not need a full SIEM rule language to find periodic callbacks in normalized logs. You need one event shape, a stable sort key, and a scoring function that is honest about what it knows.

Ingest DNS, proxy, and TLS logs into a single event shape

The first mistake is analyzing each log source separately. DNS sees name resolution. Proxy logs see URLs and user-agents. TLS logs see SNI and certificate metadata. Beacons often leak across all three.

I normalize everything into this shape:

{
  "ts": "2026-06-26T12:00:00.000Z",
  "host": "WS-143",
  "dest": "example.net",
  "port": 443,
  "proto": "tls",
  "ua": "",
  "source": "tls",
  "event": "connect"
}

A sane normalizer maps source-specific fields into host and dest before any hunting logic runs.

normalize-event.mjs
export function normalize(rec) {
const ts = Date.parse(rec.ts ?? rec.timestamp ?? rec.time);
const host =
  rec.host ??
  rec.client_host ??
  rec.src_host ??
  rec.src_ip ??
  "unknown";

const dest =
  rec.dest ??
  rec.domain ??
  rec.sni ??
  rec.host_header ??
  rec.query ??
  rec.dst_ip ??
  "unknown";

return {
  ts,
  host,
  dest,
  port: Number(rec.port ?? rec.dst_port ?? 0),
  proto: rec.proto ?? rec.log_type ?? "unknown",
  ua: rec.user_agent ?? rec.ua ?? "",
  source: rec.source ?? "normalized",
  event: rec.event ?? "connect",
};
}

Normalize timestamps, host IDs, and destination fields

Normalize before grouping, and be strict about time zones. If you mix local time strings, ISO timestamps, and epoch milliseconds in one bucket, the interval math becomes junk.

I also recommend normalizing host IDs early. In one environment, the same endpoint may appear as a hostname in one feed and as an IP in another. Pick a canonical host key and stick to it. If you need both, carry both fields forward, but group on one.

For destinations, prefer a stable network identity:

  • sni for TLS
  • domain for DNS
  • host_header for proxy
  • dst_ip only when you have no better option

If you group on IP alone, CDN-backed traffic will create noise. If you group on domain alone, you may miss shared IP infrastructure. In practice I store both and rank by the most specific destination string available.

Indicators that matter more than a single IOC

A single IOC is a breadcrumb. Beacon hunting needs behavior.

Repeated low-and-slow callbacks

Low-and-slow traffic is the classic problem. The host sends a small request, waits, then sends another small request. On its own, that is not suspicious. Plenty of software polls.

The difference is the combination of:

  • same host
  • same destination
  • long run of small sessions
  • minimal payload variance
  • no obvious user interaction

If a workstation reaches out every few minutes to the same domain all day while the user is idle, I start paying attention.

Regular intervals with jitter

Beacons often try to look human by adding jitter, but the shape still tends to cluster. You do not need perfect periodicity to make this useful. A median interval with low variance is enough to score.

I usually look for:

  • at least 5 to 6 callbacks
  • intervals in the tens of seconds to many minutes
  • a coefficient of variation that stays low
  • the same host-destination pair repeating over time

That is enough to separate a beacon candidate from a one-off API call.

Rare user-agent, TLS, and DNS patterns

The weak spots are often the boring fields:

  • an odd or empty user-agent
  • SNI that looks generated or mismatched
  • DNS names that do not fit the host’s normal profile
  • TLS connections with no obvious browser fingerprint
  • a destination that only one or two hosts ever use

None of those prove Cobalt Strike by themselves. Together they are enough to prioritize the host for endpoint review.

A practical Node.js detection pass

The detector below reads normalized NDJSON from stdin, groups by host + dest, and scores the pair for periodic behavior and odd client metadata.

beacon-hunt.mjs
#!/usr/bin/env node


const rl = readline.createInterface({
input: process.stdin,
crlfDelay: Infinity,
});

const groups = new Map();

function keyOf(e) {
return `${e.host}|${e.dest}`;
}

function push(group, e) {
group.events.push(e);
}

function mean(nums) {
return nums.reduce((a, b) => a + b, 0) / nums.length;
}

function stddev(nums) {
const m = mean(nums);
const v = mean(nums.map((n) => (n - m) ** 2));
return Math.sqrt(v);
}

function median(nums) {
const sorted = [...nums].sort((a, b) => a - b);
const mid = Math.floor(sorted.length / 2);
return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
}

function scoreGroup(group) {
const ts = group.events
  .map((e) => e.ts)
  .filter((n) => Number.isFinite(n))
  .sort((a, b) => a - b);

if (ts.length < 6) return null;

const deltas = [];
for (let i = 1; i < ts.length; i++) {
  const deltaSec = (ts[i] - ts[i - 1]) / 1000;
  if (deltaSec > 0) deltas.push(deltaSec);
}

if (deltas.length < 5) return null;

const med = median(deltas);
const cv = stddev(deltas) / Math.max(mean(deltas), 1);

let score = 0;
const reasons = [];

if (med >= 30 && med <= 1800) {
  score += 3;
  reasons.push("regular-intervals");
}
if (cv < 0.35) {
  score += 3;
  reasons.push("low-variance");
}
if (group.events.some((e) => !e.ua || e.ua.length < 8)) {
  score += 1;
  reasons.push("missing-or-short-user-agent");
}
if (group.events.some((e) => e.proto === "tls" && !e.ua)) {
  score += 1;
  reasons.push("tls-without-browser-style-ua");
}

if (score < 4) return null;

return {
  host: group.events[0].host,
  dest: group.events[0].dest,
  count: ts.length,
  medianIntervalSec: Math.round(med),
  cv: Number(cv.toFixed(2)),
  score,
  reasons,
};
}

rl.on("line", (line) => {
if (!line.trim()) return;
const rec = JSON.parse(line);
const e = {
  ts: Date.parse(rec.ts),
  host: rec.host ?? "unknown",
  dest: rec.dest ?? "unknown",
  ua: rec.ua ?? "",
  proto: rec.proto ?? "unknown",
};

const k = keyOf(e);
if (!groups.has(k)) groups.set(k, { events: [] });
push(groups.get(k), e);
});

rl.on("close", () => {
for (const group of groups.values()) {
  const finding = scoreGroup(group);
  if (finding) {
    process.stdout.write(JSON.stringify(finding) + "\n");
  }
}
});

Run it against a sanitized export like this:

cat unified-telemetry.ndjson | node beacon-hunt.mjs

On a benign test fixture, the useful output should look like this:

{"host":"WS-143","dest":"cdn-sync.example.net","count":11,"medianIntervalSec":180,"cv":0.14,"score":7,"reasons":["regular-intervals","low-variance","missing-or-short-user-agent"]}

That output is not a verdict. It is a triage queue.

Emit findings as JSON your SIEM can consume

Keep the output machine-readable. Your SIEM, SOAR, or a second-stage rule can enrich it with asset criticality, geolocation, ASN reputation, and historical baselines.

I would usually add these fields before shipping the finding:

  • asset_role
  • first_seen
  • last_seen
  • log_sources
  • baseline_rank
  • confidence

That makes the detector portable. It can run as a cron job, a Lambda, or a batch step that feeds your existing alerting stack.

What I would test first in a real environment

Known-beacon replay against sanitized logs

Before trusting the detector, replay a known-beacon pattern into sanitized logs. You do not need real malware traffic to prove the logic. You need enough structure to test cadence and grouping.

I would verify:

  1. the same host-destination pair gets grouped correctly
  2. interval math survives mixed log sources
  3. the detector emits a finding only when the cadence is stable
  4. a single burst of traffic does not trigger an alert

A good first pass is to generate synthetic callback times and confirm the score rises only when interval variance drops.

False-positive checks for browsers, updaters, and SaaS clients

The false positives usually come from software that already polls:

  • browser sync and extension traffic
  • OS update clients
  • endpoint management agents
  • SaaS desktop apps
  • backup tools

These usually have one or more saving graces:

  • known user-agent strings
  • repeated destinations tied to vendor domains
  • clear user interaction
  • higher session volume
  • documentation you can validate

I would whitelist only after comparing against a baseline. Blind allowlists age badly. If you must suppress noise, suppress by a combination of asset role, vendor domain, and known client behavior, not by destination alone.

Response and containment once a beacon is likely

Network blocks, host isolation, and credential resets

If the beacon looks real, I would move in this order:

  1. block the destination at the proxy or egress layer
  2. isolate the host from the network
  3. reset credentials used on or near that host
  4. revoke active sessions and tokens where possible
  5. look for lateral movement and additional callbacks

If the workstation was used for admin work, credential response matters immediately. A beacon on an unprivileged laptop is bad; a beacon on a privileged endpoint is a credential problem first and a malware problem second.

Preservation steps for later triage

Do not destroy evidence early.

Preserve:

  • the raw logs that triggered the alert
  • the surrounding 24 to 72 hours of DNS/proxy/TLS telemetry
  • process and memory data if endpoint tooling supports it
  • the host’s recent user sessions
  • firewall and egress logs
  • any proxy authentication records

If you wipe the host first, later attribution gets much harder. The fastest containment move is not always the best forensic move.

Limits, caveats, and what this post does not claim

What I confirmed:

  • the public summary links SharkLoader with Cobalt Strike in StrikeShark cyberattacks
  • periodic callback patterns are detectable from network telemetry
  • Node.js is enough to build a useful first-pass detector

What I did not confirm:

  • the exact SharkLoader delivery chain
  • the victim profile
  • whether the campaign used a unique Cobalt Strike profile
  • whether the public report included host artifacts

This post is intentionally about detection mechanics, not attribution theater. A network-only detector can tell you a host is behaving like a beacon. It cannot, by itself, prove which loader family launched it or whether a specific operator is responsible.

My practical conclusion is that defenders should prefer behavior-first hunting here. The malware name changes, the infrastructure rotates, and the cadence keeps leaking through telemetry.

Further reading and source links

Share this post

More posts

Comments