Wiz verfolgt die Aktivität seit dem 15. September und nennt sie Gheist, nach dem User-Agent „gheist", den das Werkzeug der Angreifer bei Anfragen an die GitHub-API mitsendet. Aus mehr als 850 kompromittierten Konten hat Wiz über 13.500 eingeschleuste Branches rekonstruiert — von privaten Hobbyprojekten bis zu privaten Repositories von Unternehmensorganisationen. Wie viele Organisationen tatsächlich Secrets verloren haben, geht aus der Analyse nicht hervor.

Grundlage des Angriffs ist eine dokumentierte Eigenschaft von GitHub: Wer Schreibzugriff auf ein Repository hat, kann laut GitHub-Dokumentation jedes dort konfigurierte Secret lesen. Ein gestohlenes Token mit diesem Zugriff genügt.

Für jedes Repository, das ein Token erreicht, legt das Werkzeug einen Branch mit dem Namensmuster „ci-sync-" plus sechs zufälligen Zeichen an und committet eine einzige Datei: .github/workflows/sync-check.yml, mit der Commit-Nachricht „ci: sync config". Der Commit ist unsigniert und trägt den Namen des Token-Inhabers; allein das Pushen startet den Workflow.

Der Workflow gibt die Umgebung des Jobs sowie jedes für ihn lesbare Secret aus — base32-kodiert. GitHub maskiert Secrets in Logs, laut eigener Dokumentation ist das aber nicht garantiert, sobald ein Wert kodiert oder anderweitig verändert wurde. Wiz zufolge stimmt der base32-Text nicht mehr mit dem ursprünglichen Secret überein, weshalb er vollständig im Protokoll erscheint. Der Workflow setzt zudem einen leeren permissions-Block; das beschränkt den GITHUB_TOKEN des Jobs, den die Angreifer nicht benötigen, hindert ihn aber nicht am Lesen der Secrets. Eine Variante des Werkzeugs startet zusätzlich für jede Deployment-Umgebung des Repositorys einen eigenen Job, sodass auch auf diese Umgebungen beschränkte Secrets ausgegeben werden — nach Einschätzung von Wiz häufig Produktionszugangsdaten.

Weil die Daten über GitHub selbst abfließen, gibt es weder einen Angreiferserver noch eine ausgehende Verbindung, die sich blockieren ließe.

Seit dem 28. Juli hält GitHub Workflow-Läufe, die es als potenziell bösartig einstuft, bis zur Freigabe durch einen Mitarbeitenden mit Schreibzugriff zurück. Laut GitHub gilt dies „derzeit nur für öffentliche Repositories auf github.com"; GitHub Enterprise Server verfügt zu diesem Zeitpunkt nicht darüber. Jeder von Wiz überprüfte öffentliche Gheist-Lauf — über alle Versionen des Werkzeugs hinweg — wartete noch auf Freigabe, die Nutzlast wurde also nie ausgeführt. Die Sperre greift jedoch nur bei Läufen, die GitHub auch markiert: Eine neue Variante der separaten Kampagne GhostAction lief nach Untersuchungen des Sicherheitsunternehmens GitGuardian in öffentlichen Repositories, bis GitHub sie am 9. Oktober zu blockieren begann. Gheist ist laut Wiz von GhostAction zu unterscheiden, das andere Workflow-Dateien verwendet.

In privaten Repositories und auf GitHub Enterprise Server zeigen die GitHub-Audit-Logs, wie Läufe auf den eingeschleusten Branches abschließen und anschließend vom User-Agent „gheist" gelöscht werden. In AWS-CloudTrail-Logs tauchen zudem organisationseigene Workflows auf, die auf diesen Branches laufen und AWS-Rollen annehmen — ein Hinweis darauf, dass GitHub Actions dort ohne Freigabe ausgeführt wurde.

In Unternehmensorganisationen wechselte das Werkzeug innerhalb von Minuten durch Tokens mehrerer Nutzer und Automatisierungskonten und klonte private Repositories, bevor es den Workflow platzierte. Stunden später wurden Cloud-Zugangsdaten derselben Organisation genutzt, um den AWS Secrets Manager zu durchsuchen. Woher diese Zugangsdaten stammten und wer sie einsetzte, lässt die Analyse offen.

Um den 19. Oktober begann eine Version des Werkzeugs, einen zweiten Job für selbst betriebene Runner (self-hosted runners) zu pushen; er zielt offenbar nur auf Repositories, bei denen ein solcher Runner registriert ist. Der Job durchsucht die Runner-Maschine nach Zugangsdaten, darunter 31 Token-Typen: private Schlüssel, Prozessumgebungen, AWS-Instanzrollen-Credentials, Runner-Credential-Dateien und das Kubernetes-Service-Account-Token. Wiz bezeichnet den Job als fehlerhaft und berichtet von keinem erfolgreichen Lauf.

Als nachvollziehbare Quellen der gestohlenen Tokens nennt Wiz Infostealer-Logs sowie Tokens, die bei früheren Supply-Chain-Angriffen offengelegt wurden — etwa bei der Kompromittierung des Schwachstellen-Scanners Trivy im März.

