NEW

The Complete AI SOC Platform is here. Read the announcement →

close

The Complete AI SOC Platform is here. Read the announcement →

close

BLOG

Tracking an OtterCookie Infostealer Campaign Across npm

Alessandra

Rizzo

Introduction

Between April 6 and April 9, 2026, our npm scanner identified a cluster of obfuscated malicious packages published by multiple throwaway accounts. Our analysis revealed these packages to be variants of the OtterCookie infostealer, a credential theft and backdoor toolchain attributed to North Korean threat actors.

The packages use a two-layer distribution strategy: a benign wrapper packages that clones legitimate libraries,big.js in our case, which pulls in a malicious dependency containing the actual payload, ensuring the malicious code is one dependency layer deeper. To date we have identified 5 malicious packages.

Each payload package contains two obfuscated JavaScript files: a loader (test.js) and a secondary-stage stealer/backdoor (index.js). On install, the postinstall hook triggers the loader, which imports and executes the main exfiltration and persistence logic. The malware steals files and credentials and exfiltrates them to attacker-controlled Vercel-hosted C2. As the last step, it installs an SSH public key backdoor on Linux systems.

This campaign overlaps directly with infrastructure documented by Walmart Global Tech in their April 2026 analysis of OtterCookie infrastructure, sharing the same C2 domains and behavioral patterns.

Campaign Overview

The diagram below traces the full campaign lifecycle from operator to impact. Throwaway npm accounts publish both benign wrapper packages and obfuscated payload packages, which reach victims through dependency chaining and trigger file exfiltration and SSH backdoor installation on the victim's machine.

Stolen data flows through Vercel-hosted C2 domains which double as configuration servers, allowing operators to dynamically retarget what files are stolen before being relayed to backend DPRK infrastructure.

Infection chain

The attack uses a two-layer package distribution strategy. The victim encounters a wrapper package — such as bjs-biginteger, which is a clone of the legitimate big.js library, complete with repository and homepage fields pointing at the real github.com/MikeMcl/big.js.

The wrapper's source code is entirely benign; the only modification is the addition of a payload package (e.g., bjs-lint-builder) as a dependency. When the victim runs npm install, npm resolves and installs the payload automatically.

Technical Analysis

Both test.js and index.js use a custom base91-like string encoding scheme. Each function scope uses a different alphabet, so the same encoded string produces entirely different output depending on which decoder processes it.

The main payload contained in index.js runs two attack chains in parallel.

The first is a quick targeted pass that searches the current working directory for high-value files — Solana wallet keypairs, Rust configuration, and environment files containing secrets — reads each one, prepends the victim's username, and uploads it to cloudflarefirewall[.]vercel[.]app/api/v1 as a raw octet stream.

The second chain is more comprehensive: it fetches dynamic configuration from cloudflareinsights[.]vercel[.]app — a list of file extensions to target, directories to exclude, and an SSH public key — then recursively walks the victim's filesystem, matching files against the fetched scan patterns. All matches are batch-uploaded as a multipart form POST to cloudflareinsights[.]vercel[.]app/api/v1 alongside metadata including the victim's username, platform, and public IP.

On Linux, the malware also installs a persistent SSH backdoor by appending the fetched public key to ~/.ssh/authorized_keys, creating the directory if needed, fixing ownership with chown, and opening port 22 through the firewall with ufw.

Attribution

This campaign is attributed with high confidence to DPRK / FAMOUS CHOLLIMA. The evidence rests on direct infrastructure, tradecraft, and operational overlaps with two documented campaigns: Contagious Interview and Contagious Trader.

The packages in this report are the latest iteration of the Contagious Trader campaign documented by kmsec in March 2026.

The cloudflareinsights[.]vercel[.]app domain decoded from our obfuscated samples is the same exfiltration server used by at least six other Contagious Trader packages dating back to February 2026. The API endpoint structure is identical across all variants.

