A malicious npm package hidden three dependencies deep: the ulid-xyz delivery chain

6 min read

Summary

SafeDep malicious package analysis flagged ulid-xyz on npm in June 2026. It typosquats ulidx, a library that generates sortable unique identifiers, and it is a cross-platform remote access trojan.

The postinstall hook reads as a guard that checks a build file exists. It actually launches dist/node/utils.js as a detached background process, which starts dist/node/payload.js, a 467 KB bundled trojan. That bundle decodes an obfuscated configuration and beacons to a hardcoded command server over WebSocket. It then installs persistence on Windows, macOS and Linux under the name MicrosoftSystem64. The package grew from 64 KB to 536 KB when the payload arrived.

We reported it as OSVMAL-2026-6672.

We traced the delivery path. It runs through three npm packages:

28 attacker-created GitHub repositories
└─ ioredis-xyz the dependency a developer adds
└─ redis-type-xyz npm resolves this one
└─ ulid-xyz npm resolves this one, and it carries the payload

None of these repositories is a compromised legitimate project. Each is an application the operator wrote or copied and published, then used to advertise ioredis-xyz. A developer who installs ioredis-xyz gets the other two packages without naming them, and no single package in the chain looks malicious on its own.

The three packages

Layer 1: ioredis-xyz

ioredis is a Redis client for Node.js. ioredis-xyz is the same package. Each archive holds the same 89 files, and every file except package.json is identical, so it works exactly like the real client. Only the name changes. The rest of the metadata still points at the real project, so the npm page looks genuine.

Version 5.11.2 adds one dependency, redis-type-xyz@^1.10.5. Nothing in the package imports it. npm installs it anyway, which is its only purpose.

Layer 2: redis-type-xyz

This package presents itself as Redis OM, the object mapping library for Redis. Its archive holds 33 files and none of them run. It contains no .js, .ts, .mjs or .cjs file. package.json declares "main": "dist/index.js", and no dist directory exists, so importing the package fails.

It is a manifest and nothing else. Version 1.10.5 declared no dependency. Version 1.10.6 added ulid-xyz@^2.12.2. Its advisory is OSVMAL-2026-11205.

The README file still tells you to install redis-type-os. npm had removed that package 18 hours earlier, under advisory OSVMAL-2026-5882. The operator rebuilt under a new name and reused the documentation.

Layer 3: ulid-xyz

This one imitates ulidx, which generates sortable unique identifiers. The real ulidx ships no runtime dependencies. This version declares ten, including a WebSocket client and a package named postinstall.

The trigger is the postinstall script in the 2.12.x releases:

"postinstall": "node -e \"...check dist/node/utils.js exists...\" && node dist/node/utils.js"

The check in the first half is a screen. The command after it runs the payload. That same check text also appears in ioredis-xyz, which ties together two packages published by two different npm accounts.

The publisher accounts tie the lower two layers together as well. ulid-xyz came from [email protected] and redis-type-xyz from [email protected], two disposable addresses on the same domain root.

The payload was added after publication

A review of ioredis-xyz on the day it shipped would have found nothing. The payload arrived 19 minutes later.

Time (UTC, 2026-06-17)Event
07:37:08[email protected] published, depending on redis-type-xyz@^1.10.5
07:56:44[email protected] published, adding the ulid-xyz dependency

At 07:37 the chain was harmless. [email protected] was the only version in the 1.x line, and it declared no payload dependency. The range ^1.10.5 matches any 1.x version at or above 1.10.5, so once 1.10.6 existed, every resolution pulled in ulid-xyz.

The operator never republished ioredis-xyz. The visible package did not change. They completed the chain from one layer below it, where nobody was looking.

The operator planted the chain in 28 repositories

These are not compromised upstream projects. They are purpose-built repositories.

RepositoryStarsForksDeclared range
Cesarjoquin/Marketing-Skills1821236^5.11.2
KORAYTEACHER/fintech-advisor1711124^5.11.2
jaipreet15/tradingview-mcp150204^5.11.2
UHolli/ai-website-cloner1493^5.11.2
pifferologo/ai-agent-video-editor1441060^5.11.2
angieruiz17/claude-fintech-skills143959^5.11.1
kazelad/prediction-market-trade-sdk14111095.11.1
Cesarjoquin/shopify-mcp140400^5.11.2
JesusRS1/stock-trade-finance-api1404395.11.1
jinm29/ai-healthcare-app139989^5.11.2
Alice53211/auth-codex-plugin135959^5.11.1
Elias569/fintech-dashboard1350^5.11.2
Matchameleon/moneyclaw135937^5.11.1
Signal-Execution-Labs/forex-trading-ai-agent134869^5.11.1
1canhhoa/sports-betting-toolbox133870^5.10.2
pueschel88/Tradingview-MCP133312^5.11.1
Don-Uwe/video-editor-ai-agent1311024^5.11.2
Elias569/fintech-app1311038^5.11.1
pamgarcia1993/robinhood-lp-bot130956^5.11.2
AhmedHazem02/fifa-world-cup-2026-prediction-agent129852^5.10.2
FR0ZON3/notion-mcp11710835.11.2
nkosresearch/web3-casino1041078^5.11.1
whisdev/flash-loan-trading-bot10277^5.10.2
whisdev/NBA-prediction-sports-betting902^5.10.2
white-trade-loan/algo-trading-platform88936^5.10.2
Berliwu/world-cup-2026-match-prediction-engine87827^5.11.1
tradinglabpremium/sports-prediction-market-scanner841^5.10.2
Signal-Execution-Labs/mexc-future-agent83660^5.11.1

