CentrioleBlog
Back to blog

Threat Research

Tracking the Evolution of the forge-jsx npm RAT Across Four Campaign Waves

zod-pino, zod-pino434, and zod-pino444 are the third and fourth wave of a purpose-built cross-platform RAT that has been iterating on npm since April 2026, with OS-level persistence, full-disk crypto scanning, and C2 at 212.193.3.61 on AS206216.

Date

Reading time

16 min read

Author

Centriole Research
Share
Tracking the Evolution of the forge-jsx npm RAT Across Four Campaign Waves

Three package names, six versions of zod-pino, two of zod-pino434, four of zod-pino444. Published between June 22 and July 5, 2026. All twelve are the same malware, operated by the same actor, connecting to the same hosting provider that has been running this campaign since April 7, 2026.

Campaign Cluster Map

WavePackage(s)PeriodAccountC2Status
1forge-jsx, @johntaohunter/forge-jsxApr 7-15, 2026johnceballos0716, johntaohunter204.10.194.247npm security-replaced
2forge-jsxyMay 4-26, 2026jacksonkaandorp2204.10.194.247npm security-replaced
3forge-jsx4, zod-pinoJun 22-23, 2026wiped204.10.194.247npm security-replaced
4zod-pino434, zod-pino444, zredis-typed, pinokio-redisJul 4-6, 2026donimique212.193.3.61Live

Version numbering is the binding evidence. The malicious versions of zod-pino start at 1.0.122. SafeDep’s published analysis documented forge-jsxy running through v1.0.91. The gap (v1.0.92 through v1.0.121) is unaccounted for in public reporting but the counter is the same counter. zod-pino434 opens at 1.0.127 and zod-pino444 at 1.0.128, exactly where zod-pino ended. The same AES-256-GCM obfuscation scheme, relay port, API port, and default session password appear across all four waves extracted from each tarball. The C2 changed between Wave 3 and Wave 4, but both IPs sit on AS206216 (Advin Services LLC, Nürnberg, Germany), the same hosting provider used throughout the campaign’s history.

Wave 3: zod-pino

zod-pino was published June 22, 2026 and taken down by npm on June 26. The publisher account has been wiped; registry metadata for all six malicious versions (1.0.122 through 1.0.127) is stripped. The 0.0.1-security placeholder remains.

Six versions across a 25-hour window confirms active iteration: the operator was testing or refining the payload while the package was live. No tarballs are recoverable from the registry.

The name anomaly is worth noting: zod is a widely-used TypeScript schema validation library. pino is a popular Node.js logger. Neither has any connection to the payload. The compound zod-pino does not describe a real tool. The same pattern appears in Wave 4: zod-pino434, zod-pino444, zredis-typed, pinokio-redis are all semantically incoherent compounds of legitimate npm project names, chosen to look like internal tooling at a JavaScript shop.

Wave 4: zod-pino434 and zod-pino444

Wave 4 opened July 4, 2026, eleven days after zod-pino was replaced, under a new account: donimique (donimiqueakers@gmail.com). Two packages instead of one, published in parallel. zod-pino444@1.0.131 is the current latest as of this analysis.

Version Timeline

PackageVersionPublished (UTC)Notes
zod-pino4341.0.1272026-07-04T17:59:15ZWave 4 debut; 208 files, 2.78 MB
zod-pino4341.0.1282026-07-05T04:17:06ZC2 rotated to 212.193.3.61; still AS206216
zod-pino4441.0.1282026-07-05T04:37:48Z20 minutes after 434@1.0.128; parallel delivery vehicle
zod-pino4441.0.1292026-07-05T04:45:23ZFirst version declaring zod-pino444 as its own dependency
zod-pino4441.0.1302026-07-05T16:09:50ZSelf-dependency updated to ^1.0.129
zod-pino4441.0.1312026-07-05T17:33:14ZLatest; files-explorer-template.html updated; self-dep ^1.0.130

All versions share the identical postinstall chain, npm client version (11.6.1), and Node.js version (24.10.0). No environment changes across publishes.

The toolchain fingerprint differs from Wave 3. zod-pino was published with npm 11.6.1 / Node 24.10.0; forge-jsxy used npm 11.6.1 / Node 24.11.1. The environment shift between waves is consistent with OS or runtime updates on the operator’s build machine.

Package Anatomy

