הבקשה לקובץ תמונה נעצרה ב-load balancer ומעולם לא הגיעה לשרת: דלת אחורית שקומפלה לתוך HAProxy של הקורבן
בקשת HTTP אחת לקובץ בשם favorite_list_2x_m500_ico.jpg מגיעה אל מאזן העומסים של הארגון, ונעצרת שם. אף שרת אפליקציה מאחוריו לא רואה אותה, שום שורה לא נכתבת ביומן, ומוני החיבורים של השרת נשארים בדיוק כפי שהיו רגע קודם. הבקשה הזאת אינה בקשה לתמונה. היא פקודה, והרכיב שמפרסר אותה הוא מאזן העומסים עצמו.
חוקרי Rapid7 Labs פרסמו ב-4 בספטמבר ניתוח של ערכת כלים ללינוקס שפעלה מול ארגונים בדרום קוריאה בתחומי הרכב והתקשורת, ובמרכזה רכיב שהתוקפים עצמם קראו לו ted backdoor. מה שמייחד אותו הוא לא רשימת היכולות, שהיא סטנדרטית למדי, אלא המקום שבו הוא יושב: הקוד שלו קומפל יחד עם קוד המקור של HAProxy בגרסה 2.8.12, אותה גרסה שהקורבנות ממילא הריצו. בהמשך נראה איך הפקודות מגיעות אליו בלי להשאיר עקבות, לפי אילו כללים הוא בוחר לאיזה גולש להזריק קוד, ומה עוד ישב על אותם שרתים במקביל.
למה דווקא הרכיב הזה?
מאזן עומסים הוא השרת שעומד בקצה הרשת: הוא מקבל את כל בקשות ה-HTTP מהאינטרנט, מפענח את ה-TLS, ומחלק את הבקשות בין שרתי האפליקציה שמאחוריו. כלומר כל תעבורת המשתמשים של הארגון עוברת דרכו, ובנקודה שבה היא עוברת היא כבר מפוענחת. HAProxy הוא אחד המימושים הנפוצים של התפקיד הזה, והוא מציע ממשק מתועד לכתיבת תוספים שמתערבים בעיבוד התעבורה.
התוקפים לא כתבו מודול חיצוני שנטען לתהליך. הם לקחו את קוד המקור, הוסיפו לו תוסף בשם ted_plugin, ובנו מחדש בינארי של 18 מגה שנראה כמו HAProxy תקין לכל דבר. Rapid7 מציינים שהם זיהו את השם מתוך מחרוזות דיבאג שנשארו בבינארי. התוסף רושם פילטר משלו שמתחבר ישירות למפרסר ה-HTTP הפנימי, ומשם הוא קורא את המבנה שבו HAProxy מחזיק את הבקשה אחרי הפענוח והניתוח. איזון העומסים האמיתי, בינתיים, ממשיך לעבוד כרגיל.
אצל שני הקורבנות שנבדקו היו חשופים לאינטרנט הפורטים 80, 443 ו-25. על 443 ישב פורטל התחברות של Groupware, תוכנת ניהול עבודה שנפוצה מאוד בארגונים קוריאניים, ועל 25 ישב שרת דואר. Rapid7 מדגישים שהראיות שנאספו לא הספיקו כדי לקבוע איך הושגה הגישה הראשונית: שני המשטחים האלה סבירים, ההשערה שבתרשים המתקפה שלהם מניחה ניצול של חולשה בפורטל ה-Groupware, אבל נקודת הכניסה המדויקת וכל CVE שקשור אליה נשארות לא מאומתות.
איך פקודה נכנסת בלי להשאיר עקבות
כשבקשה נכנסת, הפילטר משווה את הנתיב שלה למחרוזת קבועה אחת. אם היא תואמת ל-favorite_list_2x_m500_ico.jpg, הרכיב עובר למצב שליטה ובקרה, וכאן מתחיל החלק המעניין.
כל שרת כזה סופר כמה חיבורים ובקשות עברו דרכו וכמה בייטים נכנסו ויצאו, והמונים האלה הם מה שמנהל מערכת מסתכל עליו כשהוא בודק אם משהו חריג קרה. הדבר הראשון שהתוסף עושה הוא להחסיר את עצמו מהם: הוא ניגש למבני הנתונים הפנימיים של HAProxy דרך היסטים קבועים שמתאימים בדיוק לגרסה 2.8.12, ומוריד באחד את מוני החיבורים הפעילים לכל backend ואת המונה הגלובלי, ואז את המונים המצטברים של חיבורים, בקשות ובייטים. גם מוני השיא מטופלים. אחרי הפעולה הזאת המספרים נראים כאילו הבקשה הזאת מעולם לא הגיעה.
אחר כך התוסף קורא כותרת בינארית קבועה של 14 בתים כדי לדעת את אורך המטען, ופותח צינור בשם (named pipe) תחת /tmp, ששמו נגזר ממזהה החיבור של HAProxy. גוף הבקשה נכתב לצינור, ובקצה השני רץ מנגנון שמפענח את הפקודה. חמש הפקודות הנתמכות פשוטות: איתות חזרה עם גרסת הבינארי, העלאת קובץ לשרת, הורדת קובץ ממנו, הרצת קוד באמצעות popen, והחלפת קובץ ההגדרות של התוסף.
וזה בדיוק הרגע שבו הבקשה נמחקת. אחרי שגוף הבקשה הועבר לצינור, התוסף מאפס את השדות שאומרים ל-HAProxy כמה נתונים נשאר להעביר הלאה, ולכן הפקודה של התוקף לעולם לא מגיעה לשרת שמאחור, ולא מגיעה ליומן שלו. התשובה חוזרת בכיוון ההפוך באותה שיטה: כל אחד מהמטפלים כותב לצינור כותרת "HTTP/1.0 200 OK" רגילה לפני התוכן, והתוצאה נשלחת החוצה ישירות על הסוקט. על החוט זה נראה כמו תשובת HTTP רגילה לגמרי, למרות שהיא מעולם לא עברה במסלול התשובות של HAProxy. הפלט עצמו מוצפן בצופן XOR מתגלגל שבו כל בלוק טקסט משמש כמפתח להצפנת הבלוק הבא.
מי נבחר לקבל את המטען
אם הנתיב לא תואם, התוסף עובר למצב האזנה. הוא מריץ ביטויים רגולריים על התעבורה הנכנסת כדי לדוג בקשות בעלות ערך, למשל משתמש שמגיע ל-endpoint של התחברות או לממשק ניהול, ומתעד מהבקשה את כתובת המקור, את ה-Host, את ה-Referer ואת ה-User-Agent.
וכאן נכנס לתמונה החלק שהופך את השרת לכלי תקיפה אקטיבי. כשההגדרה דורשת זאת, בקשה שנתפסה צריכה לעבור ארבעה מסננים לפני שמזריקים לה קוד: חייב להיות User-Agent שתואם ביטוי רגולרי מוגדר, מתוכו נבחר אחד משני נתיבי מטען לפי ארכיטקטורה, כלל ההזרקה נבחר לפי התאמה כפולה של הכתובת ושל ה-Referer, ולבסוף נבדקת כתובת ה-IP מול רשימת היתר או רשימת חסימה, פעם ברמת תת-רשת /24 ופעם בהתאמה מדויקת. התוצאה היא שהתוקף יכול לתפוס גולש שהגיע לדף מסוים דווקא מאתר מפנה מסוים, במקום להזריק לכל מי שנכנס לכתובת.
יש גם דרך לעקוף את כל זה. התוסף מפרסר את כותרת Accept-Language ומחלץ ממנה שדות שהתוקף שולט בהם, וביניהם מפתח באורך 64 בתים. אם המפתח נמצא ברשימת המפתחות המורשים, הוא גובר על סינון ה-IP לגמרי ומאפשר למפעיל להגיע לדף המוזרק מכל מקום. בפועל זו דלת שירות למי שמחזיק את המפתח הנכון.
ההזרקה עצמה קורית בדרך החוצה, ומשתמשת שוב בממשקי HAProxy עצמו. הרכיב מוודא שסוג התוכן ושורת הסטטוס מתאימים לכלל שהותאם, מעדכן את Content-Type ואת אורך הגוף, מוחק את הכותרת Accept-Ranges כדי שהדפדפן לא יוכל לבקש טווח בתים ולגלות את הפער בגודל, וכופה סטטוס 200 גם אם התשובה המקורית הייתה משהו אחר. את גוף התשובה הוא מחליף בפועל דרך קריאות עריכת הגוף המובנות. מטען שגדול מהתשובה המקורית לא נדחס בכוח אלא מתווסף בסבבים, בעזרת המתזמן של HAProxy, עד שכולו נמסר.
מה עוד ישב על אותם שרתים
ted לא פעל לבד. לצדו נמצאו stager, נוזקת גישה מרחוק שהחוקרים קראו לה curlRAT, וקילוגר שהושתל בתוך sshd ושמר סיסמאות של משתמשים לגיטימיים לקובץ יומן מוצפן תחת /var/lib/sshd.
ה-stager בודק שיש לו הרשאות root, מזהה את ההפצה, גרסת הליבה והארכיטקטורה כדי לבחור את המטען הנכון, ואז מפענח בזיכרון בינארי crond מתוקן ודורס איתו את הדימון הלגיטימי. אחרי ההתקנה הוא מיישר את חותמת הזמן של הקובץ החדש לזו של /usr/bin/ssh, ומוחק ממחרוזות מפתח כמו tmp, wget ו-cron משורת ההיסטוריה של root ומיומני המערכת, בהם messages, audit.log, secure ו-auth.log. הגרסאות הנתמכות של crond הן CentOS 7.7 עד 7.9 ואובונטו 22.04.
curlRAT עצמו שקט מאוד. כברירת מחדל הוא מושך הגדרות מהשרת אחת ל-43,200 שניות, כלומר פעם ב-12 שעות, והמפעיל יכול להדליק דגל שמקצר את המרווח ל-30 שניות כשהוא רוצה לעבוד. מזהה הקורבן נבנה משם המארח, כתובת ה-IPv4 ומזהה החומרה, עובר MD5, ונשלח ככותרת בכל פנייה. שישה מצבי הפעלה נתמכים, ביניהם הרצת פקודות, הורדת מטען, מעטפת הפוכה ומעטפת PTY אינטראקטיבית מלאה. תהליכון נפרד משמש כשומר: לפני שהוא מתחיל לעבוד הוא בודק שקיים הקובץ /usr/lib/libvirtlog.so.0, ואם אין סימן לכך שהמכונה מווירטואלית הוא ישן שש דקות ונסגר. אחת לשעה הוא קורא את קובץ ה-pid של HAProxy, בודק את מצב התהליך, ומדווח לשרת אם הוא עלה, נפל, אותחל מחדש או נטען מחדש.
על מי מצביעות הראיות
Rapid7 מייחסים את הערכה לקבוצות תקיפה צפון קוריאניות ברמת ביטחון בינונית, ומפרטים על מה היא נשענת: המיקוד בתוכנה אזורית, שמות דומיינים שמחקים את הדומיין שדרכו נמסר תוכן סטטי של Naver הקוריאנית, שימוש בצפני XOR והחלפה פשוטים, ומודל ההגשה דרך אתר שנפרץ. רשימת שרתי השליטה שמוטמעת בקוד מקושרת ל-APT37 לפי ThreatFox ולפי maltrail.
החוקרים מציינים חפיפה בלוח הזמנים ובשיטת ההגשה עם Operation SyncHole, קמפיין של Lazarus שתועד על ידי קספרסקי ושלפי Rapid7 רץ מנובמבר 2024 עד פברואר 2025 מול ארגונים בדרום קוריאה. שתי הקבוצות הן אשכולות נפרדים: לפי הערכת Mandiant ש-Rapid7 מצטטים, APT37 פועלת תחת משרד ביטחון המדינה ו-Lazarus תחת לשכת הסיור הכללית. "נדרשות ראיות נוספות כדי לגבש הערכה נחרצת יותר", כתבו החוקרים. גם הקישור לדלת אחורית ל-nginx, שעולה משמות שגרות שנשארו בקוד, נשאר אצלם ברמת השערה בלבד.
מה זה אומר בפועל
המסקנה המעשית שלהם ישירה: על המגנים להתייחס לכל רכיב קצה שמנהל תעבורת משתמשים, SSL או מודולים בזמן ריצה באותם סטנדרטים שבהם הם מתייחסים לשרתי האפליקציה המרכזיים. אם אתם מריצים HAProxy, nginx או כל רכיב דומה בקצה, שווה לוודא שהבינארי שרץ אצלכם באמת זהה לזה שהיצרן מפיץ, ושאתם יודעים מתי הוא הוחלף לאחרונה. הבדיקה הזאת פשוטה יחסית והיא בדיוק מה שהמתקפה הזאת מנסה לחמוק ממנו.
הנקודה השנייה חשובה לא פחות: היומנים והמונים של אותו רכיב אינם עדות קבילה על עצמו. במקרה הזה שני מקורות המידע שמנהל שרת היה בודק ראשונים, יומן הגישה וסטטיסטיקת החיבורים, נוקו על ידי הקוד שהם היו אמורים לחשוף. מה שכן נשאר זמין הוא מה שקורה מסביב, כלומר תיעוד תעבורה ברמת הרשת מצומת אחר וניטור התנהגות התהליך עצמו.
תגובות