פעולת deserialization לא בטוחה

קטגוריה ב-OWASP: MASVS-CODE: איכות הקוד

סקירה כללית

כשמאחסנים או מעבירים כמויות גדולות של נתוני אובייקט Java, בדרך כלל יעיל יותר לבצע קודם סריאליזציה של הנתונים. לאחר מכן, הנתונים יעברו תהליך של ביטול סריאליזציה (deserialization) על ידי האפליקציה, הפעילות או הספק המקבלים, שבסופו של דבר מטפלים בנתונים. בנסיבות רגילות, הנתונים עוברים סריאליזציה ואז דה-סריאליזציה ללא התערבות של משתמש. עם זאת, גורם זדוני יכול לנצל לרעה את יחסי האמון בין תהליך הדה-סריאליזציה לבין האובייקט המיועד שלו, למשל, ליירט אובייקטים בסריאליזציה ולשנות אותם. כך הגורם הזדוני יוכל לבצע התקפות כמו מניעת שירות (DoS), הסלמת הרשאות והפעלת קוד מרחוק (RCE).

הכיתה Serializable היא שיטה נפוצה לניהול שרשור, אבל ל-Android יש כיתה משלו לטיפול בשרשור שנקראת Parcel. באמצעות המחלקה Parcel, אפשר לבצע סריאליזציה של נתוני האובייקטים לנתוני סטרימינג של בייטים ולדחוס אותם ב-Parcel באמצעות הממשק של Parcelable. כך אפשר להעביר או לאחסן את ה-Parcel בצורה יעילה יותר.

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

יש שלושה כיוונים עיקריים לניצול לרעה של דה-סריאליזציה ב-Android:

  • ניצול ההנחה השגויה של המפתח שהפעלת ביטול שרשור (deserialization) של אובייקטים שמגיעים מסוג של כיתה בהתאמה אישית היא בטוחה. בפועל, אפשר להחליף כל אובייקט שנוצר על ידי כל מחלקה בתוכן זדוני, שבתרחיש הגרוע ביותר עלול לשבש את אותה מחלקה של אותה אפליקציה או את המחלקה שלה. ההפרעה הזו גורמת להחדרה של ערכים מסוכנים, שבהתאם למטרה הכיתתית עלולים להוביל, למשל, לזליגת נתונים או להשתלטות על החשבון.
  • ניצול שיטות של פעולת deserialization שנחשבות לא בטוחות משלב התכנון (לדוגמה CVE-2023-35669, שגיאה מקומית בהסלמת הרשאות (privilege escalation) שאפשרה החדרה של קוד JavaScript שרירותי דרך וקטור deserialization של קישור עומק)
  • ניצול פגמים בלוגיקה של האפליקציה (למשל CVE-2023-20963, כשל מקומי בהסלמת הרשאות (privilege escalation) שאפשר לאפליקציה להוריד ולהפעיל קוד בסביבה בעלת הרשאות, באמצעות פגם בלוגיקת החבילות של WorkSource ב-Android.

השפעה

כל אפליקציה שמבצעת פעולת סריאליזציה של נתונים סידוריים לא מהימנים או זדוניים, עלולה להיות חשופה להרצת קוד מרחוק או להתקפות מסוג מניעת שירות (DoS).

סיכון: דה-סריאליזציה של קלט לא מהימן

תוקף יכול לנצל את היעדר האימות של חבילות בתוך הלוגיקה של האפליקציה כדי להחדיר אובייקטים שרירותיים, שיכולים לאלץ את האפליקציה להריץ קוד זדוני לאחר דה-סריאליזציה, וכתוצאה מכך לגרום להתקפת מניעת שירות (DoS), להסלמת הרשאות (privilege escalation) ולביצוע קוד מרחוק (RCE).

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

מיטיגציות

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

פתרון אפשרי הוא להטמיע את התבנית של צפייה קדימה בjava.io.ObjectInputStream ספרייה. שינוי הקוד שאחראי לביטול הסריאליזציה מאפשר לוודא שרק קבוצה של כיתות שצוינה במפורש תבוטל הסריאליזציה שלה בתוך ה-Intent.

החל מ-Android 13 (רמת API ‏33), כמה שיטות עודכנו בכיתה Intent, והן נחשבות לחלופות בטוחות יותר לשיטות ישנות יותר לטיפול בחבילות, שכבר הוצאו משימוש. השיטות החדשות האלה למניעת שגיאות מסוג (type-safe), כמו getParcelableExtra(java.lang.String, java.lang.Class) ו-getParcelableArrayListExtra(java.lang.String, java.lang.Class), מבצעות בדיקות של סוגי הנתונים כדי לזהות נקודות חולשה של אי-התאמה שעלולות לגרום לקריסה של אפליקציות, ויכול להיות שאפשר לנצל אותן כדי לבצע התקפות של הסלמת הרשאות (privilege escalation), כמו CVE-2021-0928.

הדוגמה הבאה ממחישה איך אפשר להטמיע גרסה בטוחה של הכיתה Parcel:

נניח שהכיתה UserParcelable מיישמת את Parcelable ויוצרת מכונה של נתוני משתמש, שנכתבת לאחר מכן ב-Parcel. לאחר מכן אפשר להשתמש בשיטה הבאה של readParcelable כדי לקרוא את החבילה בסריאליזציה:

Kotlin

val parcel = Parcel.obtain()
val userParcelable = parcel.readParcelable(UserParcelable::class.java.classLoader)

Java

Parcel parcel = Parcel.obtain();
UserParcelable userParcelable = parcel.readParcelable(UserParcelable.class, UserParcelable.CREATOR);

שימו לב בדוגמת ה-Java שמעל השימוש ב-UserParcelable.CREATOR ב-method. הפרמטר הנדרש הזה מאפשר לשיטה readParcelable לדעת איזה סוג לצפות לו, והוא מספק ביצועים טובים יותר מהגרסה הקודמת של השיטה readParcelable, שכבר לא מומלצת לשימוש.

סיכונים ספציפיים

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

סיכון: דה-סריאליזציה לא רצויה של אובייקטים

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

מיטיגציות

אם מחלקה עוברת בירושה את הממשק Serializable, בהתאם להנחיות של OWASP, צריך להטמיע את השיטה readObject באופן הבא כדי למנוע דה-סריאליזציה של קבוצת אובייקטים במחלקה:

Kotlin

@Throws(IOException::class)
private final fun readObject(in: ObjectInputStream) {
    throw IOException("Cannot be deserialized")
}

Java

private final void readObject(ObjectInputStream in) throws java.io.IOException {
    throw new java.io.IOException("Cannot be deserialized");
}

משאבים