Skip to content
OpenSourceMalware

OpenSourceMalware

opensourcemalware.com September 18, 2026

The OpenSourceMalware team spends a lot of time looking at malicious npm packages, and after a while you start seeing the same patterns over and over. So when something new and genuinely different shows up, it’s worth stopping for. That happened this week when we found 11 npm packages secretly hiding a brand new JavaScript stealer. It looks and feels like the DPRK’s BeaverTail and OtterCookie, but it isn’t quite either one. It’s smaller, lighter, and stripped down, with many of the heavier functions removed entirely.

We're calling it WeaselBiscuit . It isn’t an established family name. I made it up, and I think it’s dope.

Our automation flagged these packages as malicious and grouped them together because they IOCs. That’s when we recognized an emerging Node.js infostealer architecture worth investigating, one that reads as a possible new or lightly documented DPRK-linked strain, or as a simplified branch or fork of the BeaverTail/OtterCookie ecosystem. You can see all the threat reports under the #weaselbiscuit tag.

@biz44/process-runtime-utils

@vibecheck-polid/process-runtime-utils

2026-09-16T03:44:28Z (version 1.3.99 still live on NPM)

2026-09-16T13:51:46Z (version 1.3.10 still available on NPM)

We're tentatively attributing this malware to DPRK but it's important to be clear that it's an analytic hypothesis, not a confirmed attribution. The recovered code has meaningful overlap with DPRK-associated Contagious Interview tooling, but we haven’t recovered operator infrastructure, victimology, campaign metadata, signing material, or a unique-code comparison sufficient to name a new family or conclusively attribute it to DPRK.

What WeaselBiscuit does

Lands via an npm import and auto-runs a detached background Node process

Lands via an npm import and auto-runs a detached background Node process

Pulls its real payload from an Npoint URL and runs it in memory, never touching disk

Pulls its real payload from an Npoint URL and runs it in memory, never touching disk

Beacons to a shared HTTP C2 at 103.170.217.184:8787

Beacons to a shared HTTP C2 at 103.170.217.184:8787

Profiles the host: hostname, user, OS, CPU/RAM, local and public IP, geolocation

Profiles the host: hostname, user, OS, CPU/RAM, local and public IP, geolocation

Steals Chrome extension storage on Windows, macOS, and Linux, which is exactly where wallet extensions keep their signing state

Steals Chrome extension storage on Windows, macOS, and Linux, which is exactly where wallet extensions keep their signing state

Captures clipboard contents, on operator command

Captures clipboard contents, on operator command

Logs Windows keystrokes, on operator command

Logs Windows keystrokes, on operator command

Tags each install with a numeric campaign ID ( 10 , 12 , 44 , 79 , 95 , 99 ) for server-side sorting

Tags each install with a numeric campaign ID ( 10 , 12 , 44 , 79 , 95 , 99 ) for server-side sorting

More on exactly what gets uploaded, and what doesn’t, in the Capabilities section below.

What it doesn’t do , compared to BeaverTail and OtterCookie:

No wallet-draining code, no hardcoded wallet extension ID list

No wallet-draining code, no hardcoded wallet extension ID list

No Chrome password decryptor, no seed-phrase regex sweep

No Chrome password decryptor, no seed-phrase regex sweep

No InvisibleFerret Python second stage

No InvisibleFerret Python second stage

No Socket.IO , WebSocket, or remote shell, just plain polling HTTP

No Socket.IO , WebSocket, or remote shell, just plain polling HTTP

No persistence beyond the detached Node process and a .pid file

No persistence beyond the detached Node process and a .pid file

The implant combines a package-borne Node.js loader, Npoint dead-drop resolution, Chrome extension-storage theft, clipboard collection, Windows keylogging, and a small custom HTTP control plane. Operationally, it’s simpler than commonly documented OtterCookie deployments:

There’s no Socket.IO , no remote shell, no screenshot capability, no Python InvisibleFerret handoff, no browser-password decryptor, no hardcoded wallet list, and no server-delivered follow-on payload in the recovered client. That absence could reflect an early-stage build, a deliberately reduced operational module, or a distinct actor copying familiar tradecraft.

