ב-30 בספטמבר הועלה ל-VirusTotal ארכיון בשם evidencia.tgz, מברזיל. בתוכו שני קבצים. את הראשון, libkeyutils.so.1.5.2, רוב מנועי האנטי-וירוס זיהו מיד כ-Ebury, דלת אחורית ותיקה ל-OpenSSH. את השני, קובץ בשם cwpsrv.suspeito, אף מנוע לא סימן. אפס זיהויים.

חוקרי XLab של QiAnXin פרסמו ב-5 בנובמבר את הניתוח של הקובץ השני. הם קוראים לו cwpdoor, והוא לא נוזקה שיושבת לצד שרת הניהול. הוא שרת הניהול עצמו: הקובץ cwpsrv, שמגיש את ממשק Control Web Panel, אחרי שמישהו הוסיף קוד לקוד המקור וקימפל מחדש. בהמשך נראה איך בקשת HTTP רגילה למראה מפעילה אותו, למה היא לא נרשמת בשום לוג, ומה סקריפט ההתקנה עושה כדי שגם התאריכים על הקובץ ייראו תקינים.

מה זה cwpsrv, ולמה דווקא הוא היעד

Control Web Panel הוא פאנל ניהול שרתים חינמי בקוד פתוח ל-CentOS ולהפצות לינוקס אחרות. הוא נותן ממשק גרפי לניהול אתרים, DNS ודואר, כדי שמי שמתחזק את השרת לא יצטרך לעשות הכול בשורת פקודה מול SSH. הרכיב שמגיש את הפאנל הזה נקרא cwpsrv, ולפי XLab הוא לא שרת web גנרי אלא גרסה מותאמת של nginx ש-CWP מתחזקת בעצמה. זה הפרט שמסביר את כל מה שבא אחר כך: קוד פתוח, אפשר לקמפל, ומה שיוצא מהקומפילציה נראה בדיוק כמו הקובץ המקורי.

לפי נתוני פלטפורמת Hunter של QiAnXin שמצוטטים באותו דוח, יותר מ-340 אלף מופעים של CWP היו חשופים לאינטרנט בחודש שקדם לפרסום. המספר הזה מודד חשיפה, לא הדבקה: אלה השרתים שממשק הניהול שלהם נגיש מבחוץ, כלומר גודל קהל היעד.

הבקשה שמחכה בדלת, והחתימה שפותחת אותה

רוב הנוזקות שמנהלי שרתים מכירים עושות משהו שאפשר לראות ברשת. הן פותחות חיבור יוצא לשרת של התוקף, מבקשות פקודות, מדווחות בחזרה. cwpdoor לא עושה כלום מזה. הוא מגיש את הפאנל, מתנהג כמו cwpsrv אמיתי, ומחכה. הפקודה מגיעה אליו בתוך בקשת HTTP שהתוקף שולח כמו כל גולש אחר. לסוג הזה קוראים דלת אחורית פסיבית, ומבחינת ניטור תעבורה הוא כמעט בלתי נראה, כי אין תעבורה יוצאת חשודה לתפוס.

מה שמבדיל בקשה של התוקף מבקשה רגילה הוא שלושה headers שהוא מוסיף לה, וכולם נושאים את אותה תחילית. X-Ngx-Integrity-Time נושא חותמת זמן, X-Ngx-Integrity-Data את הפקודה המוצפנת, ו-X-Ngx-Integrity-Sig את החתימה. בצד השרת הטיפול בהם נעשה בשתי פונקציות שנוספו לקוד, ngx_http_integrity_init ו-ngx_http_integrity_check, והשנייה נקראת בכניסה לטיפול בכל בקשה נכנסת.

הבדיקה שהיא מבצעת הדוקה יותר ממה שנשמע. חותמת הזמן חייבת ליפול בחלון של 300 שניות לכל כיוון מהשעה על השרת. החתימה מאומתת מול מפתח RSA ציבורי שמוטבע בקובץ, והמחרוזת שעליה היא חתומה היא שרשור של שני השדות האחרים, חותמת הזמן ואחריה הפקודה המוצפנת. המשמעות המעשית: מי שיקליט בקשה אחת וינסה לשלוח אותה שוב מאוחר יותר ייכשל על חלון הזמן, ומי שינסה להחליף את הפקודה בתוך בקשה מוקלטת ייכשל על החתימה. רק מי שמחזיק את המפתח הפרטי יכול לייצר בקשה חדשה תקינה.