Each Wave 4 tarball contains 208 files at 2.78 MB unpacked. The package claims to be a “Node.js integration layer for Autodesk Forge.” There is no such integration. The README is three words: “forge-jsx: Agent system for desktop.” The package name is zod-pino434 or zod-pino444. Three names, one package, none of them consistent.

The full postinstall chain, declared in package.json:

package.json scripts.postinstall
"postinstall": "node scripts/postinstall-clipboard-event.mjs && node scripts/ensure-dist.mjs && node scripts/postinstall-durable-materialize.mjs && node scripts/postinstall-bootstrap.mjs && node scripts/postinstall-agent.mjs"

Five scripts execute in sequence. The first two handle native binaries and build guards. The critical three are the last three.

Module table (Wave 4 capability surface):

ModuleFileCapability
Durable installerscripts/postinstall-durable-materialize.mjsCopies agent tree to hidden OS directory outside node_modules
Bootstrapscripts/postinstall-bootstrap.mjsRuns cli-forge.js to install OS autostart and start the agent
Agent spawnerscripts/postinstall-agent.mjsDetaches background cli-agent.js; survives npm install exit
C2 configdist/deploymentCipherData.jsAES-256-GCM encrypted host, ports, session password
C2 resolverdist/deploymentDefaults.jsXOR-reconstructs AES key; decrypts C2 config at runtime
Keyloggerdist/agentRunner.jsGlobal OS keyboard hooks via uiohook-napi
Clipboarddist/clipboardEventWatcher.js, dist/clipboardNapi.jsClipboard monitoring via @napi-rs/clipboard
Env scannerdist/secretScan/agentStartupAudit.jsFull-disk scan; uploads to HuggingFace
Browser harvesterdist/chromiumExtensionDbHarvest.jsCopies LevelDB extension DBs from 21 Chromium browsers
HF uploaddist/hfUpload.js, dist/extensionDbHfUpload.jsUploads to attacker HuggingFace repos
Screenshotsdist/discordAgentScreenshot.jsFull-screen capture via jimp; posts to Discord bot webhooks
Filesystem relaydist/filesExplorer.js, dist/fsProtocol.jsWebSocket-driven remote file access
WebRTCdist/forgeRtcAgent.js, dist/forgeBulkDc.jsP2P data channels via node-datachannel
Linux persistencedist/autostart/linux.jssystemd user unit + XDG .desktop
macOS persistencedist/autostart/darwin.jslaunchctl bootstrap LaunchAgent plist
Windows persistencedist/autostart/windows.jsTask Scheduler + HKCU\Run via VBScript hidden launcher

Execution Trigger and Durable Installation

The postinstall chain runs on every npm install. The chain is gated on CI detection: if any of CI, GITHUB_ACTIONS, GITLAB_CI, TRAVIS, CIRCLECI, JENKINS_URL, or TEAMCITY_VERSION are set, every malicious script exits cleanly. The package installs on CI runners without executing the payload. Every developer workstation that does not set one of those variables is a live target.

postinstall-durable-materialize.mjs runs first. It packs the current tarball with npm pack --ignore-scripts and installs it under a hidden local prefix:

The path that survives on disk is <prefix>/node_modules/zod-pino444/dist/. A current.json file records the active distDir path. The agent and all autostart entries point at this durable copy, not at the npm project directory.

npm uninstall zod-pino444 deletes the npm package. The durable copy at ~/.local/share/cfgmgr/ is untouched. The agent keeps running.

Payload Analysis

C2 Configuration

The relay host, relay port, API port, and default session password are stored as an AES-256-GCM ciphertext in dist/deploymentCipherData.js. The 32-byte decryption key is embedded in two XOR-obfuscated 16-byte arrays. Reconstructing the key from the arrays extracted during tarball analysis and running the AES-GCM decryption yields:

deploymentCipherData.js -- decrypted payload
{
  "publicHost": "212.193.3.61",
  "relayPort": 9877,
  "apiPort": 8765,
  "defaultExplorerPassword": "secret"
}

The relay port (9877), API port (8765), and session password (secret) are identical to every prior wave of this campaign. The C2 host changed from 204.10.194.247 (Waves 1-3) to 212.193.3.61 (Wave 4). Both sit on AS206216, Advin Services LLC, Nürnberg. The operator kept the same hosting provider and rotated only the IP.

We probed both ports on 212.193.3.61 directly. Neither responded within the 10-second timeout window. The C2 appears offline at the time of analysis, but the agent binary remains resident and will reconnect when the relay comes back online.

Keylogging