The two-layer distribution pattern is the same: a benign wrapper impersonating big.js pulls in a malicious payload with a lint/builder naming theme. Our bjs-biginteger pulling bjs-lint-builder mirrors pairs like bignum-tsts-lint-builder and big-nunberlint-builder.

The dotted Gmail addresses used by our operators match the pattern documented by kmsec. Gmail ignores dots, making these throwaway accounts generated via emailinator, a previously documented FAMOUS CHOLLIMA preference.

Even the OPSEC failures are shared: kmsec notes that npm-builders v1.0.8 was published unobfuscated, and we independently found the same mistake in bjs-lint-builders v1.0.4. The two plaintext samples are functionally identical.

The broader campaign connects back to FAMOUS CHOLLIMA through ReversingLabs' attribution of the big.js vibe-squatting pattern to Contagious Interview (dubbed "graphalgo"), the shared use of *.vercel.app C2 staging across both campaigns, and Walmart Global Tech's independent identification of the same cloudflareinsights[.]vercel[.]app infrastructure as OtterCookie, a known DPRK infostealer alongside BeaverTail and InvisibleFerret.

Conclusion

This campaign demonstrates the continued evolution of North Korean software supply chain attacks targeting developers. The custom base91 encoding with per-function alphabet rotation represents a notable advancement over the obfuscator.io techniques used in earlier campaigns like BeaverTail and Koalemos. It defeats static string extraction entirely and requires analysts to identify the correct decoder context for each function scope. The two-layer package distribution strategy (benign wrapper + malicious dependency) adds another obstacle to manual review.

The packages identified here are not isolated. They are the April 2026 iteration of a campaign that has been running since at least February, rotating through dozens of throwaway accounts and package names while keeping the same C2 infrastructure, the same malware, and the same SSH backdoor logic.

See it in action

Most AI closes the alert. Panther closes the loop.

MITRE ATT&CK Mapping

Technique ID

Name

Usage

T1195.002

Supply Chain Compromise: Compromise Software Supply Chain

Distribution through malicious npm packages with dependency chaining

T1059.007

Command and Scripting Interpreter: JavaScript

Stealer implemented in Node.js

T1071.001

Application Layer Protocol: Web Protocols

HTTPS C2 via Vercel-hosted endpoints

T1082

System Information Discovery

Username, hostname, platform, CPU count, environment variables

T1083

File and Directory Discovery

Recursive filesystem scan with pattern matching

T1005

Data from Local System

Reading files matching target extensions

T1041

Exfiltration Over C2 Channel

File upload via axios POST to C2 endpoints

T1027

Obfuscated Files or Information

Custom base91 encoding with per-function alphabets

T1098.004

Account Manipulation: SSH Authorized Keys

Appending attacker's SSH key to ~/.ssh/authorized_keys

T1059.004

Command and Scripting Interpreter: Unix Shell

sudo chown, sudo ufw enable, sudo ufw allow 22/tcp via execSync

T1036.005

Masquerading: Match Legitimate Name or Location

C2 domains impersonating Cloudflare services

T1656

Impersonation

Wrapper packages cloning legitimate npm libraries

Detection

YARA Rules

rule OtterCookie_NPM_Base91_Stealer {
    meta:
        description = "Detects OtterCookie npm stealer with custom base91 encoding"
        author = "PantherLabs"
        date = "2026-04-09"
        reference = "<https://medium.com/walmartglobaltech/mapping-ottercookie-infrastructure-1c49f0cd3883>"

    strings:
        $const_array = /const\\s+\\w+\\s*=\\s*\\[0x0,\\s*0x1,\\s*0x8,\\s*0xff,\\s*"length",\\s*"undefined",\\s*0x3f/
        $from_code_point = "\\"fromCodePoint\\"" ascii
        $push_method = "\\"push\\"" ascii
        $base91_magic1 = "0x5b" ascii
        $base91_magic2 = "0x1fff" ascii
        $base91_magic3 = "0x58" ascii
        $utf8_decoder = "fromCharCode" ascii
        $alphabet_pattern = /var\\s+\\w+\\s*=\\s*"[^\\\\"]{85,95}"/

    condition:
        filesize < 200KB and
        $const_array and
        $from_code_point and
        $push_method and
        all of ($base91_magic*) and
        $utf8_decoder and
        #alphabet_pattern > 3
}

