Hunting Lazarus Part VIII: OtterCookie
OtterCookie is a separate JavaScript / Node.js RAT running beside BeaverTail in the Contagious Interview operation. Its Socket.IO control plane maintains a live roster of connected developer workstations and broadcasts it on a thirty-second clock. Part VIII breaks down the protocol, the collection profile, the uid/userKey batch labels, the npm and Vercel delivery layer, and the operational shift from stored-data theft to live surveillance of developer machines.
The full investigation is consolidated in the Inside the Machine research article.
The JavaScript RAT that turned developer compromise into live surveillance.
Every thirty seconds, the server spoke.
Not to us. Not intentionally.
It was broadcasting a roster: five live macOS machines, campaign numbers in the 900s, identifiers that looked like fingerprints until they did not. The server was not waiting for an operator to ask who was online. It was announcing who was online.
That was the first difference.
BeaverTail had stolen what developers had already saved.
OtterCookie was watching what they did next.
Executive Summary
- OtterCookie is a separate JavaScript / Node.js RAT, not a BeaverTail variant.
- Its command-and-control runs on Socket.IO over Engine.IO v4, not on the HTTP-and-upload pattern that defined BeaverTail.
- The C2 maintains a live roster of connected victims and pushes that roster outward on a clock, rather than waiting for inbound requests.
- Command dispatch appeared server-side from the victim-facing channel. Six thousand client-side event combinations against the live C2 produced zero command execution.
- Collection is not a one-time sweep. It runs continuously against an active developer workstation – clipboard, keystrokes, screen captures, browser secrets, wallet artifacts, and developer credentials.
- The identifier-shaped fields in the protocol –
uidanduserKey– do not identify machines. Distinct victims share the same values, which makes them campaign-batch identifiers rather than hardware fingerprints. - Campaign numbering establishes operational history. It does not establish that every missing number existed.
- npm and Vercel delivery extended OtterCookie's reach. Public reporting from Socket.dev and The Hacker News has documented roughly 197 malicious packages and on the order of 31,000 downloads in the late-2025 wave attributed to this family.
The cash-out infrastructure that OtterCookie data eventually fed is deferred to a later article.
Where Part VII Left Off
Part VII established that 195.201.104.53, the Hetzner host that earlier parts had described as the FTP exfiltration sink, was running six Express services on non-standard ports alongside the FileZilla server on port 21. Two of those Express services mapped to OtterCookie infrastructure. Port 6931 broadcast a victim roster every thirty seconds over Socket.IO. Port 6101 still accepted Socket.IO connections but no longer broadcast – the predecessor of port 6931, still listening on its own port after the campaign on it had moved on.
Part VII identified the C2, the cadence, the roster, the five live macOS victims under campaigns 902 through 906, and the existence of an earlier campaign generation tied to port 6101 with campaign numbers 23 and 77. Three other Express services on the same host – ports 6109, 6936, and 6939 – were left as unresolved operational surface.
This part asks the next question: what was the malware on the other end of port 6931 actually doing?
A note on what is already public. Part III already named the original OtterCookie infrastructure. Part VII added the Hetzner co-residence finding. This article does not re-run those IOC tables. It uses them as baseline and focuses on what the later corpus clarified.
Not BeaverTail
BeaverTail, OtterCookie, and InvisibleFerret are not three names for the same implant. They occupy different roles in the same operation, run on different protocols, and collect on different windows.
| Feature | BeaverTail | OtterCookie | InvisibleFerret |
|---|---|---|---|
| Role | Initial access and stored-data theft | Live surveillance RAT with infostealer payload | Backdoor and persistence layer |
| Runtime | JavaScript and variant ports | JavaScript / Node.js | Python and variant ports |
| Collection window | One-shot sweep of saved state | Continuous live-activity capture | Interactive operator access |
| Command-and-control | HTTP request / response, REST-shaped | Socket.IO over Engine.IO v4, persistent | Custom TCP and variant transports |
| Typical data taken | Browser stores, wallet files, environment files | Clipboard, keystrokes, screenshots, secrets | Shell sessions, file transfers, persistence |
BeaverTail wants what is already on the machine when it arrives. OtterCookie wants what shows up after it arrives. InvisibleFerret wants a way back in.
Some public reporting in late 2025 described an "OtterCookie v5" in which BeaverTail and OtterCookie capabilities appeared to merge inside a single sample. Whether a later sample bundles their capabilities into one binary is a packaging question. The operational distinction between stored-data theft and live-session surveillance is the analytic point, and the corpus underlying this article preserves it.
The Live Roster
Part VII described the port 6931 broadcast at the level of behavior: every thirty seconds, the server emitted a list of connected victims, and that list named five machines running macOS under campaigns 902 through 906. This part describes the mechanism.
OtterCookie's C2 does not only receive uploads. It maintains a live view of connected victims and periodically broadcasts that view to anyone connected to it. The mechanism is a Socket.IO list event. The server fires it on a clock. Every connected client receives the same payload. The payload contains, at minimum, a per-victim identifier and enough state for an operator-side console to render an active roster.
That is the difference between a C2 that operators visit and a C2 that narrates itself.
A C2 that operators visit answers requests. The operator connects, asks for a list, and the server replies. A C2 that narrates itself emits state on a clock regardless of whether anyone is asking. An investigation that connects once, holds the connection open, and stays quiet learns more from the second pattern than from the first. It learns who is checking in, how often, and how the roster changes between broadcasts. It learns this without needing to issue a single request.
That is how Part VII could describe a five-machine roster under campaigns 902 through 906 without naming any victim. The roster was being read aloud, on a clock, by the server itself.
Socket.IO as the Control Plane
Socket.IO gave the operators something BeaverTail did not: session state.
A victim did not have to appear as a fresh HTTP request every time. It connected, upgraded to a WebSocket, stayed present, and became part of a roster. Registration, heartbeat, list broadcast, and whatever operator-side command flow existed all rode the same persistent channel. The server held the state. The client did not have to rebuild it.
That persistence is what made the C2 observable. An investigation that opened a Socket.IO session against port 6931 and stayed quiet was already inside the channel that the victims used. It could read the roster the server was broadcasting and watch how it changed. It could not, however, issue commands. From the victim-facing channel, command dispatch appeared one-way. The server spoke. The client received. Six thousand event combinations later, the result was still the same: no command execution from that side of the protocol. Whatever path operators used to issue commands sat on a side the investigation did not see from the victim's seat.
The roster broadcast itself rode the same channel. Thirty seconds at the Hetzner observation window described in Part VII; up to roughly 120 seconds elsewhere in the corpus, on related nodes, following a service event. The cadence is a server-side configuration, not a property of the protocol. The protocol allows either. The operators changed it.
What OtterCookie Collected
OtterCookie's collection profile is the operational answer to the question Part VII left implicit. The live roster is interesting. What gets pulled off the roster is the point.
| Collection class | Behavior |
|---|---|
| Clipboard | Recurring monitoring of clipboard contents, with relevance to crypto-asset workflows where addresses and seed material transit the clipboard. |
| Keystrokes | System-wide key capture across analyzed samples – not limited to browser input. |
| Screenshots | Recurring screen capture of the active workspace, multi-monitor capable. |
| Browser data | Credential and cookie theft consistent with the broader Contagious Interview campaign. |
| Wallet data | Targeting of browser-extension wallet artifacts and adjacent material. |
| Developer secrets | .env files, SSH material, cloud credentials, source-control tokens, and adjacent on-disk secrets. |
The shape of that list is the shape of the change. BeaverTail's collection profile reads as a sweep of what the developer already has. OtterCookie's reads as a surveillance window into what the developer is doing now – which messages are open, which addresses are on the clipboard, which credentials are being typed, which screens are visible during which calls.
A stored-data sweep produces a victim archive. A live-surveillance window produces a victim feed.
The UID Mistake
Our first read was wrong.
The field looked like a machine identifier. It behaved like a batch label.
OtterCookie's protocol carries two identifier-shaped fields that look, on a quick read, like machine identifiers. One is a long hexadecimal string the protocol calls uid. The other is a shorter numeric field the corpus has tracked variously as ukey and userKey. Part III published a sample of each. The natural reading – the one this team's early analysis defaulted to – was that one or both of these identified the machine that registered it.
Across the observed infrastructure, both fields collide across distinct victims. userKey collisions are the wider pattern: single userKey values – 626, 818, 1014, 524, 324 – span groups of three to ten distinct victims each, on different operating systems, in different regions, under different login users. uid collisions appear in narrower form: in at least one observed cohort, two unrelated machines registering on the same C2 in the same window carried the same uid prefix.
A hardware fingerprint that collides across unrelated machines is not a fingerprint.
The cleaner read is that these fields identify deployment batches rather than the machines that received them. A single OtterCookie build, packaged with a particular configuration and dropped into a particular delivery wave, carries a single uid and registers under a single userKey. Every machine that receives that build registers under the same pair. The fields separate campaigns from each other, not victims from each other.
For the investigation, that means neither field is reliable as a victim count. A userKey with many connections is one delivery wave with many victims, not one busy machine. A uid with one connection is one delivery wave with one victim, or one wave whose other victims simply were not connected during the observation window.
For defenders, it means a uid or userKey value found on an analyzed sample is a campaign indicator, not a per-incident identifier. Two unrelated organizations finding the same value in OtterCookie samples are seeing the same malware build, not the same compromised host.
The fields look like fingerprints. They behave like batch labels.
Campaign Generations
Part VII published campaigns 23 and 77 on the silent predecessor port and 902 through 906 on the live one. Other waves of the investigation, on different but related infrastructure, surfaced 324, 524, 626, 818, and 1014. None of these sets are exhaustive.
The temptation is to read the gap between 77 and 902 as eight hundred unseen campaigns. The technical record does not support that claim, and this article does not make it.
What the record does support is that the numbering scheme has history. It is not a per-incident label discarded after use. It accumulates across campaigns. It was high enough by the observation window to imply an operational lifecycle considerably longer than any single snapshot shows.
The five live macOS victims on port 6931 are not a lifetime count for OtterCookie. They are who happened to be checking in during the window the investigation was watching, under numbers drawn from a counter that had been running long before.
Delivery: npm, Vercel, and the Supply-Chain Layer
The interview lure was hand-tailored. The npm pipe was industrial. Both fed the same RAT.
OtterCookie was not only a tool for victims who had been personally walked through a fake interview. It also sat behind package infrastructure that could reach developers who never spoke to a recruiter.
Part III published tetrismic.vercel.app as the Vercel staging domain used to host OtterCookie payloads. That domain is taken down. The pattern around it is not. The broader corpus, and a series of independent public reports through late 2025, document an OtterCookie-attributed wave of malicious npm packages staged behind Vercel-hosted payload delivery. Public reporting from Socket.dev and The Hacker News has documented roughly 197 malicious packages in this wave and on the order of 31,000 downloads. The figures belong to that reporting, not to this investigation.
A later article in this series will treat the delivery layer in its own right. The point here is narrower: to place OtterCookie on it. The same family that was checking in five live macOS developers on port 6931 was also being delivered through public package infrastructure to an order-of-magnitude larger population.
The Write-Only Sink Pattern
OtterCookie did not collapse control and exfiltration into the same service.
| Plane | Role | What it gave the operators |
|---|---|---|
| Control plane | Socket.IO roster and session state | Who is online, which victim can be watched, when |
| Data plane | Upload-style services | Where screenshots, keylogger output, and file dumps land |
| Delivery plane | npm packages, Vercel staging, interview repos | How the RAT reaches new machines |
The three planes did not have to share a host. The Part VII observation – live C2 and stored-data theft co-resident on the Hetzner server – was one configuration, not the architecture. Elsewhere in the corpus, the same operation kept the planes on separate hosts.
The decoupling is operational. The control plane keeps state. The data plane keeps nothing it has to keep. An operator monitoring a live roster does not need to worry about whether the upload sink is full, rotated, or freshly provisioned, because the upload sink is a write-only target the control plane does not depend on.
The npoint Fork
The corpus contains one branch that looks close enough to OtterCookie to matter, and different enough to be dangerous to classify too quickly.
Internal notes labelled it npoint. It is a Node.js implant. It uses Socket.IO for command-and-control. It targets stored credential material on macOS and Windows. It reaches the same operator infrastructure substrate from the same delivery pipeline. The sample analyzed in the corpus uses the same @primno/dpapi npm module for Windows credential decryption that OtterCookie samples use, and it targets the same macOS login keychain artifacts.
It diverges in the place that matters for taxonomy. Its Socket.IO event vocabulary is not OtterCookie's. The signature events – whour, w, list, hour – do not appear. In their place, the npoint sample uses a different vocabulary covering the same functional categories: a victim-registration event, a shell-command dispatch event, directory-listing and file-read primitives, scanned-file notifications during priority sweeps, and a family of upload state-tracking events. The transport is the same. The verbs are different.
npoint is a related developmental fork or parallel campaign variant, not the same family with a different label. Same toolchain, same targets, a different dialect over the same protocol. This article does not settle the taxonomy. The corpus does not settle the taxonomy. npoint exists, it overlaps, it diverges, and the relationship is open.
Detection Opportunities
The useful detections are behavioral, not just IOC-based.
| Signal | Defensive use |
|---|---|
| Engine.IO v4 / Socket.IO upgrade to a non-standard high-numbered port on an internet-facing host | Inspect for live roster behavior; correlate with Node.js / Express server fingerprint. |
| Node.js child processes running detached or hidden from the user-facing desktop | Hunt for OtterCookie-shape implant behavior on developer workstations. |
| Modules consistent with system-wide keyboard or screen capture loaded by a Node.js process | Flag for review; legitimate developer use is rare. |
| Recurring clipboard reads with no user-facing application context | Surface as candidate live-monitoring behavior. |
| macOS persistence pair from Part III: Login Items entry plus a per-user LaunchAgent referencing a Node process | Hunt on managed macOS endpoints. |
| Multi-port Express clusters co-resident with FTP services on the same host | Flag as candidate multi-campaign substrate behavior. |
npm postinstall hooks fetching payloads from Vercel-hosted staging domains | Inspect package install logs and package-lock graphs. |
Run these against your own infrastructure. The active-IP material – operator nodes, current sinks – stays in threat-intel sharing channels.
From Stored Theft to Live Surveillance
BeaverTail stole what the developer had already saved.
OtterCookie watched what the developer did next.
That is the operational distinction this part exists to make. The earlier malware in this operation was built around the developer's machine as a storage container – a place full of saved credentials, retained cookies, wallet files, .env secrets, and stored sessions. The later malware was built around the developer's machine as a workspace – a place where keystrokes are typed, addresses are pasted, screens are open, and decisions are made.
Both lanes ran in the same operation. They shared substrate at Hetzner. They shared delivery infrastructure through npm and Vercel. They shared a downstream pipeline whose details belong to the next article in this series, not to this one. What they did not share was a collection window. One looked backward at what the victim had. One looked forward at what the victim did.
The data collected by OtterCookie eventually reached a downstream aggregation layer that the next article will examine.
The investigation continues.
References
- Socket.dev. Research on the
stardev0914GitHub account and the late-2025 OtterCookie npm package wave. November 2025. - The Hacker News. Coverage of the OtterCookie-attributed npm campaign, source of the 197-package and approximately 31,000-download figures. November 2025.
- NTT Security. Analysis of an "OtterCookie v5" sample in which BeaverTail and OtterCookie capabilities appear to merge inside a single binary. November 2025.
Victim identities are withheld. Campaign numbers are operator-assigned, recovered from forensic artifacts.
Share this article
Help spread the word about security best practices.
Related Articles
A Fake Coding Interview Is an Execution Request: Developer Safety Checklist
A coding interview repo is a request to run unknown code on a machine that holds your browser sessions, SSH keys, GitHub tokens, and cloud credentials. This checklist covers what to check before the call, what to look for in the repo, and what to do if you already ran it.
Hunting Lazarus Part IX: The Google Mirror
Five trojanized browser extensions extracted Google profile identity through chrome.identity and routed it through an Aptos blockchain dead drop. Before any wallet artifact moved, the extension asked Chrome who owned the browser.
Need Security Help?
Our team can help secure your blockchain, web applications, and infrastructure.