You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: src/pentesting-ci-cd/github-security/abusing-github-actions/README.md
+85-2Lines changed: 85 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -464,6 +464,21 @@ If other repositories where using **dependencies from this user repos**, an atta
464
464
465
465
GitHub Actions still encourages consumers to reference `uses: owner/action@v1`. If an attacker gains the ability to move that tag—through automatic write access, phishing a maintainer, or a malicious control handoff—they can retarget the tag to a backdoored commit and every downstream workflow executes it on its next run. The reviewdog / tj-actions compromise followed exactly that playbook: contributors auto-granted write access retagged `v1`, stole PATs from a more popular action, and pivoted into additional orgs.
466
466
467
+
This becomes even more useful when the attacker **force-pushes many existing tags at once** (`v1`, `v1.2.3`, `stable`, etc.) instead of creating a new suspicious release. Downstream pipelines keep pulling a "trusted" tag, but the referenced commit now contains attacker code.
468
+
469
+
A common stealth pattern is to place the malicious code **before** the legitimate action logic and then continue executing the normal workflow. The user still sees a successful scan/build/deploy, while the attacker steals secrets in the prelude.
470
+
471
+
Typical attacker goals after tag poisoning:
472
+
473
+
- Read every secret already mounted in the job (`GITHUB_TOKEN`, PATs, cloud creds, package-publisher tokens).
474
+
- Drop a **small loader** in the poisoned action and fetch the real payload remotely so the attacker can change behavior without re-poisoning the tag.
475
+
- Reuse the first leaked publisher token to compromise npm/PyPI packages, turning one poisoned GitHub Action into a wider supply-chain worm.
476
+
477
+
**Mitigations**
478
+
479
+
- Pin third-party actions to a **full commit SHA**, not a mutable tag.
480
+
- Protect release tags and restrict who can force-push or retarget them.
481
+
- Treat any action that both "works normally" and unexpectedly performs network egress / secret access as suspicious.
467
482
468
483
---
469
484
@@ -650,6 +665,16 @@ jobs:
650
665
651
666
Tip: for stealth during testing, encrypt before printing (openssl is preinstalled on GitHub-hosted runners).
652
667
668
+
- GitHub log masking only protects rendered output. If the runner process already holds plaintext secrets, an attacker can sometimes recover them directly from the **runner worker process memory**, bypassing masking entirely. On Linux runners, look for `Runner.Worker` / `runner.worker` and dump its memory:
The same idea applies to procfs-based memory access (`/proc/<pid>/mem`) when permissions allow it.
677
+
653
678
### Systematic CI token exfiltration & hardening
654
679
655
680
Once an attacker’s code executes inside a runner, the next step is almost always to steal every long-lived credential in sight so they can publish malicious releases or pivot into sibling repos. Typical targets include:
@@ -668,6 +693,39 @@ With a single leaked credential the attacker can retag GitHub Actions, publish w
668
693
- Disable npm lifecycle hooks in CI (`npm config set ignore-scripts true`) so compromised dependencies can’t immediately run exfiltration payloads.
669
694
- Scan release artifacts and container layers for embedded credentials before distribution, and fail builds if any high-value token materializes.
If an attacker steals a publisher token from CI, the fastest follow-up is often to publish a malicious package version that executes **during install** or **at interpreter startup**:
699
+
700
+
- **npm**: add `preinstall` / `postinstall` to `package.json` so `npm install` executes attacker code immediately on developer laptops and CI runners.
701
+
- **Python**: ship a malicious `.pth` file so code runs whenever the Python interpreter starts, even if the trojanized package is never explicitly imported.
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).
720
+
721
+
#### Alternate exfil when outbound traffic is filtered
722
+
723
+
If direct exfiltration is blocked but the workflow still has a write-capable `GITHUB_TOKEN`, the runner can abuse GitHub itself as the transport:
724
+
725
+
- Create a private repository inside the victim org (for example, a throwaway `docs-*` repo).
726
+
- Push stolen material as blobs, commits, releases, or issues/comments.
727
+
- Use the repo as a fallback dead-drop until network egress returns.
728
+
671
729
### AI Agent Prompt Injection & Secret Exfiltration in CI/CD
672
730
673
731
LLM-driven workflows such as Gemini CLI, Claude Code Actions, OpenAI Codex, or GitHub AI Inference increasingly appear inside Actions/GitLab pipelines. As shown in [PromptPwnd](https://www.aikido.dev/blog/promptpwnd-github-actions-ai-agents), these agents often ingest untrusted repository metadata while holding privileged tokens and the ability to invoke `run_shell_command` or GitHub CLI helpers, so any field that attackers can edit (issues, PRs, commit messages, release notes, comments) becomes a control surface for the runner.
@@ -732,6 +790,32 @@ The way to find which **Github Actions are being executed in non-github infrastr
732
790
733
791
**Self-hosted** runners might have access to **extra sensitive information**, to other **network systems** (vulnerable endpoints in the network? metadata service?) or, even if it's isolated and destroyed, **more than one action might be run at the same time** and the malicious one could **steal the secrets** of the other one.
734
792
793
+
They also frequently sit close to container build infrastructure and Kubernetes automation. After initial code execution, check for:
794
+
795
+
- **Cloud metadata** / OIDC / registry credentials on the runner host.
796
+
- **Exposed Docker APIs** on `2375/tcp` locally or on adjacent builder hosts.
797
+
- Local `~/.kube/config`, mounted service-account tokens, or CI variables containing cluster-admin credentials.
798
+
799
+
Quick Docker API discovery from a compromised runner:
800
+
801
+
```bash
802
+
for h in 127.0.0.1 $(hostname -I); do
803
+
curl -fsS "http://$h:2375/version" && echo "[+] Docker API on $h"
804
+
done
805
+
```
806
+
807
+
If the runner can talk to Kubernetes and has enough privileges to create or patch workloads, a malicious **privileged DaemonSet** can turn one CI compromise into cluster-wide node access. For the Kubernetes side of that pivot, check:
In self-hosted runners it's also possible to obtain the **secrets from the \_Runner.Listener**\_\*\* process\*\* which will contain all the secrets of the workflows at any step by dumping its memory:
736
820
737
821
```bash
@@ -817,7 +901,6 @@ An organization in GitHub is very proactive in reporting accounts to GitHub. All
0 commit comments