Dieselben Konten werden auch von anderen Akteuren genutzt: Am 6. Oktober benannten 391 Konten, viele davon auch von Gheist verwendet, rund 1.461 Repositories so um, dass sie wie geknackte Software wirkten, und verbreiteten Schadsoftware; dabei wurden 610 der eingeschleusten Branches gelöscht. Einige Opfer scheinen zudem mit PolinRider infiziert zu sein, einer Schadsoftware, die Forscher Nordkorea zuordnen: Am 19. und 20. Oktober pushten drei von ihnen ihre Gheist-Commits erneut, ergänzt um einen Loader in einer Datei vite.config.js. Die Kampagne Megalodon, die im Mai Secret-stehlende Workflows in Tausende GitHub-Repositories schob, hatte 57 der Konten ebenfalls getroffen.

Stand 24. Oktober lief die Kampagne weiter und veränderte sich; rund zwei Drittel der beteiligten Konten waren auf GitHub nicht mehr erreichbar. Wiz hat die Aktivität an GitHub gemeldet und führt das Verschwinden wahrscheinlich auf eine Reaktion zurück.

Das Löschen eines Branches oder des von ihm gestarteten Laufs macht die Offenlegung nicht rückgängig. Jedes Secret, das das Repository lesen konnte, sollte rotiert und das Token, das den Branch gepusht hat, widerrufen werden.

Zur Prüfung empfiehlt Wiz:

  • Suche nach Branches „ci-sync-" plus sechs Zeichen, deren einzige Änderung eine neue Datei .github/workflows/sync-check.yml mit der Commit-Nachricht „ci: sync config" ist.
  • In GitHub-Audit-Logs: Workflow-Läufe auf ci-sync-*-Branches, die binnen einer Minute nach Abschluss gelöscht wurden, sowie jede Aktivität des User-Agents „gheist".
  • Bei OIDC-Anmeldungen an einen Cloud-Anbieter: in Cloud-Audit-Logs nach Rollenübernahmen oder Token-Tauschvorgängen suchen, deren Subject einen ci-sync-Branch enthält — das funktioniert auch ohne eigene GitHub-Logs. Laut GitHubs OIDC-Dokumentation enthält das Subject eines Tokens den Branch-Namen standardmäßig nur, wenn der Job keine Deployment-Umgebung nutzt.
  • Auf selbst betriebenen Runnern: nach den Dateien .gdmp_pat, .gdmp_patg und .gdmp_sweep sowie der Zeichenkette „gdmp-pivot" in Job-Logs suchen.

Veröffentlichte Indikatoren:

  • User-Agent: gheist
  • Branch: ^ci-sync-[0-9a-z]{6}$
  • Commit-Nachricht: „ci: sync config", fügt nur .github/workflows/sync-check.yml hinzu
  • Zeichenkette: toJSON(secrets) }}’ | base32 -w 120
  • Zeichenkette: ===== gdmp-pivot:
  • Dateien: .gdmp_pat, .gdmp_patg, .gdmp_sweep
  • Git-Blob-SHA (einfacher Workflow): f3da2b8b357dcda02fd455d314be2a95007516a6
  • Git-Blob-SHA (Timeout-Version): d09535857a741bbb976bdc084a9a021713133291
  • Git-Blob-SHA (Separator-Version): 1dc5af9cce950aad1eb1546ecce8565b117540c4
  • Git-Blob-SHA (Version mit Self-hosted-Job): c665b778b2d822453293003e7ca0bbb2cb08e39d

Workflow-Versionen, die die Deployment-Umgebungen eines Repositorys auflisten, unterscheiden sich je Repository und haben daher keinen festen Hash.

Für betroffene Repositories rät Wiz: alle Repository-, Organisations- und Umgebungs-Secrets rotieren, die das Repository nutzen konnte; die Tokens widerrufen, die die Commits gepusht haben, und die Geräte ihrer Inhaber auf Infostealer prüfen; lief ein ci-sync-Branch auf einem selbst betriebenen Runner, sind die Maschine und ihre Cloud-Instanzrolle als kompromittiert zu behandeln; zurückgehaltene sync-check-Läufe nicht freigeben und keinen Code aus ci-sync-Branches ausführen.

Um gestohlene Tokens weniger wertvoll zu machen, empfiehlt Wiz zudem, für Umgebungen mit Produktions-Secrets Prüfer vorzuschreiben, OIDC-Vertrauensrichtlinien auf geschützte Branches oder Umgebungen zu begrenzen, statt persönlicher Zugriffstokens GitHub Apps oder kurzlebige fein granulierte Tokens einzusetzen und selbst betriebene Runner als kurzlebige Maschinen ohne Zugriff auf den Docker-Socket und ohne offenen Zugang zu Cloud-Metadaten zu betreiben. Laut GitHub-Dokumentation kann ein Job die Secrets einer Umgebung erst lesen, wenn ein erforderlicher Prüfer ihn freigegeben hat.