rule OtterCookie_NPM_Loader {
    meta:
        description = "Detects OtterCookie npm loader component"
        author = "PantherLabs"
        date = "2026-04-09"

    strings:
        $const_array = /const\\s+\\w+\\s*=\\s*\\[0x0,\\s*0x1,\\s*0x8,\\s*0xff,\\s*"length",\\s*"undefined"/
        $require_dot = "require(\\".\\")" ascii
        $async_try = /async\\s+function\\s+\\w+\\(\\)\\s*\\{\\s*try\\s*\\{\\s*await\\s+\\w+\\(\\)/
        $silent_catch = /catch\\s*\\(\\s*\\w*\\s*\\)\\s*\\{\\s*\\}/

    condition:
        filesize < 50KB and
        $const_array and
        $require_dot and
        $async_try and
        $silent_catch
}
rule OtterCookie_NPM_Base91_Stealer {
    meta:
        description = "Detects OtterCookie npm stealer with custom base91 encoding"
        author = "PantherLabs"
        date = "2026-04-09"
        reference = "<https://medium.com/walmartglobaltech/mapping-ottercookie-infrastructure-1c49f0cd3883>"

    strings:
        $const_array = /const\\s+\\w+\\s*=\\s*\\[0x0,\\s*0x1,\\s*0x8,\\s*0xff,\\s*"length",\\s*"undefined",\\s*0x3f/
        $from_code_point = "\\"fromCodePoint\\"" ascii
        $push_method = "\\"push\\"" ascii
        $base91_magic1 = "0x5b" ascii
        $base91_magic2 = "0x1fff" ascii
        $base91_magic3 = "0x58" ascii
        $utf8_decoder = "fromCharCode" ascii
        $alphabet_pattern = /var\\s+\\w+\\s*=\\s*"[^\\\\"]{85,95}"/

    condition:
        filesize < 200KB and
        $const_array and
        $from_code_point and
        $push_method and
        all of ($base91_magic*) and
        $utf8_decoder and
        #alphabet_pattern > 3
}

rule OtterCookie_NPM_Loader {
    meta:
        description = "Detects OtterCookie npm loader component"
        author = "PantherLabs"
        date = "2026-04-09"

    strings:
        $const_array = /const\\s+\\w+\\s*=\\s*\\[0x0,\\s*0x1,\\s*0x8,\\s*0xff,\\s*"length",\\s*"undefined"/
        $require_dot = "require(\\".\\")" ascii
        $async_try = /async\\s+function\\s+\\w+\\(\\)\\s*\\{\\s*try\\s*\\{\\s*await\\s+\\w+\\(\\)/
        $silent_catch = /catch\\s*\\(\\s*\\w*\\s*\\)\\s*\\{\\s*\\}/

    condition:
        filesize < 50KB and
        $const_array and
        $require_dot and
        $async_try and
        $silent_catch
}
rule OtterCookie_NPM_Base91_Stealer {
    meta:
        description = "Detects OtterCookie npm stealer with custom base91 encoding"
        author = "PantherLabs"
        date = "2026-04-09"
        reference = "<https://medium.com/walmartglobaltech/mapping-ottercookie-infrastructure-1c49f0cd3883>"

    strings:
        $const_array = /const\\s+\\w+\\s*=\\s*\\[0x0,\\s*0x1,\\s*0x8,\\s*0xff,\\s*"length",\\s*"undefined",\\s*0x3f/
        $from_code_point = "\\"fromCodePoint\\"" ascii
        $push_method = "\\"push\\"" ascii
        $base91_magic1 = "0x5b" ascii
        $base91_magic2 = "0x1fff" ascii
        $base91_magic3 = "0x58" ascii
        $utf8_decoder = "fromCharCode" ascii
        $alphabet_pattern = /var\\s+\\w+\\s*=\\s*"[^\\\\"]{85,95}"/

    condition:
        filesize < 200KB and
        $const_array and
        $from_code_point and
        $push_method and
        all of ($base91_magic*) and
        $utf8_decoder and
        #alphabet_pattern > 3
}

rule OtterCookie_NPM_Loader {
    meta:
        description = "Detects OtterCookie npm loader component"
        author = "PantherLabs"
        date = "2026-04-09"

    strings:
        $const_array = /const\\s+\\w+\\s*=\\s*\\[0x0,\\s*0x1,\\s*0x8,\\s*0xff,\\s*"length",\\s*"undefined"/
        $require_dot = "require(\\".\\")" ascii
        $async_try = /async\\s+function\\s+\\w+\\(\\)\\s*\\{\\s*try\\s*\\{\\s*await\\s+\\w+\\(\\)/
        $silent_catch = /catch\\s*\\(\\s*\\w*\\s*\\)\\s*\\{\\s*\\}/

    condition:
        filesize < 50KB and
        $const_array and
        $require_dot and
        $async_try and
        $silent_catch
}
rule OtterCookie_NPM_Base91_Stealer {
    meta:
        description = "Detects OtterCookie npm stealer with custom base91 encoding"
        author = "PantherLabs"
        date = "2026-04-09"
        reference = "<https://medium.com/walmartglobaltech/mapping-ottercookie-infrastructure-1c49f0cd3883>"

    strings:
        $const_array = /const\\s+\\w+\\s*=\\s*\\[0x0,\\s*0x1,\\s*0x8,\\s*0xff,\\s*"length",\\s*"undefined",\\s*0x3f/
        $from_code_point = "\\"fromCodePoint\\"" ascii
        $push_method = "\\"push\\"" ascii
        $base91_magic1 = "0x5b" ascii
        $base91_magic2 = "0x1fff" ascii
        $base91_magic3 = "0x58" ascii
        $utf8_decoder = "fromCharCode" ascii
        $alphabet_pattern = /var\\s+\\w+\\s*=\\s*"[^\\\\"]{85,95}"/

    condition:
        filesize < 200KB and
        $const_array and
        $from_code_point and
        $push_method and
        all of ($base91_magic*) and
        $utf8_decoder and
        #alphabet_pattern > 3
}

rule OtterCookie_NPM_Loader {
    meta:
        description = "Detects OtterCookie npm loader component"
        author = "PantherLabs"
        date = "2026-04-09"

    strings:
        $const_array = /const\\s+\\w+\\s*=\\s*\\[0x0,\\s*0x1,\\s*0x8,\\s*0xff,\\s*"length",\\s*"undefined"/
        $require_dot = "require(\\".\\")" ascii
        $async_try = /async\\s+function\\s+\\w+\\(\\)\\s*\\{\\s*try\\s*\\{\\s*await\\s+\\w+\\(\\)/
        $silent_catch = /catch\\s*\\(\\s*\\w*\\s*\\)\\s*\\{\\s*\\}/

    condition:
        filesize < 50KB and
        $const_array and
        $require_dot and
        $async_try and
        $silent_catch
}

IoCs

Behavioral Indicators

  • postinstall hook executing node test.js in packages with no stated functionality

  • Dependencies on axios, form-data, child_process, and os as npm packages

  • Outbound HTTPS connections to .vercel.app domains from Node.js processes

  • Modifications to ~/.ssh/authorized_keys by non-interactive processes

  • sudo ufw allow 22/tcp executed by non-root Node.js processes

  • wmic logicaldisk get name executed by Node.js on Windows

Malicious Packages

Package

Version(s)

Account

Email

bjs-lint-builders

1.0.4, 1.0.5, 1.1.0

a.l.l.a.nh.orca0.7

a.l.l.a.nh.orca0.7[@]googlemail.com

bjs-lint-builder

1.0.5

a.l.l.a.nh.orca0.7

a.l.l.a.nh.orca0.7[@]googlemail.com

bjs-biginteger

5.0.6

a.l.l.a.nh.orca0.7

a.l.l.a.nh.orca0.7[@]googlemail.com

hjs-lint-builders

1.0.4

nar.a.tat.ia.n.aaa

nar.a.tat.ia.n.aaa[@]gmail.com

sjs-builders

1.0.4

ayal.a.d.av.e.7

ayal.a.d.av.e.7[@]gmail.com

sjs-builder

1.0.4

a.n.n.as.ibal2.36

a.n.n.as.ibal2.36[@]googlemail.com

npm-doc-builder

1.0.5

al.lanjaysa.t.i.a.gi

al.lanjaysa.t.i.a.gi[@]gmail.com)

C2 Infrastructure

Category

Indicator

Description

C2 Domain

cloudflareinsights[.]vercel[.]app

Primary C2 — config fetch and multipart upload

C2 Domain

cloudflarefirewall[.]vercel[.]app

Secondary C2 — individual file upload

C2 Domain

cloudflaresecurity[.]vercel[.]app

Legacy C2 — observed in unobfuscated v1.0.4

C2 Endpoint

/api/ssh-key

SSH public key retrieval

C2 Endpoint

/api/scan-patterns

File extension targeting config

C2 Endpoint

/api/block-patterns

Directory exclusion config

C2 Endpoint

/api/v1

File exfiltration upload

Targeted Files

Pattern

Context

id.json

Solana wallet keypairs

config.toml / Config.toml

Cargo and application config

.env / env

Environment variable files with secrets

.bash_history

Shell command history

ConsoleHost_history.txt

PowerShell command history

~/.ssh/authorized_keys

SSH key injection target

Related Infrastructure

IP Address

Notes

144.172.116[.]22

OtterCookie backend (ports 8085-8087)

144.172.110[.]96

Related infrastructure

144.172.110[.]228

Related infrastructure

144.172.99[.]248

Related infrastructure

107.189.22[.]20

Related infrastructure

144.172.110[.]132

Related infrastructure

144.172.99[.]81

Related infrastructure

144.172.93[.]169

Related infrastructure

144.172.93[.]253

Related infrastructure

References

  1. Jason Reaves, Mapping Ottercookie Infrastructure, Walmart Global Tech Blog, April 2026

  2. Microsoft Security Blog, Contagious Interview malware delivered through fake developer job interviews, March 2026

  3. KMSec, Contagious Trader

  4. Enki, Contagious Interview Campaign Abusing VSCode Distributed on GitHub

  5. Panther Labs, No Fool's Errand: The Koalemos RAT Campaign, January 2026

Share:

Bolt-on AI closes alerts. Panther closes the loop.

See how Panther compounds intelligence across the SOC.

Bolt-on AI closes alerts. Panther closes the loop.

See how Panther compounds intelligence across the SOC.

Bolt-on AI closes alerts. Panther closes the loop.

See how Panther compounds intelligence across the SOC.

Bolt-on AI closes alerts. Panther closes the loop.

See how Panther compounds intelligence across the SOC.

Get product updates, webinars, and news

By submitting this form, you acknowledge and agree that Panther will process your personal information in accordance with the Privacy Policy.

Get product updates, webinars, and news

By submitting this form, you acknowledge and agree that Panther will process your personal information in accordance with the Privacy Policy.

Get product updates, webinars, and news

By submitting this form, you acknowledge and agree that Panther will process your personal information in accordance with the Privacy Policy.