Confirmed infection chain

An application imports [email protected] .

An application imports [email protected] .

index.js automatically calls initialize() .

index.js automatically calls initialize() .

init.js starts a detached node loader.js process and stores its PID in /.pid .

init.js starts a detached node loader.js process and stores its PID in /.pid .

loader.js retrieves JSON from:

loader.js retrieves JSON from:

The loader Base64-decodes the JSON code field and executes it through new Function .

The loader Base64-decodes the JSON code field and executes it through new Function .

The retrieved response is an exact match for the supplied second-stage artifact, SHA-256: 7b15605f23b131b3eeea57e031ae7cb32fc4b78c7bbb2025aa7a561ea5ae5159

The retrieved response is an exact match for the supplied second-stage artifact, SHA-256:

The second stage obtains the C2 configuration from:

The second stage obtains the C2 configuration from:

The configuration resolves the C2 to:

The configuration resolves the C2 to:

Campaign structure and identifiers

WeaselBiscuit isn’t an isolated sample. Static analysis and user-authorized inert retrievals identified a cluster of cloned npm loaders that pull near-identical WeaselBiscuit stages from distinct Npoint URLs. The stages differ materially only in a numeric identifier field included in system-information and Chrome-extension uploads, which strongly suggests a server-side campaign, tenant, operator, or victim-group label.

That clustering is easier to see than to describe. The graph below maps the shared infrastructure our platform recovered across the cluster, with process-tailwind at the center, connected through shared file hashes, Npoint resolver URLs, and the 03.170.217.184 C2 to id79-client , engin1 , and the @biz44 packages covered in the tables below. It contains nineteen nodes, eight of them tied together by shared indicators rather than naming similarity alone. Every edge in this graph is a shared hash, resolver URL, or C2 address, not an assumption

Embedded stage identifier

Confirmed process-tailwind payload

Confirmed swnwall payload

Confirmed WeaselBiscuit clone

Confirmed WeaselBiscuit clone

Confirmed WeaselBiscuit clone

Hard-coded by id79-client ; returned HTTP 404 when retrieved

All five recovered stages use the same second Npoint configuration resolver, , which supplied the same C2: .

The confirmed stage-level identifiers are 10 , 12 , 79 , 95 , and 99 . They’re useful as campaign-hunting markers, but their precise meaning isn’t yet known. The code doesn’t label them as campaign IDs.

[email protected]

All four package loaders the same detached Node process, .pid marker, Npoint JSON retrieval, Base64 decoding, and dynamic new Function execution template. process-tailwind and engin1 have byte-identical loader, init, and index source files. The ID-79 label in id79-client is consistent with the separate recovered stage carrying identifier: "79" , but the package’s current resolver response was unavailable, so that direct delivery link isn’t proven.

The WeaselBiscuit infostealer uploads the following to the IP-hosted C2:

Hostname, username, OS details, CPU, memory, /temp paths, local interface IP/MAC addresses, public IP, and public-IP geolocation

Hostname, username, OS details, CPU, memory, /temp paths, local interface IP/MAC addresses, public IP, and public-IP geolocation

Every readable, nonempty file under Chrome profiles’ Local Extension Settings directories on Windows, macOS, and Linux

Every readable, nonempty file under Chrome profiles’ Local Extension Settings directories on Windows, macOS, and Linux

Changed clipboard contents, when the server enables monitoring

Changed clipboard contents, when the server enables monitoring

Windows keyboard events, when the server enables monitoring

Windows keyboard events, when the server enables monitoring

While this malware does not have the same crypto wallet stealer functions as its big siblings, the Chrome extension-storage capability is financially relevant: it can expose wallet-extension state or other extension-held sensitive data. It uploads every readable, nonempty file under the extension's Local Extension Settings directory — a raw LevelDB key/value store — wholesale.

That store legitimately contains a mix of:

The victim's own account address(es) — typically a small number

The victim's own account address(es) — typically a small number

Wallet extensions like MetaMask and their internally cached token/contract address list (used for balance display, price feeds, swap routing)