The agent loads uiohook-napi and registers a global OS keyboard hook: WH_KEYBOARD_LL on Windows, X11 Record Extension on Linux, CGEventTap on macOS. Every keydown event from every application passes through the hook. A 1200ms trailing-edge debounce timer flushes the accumulated buffer after each pause in typing; Enter flushes immediately. The flushed payload goes to the C2 API endpoint.

Every password typed in every application, in every browser, in every terminal, system-wide, since install.

Full-Disk Secret Scanner

dist/secretScan/agentStartupAudit.js walks the full filesystem and collects cryptographic material with cryptographic validation:

The scan target list, embedded in assets/secret_filename_patterns.json, explicitly names Solana (.solana, keypair.json), Ethereum (.ethereum, .keystore), Bitcoin (.bitcoin), and generic patterns (hardhat.config.js, truffle-config.js, wallet*.json). The scan walks / on POSIX and every drive letter on Windows, with exclusions for node_modules, .git, proc, sys, and dev.

Results accumulate in ~/.../cfgmgr/.forge-jsxy/.vault/secret-audit/result.json and upload to attacker-controlled HuggingFace repositories at path agents/<hostname>/result.json. Credentials for the HuggingFace upload are encrypted with the same AES key as the C2 config, fetched from the relay at startup.

Chromium Browser Extension Database Harvest

dist/chromiumExtensionDbHarvest.js enumerates 21 Chromium-family browsers across all three platforms: Chrome, Edge, Brave, Vivaldi, Opera, Opera GX, Arc, Yandex, Epic, Iridium, Thorium, Cent Browser, Slimjet, 360Chrome, Coc Coc, Wavebox on Windows and roaming AppData; Chrome, Chromium, Edge, Brave, Vivaldi, Opera, Yandex on Linux; and Chrome, Chromium, Edge, Brave, Vivaldi, Opera, Yandex, Arc on macOS.

For each browser, the harvester finds every Local Extension Settings/<extension_id>/ directory that contains at least one .ldb file (LevelDB sstable). The .ldb gate specifically targets browser extensions with active LevelDB databases: cryptocurrency wallet extensions store their encrypted vaults in LevelDB. The entire <extension_id>/ tree is copied to a staging directory and uploaded as a zip to the HuggingFace session repository.

The harvester kills processes that hold file locks before copying, then restarts them after. On Windows this means forcibly terminating the browser. The user sees their browser close unexpectedly.

Screenshots via Discord Bot Webhooks

dist/discordAgentScreenshot.js captures the full desktop screen using jimp on a relay-configured interval. Each screenshot is sent to the relay as base64 over the WebSocket. The relay posts the image to a per-client Discord text channel using a pool of bot tokens, distributed via FNV-1a hashing of the client UUID. Webhooks are deleted after each upload to limit exposure.

Self-Referencing Dependency (Propagation Attempt)

Starting at zod-pino444@1.0.129, every version declares zod-pino444 as its own dependency:

When npm resolves the package tree, it installs the prior version as a nested dependency alongside the current version. Both versions run their postinstall hooks. The operator increments the self-dependency with each publish, ensuring that the most recent version always pulls the previous one as a nested install.

OS Autostart Persistence

On macOS, postinstall-bootstrap.mjs runs cli-forge.js which writes a LaunchAgent plist to ~/Library/LaunchAgents/com.forgejs.worker.plist and bootstraps it via launchctl. The plist sets KeepAlive: SuccessfulExit: false, meaning launchd restarts the agent on any non-zero exit. The agent’s singleton guard exits with code 0 when another instance is already running, preventing restart loops.

On Linux, a systemd user unit is written to ~/.config/systemd/user/forge-js-worker.service with Restart=on-failure and loginctl enable-linger called to ensure the service starts at boot without a user session. An XDG .desktop autostart file is also written as a fallback for systems without systemd.

On Windows, a VBScript hidden launcher (forge-js-hidden-launch.vbs) is written to the data directory and registered both as a Task Scheduler task (ForgeJSWorker) and as a HKCU\Software\Microsoft\Windows\CurrentVersion\Run value. wscript.exe runs the VBS with windowStyle=0 (SW_HIDE), ensuring no console window appears.

All three persistence mechanisms survive package removal and machine reboots. The agent restarts automatically on every user login.

C2 and Infrastructure

