# Remote Access Trojan (RAT)

> A remote access trojan (RAT) is malware disguised as legitimate software that gives an attacker persistent, interactive control of the machine running it.

Also known as: RATs, remote access trojans, remote administration trojan, RAT malware, remote access trojan malware

Source: https://socket.dev/glossary/remote-access-trojans-rats

Last updated: 2026-10-30

A remote access trojan (RAT), also called a remote administration trojan, is malware that hides inside software a victim chooses to run, then opens an outbound command channel so an attacker can operate the machine: run shell commands, read and upload files, and install more payloads. Developers now receive RATs through npm and PyPI packages.

## What is a remote access trojan?

A remote access trojan is a remote administration tool that the machine's owner never agreed to run. The "trojan" half describes delivery: the code arrives inside something the victim wants, such as a cracked game, a meeting app, or a logging library. The "remote access" half describes what it does once running: it registers the machine with attacker infrastructure and waits for instructions.

That waiting loop separates a RAT from its neighbors. An [infostealer](https://socket.dev/glossary/infostealer) grabs credentials and exits. A backdoor is a hidden way in. A RAT is a full agent with a command vocabulary, and the operator decides what to do after looking around the machine. SSH and TeamViewer do the same job with the owner's consent and a visible session. A RAT does it covertly.

The interactive session changes the incident response. After an infostealer runs, you rotate what it could read. After a RAT runs, the attacker could have done anything the user account could do, for as long as the session lived. The host is compromised, not just its secrets.

Our position: RATs are now the highest-impact payload in open source [malware](https://socket.dev/glossary/malware), because developer machines hold publish rights. [The axios compromise](https://socket.dev/blog/axios-npm-package-compromised) is the clearest case. The [axios post-mortem](https://github.com/axios/axios/issues/10636) states the root cause directly: "The attacker gained access to the lead maintainer's PC through a targeted social engineering campaign and RAT malware." The attacker then used that access to publish axios versions that installed a second RAT on every machine that pulled them. Axios has about 100 million weekly downloads on npm.

## How a RAT works: beacon, command table, staged payload

A RAT dials out; it does not listen. Home routers, corporate NAT, and cloud security groups drop unsolicited inbound connections, but nearly every network allows outbound HTTPS. So the implant starts every exchange. It sends a check-in, called a beacon, to its [command and control (C2)](https://socket.dev/glossary/command-and-control-c2) server. The response carries the next command, and the next beacon carries the result.

The beacon interval is the main design trade-off. A short interval gives the operator a responsive shell but leaves a regular, detectable rhythm in network logs. The [discordpydebug RAT on PyPI](https://socket.dev/blog/malicious-pypi-package-targets-discord-developers-with-RAT) polled its server every second and reached more than 11,000 downloads. The macOS RAT in the axios attack beaconed every 60 seconds. The RAT recovered from [the Joyfill compromise](https://socket.dev/blog/joyfill-npm-beta-releases-compromised) skipped polling and held an open Socket.IO connection, so its operator could push commands instantly.

Every RAT ships a command table, and the table tells you what the operator can do. Our analysis of the axios macOS second stage, a C++ binary, recovered four commands:

```text
peinject   receive a Base64 binary, write it to a hidden random file in /private/tmp, ad-hoc codesign it, execute it
runscript  run a shell command via /bin/sh, or a Base64 AppleScript via /usr/bin/osascript
rundir     enumerate a directory: names, sizes, timestamps, hierarchy
kill       terminate the implant
```

Four commands is plenty. `runscript` is arbitrary code execution, and `peinject` installs anything else. Google's Threat Intelligence Group named this implant WAVESHAPER.V2, called it "a fully functional RAT," and [attributed the axios attack](https://cloud.google.com/blog/topics/threat-intelligence/north-korea-threat-actor-targets-axios-npm-package/) to [UNC1069](https://socket.dev/blog/attackers-hunting-high-impact-nodejs-maintainers), a North Korea-nexus group active since 2018.

Package RATs are almost always staged. The code in the package is a small loader, and the RAT is fetched at run time. The axios dropper was a 4,209-byte `setup.js`. The Joyfill loader looked up a Tron or Aptos account, read an encrypted payload from a BNB Smart Chain transaction, and repeated the trick once more before reaching a 77,276-byte Node.js RAT. Staging keeps the published artifact small and lets the operator swap payloads without republishing. It also means the copy on the registry often contains no RAT at all, only instructions for fetching one.

## How do remote access trojans get into npm and PyPI packages?

Remote access trojans get into npm and PyPI packages in three ways: attackers publish lookalike packages, take over a real maintainer's account, or inject code into a legitimate build. For defenders, the entry route matters less than the trigger, the moment in a package's life when the loader first runs.

> Where a package-delivered RAT loader first runs, and which controls see it.
>
> Three points where a package-delivered RAT loader can first run: install time, import time, and call time, with the controls that stop each.

There are three triggers, and each one defeats a different control.

**Install time.** The loader runs from an [install script](https://socket.dev/glossary/postinstall-scripts) such as `postinstall`. The axios attack used this trigger: `plain-crypto-js@4.2.1` ran `node setup.js` from its `postinstall` hook. `npm install --ignore-scripts`, the [pnpm 10](https://socket.dev/blog/pnpm-10-0-0-blocks-lifecycle-scripts-by-default) default, and the [npm v12](https://socket.dev/blog/npm-12) `allowScripts` default all block it.

**Import time.** The loader runs as top-level module code the first time anything calls `require()` or `import`. Install-script controls never see it. The Joyfill beta releases of July 28, 2026, used this trigger, 20 days after npm v12 shipped on July 8, 2026, with install scripts off by default. Our Joyfill write-up is blunt: "`npm install --ignore-scripts` does not help here."

**Call time.** The loader sits behind an ordinary-looking function and runs only when the application calls it. The [Contagious Interview campaign](https://socket.dev/supply-chain-attacks/north-korea-s-contagious-interview-campaign), which Socket had linked to more than 1,700 malicious packages as of April 2026, hid loaders in a Rust logger's `trace()` method and a Python license helper's `find_by_key()` in [one five-ecosystem cluster](https://socket.dev/blog/contagious-interview-campaign-spreads-across-5-ecosystems). Neither install-script blocking nor reading module entry points catches this.

Only checks that run before the package lands apply to all three triggers: analyzing the package's code, and refusing known-malicious versions at the point of install.

You can watch an install-time control miss import-time code with a harmless local package:

```bash
mkdir demo-pkg app
cat > demo-pkg/package.json <<'EOF'
{ "name": "demo-pkg", "version": "1.0.0", "main": "index.js",
  "scripts": { "postinstall": "node -e \"console.log('[postinstall] ran')\"" } }
EOF
echo "console.log('[import] demo-pkg top-level code ran');" > demo-pkg/index.js
(cd demo-pkg && npm pack --silent)
cd app && npm init -y >/dev/null
npm install --ignore-scripts ../demo-pkg/demo-pkg-1.0.0.tgz
node -e 'require("demo-pkg")'
```

```text
added 1 package, and audited 2 packages in 452ms
[import] demo-pkg top-level code ran
```

The postinstall line never prints. The import line always does. Replace the `console.log` with fetch-and-eval and you have the execution model of the Joyfill loader. Default-on install scripts were a design mistake, and turning them off is now table stakes. Attackers have already moved to the next trigger.

## Six package RATs, compared

We compared six RAT deliveries that our research team has published on, from 2024 to 2026. They include a commodity RAT, [Quasar RAT disguised as an npm package](https://socket.dev/blog/quasar-rat-disguised-as-an-npm-package), and a targeted one, the [npm cluster aimed at Alibaba developers](https://socket.dev/blog/npm-rat-targets-alibaba). The table lists how each one ran, how it talked home, and where it hid.

| Package (ecosystem, date) | Trigger | Channel | Capability highlights | Persistence |
| --- | --- | --- | --- | --- |
| ethereumvulncontracthandler (npm, Dec 2024) | Install | Quasar RAT C2 on port 7000 | Keylogging, screenshots, credential theft | Registry Run key, binary renamed client.exe |
| discordpydebug (PyPI, published 2022, reported May 2025) | Call: run() and debug() | HTTP polling every 1 s | readfile, writefile, shell | None |
| plain-crypto-js via axios 1.14.1 and 0.30.4 (npm, Mar 2026) | Install: postinstall | HTTP POST beacon every 60 s | peinject, runscript, rundir, kill | /Library/Caches/com.apple.act.mond on macOS |
| license-utils-kit and siblings (PyPI, npm, Go, crates.io, Packagist; Apr 2026) | Call: logger and helper methods | Server returns a download URL for a staged ZIP | Remote shell, keylogging, browser and wallet theft | Not detailed; payloads named com.apple.systemevents and systemd-resolved |
| @joyfill/layouts 0.1.2-2773.beta.0 (npm, Jul 2026) | Import: module load | Socket.IO; stages resolved through blockchain transactions | ss\_eval, ss\_upf, ss\_cb (clipboard), shell | Code injected into VS Code, Cursor, Discord, GitHub Desktop, and the global npm CLI |
| local-config-parser and smart-config-manager cluster (npm, published Apr 2026, reported Jul 2026) | Load: auto-initialization code | Hardcoded C2 plus encrypted reverse TCP proxy | Unknown commands go to the OS shell; DingTalk lateral movement | Patches Python files in AI tools' .skills directories |

Three patterns stand out. At most two of the six ran at install time. The command sets are small, because shell access makes everything else redundant. And the two most recent RATs persist inside developer tools, not inside `node_modules`.

That last pattern is the one most teams miss. The Joyfill RAT inserts a self-reloading block into `lib/cli.js` of the global npm install, so it runs again every time someone types `npm`. Deleting the malicious package, regenerating the lockfile, and reinstalling leaves that file untouched.

## The axios compromise, minute by minute

The npm registry keeps publish timestamps after it removes a version. Querying the axios packument on October 30, 2026, still returns both malicious releases in its `time` map, though they are gone from `versions`:

```bash
npm view axios time --json | grep -E '"(0\.30\.[34]|1\.14\.[01])"'
npm view plain-crypto-js time --json
```

```json
  "0.30.3": "2026-02-18T17:19:20.081Z",
  "1.14.0": "2026-03-27T19:01:40.915Z",
  "1.14.1": "2026-03-31T00:21:58.168Z",
  "0.30.4": "2026-03-31T01:00:57.285Z",

  "4.2.0": "2026-03-30T05:57:32.867Z",
  "4.2.1": "2026-03-30T23:59:12.278Z",
  "0.0.1-security": "2026-03-31T03:25:11.636Z",
```

Combined with our detection log and the maintainer's post-mortem, those timestamps give the sequence:

> The axios compromise, from npm registry time data, Socket's detection log, and the axios post-mortem. Times in UTC.
>
> Timeline of the axios compromise, March 30 to 31, 2026, in UTC: clean plain-crypto-js 4.2.0 at 05:57, malicious 4.2.1 at 23:59, Socket flags it at 00:05, axios 1.14.1 at 00:21, axios 0.30.4 at 01:00, npm security placeholder for plain-crypto-js at 03:25.

| Time (UTC) | Event |
| --- | --- |
| Mar 30, 05:57:32 | plain-crypto-js@4.2.0 published: a clean copy of crypto-js |
| Mar 30, 23:59:12 | plain-crypto-js@4.2.1 published with a postinstall dropper |
| Mar 31, 00:05:41 | Socket flags plain-crypto-js@4.2.1 as malware |
| Mar 31, 00:21:58 | axios@1.14.1 published with plain-crypto-js as a new dependency |
| Mar 31, 01:00:57 | axios@0.30.4 published, 39 minutes later |
| Mar 31, 03:25:11 | npm replaces plain-crypto-js with a security placeholder; the axios versions came down after about three hours live |

The order matters. The attacker published a clean `plain-crypto-js@4.2.0` 18 hours early, so the name had a history. The malicious 4.2.1 went up 23 minutes before the first axios release. Socket's automated malware detection flagged 4.2.1 at 00:05:41 UTC, 16 minutes before `axios@1.14.1` existed. Anyone who installed the bad axios releases was pulling a dependency already marked as malware. The malicious axios versions were live for about three hours, according to the post-mortem.

The dropper also covered its tracks. After launching the second stage, `setup.js` deleted itself and renamed a bundled `package.md` to `package.json`, restoring a clean 4.2.0 manifest with no `postinstall` field. Anyone who inspected `node_modules/plain-crypto-js` afterward saw an ordinary copy of crypto-js.

Exposure reached beyond projects that list axios as a dependency. Most packages declare axios with a caret range, so any fresh resolution during the window picked up 1.14.1. As we showed in [the hidden blast radius of the axios compromise](https://socket.dev/blog/hidden-blast-radius-of-the-axios-compromise), tools run through `npx`, global installs, and new CI images resolved against the live registry, and none of that shows up in a project lockfile. A clean lockfile today is not evidence you were safe in March.

## How do you detect a remote access trojan?

You detect a remote access trojan in a dependency at two points: before it runs, by reading what the package code does, and after it runs, by finding its beacon and its persistence. The second is the classic endpoint problem. The first exists only in the supply chain, and it is where we focus.

**Before execution.** A package-delivered RAT must do things in code that a logger or a crypto helper has no reason to do. It opens network connections to hardcoded hosts, spawns shells or child processes, decodes and evaluates remote content, and hides those strings behind obfuscation. Socket reports these behaviors as alerts, including [Network access](https://socket.dev/alerts/networkAccess), [Shell access](https://socket.dev/alerts/shellAccess), [Uses eval](https://socket.dev/alerts/usesEval), and [Obfuscated code](https://socket.dev/alerts/obfuscatedFile), and marks confirmed versions as [Known malware](https://socket.dev/alerts/malware). The limit: staged loaders keep the RAT off the registry, so analysis often sees only the loader, not the implant. A loader that reads its next stage from a blockchain transaction can look like ordinary web3 code until someone traces the decode-and-eval.

Blocking at install time turns a detection into protection. A [package firewall](https://socket.dev/glossary/package-firewall) such as [Socket Firewall](https://docs.socket.dev/docs/socket-firewall-free) sits between the package manager and the registry and refuses artifacts already confirmed as malware, including transitive ones. It does not block versions nobody has analyzed yet, and it cannot block a tarball already in your local cache.

**After execution.** These [indicators of compromise](https://socket.dev/glossary/indicators-of-compromise-ioc) are worth hunting on hosts and in network logs:

- **Beacon rhythm.** Fixed-interval outbound requests to one host, especially from a build agent. The axios RAT sent an Internet Explorer 8 on Windows XP User-Agent from a Mac, which stands out in any proxy log.
- **Unexpected process trees.** `node` or `python` spawning `curl`, `osascript`, `cscript`, or a detached `node -e`. On Windows, the axios dropper copied `powershell.exe` to `%PROGRAMDATA%\wt.exe` to pass as Windows Terminal.
- **Odd destinations.** Blockchain RPC endpoints contacted from a CI runner, which our Joyfill write-up calls "a high-fidelity signal on its own."
- **Persistence in developer tools.** Modified files inside editors, desktop apps, and the global npm CLI.

```bash
# axios macOS second stage
ls -la /Library/Caches/com.apple.act.mond 2>/dev/null

# Joyfill persistence markers injected into the global npm CLI
NPM_PKG=$(dirname "$(dirname "$(readlink -f "$(command -v npm)")")")
grep -lE '/\*(C2506(1[7-9]|20)A|C26051[12]A|RS260605)\*/' "$NPM_PKG/lib/cli.js" \
  || echo "no Joyfill marker in npm CLI"

# Alibaba-targeting cluster marker in Python files
grep -rl '# __INJECT_MARKER__' ~ --include='*.py' 2>/dev/null | head
```

Each check catches one family and misses the next. The Joyfill RAT also refuses to run on hostnames such as `github-runner` and `buildbot`, so a sandbox that looks like CI may never see it execute.

## Removing the package does not remove the RAT

A RAT that has run is an incident on the host, not a bug in a dependency. Treat every machine that installed or imported the package as compromised, and work in a fixed order. Isolate the host and preserve logs. Find and disable persistence before you touch credentials, because some implants watch for revocation. The payload in the [TensorLake compromise](https://socket.dev/blog/tensorlake-compromise) polled a stolen GitHub token, and revoking that token while its monitor ran would trigger a dead-man switch that wipes the user's home directory, no matter where the revocation happened. Then rotate every credential the account could reach, from a separate clean machine, since a live RAT can watch what you type on the infected one. Then rebuild.

Uninstalling the package, deleting `node_modules`, and running `npm ci` restores your project, not your machine.

Strong authentication does not protect a maintainer whose laptop runs a RAT. Security researcher Tay, quoted in our report on the campaign against Node.js maintainers, put it this way: "When you have a RAT on your device, it grabs your post-authentication state, making 2FA irrelevant." [Our report on the axios maintainer's account](https://socket.dev/blog/axios-maintainer-confirms-social-engineering-behind-npm-compromise) reached the same conclusion: nothing in the publishing pipeline, [trusted publishing](https://socket.dev/glossary/npm-trusted-publishing) and 2FA included, would have stopped a release made with the maintainer's own access.

That is why we treat [maintainer account takeover](https://socket.dev/glossary/maintainer-account-takeover) and RATs as one problem. Provenance and signatures prove which account or pipeline published a version. They cannot prove that the account's owner was the one at the keyboard. Until registries can tell those apart, the dependency tree is only as secure as a small number of maintainers' laptops, and the check that scales is reading what each new version does before it runs.
