פתחתם את התיקייה ב-VS Code, וזה כל מה שהתוקף היה צריך: השלב השני של GhostAction יוצא מ-GitHub אל הלפטופ
מפתח פותח תיקיית פרויקט ב-VS Code. לא מריץ כלום, לא פותח טרמינל ולא לוחץ על Run, רק פותח את התיקייה. שנייה אחר כך רץ על המכשיר קוד שקורא את מפתחות ה-AWS, את ה-token של npm, את המפתח הפרטי של ה-SSH ואת פרטי ההתחברות ל-Claude ול-Gemini, שולח את הכול לשרת של תוקף, ומתקין ג'וב שמבקש מאותו שרת פקודות חדשות כל חמש דקות.
אשכול של 94 מאגרי GitHub שבהם הקוד הזה מחכה אותר בסריקה שגרתית של Jamf Threat Labs אחר קורבנות של קמפיין אחר לגמרי, והניתוח המלא פורסם ב-16 באוקטובר ב-OpenSourceMalware (הקרדיט על האיתור בפוסט הוא ל-Thijs Xhaflaire). זה שלב שני של GhostAction, קמפיין שגונב פרטי אימות דרך workflow-ים זדוניים ב-GitHub Actions. השלב הראשון סחף מאות מאגרים ב-8 באוקטובר וחילץ סודות דרך ה-CI של GitHub. השלב החדש לא עובר דרך GitHub בכלל. בהמשך נראה מה יושב בתוך המאגרים, למה זה רץ בלי שאף אחד מריץ שום דבר, ולמה המעבר הזה קרה שישה ימים בדיוק אחרי ש-GitHub חסם את השלב הראשון.
שלושה קבצים, וג'וב אחד על הלפטופ
VS Code יודע להריץ משימות אוטומטיות לפי קובץ הגדרות שיושב בתוך הפרויקט עצמו, בתיקייה בשם .vscode. אחת ההגדרות שם קובעת מתי המשימה תרוץ, ואחד הערכים שהיא מקבלת אומר בפשטות "ברגע שמישהו פותח את התיקייה הזאת". זה המנגנון שהקמפיין רוכב עליו. בכל אחד מהמאגרים הוא מניח קובץ tasks.json עם משימה אחת בשם eslint-check, שנראית כמו בדיקת סגנון קוד שגרתית, ועם הגדרות תצוגה שמונעות ממנה להופיע על המסך. ב-79 המאגרים ש-OpenSourceMalware הצליחו לשכפל ולנתח, הקובץ הזה זהה בייט-בייט.
המשימה מריצה קובץ אחד, public/fonts/fa-solid-400.woff2. השם נראה כמו גופן של Font Awesome, ספריית האייקונים הנפוצה, והוא יושב בדיוק בתיקייה שבה גופנים אמורים לשבת. בפועל זה לא גופן אלא 38 שורות JavaScript פשוט, ו-Node לא בודק סיומות: הוא מקבל את הנתיב, קורא את התוכן ומריץ אותו. הקובץ השלישי הוא workflow נוסף ב-GitHub Actions, גיבוי למקרה שהמפתח לא יפתח את התיקייה בכלל.
ה"פונט" שהוא בכלל JavaScript
הסקריפט עובד בארבע פעימות, וכדאי לראות מה הוא מחפש, כי זה מספר מי התוקף. קודם כול הוא פותח דוח עם שם המשתמש, שם ה-host, וכשהוא רץ בתוך CI גם שם המאגר. אחר כך הוא קורא שמונה קבצי פרטי אימות ושומר מכל אחד את 3,000 הבייטים הראשונים: מפתחות AWS, ה-token-ים של PyPI ושל npm, פרטי התחברות ל-git שנשמרו בדיסק, קובץ ה-netrc שבו curl ו-git שומרים התחברויות, קבצי ההתחברות של Claude ושל Gemini, וקובץ ההגדרות של Kubernetes עם פרטי הגישה לקלאסטר. את שני המפתחות הפרטיים של SSH הוא קורא במלואם, בלי תקרה.
הפעימה השלישית היא חיפוש רקורסיבי בכל ספריית הבית אחר תבניות של מפתחות AWS, Anthropic, GitHub, npm ו-PyPI, ששומר את 30 ההתאמות הייחודיות הראשונות. הרביעית אוספת משתני סביבה ששמם מתחיל ב-AWS_, ANTHROPIC, OPENAI, HF_TOKEN או GITHUB_TOKEN. הכול יוצא בבקשת POST אחת, ב-HTTP רגיל ולא מוצפן, לפורט 8080 בכתובת IP קבועה, עם תג אצווה בשורת הכתובת.
ועכשיו הצד המשעשע: החלק הזה עובד פחות טוב ממה שהוא נראה. OpenSourceMalware מצאו שני באגים בקוד של התוקף. סינון משתני הסביבה עוגן כך שהתבניות הכלליות לשמות שמכילים API_KEY או TOKEN תופסות רק משתנים ששמם מתחיל בקו תחתון, ולכן רוב מפתחות ה-API האמיתיים חומקים. והחיפוש בספריית הבית מוגבל לעשר שניות, זמן שסריקה של ספריית בית שלמה חורגת ממנו לא מעט, וכשהוא חורג הקוד זורק את כל מה שכבר נמצא. החוקרים מדגישים שלא בדקו בפועל אף אחת משתי ההתנהגויות. שימו לב שאף אחד מהשניים לא נוגע בקבצי פרטי האימות ובמפתחות ה-SSH, שנקראים ישירות.
מה שבאמת מסוכן הוא ארבע השורות האחרונות. הן מתקינות רשומה ב-crontab של המשתמש, וכל חמש דקות המכשיר מבקש מהשרת סקריפט בנתיב שמבוסס על שם ה-host שלו עצמו, ומריץ אותו. שם ה-host הוא מה שמאפשר למפעיל לתת לכל מכשיר הוראות אחרות. "עדיין לא הצלחנו לשלוף מה השרת מחזיר, ולכן איננו יכולים לומר לכם מה עושה שלב 3", כתבו החוקרים. "מה שכן אנחנו יכולים לומר הוא שהמכשיר שעליו פתחתם את הפרויקט מקבל כרגע הוראות ממישהו אחר, ושמחיקת קובץ הגופן לא מבטלת את ה-cron."
העתקה של טכניקה קוריאנית, לא הרחבה שלה
הטריק של tasks.json והגופן המזויף הוא לא המצאה של GhostAction. הוא תועד בינואר בקמפיין PolinRider, שמיוחס לקוריאה הצפונית, ושבמשך רוב 2026 השתמש בדיוק באותו שם קובץ. אותו חוקר שתיעד אותו אז הוא שנתקל באשכול הנוכחי, ושני הבדלים הם מה שהדליק את הנורה: בגרסה הזאת הקובץ המזויף לא מכיל JavaScript מעורפל אלא קוד קריא לגמרי, וכתובת ה-IP שבתוכו היא של GhostAction.
ההבדלים ממשיכים גם פנימה. PolinRider שולף את המטען שלו מכתובות שמאוחסנות על בלוקצ'יין ציבורי ולא פונה לשרת של התוקף בכלל, והוא אוסף פרטי דפדפן וארנקי קריפטו. האשכול הזה פונה לכתובת IP חשופה ולא נוגע בדפדפנים ובארנקים בכלל. מי שכתב אותו רצה מפתחות ענן, token-ים לרישומי חבילות ופרטי התחברות לכלי AI, בדיוק הקטגוריות שה-workflow של השלב הראשון כבר חיפש. OpenSourceMalware לא מצאו ולו מזהה אחד משותף בין GhostAction לנוזקות קוריאניות שהם מכירים, ולכן הם מגדירים את זה במפורש כחיקוי ולא כהרחבה של הקמפיין הקוריאני. במאגר אחד מתוך ה-79 הם מצאו גם טכניקה שנייה של PolinRider, אבל התאריכים והכלים שונים לגמרי והם לא מקשרים בין השניים.
אז למה דווקא עכשיו?
כאן הטיימינג מספר את הסיפור. ב-9 באוקטובר, לפי GitGuardian כפי שדווח ב-The Hacker News, GitHub התחיל למנוע מה-workflow הזדוני לרוץ. הגלים שנדחפו ב-10 וב-12 באוקטובר לא הוציאו שום סוד. במאגר אחד שחוקרי OpenSourceMalware עקבו אחריו, מתוך עשר הרצות של ה-workflow רק זו של 8 באוקטובר הושלמה, והתשע האחרות נתקעו בהמתנה לאישור שאף אחד לא נתן. השלב השני, זה שלא צריך runner של GitHub בכלל, נחת ב-15 באוקטובר.
"אנחנו לא יכולים לומר שהחסימה היא שגרמה לזה", כתבו החוקרים. "מה שאנחנו יכולים לומר הוא שהמפעיל עבר למקום שהבקרות של GitHub לא מגיעות אליו." בין 12 ל-15 באוקטובר השתנו גם הפורט, תג האצווה, שם הקובץ של השלב השני והודעת הקומיט, וסימן זיהוי מרכזי שחוקרים חיפשו לפיו נעלם מה-workflow החדש. מה שלא זז הוא כתובת ה-IP, וזאת הסיבה שחיפוש לפיה עוד עובד.
התזמון של ההדבקות עצמן מצביע על החלטה ולא על טיימר. השלב הראשון נכנס ל-72 מהמאגרים ב-8 באוקטובר בין 13:03 ל-22:41 UTC, והשלב השני נחת ב-76 מאגרים ב-15 באוקטובר בין 01:00 ל-10:31, כש-32 מהם קיבלו את הכול בשעתיים הראשונות. הפער החציוני בין השלבים הוא 6.3 ימים, אבל ארבעה מאגרים שקיבלו את השלב הראשון רק ב-12 עד 14 באוקטובר קיבלו את השני גם הם ב-15, אחד מהם 26 שעות בלבד אחרי. מישהו החליט שה-15 באוקטובר הוא היום.
מי נפגע, ומה קרה למי שניקה
הנקודה הטובה בכל הסיפור היא מי המאגרים האלה. מתוך 79, ב-65 לא נכנס קומיט במשך יותר משנה לפני שהתוקף הגיע, והוותיק שבהם לא נגע בו אף אחד יותר מתשע שנים. החציון הוא 792 ימי שקט. התוקף לא בוחר פרויקטים, האוטומציה פשוט דוחפת לכל מה שלחשבון הפרוץ יש הרשאת כתיבה אליו, ולכן מאגר שהבעלים שכחו שיש להם נושא בדיוק את אותה נוזקה כמו זה שהם עובדים בו כל יום.
הצד הפחות טוב הוא מה שקרה למי שכן שם לב. שני בעלים מחקו את ה-workflow של השלב הראשון ב-9 באוקטובר, בוקר אחרי הגל, ואחד מהם מחק את כל תיקיית ה-workflows; שניהם קיבלו את השלב השני ב-15 באוקטובר בכל זאת. מה שדחף את הקבצים בפעם הראשונה עוד היה שם שישה ימים אחר כך. במאגר אחר נמחקו שני ה-workflow-ים שש שעות אחרי שנחתו, עם הודעת קומיט שאמרה "Remove malicious workflow", אבל ה-tasks.json והגופן נשארו שם עד שהחוקרים הגיעו. GitGuardian מצאו שמתוך 772 מאגרים בגל של אוגוסט וספטמבר, רק 16 אחוז נוקו באמת מההיסטוריה הציבורית עד 5 באוקטובר, וב-92 מקרים התוקף פשוט ערך workflow שנשאר מגל קודם.
גם ה-workflow השני שונה מקודמו. הוא מחפש יותר פורמטים של פרטי אימות, סורק את ה-wiki של המאגר בעזרת ה-GITHUB_TOKEN של ההרצה עצמה, ואם ה-POST נכשל הוא מעלה את מה שמצא ל-gist סודי. התוקף נתן לו שבעה שמות שונים, בין היתר lint.yml ו-audit.yml, כל אחד בין חמישה ל-16 מאגרים. במאגר בודד אחד נמצא workflow שלישי שמבקש משירות המטא-דאטה של הענן את פרטי האימות של תפקיד ה-IAM של המכונה שעליה הוא רץ. זה היחיד שמושיט יד אל מעבר לסודות של המאגר, אל המכונה שמתחתיו.
מה זה אומר בפועל
אם אתם מתחזקים פרויקט ב-GitHub, הסדר כאן חשוב יותר מהניקיון: מחיקת קבצים לא מבטלת את ה-token שדחף אותם, ושני הבעלים שניקו ונדבקו שוב שישה ימים אחר כך הם ההוכחה. בטלו קודם כול, ממכשיר נקי, כל session, personal access token, הרשאת OAuth, מפתח SSH ומפתח deploy בחשבון, ורק אחר כך עברו על כל מאגר שלחשבון יש הרשאת כתיבה אליו, כולל fork-ים ופרויקטים נטושים. מה שמחפשים הוא workflow שמוציא נתונים לכתובת חשופה, קובץ .vscode/tasks.json עם משימה מוסתרת, וקובץ טקסט שמתחזה לגופן תחת public/fonts.
ואם אתם רק מפתחים ששכפלתם מאגר כזה, הסיכון שלכם הוא לא ב-CI אלא במכשיר. בדקו את ה-crontab של המשתמש שלכם, והחליפו כל מפתח שיושב באחד מהקבצים שהסקריפט קורא. מחיקת הגופן מהפרויקט לא מורידה את הג'וב שהוא כבר התקין.
תגובות