---
title: "A malicious npm package hidden three dependencies deep: the ulid-xyz delivery chain"
description: "ulid-xyz is a remote access trojan we reported to OSV as MAL-2026-6672. It reached developers as a third-level transitive dependency of ioredis-xyz, a copy of the real Redis client planted in 28 attacker-created GitHub repositories. The implant matches the DPRK-linked MicrosoftSystem64 cluster."
url: "https://safedep.io/blog/ulid-xyz-transitive-dependency-delivery-chain"
author: "Kunal Singh"
published: "2026-09-01"
---

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

## 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](https://osv.dev/vulnerability/MAL-2026-6672).

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

```text showLineNumbers=false wrap=false
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
```

## 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](https://osv.dev/vulnerability/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](https://osv.dev/vulnerability/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:

```json
"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:

```javascript
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](https://safedep.io/blog/microsoftsystem64-binary-payload-analysis), 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: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                            |