את המפתח הציבורי עצמו XLab חילצה בלי קושי מיוחד. הוא יושב בקובץ מעורפל בעזרת XOR מול הבית 0x55, וכל מה שנדרש היה להפעיל את אותה פעולה פעם נוספת. הפקודה שב-X-Ngx-Integrity-Data מקודדת ב-Base64, ואחרי הפענוח 16 הבתים הראשונים הם ה-IV והשאר הוא טקסט מוצפן ב-AES-256-CBC. את מפתח ההצפנה גוזרים מתקציר SHA-256 של מחרוזת המפתח הציבורי, כך שהחתימה וההצפנה תלויות באותו סוד אחד.

כשהאימות עובר, תהליך ה-master מפענח את הפקודה ומעביר אותה לתהליך helper נפרד דרך Unix domain socket בשם ngx_http_integrity_helper, והוא זה שמריץ אותה. הפלט חוזר מוצפן ב-AES בתוך תשובת HTTP 200 עם Content-Type של application/octet-stream, כלומר נראה כמו הורדת קובץ בינארי רגילה. ואם אחת מהבדיקות נכשלת, הבקשה ממשיכה במסלול הרגיל ומוגשת כמו כל בקשה אחרת. אין שגיאה, אין התנהגות חריגה, אין שום רמז שהרכיב מכיל משהו.

למה הבקשה לא מופיעה בלוג

ברגע שבקשה מפעילה את הדלת האחורית, cwpdoor לא מסתפק בהרצת הפקודה. nginx, וגם cwpsrv שבנוי עליו, מחזיק לכל בקשה מערך של הקשרים, אחד לכל מודול שמטפל בה, וביניהם המודול שאחראי לכתוב את שורת הלוג. cwpdoor מעתיק את המערך הזה עבור הבקשה הנוכחית ומחליף בו את המצביע של מודול הלוג. התוצאה היא שהבקשה הספציפית הזאת לא נרשמת, בעוד שכל יתר התעבורה על אותו שרת ממשיכה להיכתב ללוג כרגיל. אין חור בקובץ, אין טווח שעות חסר, רק בקשות שמעולם לא היו שם.

את האינדקס של מודול הלוג הקוד מאתר ושומר מראש, בהרצה של ngx_http_integrity_init בסוף שלב פענוח הקונפיגורציה. אותה פונקציה גם מנקה תהליכי helper שנשארו מריצה קודמת, יוצרת את ה-socket ומרימה את תהליך ה-helper. כל ההכנה הזאת קורית פעם אחת, בעלייה של השרת.

מי שמנסה לשחזר אירוע אחרי פריצה עובד בדרך כלל מהלוג של שרת ה-web אחורה. בדיוק שם אין כלום.

ההתקנה שמחזירה את התאריכים אחורה

הקובץ שהתחיל את החקירה הוא דווקא הפשוט מבין השניים: סקריפט bash בשם cwpsrv_update.sh, שמערכת המודיעין של XLab תפסה ב-29 ביוני. הוא מחופש לעדכון גרסה, ותומך בשתי גרסאות בלבד, 1.15.10 ו-1.24.0, שלהן מותאמים שני ה-binary-ים שהחוקרים מצאו. הוא דורש הרשאות root כדי לרוץ, ומכאן שהוא לא כלי חדירה: בשלב שבו מישהו מפעיל אותו, התוקף כבר שולט בשרת.

מה שהסקריפט עושה בפועל נראה כמו עבודה של מנהל מערכת מסודר. הוא בודק איזו גרסה מותקנת, מוריד את ה-binary המתאים מהשרת 136.0.141.71, מאמת שהגרסה אכן תואמת, מגבה את הקובץ המקורי, עוצר את ה-service, דורס את /usr/local/cwpsrv/bin/cwpsrv, מרים את ה-service בחזרה ובודק שתהליכי master, worker ו-helper אכן עלו. אם הבדיקה הזאת נכשלת הוא מחזיר את המצב לאחור בעצמו, ואם היא מצליחה הוא מוחק את הקבצים הזמניים ואת הגיבוי.

