Last week, 318 malicious npm packages appeared on the public registry. They were squatting on the private npm scopes organisations use internally. The packages were dressed up as telemetry. The user-agents read <scope>-telemetry/1.0. The kill switch was called _NO_TELEMETRY. The attacker wanted anyone who glanced at outbound traffic to keep scrolling.
The C2 domain was oob[.]moika[.]tech.
Ossprey's detection engine flagged all 318 packages within seconds of publication. This post covers how the attack worked, what it stole, and what the signals look like.

The Dependency Confusion Mechanic
Dependency confusion exploits a design decision in npm and most other package managers: when a resolver is configured to check both a private registry and the public one, it picks whichever has the higher version number. An internal package @bcs-payments/auth living at 1.4.2 in a private registry will lose to a public package of the same name at 99.99.99.
That is exactly how this campaign worked. The attacker identified scoped package names used internally by their targets - derived from public artefacts, job postings, or prior reconnaissance - registered those scopes on npm, and published packages at absurd version numbers: 99.99.99, 100.100.100, 11.11.11, and 99.0.x for later variants. When a developer or CI pipeline ran npm install, the resolver picked the attacker's version and the lifecycle hook ran.
We confirmed every package in the inventory carries a preinstall or postinstall hook. No user interaction is required beyond a dependency install.
Two Variants
Pulling the install scripts from the source packages, we found two distinct payload types across the 318 packages.
The Beacon (271 of the 318) is the lighter variant. It reads the package name from the npm_package_name environment variable, derives the scope, and contacts http://{pkg}.{scope}.oob[.]moika[.]tech/poc.js. Whatever the server sends back, the beacon runs with eval(). There is no payload baked into the package itself - it is a remote execution stub with a per-package callback URL so the operator can see exactly which victim triggered it and serve a tailored response.
The Dropper (47 packages) is the full payload. It collects system information immediately - hostname, OS, Node version, complete process.env - then spawns a detached child process that runs independently of the npm install session. That child downloads the stage-1 loader from oob[.]moika[.]tech/payload/{os}.js (authenticated with an X-Secret header), writes it to a temp file, and executes it. If the download fails, it still reports what it gathered to oob[.]moika[.]tech/report. The dropper variant includes a 3-second startup sleep, annotated in the source as "обходит sandbox" - "bypasses sandbox".
What Stage 1 Collects
Deobfuscating the stage-1 loader revealed a 400-line JavaScript credential harvester behind the obfuscator.io base64 + RC4 string array. It sweeps a fixed list of paths and patterns covering:
SSH private keys (
~/.ssh/) and authorised keysAWS credentials (
~/.aws/credentials, EC2 IMDS)GCP service account JSON and metadata endpoint
Azure CLI credentials
Kubernetes
~/.kube/configand in-cluster service account tokensDocker credentials and daemon socket
CI/CD configs (Jenkins, GitLab Runner, Ansible)
.envvariants,.npmrc,.pypirc,composer auth.jsonPrivate key files by extension (
.pem,.key,.pfx,.p12,.jks)Shell history files
Windows: PuTTY sessions and SSH host keys, Winlogon credentials, SNMP community strings
Everything collected is POSTed to oob[.]moika[.]tech/report. Beyond credential exfil, the loader also injects itself into .bashrc and .profile - persistence that survives stage-2 removal.
After collection, stage 1 downloads the stage-2 binary from oob[.]moika[.]tech/bins/{os}/{os}, validates the ELF header, writes it to ~/.cache/._kworker on Linux or %AppData%\Roaming\Microsoft\Windows\WinUpdate.exe on Windows, and runs it.
Stage 2: The RAT
We identified the stage-2 binary as a Go reverse-SSH implant compiled with garble, which strips and scrambles Go metadata including the pclntab magic bytes to obstruct analysis. On Windows it bundles winpty for interactive terminal support; the build artefact path in the binary reveals the development username mail (C:\Users\mail\source\winpty).
The implant connects to 141[.]98[.]189[.]248:32322/ws over WebSocket (subprotocol ssh) using a self-signed TLS certificate with O="Cloudflare, Inc", which passes a cursory glance but fails any pinning or transparency check. Capabilities include:
Interactive PTY shell
SFTP file operations
SOCKS5 proxy tunnelling
NTLM proxy authentication for corporate network egress
NTLM proxy support is a specific design choice. NTLM is the authentication scheme used in Windows domain environments. The attacker built egress support for exactly the kind of corporate network their targets run.
On Linux the implant persists as a systemd --user service named dbus-broker.service, a common system service name, and adds a @reboot crontab entry as a fallback. On Windows it registers under HKCU\...\Run as MicrosoftUpdateService and creates a scheduled task triggering on logon.
Infrastructure and Attribution Signals
Tracing the C2 infrastructure, we found deliberate preparation well ahead of the campaign. moika[.]tech nameservers were set to ns1.reg.ru and ns2.reg.ru, the largest Russian domain registrar. Certificate Transparency logs show the subdomains admin[.]moika[.]tech, erp-dev[.]moika[.]tech, and work[.]moika[.]tech appearing on 2026-03-14, 75 days before the first packages were published. The attack infrastructure was live and tested for over two months before any victim was exposed.
Stage-0/1 C2 (oob[.]moika[.]tech, 72[.]56[.]97[.]200) sits on Timeweb, a Russian cloud provider, though the specific VPS is in Amsterdam. Stage-2 C2 (141[.]98[.]189[.]248:32322) sits on UFO Hosting LLC (AS33993) in Moscow.
One detail in the stage-2 binary cuts against simple attribution: the RAT exits if the system locale starts with ru. An operator targeting Russian financial institutions built a safeguard to avoid running on Russian-locale machines. The most common explanations are protecting contractor infrastructure, operational security against self-compromise, or a deliberate false flag.
The Targets
Looking at the scope names, we found they mirror naming conventions.
@bcs-*- BCS Bank, one of Russia's largest brokerages@car-loans/*- consumer lending platforms@cloudplatform-single-spa/*- 41 packages, the largest single family; single-spa is a micro-frontend framework common in enterprise banking portals@activation_code/*,@apple-pay-trust/*,@google-pay-trust/*- payment processor integration layers@polka-ui/*,@service-suppliers/*- internal UI component libraries
IOCs
Defanged for safe distribution.
Network
Indicator | Type | Stage |
|---|---|---|
| C2 hostname/IP | Stage 0/1 |
| C2 IP:port | Stage 2 RAT |
| Exfil host | Env-theft variant |
| Related subdomains | Infrastructure |
C2 Endpoints
Strings / Config
Persistence Paths
File Hashes (SHA-256)
File | SHA-256 |
|---|---|
|
|
|
|
|
|
|
|
|
|
MITRE ATT&CK
Selected techniques from the MITRE ATT&CK framework observed in this campaign:
ID | Technique | Notes |
|---|---|---|
T1195.002 | Supply Chain Compromise | Dependency confusion, version inflation |
T1059.007 | JavaScript | npm lifecycle hooks ( |
T1027 | Obfuscated Files | obfuscator.io (stage-1); garble (stage-2) |
T1497.003 | Time-Based Evasion | 3-second startup delay |
T1552.001 | Credentials In Files | .env, .npmrc, .pypirc, cloud credential files |
T1552.004 | Private Keys | SSH keys, .pem/.key/.pfx sweep |
T1552.005 | Cloud Instance Metadata | AWS IMDS, GCP metadata |
T1572 | Protocol Tunneling | SSH over WebSocket |
T1090 | Proxy | SOCKS5 + NTLM proxy auth |
T1543.002 | Systemd Service |
|
T1547.001 | Registry Run Keys |
|
T1614.001 | System Language Discovery | RAT exits if locale starts with |
FAQ
Was my organisation affected? If your private npm registry uses any of the 40+ scopes listed in the IOC table and you ran npm install between May 27-29 2026, check your install logs for outbound connections to oob[.]moika[.]tech. Any connection is a confirmed execution of the lifecycle hook.
What is the difference between the beacon and dropper variants? The beacon (271 packages) only calls home and eval()s whatever the server returns. The dropper (47 packages) runs a full credential sweep locally and downloads the stage-2 RAT. Both execute on npm install with no other user action required.
How do I check if a package I installed was malicious? SHA-256 hashes for the stage-1 loaders and stage-2 RAT binaries are in the IOC table. Match against artefacts in your install cache or any binaries found at the persistence paths listed.

Real World Attacks

Megalodon: Active GitHub Actions Supply Chain Attack Harvesting CI/CD Secrets at Scale

Ossprey Research Team
21 May 2026
Real World Attacks

The Complete TeamPCP Campaign

Valentino Duval
21 May 2026
Real World Attacks

Mini Shai-Hulud spreads to Microsoft's durabletask PyPi packages

Valentino Duval
20 May 2026