Wallet extensions like MetaMask and their internally cached token/contract address list (used for balance display, price feeds, swap routing)

Address-book/ entries (other people's addresses, not the victim's)

Address-book/ entries (other people's addresses, not the victim's)

Various other cached metadata

Various other cached metadata

Meanwhile, clipboard and keylogging can capture credentials, API tokens, wallet addresses, or recovery phrases entered or copied during normal use. However, the code does not contain direct wallet draining, browser-password decryption, seed-phrase searching, or cryptocurrency transaction functionality.

The C2 is a plain Express HTTP service, and its client-facing interface is small:

Host reconnaissance report

/api/upload-local-extension-settings

Chrome extension storage

/api/clipboard-status/

Clipboard collection switch

Clipboard exfiltration

/api/keyboard-mouse-status/

/api/keyboard-mouse-data

Keystroke exfiltration

The client polls the two status routes every five seconds. Recorded responses for a synthetic host were 200 OK Express JSON responses with isMonitoring: false . No payload, command, redirect, or -stage URL was observed in those responses.

That’s a real departure from documented OtterCookie variants, which use Socket.IO and can support broader command execution. This is a polling infostealer control plane, not a full interactive RAT, at least in the recovered code.

How it compares to BeaverTail and OtterCookie

The overlap supporting the DPRK hypothesis is behavioral, not dispositive:

Node.js delivery and execution in a developer/package ecosystem

Node.js delivery and execution in a developer/package ecosystem

Browser extension-storage theft with potential wallet relevance

Browser extension-storage theft with potential wallet relevance

Native clipboard collection using Get-Clipboard and pbpaste

Native clipboard collection using Get-Clipboard and pbpaste

Keylogging, host profiling, and HTTP exfiltration

Keylogging, host profiling, and HTTP exfiltration

Use of lightweight external configuration to decouple the loader from C2 infrastructure

Use of lightweight external configuration to decouple the loader from C2 infrastructure

The divergences matter just as much:

No Socket.IO , WebSocket, or remote-shell client

No Socket.IO , WebSocket, or remote-shell client

No explicit crypto-wallet extension IDs, browser credential databases, or broad file-targeting rules

No explicit crypto-wallet extension IDs, browser credential databases, or broad file-targeting rules

No InvisibleFerret/Python downloader

No InvisibleFerret/Python downloader

No observed persistence beyond a detached process and a local PID marker

No observed persistence beyond a detached process and a local PID marker

Cisco Talos has reported that the line between BeaverTail and OtterCookie has blurred in recent campaigns, including Node.js keylogging, clipboard monitoring, extension and wallet data targeting, and shifting C2 architectures. That makes a lineage relationship plausible, but it doesn’t prove it. See BeaverTail and OtterCookie evolve with a new JavaScript module .

Capability comparison

JavaScript (also ported to Qt/native)

Fake-interview lure + malicious npm packages

Malicious npm packages

Malicious npm packages

Loaded directly by lure package

Loaded directly by lure package

Npoint dead-drop, Base64, in-memory new Function

HTTP POST to hardcoded C2

Socket.IO (bidirectional)

Polling HTTP (Express routes)

Public IP + geolocation (nested ipify to ip-api )

Chrome extension storage theft

Hardcoded crypto-wallet extension ID list

Browser credential DB decryption

Partial (variant-dependent)

Seed-phrase / wallet-address regex sweep

File exfiltration (documents, keystores, profiles)

No (extension storage only)

Yes (recent variants)

Yes, Windows only (operator-gated)

Yes (recent variants)

Remote shell / arbitrary command execution

Via InvisibleFerret handoff

Yes (over Socket.IO )

InvisibleFerret / Python second stage

LaunchAgent / registry / startup entries

Detached process + variant-specific

Detached Node process + .pid marker only

Per-install campaign tagging in code

Not observed publicly

Not observed publicly

Yes, numeric identifier ( 10 , 12 , 44 , 79 , 95 , 99 )

Package is a malicious staged Node.js loader

Auto-start, detached loader, remote Base64 code execution, and exact recovered payload match

