diff --git a/src/pentesting-ci-cd/github-security/abusing-github-actions/gh-actions-npm-supply-chain-abuse.md b/src/pentesting-ci-cd/github-security/abusing-github-actions/gh-actions-npm-supply-chain-abuse.md index 41e7f07b03..89a0f54c91 100644 --- a/src/pentesting-ci-cd/github-security/abusing-github-actions/gh-actions-npm-supply-chain-abuse.md +++ b/src/pentesting-ci-cd/github-security/abusing-github-actions/gh-actions-npm-supply-chain-abuse.md @@ -29,6 +29,8 @@ The classic npm route is to publish a malicious package version with `preinstall ChainDrop kept legitimate package functionality and added a small `setup.mjs` loader for a larger obfuscated payload. Its loader bootstrapped Bun when needed, used an environment marker to avoid recursive execution, detached on workstations, and ran inline in CI, leaving its debug output in the workflow log.[[13]](#references) +A staging loader does not need to ship its final runtime. One observed loader accepted any executable named `bun` that made `bun --version` succeed; otherwise it downloaded a platform-specific archive, followed cross-origin redirects, imposed no response-size limit, and executed the extracted runtime without verifying a digest or signature. It then inherited the installer's working directory, environment, and standard streams. Review both the package payload and the resulting process/network chain: `npm -> node setup.mjs -> bun ` is materially different from ordinary Node-only package execution.[[18]](#references) + Defenders often monitor these scripts, so red-team reviews should also inspect less obvious execution paths.[[11]](#references) ### `binding.gyp` / node-gyp execution (Phantom Gyp) @@ -92,6 +94,8 @@ cat /proc/$PID/maps | head Memory scraping can expose short-lived secrets that never reach disk and may disappear when the job ends.[[13]](#references) +A real collector gated this path on Linux, `GITHUB_ACTIONS=true`, and working `sudo` access. It streamed an embedded Python reader into `sudo python3`, removed null bytes from readable `Runner.Worker` mappings, and parsed serialized records marked `isSecret: true`. This means "the secret was never exported by the malicious step" is not sufficient scoping when the same runner exposes another process's memory.[[18]](#references) + ## Workflow-Assisted Exfiltration With a write-capable GitHub identity, an attacker can add a workflow that serializes the `secrets` context and stores it as a workflow artifact. This can avoid a direct connection to an external C2 when GitHub traffic remains allowed.[[13]](#references)[[14]](#references) @@ -108,7 +112,9 @@ steps: path: secrets.json ``` -Review unexpected `.github/workflows/*.yml` additions, especially files that call `toJSON(secrets)` or upload artifacts.[[13]](#references)[[14]](#references) +GitHub's Actions secrets APIs expose names and metadata, not plaintext values. A write-capable classic token with the `workflow` scope can therefore use those names to choose a repository, create a temporary branch and workflow, run the workflow above, download the artifact, and delete the run and branch. Only secrets made available to that job are recovered; environment protection can still withhold environment-scoped secrets, and cleanup can leave audit, workflow-run, and artifact evidence.[[18]](#references) + +Review unexpected `.github/workflows/*.yml` additions, especially files that call `toJSON(secrets)` or upload artifacts.[[13]](#references)[[14]](#references)[[18]](#references) ## Wormable npm Publishing @@ -121,6 +127,8 @@ Once code runs in a maintainer workstation or release workflow, a single stolen ChainDrop downloaded or reconstructed each writable package while preserving its expected functionality, added the loader to `preinstall`, bumped the patch version, and republished it so later installs generated more victims.[[13]](#references) +In the direct-token propagation path, the implant first matched the token against npm's token API and required package-write permission plus `bypass_2fa: true`. It downloaded each writable package's latest tarball, copied the loader and second stage into it, **replaced the complete `scripts` object** with `{"preinstall":"node setup.mjs"}`, incremented the patch version, and published with the stolen token. This path generated no provenance; wholesale disappearance of legitimate build/test scripts is therefore a useful tarball-diff signal as well as a likely functional breakage.[[18]](#references) + Useful enumeration from a compromised npm identity:[[7]](#references) ```bash @@ -131,6 +139,26 @@ npm access ls-collaborators Attackers usually prefer packages with frequent CI installs, transitive popularity, or release automation that will install the malicious version quickly. +## Repository-to-Workstation Propagation + +Repository write access can extend the infection beyond package installation by planting developer-tool execution hooks across branches. One worm wrote the loader and payload into both `.vscode/` and `.claude/`; a VS Code folder-open task launched `node .claude/setup.mjs`, while a Claude `SessionStart` hook launched `node .vscode/setup.mjs`. Opening the repository or starting a supported assistant session could therefore retrigger the loader on a maintainer workstation even if no package install occurred.[[18]](#references) + +High-signal review targets include unexpected `.vscode/tasks.json`, `.claude/settings.json`, copied JavaScript blobs on many branches, and GitHub-signed bot commits that do not correspond to an approved automation change. A valid GitHub signature proves which GitHub identity produced a commit; it does not prove the committed content is benign.[[18]](#references) + +## Dead-Drop C2 and GitHub Exfiltration + +A supply-chain implant can avoid a fixed C2 hostname by resolving infrastructure at runtime. One implementation queried many public Ethereum RPC endpoints with `eth_call`, decoded an ABI dynamic string array from a contract, and selected the first returned domain whose HTTPS `/router` endpoint produced an expected status. If contract lookup failed, it searched GitHub commit messages for a marker carrying a Base64 domain and RSA/SHA-256 signature.[[18]](#references) + +```text +contract = 0xE1f2395ee43e45A1556EC6438a88c31B83493103 +selector = 0x53ed5143 +probe = https://:443/router +``` + +The collected JSON was gzip-compressed, encrypted with a random AES-256-GCM key and IV, and sent with the AES key wrapped by embedded RSA-OAEP/SHA-256. However, encryption of outbound data did not authenticate inbound commands: after HTTP 200, the implant parsed JSON and evaluated a `code` property without a signature or sandbox. Control of a contract-selected HTTPS endpoint therefore became arbitrary JavaScript execution in the Bun process; TLS authenticated the server, not the safety of its response.[[18]](#references) + +When no primary endpoint was available, the implant used a stolen classic GitHub token to create a public repository and upload Base64 result files through the Contents API, splitting objects larger than 30 MiB. It could also place a twice-Base64-encoded token in a public commit beside a distinctive marker, then search public commits and reuse tokens retaining `repo` or `public_repo` scope. During response, review newly created public repositories, repository descriptions, commit-search markers, and large or multipart files under unexpected `results/` paths.[[18]](#references) + ## Trusted Publishing and Provenance Limits Trusted publishing/OIDC removes long-lived static npm tokens, but it does not make a compromised release workflow safe. If attacker-controlled code runs inside a workflow already authorized as an npm trusted publisher and granted `id-token: write`, the job can mint a GitHub OIDC token and exchange it for an npm publishing credential; the malicious release can also receive valid provenance because the legitimate workflow built and published it.[[1]](#references)[[2]](#references)[[8]](#references)[[13]](#references)[[15]](#references) @@ -148,6 +176,18 @@ High-signal review points: - Package diffs that add a dependency imitating the maintainer's own scope or an internal helper name.[[13]](#references) - Provenance that verifies correctly but points to a workflow run or workflow file that executed attacker-controlled code.[[13]](#references)[[15]](#references) +## Incident Reconstruction and Hunting + +Do not use a GitHub Release object as the publication boundary. In the observed campaign, npm publication preceded creation and publication of the corresponding GitHub Release, while tags were repeatedly deleted and recreated. Correlate registry publication timestamps with branch/ref activity, exact Git object IDs, Actions run IDs, package tarball contents, and Rekor provenance; signed bot commits and valid provenance can coexist with malicious source.[[18]](#references) + +Useful behavior-level signals from the same chain include:[[18]](#references) + +- `npm` spawning `node setup.mjs`, followed by Bun execution or a Bun release download into a `bun-dl-` temporary directory. +- A detached Bun child with `_NODE_RUNTIME_INIT=1`, ignored standard streams, and a temporary lock named `tmp.dpkg_14527.lock`. +- A package process invoking `gh auth token`, enumerating many cloud regions, reading `/proc//mem` through `sudo python3`, or contacting many Ethereum RPC endpoints before HTTPS traffic to `/router`. +- Cross-branch additions of `.vscode/tasks.json`, `.claude/settings.json`, `setup.mjs`, or identical large obfuscated JavaScript blobs. +- Temporary workflow branches, `toJSON(secrets)`, artifact upload/download followed by run deletion, or creation of an unrelated public repository through a maintainer token. + ## Hardening - Use trusted publishing/OIDC instead of static npm tokens, but pair it with protected environments and human approval for sensitive scopes.[[2]](#references)[[3]](#references) @@ -158,6 +198,8 @@ High-signal review points: - Disable or tightly review lifecycle scripts in CI (`npm config set ignore-scripts true`) where builds do not need them.[[1]](#references)[[6]](#references)[[10]](#references) - Monitor package access (`npm access ls-packages`) and remove stale maintainers, bots, and teams.[[7]](#references)[[12]](#references) - Treat new workflow files and artifact uploads as part of the package-publishing attack surface, not only as GitHub-side persistence.[[13]](#references)[[14]](#references) +- Review repository-scoped developer automation files such as `.vscode/tasks.json` and `.claude/settings.json` as executable supply-chain inputs, and protect non-default release branches as rigorously as `main`.[[18]](#references) +- Monitor unexpected runtime bootstrap downloads and package-manager child processes rather than limiting detections to Node.js internals; block release jobs from fetching unpinned runtimes where the build does not require them.[[18]](#references) ## References @@ -178,5 +220,6 @@ High-signal review points: - [15] [Generating provenance statements | npm Docs](https://docs.npmjs.com/generating-provenance-statements/) - [16] [Malicious key-derivation telemetry commit in InjectiveLabs/injective-ts](https://github.com/InjectiveLabs/injective-ts/commit/01219285b16ce85c70cdf47a71a551ff5e41f1ed) - [17] [Not-So-Anonymous Telemetry: The @injectivelabs/sdk-ts Backdoor](https://securitylabs.datadoghq.com/articles/not-so-anonymous-telemetry-injectivelabs-sdk-ts-backdoor) +- [18] [NPM Worm Compromises Hundreds of Popular npm Packages](https://securitylabs.datadoghq.com/articles/npm-worm-compromises-popular-npm-packages) {{#include ../../../banners/hacktricks-training.md}}