TL; DR
אשכול של חמש חבילות npm, שפורסם על פני שני שמות חשבון, נשלח א postinstall וו שקורא אישורי ענן מהמארח ושולח אותם מחוץ לקופסה. החבילות נושאות שמות רפאים ופיראטיים - coral-wraith, ecto-corsair-whisper-6f3b9, ecto-corsair-flag-x9m4, ecto-rust-read-f3a9c1, ecto-nightly-spirit — ולסכם כל מה שהם אוספים לזיוף ecto_module: מניפסט YAML לפני שידורו. אנו עוקבים אחר האשכול כ האקטופלסמה.
המטען פועל רק כאשר הוא מזהה סביבה ספציפית: מארח ששמו הוא מחרוזת הקסדצימלית בת 12 תווים וספריית עבודה תחת /app/node_modules — צורת מבנה ממוכן או עובד CI. כאשר שער זה עובר, הוו שואל את שירות מטא-נתונים של מופעי AWS (IMDSv2) עבור אישורי תפקיד IAM, מונה מנהל סודות AWS על פני שלושה אזורים, דוחה משתני סביבה, קורא קבצים תחת /app, ומחפש אחר מחרוזות לכידת הדגל. לאחר מכן הוא מסנן את התוצאה בשתי דרכים: משואת אנרגיה אל webhook.site אספן ו-PUT מניפסט לנקודת קצה של IP גולמי, עם רשימה של אפשרויות גיבוי של localhost-first.
תיאורי החבילות המאוחרים יותר קוראים "מטען CTF עבור בדיקות שרשרת אספקה של verdaccio". אנו מדווחים על תיאור עצמי זה כעובדה נצפית. ההתנהגות עצמה - יציאה חיה לכתובת IP ציבורית, קריאות אמיתיות של אישורי IMDS, קריאות אמיתיות של Secrets Manager - היא כפי שהיא ללא קשר לתווית, וזו הסיבה שגרסאות אלו סווגו כזדוניות.
שם אחד באשכול, coral-wraith, לא הסתפק בגרסה אחת. הוא פורסם מחדש ברצף מהיר באמצעות עשרות גרסאות תוך שעות ספורות - 1.0.0 מטפס אל 6.0.0 — וסדרה קודמת באותו שם השתמשה בגרסה מנופחת 9999.0.x מספרי גרסה, הצורה הקלאסית של ניסיון בלבול תלותלאורך כל התנועה הזו, המטען התבגר באופן ניכר: ממשואה חד-פעמית של ספירת מארח לציר גישה מלא של AWS, עטוף בבדיקות סביבה ששומרות עליו שקט מחוץ ליעדו המיועד.
| מארזים | 5 שמות; coral-wraith פורסם מחדש בעשרות גרסאות בלבד |
| מערכת אקולוגית | Npm |
| התקנת וקטור | postinstall סקריפט מחזור חיים |
| יעד ראשי | אישורי תפקיד AWS IAM + ערכי סוד של מנהל סודות, משתני סביבה, /app קבצים |
| אקספיל | webhook.site משואה + IP גולמי C2 PUT |
| שער טריגר | שם מארח 12-הקס + /app/node_modules cwd, בנוסף לבדיקת סביבה שמדכאת את המטען מחוץ להקשר זה |
| חומרה | גָבוֹהַ — גילוי אישורי ענן וסודות מנוהלים מאת סביבות בנייה וזמן ריצה ממכולות |
אנטומיה של התקפה
כל חבילה באשכול בנויה באותו אופן: כמעט ריקה index.js (מודול.יצוא = {}), שורה אחת package.json תסריט — "postinstall": "צומת postinstall.js" — והמטען ב postinstall.jsהתקנת החבילה מספיקה כדי להפעיל את ה-hook; אין צורך בייבוא או קריאה.
שער המיקוד. לפני ביצוע פעולה כלשהי, מטען משפחת האקטו בודק את סביבתו:
function isAppWorker():
host = os.hostname()
if host does NOT match /^[0-9a-f]{12}$/ -> exit
if cwd does NOT contain "/app/node_modules" -> exit
if cwd contains "/tmp/npm-safe" -> exit
otherwise -> proceedשם מארח בן 12 הקסדצימליים הוא הצורה המוגדרת כברירת מחדל ש-Docker מקצה למכולה, ו /app/node_modules הוא נתיב התקנה קונבנציונלי בתוך מכולה. הסעיף השלישי נפסל אם הנתיב נראה כמו ספריית חילוץ בארגז חול. התוצאה הסופית היא שהמטען נשאר רדום במחשב נייד של המפתח או בארגז חול של ניתוח ומופעל רק בתוך בנייה או עבודה בזמן ריצה ממכולה - סוג הסביבה שסביר להניח שתשא אישורי ענן חיים. החבילה המוקדמת ביותר באשכול, רוח אלמוגים, אין לה שער כזה והיא מפעילה את האוסף (הפשוט יותר) שלה ללא תנאי.
אוספיםכאשר השער עובר, הקרס יוצא דרכו execFileSync("/bin/sh", ["-c", …]) ומפעיל פקודה מורכבת אחת אשר, לפי הסדר:
1. PUT /latest/api/token to 169.254.169.254 (IMDSv2 token request)
2. GET .../iam/security-credentials/ (IAM role name)
3. GET .../iam/security-credentials/<role> (temporary credentials)
4. dump env | sort (environment variables)
5. list /app (excl. node_modules) + cat first 15 (application files)
6. aws secretsmanager list-secrets (us-east-1, eu-west-1, eu-central-1)
7. scrape readable files for HTB{...} (capture-the-flag strings)שלבים 1-3 הם אחזור IMDSv2 בספר לימוד: בקשו אסימון סשן, ולאחר מכן צרף אותו כ- אסימון מטא-נתונים של X-aws-ec2 כותרת כדי למשוך את תפקיד ה-IAM של המופע ואת מפתחות הגישה הזמניים של תפקיד זה. הבחירה ליישם את IMDSv2 במקום את IMDSv1 הפשוט והלא מאומת. לקבל ראוי לציין - משמעות הדבר היא שהמטען פועל גם במקרים שתצורתם נקבעה לדרוש גישה למטא-נתונים מבוססת אסימון, שהיא ההקשחה המומלצת על ידי AWS. האישורים המוחזרים על ידי שלב 3 הם קצרי מועד. AccessKeyId/מפתח גישה סודי/אסימון משולשים המותאמים לתפקיד המופע; כל מה שתפקיד זה יכול לעשות, בעל המפתחות הללו יכול לעשות למשך חיי האישור.
שלבים 4-6 מרחיבים את הגישה. ה env dump לוכד כל דבר שתהליך הבנייה או זמן הריצה ירש - בפועל, זה המקום שבו נמצאים לרוב אסימוני רישום, מחרוזות חיבור למסד נתונים ומפתחות API. / אפליקציה קבצי walk קוראים עד חמישה עשר קבצי יישומים מחוץ למערכת צומת_מודולים, אשר יכול ליצור תצורת פני שטח, .v קבצים, או מקור. שלב 6 קורא רשימת סודות של מנהל סודות של aws בשלושה אזורים; האישורים שנאספו בשלבים 1-3 הם בדיוק מה שמאמת את הקריאות הללו, כך שקריאת ה-IMDS וספירת מנהל הסודות משתלבות יחד להסלמה אחת: תפקיד מופע → מלאי סודות מנוהלים. שלב 7 הוא קריצה למסגור של לכידת הדגל - כאשר HTB{…} כאשר נמצא דגל, הוא נשלח בפני עצמו, אחרת הגוש הגולמי שנאסף מפורק לארבעה חלקים ונשלח.
גרסאות מאוחרות יותר באשכול מעבירות את ההסלמה הלאה. במקום לעצור במלאי, הן מנתחות את תגובת ה-IMDS, מייצאות את המפתחות הזמניים כ... AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN משתני סביבה, אשר את הזהות עם קבל את זהות המתקשר שלך ב-aws sts, ולאחר מכן עוברים בלולאה על כל סוד שמוחזר על ידי רשימה של סודות קוראים מנהל סודות של aws על כל אחד מהם - אחזור התוכן הסודי, לא רק שמותיהם. אותן גרסאות קוראות גם קבצי בינארי של דגלים שהוחלפו על ידי תהליך (/דגל קריאה וחברים) ולנסות ריצת טעינה כנגד כל פרויקט חלודה שנמצא תחת / אפליקציה, ומרחיב את הקציר מעבר לאישורי ענן לכל מה שסביבת הבנייה חושפת.
גרסאות מאוחרות יותר אלו גם משתמשות ב-gates אגרסיביים יותר. בנוסף לשם המארח בן 12 הקסדצימליים ו- /app/node_modules בדיקות, המטען בודק את תצורת רישום החבילה הפעילה ואת נתיב ספריית העבודה ויוצא בשקט כאשר הם מצביעים על הקשר ניתוח או שיקוף ולא על יעד חי. האפקט המשולב הוא מטען שלא עושה שום דבר נצפה ברוב סביבות הבדיקה ומפעיל את האוסף המלא שלו רק כאשר הוא שופט את עצמו כנמצא על מארח קונטיינר אמיתי.
exfiltrationהנתונים שנאספו יוצאים מהמארח בשני ערוצים. ראשית, משואת אות POST למצב קבוע אתר אינטרנט אספן, הנושא את שם המארח, UID מספרי, ספריית עבודה ועד 120 KB של נתונים שנאספו. שנית, הנתונים מקופלים לתוך "מניפסט מודול" מזויף של YAML ו PUT ל /api/modules/ בשרת יעד:
ecto_module:
name: "<flag-or-chunk-0>"
version: "1.0.0"
power_level: "<chunk-1>"
ship_deck: "<chunk-2>"
cargo_hold: "<chunk-3>"שמות שדות המניפסט (רמת_הספק, סיפון_ספינה, תא מטען) הם קישוט - הנתונים הגנובים נמצאים בתוך ערכי המחרוזת, וזו הסיבה שמנטר רשת רואה מה שנראה כמו העלאת מניפסט שפירה של רישום חבילות ולא כקובץ נתונים ברור מאליו. ערוץ ה-beacon נושא עוד: ה- POST הגוף ל אתר אינטרנט כולל את שם המארח, ה-UID המספרי, ספריית העבודה ועד 120 KB של ה-blob שנאסף, כך שאפילו משואה מוצלחת אחת מספקת את הקלט המלא. אתר אינטרנט הוא שירות חינמי לבדיקת בקשות; השימוש בו כשירות אוסף בקשות פירושו שהמפעיל לעולם לא יצטרך להקים תשתית קליטה משלו עבור ערוץ זה, והבקשות המוקלטות נשארות בסל של השירות.
המניפסט PUT עוברת רשימת גיבוי שמתחילה בכמה 127.0.0.1/localhost פורטים ואז עובר לשלוש כתובות ציבוריות ב-`154.57.164.0/24` טווח, ועוצר בנקודת הקצה הראשונה שעונה עם סטטוס 2xx. הסדר של localhost-first עולה בקנה אחד עם התיאור העצמי של "verdaccio testing" (רישום מקומי בלולאה חוזרת), אך אפשרויות ה- fallback של IP public פירושן שהנתונים עוזבים את המארח בכל פעם שהלולאה חוזרת אינה מקשיבה - כלומר, בכל מכונה שאינה מערכת הבדיקה של המחבר עצמו.
ציר זמן
האשכול מציג צמיחה מצטברת של יכולות ולא ירידה בודדת. אנו מסדרים אותו לפי התנהגות שנצפתה, ולא לפי שעון קיר פרסומי:
| התמחות | חבילות / גרסאות | התנהגות |
|---|---|---|
| ריצה מוקדמת | coral-wraith 9999.0.x | מספרי גרסה מנופחים התואמים לניסיון בלבול תלויות; ספירה בזמן התקנה וקבצי exfil |
| זרע | coral-wraith 1.0.0 | postinstall אוספת קבצי id/env/flag; PUT יחיד אל 154[.]57[.]164[.]71:30782, סמן ECT-472839 |
| איטרציה מהירה | coral-wraith 1.0.1 → 6.0.0 | עשרות שחרורים תוך שעות; מטען מרוויח את isAppWorker() שער, משיכת אישורי IMDSv2, המלא get-secret-value ציר, בדיקת סביבת הרישום/נתיב וסמני כיור כפולים |
| שמות מקבילים | ecto-corsair-whisper-6f3b9 1.0.14–1.0.18 | אותו מטען משוער; webhook.site רשימת גיבוי של משואות ורשימת נקודות קצה מרובות |
| גרסאות | ecto-rust-read-f3a9c1 1.0.1–1.0.2 | מוסיף סמני כיור נוספים ECT-987654, ECT-654321, ECT-839201 |
| גרסאות | ecto-corsair-flag-x9m4 1.0.0, ecto-nightly-spirit 1.1.0 | אותו מטען מגודר, אותו C2 ומשואה |
המאפיין המגדיר את האשכול הוא קצב הפרסום שלו: במקום חבילה אחת וגרסה אחת, אותו שם מתפרסם שוב ושוב ברצף מהיר, כל מהדורה כוללת וריאציה קטנה של הגרסה הקודמת, לצד קומץ אחים בעלי שמות שונים הנושאים את אותו מטען. לחישה במשפחה, הקוד התפצל לשתי טביעות אצבע קרובות - קבוצה אחת מפעילה שתי גילוי קריטי, קבוצה נוספת שלוש (כיור קריאת קבצים נוסף) - אך שתיהן מתפרקות לאותו מטען; ההבדל הוא סחף קוד, לא מזלג התנהגותי. גרסאות של לחישה מעבר לטווח המנותח (עד לפחות 1.0.25 בזמן כתיבת שורות אלה) נצפו בשידור חי ברישום, וה- רוח אלמוגים השם המשיך לטפס בסולם הגרסאות שלו מעל אותו חלון.
אינדיקטורים של פשרה
כל האינדיקטורים שלהלן חולצו ממקור החבילה בדיסק. אינדיקטורי הרשת בוטלו.
רשת
| אינדיקטור | תפקיד |
|---|---|
hxxp://154[.]57[.]164[.]71:30782 | יעד C2 PUT (coral-wraith) |
hxxp://154[.]57[.]164[.]80:30543 | גיבוי PUT C2 (ecto-*) |
hxxp://154[.]57[.]164[.]82:31250 | גיבוי PUT C2 (ecto-*) |
hxxp://154[.]57[.]164[.]71:31289 | גיבוי PUT C2 (ecto-*) |
hxxps://webhook[.]site/602a4c72-7033-4e28-92ea-dc66e59206e5 | אספן משואות |
169[.]254[.]169[.]254/latest/... | קריאת אישורי IMDSv2 (צד היעד, מטא-נתונים של AWS) |
התנהגות/קובץ
| אינדיקטור | תפקיד |
|---|---|
"postinstall": "node postinstall.js" | התקנת וקטור |
ecto_module: YAML עם power_level / ship_deck / cargo_hold מפתחות | סכמת המניפסט של Exfil |
סמני כיור ECT-472839, ECT-987654, ECT-654321, ECT-839201 | קטע נתיב C2 /api/modules/<marker> |
isAppWorker() שער: מארח /^[0-9a-f]{12}$/, cwd מכיל /app/node_modules | תנאי הפעלה |
aws secretsmanager list-secrets יותר us-east-1, eu-west-1, eu-central-1 | ספירת סודות |
HTB{...} גירוד ביטוי רגולרי | קציר לכידת הדגל |
גיבובי קבצים (sha256, נתפס בזמן הניתוח)
| שלח | sha256 |
|---|---|
coral-wraith/postinstall.js | ce5ff035cfdfed1d0015446424b352c27b66bcb77e9fdb0a51e4245199146824 |
ecto-corsair-whisper-6f3b9 1.0.18/postinstall.js | b58432acba376aa6976f0490d9a1c04257ccdbc856d8390260c50322d63e31c3 |
ייחוס והתנהגות נצפית
חמשת שמות החבילות פורסמו תחת שני שמות חשבון npm, אך הם חולקים מספיק תשתית כדי להתייחס אליהם כאל אשכול אחד: אותו הדבר. מודול ecto סכימת המניפסט, אותה ECT-472839 סמן כיור ראשי, אותו הדבר אתר אינטרנט מזהה אספן, ונקודות קצה C2 באותו בלוק 154.57.164.0/24`. חבילת הזרעים (`coral-wraith`), פשוט יותר ולא מגודרת) ומשפחת האקטו-אקטו-גודרת נקראת אפוא כאיטרציות על ארגז כלים אחד ולא כמאמצים עצמאיים.
החבילות מתארות את עצמן, בגרסאות מאוחרות יותר, כ "מטען CTF לבדיקות שרשרת אספקה של Verdaccio." אנו מציגים את התווית הזו כעובדה נצפית ואיננו חוזרים עליה כממצא בנוגע למטרה. מה שהקוד עושה הוא חד משמעי ועצמאי באופן שבו הוא מתויג: הוא קורא אישורי תפקיד IAM משירות המטא-דאטה של המופע, מונה סודות מנוהלים על פני שלושה אזורי AWS, ומשדר את התוצאות לכתובת IP ציבורית ולאספן webhook של צד שלישי. רתמת בדיקה אמיתית של לולאה חוזרת בלבד לא תזדקק לרשימת הגיבוי של IP ציבורי, קריאות אישורי IMDS או קריאות של מנהל הסודות בין-אזוריים. מכיוון שהיציאה והגעה לאימות אמיתיים, הגרסאות המגודרות סווגו כזדוניות.
שער המיועד למכולות בלבד הוא התכונה הבולטת ביותר מבחינה תפעולית. זהו גם אמצעי התחמקות - שמירה על שתיקה במחשבים ניידים ובארגזי חול של ניתוח - וגם אמצעי מיקוד, המופעל רק במקומות בהם תפקיד IAM אמיתי וסודות חיים צפויים להימצא. אנליסטים המריצים חבילות אלו בארגז חול כללי לא יבחינו בדבר; ההתנהגות מתבטאת רק תחת שם מארח בסגנון Docker ונתיב התקנה בתוך המכולה.
השפעה, מגמות והנחיות למגינים
החשיפה כאן היא גילוי סודיות ופרטי ענן בתוך קונטיינרים של בנייה וזמן ריצה. אישור תפקיד IAM הנשלף מ-IMDS נושא את כל ההרשאות שיש לתפקיד זה; מנהל סודות: רשימת סודות (וכל המשך קבל ערך סודי) מרחיב זאת לסודות יישומים מאוחסנים. קבצי dump של משתני סביבה נושאים לעתים קרובות אסימוני רישום, כתובות URL של מסד נתונים ומפתחות API. בהקשר של CI או מכולה - בדיוק מה שהשער בוחר - התקנה טרנזיטיבית אחת של אחת מהחבילות הללו מספיקה כדי לדלוף את החומר הזה.
אקטופלזמה מתאימה לדפוס שאנו ממשיכים לראות: מטענים בזמן ההתקנה שמושכים למטא-דאטה בענן ולסודות מנוהלים במקום לקבצים מקומיים, ושמתקדמים לירי רק בסביבות בעלות ערך גבוה. שתי תצפיות הגנתיות מופיעות בהמשך.
- הצורה ניתנת לזיהויוו התקנה של npm/PyPI שגרף הקריאה שלו מגיע גם ל-API של סודות ענן (מנהל סודות של AWS, סודות גוגל קלאוד, כספת מפתחות של az) או כתובת ה-IMDS וכיור יציאה מרשת הוא דפוס צר בעל אותות גבוהים - הוא כמעט אף פעם לא מתרחש בסקריפט מחזור חיים לגיטימי. ניתוח זרימה סטטית יכול לסמן אותו מבלי להיות תלוי בדומיין או IP ספציפיים.
- הקשחת הסביבה מקהה אותו. אכיפת IMDSv2 עם מגבלת קפיצה של 1 מונעת מעומסי עבודה של קונטיינרים להגיע למטא-דאטה של מופעים; קביעת היקף תפקידי IAM להרשאות הנמוכות ביותר מגבילה את רדיוס הפיצוץ של כל אישור שדולף; והפעלת התקנות עם –להתעלם מתסריטים ב-CI מסיר את וקטור ה-install-hook לחלוטין עבור חבילות שאינן זקוקות לו.
עבור מגיני הגנה, הבדיקות המעשיות הן: התראה על חיבורים יוצאים ממכולות בנייה/CI לכתובות IP ציבוריות שאינן ברשימת היתרים במהלך התקנת npmחפשו גישת IMDS שמקורה בסקריפטים של מחזור חיי החבילה; והתייחסו לכל hook להתקנה שמופעל על ממשק שורת פקודה בענן כחשוד עד שיוכח אחרת.
שתי הערות נוספות ספציפיות לאשכול זה. ראשית, מכיוון שההפעלה מתבצעת בסביבות קונטיינריות, חבילה שנראית אדירה כאשר היא נבדקת בתחנת עבודה עדיין יכולה להיות פעילה בסביבת ייצור - הבדיקה צריכה לשחזר את שם המארח של הקונטיינר ואת תנאי הנתיב, או לקרוא את המקור ישירות, במקום להסתמך על "התקנתי אותו ושום דבר לא קרה". שנית, השימוש בשירות ציבורי לבדיקת בקשות כאספן משואות פירושו שחלק מהנתונים המסוננים עשויים להיות ניתנים לשחזור לצורך תגובה לאירועים: ארגון שמוצא אחת מהחבילות הללו בעץ התלות שלו יכול להסיק מה היה מכיל משואת מוצלחת מלוגיקת האיסוף של המטען, ועליו לסובב את כל אישורי תפקידי IAM, אסימוני רישום וסודות מנוהלים שהיו נגישים מסביבת הבנייה או זמן הריצה המושפעת. סבב אישורים, לא הסרת חבילה, היא התיקון המבצעי לאחר שההתקנה הסתיימה.