Second stage is an infostealer

Static collection and upload logic

103.170.217.184:8787 is the C2 used by this stage

Retrieved configuration and observed client routes

The implementation is a simplified or new branch

Distinct architecture and reduced capability set, though it could also be a commodity copy

Linked to BeaverTail/OtterCookie lineage

Npoint dead-drop pattern is near-identical to prior DPRK npm samples; nested api.ipify.org to ip-api.com lookup matches DPRK stealer convention; Express polling C2 reads as a lightweight port of OtterCookie’s Socket.IO control plane

Consistent DPRK tradecraft signals, but no exclusive infrastructure or shared code recovered

A wholly new malware family

Requires code-cluster, infrastructure, and victimology comparison

Three of those signals are worth walking through in more detail.

Tradecraft signals worth flagging

Npoint.io as the dead drop. DPRK npm crews have been using api.npoint.io as a first-stage dead-drop for a long time, and WeaselBiscuit’s usage is nearly identical to prior DPRK samples: hardcoded UUID, JSON blob containing a Base64 code field, in-process new Function execution. Same service, same shape, same execution pattern.

Nested public-IP and geolocation lookup. The second stage queries api.ipify.org for the public IP, then feeds that IP to ip-api.com for geolocation. That two-step nested lookup shows up in other DPRK-linked npm stealers too. It isn’t a common commodity design.

Express HTTP C2 as a lightweight port of OtterCookie’s Socket.IO plane. The C2 architecture itself is new: plain Express routes, polling-based, no bidirectional socket. But its shape, with per-host status endpoints gating live collection and separate exfiltration endpoints for clipboard and keyboard, reads as a stripped-down re-implementation of the same control model OtterCookie runs over Socket.IO .

Campaign markers resemble PolinRider. The numeric per-install identifiers ( 10 , 12 , 44 , 79 , 95 , 99 ) baked into both package names and stage uploads mirror the campaign-marker convention seen in the PolinRider cluster. The operator is tracking installs at the same granularity, using the same “ID in the package name” pattern.

Indicators of compromise

(identifier 99 )

(identifier 10 )

(identifier 79 )

(identifier 12 )

(identifier 95 )

(identifier unknown, hard-coded by id79-client , HTTP 404 on retrieval)

Npoint C2-config resolver

7b15605f23b131b3eeea57e031ae7cb32fc4b78c7bbb2025aa7a561ea5ae5159

@biz44/process-runtime-utils

process-tailwind (v1.1.99)

Preserve the npm tarball, package publication metadata, maintainer account, dependency graph, download history, and source repository references.

Preserve the npm tarball, package publication metadata, maintainer account, dependency graph, download history, and source repository references.

public and internal telemetry for all six Npoint UUIDs, the C2 IP and port, the stage identifiers 10 , 12 , 79 , 95 , and 99 , and the API route strings.

public and internal telemetry for all six Npoint UUIDs, the C2 IP and port, the stage identifiers 10 , 12 , 79 , 95 , and 99 , and the API route strings.

Compare the loader and decoded stage against known BeaverTail/OtterCookie samples for shared functions, , string conventions, package names, and C2 patterns.

Compare the loader and decoded stage against known BeaverTail/OtterCookie samples for shared functions, , string conventions, package names, and C2 patterns.

Identify the importing parent application. The package itself has no npm lifecycle hook, so execution requires an import or a manual start.

Identify the importing parent application. The package itself has no npm lifecycle hook, so execution requires an import or a manual start.

Review affected endpoints for .pid files, Node child processes, PowerShell processes, and %TEMP%\kb-monitor\keyboard-monitor-*.ps1 .

Review affected endpoints for .pid files, Node child processes, PowerShell processes, and %TEMP%\kb-monitor\keyboard-monitor-*.ps1 .

Treat process-tailwind as a high-confidence malicious supply-chain package, and treat it as a potentially important data point in how DPRK-linked JavaScript infostealers are evolving. Treat the “new DPRK strain” label as a working investigative hypothesis, not a conclusion, until it’s backed by corroborating code, infrastructure, or campaign evidence.