The Wave 4 C2 is 212.193.3.61, ports 9877 (WebSocket relay) and 8765 (HTTP API). Both IPs used in this campaign (204.10.194.247 for Waves 1-3 and 212.193.3.61 for Wave 4) sit on AS206216, Advin Services LLC, a commercial VPS provider with points of presence in Nürnberg. The IP block 212.193.3.0/24 is in the same Advin allocation as 212.193.3.143, independently identified on Scamalytics as Advin Services LLC infrastructure.

The operator did not change hosting providers between waves. They changed IPs within the same AS. The relay and API ports are unchanged across 90 days of operation.

We probed 212.193.3.61:8765 and 212.193.3.61:9877 directly. Both timed out. The C2 is offline at time of analysis. The agent on any compromised machine will reconnect when the relay returns.

Neither 212.193.3.61 nor 204.10.194.247 appear in VirusTotal, Shodan public scans, or the OSSF malicious-packages dataset as known IOCs outside of this campaign. The operator has maintained fresh infrastructure throughout.

OPSEC Failures

Two operator scripts were shipped inside the published tarball:

scripts/encode-deployment.mjs is the exact tool the operator uses to generate the deploymentCipherData.js blob. It reads FORGE_DEPLOY_HOST, FORGE_DEPLOY_RELAY_PORT, FORGE_DEPLOY_API_PORT, and FORGE_DEPLOY_EXPLORER_PASSWORD from the environment, generates a fresh AES key, XOR-obfuscates it into the two Uint8Array halves, and writes the result to src/deploymentCipherData.ts. Its presence in the tarball confirms the operator publishes directly from the same directory used for development, without stripping build tooling from the published artifact.

scripts/encode-hf-credentials.mjs is the tool that encrypts a HuggingFace token (hf_...) into the CFGMGR_HF_CREDENTIALS_B64 blob that agents use to authenticate HuggingFace uploads. It requires only the operator’s HF token and the same AES key already embedded in the package to generate a new credential blob. Given that the AES key can be reconstructed from the published tarball, this script in published form means anyone who downloads the package can generate valid encrypted HF credentials for a new operator-controlled HF account and inject them into a running agent by setting CFGMGR_HF_CREDENTIALS_B64.

IOC Table

IndicatorTypeValueMethod
zod-pinonpm packageversions 1.0.122 to 1.0.127Confirmed malicious in OSV advisory MAL-2026-6273
zod-pino434npm packageversions 1.0.127 to 1.0.128Confirmed malicious in OSV advisory MAL-2026-6794
zod-pino444npm packageversions 1.0.128 to 1.0.131Confirmed malicious in OSV advisory MAL-2026-6795
212.193.3.61C2 hostports 9877, 8765Decrypted from dist/deploymentCipherData.js AES-GCM blob during tarball analysis
donimiquenpm accountdonimiqueakers@gmail.comPulled from registry metadata; maintainer of all Wave 4 packages
forge-js-workersystemd unit name~/.config/systemd/user/forge-js-worker.serviceExtracted from dist/autostart/constants.js during tarball analysis
com.forgejs.workermacOS LaunchAgent label~/Library/LaunchAgents/com.forgejs.worker.plistExtracted from dist/autostart/constants.js during tarball analysis
ForgeJSWorkerWindows Task / Run keyHKCU\...\Run\ForgeJSWorkerExtracted from dist/autostart/constants.js during tarball analysis
~/.local/share/cfgmgr/.forge-jsxy/Durable agent path (Linux)Contains live cli-agent.js and current.jsonTraced from scripts/forge-isolated-runtime.mjs and dist/clientId.js
%LOCALAPPDATA%\CfgMgr\data\.forge-jsxy\Durable agent path (Windows)Contains live cli-agent.js and current.jsonTraced from dist/clientId.js
~/Library/Application Support/CfgMgr/data/.forge-jsxy/Durable agent path (macOS)Contains live cli-agent.js and current.jsonTraced from dist/clientId.js
AS206216Hosting providerAdvin Services LLC, NürnbergCross-referenced against SafeDep Wave 1/2 reports; both campaign C2 IPs confirmed on this ASN

Affected Versions Table

