בענף חדש ב-repository מופיע קובץ אחד. שם הענף הוא ci-sync ועוד שישה תווים רנדומליים, הקובץ הוא .github/workflows/sync-check.yml, והודעת הקומיט אומרת "ci: sync config". הקומיט לא חתום, והוא רשום על שמו של בעל ה-token שנגנב. הדחיפה עצמה מפעילה את ה-workflow. מי שמסתכל בבוקר על רשימת הענפים רואה משהו שנראה כמו עוד סידור שגרתי של ה-CI.

חוקרי Wiz, רמי מקארתי והילה רמתי, פרסמו ב-25 באוקטובר ניתוח של הקמפיין שהם מכנים Gheist ועוקבים אחריו מאז 15 בספטמבר. הם שחזרו יותר מ-13,500 ענפים שנשתלו כך, מתוך יותר מ-850 חשבונות שנפרצו, מפרויקטים אישיים ועד repositories פרטיים של ארגונים גדולים. מה שמעניין פה יותר מהגודל הוא דווקא מה שחסר: אין שרת שליטה ובקרה (C2, השרת שדרכו תוקף שולח פקודות ומקבל בחזרה מידע גנוב), אין דומיין של תוקף, ואין חיבור יוצא שאפשר לחסום. בהמשך נראה איך הסודות יוצאים בכל זאת, למה ההגנה שגיטהאב הוסיפה ביולי עצרה רק חלק מהמתקפה, ומה הכלי הוסיף לעצמו בשבועיים האחרונים.

הסודות לא נשלחים לשום מקום, הם מודפסים

בדרך כלל, כלי שגונב סודות ממערכת CI צריך להוציא אותם משם. לשלוח אותם ל-endpoint של התוקף, להעלות אותם לאחסון בענן, משהו שיוצא מהרשת ואפשר לראות אותו. ה-workflow של Gheist לא שולח כלום. הוא מדפיס את משתני הסביבה ואת כל הסודות שהג'וב מורשה לקרוא אל תוך הלוג של הריצה, והלוג הזה נשמר אצל GitHub כמו כל לוג אחר. אחר כך הכלי מוריד את הלוג דרך ה-API של GitHub, בבקשה מאומתת לגמרי, ומוחק את הריצה כמה שניות לאחר מכן. הדליפה כולה עברה בתוך הפלטפורמה, מתחילתה ועד סופה.

זה עובד בגלל שלושה פרטים שכל אחד מהם בנפרד נשמע סביר לגמרי. הראשון: GitHub באמת מסתירה ערכים של סודות בלוגים, אבל היא מחפשת את הערך כפי שהוא, והכלי מעביר את הסודות דרך קידוד base32 לפני ההדפסה. הערך המקודד לא זהה למה שהמסכה מחפשת, ולכן הוא נדפס במלואו. הפרט השני הוא בלוק הרשאות ריק שה-workflow מגדיר לעצמו, שמצמצם את ה-token האוטומטי של הג'וב אבל לא נוגע בגישה לסודות עצמם, ואת ה-token הזה התוקף לא צריך בכלל. הפרט השלישי הוא הנדיב מכולם: אם ב-repository מוגדרות סביבות פריסה, הכלי מוסיף אותן למטריצת ג'ובים ומריץ ג'וב לכל סביבה בנפרד, וכך מגיע גם לסודות שמשויכים לסביבה אחת ולא ל-repository כולו. אלה בדרך כלל פרטי האימות של הפרודקשן.

מחיקת הענף או הריצה לא מבטלת שום דבר מזה. הסוד כבר נקרא.

אז למה ההגנה החדשה של GitHub עצרה רק חלק מזה?