The payload

The launcher exits when the machine reports fewer than four processors, which evades analysis machines and continuous integration runners. It then starts the agent as a detached process that survives the installing shell.

The agent is 467 KB. It opens a WebSocket to the command server and runs tasks:

exports.CAPABILITIES = ['ping', 'get_system_info', 'list_drives', 'list_dir', 'deploy_binary', 'remove_agent'];

This bundle does not steal credentials itself. It holds no wallet, browser or keychain strings. It is the first stage of a full remote access trojan: it fingerprints the host, lets an operator read the file system, and then takes whatever the operator sends. deploy_binary accepts a base64 field, writes it to disk, installs persistence and restarts into that code, so the operator chooses the capability set per machine after reading the reconnaissance.

The agent persists as MicrosoftSystem64 on all three operating systems, through a scheduled task on Windows, a LaunchAgent on macOS and a systemd user unit on Linux. Base64 and a repeating XOR key hide its two command servers, 65.21.30.171 and 95.216.232.162, both at Hetzner in Germany. MAL-2026-6672 records only the second address.

The implant matches a DPRK-linked cluster

MicrosoftSystem64 is a name we have analysed before. In May 2026 we published a binary analysis of MicrosoftSystem64, an 81 MB Node.js single executable delivered through the js-logger-pack and terminal-logger-utils packages. That implant steals browser credentials, more than 80 cryptocurrency wallet extensions, Telegram sessions and SSH keys, and exfiltrates to HuggingFace. kmsec.uk and OX Security attribute the cluster to FAMOUS CHOLLIMA, also tracked as Contagious Interview, a DPRK-linked group.

Four things tie this chain to that cluster:

  • The same implant name, MicrosoftSystem64, and the same persistence design across Windows, macOS and Linux.
  • The same command server port, 8010. The earlier binary used 195.201.194.107:8010.
  • The same hosting provider, Hetzner, for all three addresses.
  • whisdev, the persona JFrog traced through copilot-ai.whisdev.org on that earlier command server, owns two of the 28 repositories listed above.

That last point is a direct overlap rather than a shared technique. The likely second stage delivered through deploy_binary is the credential stealer documented in that earlier post.

Limit of this analysis: no second stage appears in any published archive of these three packages, so we cannot say what reached any specific machine.

Timeline

Date and time (UTC)Event
2026-06-16 11:53npm removes redis-type-os (MAL-2026-5882)
2026-06-16 17:08[email protected] published, no payload dependency
2026-06-17 06:07[email protected] published, no payload dependency
2026-06-17 07:37[email protected] adds redis-type-xyz@^1.10.5
2026-06-17 07:56[email protected] adds ulid-xyz@^2.12.2; the chain is armed
2026-06-29 15:10[email protected] published with a new command server
2026-08-25 08:41A copied trading bot repository adds ioredis-xyz@^5.11.2
2026-08-25 16:36npm removes ulid-xyz (MAL-2026-6672)

That last commit preceded the removal by 7 hours and 55 minutes.

Indicators of compromise

IndicatorValue
Entry package[email protected]
Relay package[email protected], [email protected]
Payload package[email protected], [email protected]
Command server (June 17)hxxp://65[.]21[.]30[.]171:8010 and ws://65[.]21[.]30[.]171:8010
Command server (June 29)hxxp://95[.]216[.]232[.]162:8010 and ws://95[.]216[.]232[.]162:8010
Hosting providerHetzner Online GmbH, Germany
XOR key5A 3C 7E 12 9F 4B 6D 8A
Persistence nameMicrosoftSystem64
Windows task\MicrosoftSystem64 and MicrosoftSystem64.vbs
macOS agent~/Library/LaunchAgents/com.launchkeeper.MicrosoftSystem64.plist
Linux unit~/.config/systemd/user/MicrosoftSystem64.service
Lock file.pkg-agent.lock in the temporary directory
Payload SHA-256 (2.12.2)a3c28435295fed4babdeefedcefdd0ed037ff24ed3ff363a49d080c2768d07f2
Payload SHA-256 (2.12.3)3a9089e9db3650dd6d1584fae709022002dc34854b961abfb014a90f0a7c6a50
Launcher SHA-256aa01a83c7a420c22a719b02ec327451ddd751ab5b189fd0127824ea43533b96a
Related advisoriesMAL-2026-6672, MAL-2026-11205, MAL-2026-5882
  • malware
  • npm
  • supply-chain

Author

Kunal Singh

Kunal Singh

safedep.io

Share

The Latest from SafeDep blogs

Follow for the latest updates and insights on open source security & engineering

Background
SafeDep Logo

Ship Code.

Not Malware.

Start free with open source tools on your machine. Scale to a unified platform for your organization.