אף אחד לא תקף שוב, והנוזקה חזרה לרוץ: שני repositories שהושבתו במאי חזרו לאוויר, והתגיות הזדוניות עוד היו שם
ב-16 בספטמבר 2026, מתישהו בין 09:09 ל-16:16 UTC, שני repositories ב-GitHub חזרו להיות נגישים. הם הושבתו ב-19 במאי, יום אחרי שנמצא בהם קוד זדוני, ומאז החזרה שלהם אף אחד לא נגע בהם. גם לא היה צריך. חוקר Socket קרלו זנקי תיאר את מה שקרה אחר כך: "תגיות הגרסה שלהם לא נוקו קודם. הן עדיין מפנות לתוכן הזדוני שהוכנס ב-18 במאי, כך שכל workflow שמפנה לאחת מהפעולות לפי תגית גרסה חזר להוריד ולהריץ את המטען בהרצה הבאה שלו."
שתי הפעולות, actions-cool/issues-helper ו-actions-cool/maintain-one-comment, הן כלי תחזוקה משעממים: הן סוגרות issues שנשכחו, בודקות issues חדשים, ומעדכנות תגובה אחת של בוט במקום להוסיף עוד אחת. לפי Socket, ה-workflows שקוראים להן רצים בדרך כלל לפי לוח זמנים יומי, או בכל פעם שמישהו פותח issue או pull request. כלומר רוב מי שהיה חשוף הריץ את המטען בתוך יום מהרגע שה-repositories חזרו. בהמשך נראה למה הפריצה המקורית עבדה, מה המטען עושה כשהוא רץ בתוך pipeline, ולמה דווקא המקרה הזה מסמן בעיה מבנית בדרך שבה כולנו מייבאים קוד של אחרים.
commit שלא היה בהיסטוריה
כדי להבין למה החזרה לאוויר הספיקה, צריך להסתכל על מה שהתוקף עשה במאי. הוא לא הוסיף commit חדש לענף הראשי ולא פרסם גרסה חדשה. הוא לקח את כל תגיות הגרסה הקיימות של ה-repository והזיז אותן, כך שכל אחת מהן תפנה ל-commit שלא מופיע בשום מקום בהיסטוריית הפיתוח הרגילה של הפרויקט. ל-commit כזה, שנשתל בצד ונקשר לתגית קיימת, קוראים imposter commit.
המספרים ש-StepSecurity פרסמה מראים כמה זה היה מהיר: ב-issues-helper הוזזו 53 תגיות, וכל 53 ה-commits הזדוניים נוצרו בחלון של שלוש דקות ו-16 שניות, בין 19:10:24Z ל-19:13:40Z. ב-maintain-one-comment הוזזו 15 תגיות, וזה קרה בתוך 39 שניות. ורון שארמה מ-StepSecurity תיאר את התוצאה כך: "כל תגית קיימת ב-repository הוזזה כדי להפנות ל-imposter commit שלא מופיע בהיסטוריית ה-commits הרגילה של הפעולה." ומה שנגזר מזה, כתבה החברה, הוא ש"כל workflow שמפנה לפעולה לפי גרסה שולף את הקוד הזדוני בהרצה הבאה שלו."
וזה בדיוק העניין עם תגיות ב-Git. תגית היא מצביע, לא טביעת אצבע. כשאתם כותבים בקובץ ה-workflow שלכם actions-cool/[email protected] אתם לא אומרים "הרץ את הקוד הזה", אתם אומרים "הרץ את מה שנקרא כרגע v2.2.1 בצד ההוא". מי ששולט ב-repository יכול להזיז את השם הזה לאן שהוא רוצה, ואצלכם שום דבר לא משתנה. אותה שורה בקובץ, אותו מספר גרסה, קוד אחר. הדרך היחידה לומר "הרץ את הקוד הזה" היא לרשום את ה-SHA המלא של ה-commit, שהוא גיבוב של התוכן עצמו ולכן אי אפשר להזיז אותו.
מה המטען עושה בתוך ה-runner
החלק שבאמת כדאי להכיר הוא מה שרץ על מכונת ה-CI אחרי שהמטען נטען. לפי הניתוח של StepSecurity, הרצף מתחיל בהורדה של סביבת ההרצה bun אל /home/runner/.bun/bin/bun, כלומר המטען מביא איתו סביבת הרצה משלו ל-JavaScript במקום להסתמך על מה שמותקן על המכונה.
אחר כך מגיע השלב המעניין. ב-GitHub Actions הסודות של ה-workflow, מפתחות ה-API, ה-token-ים ופרטי האימות לענן, מוצפנים במנוחה ומפוענחים רק בתוך התהליך שמריץ את ה-job. המטען קורא את /proc/<PID של Runner.Worker>/mem, כלומר את הזיכרון של אותו תהליך בדיוק, ומשם מחלץ את הסודות במצב מפוענח. הוא גם מריץ gh auth token ו-sudo python3, ומסנן את מה שהוא אוסף לפי הסימון "isSecret":true, שמפריד בין מה שסומן כסוד לבין רעש רגיל. בסוף הכול נשלח בקריאת HTTPS יוצאת לדומיין שבשליטת התוקף, t.m-kosche[.]com, בפורט 443.
שימו לב למה שאין כאן: אין חולשה, אין CVE, אין ניצול של באג בתוכנה. הזיכרון של תהליך אחד נגיש לתהליך אחר שרץ באותה מכונה עם אותן הרשאות, וזה בדיוק מה ש-runner של CI הוא, מכונה שמריצה קוד זר לצד הסודות שלכם.
StepSecurity זיהתה את הפריצה ב-18 במאי דרך Harden-Runner, הכלי שלה שמנטר מה קורה בתוך ההרצה: ההורדה של bun, הקריאה לזיכרון של תהליך אחר, והחיבור היוצא לדומיין שאף אחד לא ביקש, שלושתם יחד. לקוחות שהפעילו את מדיניות הפעולות הפרוצות שלה, כותבת החברה, קיבלו ביטול של ההרצה שהפנתה לפעולה הזדונית "לפני שהקוד הזדוני הצליח לרוץ."
איך זה מתחבר ל-Mini Shai-Hulud
הדומיין t.m-kosche[.]com הוא מה שקשר את הפריצה ב-GitHub Actions לקמפיין רחב בהרבה. באותו שבוע במאי פורסמו ב-npm מאות גרסאות זדוניות של חבילות מאקוסיסטם @antv, ספריות התרשימים הפופולריות, ואותו דומיין שימש גם שם. "זה מצביע על אותו אשכול פעילות של Mini Shai-Hulud, לא על תקרית נפרדת שמוגבלת ל-npm," אמר פיליפ בורקהרדט, ראש מחקר האיומים ב-Socket, ל-The Hacker News באותו זמן.
היקף הקמפיין ההוא, כפי שדווח אז, היה 639 גרסאות זדוניות על פני 323 חבילות, מהן 558 גרסאות ב-279 חבילות @antv, כשהחבילה echarts-for-react לבדה נספרה בכ-1.1 מיליון הורדות בשבוע. הגורם שמייחסים לו את הפעילות הוא TeamPCP. המטען שם היה שאפתן יותר מזה שרץ ב-Actions: הוא חיפש יותר מ-20 סוגי פרטי אימות, בין השאר AWS, Google Cloud, Azure, GitHub, npm, SSH, Kubernetes, Vault ומחרוזות התחברות למסדי נתונים, ואת מה שמצא הוא גם דחף ל-repository ציבורי חדש תחת החשבון של הקורבן עצמו, בעזרת ה-token הגנוב.
עד כאן הקמפיין המקורי. מה שהופך את התקרית של ספטמבר לסיפור בפני עצמו הוא שהיא לא דרשה ממנו שום דבר נוסף.
אז למה זה שונה מכל תקרית שרשרת אספקה אחרת?
זנקי שם את האצבע על ההבדל: "רוב תקריות שרשרת האספקה כוללות משהו חדש: גרסה זדונית שפורסמה עכשיו, חשבון שנחטף עכשיו, או workflow שהוזרק עכשיו. זו לא. לא פורסם קוד חדש ולא שונתה שום הגדרה."
בפועל, מה שקרה כאן הוא שהאיום הועבר לקרנטינה בלי לחטא אותו. GitHub חסמה את הגישה ל-repositories, וזה עצר את ההדבקה, אבל התגיות הזדוניות נשארו במקום בתוך הקרנטינה. ברגע שהגישה חזרה, מסיבה שלא הובהרה, הקוד של מאי היה שם וחיכה, וה-workflows של אנשים אחרים רצו לפי לוח הזמנים הרגיל שלהם והפעילו אותו. Socket מציינת שגרף התלויות של GitHub מונה כ-15,000 repositories תלויים ב-issues-helper לבדו, אם כי זה מספר התלויות הרשום ולא ספירה של מי הריץ את המטען בפועל. לפי החברה, שתי הפעולות הושבתו שוב ב-25 בספטמבר, ולמה הן חזרו להיות נגישות מלכתחילה עוד לא ידוע.
"התקרית הזאת מראה שתגית ניתנת לשינוי יכולה להיפרץ, להיות מוכלת, ואז להתעורר מחדש בלי שום שינוי בקובץ ה-workflow שלכם," כתב זנקי. "נעילה ל-SHA מסירה את התלות הזאת במצב של ה-repository שבצד השני."
מה זה אומר בפועל
מי שמריץ אחת משתי הפעולות לפי תגית צריך להתייחס לסודות של אותם workflows כאילו דלפו, ולהחליף אותם: לא רק את ה-token של GitHub אלא כל מפתח שהיה נגיש לאותה הרצה. שווה גם לעבור על היסטוריית ההרצות ולחפש הרצות שהצליחו לפתע אחרי תקופה ארוכה של כשלים בשלב הקמת ה-job, ועל היסטוריית ה-repository ולחפש commits לא מוסברים מ-16 בספטמבר והלאה. הבדיקה הרחבה יותר פשוטה יותר: תחפשו ב-.github/workflows שלכם כל הפניה לפעולה חיצונית שנכתבה כמספר גרסה ולא כ-SHA. כל אחת מהן היא הבטחה שמישהו אחר יכול לשנות בלי לספר לכם.
תגובות