ב-28 ביולי הודיעה GitHub שהיא מתחילה לעכב ריצות workflow שנראות לה זדוניות, עד שמשתף עם הרשאת כתיבה יאשר אותן דרך session מאומת בדפדפן. זו הייתה תשובה ישירה לגל מתקפות שרשרת אספקה שהשתמשו בפרטי אימות גנובים של GitHub כדי לדחוף workflows עוינים. לפי ההודעה, ההגנה חלה על repositories ציבוריים ב-github.com בלבד, ו-GitHub Enterprise Server לא מקבל אותה בשלב הזה.

בבדיקה של Wiz, כל ריצה ציבורית של Gheist שנבדקה, בכל גרסאות הכלי, נתקעה במצב action_required ולא התחילה בכלל. "השליטה עבדה, והמטענים הזדוניים לא רצו", כתבו החוקרים. הבעיה היא שההגנה מכסה דווקא את החלק הפחות רגיש. ב-repositories פרטיים ובסביבות Enterprise, לוגי הביקורת של GitHub מראים ריצות על ענפי ci-sync שהסתיימו כרגיל ואז נמחקו על ידי user agent בשם gheist, השם שממנו הקמפיין קיבל את הכינוי שלו. בארגונים שבהם ה-workflows מתחברים לענן דרך OIDC, Wiz מצאה ב-CloudTrail שהענפים האלה אכן קיבלו הרשאות: ה-workflows של הארגון עצמו רצו על ענף שהתוקף שתל ולקחו role ב-AWS.

בארגונים גדולים זה גם לא נעצר בסודות של ה-repository. Wiz ראתה את הכלי מחליף tokens של כמה משתמשים וחשבונות אוטומציה בתוך דקות, משכפל repositories פרטיים לפני שהוא משתיל את ה-workflow, וכמה שעות אחר כך פרטי אימות של ענן מאותו ארגון שימשו למיפוי של AWS Secrets Manager. מאיפה הגיעו פרטי האימות האלה בדיוק, החוקרים לא קובעים.

הכלי ממשיך להשתנות תוך כדי ריצה

Wiz מזהה שתי גרסאות שרצו במקביל, ואפשר להבדיל ביניהן לפי שם הענף: אחת משתמשת בסיומת הקסדצימלית ובמטען הפשוט, השנייה בסיומת base36. השנייה היא זו שמתפתחת. תוך שעות מהריצה הראשונה שלה, ב-28 בספטמבר, היא הוסיפה לג'וב מגבלת זמן של שש דקות ואת מטריצת הסביבות, והיא משנה את המטען לכל repository בנפרד. ב-19 באוקטובר היא הוסיפה מפריד בין בלוקי הפלט בלוג, מה שנראה בעיקר כמו נוחות של מי שמפרסר את הלוגים בצד השני.

כמה שעות אחרי זה הגיעה התוספת המעניינת יותר: ג'וב שני שמבקש לרוץ על runner שהארגון מתחזק בעצמו, ומגיע רק ל-repositories שרשום להם runner כזה. הג'וב הזה לא מסתפק בסודות של ה-repository. הוא סורק את תיקיית הבית, את ה-workspace ונתיבי מערכת אחרי 31 פורמטים של tokens, בהם מפתחות של ספקי AI כמו Anthropic, Hugging Face, Groq ו-NVIDIA, מפתחות פרטיים, קובצי env, state של Terraform והיסטוריית shell. הוא קורא את הסביבה של כל תהליך שרץ על המכונה דרך מערכת הקבצים הווירטואלית proc. הוא מרים קונטיינר דרך ה-socket של Docker כשמערכת הקבצים של ה-host מחוברת אליו, וחוזר על אותו חיפוש על ה-host עצמו. ובסוף הוא לוקח את פרטי האימות של ה-role שהמכונה מקבלת ב-AWS ואת ה-token של ה-service account ב-Kubernetes.

