A malicious npm package hidden three dependencies deep: the ulid-xyz delivery chain
On this page
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
MAL-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 payloadNone 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
MAL-2026-11205.
The README file still tells you to install redis-type-os. npm had removed that package 18 hours earlier, under
advisory
MAL-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.
| Repository | Stars | Forks | Declared range |
|---|---|---|---|
Cesarjoquin/Marketing-Skills | 182 | 1236 | ^5.11.2 |
KORAYTEACHER/fintech-advisor | 171 | 1124 | ^5.11.2 |
jaipreet15/tradingview-mcp | 150 | 204 | ^5.11.2 |
UHolli/ai-website-cloner | 149 | 3 | ^5.11.2 |
pifferologo/ai-agent-video-editor | 144 | 1060 | ^5.11.2 |
angieruiz17/claude-fintech-skills | 143 | 959 | ^5.11.1 |
kazelad/prediction-market-trade-sdk | 141 | 1109 | 5.11.1 |
Cesarjoquin/shopify-mcp | 140 | 400 | ^5.11.2 |
JesusRS1/stock-trade-finance-api | 140 | 439 | 5.11.1 |
jinm29/ai-healthcare-app | 139 | 989 | ^5.11.2 |
Alice53211/auth-codex-plugin | 135 | 959 | ^5.11.1 |
Elias569/fintech-dashboard | 135 | 0 | ^5.11.2 |
Matchameleon/moneyclaw | 135 | 937 | ^5.11.1 |
Signal-Execution-Labs/forex-trading-ai-agent | 134 | 869 | ^5.11.1 |
1canhhoa/sports-betting-toolbox | 133 | 870 | ^5.10.2 |
pueschel88/Tradingview-MCP | 133 | 312 | ^5.11.1 |
Don-Uwe/video-editor-ai-agent | 131 | 1024 | ^5.11.2 |
Elias569/fintech-app | 131 | 1038 | ^5.11.1 |
pamgarcia1993/robinhood-lp-bot | 130 | 956 | ^5.11.2 |
AhmedHazem02/fifa-world-cup-2026-prediction-agent | 129 | 852 | ^5.10.2 |
FR0ZON3/notion-mcp | 117 | 1083 | 5.11.2 |
nkosresearch/web3-casino | 104 | 1078 | ^5.11.1 |
whisdev/flash-loan-trading-bot | 102 | 77 | ^5.10.2 |
whisdev/NBA-prediction-sports-betting | 90 | 2 | ^5.10.2 |
white-trade-loan/algo-trading-platform | 88 | 936 | ^5.10.2 |
Berliwu/world-cup-2026-match-prediction-engine | 87 | 827 | ^5.11.1 |
tradinglabpremium/sports-prediction-market-scanner | 84 | 1 | ^5.10.2 |
Signal-Execution-Labs/mexc-future-agent | 83 | 660 | ^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 throughcopilot-ai.whisdev.orgon 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:53 | npm 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:41 | A copied trading bot repository adds ioredis-xyz@^5.11.2 |
| 2026-08-25 16:36 | npm removes ulid-xyz (MAL-2026-6672) |
That last commit preceded the removal by 7 hours and 55 minutes.
Indicators of compromise
| Indicator | Value |
|---|---|
| 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 provider | Hetzner Online GmbH, Germany |
| XOR key | 5A 3C 7E 12 9F 4B 6D 8A |
| Persistence name | MicrosoftSystem64 |
| 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-256 | aa01a83c7a420c22a719b02ec327451ddd751ab5b189fd0127824ea43533b96a |
| Related advisories | MAL-2026-6672, MAL-2026-11205, MAL-2026-5882 |
- malware
- npm
- supply-chain
Author
Kunal Singh
safedep.io
Share
The Latest from SafeDep blogs
Follow for the latest updates and insights on open source security & engineering
Mini Shai-Hulud Strikes Again: openapi-react-query-codegen
An attacker exploited a flawed GitHub Actions workflow to publish 10 malicious versions of @7nohe/openapi-react-query-codegen (671K downloads/month) via npm OIDC trusted publishing. The payload uses...
Malicious Rust Crate arrayref Runs a Build-Time Payload
A compromised release of the popular Rust crate arrayref pulled in a typosquatted proc-macro1 whose build script downloads and runs a remote binary at compile time. The malicious versions are now...
From YouTube Ad to Root: How a Fake TradingView Installer Delivers a macOS Stealer
A YouTube video ad impersonating TradingView delivered a fake .pkg installer. Inside: a self-healing LaunchAgent, an AES-encrypted V8 bytecode payload, and a Node.js MITM proxy trusted by a rogue...
npm Bin Entry Harvesting: A Dependency Confusion Blind Spot
21 malicious npm packages targeted Google by squatting CLI binary names from scoped packages, not package names. The technique exploits a structural gap that standard dependency confusion defenses do...
Ship Code.
Not Malware.
Start free with open source tools on your machine. Scale to a unified platform for your organization.