Detecting SharkLoader’s Cobalt Strike Beacons with Node.js and Network Telemetry
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:
| Signal | What it suggests | Confidence |
|---|---|---|
| Repeated callback to one destination | Scheduled control traffic | Medium |
| Tight interval clustering | Beacon-like behavior | Medium to high |
| Rare UA or SNI | Non-browser client or custom config | Medium |
| DNS-only resolution followed by quiet HTTPS | Staged telemetry or short session | Medium |
| Single IOC match only | Possible artifact hit, weak by itself | Low |
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.
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:
snifor TLSdomainfor DNShost_headerfor proxydst_iponly 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.
#!/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_rolefirst_seenlast_seenlog_sourcesbaseline_rankconfidence
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:
- the same host-destination pair gets grouped correctly
- interval math survives mixed log sources
- the detector emits a finding only when the cadence is stable
- 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:
- block the destination at the proxy or egress layer
- isolate the host from the network
- reset credentials used on or near that host
- revoke active sessions and tokens where possible
- 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.