מה שכן, לפי Wiz הג'וב הזה באגי: הוא מחפש את קובץ המפתח הלא נכון של ה-runner, הביטוי הרגולרי שלו לחילוץ פרטי אימות מתוך כתובות שבור, והוא לא באמת אוסף את ה-token-ים של GCP ושל Azure. על ריצה מוצלחת אחת שלו החוקרים לא דיווחו. נכון ל-24 באוקטובר המתקפה עדיין פעילה וממשיכה להשתנות, וכשני שלישים מהחשבונות המזוהים כבר לא זמינים ב-GitHub. החוקרים משערים שזו תגובה לדיווח שהם העבירו לגיטהאב, ולא נסיגה של התוקפים.

אותם חשבונות, כמה קמפיינים שונים

החלק שמסביר למה הסיפור גדול מ-Gheist הוא מה שקרה לאותם חשבונות עצמם בשבועות האחרונים. ב-6 באוקטובר 391 חשבונות שינו שמות של כ-1,461 repositories כך שייראו כמו תוכנות קרק, בשמות כמו fl-studio-crack ו-solana-sniper-lite, לצורך הפצת נוזקות. שינוי השמות מחק דרך אגב 610 ענפי ci-sync שנשתלו שם קודם. 166 מהחשבונות האלה לא מופיעים בפעילות של Gheist בכלל, כלומר מאגר החשבונות הפרוצים גדול מהטביעה של הקמפיין הזה.

ב-18 באוקטובר 25 חשבונות של Gheist פתחו כל אחד repository בשם Windows RDP Runner, כולם בתוך תשע דקות, בניסיון לנצל את GitHub Actions בשביל כוח חישוב חינם. חלק מהקורבנות נגועים גם ב-PolinRider, נוזקה שמיוחסת לקוריאה הצפונית, שמסתירה JavaScript בתוך קובצי הגדרות של בילד ודוחפת קומיטים עם תאריך אחורה. ב-19 וב-20 באוקטובר שלושה קורבנות דחפו מחדש את הקומיטים של Gheist, הפעם עם loader שהתווסף ל-vite.config.js מאחורי מאות תווי רווח. ו-57 חשבונות מהשלב הראשון של Gheist היו קורבנות של קמפיין Megalodon עוד במאי.

את מקורות פרטי האימות שאפשר היה לעקוב אחריהם Wiz מחברת לשני דברים: לוגים של נוזקות גניבת מידע, ו-tokens שנחשפו בפריצות שרשרת אספקה קודמות, כמו הפריצה לסורק Trivy במרץ. (Wiz מדגישה ש-Gheist נפרד מ-GhostAction, קמפיין שמשתמש בקובצי workflow אחרים ושולח את הסודות לכתובת IP משלו.)

מה זה אומר בפועל

אם אתם מחזיקים repositories ב-GitHub, החיפוש הראשון הוא ענפים בתבנית ci-sync ועוד שישה תווים, שבהם השינוי היחיד הוא קובץ workflow חדש בשם sync-check.yml. בלוגי הביקורת כדאי לחפש ריצות על ענפים כאלה שנמחקו תוך דקה מסיומן, וכל פעילות של user agent בשם gheist. מי שלא אוסף לוגים של GitHub אבל כן מתחבר לענן דרך OIDC יכול לראות את זה מהצד השני: בקשות לקבלת role שה-subject שלהן מכיל ענף ci-sync הן עדות לכך ש-Actions רץ על ענף שהתוקף שתל.

ואם מצאתם, ההנחה לעבוד לפיה היא שהסודות כבר בחוץ. Wiz ממליצה להחליף כל סוד שהיה זמין ל-repository שנפגע, ברמת ה-repository, הארגון והסביבה, לבטל את ה-tokens שדחפו את הקומיטים ולבדוק את המכשירים של בעליהם מול נוזקות גניבת מידע, ובמקרה שענף ci-sync רץ על runner עצמאי, להתייחס למכונה הזאת ול-role שלה כאל פרוצים. ואל תאשרו ריצת sync-check שמחכה לאישור, גם אם היא נראית כמו עוד תקלה שגרתית ב-CI.