החבילה עברה את בדיקת החתימה, ואז הוחלפה: החולשה ב-APT שנותנת root דרך מירור עוין
השרת מריץ apt upgrade בארבע בבוקר, כמו בכל שבוע. החבילות מגיעות ממירור ציבורי דרך HTTP רגיל, בלי הצפנה, וזאת לא רשלנות אלא החלטה ותיקה: המודל של דביאן לא סומך על התעבורה, הוא סומך על החתימה. כל חבילה נבדקת מול hash שמופיע בקובץ מטא-דאטה חתום, ואם ה-hash לא תואם, ההתקנה נעצרת. אז מי שמאזין לקו יכול לראות מה אתם מתקינים, ולא להחליף לכם את מה שמותקן.
ב-4 בנובמבר הנחת היסוד הזאת קיבלה סדק. דביאן פרסמה את DSA-6607-1, ולפיה החוקר Max Justicz מצא דרך לגרום ל-APT להחליף חבילה אחרי שהיא כבר עברה את האימות. בניסוח של ההודעה, "טיפול לא נכון בקבצים בעת חידוש הורדת HTTP מאפשר לתוקף מאמצע הדרך להחליף חבילה אחרי האימות, מה שמוביל להרצת קוד שרירותי כ-root". בהמשך נראה מה קורה שם בין ההורדה לבדיקה, למה צריך שלושה תנאים יחד, ולמה דווקא התקנת התיקון היא הרגע שדורש תשומת לב.
למה HTTP בכלל נחשב בסדר כאן
ההורדה עצמה חשופה, והשלמות מובטחת בשכבה אחרת. apt update מוריד קובץ Release שמרכז את ה-hash-ים של כל קובץ במחסן, והקובץ הזה חתום במפתח של המפיצה. מכאן כל חבילה שיורדת נמדדת מול המספר החתום, כך שגם מירור ציבורי שמישהו הקים בחינם לא יכול להזריק קוד: הוא מגיש בייטים, אבל לא מחזיק את המפתח שקובע אילו בייטים תקפים. זה אימות שלמות שמנותק לחלוטין מהתעבורה, וזאת הסיבה שאפשר להתקין חבילות על קו לא מוצפן בלי להזיע.
הבעיה היא שהבדיקה הזאת מתייחסת לקובץ, והקובץ הוא רשומה במערכת קבצים. בין הרגע שבו ה-hash אומת לרגע שבו dpkg פותח את החבילה ומריץ את סקריפטי ההתקנה שלה, יש מרווח. מי ששולט במה שיושב באותו נתיב על הדיסק בתוך המרווח הזה, שולט במה שמותקן.
אז מה בדיוק נשבר?
בהרצה אחת APT מוריד בדרך כלל יותר מחבילה אחת, וכל הורדה היא פריט בתור. לפי ההודעה של דביאן, "APT יכול למזג שתי הורדות חבילה שונות אחרי שהפניית HTTP נותנת להן אותו URI". תוקף שיושב על התעבורה יכול להחזיר הפניה שמכוונת שתי הורדות שונות לאותה כתובת, וברגע שזה קורה APT מתייחס אליהן כאל אותו פריט. מכיוון שהמנגנון שחוסך הורדות כפולות עובד דרך hardlink, כלומר שתי רשומות שמצביעות לאותם נתונים ממש על הדיסק, שתי ההורדות נפגשות בקובץ אחד.
כאן נכנס לתמונה החלק השני, הניסיון החוזר. מפתחי APT תיארו את זה בהודעה לרשימת הדיוור deity: תוקף מרוחק יכול "לעקוף אימות של חבילות שהורדו בעזרת הפניות ושגיאות שרת HTTP חולפות ורגילות שגורמות ל-APT לנסות להוריד שוב", ובדרך הזאת "לדרוס hardlink-ים קיימים עם מטען לבחירתו". השרת העוין לא צריך לשבור שום דבר קריפטוגרפי, אלא רק להחזיר שגיאה זמנית מהסוג שכל שרת מחזיר מדי פעם, ולחכות שהלקוח יחזור. ההודעה של אובונטו, USN-8996-1, מסכמת את אותו מנגנון בשורה אחת: APT טיפל לא נכון ב-hardlink-ים כשעיבד הפניות וניסה להוריד חבילות מחדש.
הסוף צפוי מכאן. ה-deb שנפתח בפועל הוא זה שהתוקף דחף אחרי הבדיקה, וחבילות deb מותקנות בהרשאות root, ולכן, כפי שכתבו מפתחי APT, "יש כאן פוטנציאל להרצת קוד מרחוק". החתימה לא נשברה. היא נבדקה על קובץ אחר.
שלושה תנאים, ואחד מהם הוא ברירת המחדל
התנאי הראשון הוא מקום על הנתיב: לפי עמוד ה-CVE של אובונטו, מדובר בתוקף שיכול לשנות את תעבורת ה-HTTP בין הלקוח לשרת המחסן, "כולל מירור או proxy זדוני". השני הוא שהלקוח עוקב אחרי הפניות. השלישי הוא ההגדרה Acquire::Retries, שקובעת אם הורדה שנכשלה מנוסה שוב, וערך ברירת המחדל שלה הוא 3 מדביאן 12 ומאובונטו 22.04 LTS ומעלה. גרסאות ישנות יותר, נכתב שם, אינן חשופות בתצורת ברירת מחדל.
מפתחי APT מוסיפים לנחמה הזאת הערה שמצמצמת אותה: ההגדרה היא ברירת מחדל רק מגרסה 2.3.2, ולכן "סקריפטים רבים של משתמשים כנראה השתמשו בה בגרסאות קודמות". מי שהוסיף Acquire::Retries לסקריפט אוטומציה כדי שהתקנות לא ייפלו על רשת רועשת, החזיר לעצמו את החשיפה בידיים. הפוסט שפרסמה אובונטו מוסיף שני חידודים שמצמצמים את התמונה לכיוון השני: החולשה יושבת במנגנון ה-HTTP של ספריית APT, ולכן היא נוגעת גם ל-apt-get, ל-aptitude ולכלים הגרפיים, אבל התקנות דרך snap לא נוגעות בה, וגם FTP לא.
על חומרת העניין יש פער קטן בין המקורות, וכדאי לקרוא אותו ישר. אובונטו נותנת לחולשה 7.7 בסולם CVSS 4.0 ומסמנת עדיפות High, בעיקר מפני שנדרשים תנאים סביבתיים ומעורבות של המשתמש שמתקין משהו. מפתחי APT, מצדם, מתארים את אותה חולשה כ-critical.
מי מתוקן, ומי עוד לא
בדביאן, הגרסה היציבה trixie תוקנה ב-3.0.4, ו-bookworm קיבלה 2.6.2 דרך עדכון אבטחה. התמונה לא שלמה: לפי טבלת המצב ב-Debian Security Tracker, בבדיקה הבוקר sid מתוקן ב-3.3.6, בעוד forky עוד מופיע כחשוף בגרסה 3.3.5. באובונטו, לפי USN-8996-1, 26.10 מתוקנת ב-3.3.3ubuntu0.1, 26.04 LTS ב-3.2.1ubuntu0.1, 24.04 LTS ב-2.8.4ubuntu0.1 ו-22.04 LTS ב-2.4.15ubuntu0.1. מי שמריץ 20.04 או 18.04 מקבל את התיקון רק דרך Ubuntu Pro, ו-16.04 ו-14.04 רק דרך תוספת ה-Legacy Support.
רשימת הסדרות המתוקנות שמפתחי APT פרסמו נפתחת ב-1.2.36 ומגיעה עד 3.2.1, אחת-עשרה סדרות במקביל. מי שתוהה כמה זמן ההתנהגות הזאת יושבת בקוד, זאת התשובה הזמינה: מספיק זמן כדי שהתיקון יצטרך לגעת בכל סדרה שעוד נתמכת.
פעם שנייה, אותו רכיב, אותו חוקר
ב-2019 Justicz פרסם חולשה אחרת באותו מקום, CVE-2019-3462. המנגנון היה שונה לגמרי: רכיב ההורדה לא ניקה שורות חדשות מכותרת ה-Location של הפניה, ולכן תוקף יכול היה להזריק שורות שתהליך האב קרא כתשובה פנימית מזויפת. "תהליך האב יסמוך על ה-hash-ים שחוזרים בתשובת 201 URI Done המוזרקת", כתב אז. במילים אחרות, ב-2019 התוקף הכתיב את ה-hash שמולו נמדדת החבילה, וב-2026 הוא מחליף את החבילה אחרי שהיא נמדדה. (וזאת, אגב, אותה שכבה ואותו חוקר, שבע שנים אחרי.)
המשותף לשתי הפעמים הוא לא הבאג אלא המקום שבו הוא נמצא: התפר שבין "הורדתי בייטים" ל"הבייטים האלה אומתו". המודל של דביאן חזק בדיוק בנקודה שהוא נבנה בשבילה, הקריפטוגרפיה, והוא תלוי לגמרי בקוד הפרוזאי שמסביבה, זה שמנהל קבצים זמניים, hardlink-ים והורדות שנפלו באמצע. שתי הפעמים נגמרו גם באותה המלצה. ב-2019 Justicz טען ש-HTTPS צריך להיות ברירת המחדל למחסנים, והפוסט של אובונטו מציע היום להעביר את מקורות החבילות ל-HTTPS כאמצעי זהירות נוסף, עם ההסתייגות שחלק ממירורי הקהילה לא תומכים בזה. HTTPS לא מחליף את החתימה. הוא פשוט מוציא את התוקף מהנתיב.
מה זה אומר בפועל
הנקודה המעשית החדה כאן היא שהתיקון מגיע דרך אותו ערוץ שהחולשה חיה בו, ולכן ההנחיה של אובונטו היא לא רק לעדכן אלא לעדכן כשהניסיון החוזר מכובה, עם sudo apt update && sudo apt upgrade -o Acquire::Retries=0. כשההגדרה על 0, לפי אותו פוסט, אין מה לנצל גם לפני שהתיקון מותקן. מי שמריץ unattended-upgrades יקבל את התיקון תוך 24 שעות, אבל לא בהכרח בדרך המוגנת הזאת.
אף אחת משלוש הודעות האבטחה לא מדווחת על ניצול בפועל. בדקו את גרסת ה-apt שעל המכונות שלכם מול הרשימה של המפיצה, ואם יש אצלכם סקריפטים שמגדירים Acquire::Retries על מכונות ישנות, הם המקומות שכדאי להתחיל בהם.
תגובות