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 payloadThe 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 iloiyxo643@ufiwi.space and
redis-type-xyz from qmtijao982@ufiwi.site, 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 | ioredis-xyz@5.11.2 published, depending on redis-type-xyz@^1.10.5 |
| 07:56:44 | redis-type-xyz@1.10.6 published, adding the ulid-xyz dependency |
At 07:37 the chain was harmless. redis-type-xyz@1.10.5 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 | ioredis-xyz@5.11.1 published, no payload dependency |
| 2026-06-17 06:07 | redis-type-xyz@1.10.5 published, no payload dependency |
| 2026-06-17 07:37 | ioredis-xyz@5.11.2 adds redis-type-xyz@^1.10.5 |
| 2026-06-17 07:56 | redis-type-xyz@1.10.6 adds ulid-xyz@^2.12.2; the chain is armed |
| 2026-06-29 15:10 | ulid-xyz@2.12.3 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 | ioredis-xyz@5.11.2 |
| Relay package | redis-type-xyz@1.10.6, redis-type-xyz@2.1.1 |
| Payload package | ulid-xyz@2.12.2, ulid-xyz@2.12.3 |
| 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
DirtyBlanket: Fake Express Packages on npm Spread a Linux Worm
Nine fake Express and React packages on npm run a Linux worm at install time. It installs a Tor backdoor and spreads through SSH, AUR packages, and npm tokens.
Mini Shai-Hulud Is Still Infecting GitHub Repositories
The May 2026 Mini Shai-Hulud worm is still infecting new GitHub repositories. Hijacked actions-cool/issues-helper tags planted Claude Code and VS Code hooks in six popular repositories between 20 and...
Detecting Compromised AI Coding Agents with Jev and Gryph
I checked every action of my own Claude Code agent against a profile of how I work and a set of org policies, using Jev. It caught 14 of 14 attacks for $0.15 per 1,000 events.
MemTensor npm and PyPI Packages Hit by a Go Worm
An attacker used a Go worm to steal CI publish tokens from MemTensor and ship malicious MemOS packages to npm and PyPI. See how it works, with code and indicators of compromise.
Ship Code.
Not Malware.
Start free with open source tools on your machine. Scale to a unified platform for your organization.