ואז מגיע החלק שמעניין את הצד המגן. בלי קשר לשאלה אם ההחלפה הצליחה או נכשלה, הסקריפט מחזיר ל-binary החדש את זמני ה-atime וה-mtime המקוריים, ומחזיר גם את חותמת הזמן של התיקייה שמכילה אותו. אחרי זה הוא מריץ history -d כדי לנקות את היסטוריית ה-session הנוכחי, ומדווח את תוצאת ההתקנה בחזרה לאותה כתובת. מי שיסתכל על הקובץ עם ls או stat יראה קובץ שלא נגעו בו מאז ההתקנה המקורית, ו-XLab אומרת את זה במפורש: שתי הפקודות האלה לא מספיקות כאן.

בהמלצות הטיפול שבסוף הדוח יש פרט אחד ששווה שימת לב. הן מורות לשחזר את ה-binary התקין מהגיבוי שהתוקף עצמו יצר, קובץ בשם cwpsrv.prev ועוד חותמת תאריך ושעה, אבל לפי ניתוח הסקריפט באותו דוח הגיבוי הזה נמחק בדיוק כשההחלפה מצליחה. כלומר סביר למצוא אותו רק בשרת שבו משהו בדרך נכשל, וכדאי לא להסתמך עליו כשמתכננים את הטיפול מראש.

אז איך הם משיגים root מההתחלה?

זאת השאלה שהדוח משאיר פתוחה. "אנחנו עוד לא יודעים באיזו חולשה cwpdoor משתמש כדי להתפשט", כותבים החוקרים, וזאת גם הסיבה שהם בחרו לפרסם: מכיוון שספקי האבטחה הגדולים עוד לא מזהים את הקובץ, הם העדיפו לשחרר את המודיעין שיש להם ולבקש מהתעשייה מידע על ערוצי ההפצה.

מה שכן ידוע הוא שהפאנל הזה יעד מוכר. בקטלוג KEV של CISA, רשימת החולשות שהסוכנות מאשרת שנוצלו בפועל, יושבות שתי חולשות הזרקת פקודות מערכת ב-CWP: CVE-2022-44877, שנוספה לרשימה בינואר 2023, ו-CVE-2025-48703, שנוספה בנובמבר 2025. שתיהן מאפשרות לתוקף להריץ פקודות על השרת. זה לא אומר שאחת מהן היא הדרך שבה cwpdoor נכנס, והחוקרים לא טוענים את זה. זה כן אומר שבשרת CWP שלא עודכן, הדרישה להרשאות root שבסקריפט ההתקנה היא חסם נמוך יותר ממה שהיא נשמעת.

הקובץ השני באותו ארכיון מברזיל מוסיף שכבה. Ebury, לפי המחקר של ESET ממאי 2024, היא דלת אחורית ל-OpenSSH וגם כלי לחילוץ פרטי אימות, ומאז 2009 היא הודבקה לכ-400 אלף שרתים, כשבסוף 2023 יותר מ-100 אלף מהם עוד היו נגועים. בין הקורבנות יש הרבה ספקי אירוח, וההמחשה הטובה ביותר לכך באותו מחקר היא ניסוי: החוקרים שכרו שרת וירטואלי אצל ספק נגוע, ותוך שבעה ימים Ebury הותקנה גם עליו. XLab לא קובעת שיש קשר בין שתי הנוזקות, ומעלה גם אפשרות אחרת לגמרי, ששני תוקפים שונים הגיעו לאותו שרת.

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

מי שמריץ Control Web Panel יכול לסגור את שלוש הבדיקות שבדוח בפחות מדקה, בלי להתקין כלום. הפקודה ss -axp עם סינון למילה integrity תחשוף את ה-socket של ה-helper אם הוא קיים. הפקודה ps aux עם סינון ל-cwpsrv תראה תהליכי helper חריגים שתלויים בו. ו-strings על /usr/local/cwpsrv/bin/cwpsrv, עם סינון למחרוזת X-Ngx-Integrity, יגלה את שמות ה-headers בתוך ה-binary עצמו. כל אחת מהשלוש מספיקה כדי להרים דגל.

שרת שבו אחת הבדיקות חוזרת חיובית צריך להיחשב פרוץ במלוא מובן המילה, לא רק נגוע. התוקף הריץ עליו פקודות שרירותיות בלי להשאיר שורת לוג, ואין דרך לדעת מתי זה התחיל או מה בדיוק נעשה שם. ההנחיה של XLab היא לעלות לגרסה העדכנית, לוודא שבדיקת ה-socket חוזרת ריקה, ולהחליף את כל פרטי האימות של הפאנל. בשרת שמשרת לקוחות אירוח, גם את אלה שלהם.