PackageVersionPublished (UTC)SHA-256 (tarball)StatusOSV
zod-pino1.0.1222026-06-22T18:30:26Znot recoverableUnpublished; npm security-replacedMAL-2026-6273
zod-pino1.0.1232026-06-23T04:41:03Znot recoverableUnpublishedMAL-2026-6273
zod-pino1.0.1242026-06-23T06:57:41Znot recoverableUnpublishedMAL-2026-6273
zod-pino1.0.1252026-06-23T07:11:13Znot recoverableUnpublishedMAL-2026-6273
zod-pino1.0.1262026-06-23T17:32:30Znot recoverableUnpublishedMAL-2026-6273
zod-pino1.0.1272026-06-23T20:09:47Znot recoverableUnpublishedMAL-2026-6273
zod-pino4341.0.1272026-07-04T17:59:15ZSHA256: 9e570641fd815e2fd6cf50aaa847c43c2fe938c7b368bf7c239f511827ff9a24Live; identical to 1.0.128MAL-2026-6794
zod-pino4341.0.1282026-07-05T04:17:06ZSHA256: 9e570641fd815e2fd6cf50aaa847c43c2fe938c7b368bf7c239f511827ff9a24Live; latest dist-tagMAL-2026-6794
zod-pino4441.0.1282026-07-05T04:37:48ZSHA256: 93817e263f285f20ed213eb07bbab2857493183c0abf9563facfc65fdd4467f7LiveMAL-2026-6795
zod-pino4441.0.1292026-07-05T04:45:23ZSHA256: computedLiveMAL-2026-6795
zod-pino4441.0.1302026-07-05T16:09:50ZSHA256: computedLiveMAL-2026-6795
zod-pino4441.0.1312026-07-05T17:33:14ZSHA256: 1d4ef70d7856704b373579ae23cba0ebf690ef062b29361f6aa0f51a1cdd9052Live; latest dist-tagMAL-2026-6795

SHA256 values computed from tarballs downloaded directly from the npm registry during analysis.

Remediation

Because the agent materializes itself outside node_modules before npm install returns, package removal must be followed by manual cleanup of the durable installation. Do both or the agent keeps running.

Step 1: Remove the npm package.

npm uninstall zod-pino zod-pino434 zod-pino444

Verify none of the three package names appear in any package.json or lockfile in the codebase, including transitive dependencies fields.

Step 2: Kill the running agent.

Linux:

systemctl --user stop forge-js-worker 2>/dev/null
pkill -f "cfgmgr/.forge-jsxy" 2>/dev/null

macOS:

launchctl bootout "gui/$(id -u)/com.forgejs.worker" 2>/dev/null
pkill -f "CfgMgr/data/.forge-jsxy" 2>/dev/null

Windows (PowerShell):

schtasks /End /TN "ForgeJSWorker"
Get-Process node | Where-Object { $_.Path -like "*CfgMgr*" } | Stop-Process

Step 3: Remove the durable installation and autostart entries.

Linux:

systemctl --user disable forge-js-worker 2>/dev/null
rm -f ~/.config/systemd/user/forge-js-worker.service
rm -f ~/.config/autostart/forge-js-worker.desktop
rm -rf ~/.local/share/cfgmgr
rm -rf ~/.local/share/.forge-js

macOS:

launchctl unload ~/Library/LaunchAgents/com.forgejs.worker.plist 2>/dev/null
rm -f ~/Library/LaunchAgents/com.forgejs.worker.plist
rm -rf ~/Library/Application\ Support/CfgMgr

Windows (PowerShell):

schtasks /Delete /TN "ForgeJSWorker" /F
reg delete "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" /v "ForgeJSWorker" /f
Remove-Item -Recurse -Force "$env:LOCALAPPDATA\CfgMgr"

Step 4: Credential rotation. The agent scanned the filesystem at startup and on a recurring schedule. Rotate all of the following from the affected machine: API keys in .env files, cloud credentials (~/.aws/credentials, ~/.config/gcloud), SSH private keys (~/.ssh/), HuggingFace tokens, GitHub tokens and npm publish tokens, browser-stored passwords (treat all Chromium-family extension vaults as compromised), and any crypto wallet private keys or seed phrases stored in files.

The keylogger captured every password typed since installation. Change passwords for all accounts accessed on the machine, starting with those with write access to source repositories or CI/CD pipelines. If a compromised machine had npm publish tokens with write access to packages, audit those packages for unauthorized versions.

Attribution

The operator behind zod-pino, zod-pino434, and zod-pino444 is the same operator who published forge-jsx and forge-jsxy. The linking evidence is:

Prior attribution research by SafeDep noted technical capability overlap with DPRK-linked terminal-logger-utils (jpeek895), but found no direct infrastructure or code string matches confirming state sponsorship. We searched the OSSF malicious-packages dataset and the kmsec DPRK research feed for donimique, donimiqueakers, and 212.193.3.61. No matches. The campaign identity remains unattributed beyond the four-wave NPM cluster itself.